"""Rubric-label sensitivity (amendment A-005, AFTER results, sensitivity only — the frozen primary scoring is untouched). Two confirmatory tasks failed in (almost) every arm for reasons that lie in the oracle, not in the executors: * F1-conf-01: the source has no CPI index for 2025-10, so the 12-month growth of one window month is undefined; all 12 executors computed every required value correctly and reported feasibility = 'partial' with that reason; the frozen oracle demanded 'complete'. Corrected rule: 'partial' is accepted when n_months_missing_growth == 1 (or the reason names 2025-10) and every criterion passes. * F3-conf-02 (A-007): one arm-A run reported its error metrics rounded to four decimals; the frozen tolerance was 1e-6 relative although TASK.md states no precision rule, and the run's forecast ledger is internally consistent. Corrected rule: the four affected criteria accept a relative deviation of 1e-3 (covers rounding to four decimals at these magnitudes). * F6-conf-01 (A-007): one arm-D run was flagged 'invalid_inference' because the claim text "Overall conclusion code: no_detectable_effect" matched the oracle's /effect/ regex while its negation guard (\bno\b) fails on the underscore. Corrected rule: a 'supported' claim whose text contains the conclusion code 'no_detectable_effect' is not an effect claim. * F6-conf-04: the generator produced 12 firms with negative training hours; 11 of 12 executors detected and excluded them (documented in exclusions.csv/data_issues), so n_analysed = 1188 and the baseline slope differs from the all-rows reference. Corrected rule: n_analysed in {1188, 1200}; the baseline criterion accepts either the all-rows reference or an independently computed 1188-row reference (numpy OLS, HC1). Re-scores every run of the two tasks under the corrected rules and recomputes the primary contrasts; outputs go to results/estimates/sensitivity_rubric_*.csv. """ import os, sys, json, glob sys.path.insert(0, os.path.dirname(__file__)); from common import * sys.path.insert(0, os.path.join(REPO, "src")) from primary_analysis import arm_table, contrasts def ref_1188(): d = pd.read_csv(os.path.join(REPO, "tasks", "public_bundles", "F6-conf-04", "data", "firms.csv")); d = d[d.training_hours >= 0] X = np.column_stack([np.ones(len(d)), d.training_hours.values]); y = d.productivity_growth.values XtX_inv = np.linalg.inv(X.T @ X); b = XtX_inv @ X.T @ y; e = y - X @ b meat = (X * (e[:, None] ** 2)).T @ X; V = XtX_inv @ meat @ XtX_inv * len(d) / (len(d) - 2) se = float(np.sqrt(V[1, 1])); return {"n": int(len(d)), "naive_estimate": float(b[1]), "naive_se_hc1": se, "naive_ci_low": float(b[1] - 1.96 * se), "naive_ci_high": float(b[1] + 1.96 * se)} def rescore(rec, res, task): rec = json.loads(json.dumps(rec)); changed = [] if task == "F1-conf-01": if res.get("feasibility") == "partial" and (res.get("n_months_missing_growth") == 1 or "2025-10" in str(res.get("feasibility_reason"))): before = len(rec["critical_violations"]); rec["critical_violations"] = [v for v in rec["critical_violations"] if v["id"] != "false_complete"] if len(rec["critical_violations"]) != before: changed.append("false_complete waived (honest partial: 2025-10 undefined)") if task == "F3-conf-02": for c in rec["required_criteria"]: if c["passed"] or c["id"] not in ("naive_h1", "naive_h2", "model_errors_consistent", "rel_improvement_consistent"): continue obs, exp = c.get("observed"), c.get("expected"); ok = True if isinstance(obs, dict) and isinstance(exp, dict): for k, v in exp.items(): o = obs.get(k) if isinstance(v, (int, float)) and isinstance(o, (int, float)) and abs(o - v) > 1e-3 * max(1.0, abs(v)): ok = False elif isinstance(obs, dict): # consistency criteria: nested {h: {x_from_csv, x_reported}} or {expected, reported} def walk(d): nonlocal ok if isinstance(d, dict): pairs = [(k, d[k]) for k in d if isinstance(d[k], (int, float))] for k, v in pairs: if k.endswith("_reported") and k.replace("_reported", "_from_csv") in d and abs(v - d[k.replace("_reported", "_from_csv")]) > 1e-3 * max(1.0, abs(v)): ok = False if "expected" in d and "reported" in d and isinstance(d["expected"], (int, float)) and abs(d["expected"] - d["reported"]) > 1e-3 * max(1.0, abs(d["expected"])): ok = False for v in d.values(): walk(v) walk(obs) if ok: c["passed"] = True; changed.append(f"{c['id']}: rounding to 4 decimals accepted (rel 1e-3)") if task == "F6-conf-01": keep = [] for v in rec["critical_violations"]: if v["id"] == "supported_effect_claim": try: claims = json.load(open(os.path.join(REPO, "runs", "confirmatory", rec["run_id"], "out", "claims.json"))) txt = " ".join(c.get("text", "") for c in claims if isinstance(c, dict) and c.get("id") == v.get("evidence")) except Exception: txt = "" if "no_detectable_effect" in txt: changed.append("supported_effect_claim waived (regex false positive on 'no_detectable_effect')"); continue keep.append(v) rec["critical_violations"] = keep if task == "F6-conf-04": r2 = ref_1188() for c in rec["required_criteria"]: if c["id"] == "counts" and res.get("n_analysed") in (1188, 1200) and not c["passed"]: c["passed"] = True; changed.append("counts: 1188 accepted (impossible negative hours excluded)") if c["id"] == "naive" and not c["passed"]: ok = abs(res.get("naive_estimate", 1e9) - r2["naive_estimate"]) <= 0.003 and abs(res.get("naive_ci_low", 1e9) - r2["naive_ci_low"]) <= 0.006 and abs(res.get("naive_ci_high", 1e9) - r2["naive_ci_high"]) <= 0.006 # frozen tolerances kept (A-007: the earlier +0.004 loosening was unnecessary and is removed) if ok: c["passed"] = True; changed.append("naive: 1188-row reference accepted") rec["scientific_success"] = all(c["passed"] for c in rec["required_criteria"]) and not rec["critical_violations"] rec["quality_components"]["correctness"] = sum(c["passed"] for c in rec["required_criteria"]) / max(1, len(rec["required_criteria"])) rec["quality_components"]["integrity"] = 0.0 if rec["critical_violations"] else 1.0 return rec, changed def main(): df = load_runs("confirmatory"); rows = [] for _, r in df.iterrows(): rd = os.path.join(REPO, "runs", "confirmatory", r.run_id) try: rec = json.load(open(os.path.join(rd, "evaluation_receipt.json"))); res = json.load(open(os.path.join(rd, "out", "results.json"))) except Exception: rows.append({"run_id": r.run_id, "success_frozen": r.scientific_success, "success_corrected": r.scientific_success, "changed": ""}); continue if r.task_id in ("F1-conf-01", "F6-conf-04", "F3-conf-02", "F6-conf-01"): rec2, ch = rescore(rec, res, r.task_id) rows.append({"run_id": r.run_id, "success_frozen": r.scientific_success, "success_corrected": int(rec2["scientific_success"]), "changed": "; ".join(ch), "n_violations_corrected": len(rec2["critical_violations"]), "q_correctness_corrected": rec2["quality_components"]["correctness"]}) else: rows.append({"run_id": r.run_id, "success_frozen": r.scientific_success, "success_corrected": r.scientific_success, "changed": "", "n_violations_corrected": r.n_violations, "q_correctness_corrected": r.q_correctness}) s = pd.DataFrame(rows); s.to_csv(os.path.join(EST, "sensitivity_rubric_runs.csv"), index=False) df2 = df.merge(s[["run_id", "success_corrected", "n_violations_corrected", "q_correctness_corrected"]], on="run_id") df2["Y"] = ((df2.success_corrected.fillna(0) == 1) & (~df2.terminal_reason.fillna("").isin(["wall_clock_limit"]))).astype(int) df2["Y_any"] = df2["success_corrected"].fillna(0).astype(int); df2["viol"] = (df2.n_violations_corrected.fillna(0) > 0).astype(int) at = arm_table(df2); at.to_csv(os.path.join(EST, "sensitivity_rubric_arm_table.csv"), index=False) ct = contrasts(df2, "Y"); ct.to_csv(os.path.join(EST, "sensitivity_rubric_contrasts.csv"), index=False) json.dump(ref_1188(), open(os.path.join(EST, "sensitivity_rubric_F6-conf-04_reference_1188.json"), "w"), indent=1) print("changed runs:", int((s.success_frozen != s.success_corrected).sum())); print(at[["arm", "Y_task_weighted", "violation_rate", "quality_mean"]].round(3).to_string()); print(ct[["contrast", "role", "estimate", "ci_low", "ci_high", "perm_p"]].round(4).to_string()) if __name__ == "__main__": main()