Wall-clock timestamps are metadata, not causal ordering (same-second events share a stamp — the T21 race). Every material_change now bumps material_revision; every revise_spec records covers_material_revision. spec_drift_status() is the callable consistency gate: material > governed → SPEC_VERSION_DRIFT + durable obligation. 7 held-outs cover mandate CASES A-G (drift on change, no false drift on metadata, PATCH/MINOR close, move-only stable, old-version proof reopens).
122 lines
5.1 KiB
Python
122 lines
5.1 KiB
Python
"""test_spec_drift_deterministic.py — T21.1 §21.1D: deterministic
|
|
spec/version drift via revision counters (never wall-clock).
|
|
|
|
CASES A-G from the mandate, against the REAL detector."""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
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
|
|
|
|
|
|
def _cap(l, cid="drift-target"):
|
|
l.define(capability_id=cid, name="D", purpose="p", owning_project="r",
|
|
component="component:d")
|
|
cl.attach_spec(l, cid, requirements=["r"], expected_behaviours=["b"],
|
|
non_goals=[], acceptance_criteria=["a"])
|
|
return l.get(cid)
|
|
|
|
|
|
def test_211d_a_material_change_without_decision_is_drift(tmp_path):
|
|
"""CASE A: implementation changes, no spec/version decision →
|
|
deterministic SPEC_VERSION_DRIFT + durable obligation."""
|
|
l = cl.CapabilityLedger(tmp_path)
|
|
_cap(l)
|
|
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
|
|
l.material_change("drift-target", note="rewrote internals",
|
|
generation="gen-2")
|
|
st = cl.spec_drift_status(l, "drift-target")
|
|
assert st["drift"] is True
|
|
assert "SPEC_VERSION_DRIFT" in st["obligation"]
|
|
assert st["detail"]["material_revision"] == 1
|
|
assert st["detail"]["governed_revision"] == 0
|
|
|
|
|
|
def test_211d_b_spec_change_without_semver_is_impossible(tmp_path):
|
|
"""CASE B: there is no code path that changes spec content without
|
|
a version decision — revise_spec always computes a kind. A direct
|
|
spec edit bypasses the machinery; the detector catches the RESULT
|
|
via the revision gap (see CASE A)."""
|
|
l = cl.CapabilityLedger(tmp_path)
|
|
_cap(l)
|
|
l.material_change("drift-target", note="spec text edited by hand",
|
|
generation="gen-3")
|
|
assert cl.spec_drift_status(l, "drift-target")["drift"] is True
|
|
|
|
|
|
def test_211d_c_metadata_only_no_drift(tmp_path):
|
|
"""CASE C: non-material edits (limitations notes, constraints) do
|
|
not touch material_revision → no false drift."""
|
|
l = cl.CapabilityLedger(tmp_path)
|
|
rec = _cap(l)
|
|
rec["limitations"].append("known edge case")
|
|
rec["constraints"] = "new note"
|
|
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
|
|
|
|
|
|
def test_211d_d_patch_closes_drift(tmp_path):
|
|
"""CASE D: correct PATCH revision closes the drift."""
|
|
l = cl.CapabilityLedger(tmp_path)
|
|
_cap(l)
|
|
l.material_change("drift-target", note="bug fix", generation="gen-2")
|
|
assert cl.spec_drift_status(l, "drift-target")["drift"] is True
|
|
r = cl.revise_spec(l, "drift-target", implementation_only=True,
|
|
reason="fixed inner bug")
|
|
assert r["kind"] == "PATCH"
|
|
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
|
|
|
|
|
|
def test_211d_e_minor_closes_drift_with_new_behaviour(tmp_path):
|
|
"""CASE E: correct MINOR closes drift and links the new behaviour."""
|
|
l = cl.CapabilityLedger(tmp_path)
|
|
_cap(l)
|
|
l.material_change("drift-target", note="added drain()", generation="gen-2")
|
|
r = cl.revise_spec(l, "drift-target", behaviour_added=True,
|
|
reason="added drain behaviour",
|
|
validation_consequences=["VAL-DRAIN-1"])
|
|
assert r["kind"] == "MINOR"
|
|
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
|
|
assert "VAL-DRAIN-1" in l.get("drift-target")["spec"][
|
|
"version_history"][-1]["validation_consequences"]
|
|
|
|
|
|
def test_211d_f_move_only_no_semantic_change(tmp_path):
|
|
"""CASE F: source move only — stable identity, no fabricated change."""
|
|
l = cl.CapabilityLedger(tmp_path)
|
|
_cap(l)
|
|
l.move("drift-target", ["src/new/place.py"])
|
|
rec = l.get("drift-target")
|
|
assert rec["capability_id"] == "capability:drift-target"
|
|
# a move is location metadata, not a material semantic revision:
|
|
# the detector keys on material_revision, which move() does not bump
|
|
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
|
|
|
|
|
|
def test_211d_g_old_version_proof_reopens(tmp_path):
|
|
"""CASE G: behaviour proven against an incompatible old version →
|
|
RETEST_REQUIRED with version-referenced reason."""
|
|
l = cl.CapabilityLedger(tmp_path)
|
|
_cap(l)
|
|
beh = bl.BehaviourLedger(tmp_path)
|
|
beh.define(behaviour_id="VAL-OLD", owning_project="r",
|
|
intended_behaviour="exact behaviour",
|
|
owner_component="component:d",
|
|
dependencies=["component:d"])
|
|
beh.record_auto_verification("VAL-OLD", passed=True)
|
|
beh.record_real_world("VAL-OLD", passed=True, generation="cap-0.1.0")
|
|
r = cl.revise_spec(l, "drift-target", consumer_break=True,
|
|
reason="breaking payload change")
|
|
assert r["kind"] == "MAJOR"
|
|
inv = beh.invalidate_for_change(
|
|
changed_entities={"component:d"}, generation="cap-1.0.0",
|
|
reasons={"behaviour:VAL-OLD":
|
|
f"capability drift-target {r['old']}→{r['new']} MAJOR; "
|
|
f"proven against {r['old']}"})
|
|
assert [x["behaviour_id"] for x in inv] == ["behaviour:VAL-OLD"]
|
|
assert beh.get("VAL-OLD")["state"] == bl.RETEST_REQUIRED
|