feat: harden zone reference contracts
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
This commit is contained in:
parent
bed5c3b53a
commit
be29c28100
19 changed files with 1712 additions and 454 deletions
125
tools/check_canon_lineage.py
Normal file
125
tools/check_canon_lineage.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Verify a local canon-lineage record against an authoritative checkout."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
LINEAGE_STANDARD = "canon-lineage_v0.1"
|
||||
|
||||
|
||||
class LineageError(ValueError):
|
||||
"""The lineage manifest or canonical artifact is inconsistent."""
|
||||
|
||||
|
||||
def _required(mapping: Mapping[str, Any], key: str) -> Any:
|
||||
value = mapping.get(key)
|
||||
if value is None or value == "":
|
||||
raise LineageError(f"lineage.{key} is required")
|
||||
return value
|
||||
|
||||
|
||||
def _sha256(content: bytes) -> str:
|
||||
return hashlib.sha256(content).hexdigest()
|
||||
|
||||
|
||||
def _frontmatter(content: bytes) -> dict[str, Any]:
|
||||
text = content.decode()
|
||||
if not text.startswith("---\n") or "\n---\n" not in text[4:]:
|
||||
raise LineageError("canonical artifact requires YAML frontmatter")
|
||||
raw = text.split("\n---\n", 1)[0][4:]
|
||||
value = yaml.safe_load(raw) or {}
|
||||
if not isinstance(value, dict):
|
||||
raise LineageError("canonical frontmatter must be a mapping")
|
||||
return value
|
||||
|
||||
|
||||
def check_lineage(
|
||||
manifest: Any,
|
||||
canon_root: Path,
|
||||
*,
|
||||
verify_revision: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
if not isinstance(manifest, Mapping):
|
||||
raise LineageError("lineage manifest must be a mapping")
|
||||
if manifest.get("standard") != LINEAGE_STANDARD:
|
||||
raise LineageError(f"lineage.standard must be {LINEAGE_STANDARD}")
|
||||
relative = Path(str(_required(manifest, "canonical_path")))
|
||||
if relative.is_absolute() or ".." in relative.parts:
|
||||
raise LineageError("canonical_path must stay below canon_root")
|
||||
revision = str(_required(manifest, "canonical_revision"))
|
||||
expected_hash = str(_required(manifest, "canonical_sha256"))
|
||||
expected_status = str(_required(manifest, "canonical_status"))
|
||||
canonical_path = canon_root / relative
|
||||
try:
|
||||
content = canonical_path.read_bytes()
|
||||
except OSError as exc:
|
||||
raise LineageError(f"cannot read canonical artifact: {exc}") from exc
|
||||
actual_hash = _sha256(content)
|
||||
frontmatter = _frontmatter(content)
|
||||
errors: list[str] = []
|
||||
if actual_hash != expected_hash:
|
||||
errors.append(
|
||||
f"canonical content hash changed: expected {expected_hash}, got {actual_hash}"
|
||||
)
|
||||
if str(frontmatter.get("status")) != expected_status:
|
||||
errors.append(
|
||||
"canonical lifecycle changed: "
|
||||
f"expected {expected_status}, got {frontmatter.get('status')}"
|
||||
)
|
||||
revision_hash = None
|
||||
if verify_revision:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(canon_root), "show", f"{revision}:{relative.as_posix()}"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
errors.append(
|
||||
f"cannot read canonical artifact at revision {revision}: "
|
||||
+ completed.stderr.decode().strip()
|
||||
)
|
||||
else:
|
||||
revision_hash = _sha256(completed.stdout)
|
||||
if revision_hash != expected_hash:
|
||||
errors.append(
|
||||
f"revision {revision} content does not match canonical_sha256"
|
||||
)
|
||||
return {
|
||||
"ok": not errors,
|
||||
"artifact": manifest.get("artifact"),
|
||||
"publication_owner": manifest.get("publication_owner"),
|
||||
"canonical_path": relative.as_posix(),
|
||||
"canonical_revision": revision,
|
||||
"canonical_status": frontmatter.get("status"),
|
||||
"canonical_sha256": actual_hash,
|
||||
"revision_sha256": revision_hash,
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--manifest", required=True, type=Path)
|
||||
parser.add_argument("--canon-root", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
manifest = yaml.safe_load(args.manifest.read_text()) or {}
|
||||
result = check_lineage(manifest, args.canon_root)
|
||||
except (OSError, yaml.YAMLError, LineageError) as exc:
|
||||
result = {"ok": False, "errors": [str(exc)]}
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0 if result["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue