"""Deterministic analysis blocks (segmentation, alignment, numerals) for the prompt: the full scaffold and the optional light hint.""" from __future__ import annotations from typing import Dict, List, Optional, Sequence, Tuple from .align import align as build_align from .numerals import extract_attested, induce from .preprocess import Puzzle, strip_punct, tokenize from .segment import Segmenter MAX_SEG_LINES = 40 MAX_ALIGN_LINES = 30 MAX_VOCAB = 60 def analysis_blocks(puzzle: Puzzle) -> Tuple[str, str]: """(segmentation block, alignment block) for the prompt. Also used by the CEGIS proposer (synth.py).""" amap = build_align(puzzle.pairs) align_lines = [] for tok, cands in sorted(amap.items()): top = [f"{t} ({s:.1f})" for t, s in cands[:2] if s > 0.2] if top: align_lines.append(f" {tok} ~ {', '.join(top)}") vocab, groups = [], {} for p in puzzle.pairs: for t in tokenize(p.src): t = strip_punct(t).casefold() if t and t not in vocab: vocab.append(t) vocab = vocab[:MAX_VOCAB] for tok, cands in amap.items(): if cands: groups.setdefault(cands[0][0], set()).add(tok) seg = Segmenter().fit(vocab, share_groups=[g for g in groups.values() if len(g) > 1]) seg_lines = [] for w in vocab: parts = seg.segment(w) if len(parts) > 1: seg_lines.append(f" {w} = {'-'.join(parts)}") return ("\n".join(seg_lines[:MAX_SEG_LINES]) or " (none found)", "\n".join(align_lines[:MAX_ALIGN_LINES]) or " (none found)") def light_hint(puzzle: Puzzle, max_lines: int = 10) -> str: """A minimal, optional hint for the lean prompt: a few morpheme segmentation guesses, framed as fallible. Off by default; enabled via a toggle.""" try: seg_block, _align = analysis_blocks(puzzle) except Exception: return "" lines = [l for l in seg_block.splitlines() if l.strip() and "none found" not in l] if not lines: return "" body = "\n".join(lines[:max_lines]) return ("Optional hint (an automatic guess at word parts; it may be wrong, " "so rely on the data itself):\n" + body) def numeral_block(puzzle: Puzzle) -> str: """Induced numeral-system values, when the CSP solved and round-trip verified them — the strongest kind of hint we can give.""" if puzzle.task_type not in ("text_to_num", "num_to_text"): return "" attested = extract_attested(puzzle.pairs) system = induce(attested) if attested else None if system is None: return "" vals = ", ".join(f"{t}={v}" for t, v in sorted(system.values.items(), key=lambda kv: kv[1])) return (f"Numeral analysis (verified against every attested example):\n {vals}\n" f" combination rule: a smaller value directly before a larger one multiplies it; " f"otherwise values add.") def candidate_block(items_answers: Sequence[Tuple[str, Optional[str], float, str]]) -> str: """Symbolic candidate answers per item: (item label, answer, confidence, method). Only candidates with real evidence are shown — a low-confidence echo would anchor the model on garbage.""" lines = [] for label, ans, conf, method in items_answers: if ans and conf >= 0.4: lines.append(f" item {label}: '{ans}' (source: {method}, fit {conf:.2f})") if not lines: return "" return ("Candidate answers from mechanical analysis (adopt if consistent with " "the data, correct if not):\n" + "\n".join(lines)) def build_scaffold(puzzle: Puzzle, answers: Optional[Sequence[Optional[str]]] = None, confs: Optional[Sequence[float]] = None, methods: Optional[Sequence[str]] = None) -> str: """Full scaffold block for one puzzle's prompt.""" seg_block, align_block = analysis_blocks(puzzle) parts = [ "## Mechanical analysis (computed from the data above; may contain errors — " "the attested data always wins)", f"Morpheme segmentation hypotheses:\n{seg_block}", f"Word alignment hypotheses (task-language token ~ likely meaning):\n{align_block}", ] nb = numeral_block(puzzle) if nb: parts.append(nb) if answers is not None and confs is not None: labels = [it.number or str(i + 1) for i, it in enumerate(puzzle.items)] meths = list(methods) if methods else ["symbolic"] * len(labels) cb = candidate_block(list(zip(labels, answers, confs, meths))) if cb: parts.append(cb) return "\n\n".join(parts)