- 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.
151 lines
5.6 KiB
Python
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"]
|