Files
rune/scripts/check_boundary_codegen.py
T
Nick a54b9acd33 feat(tui): I3 live question-anchored task tree (D28) — the face operates on real runtime data
- Seam: tui/runtime/lib — library-binary child processes (entry serve
  ephemeral port; event-bus log --json), built from source at boot,
  GOPROXY=off; go.work/in-process honestly impossible (internal/ blocks
  cross-module; go.mod generator-owned) — shelling the LIBRARY binaries
  is the honest seam. Zero changes inside the five libraries.
- Question-rooted trees per D28: newest unresolved question roots the
  live tree (real task IDs, parent_task_id nesting, journal events as
  sub-trees via parent_id ancestry); resolved questions close to one-line
  history; no open question = honest runtime-activity root. Statuses
  roll on fingerprint-gated re-derivation. Controls carry real IDs,
  footer honestly marks render-only (command paths = next wave).
- Identity: runtime lib:connected vs serve: fallback.
- 32 packages green incl -race + gofmt (go_suite GREEN); 21 new tests;
  env-gated live test verified. Main-session verify: 32/32 own run;
  live pty shows question tree + lib:connected.
- Sibling-mid-flight churn (spine, uv.lock, diff.go) left for their
  waves.
2026-10-17 00:41:50 +01:00

172 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""check_boundary_codegen.py — phase 0C task 0.12 gate: ONE schema source.
Enforces, as running machinery (not spec text):
1. Regeneration parity: the committed generated files (Go
tui/boundary/gen/* + tui/go.mod; Rust src/rust/boundary_types/*) must
byte-match a fresh regeneration from schemas/boundary/v1. A hand-edited
generated file, a schema change without regeneration, or a renamed/new
schema without regeneration all trip RED.
2. Corpus validity: every schemas/boundary/corpus/*.json validates against
its message-type schema (JSON Schema 2020-12 via jsonschema) and is
byte-canonical (sorted keys, compact separators). Every schema must
have at least one corpus message.
3. Registry wiring: schema names + versions must appear in both generated
sides (the compiled artifacts carry the registry).
Usage: python scripts/check_boundary_codegen.py [--root REPO_ROOT]
Exit 0 = green; 1 = RED with the violating files listed.
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
import tempfile
from pathlib import Path
GENERATED_FILES = [
"tui/go.mod",
"tui/boundary/gen/boundary_gen.go",
"tui/boundary/gen/boundary_gen_test.go",
"src/rust/boundary_types/Cargo.toml",
"src/rust/boundary_types/build.rs",
"src/rust/boundary_types/src/lib.rs",
"src/rust/boundary_types/src/bin/build_id_probe.rs",
"src/rust/boundary_types/tests/corpus_roundtrip.rs",
]
def canonical_bytes(obj) -> bytes:
return (json.dumps(obj, sort_keys=True, separators=(",", ":"),
ensure_ascii=True) + "\n").encode()
def load_schemas(schemas_dir: Path) -> dict[str, dict]:
docs = {}
for f in sorted(schemas_dir.glob("*.schema.json")):
doc = json.loads(f.read_text())
docs[doc["title"]] = doc
return docs
def check_regeneration(root: Path, violations: list[str]) -> dict[str, dict]:
gen_script = root / "scripts" / "gen_boundary_types.py"
if not gen_script.is_file():
violations.append("scripts/gen_boundary_types.py missing")
return {}
with tempfile.TemporaryDirectory(prefix="boundary-regen-") as tmp:
r = subprocess.run(
[sys.executable, str(gen_script),
"--go-out", f"{tmp}/tui",
"--rust-out", f"{tmp}/rust/boundary_types",
"--schemas", str(root / "schemas/boundary/v1")],
capture_output=True, text=True, timeout=120)
if r.returncode != 0:
violations.append(f"generator failed: {r.stderr.strip()[:400]}")
return {}
fresh = {}
tmpdir = Path(tmp)
for rel in GENERATED_FILES:
p = tmpdir / rel.replace("src/rust/boundary_types", "rust/boundary_types")
if p.is_file():
fresh[rel] = p.read_bytes()
for rel, content in sorted(fresh.items()):
committed = root / rel
if not committed.is_file():
violations.append(f"generated file missing: {rel} (run scripts/gen_boundary_types.py)")
elif committed.read_bytes() != content:
violations.append(f"generated file drifted from schemas: {rel} "
"(hand edit or stale regeneration — regenerate with "
"scripts/gen_boundary_types.py)")
return fresh
def check_corpus(root: Path, schemas: dict[str, dict], violations: list[str]) -> None:
try:
import jsonschema
except ImportError:
violations.append("jsonschema library unavailable — cannot validate corpus")
return
corpus_dir = root / "schemas/boundary/corpus"
if not corpus_dir.is_dir():
violations.append("schemas/boundary/corpus missing")
return
covered: set[str] = set()
for f in sorted(corpus_dir.glob("*.json")):
raw = f.read_bytes()
try:
obj = json.loads(raw)
except json.JSONDecodeError as exc:
violations.append(f"corpus {f.name}: not valid JSON ({exc})")
continue
mtype = obj.get("type") if isinstance(obj, dict) else None
if mtype not in schemas:
violations.append(f"corpus {f.name}: unknown message type {mtype!r}")
continue
covered.add(mtype)
try:
jsonschema.Draft202012Validator(schemas[mtype]).validate(obj)
except jsonschema.ValidationError as exc:
violations.append(f"corpus {f.name}: schema violation: "
f"{exc.message[:200]}")
if raw.strip() + b"\n" != canonical_bytes(obj):
violations.append(f"corpus {f.name}: not canonical bytes "
"(sorted keys, compact separators)")
for title in sorted(schemas):
if title not in covered:
violations.append(f"schema {title} has no corpus message "
"(every message type needs representative coverage)")
def check_registry_wiring(root: Path, schemas: dict[str, dict],
violations: list[str]) -> None:
go = root / "tui/boundary/gen/boundary_gen.go"
rust = root / "src/rust/boundary_types/src/lib.rs"
go_text = go.read_text() if go.is_file() else ""
rust_text = rust.read_text() if rust.is_file() else ""
if "DO NOT EDIT" not in go_text or "DO NOT EDIT" not in rust_text:
violations.append("generated files lost their DO-NOT-EDIT header "
"(regenerate; hand edits are rejected)")
for title, doc in sorted(schemas.items()):
version = doc["x-version"]
# gofmt aligns the registry map entries, so the version match is
# whitespace-tolerant (wiring is the intent, spacing is incidental)
if not re.search(rf'"{re.escape(title)}":\s+{version},', go_text):
violations.append(f"schema {title} v{version} not wired into Go registry")
if f'("{title}", {version}),' not in rust_text:
violations.append(f"schema {title} v{version} not wired into Rust registry")
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description="boundary codegen gate (task 0.12)")
ap.add_argument("--root", default=str(Path(__file__).resolve().parent.parent),
help="repo root to check (defaults to this repo)")
args = ap.parse_args(argv)
root = Path(args.root).resolve()
violations: list[str] = []
schemas = load_schemas(root / "schemas/boundary/v1")
if not schemas:
print("RED: no schemas found in schemas/boundary/v1")
return 1
check_regeneration(root, violations)
check_corpus(root, schemas, violations)
check_registry_wiring(root, schemas, violations)
if violations:
print("RED: boundary codegen gate violations:")
for v in violations:
print(f" - {v}")
return 1
print(f"GREEN: boundary codegen — {len(schemas)} schemas, both sides "
"regeneration-clean, corpus canonical and schema-valid, registry wired")
return 0
if __name__ == "__main__":
sys.exit(main())