Files
rune/tests/test_contract_quality.py
Nick 0a9b0648cd 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

151 lines
5.6 KiB
Python

"""test_contract_quality.py — SCAFFOLD-TRANCHE-004 §30 test plan (A-H).
Repaired closure semantics: expected-new vs genuinely-missed; independent
observed confirmation; prediction may not self-certify.
"""
import os
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import scaffold as sc # noqa: E402
from rune import structural_store as ss # noqa: E402
def _s(tmp_path):
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "proj")
doc = s.init(purpose="quality tests")
return s, doc
def _mk(s, doc, cid, affected=None, expected_new=None):
s.new_contract(
doc,
contract_id=cid,
need="n",
scope=["src/x"],
affected_entities=affected or [],
expected_new_entities=expected_new or [],
)
s.transition_contract(doc, cid, "approved")
s.transition_contract(doc, cid, "building")
s.transition_contract(doc, cid, "verifying")
return cid
def _close(s, doc, cid, observed):
s.close_contract(doc, cid, observed_evidence=observed)
return s.contract(doc, cid)["ext"]["contract_quality"]
# A. expected-new entity observed → NOT missed
def test_a_expected_new_observed_not_missed(tmp_path):
s, doc = _s(tmp_path)
_mk(s, doc, "C1", affected=["module:old"], expected_new=["module:fresh"])
q = _close(
s,
doc,
"C1",
[{"entity": "module:fresh", "source": "file-added"}, {"entity": "module:old", "source": "git-diff"}],
)
assert "module:fresh" in q["expected_new_observed"]
assert "module:fresh" not in q["genuinely_unexpected_new"]
assert "module:fresh" not in q["missed"] # legacy alias agrees
# B. expected-new absent → explicit expected-new-missing
def test_b_expected_new_missing(tmp_path):
s, doc = _s(tmp_path)
_mk(s, doc, "C1", affected=[], expected_new=["module:promised"])
q = _close(s, doc, "C1", [])
assert q["expected_new_missing"] == ["module:promised"]
assert q["expected_new_observed"] == []
# C. truly unexpected new entity → unexpected category + CANDIDATE
def test_c_unexpected_new(tmp_path):
s, doc = _s(tmp_path)
_mk(s, doc, "C1", affected=["module:a"], expected_new=[])
q = _close(
s,
doc,
"C1",
[{"entity": "module:a", "source": "git-diff"}, {"entity": "module:surprise", "source": "git-diff"}],
)
assert q["genuinely_unexpected_new"] == ["module:surprise"]
assert q["candidate_corrections"][0]["entity"] == "module:surprise"
# D. predicted existing independently observed → correctly predicted
def test_d_predicted_existing_confirmed(tmp_path):
s, doc = _s(tmp_path)
_mk(s, doc, "C1", affected=["module:a", "module:b"])
q = _close(
s, doc, "C1", [{"entity": "module:a", "source": "git-diff"}, {"entity": "module:b", "source": "test-run"}]
)
assert set(q["correctly_predicted"]) == {"module:a", "module:b"}
assert q["predicted_but_unaffected"] == []
# E. predicted existing not observed → predicted-but-unaffected (NOT needless)
def test_e_predicted_but_unaffected(tmp_path):
s, doc = _s(tmp_path)
_mk(s, doc, "C1", affected=["module:a", "module:untouched"])
q = _close(s, doc, "C1", [{"entity": "module:a", "source": "git-diff"}])
assert q["predicted_but_unaffected"] == ["module:untouched"]
assert q["correctly_predicted"] == ["module:a"]
# legacy alias preserved
assert q["needless"] == ["module:untouched"]
# F. prediction may not self-certify observation
def test_f_no_self_certification(tmp_path):
s, doc = _s(tmp_path)
_mk(s, doc, "C1", affected=["module:a", "module:b"])
# observe ONLY module:a — module:b must NOT auto-confirm from prediction
q = _close(s, doc, "C1", [{"entity": "module:a", "source": "git-diff"}])
assert "module:b" not in q["correctly_predicted"]
assert q["predicted_but_unaffected"] == ["module:b"]
# G. closure evidence remains independently sourced (sources recorded)
def test_g_evidence_sources_recorded(tmp_path):
s, doc = _s(tmp_path)
_mk(s, doc, "C1", affected=["module:a"])
q = _close(
s, doc, "C1", [{"entity": "module:a", "source": "git-diff"}, {"entity": "module:a", "source": "test-run"}]
)
assert set(q["evidence_sources"]) == {"git-diff", "test-run"}
obs_kinds = [o["kind"] for o in doc["observations"]]
assert obs_kinds.count("observed_consequence") == 2
# H. old Construction Contracts remain readable
def test_h_legacy_contract_still_closes(tmp_path):
"""A Tranche-2-style contract (no expected_new_entities field) closes
with the repaired semantics without migration."""
s, doc = _s(tmp_path)
s.new_contract(doc, contract_id="OLD-1", need="old", scope=["src/x"], affected_entities=["module:a"])
c = s.contract(doc, "OLD-1")
assert c["identities"].get("expected_new_entities") == [] # empty, fine
s.transition_contract(doc, "OLD-1", "approved")
s.transition_contract(doc, "OLD-1", "building")
s.transition_contract(doc, "OLD-1", "verifying")
q = _close(s, doc, "OLD-1", [{"entity": "module:a", "source": "git-diff"}])
assert q["correctly_predicted"] == ["module:a"]
assert q["expected_new_entities"] == []
assert q["genuinely_unexpected_new"] == []
def test_record_observed_impact_rejects_unattributed_gently(tmp_path):
"""Evidence without entity ids is tolerated (counted) but never
fabricates observed entities."""
s, doc = _s(tmp_path)
_mk(s, doc, "C1", affected=["module:a"])
q = _close(s, doc, "C1", [{"detail": "vague note", "source": "chat"}])
assert q["correctly_predicted"] == []
assert q["predicted_but_unaffected"] == ["module:a"]