Files
rune/scripts/check_research_gate.py
Nick 834eece4d7 feat(runtime+facade): batch landing — receipt presentation, compaction, composer depth, themes, Sleight wiring, gitignore hygiene, taxonomy
- K27 receipt presentation: full decision+evidence+effect packages,
  expandable sections, UNKNOWN-never-success in every dimension state,
  spine verdicts via evidence.Evaluate (read-only import)
- 2.6-depth compaction: append-per-message (O(1) tail line, 198 bytes
  steady-state), compaction every 64 appends (512/channel hot), cool
  archives per memory-continuity contract, nothing deleted, exact-by-ID
  across compaction+restart+tamper, legacy checkpoints heal on load
- K27 composer depth: 3-line input per old TUI contracts (§2.3 history
  walk, placeholder, stash/restore), deterministic natural-language
  resolution (40+ pinned phrases), palette natural-tier + ctrl+p bind,
  Accessibility profile (keyboard-only/no-color/reduced-motion)
- K27 themes: 10 fixed Go theme tables (byte-exact from Textual 8.2.8,
  not guessed; nord/dracula-default/monokai/solarized/catppuccin/tokyo-
  night/flexoki/gruvbox), /theme command (mirrors _cmd_theme exactly),
  live-editor data shape (staged per-token preview overrides), base bg
  rgb(30,30,40), no-drift drift test anchoring Go to retiring Python
- Sleight live wiring: operator._context_broker registers seventh
  contributor 'continuity' (bucket 2, fail-closed); K16 allowlist
  amended (memory_continuity.working_set); matrices now run against
  the live road; 102 tests green
- .gitignore hygiene: directory-level traps fixed (internal/ re-includes
  .go/.json/.md only; root-anchored /index/ /journal/ /state/; fixtures
  + testdata re-included at any depth); 6-test frozen gate; unified_check
  member wired; real git add (no -f) proven on testmod/internal
- Receipts, compaction, composer, themes, sleight-wiring, hygiene all
  under ADMIT contracts (101+ contracts, 0 research-gate violations)
- main.go NOT landed (chat-live P1 wave owns it, still in flight)
2026-10-17 22:00:03 +01:00

428 lines
18 KiB
Python

#!/usr/bin/env python3
"""check_research_gate.py — the research-first admission law, made mechanical.
Owner directive 2026-10-16 (docs/factory/spawn-queue.md): every feature
wave runs the canonical research protocol FIRST (ISSUE, INWARD, OUTWARD,
APPLICABILITY, INNOVATION-on-merit, ADVERSARIAL, SYNTHESIS, QUALIFY);
construction spawns only on QUALIFY: PROCEED; MORE_RESEARCH loops back
with named unknowns. This gate enforces that law at TWO layers, both
fail-closed:
LAYER 1 — ADMISSION (admit dispatch scripts call this BEFORE minting):
python scripts/check_research_gate.py admit \\
--slug <feature-slug> \\
--objective "<the contract objective>" \\
[--research-slug <existing-chain-slug>] \\
[--exemption "<category>: <reason>"]
Resolution order (first hit wins):
1. --exemption declared: category must be one of
mechanical-repair | test-doc-only | owner-directive (the three
lawful categories; owner-directive must quote the directive) —
ALLOW with the marker `exempt:research-gate:<category>: <reason>`
which the dispatch MUST embed in the contract's approval
evidence + evidence_refs (never silent).
2. research chain at research/<slug>/qualify.json (or the round's
terminal QUALIFY artifact), when --research-slug is absent.
3. --research-slug: research/<research-slug>/qualify.json, or the
registry entry (exact slug, then glob) mapping to its chain.
4. registry (docs/factory/research-registry.json) match on --slug.
Verdict law: PROCEED = ALLOW with the marker
`research-gate:ALLOW chain=<path> verdict=PROCEED slug=<slug>`;
MORE_RESEARCH = REFUSED, the named unknowns echoed verbatim; any
other/absent verdict = REFUSED naming it. No chain, no exemption =
REFUSED naming the missing research. Exit 0 = allowed, 1 = refused.
LAYER 2 — SWEEP (unified_check member `research_gate`):
python scripts/check_research_gate.py sweep
Scans store/scaffold/segments/entities/contract__ADMIT-*.json for
contracts in building phase; EVERY one must resolve to a research
artifact chain or a declared exemption, in this order:
1. ext.research_gate record on the contract (chain verified to
exist, or exemption);
2. markers in the contract's evidence (checkpoint evidence,
evidence_refs): `exempt:research-gate:...` or
`research-gate:ALLOW chain=<path>` (chain verified to exist);
3. the registry: contract slug (id-derived, ext.task_ref,
ext.activation.builder) matched exact-then-glob; the entry's
chain verified to exist, or its exemption record.
A building contract resolving to none of these = RED, named. A
missing or unparseable registry = RED (fail-closed). Exit 0 GREEN /
1 RED.
HOW ADMIT DISPATCH SCRIPTS USE IT (the pattern every new
scripts/admit_wave_*.py follows — see scripts/admit_wave_research_gate.py
for the working exemplar):
gate = subprocess.run([sys.executable,
"scripts/check_research_gate.py", "admit",
"--slug", SLUG, "--objective", OBJECTIVE,
"--research-slug", RESEARCH_SLUG], # or
#"--exemption", "category: reason"],
capture_output=True, text=True)
if gate.returncode != 0: # REFUSED before mint — the law
print(gate.stdout + gate.stderr)
return 1
marker = <the exempt:/ALLOW: line from gate.stdout> # embed BOTH in
approval_ref = f"user:{SLUG} dispatch; {marker}" # the approval
evidence_refs = [marker, ...] # evidence
Historical contracts minted before the gate resolve through the
registry's seeded entries (each carries its chain + verdict or its
exemption record of why no chain was required).
"""
from __future__ import annotations
import argparse
import fnmatch
import json
import re
import sys
from pathlib import Path
EXEMPTION_CATEGORIES = ("mechanical-repair", "test-doc-only", "owner-directive")
ALLOW_MARKER = "research-gate:ALLOW"
EXEMPT_MARKER = "exempt:research-gate:"
ALLOW_CHAIN_RE = re.compile(re.escape(ALLOW_MARKER) + r"[^\n]*?chain=(\S+)")
TIMESTAMP_ID_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T[\d:]+Z-[0-9a-f]{6}-(.+)$")
REPO = Path(__file__).resolve().parent.parent
REGISTRY_REL = "docs/factory/research-registry.json"
# ── shared resolution machinery ────────────────────────────────────────────
def load_registry(repo: Path) -> dict:
p = repo / REGISTRY_REL
if not p.is_file():
raise FileNotFoundError(f"{REGISTRY_REL} missing — the research "
"registry is mandatory (fail-closed)")
doc = json.loads(p.read_text())
if not isinstance(doc.get("entries"), list):
raise ValueError(f"{REGISTRY_REL}: entries must be a list")
return doc
def registry_entry_for(slug: str, registry: dict) -> dict | None:
"""Exact-slug match first, then glob patterns (deterministic order)."""
for e in registry["entries"]:
if e.get("slug") == slug:
return e
for e in registry["entries"]:
for pat in e.get("match", []):
if fnmatch.fnmatchcase(slug, pat):
return e
return None
def terminal_artifact(repo: Path, research_slug: str) -> Path | None:
"""research/<slug>/qualify.json, or the round's terminal QUALIFY
artifact (a *.json in the round dir whose stage is QUALIFY)."""
d = repo / "research" / research_slug
if not d.is_dir():
return None
q = d / "qualify.json"
if q.is_file():
return q
for p in sorted(d.glob("*.json")):
try:
if json.loads(p.read_text()).get("stage") == "QUALIFY":
return p
except (OSError, json.JSONDecodeError):
continue
return None
def artifact_verdict(path: Path) -> tuple[str | None, list[str]]:
"""(verdict, named_unknowns) from a terminal artifact. The artifact is
authoritative — the registry's recorded verdict never overrides it."""
try:
doc = json.loads(path.read_text())
except (OSError, json.JSONDecodeError) as exc:
return None, [f"unparseable artifact: {exc}"]
verdict = doc.get("verdict") or doc.get("outcome")
unknowns: list[str] = []
for key in ("blocking_unknowns", "named_unknowns", "unknowns"):
v = doc.get(key)
if isinstance(v, list):
unknowns.extend(str(x) for x in v)
elif isinstance(v, str) and v.strip():
unknowns.append(v.strip())
elif isinstance(v, dict):
unknowns.extend(f"{k}: {val}" for k, val in v.items())
return (str(verdict).strip() if verdict else None), unknowns
def parse_exemption(raw: str) -> tuple[str, str] | None:
"""'category: reason' -> (category, reason); None when the category is
not one of the three lawful ones (an undeclared-category exemption is
a silent exemption — refused)."""
if ":" not in raw:
return None
cat, reason = raw.split(":", 1)
cat, reason = cat.strip(), reason.strip()
if cat not in EXEMPTION_CATEGORIES or not reason:
return None
return cat, reason
# ── layer 1: admission ─────────────────────────────────────────────────────
def cmd_admit(repo: Path, slug: str, objective: str,
research_slug: str | None, exemption: str | None) -> int:
def refuse(lines: list[str]) -> int:
print(f"REFUSED research-gate admit slug={slug}")
for line in lines:
print(f" {line}")
print("VERDICT: RED")
return 1
if exemption is not None:
parsed = parse_exemption(exemption)
if parsed is None:
return refuse([
f"exemption category not declared: an exemption must be "
f"'<category>: <reason>' with category one of "
f"{', '.join(EXEMPTION_CATEGORIES)} (never silent)",
f"got: {exemption[:200]}",
])
cat, reason = parsed
marker = f"{EXEMPT_MARKER}{cat}: {reason}"
print(f"ALLOW research-gate admit slug={slug} (exemption: {cat})")
print(marker)
print(f"embed this marker in the contract approval evidence + "
f"evidence_refs (approval_ref='user:<slug> dispatch; {marker}')")
print("VERDICT: GREEN")
return 0
# research chain resolution: an explicit --research-slug is a citation
# — it must resolve itself (fail-closed, no silent fallback to --slug);
# otherwise the slug resolves directly
candidates = [research_slug] if research_slug else [slug]
seen: set[str] = set()
chain: Path | None = None
tried: list[str] = []
registry: dict | None = None
for cand in candidates:
if cand in seen:
continue
seen.add(cand)
art = terminal_artifact(repo, cand)
if art is not None:
chain = art
break
if registry is None:
try:
registry = load_registry(repo)
except (OSError, ValueError) as exc:
return refuse([f"registry unusable: {exc}",
f"missing research: research/{slug}/ "
f"(and no exemption declared)"])
entry = registry_entry_for(cand, registry)
if entry and entry.get("chain"):
p = repo / entry["chain"]
if p.is_file():
chain = p
break
tried.append(f"registry chain for '{cand}' missing on disk: "
f"{entry['chain']}")
elif entry and entry.get("exemption"):
# a registry-recorded exemption is the historical record, not
# the dispatch's declaration — admission must declare its own
tried.append(f"registry carries only an exemption record for "
f"'{cand}' ({entry['exemption'].get('category')}); "
f"a new admission must declare --exemption itself")
else:
tried.append(f"research/{cand}/ not found")
if chain is None:
return refuse([
f"missing research: research/{slug}/ has no terminal QUALIFY "
f"artifact and no registry entry resolves a chain",
*tried[:4],
"declare --research-slug <existing chain>, or --exemption "
"'<category>: <reason>', or run the research round first",
])
verdict, unknowns = artifact_verdict(chain)
rel = str(chain.relative_to(repo))
if verdict == "PROCEED":
marker = (f"{ALLOW_MARKER} chain={rel} verdict=PROCEED slug={slug}")
print(f"ALLOW research-gate admit slug={slug}")
print(marker)
print(f"embed this marker in the contract approval evidence + "
f"evidence_refs (approval_ref='user:<slug> dispatch; {marker}')")
print("VERDICT: GREEN")
return 0
if verdict == "MORE_RESEARCH":
return refuse([
f"research chain {rel} verdict is MORE_RESEARCH — the build "
f"wave does not spawn; loop back with the named unknowns:",
*([f"unknown: {u}" for u in unknowns] or
["unknowns: (none named in the artifact — the round is "
"incomplete; refuse stands)"]),
])
return refuse([
f"research chain {rel} verdict is {verdict!r}, not PROCEED — "
f"construction spawns only on QUALIFY: PROCEED",
])
# ── layer 2: post-hoc sweep ────────────────────────────────────────────────
def contract_slug(cid: str) -> str:
"""'ADMIT-2026-10-16T09:16:01Z-ddb972-wave-G8-changes' ->
'wave-G8-changes'; plain ids pass through."""
body = cid[len("ADMIT-"):] if cid.startswith("ADMIT-") else cid
m = TIMESTAMP_ID_RE.match(body)
return m.group(1) if m else body
def resolve_contract(repo: Path, doc: dict, registry: dict) -> tuple[str, str]:
"""(resolution, detail) — 'chain:<path>' | 'exemption:<...>' |
'UNRESOLVED' with the failure detail."""
cid = str(doc.get("id", "?")).removeprefix("contract:")
ext = doc.get("ext", {}) if isinstance(doc.get("ext"), dict) else {}
rg = ext.get("research_gate")
if isinstance(rg, dict):
if rg.get("chain"):
p = repo / str(rg["chain"])
if p.is_file():
return f"chain:{rg['chain']}", "ext.research_gate record"
return "UNRESOLVED", (f"ext.research_gate chain missing on "
f"disk: {rg['chain']}")
if rg.get("exemption"):
return (f"exemption:{rg['exemption']}",
"ext.research_gate record")
evidence_strings = [str(e) for e in doc.get("evidence_refs", [])]
for cp in doc.get("checkpoints", []):
evidence_strings.append(str(cp.get("evidence", "")))
evidence_strings.append(str(cp.get("note", "")))
text = "\n".join(evidence_strings)
for line in evidence_strings:
if EXEMPT_MARKER in line:
marker = line[line.index(EXEMPT_MARKER):][:200]
return f"exemption:{marker}", "declared in contract evidence"
m = ALLOW_CHAIN_RE.search(text)
if m:
p = repo / m.group(1)
if p.is_file():
return f"chain:{m.group(1)}", "ALLOW marker in contract evidence"
return "UNRESOLVED", f"evidence ALLOW chain missing on disk: {m.group(1)}"
slugs = []
for s in (contract_slug(cid), ext.get("task_ref"),
(ext.get("activation") or {}).get("builder")):
if s and s not in slugs:
slugs.append(str(s))
for s in slugs:
entry = registry_entry_for(s, registry)
if not entry:
continue
if entry.get("chain"):
p = repo / entry["chain"]
if p.is_file():
return (f"chain:{entry['chain']}",
f"registry[{entry['slug']}] via '{s}'")
return "UNRESOLVED", (f"registry[{entry['slug']}] chain "
f"missing on disk: {entry['chain']}")
if entry.get("exemption"):
return (f"exemption:{entry['exemption'].get('category')}",
f"registry[{entry['slug']}] via '{s}'")
return ("UNRESOLVED",
f"no research artifact chain and no declared exemption "
f"(missing research: research/{slugs[0] if slugs else cid}/)")
def cmd_sweep(repo: Path, as_json: bool = False) -> int:
entities = repo / "store" / "scaffold" / "segments" / "entities"
results: list[dict] = []
fatal: list[str] = []
registry: dict | None = None
try:
registry = load_registry(repo)
except (OSError, ValueError) as exc:
fatal.append(str(exc))
contracts: list[dict] = []
if entities.is_dir():
for p in sorted(entities.glob("contract__ADMIT-*.json")):
try:
doc = json.loads(p.read_text())
except (OSError, json.JSONDecodeError) as exc:
fatal.append(f"unparseable contract {p.name}: {exc}")
continue
if doc.get("phase") == "building":
contracts.append(doc)
else:
fatal.append(f"contract store missing: {entities}")
violations = 0
lines: list[str] = []
for doc in contracts:
cid = str(doc.get("id", "?")).removeprefix("contract:")
if registry is None:
res, detail = "UNRESOLVED", "registry unusable"
else:
res, detail = resolve_contract(repo, doc, registry)
ok = not res.startswith("UNRESOLVED")
violations += 0 if ok else 1
results.append({"contract": cid, "resolution": res, "via": detail})
lines.append(f"{'GREEN' if ok else 'RED'} {cid} -> {res} ({detail})")
if as_json:
print(json.dumps({"verdict": "RED" if (violations or fatal) else "GREEN",
"contracts": len(contracts), "violations": violations,
"fatal": fatal, "results": results}, indent=1))
else:
for line in lines:
print(line)
for f in fatal:
print(f"RED {f}")
verdict = "RED" if (violations or fatal) else "GREEN"
print(f"{len(contracts)} building ADMIT contracts, {violations} "
f"violation(s), {len(fatal)} fatal condition(s)")
print(f"VERDICT: {verdict}")
return 0 if verdict == "GREEN" else 1
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
sub = ap.add_subparsers(dest="cmd", required=True)
adm = sub.add_parser("admit", help="admission-time gate (pre-mint)")
adm.add_argument("--slug", required=True, help="feature slug")
adm.add_argument("--objective", required=True,
help="the contract objective (quoted)")
adm.add_argument("--research-slug", default=None,
help="existing research chain the wave cites (e.g. "
"tui-frontier-r2 for boundary work)")
adm.add_argument("--exemption", default=None,
help="'<category>: <reason>' — mechanical-repair | "
"test-doc-only | owner-directive")
adm.add_argument("--repo", default=str(REPO),
help="repo root (default: this checkout)")
swp = sub.add_parser("sweep", help="post-hoc store sweep (unified_check)")
swp.add_argument("--json", action="store_true")
swp.add_argument("--repo", default=str(REPO),
help="repo root (default: this checkout)")
args = ap.parse_args(argv)
repo = Path(args.repo).resolve()
if args.cmd == "admit":
return cmd_admit(repo, args.slug, args.objective,
args.research_slug, args.exemption)
return cmd_sweep(repo, as_json=args.json)
if __name__ == "__main__":
sys.exit(main())