Files
ratatoskr/scripts/contract_drift_check.py
T
vh 9703eb2b6b init: seed Ratatoskr from corviduo-project-template + ship v0 scaffold
Worldtree Conversation API debug TUI. Multi-pane observability dashboard:
chat transcript + persona/Vili affect log + tool events + admin events +
Bifrost state + tool inventory + (opt-in) raw server log.

Design locked at docs/design-brief.md (originated as
brokkr-smithy/docs/ratatoskr-design-brief.md). Operator-locked decisions:

- Textual application-shell framework (multi-pane dashboard, not REPL).
- Separate repo + separate dev team (no Worldtree-source imports).
- httpx-sse for SSE consumption (reference Python SSE-resume impl).
- Triple version-skew mitigation: spec-pin in pyproject.toml + recorded
  SSE snapshot tests + conformance smoke. Initial pin: Worldtree v0.19.0
  at 55101e909abcd2219833266b6f905c5bc956e0f0.
- Persona pane: label-don't-refuse PII posture.
- Server-log pane: opt-in via --server-log <path>.
- Two-stage Ctrl-C (cancel then exit).
- Markdown rendering default-on; --raw opt-out.

In the box:

- docs/design-brief.md — the locked design with full rationale.
- docs/SPEC-PIN.md — Worldtree spec pin + bump procedure.
- docs/conversation-api-spec.md + docs/conversation_api.contract.md —
  vendored Worldtree spec snapshots at the pinned SHA.
- pyproject.toml — Python 3.12, hatchling, uv-managed, deps locked.
- src/ratatoskr/ — stub package (cli.py raises NotImplementedError).
- tests/test_no_worldtree_imports.py — boundary smoke test PASSING.
- tests/snapshots/README.md — recording convention for SSE snapshot tests.

Not in the box yet:

- Gitea remote (operator/infra-ops to register at vh/ratatoskr).
- Implementation — the dev team owns this; design brief is the spec.

Origin: althing thread 01KS3R34XD3N6HMK91VXESHGW7 (worldtree-dev →
brokkr-smithy-dev, 2026-05-20). Volva consulted via thread
01KS3VF6W33N3V5FNMGQ91YNVD.
2026-05-20 20:38:22 -07:00

133 lines
4.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Drift check: verify each .contract.md with a `prd:` block still matches its source.
Reads every `docs/contracts/**/*.contract.md` that has a `prd:` frontmatter block,
fetches the live issue body and (if specified) the lock-in comment from Gitea,
re-hashes them, and compares to the recorded `body_sha256_16` / `lock_in_sha256_16`.
Exits 0 if all pinned contracts are clean. Exits 1 with a diff report if any contract
has drifted. Skips contracts without a `prd:` block.
Usage:
python scripts/contract_drift_check.py
python scripts/contract_drift_check.py --contract docs/contracts/issues/138.contract.md
python scripts/contract_drift_check.py --json
Requires GITEA_TOKEN in environment (and GITEA_URL/OWNER/REPO if not in env.sh).
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import sys
from pathlib import Path
from typing import Any
import httpx
import yaml
PROJECT_ROOT = Path(__file__).parent.parent
CONTRACTS_GLOB = "docs/contracts/**/*.contract.md"
def sha16(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()[:16]
def load_frontmatter(text: str) -> dict[str, Any] | None:
if not text.startswith("---\n"):
return None
end = text.find("\n---\n", 4)
if end == -1:
return None
return yaml.safe_load(text[4:end])
def fetch_issue_state(token: str, base_url: str, owner: str, repo: str, n: int, comment_id: int | None) -> dict[str, Any]:
hdr = {"Authorization": f"token {token}"}
issue_url = f"{base_url}/api/v1/repos/{owner}/{repo}/issues/{n}"
issue = httpx.get(issue_url, headers=hdr, timeout=15)
issue.raise_for_status()
body = issue.json()["body"]
out: dict[str, Any] = {"body_sha256_16": sha16(body)}
if comment_id:
comment = httpx.get(
f"{base_url}/api/v1/repos/{owner}/{repo}/issues/comments/{comment_id}",
headers=hdr, timeout=15,
)
comment.raise_for_status()
out["lock_in_sha256_16"] = sha16(comment.json()["body"])
return out
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--contract", help="check a single contract file (default: all)")
ap.add_argument("--json", action="store_true", help="emit machine-readable report")
args = ap.parse_args()
token = os.environ.get("GITEA_TOKEN", "")
base_url = os.environ.get("GITEA_URL", "https://gitea.phasefinal.com")
owner = os.environ.get("GITEA_OWNER", "vh")
repo = os.environ.get("GITEA_REPO", "Worldtree")
if not token:
print("error: GITEA_TOKEN not set", file=sys.stderr)
return 2
if args.contract:
files = [Path(args.contract).resolve()]
else:
files = sorted(PROJECT_ROOT.glob(CONTRACTS_GLOB))
findings: list[dict[str, Any]] = []
for path in files:
text = path.read_text()
fm = load_frontmatter(text)
if not fm or "prd" not in fm:
continue
prd = fm["prd"]
n = prd["issue"]
live = fetch_issue_state(token, base_url, owner, repo, n, prd.get("lock_in_comment_id"))
body_drift = live["body_sha256_16"] != prd["body_sha256_16"]
lock_drift = (
prd.get("lock_in_comment_id") is not None
and live.get("lock_in_sha256_16") != prd.get("lock_in_sha256_16")
)
status = "drift" if body_drift or lock_drift else "clean"
findings.append({
"contract": str(path.relative_to(PROJECT_ROOT)),
"issue": n,
"status": status,
"body_drift": body_drift,
"lock_in_drift": lock_drift,
"pinned_at": prd.get("pinned_at"),
"live": live,
"pinned": {
"body_sha256_16": prd["body_sha256_16"],
"lock_in_sha256_16": prd.get("lock_in_sha256_16"),
},
})
if args.json:
print(json.dumps({"findings": findings}, indent=2))
else:
clean = [f for f in findings if f["status"] == "clean"]
drift = [f for f in findings if f["status"] == "drift"]
for f in findings:
mark = "OK" if f["status"] == "clean" else "DRIFT"
print(f" [{mark}] {f['contract']} (issue #{f['issue']})")
if f["body_drift"]:
print(f" body: pinned={f['pinned']['body_sha256_16']} live={f['live']['body_sha256_16']}")
if f["lock_in_drift"]:
print(f" lock-in: pinned={f['pinned']['lock_in_sha256_16']} live={f['live'].get('lock_in_sha256_16')}")
print()
print(f"{len(clean)} clean, {len(drift)} drifted")
return 1 if any(f["status"] == "drift" for f in findings) else 0
if __name__ == "__main__":
sys.exit(main())