#!/usr/bin/env python3 """Run ELECTION-01 integrity negative controls (contract §5). Attach outputs. MODEL_CHANGE=NO. No real fitting. No L1/M1 scoring. Stub/frozen predictor only. Writes PASS/FAIL report JSON with hashes under docs/election01/ and data/. """ from __future__ import annotations import hashlib import json import sys import traceback from pathlib import Path ROOT = Path(__file__).resolve().parents[2] SCRIPTS = Path(__file__).resolve().parent sys.path.insert(0, str(SCRIPTS)) from fit_eval_interface import ( # noqa: E402 StubFitter, canonical_json, prove_label_mutation_invariant, sha256_hex, ) from integrity_gates import ( # noqa: E402 PollSample, assert_no_train_test_cross, assert_train_before_test, collapse_overlapping_samples, identical_eligible_test_cycles, reject_future_release, reject_unavailable_revision, utc_now_iso, ) OUT_DIR = ROOT / "docs" / "election01" DATA_DIR = ROOT / "data" / "election01" / "negative_controls" INTEGRITY_CONTRACT = OUT_DIR / "INTEGRITY_CONTRACT.md" EXPECTED_INTEGRITY_SHA256 = ( "145046ff15faa3f6d71e07c817f1d68b4a8addcd7bafea6dbb98a44a0ba4227b" ) def _case(name: str, fn): try: detail = fn() passed = bool(detail.get("pass")) return { "id": name, "pass": passed, "detail": detail, "error": None, } except Exception as e: # noqa: BLE001 — harness must always emit report return { "id": name, "pass": False, "detail": {}, "error": f"{type(e).__name__}: {e}", "traceback": traceback.format_exc(limit=5), } def nc1_future_release_rejected() -> dict: cutoff = "2020-09-03" # T-60 America/New_York EOD proxy for 2020 cycle # Should reject r1 = reject_future_release("poll_future", "2020-10-15", cutoff) r2 = reject_future_release("feature_post_election", "2020-11-10", cutoff) # Should accept r3 = reject_future_release("poll_ok", "2020-08-20", cutoff) r4 = reject_future_release("poll_on_cutoff", "2020-09-03", cutoff) decisions = [r1, r2, r3, r4] expected = { "poll_future": False, "feature_post_election": False, "poll_ok": True, "poll_on_cutoff": True, } ok = all(d.accepted == expected[d.record_id] for d in decisions) return { "pass": ok, "cutoff": cutoff, "decisions": [ {"record_id": d.record_id, "accepted": d.accepted, "reason": d.reason} for d in decisions ], "expected": expected, } def nc2_unavailable_revision_rejected() -> dict: cutoff = "2016-09-08" r1 = reject_unavailable_revision("rev_late", "2017-01-15", cutoff) r2 = reject_unavailable_revision("rev_missing", None, cutoff) r3 = reject_unavailable_revision( "rev_flagged", "2016-01-01", cutoff, known_unavailable=True ) r4 = reject_unavailable_revision("rev_ok", "2016-06-01", cutoff) decisions = [r1, r2, r3, r4] expected = { "rev_late": False, "rev_missing": False, "rev_flagged": False, "rev_ok": True, } ok = all(d.accepted == expected[d.record_id] for d in decisions) return { "pass": ok, "cutoff": cutoff, "decisions": [ {"record_id": d.record_id, "accepted": d.accepted, "reason": d.reason} for d in decisions ], "expected": expected, } def nc3_held_out_labels_cannot_alter_preds() -> dict: fitter = StubFitter( feature_registry=["poll_avg_t60", "incumbent_party", "unemp_gap", "fundamentals"], seed=20260910, ) train_ids = ["2008", "2012"] train_labels = {"2008": 7.3, "2012": 3.9} test_ids = ["2016", "2020"] labels_a = {"2016": 2.1, "2020": -4.5} labels_b = {"2016": 99.0, "2020": -99.0} # wildly mutated held-out labels result = prove_label_mutation_invariant( fitter, train_election_ids=train_ids, train_labels=train_labels, test_election_ids=test_ids, held_out_labels_a=labels_a, held_out_labels_b=labels_b, ) return {"pass": result["ok"], **result} def nc4_no_election_crosses_boundary() -> dict: good = assert_no_train_test_cross( train_elections=["1968", "1972", "1976", "1980", "1984", "1988", "1992", "1996", "2000", "2004", "2008", "2012"], test_elections=["2016"], ) bad = assert_no_train_test_cross( train_elections=["2008", "2012", "2016"], test_elections=["2016", "2020"], ) # Harness expects: good fold passes gate; leaky fold is detected (ok=False) detected = (good["ok"] is True) and (bad["ok"] is False) and bad["overlap"] == ["2016"] return { "pass": detected, "good_fold": good, "leaky_fold_detected": bad, "rule": "train ∩ test must be empty; leaky fold must be rejected", } def nc5_overlapping_samples_not_independent() -> dict: """Verified-share collapse + independent coincident-date houses stay separate. Codex f4c7748b: same fieldwork dates are NOT evidence of respondent overlap. """ polls = [ PollSample("p1", "sample_X", "2020-08-01", "2020-08-05", house="A", source="srcA"), PollSample("p2", "sample_X", "2020-08-03", "2020-08-07", house="A", source="srcA"), # same scoped sample PollSample("p3", "sample_Y", "2020-08-10", "2020-08-12", house="B", source="srcB"), PollSample("p4", "sample_Y", "2020-08-20", "2020-08-22", house="B", source="srcB"), PollSample("p5", "sample_Z", "2020-09-01", "2020-09-02", house="C", source="srcC"), # Distinct sample / house with overlapping fieldwork — do NOT auto-merge PollSample("p6", "sample_W", "2020-08-04", "2020-08-06", house="D", source="srcD"), # Independent HouseA/HouseB coincident dates (adversarial fixture) PollSample("p7", "sA", "2020-08-20", "2020-08-22", house="HouseA", source="pollsterA"), PollSample("p8", "sB", "2020-08-20", "2020-08-22", house="HouseB", source="pollsterB"), # Verified duplicate across keys SHOULD merge PollSample( "p9", "sDup", "2020-08-20", "2020-08-22", house="HouseA", source="pollsterA", duplicate_of="pollsterA::sA", ), ] collapsed = collapse_overlapping_samples(polls) # Expected independent units: # srcA::X → 1, srcB::Y → 1, srcC::Z → 1, srcD::W → 1, # pollsterA::{sA,sDup} → 1, pollsterB::sB → 1 => 6 ok = ( collapsed["raw_poll_count"] == 9 and collapsed["independent_count"] == 6 and collapsed["no_false_independence"] and collapsed["merge_rule"] == "verified_shared_only" ) # Adversarial: HouseA/HouseB same dates remain 2 (via their groups) house_indep = [ g for g in collapsed["groups"] if any(k.endswith("::sA") or k.endswith("::sB") or k.endswith("::sDup") for k in g["sample_keys"]) ] # sA+sDup one group, sB another → 2 groups covering those ab_groups = [ g for g in collapsed["groups"] if any("sA" in k or "sB" in k or "sDup" in k for k in g["sample_keys"]) ] ok = ok and len(ab_groups) == 2 return { "pass": ok, "raw_poll_count": collapsed["raw_poll_count"], "independent_count": collapsed["independent_count"], "independent_poll_ids": collapsed["independent_poll_ids"], "groups": collapsed["groups"], "uncertain_dependence": collapsed.get("uncertain_dependence"), "rule": "merge only verified shared/tracker/duplicate; coincident dates flag uncertain, do not merge", "adversarial_independent_same_dates_count": len(ab_groups), } def nc7_timezone_aware_release_and_revision() -> dict: """Codex adversarial: cutoff ET EOD vs Pacific release same calendar day.""" cutoff = "2024-09-06T23:59:59-04:00" release = "2024-09-06T23:30:00-07:00" # 2h30m01s AFTER cutoff # same-instant different zone should accept same_instant_cut = "2024-09-06T23:59:59-04:00" same_instant_rel = "2024-09-06T20:59:59-07:00" # just-before / just-after just_before = reject_future_release("just_before", "2024-09-06T23:59:58-04:00", cutoff) just_after = reject_future_release("just_after", "2024-09-07T00:00:00-04:00", cutoff) late = reject_future_release("late_release_tz", release, cutoff) late_rev = reject_unavailable_revision("late_revision_tz", release, cutoff) same = reject_future_release("same_instant", same_instant_rel, same_instant_cut) ok = ( late.accepted is False and late_rev.accepted is False and same.accepted is True and just_before.accepted is True and just_after.accepted is False ) return { "pass": ok, "late_release_timezone": {"accepted": late.accepted, "expected": False, "pass": not late.accepted}, "late_revision_timezone": {"accepted": late_rev.accepted, "expected": False, "pass": not late_rev.accepted}, "same_instant_different_zone": {"accepted": same.accepted, "expected": True, "pass": same.accepted}, "just_before": {"accepted": just_before.accepted, "expected": True}, "just_after": {"accepted": just_after.accepted, "expected": False}, "fixture": {"cutoff": cutoff, "release": release}, } def nc8_train_before_test_chronology() -> dict: """Disjointness alone is insufficient; require max(train) dict: eligible = ["2008", "2012", "2016", "2020"] match = identical_eligible_test_cycles(eligible, list(eligible)) mismatch = identical_eligible_test_cycles(eligible, ["2008", "2012", "2016"]) # ordered identity required reorder = identical_eligible_test_cycles(eligible, ["2020", "2008", "2012", "2016"]) ok = match["ok"] and (not mismatch["ok"]) and (not reorder["ok"]) and reorder["equal_as_sets"] return { "pass": ok, "matched_comparison": match, "mismatched_comparison_detected": mismatch, "reordered_rejected": reorder, "rule": "model comparisons must share identical ordered eligible test cycles", } def file_sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def main() -> int: DATA_DIR.mkdir(parents=True, exist_ok=True) OUT_DIR.mkdir(parents=True, exist_ok=True) integrity_sha = file_sha256(INTEGRITY_CONTRACT) if INTEGRITY_CONTRACT.exists() else None integrity_match = integrity_sha == EXPECTED_INTEGRITY_SHA256 cases = [ _case("NC1_future_release_rejected", nc1_future_release_rejected), _case("NC2_unavailable_revision_rejected", nc2_unavailable_revision_rejected), _case("NC3_held_out_label_mutation_invariant", nc3_held_out_labels_cannot_alter_preds), _case("NC4_no_election_crosses_train_test", nc4_no_election_crosses_boundary), _case("NC5_overlapping_samples_not_independent", nc5_overlapping_samples_not_independent), _case("NC6_identical_eligible_test_cycles", nc6_identical_eligible_test_cycles), _case("NC7_timezone_aware_release_and_revision", nc7_timezone_aware_release_and_revision), _case("NC8_train_before_test_chronology", nc8_train_before_test_chronology), ] passed = sum(1 for c in cases if c["pass"]) failed = sum(1 for c in cases if not c["pass"]) overall = "PASS" if failed == 0 and integrity_match else "FAIL" # Artifact hashes for attached outputs source_files = { "integrity_gates.py": SCRIPTS / "integrity_gates.py", "fit_eval_interface.py": SCRIPTS / "fit_eval_interface.py", "run_negative_controls.py": SCRIPTS / "run_negative_controls.py", } source_hashes = {k: file_sha256(v) for k, v in source_files.items() if v.exists()} report = { "suite": "ELECTION-01-integrity-negative-controls", "protocol": "v0.1.1-draft", "contract_ref": "/docs/election01-integrity-contract", "contract_sha256_expected": EXPECTED_INTEGRITY_SHA256, "contract_sha256_observed": integrity_sha, "contract_sha256_match": integrity_match, "model_change": False, "fitting_claimed": False, "predictions_are_stub": True, "stub_prediction_invariance_not_production_estimator": True, "production_pipeline_invariance_pending": True, "l1_m1_scoring_untouched": True, "prior_failed_gates_sha256": "9b8d33b65400da2ca7c9d417551cd6d9641644b3014a169c9b3cc73cb6dc80e9", "codex_review_ref": "f4c7748b-afc2-4266-9dd4-67b4f198130e", "ran_at_utc": utc_now_iso(), "summary": { "overall": overall, "n_tests": len(cases), "n_passed": passed, "n_failed": failed, }, "tests": cases, "source_sha256": source_hashes, } # Hash of report body without report_sha256 field body = canonical_json({k: v for k, v in report.items()}) report["report_sha256"] = sha256_hex(body) out_json = OUT_DIR / "negative_controls_report.json" data_json = DATA_DIR / "negative_controls_report.json" text = json.dumps(report, indent=2, ensure_ascii=False) + "\n" out_json.write_text(text) data_json.write_text(text) # Also write a compact PASS/FAIL line file lines = [ f"OVERALL={overall}", f"n_tests={len(cases)} n_passed={passed} n_failed={failed}", f"contract_sha256={integrity_sha}", f"report_sha256={report['report_sha256']}", f"MODEL_CHANGE=NO fitting_claimed=false", ] for c in cases: lines.append(f"{c['id']}={'PASS' if c['pass'] else 'FAIL'}") summary_txt = "\n".join(lines) + "\n" (OUT_DIR / "negative_controls_summary.txt").write_text(summary_txt) (DATA_DIR / "negative_controls_summary.txt").write_text(summary_txt) print(summary_txt) print(f"Wrote {out_json}") return 0 if overall == "PASS" else 1 if __name__ == "__main__": raise SystemExit(main())