textmachine/eval/exp16/reaudit_14b.py

204 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()