"""Protocol freeze: hash the confirmatory configuration (models, prompts, datasets, budgets, scorers, hypotheses, stats plan).""" from __future__ import annotations import json, yaml, hashlib, datetime as dt from pathlib import Path from .util import ROOT, sha256_file, sha256_text, write_json, utc_now FROZEN_FILES = ["configs/models.yaml", "configs/data.yaml", "configs/resources.yaml", "src/wp30/prompts.py", "src/wp30/scoring.py", "src/wp30/data/synthetic.py", "src/wp30/data/exec_tasks.py", "src/wp30/methods/text.py", "src/wp30/cache/sparse.py", "src/wp30/cache/selection.py", "src/wp30/cache/serialize.py", "src/wp30/backends/mlx_backend.py", "src/wp30/runner.py", "src/wp30/chains.py", "src/wp30/fanin.py", "protocol/hypotheses.md", "protocol/analysis_plan.md", "data/manifests/splits.json"] def freeze(core_config: str) -> int: cfg = yaml.safe_load(open(core_config)) files = {f: sha256_file(ROOT / f) for f in FROZEN_FILES if (ROOT / f).exists()} extra = {f: sha256_file(ROOT / f) for f in cfg.get("frozen_extra_files", []) if (ROOT / f).exists()} body = {"frozen_utc": utc_now(), "core_config": core_config, "core_config_sha256": sha256_file(ROOT / core_config), "files": {**files, **extra}, "test_split_first_evaluation": "after this freeze; see runs/logs/*core*.log first timestamp", "hypotheses": (ROOT / "protocol/hypotheses.md").read_text()[:4000]} h = sha256_text(json.dumps(body["files"], sort_keys=True) + body["core_config_sha256"]) body["protocol_hash"] = h write_json(ROOT / "protocol/frozen.json", body) print("protocol frozen:", h) return 0 def current_hash() -> str | None: p = ROOT / "protocol/frozen.json" return json.load(open(p))["protocol_hash"] if p.exists() else None