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
- 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.
204 lines
8.9 KiB
Python
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"
|