#!/usr/bin/env python3 """exp15/Q4a — SECONDARY grok+mistral judge mini-pass (design-compliance + judge-floor calibration ON the meaning-traps). The $0 DET scorer (q4a_score.py) is PRIMARY and floor-immune; this pass exists to (1) honour the frozen judge design (grok-4.3 reasoning-ON + mistral-large-latest, both orders, 6->10, LOO, catastrophe, per-vote persist) and (2) MEASURE how well judges even see these traps — does the judge noise-floor (which swamped every S2' block at 0.126) also swamp DET-obvious meaning errors? Reuses judges.judge_cell + exp15_llm verbatim (NO edits to those rigs). Contrasts (--contrast): main : a0=flash-raw(s0), arm=pro-raw(s0) judge's pro-vs-flash draft signal (compare to DET) floor : a0=flash-raw(s0), arm=flash-raw(s1) two seeds of the SAME model = judge-noise + same-model seed-content-variance UPPER BOUND (review F11): it is pure judge noise ONLY where the two seeds DET-agree (cross-referenced against q4a_score seed_floor). editor : a0=pro-raw(s0), arm=pro-glm(s0) judge's editor-delta (does editor break the draft) Judge inputs are WINDOWED around the trap's phenomenon sentence (+/-700 chars, symmetric — flash and pro translate the SAME source so the sentence sits at ~the same position; addendum §4 authorises windowing for same-chunk symmetric contrasts) with a full-text fallback when the sentence isn't found. This keeps the phenomenon visible while holding per-cell cost far under the per-call cap (a full ~5k draft would push predict near the cap and could truncate the run mid-way). Budget: OWN Spender, ledger q4a_judge_costs.jsonl, seed_total = current Q4a GEN spend (so the hard cap is the GLOBAL $2 Q4a ceiling across gen+judge, NOT the exp15 $14.5). budget_truncated cells are DROPPED from the analysis (review F3): a cell whose votes were cut off by the cap is partial/asymmetric and must not enter the summary. per_call_cap kept high enough that ONLY the hard-cap governs (review F8). Per-call gate + every completed vote persisted (judge gate-blocks are $0 and unledgered in the reused judges.py — review F9, documented; the cutoff is recorded via budget_truncated in the results JSON).""" from __future__ import annotations import argparse import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import exp15_llm as L import q4a_traps as Q from judges import judge_cell SD = Path("/home/ubuntu/books/gu-zhenren/exp15") OUT = SD / "q4a" GEN_LEDGER = SD / "q4a_gen_costs.jsonl" JUDGE_LEDGER = SD / "q4a_judge_costs.jsonl" Q4A_CAP = 2.0 # GLOBAL Q4a ceiling ($; own budget, OUTSIDE exp15 $15 — orchestrator) SEEDS = [0, 1, 2] CONTRAST = { # name -> (a0 cell, arm cell); the SEED is chosen per-trap (see _pick_seed) "main": ("flash_raw", "pro_raw"), # judge's pro-vs-flash draft signal "floor": ("flash_raw", "flash_raw"), # same model, two DIFFERENT seeds -> noise+seed-variance floor "editor": ("pro_raw", "pro_glm"), # judge's editor-delta } def _read(cell, cid, seed): p = OUT / cell / f"{cid}.s{seed}.txt" return p.read_text(encoding="utf-8") if p.exists() else None def _located(cell, cid, seed, trap): txt = _read(cell, cid, seed) return bool(txt) and bool(Q.sentence_with(txt, trap["locator"])) def _pick_seed(a0cell, armcell, trap, floor): """Per-trap seed selection: judge the meaning GIVEN both sides RENDERED the phenomenon (omission is a separate DET-coverage metric). Return (a0seed, armseed) or None. For 'floor', a0/arm are the same cell -> pick two DISTINCT seeds both rendering the trap (else the floor conflates omission with judge noise).""" cid = trap["chunk"] if floor: rs = [s for s in SEEDS if _located(a0cell, cid, s, trap)] return (rs[0], rs[1]) if len(rs) >= 2 else None for s in SEEDS: # same seed both sides, both rendering if _located(a0cell, cid, s, trap) and _located(armcell, cid, s, trap): return (s, s) return None def _window(text, trap, half=700): """Symmetric +/-half window around the trap's phenomenon sentence; full text if not located.""" sent = Q.sentence_with(text, trap["locator"]) if not sent: return text probe = sent[:40] i = text.find(probe) if i < 0: return text lo, hi = max(0, i - half), min(len(text), i + len(sent) + half) return text[lo:hi] def _gen_spent(): t = 0.0 if GEN_LEDGER.exists(): for line in GEN_LEDGER.read_text(encoding="utf-8").splitlines(): try: t += json.loads(line).get("cost", 0.0) except Exception: pass return t def run(contrast, per_call_cap, limit=0): traps = Q.traps() if limit: traps = traps[:limit] a0cell, armcell = CONTRAST[contrast] gen = _gen_spent() sp = L.Spender(str(JUDGE_LEDGER), per_call_cap=per_call_cap, hard_cap=Q4A_CAP, seed_total=gen) print(f"[budget] Q4a gen spent ${gen:.4f}; judge ledger start ${sp.total:.4f}; global cap ${Q4A_CAP}; " f"contrast={contrast} (a0={a0cell} arm={armcell}, seed chosen per-trap where both render)") out, skipped, truncated = [], [], [] for t in traps: pick = _pick_seed(a0cell, armcell, t, floor=(contrast == "floor")) if pick is None: skipped.append((t["id"], "no-seed-both-render")); continue a0seed, armseed = pick a0_full = _read(a0cell, t["chunk"], a0seed) arm_full = _read(armcell, t["chunk"], armseed) det_a0, _ = Q.det_score(a0_full, t) det_arm, _ = Q.det_score(arm_full, t) agg = judge_cell(sp, Q.judge_trap(t), a0_text=_window(a0_full, t), arm_text=_window(arm_full, t)) if agg.get("budget_truncated"): # DROP a cap-truncated (partial/asymmetric) cell — do NOT let it enter the analysis (review F3) truncated.append(t["id"]) print(f" [HARD-CAP] Q4a ${Q4A_CAP} bound at {t['id']} — cell dropped, stopping."); break agg.update(contrast=contrast, id=t["id"], cat_class=t["catastrophe_class"], a0_seed=a0seed, arm_seed=armseed, det_a0=det_a0, det_arm=det_arm) out.append(agg) print(f" {contrast} {t['id']} (s{a0seed}/s{armseed}): judge diff(arm-a0) {agg['paired_diff_arm_minus_a0']} " f"(a0_acc {agg['a0_accuracy']} arm_acc {agg['arm_accuracy']}; DET a0={det_a0} arm={det_arm})" f" spend ${sp.total:.4f}") resfile = SD / f"q4a_judge_results_{contrast}.json" resfile.write_text(json.dumps({"contrast": contrast, "n": len(out), "skipped": skipped, "truncated_dropped": truncated, "results": out}, ensure_ascii=False, indent=2), encoding="utf-8") # quick summary decided = [a for a in out if a["paired_diff_arm_minus_a0"] is not None] if decided: import statistics diffs = [a["paired_diff_arm_minus_a0"] for a in decided] print(f"\n{contrast}: n={len(decided)} mean diff {round(statistics.mean(diffs),3)} " f"mean|diff| {round(statistics.mean(abs(d) for d in diffs),3)} spend ${sp.total:.4f}") print(f"{contrast}: {len(out)} scored, {len(skipped)} skipped {skipped}, " f"{len(truncated)} cap-dropped {truncated}") def main(): ap = argparse.ArgumentParser() ap.add_argument("--contrast", choices=list(CONTRAST), required=True) # per-call cap high enough that ONLY the $2 hard-cap governs (windowed inputs predict ~$0.03; review F8) ap.add_argument("--per-call-cap", type=float, default=0.09) ap.add_argument("--limit", type=int, default=0) a = ap.parse_args() run(a.contrast, a.per_call_cap, a.limit) if __name__ == "__main__": main()