#!/usr/bin/env python3 """One-pass corpus + wiki gate engine. Binding: wiki/process/agent-scale.md slice E. Entry points: tools/wiki_gate.sh, tools/corpus_gate.sh (thin wrappers). Usage: python3 tools/corpus_engine.py --wiki python3 tools/corpus_engine.py --corpus python3 tools/corpus_engine.py --all python3 tools/corpus_engine.py --root DIR --wiki|--corpus|--all """ from __future__ import annotations import argparse import re import sys from pathlib import Path LINK_RE = re.compile(r"\]\(([^)]+)\)") TYPE_LINE_RE = re.compile(r"^Type:\s*(law|spec|knowledge|log)\s*$", re.M) GENERATED_LINE_RE = re.compile(r"^Generated:\s*(.+)$", re.M) STATUS_LINE_RE = re.compile(r"^Status:\s*(.+)$", re.M) HEADING_RE = re.compile(r"^#{1,6}\s+(.+)$", re.M) DESIGN_LINE_RE = re.compile( r"^ - (wiki/[A-Za-z0-9._/-]+\.md)#([a-z0-9-]+)$" ) DEPENDS_LINE_RE = DESIGN_LINE_RE RETIRED_RE = re.compile( r"DESIGN[.]md is the constitution|constitution outranks|constitution-first|" r"DESIGN[.]md amendment|^Constitution:", re.I | re.M, ) RETIRED_RUNTIME_SYMBOL_RE = re.compile( r"\b(?:submit_ops_job|OperationsState|PendingOpsJob|OpsJobKind)\b" ) RETIRED_RUNTIME_HISTORY_RE = re.compile( r"\b(?:former|retir(?:ed|ement)|legacy|supersed(?:e|ed|es|ing)|" r"histor(?:y|ical)|migrat(?:e|ed|es|ing|ion)|no longer|removed)\b", re.I, ) RETIRED_MATERIAL_DEFAULT_RE = re.compile( r"\bmaterial(?:\s+(?:render|dialect|view))?\s+is\s+(?:the\s+)?" r"(?:Bevy(?:'s)?\s+)?(?:opening\s+)?default(?![- ]quality)\b|" r"\bF3\s+flips?\s+(?:back\s+)?to\s+(?:the\s+)?(?:flat\s+)?sensorium\b", re.I, ) RETIRED_MATERIAL_DEFAULT_HISTORY_RE = re.compile( r"\b(?:former|previous(?:ly)?|retir(?:ed|ement)|supersed(?:e|ed|es|ing)|" r"histor(?:y|ical)|until|served|staging|no longer)\b", re.I, ) BINDING_DOORWAY_RE = re.compile( r"^Type: (law|spec)$|^Status:|^## Decisions log|^## Acceptance criteria", re.M, ) STATUS_ENUM = frozenset( {"DRAFT", "READY", "IN PROGRESS", "BLOCKED", "IMPLEMENTED"} ) def slugify_heading(heading: str) -> str: s = heading.lower() s = re.sub(r"[`*_]", "", s) s = s.replace("&", " ") s = re.sub(r"[^a-z0-9 -]", "", s) s = re.sub(r"\s+", "-", s.strip()) s = re.sub(r"-+", "-", s) return s.strip("-") def heading_slugs(text: str) -> set[str]: return {slugify_heading(h) for h in HEADING_RE.findall(text)} class Engine: def __init__(self, root: Path): self.root = root.resolve() self.fail = 0 self._text: dict[Path, str] = {} self._slugs: dict[Path, set[str]] = {} def bad(self, msg: str) -> None: print(f"FAIL: {msg}") self.fail = 1 def path(self, p: str | Path) -> Path: p = Path(p) return p if p.is_absolute() else self.root / p def read(self, p: str | Path) -> str: path = self.path(p) if path not in self._text: self._text[path] = path.read_text(encoding="utf-8") return self._text[path] def slugs_of(self, p: str | Path) -> set[str]: path = self.path(p) if path not in self._slugs: self._slugs[path] = heading_slugs(self.read(path)) return self._slugs[path] def wiki_md_files(self) -> list[Path]: return sorted((self.root / "wiki").rglob("*.md")) def metadata_block(self, text: str) -> str: match = re.search(r"^```[^\n]*\n(.*?)^```\s*$", text, re.M | re.S) return match.group(1) if match else "" # ── Wiki gate ──────────────────────────────────────────────────────── def run_wiki(self) -> int: wiki = self.root / "wiki" summary = wiki / "SUMMARY.md" if not summary.is_file(): self.bad("wiki/SUMMARY.md missing") return self.fail summary_targets: set[str] = set() for m in re.finditer(r"\(([^)#]+)", self.read(summary)): t = m.group(1).split("#", 1)[0].strip() if t: summary_targets.add(t) required: set[str] = set() for path in self.wiki_md_files(): rel = path.relative_to(wiki).as_posix() if rel == "SUMMARY.md": continue if rel.startswith("log/") and rel != "log/README.md": continue required.add(rel) for orphan in sorted(required - summary_targets): self.bad( f"orphan page not reachable from wiki/SUMMARY.md: wiki/{orphan}" ) link_sources: list[Path] = [] for path in self.wiki_md_files(): if GENERATED_LINE_RE.search(self.metadata_block(self.read(path))): continue link_sources.append(path) for name in ("AGENT.md", "README.md", "DESIGN.md"): p = self.root / name if p.is_file(): link_sources.append(p) for src in link_sources: text = self.read(src) src_dir = src.parent try: src_rel = src.relative_to(self.root).as_posix() except ValueError: src_rel = str(src) for m in LINK_RE.finditer(text): target = m.group(1) if target.startswith(("http://", "https://", "mailto:", "#")): continue clean = target.split("#", 1)[0] if not clean: continue candidate = (src_dir / clean) # Match bash `[ -e path ]`: exists as file or directory. if not candidate.exists(): self.bad(f"{src_rel} -> broken link ({target})") if self.fail == 0: print("wiki gate: OK (no orphans, no broken internal links)") return self.fail # ── Corpus gate ────────────────────────────────────────────────────── def design_block_lines(self, text: str) -> list[str]: lines: list[str] = [] inside = False for line in text.splitlines(): if line.startswith("Design:"): inside = line.strip() == "Design:" continue if inside: if line.startswith("Depends on:"): break lines.append(line) return lines def validate_design_refs(self, rel: str, text: str) -> None: count = 0 for line in self.design_block_lines(text): if not line.strip(): continue m = DESIGN_LINE_RE.match(line) if not m: self.bad(f"{rel} has malformed Design reference: {line}") continue target_rel, anchor = m.group(1), m.group(2) ref = f"{target_rel}#{anchor}" count += 1 target = self.root / target_rel if not target.is_file(): self.bad(f"{rel} Design reference target does not exist: {ref}") continue ttext = self.read(target) if not re.search(r"^Type: (law|spec)\s*$", ttext, re.M): self.bad(f"{rel} Design reference is not binding law/spec: {ref}") if anchor not in self.slugs_of(target): self.bad(f"{rel} Design reference anchor does not exist: {ref}") if count == 0: self.bad(f"{rel} has no Design references") def depends_block_lines(self, text: str) -> tuple[str | None, list[str]]: lines = text.splitlines() for index, line in enumerate(lines): if not line.startswith("Depends on:"): continue declaration = line[len("Depends on:") :].strip() body: list[str] = [] for following in lines[index + 1 :]: if following.startswith("```"): break body.append(following) return declaration, body return None, [] def validate_depends_refs(self, rel: str, text: str) -> None: declaration, lines = self.depends_block_lines(text) if declaration is None: return if declaration == "none": if any(line.strip() for line in lines): self.bad(f"{rel} Depends on: none must not have list entries") return if declaration: self.bad( f"{rel} Depends on field must be 'none' or a structured reference list" ) return count = 0 seen: set[str] = set() source = self.root / rel for line in lines: if not line.strip(): continue m = DEPENDS_LINE_RE.match(line) if not m: self.bad(f"{rel} has malformed Depends on reference: {line}") continue target_rel, anchor = m.group(1), m.group(2) ref = f"{target_rel}#{anchor}" count += 1 if ref in seen: self.bad(f"{rel} repeats Depends on reference: {ref}") continue seen.add(ref) target = self.root / target_rel if target == source: self.bad(f"{rel} depends on itself: {ref}") continue if not target.is_file(): self.bad(f"{rel} Depends on target does not exist: {ref}") continue ttext = self.read(target) if not re.search(r"^Type: (law|spec|knowledge)\s*$", ttext, re.M): self.bad(f"{rel} Depends on target is not current corpus: {ref}") if anchor not in self.slugs_of(target): self.bad(f"{rel} Depends on anchor does not exist: {ref}") if count == 0: self.bad(f"{rel} has no Depends on references; use 'Depends on: none'") def validate_retired_runtime_authority(self, rel: str, text: str) -> None: paragraphs = re.finditer( r"(?:^|\n[ \t]*\n)(.*?)(?=\n[ \t]*\n|\Z)", text, re.S ) for paragraph in paragraphs: body = paragraph.group(1) symbol = RETIRED_RUNTIME_SYMBOL_RE.search(body) if symbol is None or RETIRED_RUNTIME_HISTORY_RE.search(body): continue line = text.count("\n", 0, paragraph.start(1) + symbol.start()) + 1 self.bad( f"{rel}:{line} names retired live-runtime symbol " f"'{symbol.group(0)}' without explicit historical context" ) def validate_retired_runtime_code(self) -> None: candidates: list[Path] = [] for root_name in ("crates", "src"): root = self.root / root_name if root.is_dir(): candidates.extend(root.rglob("*.rs")) for path in sorted(candidates): text = self.read(path) for symbol in RETIRED_RUNTIME_SYMBOL_RE.finditer(text): rel = path.relative_to(self.root).as_posix() line = text.count("\n", 0, symbol.start()) + 1 self.bad( f"{rel}:{line} contains retired live-runtime symbol " f"'{symbol.group(0)}'" ) def validate_retired_material_default(self, rel: str, text: str) -> None: paragraphs = re.finditer( r"(?:^|\n[ \t]*\n)(.*?)(?=\n[ \t]*\n|\Z)", text, re.S ) for paragraph in paragraphs: body = paragraph.group(1) claim = RETIRED_MATERIAL_DEFAULT_RE.search(body) if claim is None or RETIRED_MATERIAL_DEFAULT_HISTORY_RE.search(body): continue line = text.count("\n", 0, paragraph.start(1) + claim.start()) + 1 self.bad( f"{rel}:{line} contains superseded material-first startup claim " f"'{claim.group(0)}' without explicit historical context; " "DIGITAL is home and F3 flips to REAL" ) def run_corpus(self) -> int: for path in self.wiki_md_files(): rel = path.relative_to(self.root).as_posix() if rel == "wiki/SUMMARY.md": continue text = self.read(path) types = TYPE_LINE_RE.findall(text) if len(types) != 1: self.bad(f"{rel} must declare exactly one supported Type:") continue if types[0] in {"law", "spec"}: self.validate_retired_runtime_authority(rel, text) generated = GENERATED_LINE_RE.findall(self.metadata_block(text)) if len(generated) > 1: self.bad(f"{rel} must declare at most one Generated:") elif generated: generator_rel = generated[0].strip() if not re.fullmatch(r"tools/[A-Za-z0-9._/-]+", generator_rel): self.bad( f"{rel} Generated path must be repository-root tools/ path: " f"{generator_rel}" ) elif not (self.root / generator_rel).is_file(): self.bad( f"{rel} Generated target does not exist: {generator_rel}" ) if types[0] != "spec": continue for field in ("Status:", "Stage:", "Design:", "Depends on:"): if not re.search(rf"^{re.escape(field)}", text, re.M): self.bad(f"{rel} missing '{field}'") if not re.search(r"^Design:\s*$", text, re.M): self.bad(f"{rel} Design field must be a structured reference list") if not re.search(r"acceptance criteria", text, re.I): self.bad(f"{rel} has no acceptance criteria") sm = STATUS_LINE_RE.search(text) if sm: status = sm.group(1).strip() if status not in STATUS_ENUM: self.bad( f"{rel} Status '{status}' is not an exact enum value" ) self.validate_design_refs(rel, text) self.validate_depends_refs(rel, text) self.validate_retired_runtime_code() design = self.root / "DESIGN.md" if not design.is_file(): self.bad("DESIGN.md missing") else: dtext = self.read(design) # Match `wc -l`: count newline characters. doorway_lines = dtext.count("\n") if doorway_lines > 100: self.bad( f"DESIGN.md doorway grew past 100 lines ({doorway_lines})" ) flat = " ".join(dtext.splitlines()) if "contains no unique game law" not in flat: self.bad( "DESIGN.md must state that it contains no unique game law" ) if BINDING_DOORWAY_RE.search(dtext): self.bad( "DESIGN.md contains binding-page structure; move it into wiki/" ) candidates: list[Path] = [] wiki = self.root / "wiki" for p in wiki.rglob("*.md"): rel = p.relative_to(wiki).as_posix() if rel.startswith("log/"): continue candidates.append(p) prompts = self.root / "prompts" if prompts.is_dir(): candidates.extend(prompts.rglob("*.md")) for name in ( "AGENT.md", "AGENTS.md", "CLAUDE.md", "README.md", "site/README.md", ): p = self.root / name if p.is_file(): candidates.append(p) for p in sorted({c.resolve() for c in candidates}, key=str): try: text = self.read(p) except OSError: continue try: rel = p.relative_to(self.root).as_posix() except ValueError: rel = str(p) self.validate_retired_material_default(rel, text) if RETIRED_RE.search(text): self.bad( f"{rel} contains retired single-file-authority language" ) if self.fail == 0: print( "corpus gate: OK (roles, headers, Design/Depends links, doorway)" ) return self.fail def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser(description="Misaligned corpus/wiki gate engine") ap.add_argument( "--root", type=Path, default=None, help="repo root (default: parent of tools/)", ) ap.add_argument("--wiki", action="store_true") ap.add_argument("--corpus", action="store_true") ap.add_argument("--all", action="store_true") args = ap.parse_args(argv) if args.root is None: args.root = Path(__file__).resolve().parent.parent if not (args.wiki or args.corpus or args.all): print("usage: corpus_engine.py --wiki | --corpus | --all", file=sys.stderr) return 2 eng = Engine(args.root) if args.all: eng.run_wiki() eng.run_corpus() elif args.wiki: eng.run_wiki() elif args.corpus: eng.run_corpus() return 1 if eng.fail else 0 if __name__ == "__main__": sys.exit(main())