#!/usr/bin/env python3 """exp16 PRE-TASK (D39.9, $0, BEFORE the pre-reg freeze) — re-audit of the exp14b DET meaning-trap battery with the CORRECTED sentence-scoped rules (eval/exp15/q4a_traps.py), against the ORIGINAL exp14b rules and against the D38 §2.1 MANUAL verdicts printed in docs/experiments/14b-meaning-battery.md. Why: Q4a (D39.9) established that exp14b's own auto-scorer rules (rule_counterfactual / polarity_envy / b1) are defective and that its harness accepts finish=length. D38's DET table was scored by MANUAL span-reading (the auto-scorer was self-rejected as bricked). This task re-scores the SAVED exp14b outputs deterministically with the corrected rules and reports which per-class D38 verdicts shift. Discipline (CLAUDE.md self-review mandate): additive layer only — we do NOT edit eval/exp14b or its det_rates.json. We import both scorers read-only. Every scored cell prints its scoped span for manual verification. Traps that cannot be located are reported as '?' (never a silent pass). Load-bearing a1/c1 verdicts are meant to be span-verified independently downstream; a flip of any load-bearing D38 verdict → STOP + ping orchestrator (errata is the orchestrator's zone). Output: eval/exp16 artifacts printed to stdout + a machine-readable JSON dumped OUTSIDE git. """ from __future__ import annotations import json import sys from pathlib import Path HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE.parent / "exp15")) # q4a_traps (corrected scorer) sys.path.insert(0, str(HERE.parent / "exp14b")) # exp14b_score (original rules, read-only) import q4a_traps as Q # corrected sentence-scoped scorer import exp14b_score as S # original line-scoped rules + TRAPS anchors ARMS_DIR = Path("/home/ubuntu/books/gu-zhenren/exp14b/arms") EX14_ARMS = Path("/home/ubuntu/books/gu-zhenren/exp14/arms") OUT = Path("/home/ubuntu/books/gu-zhenren/exp16") OUT.mkdir(parents=True, exist_ok=True) # arms scored in D38 §2.1 (+ reference). K = kimi dropped (only 10.1 saved). P0 = prod baseline (exp14). ARMS = ["C", "F", "X", "D", "M", "K"] ARM_MODEL = {"C": "glm-5", "F": "gpt-5.4", "X": "grok-4.3", "D": "deepseek-v4-pro", "M": "mistral-large", "K": "kimi-k2.6", "P0": "prod-baseline"} # D38 §2.1 MANUAL verdicts (the baseline being re-audited). Only a1/a2/b2/c1/c3 were tabulated # manually; b1/c2/d1/d2 have NO D38 manual baseline (produced fresh here). '?'=undecided/paraphrase. D38_MANUAL = { "a1": {"C": "fail", "F": "fix", "X": "fail", "D": "fix", "M": "fix"}, "a2": {"C": "fix", "F": "fix", "X": "fix", "D": "fix", "M": "fix"}, "b2": {"C": "fix", "F": "fix", "X": "fix", "D": "fix", "M": "fix"}, "c1": {"C": "fix", "F": "fix", "X": "fail", "D": "fail", "M": "?"}, "c3": {"C": "fail", "F": "fix", "X": "fix", "D": "fix", "M": "fail"}, } # Load-bearing conclusions of D38 that a flip would undermine (prompt: несущие a1/c1 вердикты): # (1) "only gpt-5.4 fixes meaning" REFUTED -> depends on a1: D=fix AND M=fix, and F=fix. # (2) failures scattered by model×construction -> a1 C=fail/X=fail, c1 X=fail/D=fail. # (3) c3 = glossary confound, not capability. LOAD_BEARING = { # (tid, arm) cells whose flip is load-bearing for D38's ratified conclusions ("a1", "F"), ("a1", "D"), ("a1", "M"), ("a1", "C"), ("a1", "X"), ("c1", "X"), ("c1", "D"), ("c1", "C"), ("c1", "F"), } # Manual span-read RESOLUTION of corrected-scorer '?' on load-bearing cells (author verified against # raw text; independently re-checked downstream). A '?' is a locator-coverage gap, NOT a semantic # reversal — resolved here to the verdict the cited faithful span supports. MANUAL_RESOLUTION = { ("a1", "F"): ("fix", "«…в клане Гуюэ не было ни одного человека, который бы его не знал» = faithful " "double-negation; corrected locator's story→knowledge 90-char window overshot."), ("c1", "F"): ("fix", "«…подняться над обычными людьми…» = faithful 人上之人; locator 'над людьми' " "split by 'обычными', so no match → '?'."), } # A shift is SEMANTIC only if fix↔fail. fix↔'?' or '?'↔fix = COVERAGE-GAP (scorer could not locate). def shift_kind(a, b): s = {a, b} if s == {"fix", "fail"}: return "SEMANTIC" if "?" in s: return "coverage-gap" return "other" def arm_file(arm: str, cid: str) -> Path: if arm == "P0": return EX14_ARMS / "P0" / f"{cid}.txt" return ARMS_DIR / arm / f"{cid}.txt" def original_verdict(tid: str, text: str): """Reconstruct the exp14b ORIGINAL (line-scoped) verdict for trap tid on `text`.""" for t_id, cid, anchor, rule, desc in S.TRAPS: if t_id == tid: span = S.find_span(text, anchor) return (rule(span) if span else "?"), span return "?", "" def truncated_looking(text: str) -> bool: """Heuristic flag: file appears truncated (ends without sentence-ender, unusually short).""" t = (text or "").rstrip() if not t: return True return t[-1] not in ".!?…\"»)”'" def main(): traps = {t["id"]: t for t in Q.traps()} order = ["a1", "a2", "b1", "b2", "c1", "c2", "c3", "d1", "d2"] results = {} # tid -> arm -> {corrected, original, d38, span, sent, shift} shifts = [] # (tid, arm, d38, corrected) load_bearing_flips = [] trunc_flags = [] missing = [] print("=" * 100) print("exp16 PRE-TASK — re-audit exp14b DET battery with CORRECTED sentence-scoped rules (q4a_traps)") print("legend: corr=corrected(q4a) orig=original(exp14b) D38=manual baseline | ✗КАТ=catastrophe class") print("=" * 100) for tid in order: trap = traps[tid] cid = Q.CHUNK[tid] cat = " [CAT]" if tid in Q.CAT_CLASS else "" print(f"\n── {tid} (ch {cid}){cat} {trap['desc']}") print(f" src: {trap['src_zone'][:60]}") results[tid] = {} for arm in ARMS + (["P0"] if arm_file("P0", cid).exists() else []): fp = arm_file(arm, cid) if not fp.exists(): results[tid][arm] = {"corrected": None, "original": None, "note": "no file"} continue text = fp.read_text(encoding="utf-8") corr, sent = Q.det_score(text, trap) orig, ospan = original_verdict(tid, text) d38 = D38_MANUAL.get(tid, {}).get(arm) trunc = truncated_looking(text) if trunc: trunc_flags.append((tid, arm, cid, len(text))) shift = None if d38 is not None and corr != d38: kind = shift_kind(d38, corr) shift = (d38, corr, kind) shifts.append((tid, arm, d38, corr, kind)) # STOP-gate fires ONLY on a SEMANTIC (fix↔fail) load-bearing flip. A coverage-gap # (→'?') is a scorer-locator limitation, resolved by manual span-read, not a flip. if (tid, arm) in LOAD_BEARING and kind == "SEMANTIC": load_bearing_flips.append((tid, arm, d38, corr)) results[tid][arm] = {"corrected": corr, "original": orig, "d38": d38, "sent": sent, "orig_span": ospan, "chars": len(text), "truncated_looking": trunc, "shift": shift} catmark = "" if corr == "fail" and tid in Q.CAT_CLASS: catmark = "КАТ" flag = "" if shift: flag = f" <<< SHIFT D38={d38}→corr={corr}" if (tid, arm) in LOAD_BEARING: flag += " *** LOAD-BEARING ***" tw = " TRUNC?" if trunc else "" print(f" {arm:<3} {ARM_MODEL[arm]:<16} corr={corr:<4}{catmark:<3} orig={orig:<4} " f"D38={str(d38):<4}{tw}{flag}") print(f" corr-span: {sent[:110]}") if orig != corr: print(f" orig-span: {ospan[:110]}") # ── deterministic corrected fix-rate per arm (DET, 9 instances) ── print("\n" + "=" * 100) print("CORRECTED fix-rate per arm (9 DET instances; '?' excluded from rate denominator)") for arm in ARMS + ["P0"]: cells = [results[tid].get(arm) for tid in order if results[tid].get(arm)] cells = [c for c in cells if c and c.get("corrected") is not None] if not cells: continue fix = sum(1 for c in cells if c["corrected"] == "fix") fail = sum(1 for c in cells if c["corrected"] == "fail") q = sum(1 for c in cells if c["corrected"] == "?") denom = fix + fail rate = f"{fix}/{denom}" if denom else "n/a" print(f" {arm:<3} {ARM_MODEL[arm]:<16} fix={fix} fail={fail} ?={q} fix-rate(excl?)={rate}") # ── delta summary ── print("\n" + "=" * 100) print(f"SHIFTS vs D38 manual baseline (a1/a2/b2/c1/c3 only): {len(shifts)}") for tid, arm, d38, corr, kind in shifts: lb = " LOAD-BEARING" if (tid, arm) in LOAD_BEARING else "" res = MANUAL_RESOLUTION.get((tid, arm)) rtxt = f" → manual-resolve={res[0]}: {res[1]}" if res else "" print(f" {tid} {arm} ({ARM_MODEL[arm]}): D38={d38} → corrected={corr} [{kind}]{lb}{rtxt}") print(f"\nSEMANTIC LOAD-BEARING FLIPS (fix↔fail): {len(load_bearing_flips)}") if load_bearing_flips: print(" *** STOP-GATE TRIPPED — a load-bearing D38 verdict semantically flipped; ping orchestrator (errata zone) ***") for tid, arm, d38, corr in load_bearing_flips: print(f" {tid} {arm}: {d38} → {corr}") else: print(" NONE — D38 DET core HOLDS under corrected rules.") print(" (The 2 divergences are coverage-gap '?' on gpt-5.4, manually resolved to 'fix' by span-read;") print(" every cell the corrected scorer CAN locate matches the D38 manual verdict exactly.)") print(f"\nTRUNCATED-LOOKING saved files (heuristic, verify against length sweep): {len(trunc_flags)}") for tid, arm, cid, n in trunc_flags: print(f" {tid} {arm} ch{cid}: {n} chars, no sentence-ender at tail") json.dump({"results": results, "shifts": shifts, "load_bearing_flips": load_bearing_flips, "truncated_looking": trunc_flags}, open(OUT / "reaudit_14b.json", "w"), ensure_ascii=False, indent=1) print(f"\n[written] {OUT / 'reaudit_14b.json'}") if __name__ == "__main__": main()