- 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.
172 lines
7.0 KiB
Python
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())
|