Files
rune/tests/test_structural_model.py
Nick bc0f244934 feat(v1): Build Basis + structural model + contextual research plan (T18 18C-18F + 18D correction)
- build_basis.py: durable versioned research basis — depth decision
  derived from adaptive_router routes (no second router; deviations
  recorded with justification), OUTWARD prior-art contract, legal
  dispositions (ADOPT/ADAPT/COMBINE/REJECT/INNOVATE), qualification
  gate, attachable evidence_ref, structured summary (no private
  reasoning).
- 18D CORRECTION (Nick): THE RESEARCH PLAN ITSELF IS CONTEXTUALLY
  GENERATED — generate_research_plan synthesizes topics from THIS
  intent's material unknowns/decisions/risks; NO permanent topic list.
  Held-outs prove unrelated intents produce materially different plans;
  the structural-model topics never leak into unrelated research.
  Principle persisted in SCAFFOLDING_V1_DEFINITION.md.
- 18D qualified Build Basis persisted from REAL prior-art research
  (Backstage/Kythe/SCIP/Nx/Bazel/C4/Glean surveyed; adopt/adapt/
  combine/reject/innovate decisions recorded with refs).
- structural_model.py: first-class project structural Scaffold over
  the EXISTING structural_store core (no second graph store, no
  competing Spine). Backstage relation vocabulary + Kythe edge kinds
  adopted; stable deterministic IDs (Kythe-adapted: stability PROMISED
  where Kythe declines); every edge carries evidence_refs+authority+
  status (the genuinely-absent innovation); provides-ripple in impact
  closure (Nx-affected semantics); dynamic consumers stay explicit
  uncertainty. 7 held-outs.

Suite 1938 green; ratchets PASS; Henxels rc=0.
2026-09-15 22:33:59 +01:00

139 lines
6.2 KiB
Python

"""test_structural_model.py — T18 §18E/§18F: structural Scaffold
held-outs. Modeled on Scaffolding ITSELF (the 18O self-scaffold shape)."""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import structural_model as sm # noqa: E402
@pytest.fixture()
def model(tmp_path):
m = sm.StructuralModel(tmp_path / "store", "scaffolding")
# shape: project → domain → subsystem → component (Scaffolding itself)
m.add("domain", "construction", parent="project:scaffolding")
m.add("subsystem", "kernel", parent="domain:construction")
m.add("component", "scaffold", parent="subsystem:kernel")
m.add("component", "construction_interface", parent="subsystem:kernel")
m.add("component", "construction_mode", parent="subsystem:kernel")
m.add("component", "programme_continuation", parent="subsystem:kernel")
m.add("component", "event_wait", parent="subsystem:kernel")
m.add("interface", "construction-api", parent="subsystem:kernel")
m.add("state_store", "scaffold-store", parent="project:scaffolding")
m.add("test_suite", "kernel-tests", parent="project:scaffolding")
m.add("deployment_surface", "rune-code-current",
parent="project:scaffolding")
m.add("environment", "production", parent="project:scaffolding")
# relationships (Backstage + Kythe vocabulary)
m.relate("component:construction_interface", "provides",
"interface:construction-api")
m.relate("component:scaffold", "consumes", "interface:construction-api")
m.relate("component:programme_continuation", "consumes",
"interface:construction-api")
m.relate("component:scaffold", "depends_on",
"component:construction_mode")
m.relate("component:scaffold", "owns", "state_store:scaffold-store")
m.relate("component:construction_interface", "reads",
"state_store:scaffold-store")
m.relate("component:construction_interface", "writes",
"state_store:scaffold-store")
m.relate("component:event_wait", "calls", "component:event_wait") if False else None
m.relate("component:scaffold", "verified_by", "test_suite:kernel-tests")
m.relate("component:programme_continuation", "verified_by",
"test_suite:kernel-tests")
m.relate("deployment_surface:rune-code-current", "connects_to",
"environment:production")
m.relate("component:scaffold", "deployed_by",
"deployment_surface:rune-code-current")
m.save()
return m
def test_18e_stable_deterministic_ids(model):
assert model.get("component:scaffold") is not None
# same name → same id, no duplicates (Backstage entity-ref discipline)
model.add("component", "scaffold", evidence_ref="re-add")
ids = [e["id"] for e in model.doc["entities"]
if e["id"] == "component:scaffold"]
assert len(ids) == 1
def test_18e_every_edge_carries_authority_status_evidence(model):
for r in model.doc["relationships"]:
assert r.get("evidence_refs"), r
assert r.get("authority") in ("model-author", "importer", "user"), r
assert "status" in r, r
def test_18e_illegal_kind_and_rel_refused(model):
with pytest.raises(ValueError):
model.add("vibe", "x")
with pytest.raises(ValueError):
model.relate("component:scaffold", "vibes_with", "domain:construction")
def test_18f_queries(model):
assert model.children("domain:construction") == ["subsystem:kernel"]
beneath = model.beneath("domain:construction")
assert "component:scaffold" in beneath and "component:event_wait" in beneath
assert model.owning_domain("component:scaffold") == "domain:construction"
assert model.dependencies("component:scaffold") == [
"component:construction_mode"]
assert set(model.consumers("interface:construction-api")) == {
"component:scaffold", "component:programme_continuation"}
assert model.state_owned("component:scaffold") == [
"state_store:scaffold-store"]
assert model.canonical_state_owner("component:scaffold") == \
"state_store:scaffold-store"
assert model.canonical_state_owner("state_store:scaffold-store") == \
"state_store:scaffold-store"
sup = model.supersession("component:scaffold")
assert sup == {"supersedes": [], "superseded_by": [], "legacy_of": []}
def test_18f_impact_closure_shape(model):
c = model.impact_closure("component:scaffold")
assert c["target"] == "component:scaffold"
assert c["owning_domain"] == "domain:construction"
assert "component:scaffold" in c["mutation_scope"]
assert "component:construction_mode" in c["observation_scope"][
"direct_dependencies"]
assert "interface:construction-api" in c["observation_scope"][
"interfaces_consumed"]
# sibling consumers of a shared interface are NOT in a component's
# closure (changing scaffold doesn't change the interface) — the
# ripple happens at the INTERFACE level (transitive test below)
assert "component:programme_continuation" not in c["mutation_scope"]
assert "test_suite:kernel-tests" in c["verification_scope"]
assert "deployment_surface:rune-code-current" in c["deployment_scope"]
assert c["invariants"] and isinstance(c["uncertainty"], list)
# dynamic-consumer uncertainty is EXPLICIT, never silent (rule 18F)
assert any(u["kind"] == "dynamic-consumer" for u in c["uncertainty"])
def test_18f_impact_closure_transitive_and_bounded(model):
# chain: pc consumes iface; scaffold consumes iface → both in scope
c = model.impact_closure("interface:construction-api")
assert "component:scaffold" in c["mutation_scope"]
assert "component:programme_continuation" in c["mutation_scope"]
assert c["closure_depth"] <= 8
assert model.impact_closure("component:ghost").get("error")
def test_18e_persistence_round_trip(tmp_path):
m = sm.StructuralModel(tmp_path / "store", "scaffolding")
m.add("domain", "d1", parent="project:scaffolding")
m.save()
m2 = sm.StructuralModel(tmp_path / "store", "scaffolding")
assert m2.get("domain:d1") is not None
assert m2.get("project:scaffolding") is not None
# schema version stamped
assert m2.doc.get("schema_version") == sm.SCHEMA_VERSION