diff --git a/docs/expression-contract.md b/docs/expression-contract.md index 495ae0a..8e01b43 100644 --- a/docs/expression-contract.md +++ b/docs/expression-contract.md @@ -218,6 +218,13 @@ leads with and the increment each of its blocks adds, plus counter-exemplars for the named bans. Every issuer in them is fictional (Widgetron WDGT, Gridcore GRDC, Fabrion FABR, ACME) and nothing is derived from a user record. +Since #834 the corpus is not the only place they live. Each conversational +surface's reference file **opens with its own copy of one of them**, so the +example is met while an answer is being written rather than only while one is +being graded. The two copies are one text, not two: +`tests/test_expression_contract.py` compares each reference block against the +scene its fence names and fails on drift. + `tests/agent/check_expression.py` derives E-7 and E-8 from that corpus (§7). What they decide is exactly two things — that the declared answer really leads, and that every declared block really adds a distinct increment — and the diff --git a/docs/maintainer-guide.md b/docs/maintainer-guide.md index 79ae843..efc3cdb 100644 --- a/docs/maintainer-guide.md +++ b/docs/maintainer-guide.md @@ -153,6 +153,7 @@ reader-question-chain) while leaving those rows' every other ruling intact. | Output structure & language | `docs/output-contract.md` (single authority on section order) and `docs/output-language.md` (locale contract) ↔ `card_renderer.py` ↔ `references/card-policy.md` / `card-spec.md` (subordinated: wording and in-block ranking only) | | How the product speaks, on every surface (#823, #825) | `docs/expression-contract.md` is the single authority for expression — voice routes to `docs/output-voice.md`, disclosure relevance is D1–D6, and citation/provenance is C1–C4. Its surfaces are `docs/output-contract.md` §4 ↔ `references/trade-consequence.md` ↔ `references/freeform-answers.md` ↔ `references/decision-framing.md` ↔ `references/weekly-market-read.md`. The card may keep its footnote as local layout, but conversational surfaces have no mandatory tail block, prefix, or line cap. `tests/agent/check_expression.py` now enforces only E-5/C4 (no engine-token leaks); #825 retired E-1–E-4 because block position, marker syntax, line count, and literal deduplication did not prove relevance or clarity. `tests/test_expression_contract.py` keeps registry/routing checks and pins that D1–D6 honestly declare instruction-only verification. | | One communication method: the answer pyramid (#832, 2026-08-21) | `docs/expression-contract.md` §3 is the **mother law** of answer shape and the only statement of it. Its readers, each of which now carries a *derivation* rather than a phrasing: `docs/output-contract.md` §2 (the card is the document incarnation — keynote = top floor, three middle blocks = the middle, the Block-1 footnote = the end block; the card's own structure is unchanged) ↔ `docs/output-voice.md` (V1 keeps its ID as the **failure class** and stops being a second statement of the rule) ↔ `skills/fomo-kernel/SKILL.md` "Shape of the answer" (the always-on projection, explicitly labelled as one) ↔ `references/trade-consequence.md` (adds exactly two parameters: lead selection and answer slots) ↔ `references/decision-framing.md` (adds two: the baseline is the top sentence with no book, the strategy-class map is a middle-floor block set) ↔ `references/weekly-market-read.md` (adds one: its optional question comes after the complete brief) ↔ `references/freeform-answers.md` (**adds nothing — an empty derivation is valid and is the default**) ↔ `tests/agent/expression-witnesses.json` ↔ `tests/agent/check_expression.py` (E-7/E-8) ↔ `tests/test_expression_contract.py` ↔ `tests/test_research_priors.py` (which pinned the no-book phrasing by literal and now pins the block order and the derivation instead). Four rules. **Derivation is additive-only**: a surface may add which blocks its middle may hold, which questions it may ask, what its end block must name, or what it may not compute — it may not restate, narrow, re-order, or contradict the shape. **The registries are frozen for shape and length**: V, D, and C take **no new ID** for "answers are too long", "lead with X", or "stop repeating Y", because five independently-worded statements of answer-first is the disease and a sixth with an ID on it is still a sixth; V10 stays unallocated (proposed in #830, demoted there, refused here). **A style fix has two lanes and only two**: amend §3 (owner ruling required, logged in §8) or add an exemplar/counter-exemplar to the witness corpus (day-to-day, no ruling). **The exemplars are the spec, and the oracle is honest about its half**: E-7 decides that a scene's declared one-sentence answer really leads, E-8 that every declared block really adds a distinct increment; the manufactured-scenario and hedging-couplet bans have **no** mechanical oracle, and the corpus asserts that by requiring their counter-exemplars to *pass* every assertion — the day one of them can be caught, that assertion is what says the boundary moved. Why the row exists at all: #830's post-merge rerun (PR #831) fixed composition and moved first-answer length by less than 5%, which is the evidence that deleting obligations without a positive shape norm only vacates space for discretionary elaboration. **Not adopted, again**: any character-count cap — #543's ceiling stayed deleted, and length is the shape's consequence, never its rule. Integrity gates (engine-owned numbers, provenance, canonical writes, execution truth, privacy) and #829 are untouched and out of scope. | +| One canonical exemplar on each surface's generation path (#834, 2026-08-22) | The exemplars stopped living only in the QC layer. Each conversational surface's reference file **opens with one canonical exemplar copied from `tests/agent/expression-witnesses.json`**: `references/trade-consequence.md` (`consider_three_way_comparison`) ↔ `references/decision-framing.md` (`no_book_single_name`) ↔ `references/freeform-answers.md` (`freeform_positions_view`) ↔ `references/weekly-market-read.md` (`weekly_read_connection`), with `docs/expression-contract.md` §3.5 and `skills/fomo-kernel/SKILL.md` "Shape of the answer" pointing at them. Four rules. **The corpus is the source and the reference file is the copy**: `tests/test_expression_contract.py::test_every_surface_reference_opens_with_its_canonical_exemplar` normalizes both sides and fails closed on drift, so editing either alone reddens — this is the hand-mirrored surface this guide forbids, made mechanical instead of forbidden, because the whole point is that the same text has to be in two places at once. **The pairing is never hand-listed**: which reference file owns a surface is read from the corpus's own `surfaces` map, and which scene a file carries is read from that file's own fence, so the sync test compares two declarations that already existed rather than adding a third to keep in step. **The marker is one line** — a fenced block whose info string is `exemplar ` — and it is also the unit `tests/test_doc_language.py`'s `exemplar_quoted_lines` exempts from the English-only gate, since three of the four exemplars are this product speaking Traditional Chinese and quoted product output is not documentation prose (the same distinction that keeps `copy/zh-TW.json` out of `ENGLISH_IMPLEMENTATION_ASSETS`). That exemption is bounded three ways at once — by directory, by fence tag, and by the byte-identity the sync test proves — and `test_no_other_document_carries_an_exemplar_block` is what stops it spreading to a fifth file. **Placement only, per #832's stop condition**: no new registry ID, no length cap, no obligation, and the always-loaded pair stays inside #507's byte budget with SKILL.md gaining one clause, because progressive disclosure puts the exemplar in the file that is already loaded when its surface is in play. Why the row exists: #832's post-merge rerun (receipts on PR #833) bent the length curve for the first time in three of four frozen scenes and regressed the fourth — the no-book freeform one, the surface with the emptiest guidance and no engine payload anchoring the answer — which is the evidence that a norm the model never meets at generation time does not bind scale. | | Runtime behavior | engine ↔ `SKILL.md` and routed flows/references ↔ `docs/eval-design.md` ↔ `evals/EVALS.md` | | Demo card values | English README ↔ English demo HTML/image; Traditional Chinese README ↔ Traditional Chinese demo HTML/image. Values must match; only wording differs. | | GTM documentation | `README.md` is the English default; `README.zh-TW.md` is the complete Traditional Chinese counterpart. Keep language links and substantive product claims synchronized. | diff --git a/skills/fomo-kernel/SKILL.md b/skills/fomo-kernel/SKILL.md index 758d104..509e70c 100644 --- a/skills/fomo-kernel/SKILL.md +++ b/skills/fomo-kernel/SKILL.md @@ -42,7 +42,7 @@ The engine computes portfolio consequence; it is not a company-research service. One shape, every answer (`../../docs/expression-contract.md` §3 owns it; this is its projection, not a second wording). **A fact lives on exactly one floor, and twice is a bug.** *Top:* one sentence — the stance and the reason that decides it (proceed, resize, delay, collect evidence, choose one candidate, no trade). *Middle:* only blocks that add a new decision-relevant fact or judgment — delete one; if the decision does not change, delete it. There live the numbers that would flip the call, every `rule_effects` entry (never optional), a truth-critical denominator, unit, or pricing set beside its number, and a falsifier on any directional call — the counter-case needs no section. *Bottom:* the rest of the inventory stays in the data layer; say once you can expand it. *End:* one compact block for other material limitations; machine anchors and engine narration nowhere. -Never manufacture a scenario nobody asked for, restate a system default as insight, hedge in couplets, or make one point twice. Ask only decision-changing questions, then stop. `references/trade-consequence.md` holds the rest. +Never manufacture a scenario nobody asked for, restate a system default as insight, hedge in couplets, or make one point twice. Ask only decision-changing questions, then stop. `references/trade-consequence.md` holds the rest. Each reference opens with its exemplar; follow it. Label judgment — thesis, valuation, timing, forecast, recommendation, ranking, selection — separate from engine facts. Give a target or forecast's material assumptions and uncertainty; never disguise it as fact or certainty. Never claim what the user did or will do. diff --git a/skills/fomo-kernel/references/decision-framing.md b/skills/fomo-kernel/references/decision-framing.md index 5ccea92..28a2ba6 100644 --- a/skills/fomo-kernel/references/decision-framing.md +++ b/skills/fomo-kernel/references/decision-framing.md @@ -1,5 +1,25 @@ # Framing a decision when there is no recorded book +## The exemplar + +One whole no-book answer. Every book-derived claim is refused and the answer +still lands a stance, names the gap that decides it, and leaves the user a +falsifier they could write down themselves. Read it before writing one. + +It is copied verbatim from `tests/agent/expression-witnesses.json`, scene +`no_book_single_name` — the corpus that +[expression-contract.md](../../../docs/expression-contract.md) §3.5 makes the +binding statement of this shape. Every issuer in it is invented, and +`tests/test_expression_contract.py` fails if the two copies disagree. + +```exemplar no_book_single_name +WDGT 這家公司的證據支持買,但「現在進場」我不背書——缺的是估值,不是基本面。上季營收 +24%、EPS 超預期、同日上修全年(公司新聞稿,2026-07-24),這是硬的;但這些已公開三週,而我查不到現在的估值倍數,「好消息是否已在價格裡」這一半我答不了。 + +$5,000 試水溫,真正的洞在出場:「故事變了就賣」跟你的進場理由是同一個變數,等於沒有獨立的認錯線。可檢查的替代就用它自己簽的支票:全年營收財測或毛利率轉向,擇一寫下來,就可以進場。 + +(價 8/14 收盤 $188.20;你未提供持倉,部位佔比與重疊無法評) +``` + `review.py consider` answers what a trade does to the user's own book, and it fails closed when there is no book to answer against. That refusal is correct — it protects the arithmetic — but it is not the end of the conversation. A user who has recorded nothing still arrives with a live decision, and refusing is not what earns their transaction history. Guidance is. This file is the contract for decisions made without a recorded book. The user diff --git a/skills/fomo-kernel/references/freeform-answers.md b/skills/fomo-kernel/references/freeform-answers.md index 9c5c124..0e4497e 100644 --- a/skills/fomo-kernel/references/freeform-answers.md +++ b/skills/fomo-kernel/references/freeform-answers.md @@ -1,5 +1,26 @@ # Freeform informational answers +## The exemplar + +One whole freeform answer. This route adds no parameter of its own (rule 4 +below), so the exemplar is what this file has to show: the one position the +question turns on, the rest of the computed inventory behind a single offer, +and one end block. Read it before writing one. + +It is copied verbatim from `tests/agent/expression-witnesses.json`, scene +`freeform_positions_view` — the corpus that +[expression-contract.md](../../../docs/expression-contract.md) §3.5 makes the +binding statement of this shape. Every issuer in it is invented, and +`tests/test_expression_contract.py` fails if the two copies disagree. + +```exemplar freeform_positions_view +六檔裡只有一檔值得你現在看:GRDC 佔 31.4%,其餘五檔全在 10% 以下。 + +最大的那筆已經是第二大的三倍,其他五檔加起來還不到它。要完整的逐檔表(股數、成本、市值、損益、診斷標籤)跟我說一聲就給。 + +(帳本 8/14;價格 8/14 收盤;ETF 未拆解,成分重疊未評) +``` + The user does not only meet this product through `prepare → preview → finalize`. They ask ad hoc questions mid-conversation — "what's my portfolio worth right now," "how much cash do I have," "what if I add to this" — and diff --git a/skills/fomo-kernel/references/trade-consequence.md b/skills/fomo-kernel/references/trade-consequence.md index 3180a18..73f1896 100644 --- a/skills/fomo-kernel/references/trade-consequence.md +++ b/skills/fomo-kernel/references/trade-consequence.md @@ -1,5 +1,27 @@ # Weighing a trade the user has not placed yet +## The exemplar + +One whole `consider` answer, over a three-candidate comparison: the stance and +its deciding reason on top, the counter-side present only as the line that could +overturn the pick, and the book date, the price session and the unevaluated +valuation gap as one end block. Read it before writing one. + +It is copied verbatim from `tests/agent/expression-witnesses.json`, scene +`consider_three_way_comparison` — the corpus that +[expression-contract.md](../../../docs/expression-contract.md) §3.5 makes the +binding statement of this shape. Every issuer in it is invented, and +`tests/test_expression_contract.py` fails if the two copies disagree. + +```exemplar consider_three_way_comparison +三個裡我會選 GRDC 加 15 股。決定性理由:三案對組合的影響都在一個百分點內——誰都不改變你的集中度——真正有差的只有事件風險:WDGT 六天後出財報、預期已拉滿(公司財報行事曆,2026-08-14),這時把最大倉再加大,是三案裡波動最大的;FABR 18 股只佔 1.2%,公司再好這個大小也改變不了結果。GRDC 下次財報在十月底,中間乾淨,上季主業 +82%(公司財報,2026-07-30)撐著。 + +反面就一條:前三大會從 51.3% 升到 51.9%(GRDC 本來就是第二大)——嫌集中的話這是三案共同的問題,答案是減碼不是選誰。 +會讓我改口:你本來就想賭財報超預期——那 WDGT 反而是最直接的表達,排序整個反過來。 + +(帳本 8/14、價格 8/14 收盤;三案動用 $4.4K/$5.1K/$4.9K;估值未評) +``` + A user mid-decision asks something like *"I'm thinking of buying NVDA — what does that do to my book?"* They are not in a review and will not hand over a CSV. `consider` answers from what the product already stores: the local ledger, or transaction files if you have them in hand. This is Layer 2 (docs/decision-fomo-kernel-shape.md §3-4): deterministic arithmetic over a hypothetical trade. The engine computes the consequence; the agent turns it and any relevant sourced evidence into an explicit recommendation. The recommendation is `agent_judgment`, never a disguised engine output or execution claim. diff --git a/skills/fomo-kernel/references/weekly-market-read.md b/skills/fomo-kernel/references/weekly-market-read.md index 4114cb0..27c04e5 100644 --- a/skills/fomo-kernel/references/weekly-market-read.md +++ b/skills/fomo-kernel/references/weekly-market-read.md @@ -1,5 +1,21 @@ # Weekly Market Read prototype +## The exemplar + +One whole weekly brief. The connection between the frozen reading and a +diagnosed holding leads, the bound on what that alert claims follows it, and +the next-week check closes. Read it before writing one. + +It is copied verbatim from `tests/agent/expression-witnesses.json`, scene +`weekly_read_connection` — the corpus that +[expression-contract.md](../../../docs/expression-contract.md) §3.5 makes the +binding statement of this shape. Every issuer in it is invented, and +`tests/test_expression_contract.py` fails if the two copies disagree. + +```exemplar weekly_read_connection +Volatility rose through the week while your heaviest name was already flagged as too large. Both readings are frozen with the review rather than refreshed today, and valuation was not checked, so this is a concentration alert rather than a claim that the holding is expensive. Watch whether the name's weight and the volatility reading remain elevated next week. +``` + The #683 prototype is a read-only companion to a prepared `weekly_review`. Run its first read only after the complete, current private-card preview and before the existing rule choice: diff --git a/tests/test_doc_language.py b/tests/test_doc_language.py index c0eb40f..797d5db 100644 --- a/tests/test_doc_language.py +++ b/tests/test_doc_language.py @@ -41,6 +41,9 @@ Path("skills/fomo-kernel/evals/evals.json"), ) SKILL_DIR = Path("skills/fomo-kernel") +# #834: see exemplar_quoted_lines() for what this exempts and why. +EXEMPLAR_CARVE_OUT_DIR = SKILL_DIR / "references" +EXEMPLAR_FENCE = re.compile(r"^```exemplar [a-z0-9_]+$") AGENT_RUNTIME_SURFACES = ( Path("AGENTS.md"), SKILL_DIR / "SKILL.md", @@ -916,15 +919,65 @@ def markdown_section(text, heading): return text[content_start:] if next_heading < 0 else text[content_start:next_heading] +def exemplar_quoted_lines(rel, lines): + """1-based line numbers holding product output quoted verbatim (#834). + + The four surface references open with one canonical exemplar copied from + `tests/agent/expression-witnesses.json`, and three of the four are this + product speaking Traditional Chinese. That text is an answer, not + documentation prose -- the same distinction that keeps `copy/zh-TW.json` + out of `ENGLISH_IMPLEMENTATION_ASSETS` -- so the English-only gate skips + it and keeps reading every other line of the file. + + The exemption is narrow in three ways at once, and widening any one of + them turns it into a licence for translated documentation: only under + `references/`, only inside a fence whose info string is `exemplar + `, and only for text `tests/test_expression_contract.py` proves + is the corpus copy byte for byte. + """ + if EXEMPLAR_CARVE_OUT_DIR not in rel.parents: + return frozenset() + quoted, inside = set(), False + for number, line in enumerate(lines, 1): + if inside: + if line == "```": + inside = False + else: + quoted.add(number) + elif EXEMPLAR_FENCE.match(line): + inside = True + assert not inside, f"{rel}: an exemplar fence is never closed" + return frozenset(quoted) + + def test_implementation_markdown_is_english_only(): violations = [] for rel, path in implementation_markdown_files(): - for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): - if CJK.search(line): + lines = path.read_text(encoding="utf-8").splitlines() + quoted = exemplar_quoted_lines(rel, lines) + for line_number, line in enumerate(lines, 1): + if line_number not in quoted and CJK.search(line): violations.append(f"{rel}:{line_number}: {line.strip()}") assert not violations, "Non-English text found in implementation docs:\n" + "\n".join(violations) +def test_the_exemplar_carve_out_stays_narrow(): + """Mutation proof for the one exemption above (#834). + + The gate must still see the same Chinese line everywhere except inside a + tagged exemplar fence in a surface reference: in another directory, in an + untagged fence, and in the prose on either side of the fence. + """ + rel = EXEMPLAR_CARVE_OUT_DIR / "freeform-answers.md" + fenced = ["# T", "", "六檔", "```exemplar freeform_positions_view", "六檔", "```", "六檔"] + assert exemplar_quoted_lines(rel, fenced) == {5}, \ + "the carve-out does not cover exactly the quoted product output" + assert exemplar_quoted_lines(SKILL_DIR / "flows" / "first-review.md", fenced) == frozenset(), \ + "a file outside the surface references was granted the exemption" + assert exemplar_quoted_lines(rel, ["```text", "六檔", "```"]) == frozenset(), \ + "an untagged fence was read as an exemplar" + + def test_english_skill_assets_are_english_only(): paths = [ROOT / rel for rel in ENGLISH_IMPLEMENTATION_ASSETS] paths.extend(sorted((ROOT / "skills/fomo-kernel/rubric").glob("*.lens.json"))) @@ -1541,6 +1594,7 @@ def test_every_test_in_this_module_is_registered(): def main(): tests = [ test_implementation_markdown_is_english_only, + test_the_exemplar_carve_out_stays_narrow, test_english_skill_assets_are_english_only, test_gtm_locale_pair_exists, test_readme_bash_commands_match_across_languages, diff --git a/tests/test_expression_contract.py b/tests/test_expression_contract.py index 004c965..eb4c038 100644 --- a/tests/test_expression_contract.py +++ b/tests/test_expression_contract.py @@ -75,6 +75,13 @@ INSTRUCTION_ONLY_IDS = tuple(f"D{number}" for number in range(1, 7)) + ("C3",) ROW_RE = re.compile(r"^\|\s*([DC]\d+)\s*\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|$", re.M) +# #834: the exemplar's second home. A fence tagged `exemplar` plus the scene id +# it copies is the whole marker format -- one line, no counting, and the id +# travels with the text rather than sitting in a table beside it. +EXEMPLAR_FENCE = re.compile( + r"^```exemplar (?P[a-z0-9_]+)\n(?P.*?)\n```$", re.M | re.S) +REFERENCE_IN_SURFACE = re.compile(r"references/(?P[a-z0-9-]+\.md)") + def _load_checker(): if "check_expression" in sys.modules: return sys.modules["check_expression"] @@ -287,6 +294,106 @@ def test_the_checker_derives_its_blacklist_from_the_schemas(): assert checker.internal_tokens(pathlib.Path(os.devnull).parent / "nowhere") == () +# ───── 3b. the exemplar on the generation path is the corpus copy (#834) ───── + + +def _corpus(): + return json.loads(WITNESSES.read_text(encoding="utf-8")) + + +def _normalized(text): + """Trailing whitespace and the fence's own padding are not drift.""" + return "\n".join(line.rstrip() for line in text.strip().split("\n")) + + +def _exemplars(text): + """Every `(scene_id, body)` an exemplar fence declares, in order. + + The fence carries its scene id in the info string, so extraction needs no + line offsets, no heading walk, and no second list of which file holds + which exemplar. + """ + return [(match.group("scene"), match.group("body")) + for match in EXEMPLAR_FENCE.finditer(text)] + + +def test_every_surface_reference_opens_with_its_canonical_exemplar(): + """#834. Before it, the exemplars existed only in the QC layer: the model + never saw one while generating, and prose norms alone did not bind scale. + Each surface reference now opens with its own copy. + + The pairing is read from the two declarations that already existed -- the + corpus's `surfaces` map names the reference file that owns each surface, + and the reference file's own fence names the scene -- so this compares + them rather than adding a third list to keep in step. + """ + corpus = _corpus() + scenes = {scene["id"]: scene for scene in corpus["scenes"]} + for surface, description in corpus["surfaces"].items(): + named = REFERENCE_IN_SURFACE.findall(description) + assert len(named) == 1, ( + f"the corpus surface {surface!r} names {len(named)} reference files; " + "exactly one owns it") + path = REFERENCES / named[0] + assert path.is_file(), f"surface {surface!r} names a missing file: {named[0]}" + text = path.read_text(encoding="utf-8") + found = _exemplars(text) + assert len(found) == 1, ( + f"{path.relative_to(ROOT)} carries {len(found)} exemplar blocks, not one") + headings = [match.start() for match in re.finditer(r"^## ", text, re.M)] + fence = text.index("```exemplar ") + assert headings and headings[0] < fence and ( + len(headings) == 1 or fence < headings[1]), ( + f"{path.relative_to(ROOT)} does not open with its exemplar; progressive " + "disclosure only helps if the example is what the reader meets first") + scene_id, body = found[0] + scene = scenes.get(scene_id) + assert scene is not None, ( + f"{path.relative_to(ROOT)} names scene {scene_id!r}, absent from the corpus") + assert scene["surface"] == surface, ( + f"{path.relative_to(ROOT)} opens with a {scene['surface']!r} exemplar") + assert scene["kind"] == "positive", ( + f"{path.relative_to(ROOT)} opens with a {scene['kind']} exemplar ({scene_id})") + assert _normalized(body) == _normalized(scene["answer"]), ( + f"{path.relative_to(ROOT)} and the witness copy of {scene_id!r} have drifted; " + "one of the two was edited alone and they are no longer one exemplar") + + +def test_no_other_document_carries_an_exemplar_block(): + """The English-only carve-out `tests/test_doc_language.py` gives an + exemplar fence is safe only while the surface references are the only + files that have one. Otherwise the fence is a way to put unchecked, + untranslated prose anywhere in the tree.""" + corpus = _corpus() + owned = {REFERENCES / REFERENCE_IN_SURFACE.search(description).group("name") + for description in corpus["surfaces"].values()} + # Dot directories are skipped rather than scanned: a maintainer's own + # `.claude/worktrees/` holds whole checkouts of this repository, and every + # reference file in one of them would read as a stray copy of itself. + candidates = [(path, path.relative_to(ROOT)) for path in sorted(ROOT.rglob("*.md"))] + stray = [str(rel) for path, rel in candidates + if not any(part.startswith(".") for part in rel.parts) + and path not in owned + and "```exemplar " in path.read_text(encoding="utf-8")] + assert not stray, "exemplar fence outside a surface reference: " + ", ".join(stray) + + +def test_exemplar_drift_is_caught(): + """Mutation proof for the gate above, in both directions it can go blind: + a one-character edit to either copy must redden, and a fence that loses + its `exemplar` tag must stop being read as one rather than pass.""" + scene = next(item for item in _corpus()["scenes"] + if item["id"] == "freeform_positions_view") + block = f"```exemplar {scene['id']}\n{scene['answer']}\n```" + assert _exemplars(block) == [(scene["id"], scene["answer"])], \ + "the extractor does not read a well-formed block" + edited = block.replace("六檔", "五檔", 1) + assert _normalized(_exemplars(edited)[0][1]) != _normalized(scene["answer"]), \ + "a one-character edit to the reference copy left the gate green" + assert _exemplars(f"```text\n{scene['answer']}\n```") == [], \ + "an untagged fence is being read as an exemplar" + + # ───────────── 4. the obligation floor stays a list, not a table ───────────── def _dense_challenge(): @@ -433,6 +540,9 @@ def main(): test_the_contract_routes_voice_rather_than_restating_it, test_the_witness_oracle_passes, test_the_checker_derives_its_blacklist_from_the_schemas, + test_every_surface_reference_opens_with_its_canonical_exemplar, + test_no_other_document_carries_an_exemplar_block, + test_exemplar_drift_is_caught, test_fixed_topics_stay_within_their_declared_ceiling, test_no_owed_fact_is_a_duplicate_another_could_absorb, test_the_owed_floor_stayed_smaller_than_the_whole_inventory,