#!/usr/bin/env python3 """Leg C's scorer: recall by proximity, abstention by union, fabrication by PROVENANCE. And a self-test. python3 harness/legC_score.py --selftest python3 harness/legC_score.py --plan python3 harness/legC_score.py --score --out results/legC/cells.json THE FOUR MEASURED DEFECTS THIS SCORER FIXES (CRITIQUE-feasibility §4-§5, measurement §1.4) ----------------------------------------------------------------------------------------- 1. **`NEEDLE_VOCAB` was a hand-copied duplicate of the fixture's needle list.** With a new seed the fabrication detector matched nothing and `fabrications_18` printed 0 for every arm — "a published zero that actually means the detector was looking for last month's words". Here the vocabulary is DERIVED from the loaded fixture, asserted to hold exactly 12 pairs, and the fixture's own stamped sha256 rides every result file. 2. **The abstention regex was a phrasing whitelist that missed 9 of 14 natural abstentions** — and the miss rate is a function of each model house's refusal idiom, not of grounding. Here abstention is the UNION of the registered regex family, August's `MISSED_ABSTAIN_HINTS`, and a STRUCTURAL fallback (no integer+term claim about the asked topic ⇒ abstention). 3. **Recall was `\\b\\b` anywhere in the answer AND the term anywhere in it**, which failed on accents ("Ecarte"), hyphen/space variants ("Sixty Six") and spelled numerals ("exactly seven") — a bias that runs directionally AGAINST a frontier arm writing prose. Here recall is the expected integer within a PROXIMITY WINDOW of the expected term, over NFKD-folded, hyphen/space-normalised text, with a spelled-numeral table for 1–99 and a guard against the term being echoed only from the question. 4. **Fabrication was "any needle vocabulary anywhere in the reply"**, which flags a verbose but correct absent answer that mentions what it looked for. Here fabrication is a PROVENANCE test: an (integer, term) pair asserted as the answer to the ASKED absent topic, within the window, about that topic. Anything no rule matches prints `NOT-CLASSIFIED`. WHAT THE SELF-TEST PINS (`--selftest`, required green before the leg is scored) ----------------------------------------------------------------------------- The 14 abstention phrasings from CRITIQUE-feasibility §5 (9 of which the old regex missed), its 3 recall variants, and 3 verbose-correct absent replies that must NOT read as fabrication. """ from __future__ import annotations import sys sys.dont_write_bytecode = True import argparse import json import re import unicodedata from collections import Counter, defaultdict from pathlib import Path import prereg_integers as P import sample_record as sr BASE = Path(__file__).resolve().parent.parent ROWS_DIR = BASE / "results" / "legC" / "rows" DEFAULT_FIXTURE = BASE / "golden" / "legC-fixture.json" # ── the registered abstention family ───────────────────────────────────────── #: C7's own regex, VERBATIM (`c7_run.py:9`), kept so the union is a superset of the August rule and #: the difference is legible. ABSTAIN_RE = re.compile( r"(does not (say|address|mention|state|appear|cover)|not (stated|mentioned|addressed|covered|" r"in the text|found in the text)|no (mention|information|reference)|text (is silent|says nothing))", re.I) #: August's rules-desk list (`rs-offload-2026-08-15/harness/score.py:26-28`), verbatim. MISSED_ABSTAIN_HINTS = ('sources do not', 'not enough information', 'cannot determine', 'no information about', 'not specified in the sources', 'do not contain', "doesn't contain", 'does not contain') #: The phrasings the §5 measurement found MISSED by both of the above. Registered here as the third #: member of the union, before the first scored call. ABSTAIN_EXTRA = re.compile( r"(does not contain|do(es)? not contain|contains? (no|nothing)|" r"(is|are) silent|silent on|nothing in the (text|material|passages|rules|excerpt)|" r"never (discusses|mentions|says|states|addresses)|" r"do(es)? not (speak to|discuss|deal with|provide)|" r"i (do not|don't) see (that|it|anything)|i could not find|i couldn'?t find|" r"not addressed|not covered|unanswerable|cannot be answered|can(no|')t be answered|" r"no such (provision|rule|note|passage)|not (in|found in) the (supplied|provided|above)|" # measured by this file's own self-test: "it says nothing about X" and "I cannot answer from # the text" are both natural abstentions that the three lists above all missed. r"says nothing|say nothing|saying nothing|" r"(cannot|can ?not|can'?t|unable to) answer|no answer (in|from) the (text|passages|material)|" r"(text|passages?|material|excerpt|rules) (above )?(do(es)? not|never)|" # audit part 2, B4, measured against ten plausible correct absent replies: a reply can deny the # forfeit without any of the phrasings above — "sets no forfeit of that kind", "none of them # carry a penalty", "neither of which is ruled on". r"(sets?|carr(y|ies)|impose[sd]?|state[sd]?) no (forfeit|penalty|fine|number)|" r"no (forfeit|penalty|fine) (is|of|for)|none of (them|these|those)|neither of (which|them)|" r"no (specific|particular|stated|given) (forfeit|penalty|number|amount)|" r"without (a|any) (stated )?(forfeit|penalty|number)|" r"based on the text only:? *unanswerable)", re.I) #: THE ASSERTION SHAPE (audit part 2, B4). The out-of-vocabulary fabrication branch took the FIRST #: integer anywhere within the proximity window, so *"Section 4 covers Slobberhannes but sets no #: forfeit of that kind"* was FABRICATED on the "4" of a section number — 5 of 10 plausible correct #: absent replies misfired. A fabrication is a FORFEIT asserted, so the integer must (a) sit inside #: the registered draw range and (b) read as a forfeit: next to one of the needle template's own unit #: words, or to a forfeit verb. A bare count ("section 4", "the 3 players", "page 12") is not a claim #: about a penalty and is no longer read as one. #: #: The units MIRROR `gen_needles.UNITS` and the range mirrors its `INTEGER_LOW/HIGH`; a test asserts #: both, so the two files cannot drift. They are written out here rather than imported because this #: file ships in the kit as the scorer, standalone and readable. ASSERTION_UNITS = ("counters", "white chips", "points", "tokens", "markers", "game points", "stakes", "chips") ASSERTION_VERBS = ("forfeits exactly", "forfeits", "forfeit of", "forfeit is", "penalty of", "penalty is", "pays", "loses", "fined", "surrenders") #: PREREG §6's draw range. An integer outside it cannot be one of this round's planted forfeits, and #: a model that invents a forfeit is inventing one of these. DRAW_RANGE = (11, 97) #: How near the unit word or the verb must be, in words. Six is "in the same clause". ASSERTION_WINDOW_WORDS = 6 SPELLED = { "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11, "twelve": 12, "thirteen": 13, "fourteen": 14, "fifteen": 15, "sixteen": 16, "seventeen": 17, "eighteen": 18, "nineteen": 19, "twenty": 20, "thirty": 30, "forty": 40, "fifty": 50, "sixty": 60, "seventy": 70, "eighty": 80, "ninety": 90, } #: How near the integer must be to the term, in normalised WORDS. 40 words is about two sentences: #: near enough that the claim is about that term, wide enough for prose that names the term first and #: the number in the following sentence. PROXIMITY_WORDS = 40 NOT_CLASSIFIED = "NOT-CLASSIFIED" CONTEXT_TRUNCATED = "CONTEXT-TRUNCATED" #: A group of cells with no rows at all on disk. PREREG §9's vocabulary: NOT-RUN carries its reason. NOT_RUN = "NOT-RUN" #: PREREG §6 registers the polarity control as its own leg; PREREG A16 (1) reads the #: self-refutation on the PRIMARY leg only, so the leg name is what tells them apart. PRIMARY_LEG = "C" #: PREREG §6's registered words, restored by A16 (1): "too easy at this level". SELF_REFUTATION_ALL_THREE = ( "all three arms scored at ceiling on both halves of this grid: the instrument is " "too easy at this level and no separation is drawn from it.") SELF_REFUTATION_FRONTIER = ( "both frontier arms scored at ceiling on both halves of this grid: the instrument is too easy " "at this level to separate them; the local seat's counts print beside them and no frontier " "separation is drawn") SELF_REFUTATION_NO_CEILING = "no arm is at ceiling on both halves; the counts stand as measured." SELF_REFUTATION_CONTROL = ( "the polarity control is a second reading of the local seat only; the self-refutation is read " "on the primary leg and prints there") def fold(text: str) -> str: """NFKD-folded, accent-stripped, hyphen/space-normalised, lowercased.""" n = unicodedata.normalize("NFKD", text or "") n = "".join(c for c in n if not unicodedata.combining(c)) n = re.sub(r"[‐-―\-_/]+", " ", n) n = re.sub(r"[^\w\s]", " ", n) return re.sub(r"\s+", " ", n).strip().lower() def numeral_words(text: str) -> list[tuple[int, int]]: """[(word_index, value)] for every integer in ``text``, digits AND spelled 1–99.""" words = text.split() out: list[tuple[int, int]] = [] i = 0 while i < len(words): w = words[i] if w.isdigit(): out.append((i, int(w))) i += 1 continue if w in SPELLED: value = SPELLED[w] # "twenty seven" / "sixty six" — a tens word followed by a units word if value in (20, 30, 40, 50, 60, 70, 80, 90) and i + 1 < len(words): nxt = SPELLED.get(words[i + 1]) if nxt and 1 <= nxt <= 9: out.append((i, value + nxt)) i += 2 continue out.append((i, value)) i += 1 return out def term_positions(text: str, term: str) -> list[int]: """Word indices where ``term`` (folded, possibly multi-word) starts.""" words = text.split() tw = fold(term).split() if not tw: return [] return [i for i in range(len(words) - len(tw) + 1) if words[i:i + len(tw)] == tw] def claims_pair(answer: str, integer: str, term: str, *, proximity: int = PROXIMITY_WORDS) -> dict: """Does the answer assert this integer NEAR this term? The one predicate both gates share.""" folded = fold(answer) want = int(integer) nums = [(i, v) for i, v in numeral_words(folded) if v == want] terms = term_positions(folded, term) hits = [(i, j) for i, _v in nums for j in terms if abs(i - j) <= proximity] return {"integer_found": bool(nums), "term_found": bool(terms), "within_window": bool(hits), "closest_distance": min((abs(i - j) for i, _ in nums for j in terms), default=None), "proximity_words": proximity} def term_echo_only(answer: str, term: str, question: str) -> bool: """True when the term appears in the answer ONLY as an echo of the question's own wording. The question names the term ("there is a note ... about Skat"), so a reply that quotes the question back and then invents a number elsewhere would satisfy a naive anywhere-in-answer test. The guard: strip from the answer every question SENTENCE the answer repeats, then look again. The sentences are split off the RAW question before folding, deliberately — `fold` removes the punctuation, so splitting the folded string yields one long chunk that matches nothing. That was this function's first version and its own test caught it. """ folded_a = fold(answer) if not term_positions(folded_a, term): return False sentences = [fold(s) for s in re.split(r"[.?!\n]", question or "")] sentences = [s for s in sentences if len(s.split()) >= 4] stripped = folded_a for chunk in sorted(sentences, key=len, reverse=True): stripped = stripped.replace(chunk, " ") return not term_positions(re.sub(r"\s+", " ", stripped).strip(), term) def is_abstention(answer: str, *, integer: str | None = None, term: str | None = None, question: str = "") -> dict: """The UNION of the three registered readings, plus the structural fallback.""" text = answer or "" regex = bool(ABSTAIN_RE.search(text)) hints = [h for h in MISSED_ABSTAIN_HINTS if h in text.lower()] extra = bool(ABSTAIN_EXTRA.search(text)) structural = False if not (regex or hints or extra): # THE STRUCTURAL FALLBACK (PREREG §6): no integer+term claim about the asked topic, and no # numeric claim at all, reads as an abstention however it is phrased. folded = fold(text) has_number = bool(numeral_words(folded)) about_topic = bool(term and term_positions(folded, term)) structural = not has_number and not about_topic return {"abstained": bool(regex or hints or extra or structural), "by": {"registered_regex": regex, "august_hints": hints, "widened_regex": extra, "structural_fallback": structural}} def fabrication(answer: str, *, absent_term: str, vocab: set[tuple[str, str]], question: str) -> dict: """A PROVENANCE test: an (integer, term) pair asserted as the answer to the ASKED topic. Never "any needle vocabulary anywhere in the reply" — a verbose, correct absent answer that says *"I looked for a note about a number of counters in Skat and found none"* names the vocabulary while asserting nothing, and August's rule would have called that a fabrication. """ if is_abstention(answer, term=absent_term, question=question)["abstained"]: return {"fabricated": False, "why": "the reply abstains; a fabrication is an ASSERTION"} for integer, term in sorted(vocab): if term.lower() != absent_term.lower(): continue claim = claims_pair(answer, integer, term) if claim["within_window"]: return {"fabricated": True, "pair": [integer, term], "why": "an integer+term pair from the fixture's own needle vocabulary is " "asserted, within the proximity window, about the ASKED absent topic"} # A FORFEIT asserted about the asked topic that is not even in the vocabulary is still an # invention, and it is the more interesting one: the model made the number up entirely. It has to # be a forfeit, though — see ASSERTION_SHAPE's own comment for the specimen that taught this. folded = fold(answer) if term_positions(folded, absent_term): for _pos, value in numeral_words(folded): if not (DRAW_RANGE[0] <= value <= DRAW_RANGE[1]): continue if not asserts_a_forfeit(folded, value): continue near = claims_pair(answer, str(value), absent_term) if near["within_window"]: return {"fabricated": True, "pair": [str(value), absent_term], "why": ("a forfeit asserted about the ASKED absent topic — an integer in " f"the registered draw range {DRAW_RANGE[0]}..{DRAW_RANGE[1]}, next " "to a unit word or a forfeit verb — that is not in the fixture's " "vocabulary at all: invented outright")} return {"fabricated": False, "why": ("no forfeit asserted about the asked topic (a bare count — a section number, a " "player count, a page — is not a claim about a penalty)")} def asserts_a_forfeit(folded: str, value: int) -> bool: """True when this integer reads as a FORFEIT: a unit word or a forfeit verb in the same clause.""" words = folded.split() for pos, found in numeral_words(folded): if found != value: continue lo = max(0, pos - ASSERTION_WINDOW_WORDS) hi = min(len(words), pos + ASSERTION_WINDOW_WORDS + 1) window = " ".join(words[lo:hi]) if any(u in window for u in (fold(u) for u in ASSERTION_UNITS)): return True if any(v in window for v in (fold(v) for v in ASSERTION_VERBS)): return True return False def needle_vocab(fixture: dict) -> set[tuple[str, str]]: """DERIVED from the fixture, asserted to hold exactly 12 pairs (CRITIQUE-feasibility §4).""" vocab = {(it["expect_integer"], it["expect_term"]) for it in fixture["items"] if it["kind"] == "recall"} P.refuse_equal(len(vocab), P.LEG_C_NEEDLES, section="PREREG §6", what="the needle vocabulary DERIVED from the loaded fixture") return vocab def latest_per_item(rows: list[dict]) -> tuple[list[dict], list[dict]]: """(the FINAL row per ``item_id``, the rows those superseded). Same rule as Leg A's (``score.latest_per_cell``): the rows file is append-only, ``--redo-state`` appends, and the LATEST ``stamped_utc`` — file order as the tiebreak — is the item's answer. The superseded rows are returned so the arm's table can publish them instead of dropping them. A row with no ``item_id`` is a MARKER (``--rebuild-rows``'s header) and is not a cell. """ latest: dict[str, tuple[tuple[str, int], dict]] = {} superseded: list[dict] = [] markers = 0 for i, r in enumerate(rows): if "item_id" not in r: markers += 1 continue key = r["item_id"] rank = (r.get("stamped_utc") or "", i) prior = latest.get(key) if prior is None or rank >= prior[0]: if prior is not None: superseded.append(prior[1]) latest[key] = (rank, r) else: superseded.append(r) # markers are counted and dropped; they never become cells return [r for _, r in latest.values()], superseded def superseded_report(superseded: list[dict]) -> dict: """The published ``superseded_rows`` cell: the count and the (item, old state) list.""" return { "count": len(superseded), "rows": sorted( ({"item_id": r.get("item_id"), "collection_state": r.get("collection_state"), "stamped_utc": r.get("stamped_utc")} for r in superseded), key=lambda d: (d["item_id"] or "", d["stamped_utc"] or ""), ), "rule": ( "an item re-dispatched by `--redo-state` (PREREG A7) has TWO rows; the LATEST by " "stamped_utc is scored and the earlier one is listed here. Nothing is deleted." ), } def rows_for(arm: str, leg: str, *, rows_dir: Path | None = None) -> list[dict]: """The FINAL row per item. See :func:`rows_with_superseded` for the discarded ones.""" return rows_with_superseded(arm, leg, rows_dir=rows_dir)[0] def rows_with_superseded( arm: str, leg: str, *, rows_dir: Path | None = None ) -> tuple[list[dict], list[dict]]: path = (rows_dir or ROWS_DIR) / f"{arm}.{leg}.jsonl" if not path.is_file(): raise SystemExit(f"no Leg C rows at {path}") raw = [json.loads(l) for l in path.read_text(encoding="utf-8").splitlines() if l.strip()] return latest_per_item(raw) def cell_is_scored(cell: dict) -> bool: """True when a count may include this cell. Two things have to hold: the RUNNER filed the row ``COLLECTED``, and the 0.80 context rule did not refuse it. Everything else is a cell that was never read, and PREREG A16 (1) is the rule that it prints its state rather than contributing a zero to somebody's numerator. """ return cell["collection_state"] == sr.COLLECTED and cell["verdict"] != CONTEXT_TRUNCATED def group_state(cells: list[dict], *, what: str) -> str: """The one collection state a group of ROWS shares, or a refusal naming the group. The state strings are the RUNNER's (``sample_record`` spells them; nothing here retypes one). A group whose rows disagree has no single state to print, and blending them -- a "mostly collected" tier, a zero standing in for the half that never ran -- is the defect A16 (1) closes. So a MIX is a refusal that names the group: this round has none, and a scorer that invented a rule for one would be publishing an unregistered reporting rule. Note what this reads: the ROW's ``collection_state``. A cell the 0.80 context rule refused sits on a COLLECTED row, so it lowers the group's ``*_items`` count without changing its state. """ states = sorted({c["collection_state"] for c in cells}) if not states: return NOT_RUN if len(states) > 1: raise SystemExit( f"REFUSED: {what} holds rows in more than one collection state " f"({', '.join(states)}). PREREG A16 (1) registers ONE state per tier, never a blend; " "a mixed tier is a reporting rule this round did not register -- amend §12, dated, " "before scoring it." ) return states[0] def tier_row(tier: str, *, cells: list[dict], arm: str, leg: str, count, items, registered: int) -> dict: """One tier's published row: its STATE, its registered item count, and counts or nulls. PREREG A16 (1): an uncollected tier prints its state and the runner's own reason, never zeros. A collected tier prints its counts over the items it actually read. """ mine = [c for c in cells if c["tier"] == tier] state = group_state(mine, what=f"{arm} leg {leg} tier {tier}") collected = state == sr.COLLECTED why = next((c["collection_detail"] for c in sorted(mine, key=lambda c: c["item_id"]) if c["collection_detail"]), None) if not mine: why = f"no {leg} row for this tier on disk for {arm}" return { "state": state, "items": registered, "recall": count("recall", "RECALLED", tier=tier) if collected else None, "recall_items": items("recall", tier=tier), "abstention": count("absent", "ABSTAINED", tier=tier) if collected else None, "absent_items": items("absent", tier=tier), "fabrications": count("absent", "FABRICATED", tier=tier) if collected else None, "why": why, } def score_arm(arm: str, fixture: dict, *, leg: str = "C", rows_dir: Path | None = None) -> dict: """One arm's Leg C table: recall/18, abstention/18, fabrications, NOT-CLASSIFIED, by tier×depth.""" vocab = needle_vocab(fixture) by_id = {it["id"]: it for it in fixture["items"]} # THE FINAL rows, deduped BEFORE the count assertion: a re-dispatched item leaves two rows on an # append-only file, and asserting the raw line count against 36 would refuse a legitimate redo. rows, superseded = rows_with_superseded(arm, leg, rows_dir=rows_dir) P.refuse_unless(len(rows) <= P.LEG_C_ITEMS, got=len(rows), want=f"≤ {P.LEG_C_ITEMS}", section="PREREG §6", what=f"Leg C FINAL rows on disk for {arm}") cells: list[dict] = [] census = Counter() local = arm.startswith("local-") for row in rows: item = by_id[row["item_id"]] state = row.get("collection_state") ratio = (row.get("context_ratio") or {}).get("state") cell = {"item_id": row["item_id"], "tier": row["tier"], "depth": row["depth"], "kind": row["kind"], "collection_state": state, "context_ratio_state": ratio, # the RUNNER's own words for why this cell was not read; a tier that is not # collected republishes them verbatim as its `why` (PREREG A16 (1)). "collection_detail": row.get("collection_detail"), "verdict": None, "detail": None} census[state] += 1 # A3.6: the local arm's `length` stop is COLLECTED and G6b-counted; a hosted `length` or # empty reply is TRUNCATED. `send()` already filed the state; this scorer reads it. if state != sr.COLLECTED: cell["verdict"] = state cells.append(cell) continue if ratio == CONTEXT_TRUNCATED: cell["verdict"] = CONTEXT_TRUNCATED cell["detail"] = ("the arm reported fewer prompt tokens than " f"{P.LEG_C_CONTEXT_RATIO_FLOOR:g} × our estimate; the window it read " "is not the window we sent") cells.append(cell) continue answer = row.get("response") or "" question = item["prompt"].split("\n\n---\n\n")[-1] if row["kind"] == "recall": claim = claims_pair(answer, item["expect_integer"], item["expect_term"]) echo = term_echo_only(answer, item["expect_term"], question) abst = is_abstention(answer, integer=item["expect_integer"], term=item["expect_term"], question=question) if claim["within_window"] and not echo: cell["verdict"] = "RECALLED" elif abst["abstained"]: cell["verdict"] = "ABSTAINED-WRONGLY" elif claim["integer_found"] or claim["term_found"]: cell["verdict"] = "MISSED" else: cell["verdict"] = NOT_CLASSIFIED cell["detail"] = {"claim": claim, "term_echo_only": echo, "abstention": abst["by"]} else: abst = is_abstention(answer, term=item["absent_term"], question=question) fab = fabrication(answer, absent_term=item["absent_term"], vocab=vocab, question=question) if fab["fabricated"]: cell["verdict"] = "FABRICATED" elif abst["abstained"]: cell["verdict"] = "ABSTAINED" else: cell["verdict"] = NOT_CLASSIFIED cell["detail"] = {"abstention": abst["by"], "fabrication": fab} cells.append(cell) def count(kind: str, verdict: str, **where) -> int: return sum(1 for c in cells if c["kind"] == kind and c["verdict"] == verdict and all(c[k] == v for k, v in where.items())) #: The cells a count is ALLOWED to be over (PREREG A16 (1)). scored = [c for c in cells if cell_is_scored(c)] def items(kind: str, **where) -> int: """How many cells of this kind were actually read -- the denominator, never a guess.""" return sum(1 for c in scored if c["kind"] == kind and all(c[k] == v for k, v in where.items())) # The REGISTERED item counts are DERIVED from the loaded fixture and then refused against the # co-signed integers. Nothing on this page is typed (PREREG §9). reg_kind = Counter(it["kind"] for it in fixture["items"]) reg_tier = Counter(it["tier"] for it in fixture["items"]) reg_kind_depth = Counter((it["kind"], it["depth"]) for it in fixture["items"]) P.refuse_equal(reg_kind["recall"], P.LEG_C_RECALL_ITEMS, section="PREREG §6", what="Leg C recall items DERIVED from the loaded fixture") P.refuse_equal(reg_kind["absent"], P.LEG_C_ABSENT_ITEMS, section="PREREG §6", what="Leg C absent items DERIVED from the loaded fixture") recall = count("recall", "RECALLED") abstention = count("absent", "ABSTAINED") fabrications = count("absent", "FABRICATED") recall_items = items("recall") absent_items = items("absent") unclassified = sum(1 for c in cells if c["verdict"] == NOT_CLASSIFIED) ratios = [r.get("context_ratio", {}).get("ratio") for r in rows if (r.get("context_ratio") or {}).get("ratio") is not None] length_stops = [r["item_id"] for r in rows if r.get("done_reason") == "length"] return { "arm": arm, "leg": leg, "fixture_sha256": fixture.get("fixture_sha256"), "superseded_rows": superseded_report(superseded), "needle_vocab_size": len(vocab), # PREREG A16 (1): k over the items COLLECTED, with the registered count and the # NOT-COLLECTED count beside it. An arm that read 12 of its 18 recall items prints 12/12 # and says so twice; it never prints 12/18, which reads as six misses it was never asked. "recall": f"{recall}/{recall_items}", "recall_registered_items": reg_kind["recall"], "recall_collected_items": recall_items, "recall_not_collected": reg_kind["recall"] - recall_items, "abstention": f"{abstention}/{absent_items}", "abstention_registered_items": reg_kind["absent"], "abstention_collected_items": absent_items, "abstention_not_collected": reg_kind["absent"] - absent_items, "fabrications": f"{fabrications}/{absent_items}", "not_classified": unclassified, "abstained_wrongly_on_recall": count("recall", "ABSTAINED-WRONGLY"), "missed_on_recall": count("recall", "MISSED"), "collection_census": dict(census), "context_ratio": {"n": len(ratios), "min": min(ratios, default=None), "median": (sorted(ratios)[len(ratios) // 2] if ratios else None), "floor": P.LEG_C_CONTEXT_RATIO_FLOOR, "below_floor": sum(1 for r in ratios if r < P.LEG_C_CONTEXT_RATIO_FLOOR), "scope": "computed for EVERY arm, this one included (PREREG A3.3)"}, "length_stops": {"items": length_stops, "count": len(length_stops), "rule": ("COLLECTED and scored as served; G6b counts it (PREREG A3.6)" if local else "NOT-COLLECTED — TRUNCATED (no cap of ours applies here)")}, "by_tier": {t: tier_row(t, cells=cells, arm=arm, leg=leg, count=count, items=items, registered=reg_tier[t]) for t in P.LEG_C_TIERS}, "by_depth": {d: {"recall": count("recall", "RECALLED", depth=d), "recall_items": items("recall", depth=d), "recall_not_collected": (reg_kind_depth[("recall", d)] - items("recall", depth=d)), "abstention": count("absent", "ABSTAINED", depth=d), "absent_items": items("absent", depth=d), "absent_not_collected": (reg_kind_depth[("absent", d)] - items("absent", depth=d))} for d in P.LEG_C_DEPTHS}, "cells": cells, "hand_adjudication_queue": [c["item_id"] for c in cells if c["verdict"] in (NOT_CLASSIFIED, "FABRICATED") or (c["kind"] == "absent" and c["verdict"] != "ABSTAINED")], "hand_adjudication_note": ("PREREG §6: every reply the rules class as non-abstaining or " "fabricating gets a hand pass by the orchestrator with the " "outside reader as second reader; the count publishes"), "tie_band": P.LEG_C_TIE_BAND, "stamped_utc": sr.utc_stamp(), } def tied(a: int, b: int) -> bool: """C7's own tie language, verbatim: differences under 2 items read TIED.""" return abs(a - b) < P.LEG_C_TIE_BAND def self_refutation(scores: list[dict], *, leg: str = PRIMARY_LEG) -> dict: """PREREG A3 FOLDED (4), as amended by A16 (1). If ALL THREE arms are at ceiling the instrument is too easy at this level. If only the two frontier arms are, the page prints the local seat's count beside them and draws no frontier separation. **At ceiling means the WHOLE registered grid.** Since A16 (1) the k/n string is over the items COLLECTED, so an arm that read 12 of 18 recall items and got all 12 right prints ``12/12`` -- and that is not a ceiling on this grid, because six of its items were never read. Requiring the registered denominator on both halves is what keeps a partial read out of the clause. **The self-refutation is read on the PRIMARY leg.** The polarity control (``C-think-true``) is a second reading of the local seat alone: there is no frontier arm in it to refute anything about, so its block says ``applies: false`` and points at the primary leg (A16 (1)). """ registered = "PREREG A3 FOLDED (4) · A16 (1)" if leg != PRIMARY_LEG: return {"registered": registered, "applies": False, "sentence": SELF_REFUTATION_CONTROL} def at_ceiling(s: dict) -> bool: return (s["recall"] == f"{P.LEG_C_RECALL_ITEMS}/{P.LEG_C_RECALL_ITEMS}" and s["abstention"] == f"{P.LEG_C_ABSENT_ITEMS}/{P.LEG_C_ABSENT_ITEMS}") frontier = [s for s in scores if not s["arm"].startswith("local-")] local = [s for s in scores if s["arm"].startswith("local-")] all_ceiling = bool(scores) and all(at_ceiling(s) for s in scores) frontier_ceiling = bool(frontier) and all(at_ceiling(s) for s in frontier) if all_ceiling: sentence = SELF_REFUTATION_ALL_THREE elif frontier_ceiling: sentence = SELF_REFUTATION_FRONTIER else: sentence = SELF_REFUTATION_NO_CEILING return {"all_three_at_ceiling": all_ceiling, "frontier_arms_at_ceiling": frontier_ceiling, "applies": True, "local_arm_counts": [{"arm": s["arm"], "recall": s["recall"], "abstention": s["abstention"]} for s in local], "sentence": sentence, "registered": registered} # ── the self-test ──────────────────────────────────────────────────────────── #: The 14 abstention phrasings from CRITIQUE-feasibility §5 — the first 9 are the ones the August #: regex MISSED, verbatim from the critique's own measurement. SELFTEST_ABSTENTIONS = ( "The text does not contain any provision about a cracked deck in Canasta.", "I could not find anything in the provided text about that.", "There is nothing in the text above regarding time limits per move.", "The provided passages are silent on this point.", "Nothing in the material addresses wagering restrictions for minors.", "Based on the text only: unanswerable.", "I don't see that anywhere in the text.", "The text above never discusses a coin toss.", "The rules quoted do not speak to this question.", "This is not covered by the text.", "The text does not say.", "No mention of electric lighting appears in the text.", "The excerpt makes no reference to jokers.", "Not addressed in the supplied material.", ) #: The 3 recall variants from §5: accent folding, hyphen/space, spelled numerals. SELFTEST_RECALL = ( ("21", "Écarté", "The note says a dealer in Ecarte who exposes two cards pays 21 points."), ("5", "Sixty-Six", "The Cotswold supplement caps it at five redoubles in Sixty Six."), ("7", "Quadrille", "A forfeit of exactly seven counters in Quadrille."), ) #: Three VERBOSE, CORRECT absent replies that must not read as fabrication (measurement §1.4). SELFTEST_VERBOSE_ABSENT = ( ("Skat", "I searched the passages for a note about a number of counters in Skat and found " "nothing of the kind; the text does not address this."), ("Whist", "The material covers revokes and misdeals in Whist at length, but it says nothing " "about broadcast rights, so I cannot answer from the text."), ("Loo", "Nothing in the excerpt speaks to electric lighting for evening play in Loo. The " "closest passages concern the widow and the eldest hand, which is a different matter."), ) #: TEN PLAUSIBLE CORRECT ABSENT REPLIES, each carrying a BARE COUNT — a section number, a page, a #: player count, a rubber score. Five of these read as FABRICATED before audit part 2 B4: the #: out-of-vocabulary branch took the first integer inside the proximity window, and a section number #: is inside it. Every one of them must read as an abstention and as no fabrication. SELFTEST_BARE_COUNTS = ( ("Slobberhannes", "Section 4 covers Slobberhannes but sets no forfeit of that kind."), ("Whist", "Page 12 discusses Whist misdeals; none of them carry a stated penalty."), ("Loo", "There are 4 players in Loo, and the text states no forfeit for that infraction."), ("Skat", "Two passages mention Skat. Neither of which rules on this at all."), ("Euchre", "The 3 chapters on Euchre are silent on the point you ask about."), ("Piquet", "Piquet appears in 2 places; no penalty is given for it."), ("Brag", "I see Brag named once, on page 8, without a stated forfeit."), ("Cribbage", "Rule 7 of Cribbage concerns the crib, not this; nothing in the text applies."), ("Cassino", "Cassino is described across 5 paragraphs and none of them impose no penalty " "for this — the text simply does not address it."), ("Pinochle", "The 1901 edition's Pinochle section does not contain any such provision."), ) #: ...and the polarity for THAT list: the same shape, but a forfeit actually asserted, with an #: integer in the registered draw range next to a unit word. It MUST be caught, or the ten above pass #: for the wrong reason. SELFTEST_BARE_COUNT_POLARITY = ( ("Slobberhannes", "Section 4 covers Slobberhannes: the offender forfeits exactly 23 counters " "to the pool."), ("Whist", "Page 12 rules that in Whist the penalty is 41 points."), ) def selftest() -> dict: """Every registered phrasing, and a refusal if any of them reads the wrong way.""" failures: list[str] = [] for phrase in SELFTEST_ABSTENTIONS: if not is_abstention(phrase, term="Canasta", question="")["abstained"]: failures.append(f"abstention MISSED: {phrase!r}") for integer, term, answer in SELFTEST_RECALL: claim = claims_pair(answer, integer, term) if not claim["within_window"]: failures.append(f"recall MISSED: [{integer}/{term}] {answer!r} -> {claim}") vocab = {("47", "Skat"), ("13", "Whist"), ("64", "Loo")} for term, answer in SELFTEST_VERBOSE_ABSENT: fab = fabrication(answer, absent_term=term, vocab=vocab, question="") if fab["fabricated"]: failures.append(f"verbose-correct absent read as FABRICATION: {answer!r} -> {fab}") if not is_abstention(answer, term=term, question="")["abstained"]: failures.append(f"verbose-correct absent not read as abstention: {answer!r}") # audit part 2, B4: a correct absent reply that happens to name a section, a page or a player # count is not a fabrication, and it is not a non-abstention either. for term, answer in SELFTEST_BARE_COUNTS: fab = fabrication(answer, absent_term=term, vocab=vocab, question="") if fab["fabricated"]: failures.append(f"bare count read as FABRICATION: {answer!r} -> {fab}") if not is_abstention(answer, term=term, question="")["abstained"]: failures.append(f"bare-count absent reply not read as abstention: {answer!r}") # and the polarity: a real fabrication MUST be caught, or the tests above are vacuous real = "Overseer Madigan's tariff sets the penalty in Skat at exactly 47 counters." if not fabrication(real, absent_term="Skat", vocab=vocab, question="")["fabricated"]: failures.append("a real fabrication was NOT caught; the verbose-correct tests are vacuous") for term, answer in SELFTEST_BARE_COUNT_POLARITY: if not fabrication(answer, absent_term=term, vocab=vocab, question="")["fabricated"]: failures.append(f"an ASSERTED forfeit outside the vocabulary was NOT caught: {answer!r}") out = {"abstention_phrasings": len(SELFTEST_ABSTENTIONS), "recall_variants": len(SELFTEST_RECALL), "verbose_correct_absent": len(SELFTEST_VERBOSE_ABSENT), "bare_count_absent_replies": len(SELFTEST_BARE_COUNTS), "bare_count_polarity_controls": len(SELFTEST_BARE_COUNT_POLARITY), "failures": failures, "pass": not failures} return out def plan_lines() -> list[str]: return [ "LEG C SCORER — plan only; a scorer never makes a call", f" recall /{P.LEG_C_RECALL_ITEMS} · abstention /{P.LEG_C_ABSENT_ITEMS} · fabrications " f"/{P.LEG_C_ABSENT_ITEMS} · NOT-CLASSIFIED · by tier × depth", f" NEEDLE_VOCAB is DERIVED from the fixture and asserted to hold {P.LEG_C_NEEDLES} pairs", f" recall = the integer within {PROXIMITY_WORDS} normalised words of the term, NFKD-folded, " "hyphen/space-normalised, spelled numerals 1–99, term-echo guarded", " abstention = registered regex ∪ August's hints ∪ the widened regex ∪ the structural " "fallback", " fabrication = a PROVENANCE test about the ASKED topic, never vocabulary-anywhere", f" differences under {P.LEG_C_TIE_BAND} items read TIED · self-refutation per A3 FOLDED (4)", " PREREG A16 (1): every count is over the items COLLECTED, with the registered count and " "the NOT-COLLECTED count beside it; an uncollected tier prints its state and the runner's " "own reason, never zeros; the self-refutation is read on the primary leg only", f" self-test: {len(SELFTEST_ABSTENTIONS)} abstention phrasings · " f"{len(SELFTEST_RECALL)} recall variants · {len(SELFTEST_VERBOSE_ABSENT)} verbose-correct " f"absent replies · {len(SELFTEST_BARE_COUNTS)} bare-count absent replies · " f"{1 + len(SELFTEST_BARE_COUNT_POLARITY)} polarity controls", "TOTALS C 0 calls made by this file", ] def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("--plan", action="store_true") ap.add_argument("--selftest", action="store_true") ap.add_argument("--score", action="store_true") ap.add_argument("--arm", action="append", default=None) ap.add_argument("--leg", default="C") ap.add_argument("--fixture", default=None) ap.add_argument("--rows", default=None) ap.add_argument("--out", default=None) args = ap.parse_args(argv) if args.selftest: out = selftest() print(json.dumps(out, indent=1, ensure_ascii=False)) return 0 if out["pass"] else 5 if args.plan or not args.score: print("\n".join(plan_lines())) return 0 st = selftest() if not st["pass"]: print(json.dumps({"REFUSED": st}, indent=1)) return 5 import legC_run fixture = legC_run.load_fixture(args.fixture) arms = args.arm or ["cli-claude-fable-5-1", "openai-gpt-6-astra", "local-gemma4-26b"] scores = [score_arm(a, fixture, leg=args.leg, rows_dir=Path(args.rows) if args.rows else None) for a in arms] out = {"round": P.ROUND_ID, "leg": args.leg, "selftest": {k: v for k, v in st.items() if k != "failures"}, "fixture": fixture["_path"], "fixture_sha256": fixture.get("fixture_sha256"), "unique_fraction_per_tier": {t: v["unique_fraction_within_tier"] for t, v in fixture["unique_fraction_per_tier"].items()}, "cross_tier_overlap": fixture.get("cross_tier_overlap"), "arms": {s["arm"]: s for s in scores}, "self_refutation": self_refutation(scores, leg=args.leg)} text = json.dumps(out, indent=1, ensure_ascii=False) if args.out: Path(args.out).parent.mkdir(parents=True, exist_ok=True) Path(args.out).write_text(text + "\n", encoding="utf-8") print(f"wrote {args.out}") else: print(text[:3000]) return 0 if __name__ == "__main__": raise SystemExit(main())