Files
rune/tests/test_v19_ledgers.py
T
Nick 94a4774853
CI Pipeline / best-practices (push) Successful in 11s
CI Pipeline / canary (push) Successful in 34s
CI Pipeline / lint (push) Successful in 46s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / documentation (push) Successful in 4s
Auto-version and Release / release (push) Failing after 1m11s
CI Pipeline / gitea-offline (push) Successful in 19s
CI Pipeline / unit-tests (push) Successful in 2m50s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 4s
deploy / deploy (push) Successful in 2m35s
feat(t19): project registry + capability/behaviour ledgers + iteration engine (19A-19I)
- 19A: /build contract-mint degradation paths REMOVED (fail-closed);
  4 held-outs prove no silent governance bypass (failure paths, not
  happy paths).
- 19C: project_registry.py — discovery/index ONLY; references-not-
  truth boundaries proven (no entities in entries, no Spine leakage).
- 19D: capability_ledger.py — stable IDs surviving moves, full
  history on material_change/move/supersede/retire, bounded
  context_packet.
- 19F/G: behaviour_ledger.py — versioned Behaviour/Test Specs,
  hardening lifecycle (UNVALIDATED→…→USER_CONFIRMED/RETEST_REQUIRED),
  generation-aware history PRESERVED through invalidation, automated
  green never implies USER_CONFIRMED.
- 19I: selective invalidation — only dependency-overlapping
  confirmations reopen, every invalidation carries a reason,
  unrelated hardened behaviours preserved (19S-2/5 proven).
- 19B: QUALIFIED Build Basis from prior-art research (structural
  closure ADOPT as sound backbone; generation-pinned confirmations
  ADOPT; predicted-vs-observed feedback loop INNOVATE; context
  packets COMBINE derived+curated; embedding-first REJECTED).
- iteration.py: context_packet, predictive/descriptive impact,
  impact-delta persistence, machine-derived post-change obligations.
2026-09-16 00:54:34 +01:00

204 lines
8.9 KiB
Python

"""test_v19_ledgers.py — T19 §19C/19D/19F/19G/19I held-outs: registry
boundaries, capability ledgers, behaviour ledger, selective invalidation."""
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 behaviour_ledger as bl # noqa: E402
from rune import capability_ledger as cl # noqa: E402
from rune import iteration as it # noqa: E402
from rune import project_registry as pr # noqa: E402
from rune import structural_model as sm # noqa: E402
# ── 19C: registry + authority boundaries ────────────────────────────────
def test_19c_registry_round_trip(tmp_path):
r = pr.ProjectRegistry(tmp_path)
r.register(project_id="alpha", scaffold_location="/tmp/alpha/scaffold",
repo_identity="alpha-repo", forge_repo="conductor/alpha",
structural_schema_version=1)
r.save()
r2 = pr.ProjectRegistry(tmp_path)
assert r2.get("alpha")["forge_repo"] == "conductor/alpha"
assert len(r2.all_projects()) == 1
def test_19c_registry_stores_references_not_structural_truth(tmp_path):
r = pr.ProjectRegistry(tmp_path)
r.register(project_id="alpha", scaffold_location="/x",
structural_schema_version=1)
# the registry entry must NOT contain entities/relationships —
# project structural truth lives in the project's own store
entry = r.get("alpha")
assert "entities" not in entry and "relationships" not in entry
assert entry["scaffold_location"] == "/x" # a pointer
def test_19c_registry_does_not_touch_spine(tmp_path):
"""An external project registered in the registry must NOT appear as
Rune Spine structure (rule 19C)."""
r = pr.ProjectRegistry(tmp_path)
r.register(project_id="external-thing", scaffold_location="/y")
import json
spine = json.loads((REPO / "store/spine/current.json").read_text())
spine_ids = {e["id"] for e in spine["entities"]}
assert "project:external-thing" not in spine_ids
assert "domain:external-thing" not in spine_ids
def test_19c_migration_sweep(tmp_path):
r = pr.ProjectRegistry(tmp_path)
r.register(project_id="old", scaffold_location="/o",
structural_schema_version=1)
r.register(project_id="new", scaffold_location="/n",
structural_schema_version=2)
assert r.needs_migration(proposed_schema_version=2) == ["old"]
assert r.needs_migration(proposed_schema_version=1) == []
# ── 19D: capability ledger ────────────────────────────────────────────────
def _cap(ledger, cid="widget-core", sources=("src/widget.py",)):
return ledger.define(
capability_id=cid, name="Widget core", purpose="core widget logic",
owning_project="demo", component="component:widget",
source_locations=list(sources), contract="in→out, pure",
invariants=["no global state"],
behaviour_specs=["behaviour:widget-core-behaves"])
def test_19d_capability_lifecycle_and_history(tmp_path):
l = cl.CapabilityLedger(tmp_path)
_cap(l)
l.material_change("widget-core", note="rewrote inner loop",
generation="gen-2")
l.move("widget-core", ["src/widgets/widget.py"]) # stable id survives
l.supersede("widget-core", by="widget-core-v2", note="replaced")
assert l.get("widget-core")["maturity"] == "superseded"
assert l.get("widget-core")["source_locations"] == ["src/widgets/widget.py"]
events = [h["event"] for h in l.doc["history"]]
assert events == ["material_change", "moved", "superseded"]
def test_19d_context_packet_is_bounded(tmp_path):
l = cl.CapabilityLedger(tmp_path)
_cap(l)
pkt = l.context_packet("widget-core")
assert pkt["capability_id"] == "capability:widget-core"
assert "behaviour_specs" in pkt and "invariants" in pkt
# no internal doc dump leaks
assert "history" not in pkt and "capabilities" not in pkt
def test_19d_unknown_capability_refused(tmp_path):
l = cl.CapabilityLedger(tmp_path)
with pytest.raises(ValueError):
l.material_change("ghost", note="x")
# ── 19F/G/I: behaviour ledger ─────────────────────────────────────────────
@pytest.fixture()
def world(tmp_path):
model = sm.StructuralModel(tmp_path / "store", "demo")
model.add("domain", "d", parent="project:demo")
model.add("component", "event_bridge", parent="domain:d")
model.add("component", "consumer_b", parent="domain:d")
model.add("component", "unrelated", parent="domain:d")
model.relate("component:consumer_b", "consumes",
"interface:event_completion")
model.add("interface", "event_completion", parent="domain:d")
model.relate("component:event_bridge", "provides",
"interface:event_completion")
model.save()
beh = bl.BehaviourLedger(tmp_path / "store")
beh.define(behaviour_id="VAL-001", owning_project="demo",
intended_behaviour="bridge completes events",
owner_component="component:event_bridge",
dependencies=["component:event_bridge",
"interface:event_completion"])
beh.define(behaviour_id="VAL-002", owning_project="demo",
intended_behaviour="consumer B handles completion",
owner_component="component:consumer_b",
dependencies=["component:consumer_b",
"interface:event_completion"])
beh.define(behaviour_id="VAL-003", owning_project="demo",
intended_behaviour="unrelated hardening",
owner_component="component:unrelated",
dependencies=["component:unrelated"])
for vid in ("VAL-001", "VAL-002", "VAL-003"):
beh.record_auto_verification(vid, passed=True, generation="gen-1")
beh.record_real_world(vid, passed=True, detail="user confirmed",
generation="gen-1")
model.save()
return model, beh
def test_19g_generation_history_preserved_on_invalidation(world):
model, beh = world
# confirm at gen-1 (fixture), then dependency changes at gen-2
beh.invalidate_for_change(
changed_entities={"component:event_bridge"}, generation="gen-2")
rec = beh.get("VAL-001")
assert rec["state"] == bl.RETEST_REQUIRED
# history still shows the gen-1 confirmation (rule 19G, mandatory)
hist = beh.retest_history("VAL-001")
gens = [h["generation"] for h in hist]
assert "gen-1" in gens and "gen-2" in gens
assert any(h["event"].endswith("USER_CONFIRMED") for h in hist)
def test_19i_interface_ripple_selective(world):
"""19S-2/5: bridge changes → bridge behaviour AND consumer-B reopen;
unrelated hardened behaviour stays."""
model, beh = world
beh.invalidate_for_change(
changed_entities={"component:event_bridge",
"interface:event_completion"},
generation="gen-2")
assert beh.get("VAL-001")["state"] == bl.RETEST_REQUIRED
assert beh.get("VAL-002")["state"] == bl.RETEST_REQUIRED # interface ripple
assert beh.get("VAL-003")["state"] == bl.USER_CONFIRMED # PRESERVED
# every invalidation carries a reason
for rec in (beh.get("VAL-001"), beh.get("VAL-002")):
assert rec["invalidations"][-1]["because"]
def test_19i_local_change_no_false_global_invalidation(world):
model, beh = world
beh.invalidate_for_change(
changed_entities={"component:unrelated"}, generation="gen-2")
assert beh.get("VAL-001")["state"] == bl.USER_CONFIRMED
assert beh.get("VAL-002")["state"] == bl.USER_CONFIRMED
assert beh.get("VAL-003")["state"] == bl.RETEST_REQUIRED
def test_19w22_automated_green_never_user_confirmed(world):
model, beh = world
beh.define(behaviour_id="VAL-004", owning_project="demo",
intended_behaviour="new thing")
beh.record_auto_verification("VAL-004", passed=True, generation="g")
assert beh.get("VAL-004")["state"] == bl.AUTO_VERIFIED
assert beh.get("VAL-004")["state"] != bl.USER_CONFIRMED
def test_19m_dynamic_queue(world):
model, beh = world
# all three confirmed → queue empty
assert beh.outstanding() == []
beh.invalidate_for_change(
changed_entities={"component:event_bridge"}, generation="gen-2")
# VAL-001 reopened; VAL-002 depends via interface only in its deps —
# this closure only touched the bridge component
outstanding = [b["behaviour_id"] for b in beh.outstanding()]
assert "behaviour:VAL-001" in outstanding
assert "behaviour:VAL-003" not in outstanding # unrelated stays green
# FAIL items sort first (priority)
beh.record_real_world("VAL-001", passed=False, detail="broke in prod")
assert beh.outstanding()[0]["behaviour_id"] == "behaviour:VAL-001"