summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYurenHao0426 <Blackhao0426@gmail.com>2026-07-22 20:24:31 -0500
committerYurenHao0426 <Blackhao0426@gmail.com>2026-07-22 20:24:31 -0500
commit0ccb8ff9ea6686ca8b0adf3ec552408b4b07ad6b (patch)
tree536fff5da9746f5f2d3e209e3fdc74bf2c58616b
parenta5ba631e7e5562246a9f339fad1303747c72e6ac (diff)
audit: bind oral B recovery to D4 gate
-rw-r--r--ORAL_B_RECOVERY.md5
-rw-r--r--experiments/analyze_bci_td_development.py20
-rw-r--r--experiments/bci_td_run.py17
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,