130 lines
3.6 KiB
Python
130 lines
3.6 KiB
Python
"""Freeze a review-ready M3 plan into a non-authorizing review packet."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
if __package__:
|
|
from scripts.validate_m3_hardware_protocol import validate
|
|
else:
|
|
from validate_m3_hardware_protocol import validate
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def _repository_commit() -> str:
|
|
result = subprocess.run(
|
|
["git", "rev-parse", "HEAD"],
|
|
cwd=ROOT,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def create_packet(
|
|
plan_path: Path,
|
|
output: Path,
|
|
repository_commit: str | None = None,
|
|
) -> dict:
|
|
source = plan_path.resolve()
|
|
validation = validate(source)
|
|
if validation["status"] != "ready_for_review":
|
|
raise ValueError("M3 plan is not ready for review")
|
|
|
|
output = output.resolve()
|
|
if output.exists() and any(output.iterdir()):
|
|
raise ValueError(f"review-packet directory is not empty: {output}")
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
|
|
frozen_plan = output / "plan.json"
|
|
shutil.copyfile(source, frozen_plan)
|
|
plan = json.loads(frozen_plan.read_text(encoding="utf-8-sig"))
|
|
commit = repository_commit or _repository_commit()
|
|
|
|
review = {
|
|
"schema_version": 1,
|
|
"review": "M3 hardware research session plan",
|
|
"binding": {
|
|
"plan_path": frozen_plan.name,
|
|
"plan_sha256": _sha256(frozen_plan),
|
|
"repository_commit": commit,
|
|
},
|
|
"session_window": {
|
|
"starts_at": plan["session"]["planned_at"],
|
|
"ends_at": plan["session"]["ends_at"],
|
|
},
|
|
"decision": "not_reviewed",
|
|
"reviewer": None,
|
|
"reviewed_at": None,
|
|
"notes": "",
|
|
"hardware_access_authorized": False,
|
|
"separate_human_approval_required": True,
|
|
}
|
|
review_path = output / "approval-record-template.json"
|
|
review_path.write_text(
|
|
json.dumps(review, indent=2, sort_keys=True) + "\n",
|
|
encoding="utf-8",
|
|
newline="\n",
|
|
)
|
|
|
|
manifest = {
|
|
"schema_version": 1,
|
|
"packet": "M3 hardware research plan review",
|
|
"repository_commit": commit,
|
|
"contains_private_evidence": False,
|
|
"hardware_access_authorized": False,
|
|
"files": [
|
|
{
|
|
"path": frozen_plan.name,
|
|
"bytes": frozen_plan.stat().st_size,
|
|
"sha256": _sha256(frozen_plan),
|
|
},
|
|
{
|
|
"path": review_path.name,
|
|
"bytes": review_path.stat().st_size,
|
|
"sha256": _sha256(review_path),
|
|
},
|
|
],
|
|
}
|
|
manifest_path = output / "packet-manifest.json"
|
|
manifest_path.write_text(
|
|
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
|
|
encoding="utf-8",
|
|
newline="\n",
|
|
)
|
|
return manifest
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("plan", type=Path)
|
|
parser.add_argument(
|
|
"--output",
|
|
type=Path,
|
|
default=ROOT / ".build" / "m3-review-packet",
|
|
)
|
|
args = parser.parse_args()
|
|
try:
|
|
result = create_packet(args.plan, args.output)
|
|
except ValueError as error:
|
|
parser.error(str(error))
|
|
print(f"packet={args.output.resolve()}")
|
|
print(f"plan_sha256={result['files'][0]['sha256']}")
|
|
print("hardware_access_authorized=false")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|