"""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. * 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 == "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 + 0.004 and abs(res.get("naive_ci_high", 1e9) - r2["naive_ci_high"]) <= 0.006 + 0.004 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"): 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()