Files
rune/tests/test_construction_decision.py
Nick 0a9b0648cd
CI Pipeline / best-practices (push) Successful in 9s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / lint (push) Successful in 38s
Auto-version and Release / release (push) Successful in 42s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / unit-tests (push) Successful in 1m3s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 3m29s
feat(scaffolding): live Need→Decision→Contract adoption path + contract-quality repair + layout obligation injection (SCAFFOLD-TRANCHE-004)
- construction_decision.py: the adoption layer — Need intake (references
  Task/Intent by pointer, never duplicates), requirement normalization
  (gaps recorded, never invented), deterministic discovery+fit+resolution
  across authorities with merge precedence (BUILD_NEW never a fallback
  across merges; normalization gaps block it), toolchain decision (existing
  toolchain wins), USER-LOCKED layout obligation inheritance, approval
  gate (material changes require user; approved-bounded work never re-asks),
  durable model-independent decision trace, contract generation populated
  from the trace. Research routing is an HONEST SEAM: recorded route, not
  executed ADAPT call (§6).
- scaffold.py contract-quality repair (Tranche 3 defects): closure now
  distinguishes predicted_existing_affected / expected_new /
  correctly_predicted / predicted_but_unaffected / expected_new_observed /
  expected_new_missing / genuinely_unexpected_new; prediction cannot
  self-certify observation; legacy keys (missed/needless/observed)
  preserved — old contracts readable without migration
- layout obligations: affected UI component/region/implementation pulls
  its Layout Contract into generated obligations; USER-LOCKED obligations
  state that changes require explicit user authorization; unrelated
  backend changes inherit none
- REAL LIVE PROOF (§24): the contract-quality Need ran through the new
  path — discovered module:scaffold (PARTIAL fit on closure-quality
  contract), ADAPT_EXISTING verdict, python KEEP_EXISTING, approval
  covered by SCAFFOLD-TRANCHE-004, generated LIVE-CONTRACT-QUALITY-001,
  implemented, verified (10 contract-quality tests), closed with repaired
  semantics — which correctly flagged parent-tranche surfaces as outside
  this bounded contract's scope (RED honored, attribution recorded)
- baseline correction recorded: Tranche 3 pre-build = 1386 tests (report
  wording corrected in contract-quality notes, provenance kept)
- version-display lock status: still scaffold-provenance, USER-LOCKED
  upgrade PENDING USER CONFIRMATION (not fabricated)

Tests: 37 new (decision path 15, contract quality 10, layout obligations
10, plus repaired-closure compat); suite 1476 green; henxels rc=0;
pyright 0 errors on new/changed modules.
2026-09-13 04:49:17 +01:00

376 lines
16 KiB
Python

"""test_construction_decision.py — SCAFFOLD-TRANCHE-004 §29 test plan (A-M)."""
import json
import os
import subprocess
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import capability_fit as cf # noqa: E402
from rune import construction_decision as cd # noqa: E402
from rune import scaffold as sc # noqa: E402
from rune import structural_store as ss # noqa: E402
from rune import toolchain as tc # noqa: E402
# ── fixtures ────────────────────────────────────────────────────────────────
def _spine_doc():
"""Rune-Spine-like authority with an existing export capability."""
doc = ss.new_doc()
ss.add_entity(
doc, id="capability:exporter", kind="capability", identities={}, maturity="WIRED", evidence_refs=["fixture"]
)
ss.add_entity(doc, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["fixture"])
ss.add_relationship(
doc, type="implements", from_id="capability:exporter", to_id="contract:csv_export", evidence_refs=["fixture"]
)
ss.add_entity(doc, id="module:cli", kind="module", identities={}, evidence_refs=["fixture"])
ss.add_relationship(
doc, type="consumes", from_id="module:cli", to_id="capability:exporter", evidence_refs=["fixture"]
)
return doc
def _need(**kw):
defaults = dict(
need_id="need-001",
objective="persistent project export",
origin="user",
project="rune",
constraints={"contracts": ["contract:csv_export"]},
affected_surface=[],
evidence_refs=["user-request"],
approval_context="covered by SCAFFOLD-TRANCHE-004",
)
defaults.update(kw)
return cd.Need(**defaults)
# ── A. Need → CapabilityRequirement normalization ───────────────────────────
def test_a_normalization():
req, missing = cd.normalize_need(_need())
assert req.objective == "persistent project export"
assert "contract:csv_export" in req.contracts
assert req.props["source_need"] == "need-001"
assert req.props["task_ref"] == ""
def test_a_normalization_preserves_provenance_and_flags_gaps():
need = _need(evidence_refs=[], constraints={})
req, missing = cd.normalize_need(need)
assert req.props["source_need"] == "need-001"
assert "evidence_refs" in missing # recorded, not invented
# ── B. Need references Task/Intent without duplicating authority ───────────
def test_b_need_references_task_authority():
need = _need(task_ref="intent:EXPR-42")
d = need.to_dict()
assert d["task_ref"] == "intent:EXPR-42"
# the Need carries a REFERENCE — no task status/state fields of its own
assert "status" not in d and "state" not in d and "tasks" not in d
# ── C. structural discovery invoked automatically ───────────────────────────
def test_c_live_discovery():
trace = cd.make_decision(_need(), authorities={"rune_spine": _spine_doc()})
assert any(c["id"] == "capability:exporter" for c in trace["candidates"])
assert trace["candidates"][0]["authority"] == "rune_spine"
# ── D. exact REUSE path ─────────────────────────────────────────────────────
def test_d_reuse_path():
trace = cd.make_decision(_need(), authorities={"rune_spine": _spine_doc()})
assert trace["resolution"] == "REUSE"
assert trace["research"]["route"] == "NONE"
# ── E. ADAPT_EXISTING path (with auto regression obligations) ───────────────
def test_e_adapt_path_auto_regression():
doc = ss.new_doc()
ss.add_entity(
doc, id="capability:exporter", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"]
)
ss.add_entity(doc, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
ss.add_relationship(
doc, type="implements", from_id="capability:exporter", to_id="contract:csv_export", evidence_refs=["f"]
)
ss.add_entity(doc, id="module:cli", kind="module", identities={}, evidence_refs=["f"])
ss.add_relationship(doc, type="consumes", from_id="module:cli", to_id="capability:exporter", evidence_refs=["f"])
need = _need(evidence_refs=["x"])
need.constraints["contracts"] = ["contract:csv_export", "contract:encrypted_export"]
trace = cd.make_decision(need, authorities={"rune_spine": doc})
assert trace["resolution"] == "ADAPT_EXISTING"
# BUILD-BUT-DON'T-BREAK obligations generated AUTOMATICALLY (§9):
reg = " ".join(trace["obligations"]["regression"])
assert "preserve existing behavior of capability:exporter" in reg
assert "module:cli" in reg
# ── F. COMPOSE path ──────────────────────────────────────────────────────────
def test_f_compose_path():
doc = ss.new_doc()
ss.add_entity(doc, id="capability:a", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"])
ss.add_entity(doc, id="capability:b", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"])
for c in ("contract:streaming", "contract:bounded_memory"):
ss.add_entity(doc, id=c, kind="contract", identities={}, evidence_refs=["f"])
ss.add_relationship(doc, type="implements", from_id="capability:a", to_id="contract:streaming", evidence_refs=["f"])
ss.add_relationship(
doc, type="implements", from_id="capability:b", to_id="contract:bounded_memory", evidence_refs=["f"]
)
need = _need(
objective="stream with bounded memory",
constraints={"contracts": ["contract:streaming", "contract:bounded_memory"]},
evidence_refs=["x"],
)
trace = cd.make_decision(need, authorities={"rune_spine": doc})
assert trace["resolution"] == "COMPOSE"
assert set(trace["obligations"]["composition"][0]["components"]) == {"capability:a", "capability:b"}
assert "temporary" in trace["obligations"]["composition"][0]["permanence"]
# ── G. ACQUIRE_EXTERNAL routes to research seam ─────────────────────────────
def test_g_acquire_routes_to_research():
trace = cd.make_decision(
_need(objective="quantum error correction", constraints={"contracts": ["contract:qec"]}, evidence_refs=["x"]),
authorities={"rune_spine": ss.new_doc()},
)
assert trace["resolution"] == "INSUFFICIENT_EVIDENCE" # no candidates at all
trace2 = cd.make_decision(
_need(objective="quantum error correction", constraints={"contracts": ["contract:qec"]}, evidence_refs=["x"]),
authorities={"rune_spine": _spine_doc()},
)
assert trace2["resolution"] == "INSUFFICIENT_EVIDENCE"
# external evidence → ACQUIRE + full research route recorded (seam)
doc = _spine_doc()
ss.add_entity(doc, id="capability:x", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"])
trace3 = cd.make_decision(
_need(objective="qec", constraints={"contracts": ["contract:qec"]}, evidence_refs=["x"]),
authorities={"rune_spine": doc},
)
assert trace3["resolution"] == "INSUFFICIENT_EVIDENCE"
assert trace3["research"]["status"] == "RECORDED_NOT_EXECUTED" or trace3["research"]["route"] in (
"NONE",
"TARGETED_EVIDENCE_PROBE",
)
def test_g_acquire_with_external_evidence():
doc = ss.new_doc()
ss.add_entity(doc, id="capability:p", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"])
ss.add_entity(doc, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
# candidate provides nothing required → all incompatible
res = cf.decide(
doc,
cd.normalize_need(_need())[0],
[
cf.evaluate_fit(
cd.normalize_need(_need())[0],
{
"id": "capability:p",
"kind": "capability",
"status": "CURRENT",
"superseded_by": None,
"provides": ["contract:other"],
"consumers": [],
"maturity": "WIRED",
"evidence_refs": [],
"identities": {},
},
)
],
external_evidence=[{"tool": "libcsv", "license": "MIT"}],
)
assert res["resolution"] == "ACQUIRE_EXTERNAL"
# ── H/I. INSUFFICIENT_EVIDENCE never becomes BUILD_NEW ──────────────────────
def test_h_insufficient_never_build_new():
trace = cd.make_decision(
_need(objective="impossible thing", constraints={"contracts": ["contract:nope"]}, evidence_refs=["x"]),
authorities={"rune_spine": _spine_doc()},
)
assert trace["resolution"] == "INSUFFICIENT_EVIDENCE"
def test_i_build_new_requires_positive_evidence_everywhere():
doc1 = _spine_doc() # has partials → ADAPT/COMPOSE territory
doc2 = ss.new_doc() # nothing at all → INSUFFICIENT
need = _need(
objective="odd requirement",
constraints={"contracts": ["contract:csv_export", "contract:never_seen"]},
evidence_refs=["x"],
)
t1 = cd.make_decision(need, authorities={"rune_spine": doc1})
assert t1["resolution"] != "BUILD_NEW" # csv_export partial exists → adapt
t2 = cd.make_decision(need, authorities={"rune_spine": doc1, "project_scaffold": doc2})
assert t2["resolution"] == "ADAPT_EXISTING" # merge: concrete beats insufficient
# BUILD_NEW only via brittle-coupling positive evidence (unit-covered in
# test_capability_fit) — merge never upgrades insufficient to build
def test_i_normalization_gap_blocks_build_new():
doc = ss.new_doc()
# a partial fit exists; normalization is incomplete
ss.add_entity(doc, id="capability:p", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"])
ss.add_entity(doc, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
ss.add_relationship(
doc, type="implements", from_id="capability:p", to_id="contract:csv_export", evidence_refs=["f"]
)
ss.add_entity(doc, id="contract:missing_half", kind="contract", identities={}, evidence_refs=["f"])
ss.add_relationship(
doc, type="implements", from_id="capability:p", to_id="contract:missing_half", evidence_refs=["f"]
)
ss.add_entity(doc, id="contract:unverifiable", kind="contract", identities={}, evidence_refs=["f"])
need = _need(
objective="x", constraints={"contracts": ["contract:unverifiable"]}, evidence_refs=[]
) # no evidence refs → normalization gap
trace = cd.make_decision(need, authorities={"rune_spine": doc})
assert trace["resolution"] == "INSUFFICIENT_EVIDENCE"
assert "normalization gaps recorded, not invented" in trace["rationale"]
# ── J/K. toolchain decision enters the trace ────────────────────────────────
def test_j_toolchain_in_trace():
trace = cd.make_decision(
_need(), authorities={"rune_spine": _spine_doc()}, project_toolchain={"existing_language": "python"}
)
assert trace["toolchain"]["selected_language"] == "python"
assert trace["toolchain"]["resolution"] == "KEEP_EXISTING"
def test_k_material_toolchain_change_marks_approval():
trace = cd.make_decision(
_need(),
authorities={"rune_spine": _spine_doc()},
toolchain_constraints=tc.ToolchainConstraints(existing_language="python"),
)
# simulate a requested change via direct resolver check (the live path
# records the toolchain decision; a change request surfaces approval)
d = tc.resolve_toolchain(tc.ToolchainConstraints(existing_language="python"), requested_language="rust")
assert d.user_approval_required is True
# and the approval gate honors it:
gate = cd.needs_user_approval("REUSE", d, [], _need(approval_context=""))
assert gate["required"] is True
assert "toolchain change" in gate["reasons"][0]
# ── L. approved bounded maintenance does not repeatedly ask ─────────────────
def test_l_bounded_work_inside_approved_contract_does_not_reask():
gate = cd.needs_user_approval(
"REUSE",
tc.resolve_toolchain(tc.ToolchainConstraints(existing_language="python")),
[],
_need(approval_context="covered by SCAFFOLD-TRANCHE-004"),
)
assert gate["required"] is False
assert gate["covered_by"] == "covered by SCAFFOLD-TRANCHE-004"
# ── M. fresh-runtime decision trace recovery ────────────────────────────────
def test_m_fresh_runtime_trace_recovery(tmp_path):
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "proj")
doc = s.init(purpose="test")
trace = cd.make_decision(
_need(), authorities={"rune_spine": _spine_doc()}, project_toolchain={"existing_language": "python"}
)
cd.generate_contract(s, doc, trace, contract_id="LIVE-1", scope=["src/export"])
s.transition_contract(doc, "LIVE-1", "approved")
s.save(doc)
# fresh interpreter, no chat transcript
script = (
"import sys, json;"
"sys.path.insert(0, %r);"
"from rune import scaffold;"
"s = scaffold.Scaffold(%r, 'proj');"
"doc = s.load();"
"c = s.contract(doc, 'LIVE-1');"
"t = c['ext']['decision_trace'];"
"print(json.dumps({'need': t['need']['objective'],"
"'resolution': t['resolution'],"
"'rationale': t['rationale'][:40],"
"'toolchain': t['toolchain']['selected_language'],"
"'approval_required': t['approval']['required'],"
"'contract_phase': c['phase'],"
"'candidates': [x['id'] for x in t['candidates']]}))"
) % (os.path.join(os.path.dirname(__file__), "..", "src"), str(tmp_path / ".rune" / "scaffold"))
r = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=60)
assert r.returncode == 0, r.stderr
rec = json.loads(r.stdout.strip())
assert rec["need"] == "persistent project export"
assert rec["resolution"] == "REUSE"
assert rec["toolchain"] == "python"
assert rec["approval_required"] is False
assert rec["contract_phase"] == "approved"
assert "capability:exporter" in rec["candidates"]
# ── contract generation populates fields from the trace ─────────────────────
def test_generated_contract_fields_from_trace(tmp_path):
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "proj")
doc = s.init(purpose="test")
trace = cd.make_decision(
_need(
objective="add encrypted export",
constraints={"contracts": ["contract:csv_export", "contract:encrypted_export"]},
evidence_refs=["x"],
approval_context="covered by SCAFFOLD-TRANCHE-004",
),
authorities={"rune_spine": _spine_doc()},
project_toolchain={"existing_language": "python"},
)
cd.generate_contract(s, doc, trace, contract_id="GEN-1", scope=["src/export"])
c = s.contract(doc, "GEN-1")
assert c["identities"]["resolution"] == "ADAPT_EXISTING"
assert c["identities"]["toolchain"]["selected_language"] == "python"
assert "capability:exporter" in c["identities"]["affected_entities"]
assert c["identities"]["regression_obligations"] # auto BUILD-BUT-DON'T-BREAK
assert c["ext"]["decision_trace"]["need"]["need_id"] == "need-001"
# ── failure behavior (§28) ───────────────────────────────────────────────────
def test_stale_candidate_rejected_in_live_path():
doc = _spine_doc()
e = ss.entity(doc, "capability:exporter")
e["status"] = "STALE"
trace = cd.make_decision(_need(), authorities={"rune_spine": doc})
assert trace["resolution"] == "INSUFFICIENT_EVIDENCE"
def test_merge_build_new_never_upgrades_from_insufficient():
build = {"resolution": "BUILD_NEW", "rationale": "brittle"}
insuff = {"resolution": "INSUFFICIENT_EVIDENCE", "rationale": "weak"}
merged = cd.merge_resolutions([build, insuff])
assert merged["resolution"] == "INSUFFICIENT_EVIDENCE"