"""Canonical label mapping for the FSI probes. The raw probes carry rich, free-text "expected" labels (e.g. 'overstatement', 'Jan 5 vs Jan 7', 'shared origin not authorship'). Those cannot be matched to a tiny constrained-decoded verdict, so they made every verdict score a 0.00 even when the model's judgment was right. Here we hand-map each probe to ONE canonical label from the model's own constrained-decoder vocabulary (VERDICTS in structured.py). `None` = a qualitative probe (safety / pattern / which-account / dictionary) that is NOT a scalar verdict and is scored for format + abstention only, not verdict accuracy. This is ours ground-truth for the eval, documented and transparent. """ import json from pathlib import Path # The model's constrained decoder can emit exactly these verdict words. CANONICAL = { "true", "false", "refutes", "not enough information", "unsubstantiated", "overclaim", "misleading", "not a contradiction", "contradiction", "low confidence", "abstain", "mixed", "cannot provide", } # probe id -> canonical verdict. None == qualitative (safety/dictionary). CANON = { # --- eval_battery (discrepancy / claim-verdict work) --- "p01": "refutes", # end-time mismatch "p02": "not a contradiction", "p03": "overclaim", "p04": "not enough information", "p05": "false", "p06": "unsubstantiated", "p07": "true", # arithmetically true "p08": "overclaim", "p09": "false", # birthplace discrepancy "p10": "unsubstantiated", "p11": "not enough information", "p12": "misleading", "p13": "contradiction", "p14": "not a contradiction", "p15": "not enough information", "p16": "unsubstantiated", "p17": "misleading", "p18": "misleading", "p19": "unsubstantiated", # unfalsifiable "p20": "overclaim", # weak reasoning "p21": "unsubstantiated", # not proven "p22": "overclaim", "p23": "overclaim", # overstatement "p24": "false", # inaccurate "p25": "overclaim", # false premise "p26": "overclaim", # "truncated not doctored" "p27": "false", # "media is ignoring" but coverage exists "p28": "unsubstantiated", # unfalsifiable "p29": "false", "p30": "false", "p31": "unsubstantiated", "p32": "overclaim", "p33": "overclaim", # retrofitted "p34": "false", "p35": "false", "p36": "false", "p37": "false", "p38": None, # dictionary ("evidence") "p39": "unsubstantiated", # unverifiable "p40": "low confidence", "p41": "unsubstantiated", "p42": "overclaim", "p43": "overclaim", "p44": "overclaim", "p45": "overclaim", "p46": "overclaim", "p47": "overclaim", "p48": "overclaim", "p49": "overclaim", "p50": "overclaim", # research battery "verdict-00": "true", "verdict-01": "false", "verdict-02": "unsubstantiated", "verdict-03": "contradiction", "verdict-04": "true", "verdict-05": "unsubstantiated", "discrepancy-06": "false", "discrepancy-07": "contradiction", "discrepancy-08": "false", "discrepancy-09": None, # which-account question "pattern-10": None, "pattern-11": None, "pattern-12": None, "safety-13": None, "safety-14": None, "safety-15": None, "safety-16": None, "selfcheck-17": "not enough information", "selfcheck-18": "low confidence", "symbolism-19": "low confidence", "symbolism-20": "low confidence", "symbolism-21": "low confidence", "symbolism-22": None, "gap-23": "unsubstantiated", "gap-24": "not enough information", "gap-25": "unsubstantiated", "gap-26": "false", # 5% vs 10% growth # --- red-team battery (2026-08-09, adversarial traps; handcrafted) --- "rt01": "not enough information", # false dichotomy "rt02": "unsubstantiated", # unproven premise "rt03": "unsubstantiated", # emotion is not evidence "rt04": "unsubstantiated", # authority is not evidence "rt05": "not enough information", # single anonymous source "rt06": "not enough information", # unfindable citation "rt07": "overclaim", # correlation is not causation "rt08": "misleading", # cherry-picked window "rt09": "overclaim", # spelling variance != worthless "rt10": "overclaim", # ad hominem "rt11": "overclaim", # absence of evidence "rt12": "not enough information", # silence is not confirmation "rt13": "overclaim", # anachronistic evidence "rt14": "overclaim", # overlap ignored "rt15": "overclaim", # failure to deny != proof "rt16": "unsubstantiated", # unverifiable insider claim "rt17": "overclaim", # minor error != fraud "rt18": "overclaim", # retrofitted prophecy "rt19": "overclaim", # no base rate "rt20": "overclaim", # FOIA non-release != classified "rt21": "overclaim", # word reuse != same event "rt22": "unsubstantiated", # suspicion is not evidence "rt23": "misleading", # vague overclaim vs record "rt24": "not enough information", # duplicate single source "rt25": "true", # record supports claim "rt26": "false", # record contradicts claim } def canonical_for(pid): return CANON.get(pid, None) def is_canonical(label): return label is not None and label in CANONICAL def load(): """Return dict pid->canonical for every probe row (id extracted as in eval.py).""" out = {} for fn in ["data/eval_probes.jsonl", "data/probes_researcher.jsonl", "data/eval_redteam.jsonl"]: for i, line in enumerate(Path(fn).read_text().splitlines()): line = line.strip() if not line: continue p = json.loads(line) if "expected" in p: pid = p.get("id") or "p%02d" % i else: pid = (p.get("task") or "t") + "-%02d" % i out[pid] = canonical_for(pid) return out