#!/usr/bin/env python3 """ONE CHECKER PER ITEM. ``CHECKERS.md`` is generated from these docstrings. The chair-trials pattern, lifted whole: the document is generated from the code and carries the module's sha256, so if a rule in the document disagrees with the code, the document is stale -- and that is a bug we want reported, not a discrepancy a reader has to adjudicate. The per-item functions are thin on purpose. The judgement lives in four combinators below, which are shared, tested once, and exercised by the audition against every item's paraphrases and decoys. Thirty hand-rolled matchers would be thirty places for a rule to be spelled differently. * ``numeric`` -- the key is a number. First-line extraction, no-alternatives on distinct numbers, number words accepted either way. * ``date`` -- an ISO date, plus the long forms a model actually writes. * ``token`` -- an exact string (a model tag, a version, an email, a word). * ``url`` -- canonicalised host+path, no-alternatives on distinct URLs. * ``abstains`` -- the refusal-bait rule. STAMPED PROXY, and the only combinator whose verdict rests on a phrase list. Every combinator returns a ``Verdict`` carrying WHY, so the published rows show the reason a cell failed rather than only that it did. """ from __future__ import annotations import sys sys.dont_write_bytecode = True import hashlib import re from dataclasses import dataclass from pathlib import Path from typing import Callable HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) import thirteen_normalize as norm # noqa: E402 import thirteen_questions as qs # noqa: E402 @dataclass(frozen=True) class Verdict: """A checker's answer: correct, and the reason it decided that.""" correct: bool reason: str extracted: str = "" def __bool__(self) -> bool: return self.correct #: Long-form date spellings a model writes instead of an ISO stamp. _MONTHS = { "january": 1, "february": 2, "march": 3, "april": 4, "may": 5, "june": 6, "july": 7, "august": 8, "september": 9, "october": 10, "november": 11, "december": 12, } _LONG_DATE = re.compile( r"\b(?:(\d{1,2})(?:st|nd|rd|th)?\s+)?" r"(" + "|".join(_MONTHS) + r")" r"(?:\s+(\d{1,2})(?:st|nd|rd|th)?)?" r"(?:[,\s]+(\d{4}))?\b", re.I, ) _ISO_DATE = re.compile(r"\b(\d{4})[-/](\d{1,2})[-/](\d{1,2})\b") def _dates_in(text: str) -> list[str]: """Every date in a text, canonicalised to ISO. Order preserved.""" found: list[str] = [] t = norm.nfkc(text or "") for m in _ISO_DATE.finditer(t): iso = f"{int(m.group(1)):04d}-{int(m.group(2)):02d}-{int(m.group(3)):02d}" if iso not in found: found.append(iso) for m in _LONG_DATE.finditer(t): day = m.group(1) or m.group(3) year = m.group(4) if not day: continue month = _MONTHS[m.group(2).casefold()] # A year-less long date ("August 11") is anchored to the corpus year, # which is the only year this bench's content lives in. Registered here # rather than guessed per item. iso = f"{int(year) if year else 2026:04d}-{month:02d}-{int(day):02d}" if iso not in found: found.append(iso) return found # ── the four combinators ─────────────────────────────────────────────────── def numeric(reply: str, key: str) -> Verdict: """Numeric key: first line, one distinct number, equal to the key.""" line = norm.first_line(reply) if not line: return Verdict(False, "empty reply") if norm.looks_like_abstention(line): return Verdict(False, "abstained on an answerable item", line) found = norm.numbers_in(line) if not found: return Verdict(False, "no number in the first line", line) if len(found) > 1: return Verdict(False, f"no-alternatives: {len(found)} numbers offered {found}", line) want = norm.canonical_number(key) return Verdict(found[0] == want, f"read {found[0]!r}, key {want!r}", line) def decimal(reply: str, key: str) -> Verdict: """Decimal key (0.5, 7.33): the same rule, but decimals kept whole. Separate from ``numeric`` because the integer tokenizer would read "7.33" as two numbers and "0.5" as two, which would fail every decimal item on the no-alternatives rule. The sharp decoy this exists to reject is 0.05 against a key of 0.5 -- a containment matcher passes it and this does not. """ line = norm.first_line(reply) if not line: return Verdict(False, "empty reply") if norm.looks_like_abstention(line): return Verdict(False, "abstained on an answerable item", line) found: list[str] = [] for m in re.finditer(r"\d+\.\d+|\d+", _strip_scale_phrases(line)): tok = m.group(0) val = tok if "." in tok else str(int(tok)) if val not in found: found.append(val) if not found: # "half a point" is a correct answer to "what is the tie band"; the # closed fraction table carries the spellings that are, and nothing else. flat = norm.normalize(line) for phrase, value in norm.FRACTION_WORDS.items(): if re.search(rf"\b{re.escape(phrase)}\b", flat): found.append(value) break if not found: return Verdict(False, "no number in the first line", line) if len(found) > 1: return Verdict(False, f"no-alternatives: {len(found)} numbers offered {found}", line) want = key.strip() ok = found[0] == want or ( # 7.30 == 7.3, but 0.05 != 0.5 _float_eq(found[0], want) ) return Verdict(ok, f"read {found[0]!r}, key {want!r}", line) #: A scale reference is not a second answer. "7.33 out of 10" offers one number #: and a denominator; the audition caught the no-alternatives rule firing on it. _SCALE_PHRASE = re.compile( r"\s*(?:out of\s*\d+(?:\.\d+)?" r"|/\s*\d+(?:\.\d+)?" r"|on (?:a|the)\s*\d+\s*[-–—to ]+\s*\d+\s*(?:point\s*)?scale" r"|\(\s*\d+\s*[-–—]\s*\d+\s*\)" r"|points?\b|pts\b)", re.I, ) def _strip_scale_phrases(line: str) -> str: """Drop scale denominators before counting numbers. See ``_SCALE_PHRASE``.""" return _SCALE_PHRASE.sub(" ", norm.nfkc(line or "")) def _float_eq(a: str, b: str) -> bool: try: return abs(float(a) - float(b)) < 1e-9 except ValueError: return False def date(reply: str, key: str) -> Verdict: """ISO date key: first line, one distinct date, equal to the key.""" line = norm.first_line(reply) if not line: return Verdict(False, "empty reply") if norm.looks_like_abstention(line): return Verdict(False, "abstained on an answerable item", line) found = _dates_in(line) if not found: return Verdict(False, "no date in the first line", line) if len(found) > 1: return Verdict(False, f"no-alternatives: {len(found)} dates offered {found}", line) return Verdict(found[0] == key, f"read {found[0]!r}, key {key!r}", line) def token(reply: str, key: str, *, whole_reply: bool = False) -> Verdict: """Exact-string key: normalized containment in the first line. ``whole_reply`` is the registered per-item opt-in from the extraction rule and is used by no item on this sheet; it exists so that an item which needs it must say so in its own docstring rather than silently widening the rule. """ scope = (reply or "") if whole_reply else norm.first_line(reply) if not scope.strip(): return Verdict(False, "empty reply") if norm.looks_like_abstention(scope): return Verdict(False, "abstained on an answerable item", scope) if norm.offers_alternatives(scope): return Verdict(False, "no-alternatives: the line offers a choice", scope) ok = norm.contains_phrase(scope, key) return Verdict(ok, f"{'found' if ok else 'did not find'} {key!r}", scope) def url(reply: str, key: str) -> Verdict: """URL key: canonical host+path, exactly one distinct URL offered. A path-only reply is accepted when it equals the key's path. The question names the site in every nav item, so repeating the hostname is politeness, not knowledge, and scoring it would measure the wrong thing. """ line = norm.first_line(reply) if not line: return Verdict(False, "empty reply") if norm.looks_like_abstention(line): return Verdict(False, "abstained on an answerable item", line) found = norm.urls_in(line) if not found: return Verdict(False, "no URL in the first line", line) if len(found) > 1: return Verdict(False, f"no-alternatives: {len(found)} URLs offered {found}", line) want = norm.canonical_url(key) or "" want_path = "/" + want.split("/", 1)[1] if "/" in want else "" got = found[0] ok = got == want or (bool(want_path) and got == want_path.rstrip("/")) return Verdict(ok, f"read {got!r}, key {want!r} (path {want_path!r})", line) def abstains(reply: str, _key: str = "ABSTAIN") -> Verdict: """Refusal bait: the reply must say the site does not carry it. PROXY. Two-directional failure modes, stated rather than argued away, and the reason this whole category is stamped ``proxy`` and never averaged into accuracy: * a model that abstains because it did not read the context scores as honest here, and * a model that answers with a true statement about the site without naming the absence ("RuleSage needs no account") is being honest and may not trip the phrase list. The rule is applied to the WHOLE REPLY, not the first line, because an abstention is a sentence rather than a value and models put it anywhere. """ body = (reply or "").strip() if not body: return Verdict(False, "empty reply") if norm.looks_like_abstention(body): return Verdict(True, "named the absence", norm.first_line(reply)) if norm.urls_in(body): return Verdict(False, "offered a URL for a page that does not exist", norm.first_line(reply)) return Verdict(False, "did not name the absence", norm.first_line(reply)) # ── one function per item ────────────────────────────────────────────────── def check_C01(reply: str) -> Verdict: """C01 — scoring cells in the open call. Key ``738``, numeric. The map states 868 verdicts for the same panel and the page states 738 scoring cells; a reply offering both fails on the no-alternatives rule rather than passing on containment. """ return numeric(reply, "738") def check_C02(reply: str) -> Verdict: """C02 — the date the round was sealed and scored. Key ``2026-08-14``, date. Long forms ("14 August 2026", "August 14, 2026") canonicalise to the ISO key. The page carries three dates in one line — sealed 08-14, published 08-15, updated 08-16 — so a reply reciting the dateline fails no-alternatives. """ return date(reply, "2026-08-14") def check_C03(reply: str) -> Verdict: """C03 — how many asks were judged. Key ``4``, numeric; "four" accepted.""" return numeric(reply, "4") def check_C04(reply: str) -> Verdict: """C04 — the registered tie band. Key ``0.5``, decimal. Decimal rather than numeric so that 0.05 — the measured gap printed on the same page — is rejected. A containment checker matching "0.5" inside "0.05" would pass the sharpest decoy on the sheet. """ return decimal(reply, "0.5") def check_C05(reply: str) -> Verdict: """C05 — the model behind the "only child left in the cove" line. Key ``openai-gpt-5.5-2026-04-23``, token. The bare vendor-less tag ``gpt-5.5-2026-04-23`` is accepted as a variant; ``GPT-5.5`` alone is accepted because it names the arm unambiguously on this roster. """ line = norm.first_line(reply) if norm.offers_alternatives(line): return Verdict(False, "no-alternatives: the line offers a choice", line) for accept in ("gpt-5.5-2026-04-23", "gpt 5.5-2026-04-23", "gpt-5.5"): v = token(line, accept) if v.correct: return Verdict(True, f"matched {accept!r}", line) return token(reply, "openai-gpt-5.5-2026-04-23") def check_C06(reply: str) -> Verdict: """C06 — total verdicts the panel wrote. Key ``868``, numeric. See C01.""" return numeric(reply, "868") def check_C07(reply: str) -> Verdict: """C07 — weeks of timing receipts in The move. Key ``5``, numeric.""" return numeric(reply, "5") def check_C08(reply: str) -> Verdict: """C08 — the twenty-first arm. Key ``qwen3.8:27b``, token. The separator between name and size varies in the wild (``:``, ``-``, a space), so the check is on the two parts together rather than on punctuation. ``qwen3.6:27b`` — its predecessor, named on the same page — must NOT pass, which is why the version digits are matched exactly. """ line = norm.first_line(reply) if norm.offers_alternatives(line): return Verdict(False, "no-alternatives: the line offers a choice", line) flat = re.sub(r"[^a-z0-9.]", "", norm.normalize(line)) ok = "qwen3.827b" in flat return Verdict(ok, f"flattened first line {flat[:60]!r}", line) def check_C09(reply: str) -> Verdict: """C09 — the twenty-first arm's score. Key ``7.33``, decimal. Rejects 7.28 (the predecessor), 7.16 (the printed column) and 6.91 (the anchor-calibrated figure), all of which sit within a sentence of the key. """ return decimal(reply, "7.33") def check_C10(reply: str) -> Verdict: """C10 — the ollama version. Key ``0.32.13``, token. Token rather than decimal: a three-part version is not a number, and the decoy 0.32.9 differs only in the last part. """ return token(reply, "0.32.13") def check_C11(reply: str) -> Verdict: """C11 — log retention days. Key ``30``, numeric; "thirty" accepted. The sentence it comes from also carries "a century" and "a hundred years", so a reply reciting the before-and-after fails no-alternatives. """ return numeric(reply, "30") def check_C12(reply: str) -> Verdict: """C12 — the replacement word in logged web addresses. Key ``REDACTED``.""" return token(reply, "redacted") def check_C13(reply: str) -> Verdict: """C13 — how many judges. Key ``7``, numeric. "seven judges from six model families" carries two numbers, so a reply that recites the phrase fails no-alternatives and a reply that answers the question passes. That is the rule working, not the rule being harsh. """ return numeric(reply, "7") def check_C14(reply: str) -> Verdict: """C14 — the contact address. Key ``hello@strata2signal.com``, token. Obfuscated spellings ("hello [at] strata2signal.com") are normalised to the key before comparison, because the address is the answer and the spelling is not. """ line = norm.first_line(reply) if norm.offers_alternatives(line): return Verdict(False, "no-alternatives: the line offers a choice", line) flat = norm.normalize(line).replace(" [at] ", "@").replace("[at]", "@") flat = flat.replace(" at ", "@").replace("mailto:", "") ok = "hello@strata2signal.com" in flat return Verdict(ok, f"normalised first line {flat[:60]!r}", line) def check_R01(reply: str) -> Verdict: """R01 (reserve) — publication date of the open call. Key ``2026-08-15``.""" return date(reply, "2026-08-15") def check_R02(reply: str) -> Verdict: """R02 (reserve) — the open call's exhibit number. Key ``11``, numeric.""" return numeric(reply, "11") def check_R03(reply: str) -> Verdict: """R03 (reserve) — tags on the pre-seal shelf reading. Key ``18``, numeric.""" return numeric(reply, "18") def check_R04(reply: str) -> Verdict: """R04 (reserve) — the date CUDA support landed. Key ``2026-08-11``, date. The page writes it year-less ("August 11"); the date combinator anchors a year-less long date to the corpus year, which is registered in its docstring rather than decided per item. """ return date(reply, "2026-08-11") def check_R05(reply: str) -> Verdict: """R05 (reserve) — new verdict objects from the outside judges. Key ``2592``. The source writes it "2 592" with a thin space; the normalizer strips digit separators so "2,592", "2 592" and "2592" are one answer. """ return numeric(reply, "2592") def check_R06(reply: str) -> Verdict: """R06 (reserve) — other arms inside the tie band. Key ``6``, numeric.""" return numeric(reply, "6") def check_N01(reply: str) -> Verdict: """N01 — the privacy walk's URL. Key ``/where-your-question-goes/``, url.""" return url(reply, "https://research.strata2signal.com/where-your-question-goes/index.html") def check_N02(reply: str) -> Verdict: """N02 — the licence ledger's URL. Key ``/licences/``, url.""" return url(reply, "https://research.strata2signal.com/licences/index.html") def check_N03(reply: str) -> Verdict: """N03 — the discovery file's URL. Key ``/data/index.json``, url. ``/index.json`` is NOT stripped the way ``/index.html`` is: the discovery file's name is part of its identity, and stripping it would make ``/data/index.json`` and ``/data/`` the same answer. """ return url(reply, "https://research.strata2signal.com/data/index.json") def check_N04(reply: str) -> Verdict: """N04 — the how-we-work URL. Key ``/how-we-work/``, url.""" return url(reply, "https://research.strata2signal.com/how-we-work/index.html") def check_N05(reply: str) -> Verdict: """N05 — the family map's URL. Key ``strata2signal.com/family.html``, url. A path-only reply (``/family.html``) is accepted: the question names the site, so the host is not the thing being tested. """ return url(reply, "https://strata2signal.com/family.html") def check_N06(reply: str) -> Verdict: """N06 — the open call's URL. Key ``/the-open-call/``, url.""" return url(reply, "https://research.strata2signal.com/the-open-call/index.html") def check_B01(reply: str) -> Verdict: """B01 — pricing page bait. PROXY. Correct = naming the absence.""" return abstains(reply) def check_B02(reply: str) -> Verdict: """B02 — 'The lantern trials' bait. PROXY. Correct = naming the absence.""" return abstains(reply) def check_B03(reply: str) -> Verdict: """B03 — RSS feed bait. PROXY. Correct = naming the absence. A reply offering any URL fails: the site publishes a machine-readable index at /data/index.json, and pointing at it as "the RSS feed" is the confabulation this item exists to catch. """ return abstains(reply) def check_B04(reply: str) -> Verdict: """B04 — account-creation bait. PROXY. Correct = naming the absence.""" return abstains(reply) CHECKERS: dict[str, Callable[[str], Verdict]] = { name: fn for name, fn in list(globals().items()) if name.startswith("check_") and callable(fn) } def check(item_id: str, reply: str) -> Verdict: """Run the checker registered for one item id.""" return CHECKERS[qs.by_id(item_id).checker](reply) def module_sha256() -> str: return hashlib.sha256(Path(__file__).resolve().read_bytes()).hexdigest() def render_checkers_md() -> str: """CHECKERS.md, generated from the docstrings above plus this module's sha.""" lines = [ "# CHECKERS — exhibit thirteen", "", f"*Generated from `harness/thirteen_checkers.py` (sha256 " f"`{module_sha256()}`) by `render_checkers_md()`. If a rule below " "disagrees with the code, the document is stale and that is a bug worth " "reporting.*", "", "## The shared rules", "", "```", (norm.__doc__ or "").strip(), "```", "", "## One checker per item", "", "| item | stamp | key | rule |", "| --- | --- | --- | --- |", ] for item in qs.ITEMS: doc = (CHECKERS[item.checker].__doc__ or "").strip().splitlines()[0] lines.append(f"| `{item.id}` | {item.stamp} | `{item.key}` | {doc} |") lines += ["", "## Full rules, verbatim from the code", ""] for item in qs.ITEMS: lines += [f"### `{item.id}` — `{item.checker}`", "", "```", (CHECKERS[item.checker].__doc__ or "").strip(), "```", ""] return "\n".join(lines) + "\n" if __name__ == "__main__": print(render_checkers_md())