diff options
| author | YurenHao0426 <Blackhao0426@gmail.com> | 2026-07-22 20:24:31 -0500 |
|---|---|---|
| committer | YurenHao0426 <Blackhao0426@gmail.com> | 2026-07-22 20:24:31 -0500 |
| commit | 0ccb8ff9ea6686ca8b0adf3ec552408b4b07ad6b (patch) | |
| tree | 536fff5da9746f5f2d3e209e3fdc74bf2c58616b | |
| parent | a5ba631e7e5562246a9f339fad1303747c72e6ac (diff) | |
audit: bind oral B recovery to D4 gate
| -rw-r--r-- | ORAL_B_RECOVERY.md | 5 | ||||
| -rw-r--r-- | experiments/analyze_bci_td_development.py | 20 | ||||
| -rw-r--r-- | experiments/bci_td_run.py | 17 |
3 files changed, 36 insertions, 6 deletions
diff --git a/ORAL_B_RECOVERY.md b/ORAL_B_RECOVERY.md index 2d9e8bf..7ab6785 100644 --- a/ORAL_B_RECOVERY.md +++ b/ORAL_B_RECOVERY.md @@ -80,6 +80,11 @@ observations and random stream. The oracle-role condition explicitly reads the synthetic environment map only as a labelled diagnostic ceiling and is ineligible for selection. Neutral warmup must leave maximum predictor error at most `1e-5`; all warmup and online scalar observations are reported. +The runner additionally requires the passed D4 gate itself to be tracked and +the worktree to be clean. Every R1 record binds both the frozen protocol and +that exact D4 gate by SHA-256; the complete-grid analyzer rejects any digest +drift. This is an audit-only constraint and does not alter the frozen rates, +seeds, conditions, metrics, or thresholds. For each rate and task seed, run four paired conditions on identical context, noise, perturbations, and evaluation trajectories: diff --git a/experiments/analyze_bci_td_development.py b/experiments/analyze_bci_td_development.py index 74e80e8..78dbceb 100644 --- a/experiments/analyze_bci_td_development.py +++ b/experiments/analyze_bci_td_development.py @@ -10,6 +10,8 @@ import os ETAS = (0.03, 0.1) TASK_SEEDS = (0, 1, 2) +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROTOCOL_PATH = os.path.join(ROOT, "ORAL_B_RECOVERY.md") def require(condition, message): @@ -35,7 +37,7 @@ def finite_tree(value): return True -def validate(row, path, d4_gate): +def validate(row, path, d4_gate, d4_gate_sha256, protocol_sha256): require(row.get("schema_version") == 1, f"{path}: schema") protocol = row.get("protocol", {}) require(protocol.get("name") == "oral_b_td_development_v1", @@ -50,6 +52,10 @@ def validate(row, path, d4_gate): require(float(args.get("eta")) in ETAS, f"{path}: eta") require(protocol.get("evaluation_task_seed") == args["task_seed"] + 200_000, f"{path}: evaluation seed") + require(protocol.get("d4_gate_source") == d4_gate, + f"{path}: D4 gate source") + require(protocol.get("d4_gate_sha256") == d4_gate_sha256, + f"{path}: D4 gate digest") require(args.get("d4_gate") == d4_gate, f"{path}: D4 gate path") config = row.get("config", {}) expected = { @@ -68,6 +74,12 @@ def validate(row, path, d4_gate): require(source.get("git_tracked_dirty") is False, f"{path}: dirty") require(source.get("runner_and_protocol_tracked") is True, f"{path}: untracked") + require(source.get("d4_gate_tracked") is True, + f"{path}: untracked D4 gate") + require(source.get("d4_gate_sha256") == d4_gate_sha256, + f"{path}: provenance D4 gate digest") + require(source.get("protocol_sha256") == protocol_sha256, + f"{path}: protocol digest") require(len(source.get("git_commit", "")) == 40, f"{path}: commit") require(row.get("finite") is True and finite_tree(row), f"{path}: finite") for name in ("intact", "fixed_vectorizer", "plasticity_lesion", @@ -127,6 +139,8 @@ def main(): "kp_dynamic_neutral_projection_confirmation_v1", "D4 protocol") require(d4.get("status") == "passed", "D4 did not pass") require(d4.get("review_score_after") == 7, "D4 score rule") + d4_gate_sha256 = sha256(args.d4_gate) + protocol_sha256 = sha256(PROTOCOL_PATH) paths = sorted(glob.glob(os.path.join(args.results, "bci_td_v1_*.json"))) require(len(paths) == 6, f"expected six R1 records, found {len(paths)}") records = {} @@ -134,7 +148,7 @@ def main(): for path in paths: with open(path) as handle: row = json.load(handle) - validate(row, path, args.d4_gate) + validate(row, path, args.d4_gate, d4_gate_sha256, protocol_sha256) key = (float(row["args"]["eta"]), row["args"]["task_seed"]) require(key not in records, f"duplicate R1 cell {key}") records[key] = row @@ -182,6 +196,8 @@ def main(): "selected": selected, "candidates": candidates, "source_commit": next(iter(commits)), + "protocol_sha256": protocol_sha256, + "d4_gate_sha256": d4_gate_sha256, "source_sha256": {path: sha256(path) for path in paths}, "oral_b_confirmation_opened": selected is not None, "review_score_before": 7, diff --git a/experiments/bci_td_run.py b/experiments/bci_td_run.py index 195bd22..b7948a5 100644 --- a/experiments/bci_td_run.py +++ b/experiments/bci_td_run.py @@ -31,13 +31,14 @@ def sha256(path): return digest.hexdigest() -def provenance(): +def provenance(d4_gate_path): def run(command): return subprocess.run( command, cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip() runner = os.path.relpath(os.path.abspath(__file__), ROOT) protocol = os.path.relpath(PROTOCOL_PATH, ROOT) + d4_gate = os.path.relpath(os.path.abspath(d4_gate_path), ROOT) return { "git_commit": run(["git", "rev-parse", "HEAD"]), "git_tracked_dirty": bool(run( @@ -47,6 +48,10 @@ def provenance(): capture_output=True).returncode == 0 for path in (runner, protocol)), "protocol_sha256": sha256(PROTOCOL_PATH), + "d4_gate_tracked": subprocess.run( + ["git", "ls-files", "--error-unmatch", d4_gate], cwd=ROOT, + capture_output=True).returncode == 0, + "d4_gate_sha256": sha256(d4_gate_path), } @@ -199,9 +204,12 @@ def main(): or d4.get("status") != "passed" or d4.get("review_score_after") != 7): raise ValueError("oral-B R1 requires the complete audited D4 pass") - source = provenance() - if source["git_tracked_dirty"] or not source["runner_and_protocol_tracked"]: - raise RuntimeError("R1 requires clean tracked source and protocol") + source = provenance(args.d4_gate) + if (source["git_tracked_dirty"] + or not source["runner_and_protocol_tracked"] + or not source["d4_gate_tracked"]): + raise RuntimeError( + "R1 requires clean tracked source, protocol, and D4 gate") cfg = build_config(args.eta) trajectories = generate_trajectories(cfg, args.task_seed) @@ -240,6 +248,7 @@ def main(): "confirmation_seeds_touched": False, "evaluation_task_seed": args.task_seed + 200_000, "d4_gate_source": args.d4_gate, + "d4_gate_sha256": source["d4_gate_sha256"], }, "args": vars(args), "config": vars(cfg), "provenance": source, "warmup": warmups, "conditions": conditions, |
