Open-world entity/relationship store (schema_version 1): kinds, relationship types and metadata are open tags — unknown future capabilities enter as data, never as core edits (extensibility test included, synthetic capability round-trips with zero core change). Evidence-gated maturity ladder DECLARED<PRESENT<WIRED<REACHABLE<EXERCISED<LIVE_PROVEN — no evidence, no promotion; demotion rejected and recorded. Umbrella migration Phase A→C: manifest ingested once (generated-view refusal guard prevents the ingest cycle; manual truth archived at config/umbrellas.manual.json), canaries preserved as obligated_canary relationships, config/umbrellas.json now a GENERATED compatibility view for legacy consumers (CI umbrella_impact gate + henxels) — single-authority route proven by registering module:spine through Spine alone (one store edit; the legacy view flows out; contract tests validate the round-trip). Dynamic observations: deployed identity (_build_identity.json), drift classes incl. registered_but_absent, present_but_unowned, registered_no_static_importers (D6 dead-subnet now mechanical), version_surfaces_disagree, relationship_disappeared. Deterministic snapshots + diff. Scaffolding V1 now consumes Spine for ownership/deps/ consumers (spine_maturity surfaced in every card). 31 spine tests; full gates green.
206 lines
8.3 KiB
Python
Executable File
206 lines
8.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""scaffold_projection.py — Scaffolding V1 Tranche 1 (projection, not authority).
|
|
|
|
Answers, per component/module:
|
|
what subsystem owns X? → SPINE store (structural authority)
|
|
what does X depend on? → SPINE relationships (ingested from AST)
|
|
who consumes X? → SPINE relationships (ingested from AST)
|
|
what changes if X changes? → mirrors + dependent subsystems + consumers (SPINE)
|
|
what proof obligations arise? → unit test, canary, ownership, umbrella gates
|
|
which obligations are complete? → evidence check (test file exists, canary registered)
|
|
|
|
SPINE INTEGRATION: structural facts come from the Spine store — the single
|
|
structural authority (ingested once from the umbrella manifest + AST).
|
|
This script adds change-impact and proof-obligation reasoning on top; it
|
|
no longer re-derives structure. No store → builds one (deterministic).
|
|
|
|
Commands:
|
|
component <module> full card for one src/rune module
|
|
impact <module> expected impact + obligations BEFORE a change
|
|
obligations <module> obligation checklist with satisfied/pending state
|
|
all [outfile] write the full projection JSON (default docs/)
|
|
"""
|
|
from __future__ import annotations
|
|
import ast, json, sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
SRC = ROOT / "src" / "rune"
|
|
TESTS = ROOT / "tests"
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
from rune import spine # noqa: E402 — Spine IS the structural authority
|
|
|
|
|
|
def load_spine() -> dict:
|
|
"""Structural state from the Spine store (built on first use)."""
|
|
doc = spine.load()
|
|
if doc is None:
|
|
doc = spine.build()
|
|
spine.save(doc)
|
|
return doc
|
|
|
|
|
|
def module_imports(module: str) -> set[str]:
|
|
"""Module names imported by src/rune/<module>.py (intra-package).
|
|
|
|
Includes relative imports (`from .x import y` — the dual-mode lazy
|
|
pattern used across the runtime; missing these would hide real
|
|
consumers, observed first run: rune_operator ↔ local_tool_runtime)."""
|
|
path = SRC / f"{module}.py"
|
|
if not path.is_file():
|
|
return set()
|
|
tree = ast.parse(path.read_text())
|
|
names: set[str] = set()
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
for a in node.names:
|
|
names.add(a.name.split(".")[0])
|
|
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
names.add(node.module.split(".")[0])
|
|
return names
|
|
|
|
|
|
def import_graph() -> dict[str, set[str]]:
|
|
return {p.stem: module_imports(p.stem) for p in SRC.glob("*.py")}
|
|
|
|
|
|
def consumers_of(module: str, graph: dict[str, set[str]]) -> set[str]:
|
|
return {m for m, deps in graph.items() if module in deps and m != module}
|
|
|
|
|
|
def subsystem_for(doc: dict, module: str) -> str:
|
|
rels = spine.relationships(doc, from_id=f"module:{module}", type="belongs_to")
|
|
return rels[0]["to"].split(":", 1)[1] if rels else ""
|
|
|
|
|
|
def dependent_subsystems(doc: dict, subsystem: str) -> list[str]:
|
|
return [r["from"].split(":", 1)[1] for r in
|
|
spine.relationships(doc, to_id=f"subsystem:{subsystem}", type="depends_on")]
|
|
|
|
|
|
def mirrors_for(doc: dict, module: str) -> list[str]:
|
|
subs = spine.relationships(doc, from_id=f"module:{module}", type="belongs_to")
|
|
out = []
|
|
for s in subs:
|
|
out.extend(r["to"].split(":", 1)[1] for r in
|
|
spine.relationships(doc, from_id=s["to"], type="mirrors"))
|
|
return sorted(set(out))
|
|
|
|
|
|
def canary_registered(module: str) -> bool:
|
|
try:
|
|
sys.path.insert(0, str(SRC))
|
|
from rune import canary_native
|
|
return module in canary_native.MODULES
|
|
except ImportError:
|
|
# canary_native pulls optional deps (sqlite_vec etc.); the projection
|
|
# degrades to "not registered" rather than failing the audit.
|
|
return False
|
|
|
|
|
|
def obligations(module: str, doc: dict) -> list[dict]:
|
|
"""Proof obligations for changing this module, with evidence state.
|
|
Structural facts sourced from Spine."""
|
|
umb = subsystem_for(doc, module)
|
|
test_file = TESTS / f"test_{module}.py"
|
|
obs = [
|
|
{"obligation": "unit_test", "satisfied": test_file.is_file(),
|
|
"evidence": f"tests/test_{module}.py" + ("" if test_file.is_file() else " (MISSING)")},
|
|
{"obligation": "canary", "satisfied": canary_registered(module),
|
|
"evidence": "canary_native.MODULES" if canary_registered(module)
|
|
else "not in canary registry (informational — not every module needs one)"},
|
|
{"obligation": "umbrella_manifest", "satisfied": bool(umb),
|
|
"evidence": f"umbrella '{umb}'" if umb else "NOT in any umbrella — manifest gap"},
|
|
{"obligation": "full_suite", "satisfied": None,
|
|
"evidence": "run: pytest tests/ -q (runtime evidence, not static)"},
|
|
{"obligation": "lint_ratchet", "satisfied": None,
|
|
"evidence": "run: scripts/lint_ratchet.py"},
|
|
{"obligation": "henxels", "satisfied": None,
|
|
"evidence": "run: henxels check --all"},
|
|
]
|
|
if umb:
|
|
deps = dependent_subsystems(doc, umb)
|
|
if deps:
|
|
obs.append({"obligation": "dependent_canaries", "satisfied": None,
|
|
"evidence": f"subsystems depending on '{umb}': {', '.join(deps)} — their canaries gate the change"})
|
|
mir = mirrors_for(doc, module)
|
|
if mir:
|
|
obs.append({"obligation": "mirrors_travel", "satisfied": None,
|
|
"evidence": f"files that must travel together: {', '.join(mir)}"})
|
|
return obs
|
|
|
|
|
|
def component_card(module: str, doc: dict) -> dict:
|
|
"""Structural card from SPINE (owner/deps/consumers/mirrors) + obligations."""
|
|
deps = [r["to"].split(":", 1)[1] for r in
|
|
spine.relationships(doc, from_id=f"module:{module}", type="depends_on")]
|
|
consumers = [r["to"].split(":", 1)[1] for r in
|
|
spine.relationships(doc, from_id=f"module:{module}", type="consumed_by")]
|
|
e = spine.entity(doc, f"module:{module}")
|
|
card = {
|
|
"component": module,
|
|
"subsystem": subsystem_for(doc, module),
|
|
"dependencies": sorted(set(deps)),
|
|
"consumers": sorted(set(consumers)),
|
|
"mirrors": mirrors_for(doc, module),
|
|
"obligations": obligations(module, doc),
|
|
}
|
|
if e:
|
|
card["spine_maturity"] = e.get("maturity")
|
|
card["spine_evidence"] = e.get("evidence_refs", [])
|
|
return card
|
|
|
|
|
|
def impact(module: str) -> dict:
|
|
doc = load_spine()
|
|
card = component_card(module, doc)
|
|
umb = card["subsystem"]
|
|
card["expected_impact"] = {
|
|
"direct_consumers": card["consumers"],
|
|
"subsystem_dependents": dependent_subsystems(doc, umb) if umb else [],
|
|
"must_travel": card["mirrors"],
|
|
"gates": ["full_suite", "lint_ratchet", "henxels", "umbrella_gates"]
|
|
+ (["canary"] if card and any(o["obligation"] == "canary" and o["satisfied"]
|
|
for o in card["obligations"]) else []),
|
|
}
|
|
return card
|
|
|
|
|
|
def main() -> int:
|
|
args = sys.argv[1:]
|
|
if not args:
|
|
print(__doc__)
|
|
return 2
|
|
cmd = args[0]
|
|
if cmd in ("component", "impact", "obligations") and len(args) < 2:
|
|
print(f"usage: {cmd} <module>", file=sys.stderr)
|
|
return 2
|
|
doc = load_spine()
|
|
if cmd == "impact":
|
|
print(json.dumps(impact(args[1]), indent=1))
|
|
elif cmd == "component":
|
|
print(json.dumps(component_card(args[1], doc), indent=1))
|
|
elif cmd == "obligations":
|
|
card = component_card(args[1], doc)
|
|
print(json.dumps(card["obligations"], indent=1))
|
|
elif cmd == "all":
|
|
out = Path(args[1]) if len(args) > 1 else ROOT / "docs" / "scaffold_projection.json"
|
|
cards = {p.stem: component_card(p.stem, doc)
|
|
for p in sorted(SRC.glob("*.py"))
|
|
if spine.entity(doc, f"module:{p.stem}")}
|
|
out.write_text(json.dumps({
|
|
"generated_by": "scripts/scaffold_projection.py (projection — no new authority)",
|
|
"sources": ["rune.spine store (structural authority)", "src/rune/canary_native.py",
|
|
"tests/ filesystem"],
|
|
"components": cards,
|
|
}, indent=1) + "\n")
|
|
print(f"projection written: {out} ({len(cards)} components)")
|
|
else:
|
|
print(f"unknown command: {cmd}", file=sys.stderr)
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|