- 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)
428 lines
18 KiB
Python
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())
|