9703eb2b6b
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.
133 lines
4.7 KiB
Python
Executable File
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())
|