feat(scaffolding): live Need→Decision→Contract adoption path + contract-quality repair + layout obligation injection (SCAFFOLD-TRANCHE-004)
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
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
- 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.
This commit is contained in:
1 parent
da2ec4df0e
commit
0a9b0648cd
8 files changed
+4710
-145
No files matched your search
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"generated_at": "2026-09-13T03:10:20Z",
|
||||
"generated_at": "2026-09-13T03:43:44Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
@@ -218,6 +218,7 @@
|
||||
"capability_fit",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer",
|
||||
"construction_decision",
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"layout_contract",
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
"""construction_decision.py — live Need → Decision → Construction Contract.
|
||||
|
||||
SCAFFOLD-TRANCHE-004: the adoption layer that turns the Tranche 2/3
|
||||
mechanisms into a durable procedure Rune no longer has to remember.
|
||||
|
||||
NEED (references Task/Intent — never duplicates them)
|
||||
↓ normalize_need() missing info is NOT invented
|
||||
↓ discover + fit per authority (deterministic; Spine + Project Scaffold)
|
||||
↓ resolve per authority + merge (capability_fit resolution surface)
|
||||
↓ resolve_toolchain() (existing project toolchain wins)
|
||||
↓ layout obligations (LOCKED authority inherited)
|
||||
↓ approval gate (material changes → user)
|
||||
↓ DecisionTrace (durable, model-independent)
|
||||
↓ generate_contract() (populated from the trace)
|
||||
|
||||
Boundaries (brief §3, §32, §33):
|
||||
- NOT a task engine: Need carries a reference to the existing Task/Intent
|
||||
authority; Scaffolding never becomes the "why" authority.
|
||||
- NOT a workflow engine: one decision function, one trace object.
|
||||
- NOT a research pipeline: insufficient evidence routes to a recorded
|
||||
research path (seam) — the live ADAPT wiring is a recorded route, not
|
||||
an executed pipeline call (honest seam, §6).
|
||||
- NOT a structural authority: it reads Spine/Project Scaffold and
|
||||
WRITES only Construction Contracts + decision traces.
|
||||
- BUILD_NEW is never a fallback (§8): merge precedence and the
|
||||
insufficient-evidence invariant of capability_fit enforce it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rune import capability_fit as cf
|
||||
from rune import toolchain as tc
|
||||
from rune.structural_store import relationships
|
||||
|
||||
__all__ = ["Need", "make_decision", "generate_contract", "merge_resolutions"]
|
||||
|
||||
|
||||
# ── Need intake (§4) ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class Need:
|
||||
"""Construction-decision input. References — does not copy — the
|
||||
existing Task/Intent authority (`task_ref` is a pointer, not a copy).
|
||||
Carries no task state of its own: it is WHY-adjacent input to Scaffolding,
|
||||
not a backlog entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
need_id: str,
|
||||
objective: str,
|
||||
*,
|
||||
origin: str = "user",
|
||||
project: str = "rune",
|
||||
task_ref: str = "",
|
||||
constraints: dict | None = None,
|
||||
affected_surface: list[str] | None = None,
|
||||
evidence_refs: list[str] | None = None,
|
||||
approval_context: str = "",
|
||||
requested_permanence: str = "",
|
||||
):
|
||||
if not need_id:
|
||||
raise ValueError("need_id required")
|
||||
if not objective:
|
||||
raise ValueError("objective (required effect) required — do not normalize a need we cannot state")
|
||||
self.need_id = need_id
|
||||
self.objective = objective
|
||||
self.origin = origin
|
||||
self.project = project
|
||||
self.task_ref = task_ref
|
||||
self.constraints = dict(constraints or {})
|
||||
self.affected_surface = list(affected_surface or [])
|
||||
self.evidence_refs = list(evidence_refs or [])
|
||||
self.approval_context = approval_context
|
||||
self.requested_permanence = requested_permanence
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"need_id": self.need_id,
|
||||
"objective": self.objective,
|
||||
"origin": self.origin,
|
||||
"project": self.project,
|
||||
"task_ref": self.task_ref,
|
||||
"constraints": dict(self.constraints),
|
||||
"affected_surface": list(self.affected_surface),
|
||||
"evidence_refs": list(self.evidence_refs),
|
||||
"approval_context": self.approval_context,
|
||||
"requested_permanence": self.requested_permanence,
|
||||
}
|
||||
|
||||
|
||||
# ── requirement normalization (§5) ─────────────────────────────────────────
|
||||
|
||||
|
||||
def normalize_need(need: Need) -> tuple[cf.CapabilityRequirement, list[str]]:
|
||||
"""Derive the CapabilityRequirement WITHOUT inventing facts. Contracts
|
||||
may come from explicit constraints (`contracts` key) or from contract-
|
||||
shaped entries on the affected surface (`contract:x` ids). Everything
|
||||
not determinable lands in `missing` — the caller records it and the
|
||||
resolution degrades to INSUFFICIENT_EVIDENCE rather than guessing."""
|
||||
contracts = sorted(need.constraints.get("contracts", []))
|
||||
contracts += sorted(a for a in need.affected_surface if a.startswith("contract:"))
|
||||
missing = []
|
||||
if need.constraints.get("environment"):
|
||||
pass # carried through
|
||||
else:
|
||||
missing.append("environment")
|
||||
if not need.evidence_refs:
|
||||
missing.append("evidence_refs")
|
||||
req = cf.CapabilityRequirement(
|
||||
need.objective,
|
||||
contracts=contracts,
|
||||
environment=need.constraints.get("environment", {}),
|
||||
target_project=need.project,
|
||||
source_need=need.need_id,
|
||||
task_ref=need.task_ref,
|
||||
requested_permanence=need.requested_permanence,
|
||||
**{k: v for k, v in need.constraints.items() if k not in ("contracts", "environment")},
|
||||
)
|
||||
return req, missing
|
||||
|
||||
|
||||
# ── research routing seam (§6 — honest: recorded route, not execution) ─────
|
||||
|
||||
|
||||
def research_route(req: cf.CapabilityRequirement, decision: dict) -> dict:
|
||||
"""Record which evidence path the decision needs. This is a SEAM: the
|
||||
live ADAPT interface is not yet wired for direct invocation, so the
|
||||
route is recorded on the trace for the orchestrating session to act on.
|
||||
NOT claimed as executed research."""
|
||||
if decision["resolution"] == "INSUFFICIENT_EVIDENCE":
|
||||
return {
|
||||
"route": "TARGETED_EVIDENCE_PROBE",
|
||||
"question": f"what satisfies: {req.objective[:120]}",
|
||||
"status": "RECORDED_NOT_EXECUTED",
|
||||
}
|
||||
if decision["resolution"] == "ACQUIRE_EXTERNAL":
|
||||
return {
|
||||
"route": "FULL_RESEARCH_PIPELINE",
|
||||
"question": f"external acquisition options for: {req.objective[:120]}",
|
||||
"status": "RECORDED_NOT_EXECUTED",
|
||||
}
|
||||
return {"route": "NONE", "status": "LOCAL_SOLVE"}
|
||||
|
||||
|
||||
# ── resolution merge across authorities ─────────────────────────────────────
|
||||
|
||||
_PRECEDENCE = [
|
||||
"SATISFIED",
|
||||
"REUSE",
|
||||
"COMPOSE",
|
||||
"ADAPT_EXISTING",
|
||||
"CONFIGURE",
|
||||
"ACQUIRE_EXTERNAL",
|
||||
"INSUFFICIENT_EVIDENCE",
|
||||
"BUILD_NEW",
|
||||
]
|
||||
|
||||
|
||||
def merge_resolutions(results: list[dict]) -> dict:
|
||||
"""Merge per-authority resolution results. Precedence: a concrete
|
||||
reuse/compose/adapt verdict from any authority wins over
|
||||
INSUFFICIENT_EVIDENCE from another; BUILD_NEW can only result when NO
|
||||
authority produced a better verdict AND positive gap evidence exists —
|
||||
it is never a fallback across merges (§8)."""
|
||||
if not results:
|
||||
return {"resolution": "INSUFFICIENT_EVIDENCE", "rationale": "no authority consulted"}
|
||||
best = min(results, key=lambda r: _PRECEDENCE.index(r["resolution"]))
|
||||
merged = dict(best)
|
||||
merged["per_authority"] = [{"authority": r.get("authority", ""), "resolution": r["resolution"]} for r in results]
|
||||
if best["resolution"] == "BUILD_NEW" and len(results) > 1:
|
||||
# only honest if every authority independently agrees
|
||||
if any(r["resolution"] != "BUILD_NEW" for r in results):
|
||||
weaker = [r for r in results if r["resolution"] == "INSUFFICIENT_EVIDENCE"]
|
||||
if weaker:
|
||||
merged = dict(weaker[0])
|
||||
merged["per_authority"] = [
|
||||
{"authority": r.get("authority", ""), "resolution": r["resolution"]} for r in results
|
||||
]
|
||||
merged["rationale"] = (
|
||||
"BUILD_NEW rejected: at least one authority returned "
|
||||
"INSUFFICIENT_EVIDENCE — positive gap evidence required "
|
||||
"from all consulted authorities (§8)"
|
||||
)
|
||||
return merged
|
||||
|
||||
|
||||
# ── user-approval gate (§13) ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def needs_user_approval(
|
||||
resolution: str, toolchain_decision: tc.DecisionRecord, layout_obligations: list[dict], need: Need
|
||||
) -> dict:
|
||||
"""Classify whether decision→contract requires explicit user approval.
|
||||
Small bounded work inside an already-approved contract does NOT re-ask
|
||||
(need.approval_context covers it)."""
|
||||
reasons = []
|
||||
if toolchain_decision.user_approval_required:
|
||||
reasons.append("toolchain change (CHANGE_MAJOR)")
|
||||
if resolution == "BUILD_NEW" and need.requested_permanence != "temporary":
|
||||
reasons.append("new capability construction")
|
||||
if any(o["lock"] for o in layout_obligations):
|
||||
reasons.append("USER-LOCKED layout surface affected — placement changes require user authorization")
|
||||
if need.requested_permanence == "permanent":
|
||||
reasons.append("permanent promotion requested (user-controlled)")
|
||||
if need.constraints.get("new_repository") or need.constraints.get("new_application"):
|
||||
reasons.append("new repository/application")
|
||||
covered = bool(need.approval_context) and not reasons
|
||||
if reasons and need.approval_context and need.constraints.get("approval_covers") == "all":
|
||||
covered = True
|
||||
reasons = [f"{r} — covered by: {need.approval_context}" for r in reasons]
|
||||
return {
|
||||
"required": bool(reasons) and not covered,
|
||||
"reasons": reasons,
|
||||
"covered_by": need.approval_context if covered else "",
|
||||
}
|
||||
|
||||
|
||||
# ── the live decision path ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def make_decision(
|
||||
need: Need,
|
||||
*,
|
||||
authorities: dict[str, dict],
|
||||
toolchain_constraints: tc.ToolchainConstraints | None = None,
|
||||
project_toolchain: dict | None = None,
|
||||
) -> dict:
|
||||
"""Run NEED → decision trace. `authorities` maps authority name → store
|
||||
doc (e.g. {"rune_spine": spine_doc, "project_scaffold": scaffold_doc}).
|
||||
`project_toolchain` seeds the Toolchain Resolver's existing-project
|
||||
constraint (existing toolchain wins — §12)."""
|
||||
req, missing = normalize_need(need)
|
||||
trace: dict = {
|
||||
"need": need.to_dict(),
|
||||
"requirement": req.to_dict(),
|
||||
"normalization_missing": missing,
|
||||
"candidates": [],
|
||||
"fits": [],
|
||||
"per_authority_results": [],
|
||||
"layout_obligations": [],
|
||||
"research": {},
|
||||
"toolchain": {},
|
||||
"approval": {},
|
||||
"resolution": "",
|
||||
"rationale": "",
|
||||
}
|
||||
|
||||
# live discovery + fit + resolution per authority (§7-8)
|
||||
results = []
|
||||
all_fits = []
|
||||
for name, doc in authorities.items():
|
||||
candidates = cf.discover_candidates(doc, req)
|
||||
fits = [cf.evaluate_fit(req, c) for c in candidates]
|
||||
for f, c in zip(fits, candidates):
|
||||
f["authority"] = name
|
||||
c["authority"] = name
|
||||
all_fits.extend(fits)
|
||||
trace["candidates"].extend(
|
||||
{k: c[k] for k in ("id", "kind", "maturity", "status", "authority", "provides", "consumers")}
|
||||
for c in candidates
|
||||
)
|
||||
res = cf.decide(doc, req, fits)
|
||||
res["authority"] = name
|
||||
results.append(res)
|
||||
trace["fits"] = all_fits
|
||||
if results:
|
||||
merged = merge_resolutions(results)
|
||||
else:
|
||||
merged = {"resolution": "INSUFFICIENT_EVIDENCE", "rationale": "no authorities provided"}
|
||||
if missing and merged["resolution"] in ("BUILD_NEW",):
|
||||
# normalization gaps block BUILD_NEW too (§5: do not invent)
|
||||
merged = {
|
||||
"resolution": "INSUFFICIENT_EVIDENCE",
|
||||
"rationale": f"requirement normalization incomplete: {missing}; refusing to build on guesses",
|
||||
}
|
||||
trace["resolution"] = merged["resolution"]
|
||||
trace["rationale"] = merged.get("rationale", "")
|
||||
if missing and trace["resolution"] == "INSUFFICIENT_EVIDENCE":
|
||||
trace["rationale"] += f"; normalization gaps recorded, not invented: {missing}"
|
||||
trace["per_authority_results"] = merged.get("per_authority", results)
|
||||
trace["_merged"] = merged
|
||||
|
||||
# toolchain (§12): existing project toolchain wins
|
||||
constraints = toolchain_constraints or tc.ToolchainConstraints(**(project_toolchain or {}))
|
||||
tdec = tc.resolve_toolchain(constraints)
|
||||
trace["toolchain"] = tdec.to_dict()
|
||||
|
||||
# layout obligations (§19-20): affected surface + candidate impls
|
||||
affected = list(need.affected_surface)
|
||||
for c in trace["candidates"]:
|
||||
affected.append(c["id"])
|
||||
lay = []
|
||||
for name, doc in authorities.items():
|
||||
lay.extend(layout_obligations_for(doc, affected))
|
||||
trace["layout_obligations"] = lay
|
||||
|
||||
# research routing (§6, honest seam)
|
||||
trace["research"] = research_route(req, merged)
|
||||
|
||||
# approval gate (§13)
|
||||
trace["approval"] = needs_user_approval(merged["resolution"], tdec, lay, need)
|
||||
trace["obligations"] = obligations_from_trace(trace, req)
|
||||
return trace
|
||||
|
||||
|
||||
def layout_obligations_for(doc: dict, affected: list[str]) -> list[dict]:
|
||||
"""Layout obligations from a store doc without a Scaffold instance
|
||||
(delegates to the same authority logic via a lightweight query)."""
|
||||
obligations = []
|
||||
affected_set = set(affected)
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] != "layout_contract" or e.get("status") == "RETIRED":
|
||||
continue
|
||||
ident = e["identities"]
|
||||
comp, region, impl = (ident.get("component", ""), ident.get("region", ""), ident.get("implementation", ""))
|
||||
hit = (
|
||||
comp in affected
|
||||
or f"region:{region}" in affected
|
||||
or (impl and any(a in impl for a in affected if a.endswith(".py")))
|
||||
)
|
||||
if not hit:
|
||||
continue
|
||||
locked = bool(ident.get("lock"))
|
||||
obligations.append(
|
||||
{
|
||||
"component": comp,
|
||||
"region": region,
|
||||
"lock": locked,
|
||||
"provenance": ident.get("provenance", "scaffold"),
|
||||
"obligation": (
|
||||
"USER-LOCKED: preserve user placement in region "
|
||||
"'%s'; changes require explicit user authorization" % region
|
||||
)
|
||||
if locked
|
||||
else (
|
||||
"preserve/verify %s placement in region '%s'; movable "
|
||||
"only via authorized layout amendment" % (comp, region)
|
||||
),
|
||||
"verification": ("layout_contract_check must PASS; USER-LOCKED placement unchanged")
|
||||
if locked
|
||||
else "layout_contract_check must PASS",
|
||||
"status": e.get("status", "ACTIVE"),
|
||||
}
|
||||
)
|
||||
return obligations
|
||||
|
||||
|
||||
def obligations_from_trace(trace: dict, req: cf.CapabilityRequirement) -> dict:
|
||||
"""BUILD-BUT-DON'T-BREAK (§9) + composition/acquire obligations (§10-11)
|
||||
derived automatically from the trace — the builder does not memorize the
|
||||
rule; the path produces it."""
|
||||
resolution = trace["resolution"]
|
||||
obligations: dict = {
|
||||
"new_behavior": [],
|
||||
"regression": [],
|
||||
"composition": [],
|
||||
"acquisition": [],
|
||||
"layout": trace["layout_obligations"],
|
||||
"verification": [],
|
||||
}
|
||||
merged = trace.get("_merged", {})
|
||||
selected = merged.get("selected") or merged.get("components") or []
|
||||
if isinstance(selected, str):
|
||||
selected = [selected]
|
||||
if resolution in ("ADAPT_EXISTING", "REUSE", "SATISFIED") and selected:
|
||||
# regression obligations from existing consumers + contracts (§9)
|
||||
for c in trace["candidates"]:
|
||||
if c["id"] in selected and c.get("consumers"):
|
||||
obligations["regression"].append(
|
||||
f"preserve existing behavior of {c['id']} for consumers: {', '.join(c['consumers'])}"
|
||||
)
|
||||
obligations["verification"].append("new behavior proven by tests before closure")
|
||||
if resolution == "ADAPT_EXISTING":
|
||||
obligations["verification"].append("regression proof: original capability's consumers still work")
|
||||
if resolution == "COMPOSE":
|
||||
obligations["composition"] = [
|
||||
{
|
||||
"components": merged.get("components", selected),
|
||||
"joining_contracts": req.contracts,
|
||||
"permanence": merged.get("permanence", "temporary (user-controlled)"),
|
||||
}
|
||||
]
|
||||
obligations["verification"].append("composition boundary behavior verified end-to-end")
|
||||
if resolution == "ACQUIRE_EXTERNAL":
|
||||
obligations["acquisition"] = merged.get("external", [])
|
||||
obligations["verification"].append("external acquisition requires research/approval step before install")
|
||||
if resolution == "BUILD_NEW":
|
||||
obligations["verification"].append("positive gap evidence recorded in contract")
|
||||
for lo in trace["layout_obligations"]:
|
||||
obligations["verification"].append(lo["verification"])
|
||||
return obligations
|
||||
|
||||
|
||||
# ── contract generation (§15) ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def generate_contract(
|
||||
s, doc: dict, trace: dict, *, contract_id: str, scope: list[str], proposer_note: str = ""
|
||||
) -> dict:
|
||||
"""Populate a Construction Contract from the decision trace. The builder
|
||||
no longer hand-reconstructs fields; the trace's structural truth fills
|
||||
them. Human/model additions may still extend afterwards."""
|
||||
merged = trace["_merged"]
|
||||
req = trace["requirement"]
|
||||
selected = merged.get("selected") or []
|
||||
if isinstance(selected, str):
|
||||
selected = [selected]
|
||||
affected = [c["id"] for c in trace["candidates"] if c["id"] in selected] or trace["requirement"].get(
|
||||
"affected_surface", []
|
||||
)
|
||||
obl = trace["obligations"]
|
||||
s.new_contract(
|
||||
doc,
|
||||
contract_id=contract_id,
|
||||
need=trace["need"]["objective"],
|
||||
origin=trace["need"]["origin"],
|
||||
scope=sorted(scope),
|
||||
requirement=req.get("objective", ""),
|
||||
gap_evidence=merged.get("rationale", ""),
|
||||
research_refs=(
|
||||
trace["need"]["evidence_refs"]
|
||||
+ ([trace["research"]["route"]] if trace["research"]["route"] != "NONE" else [])
|
||||
),
|
||||
resolution=trace["resolution"],
|
||||
toolchain=trace["toolchain"],
|
||||
affected_entities=sorted(set(affected) | set(trace["requirement"].get("contracts", []))),
|
||||
expected_new_entities=trace["need"]["constraints"].get("expected_new_entities", []),
|
||||
expected_consumers=[],
|
||||
regression_obligations=obl["regression"],
|
||||
verification_obligations=sorted(set(obl["verification"])),
|
||||
rollback_ref=trace["need"]["constraints"].get("rollback_ref", "git revert"),
|
||||
acceptance=[f"resolution {trace['resolution']} verified end-to-end"],
|
||||
non_goals=trace["need"]["constraints"].get("non_goals", []),
|
||||
)
|
||||
e = s.contract(doc, contract_id)
|
||||
e.setdefault("ext", {})["decision_trace"] = {
|
||||
k: trace[k]
|
||||
for k in (
|
||||
"need",
|
||||
"requirement",
|
||||
"candidates",
|
||||
"fits",
|
||||
"resolution",
|
||||
"rationale",
|
||||
"research",
|
||||
"toolchain",
|
||||
"approval",
|
||||
"obligations",
|
||||
"layout_obligations",
|
||||
"normalization_missing",
|
||||
)
|
||||
}
|
||||
if proposer_note:
|
||||
e["ext"]["decision_trace"]["proposer_note"] = proposer_note
|
||||
return e
|
||||
+136
-24
@@ -331,6 +331,7 @@ class Scaffold:
|
||||
regression_obligations: list[str] | None = None,
|
||||
verification_obligations: list[str] | None = None,
|
||||
migration_obligations: list[str] | None = None,
|
||||
expected_new_entities: list[str] | None = None,
|
||||
rollback_ref: str = "",
|
||||
acceptance: list[str] | None = None,
|
||||
non_goals: list[str] | None = None,
|
||||
@@ -338,7 +339,11 @@ class Scaffold:
|
||||
"""Create a Construction Contract in `draft` phase. One durable
|
||||
object; phases/status are plain fields with a legal-transition map.
|
||||
`toolchain` is a data seam: language/runtime/framework/libs/rationale
|
||||
— no Python-specific assumptions enforced."""
|
||||
— no Python-specific assumptions enforced.
|
||||
`expected_new_entities` (SCAFFOLD-TRANCHE-004 contract-quality
|
||||
repair): entities this build PLANS to create. At closure these are
|
||||
checked against observed reality — an observed planned-new entity is
|
||||
NOT a prediction miss (the Tranche 3 defect this field fixes)."""
|
||||
if entity(doc, f"contract:{contract_id}") is not None:
|
||||
raise ValueError(f"contract already exists: {contract_id}")
|
||||
identities = {
|
||||
@@ -352,6 +357,7 @@ class Scaffold:
|
||||
"toolchain": dict(toolchain or {}), # data seam: any language/runtime
|
||||
"affected_entities": sorted(affected_entities or []),
|
||||
"expected_consumers": sorted(expected_consumers or []),
|
||||
"expected_new_entities": sorted(expected_new_entities or []),
|
||||
"regression_obligations": sorted(regression_obligations or []),
|
||||
"verification_obligations": sorted(verification_obligations or []),
|
||||
"migration_obligations": sorted(migration_obligations or []),
|
||||
@@ -432,7 +438,25 @@ class Scaffold:
|
||||
"""Close a contract: record observed impact independently, run the
|
||||
predicted-vs-observed comparison, persist Contract Quality evidence.
|
||||
Requires the contract to be in `superseding` phase (the final work
|
||||
phase) or `verifying` (pure-observation builds)."""
|
||||
phase) or `verifying` (pure-observation builds).
|
||||
|
||||
SCAFFOLD-TRANCHE-004 contract-quality repair (§16-18): closure
|
||||
distinguishes
|
||||
predicted_existing_affected — planned changes to EXISTING entities
|
||||
expected_new — entities the build PLANNED to create
|
||||
observed_existing — predicted entities evidence confirms
|
||||
observed_new — newly observed surfaces
|
||||
correctly_predicted — predicted existing AND observed
|
||||
predicted_but_unaffected — predicted existing, NOT observed
|
||||
expected_new_observed — planned creation happened
|
||||
expected_new_missing — planned creation did NOT happen
|
||||
genuinely_unexpected_new — observed, planned by NEITHER list
|
||||
Prediction and observation remain independently sourced: the
|
||||
predicted sets come from the approval-time frontier/contract fields;
|
||||
the observed set comes ONLY from the caller's evidence items
|
||||
(git/tests/build/deploy) — never from re-deriving the prediction.
|
||||
Legacy keys preserved: `missed` aliases genuinely_unexpected_new,
|
||||
`needless` aliases predicted_but_unaffected (old contracts readable)."""
|
||||
e = entity(doc, f"contract:{contract_id}")
|
||||
if e is None:
|
||||
raise ValueError(f"contract not found: {contract_id}")
|
||||
@@ -440,33 +464,54 @@ class Scaffold:
|
||||
raise IllegalPhaseTransition(f"close requires superseding/verifying phase, got {e.get('phase')}")
|
||||
predicted = e.get("ext", {}).get("predicted_frontier")
|
||||
affected = e.get("identities", {}).get("affected_entities", [])
|
||||
expected_new = set(e.get("identities", {}).get("expected_new_entities", []))
|
||||
quality: dict = {"contract": contract_id, "captured_at": now()}
|
||||
if predicted is not None and affected:
|
||||
# observed from the caller's independent evidence items, not from
|
||||
# re-running the frontier on the same graph the prediction used
|
||||
obs = record_observed_impact(doc, contract_id, observed_evidence or [])
|
||||
pred_set = {
|
||||
|
||||
# independent observed impact (§17): evidence items only
|
||||
obs = record_observed_impact(doc, contract_id, observed_evidence or [])
|
||||
obs_set = set(obs["observed_entities"])
|
||||
|
||||
pred_existing = set(e.get("ext", {}).get("predicted_existing_affected", []))
|
||||
pred_existing |= set(affected)
|
||||
if predicted is not None:
|
||||
pred_existing |= {
|
||||
x
|
||||
for k in ("level0_direct", "level1_obligations", "contracts", "level2_verification")
|
||||
for x in predicted.get(k, [])
|
||||
}
|
||||
obs_set = set(obs["observed_entities"])
|
||||
quality.update(
|
||||
{
|
||||
"predicted": sorted(pred_set),
|
||||
"observed": sorted(obs_set),
|
||||
"missed": sorted(obs_set - pred_set),
|
||||
"needless": sorted(pred_set - obs_set),
|
||||
"candidate_corrections": [
|
||||
{
|
||||
"entity": x,
|
||||
"status": "CANDIDATE",
|
||||
"note": "missed by prediction — evidence required before store update",
|
||||
}
|
||||
for x in sorted(obs_set - pred_set)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
correctly_predicted = sorted(pred_existing & obs_set)
|
||||
predicted_but_unaffected = sorted(pred_existing - obs_set)
|
||||
expected_new_observed = sorted(expected_new & obs_set)
|
||||
expected_new_missing = sorted(expected_new - obs_set)
|
||||
genuinely_unexpected_new = sorted(obs_set - pred_existing - expected_new)
|
||||
|
||||
quality.update(
|
||||
{
|
||||
"predicted_existing_affected": sorted(pred_existing),
|
||||
"expected_new_entities": sorted(expected_new),
|
||||
"observed": sorted(obs_set), # legacy union (Tranche 2 readers)
|
||||
"observed_existing": correctly_predicted,
|
||||
"observed_new": sorted(obs_set & (expected_new | (obs_set - pred_existing))),
|
||||
"correctly_predicted": correctly_predicted,
|
||||
"predicted_but_unaffected": predicted_but_unaffected,
|
||||
"expected_new_observed": expected_new_observed,
|
||||
"expected_new_missing": expected_new_missing,
|
||||
"genuinely_unexpected_new": genuinely_unexpected_new,
|
||||
# legacy aliases (Tranche 2/3 consumers)
|
||||
"missed": genuinely_unexpected_new,
|
||||
"needless": predicted_but_unaffected,
|
||||
"candidate_corrections": [
|
||||
{
|
||||
"entity": x,
|
||||
"status": "CANDIDATE",
|
||||
"note": "genuinely unexpected new surface — evidence required before store update",
|
||||
}
|
||||
for x in genuinely_unexpected_new
|
||||
],
|
||||
"evidence_sources": sorted({item.get("source", "unattributed") for item in (observed_evidence or [])}),
|
||||
}
|
||||
)
|
||||
if scope_files is not None:
|
||||
quality["scope_check"] = self.check_scope(doc, contract_id, scope_files)
|
||||
e.setdefault("ext", {})["contract_quality"] = quality
|
||||
@@ -515,6 +560,73 @@ class Scaffold:
|
||||
doc["observations"].append({"kind": "fact_staled", **s, "observed_at": now()})
|
||||
return {"staled": staled}
|
||||
|
||||
# -- layout obligation injection (SCAFFOLD-TRANCHE-004 §19-22) -------------
|
||||
|
||||
def layout_obligations(self, doc: dict, affected: list[str]) -> list[dict]:
|
||||
"""Query Layout Contracts relevant to a change's affected surface and
|
||||
emit build obligations the generated contract MUST inherit (§19).
|
||||
|
||||
`affected` may contain: `ui:<component>` ids, implementation paths
|
||||
(matched against contract identities.implementation), or region ids
|
||||
(a change to a region touches everything bound to it).
|
||||
|
||||
Semantics:
|
||||
FLEXIBLE contract → preserve/verify current contracted placement;
|
||||
movable via authorized amendment only.
|
||||
USER-LOCKED → placement is USER AUTHORITY: the build must
|
||||
preserve it, and the obligation states that
|
||||
changing it requires explicit user
|
||||
authorization (amend_layout_contract with
|
||||
authorized_by='user'). The live path NEVER
|
||||
reconciles user-locked drift by editing the
|
||||
contract (§20) — code adapts instead.
|
||||
No applicable contract → [] (unrelated backend changes inherit
|
||||
no layout obligations — tested).
|
||||
"""
|
||||
affected_set = set(affected)
|
||||
obligations = []
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] != "layout_contract" or e.get("status") in ("RETIRED",):
|
||||
continue
|
||||
ident = e["identities"]
|
||||
comp = ident.get("component", "")
|
||||
region = ident.get("region", "")
|
||||
impl = ident.get("implementation", "")
|
||||
region_hit = f"region:{region}" in affected_set
|
||||
comp_hit = comp in affected_set
|
||||
impl_hit = bool(impl) and any(impl.startswith(a) or a in impl for a in affected_set if a.endswith(".py"))
|
||||
if not (comp_hit or region_hit or impl_hit):
|
||||
continue
|
||||
locked = bool(ident.get("lock"))
|
||||
provenance = ident.get("provenance", "scaffold")
|
||||
if locked:
|
||||
obligation = (
|
||||
f"USER-LOCKED: {comp} placement in region '{region}' is "
|
||||
f"user authority — preserve it exactly; changing requires "
|
||||
f"explicit user authorization "
|
||||
f"(amend_layout_contract authorized_by='user')"
|
||||
)
|
||||
else:
|
||||
obligation = (
|
||||
f"preserve/verify {comp} contracted placement "
|
||||
f"(region '{region}', provenance {provenance}); movable "
|
||||
f"only via authorized layout amendment"
|
||||
)
|
||||
obligations.append(
|
||||
{
|
||||
"component": comp,
|
||||
"region": region,
|
||||
"lock": locked,
|
||||
"provenance": provenance,
|
||||
"obligation": obligation,
|
||||
"verification": "layout_contract_check must PASS"
|
||||
if not locked
|
||||
else "layout_contract_check must PASS; USER-LOCKED placement unchanged",
|
||||
"status": e.get("status", "ACTIVE"),
|
||||
}
|
||||
)
|
||||
return obligations
|
||||
|
||||
# -- bridge to Rune Spine (explicit, non-duplicating) ---------------------
|
||||
|
||||
def spine_bridge_entity(self) -> dict:
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+218
-120
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,375 @@
|
||||
"""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"
|
||||
@@ -0,0 +1,150 @@
|
||||
"""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"]
|
||||
@@ -0,0 +1,156 @@
|
||||
"""test_layout_obligations.py — SCAFFOLD-TRANCHE-004 §31 test plan (A-H)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import construction_decision as cd # noqa: E402
|
||||
from rune import layout_contract as lc # 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
|
||||
|
||||
|
||||
def _doc_with_layouts():
|
||||
doc = ss.new_doc()
|
||||
lc.define_region(doc, "footer-bar", provenance="scaffold", evidence_refs=["issue-105"])
|
||||
lc.define_region(doc, "header", provenance="user")
|
||||
# version display: scaffold-provenance (NOT user-locked — §21 status)
|
||||
lc.bind_component(
|
||||
doc,
|
||||
"version_display",
|
||||
"footer-bar",
|
||||
provenance="scaffold",
|
||||
order=0,
|
||||
evidence_ref="issue-105",
|
||||
implementation="src/rune/rune_tui.py:VersionLabel",
|
||||
)
|
||||
# a user-locked element for authority tests
|
||||
lc.bind_component(doc, "status_bar", "footer-bar", lock=True, provenance="user", order=1, evidence_ref="user-2026")
|
||||
return doc
|
||||
|
||||
|
||||
def _need(**kw):
|
||||
d = dict(
|
||||
need_id="n",
|
||||
objective="tweak footer",
|
||||
origin="builder",
|
||||
constraints={},
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by contract",
|
||||
)
|
||||
d.update(kw)
|
||||
return cd.Need(**d)
|
||||
|
||||
|
||||
# A. affected UI component pulls its Layout Contract into obligations
|
||||
def test_a_ui_component_inherits_layout_obligation():
|
||||
doc = _doc_with_layouts()
|
||||
obs = cd.layout_obligations_for(doc, ["ui:version_display"])
|
||||
assert len(obs) == 1
|
||||
o = obs[0]
|
||||
assert o["component"] == "ui:version_display"
|
||||
assert o["region"] == "footer-bar"
|
||||
assert o["lock"] is False
|
||||
assert "layout_contract_check must PASS" in o["verification"]
|
||||
|
||||
|
||||
# B. unrelated backend change inherits NO layout obligations
|
||||
def test_b_backend_change_no_layout_obligations():
|
||||
doc = _doc_with_layouts()
|
||||
assert cd.layout_obligations_for(doc, ["module:memory_store"]) == []
|
||||
assert cd.layout_obligations_for(doc, ["capability:exporter"]) == []
|
||||
|
||||
|
||||
def test_b2_implementation_path_match_pulls_contract():
|
||||
doc = _doc_with_layouts()
|
||||
obs = cd.layout_obligations_for(doc, ["src/rune/rune_tui.py"])
|
||||
assert any(o["component"] == "ui:version_display" for o in obs)
|
||||
|
||||
|
||||
# C. FLEXIBLE layout obligation carried correctly
|
||||
def test_c_flexible_obligation():
|
||||
doc = _doc_with_layouts()
|
||||
s = sc.Scaffold("/tmp/never-required", "x")
|
||||
o = s.layout_obligations(doc, ["ui:version_display"])[0]
|
||||
assert o["lock"] is False
|
||||
assert "movable only via authorized layout amendment" in o["obligation"]
|
||||
assert o["verification"] == "layout_contract_check must PASS"
|
||||
|
||||
|
||||
# D. USER-LOCKED layout obligation blocks unauthorized change
|
||||
def test_d_user_locked_obligation_blocks():
|
||||
doc = _doc_with_layouts()
|
||||
s = sc.Scaffold("/tmp/never-required", "x")
|
||||
obs = s.layout_obligations(doc, ["ui:status_bar"])
|
||||
assert len(obs) == 1
|
||||
o = obs[0]
|
||||
assert o["lock"] is True
|
||||
assert "USER-LOCKED" in o["obligation"]
|
||||
assert "explicit user authorization" in o["obligation"]
|
||||
assert "USER-LOCKED placement unchanged" in o["verification"]
|
||||
# and the live-path approval gate fires for locked surfaces
|
||||
gate = cd.needs_user_approval(
|
||||
"ADAPT_EXISTING",
|
||||
tc.resolve_toolchain(tc.ToolchainConstraints(existing_language="python")),
|
||||
obs,
|
||||
_need(approval_context=""),
|
||||
)
|
||||
assert gate["required"] is True
|
||||
assert "USER-LOCKED" in gate["reasons"][0]
|
||||
|
||||
|
||||
# E. user-authorized layout amendment passes
|
||||
def test_e_user_authorized_amendment_passes():
|
||||
doc = _doc_with_layouts()
|
||||
lc.amend_layout_contract(
|
||||
doc,
|
||||
"status_bar",
|
||||
new_region="header",
|
||||
authorized_by="user",
|
||||
reason="user reposition",
|
||||
evidence_ref="user-2026-09-13",
|
||||
)
|
||||
# contract follows the user's explicit change
|
||||
e = ss.entity(doc, "layout:ui:status_bar")
|
||||
assert e["identities"]["region"] == "header"
|
||||
# obligation now reflects the amended placement
|
||||
s = sc.Scaffold("/tmp/never-required", "x")
|
||||
obs = s.layout_obligations(doc, ["ui:status_bar"])
|
||||
assert obs[0]["region"] == "header"
|
||||
|
||||
|
||||
# F. unavailable layout adapter remains UNKNOWN — never PASS
|
||||
def test_f_unknown_never_pass():
|
||||
doc = _doc_with_layouts()
|
||||
result = lc.check_layout(doc, {}) # no adapter observations
|
||||
assert result["verdict"] == "UNKNOWN"
|
||||
assert all(c["verdict"] == "UNKNOWN" for c in result["checks"])
|
||||
|
||||
|
||||
# G. drift does NOT rewrite USER-LOCKED intent
|
||||
def test_g_drift_preserves_user_locked():
|
||||
doc = _doc_with_layouts()
|
||||
violating = {"ui:status_bar": {"region": "header", "order": 0}}
|
||||
out = lc.layout_drift(doc, violating)
|
||||
assert out["gate"] == "FAIL"
|
||||
e = ss.entity(doc, "layout:ui:status_bar")
|
||||
assert e["identities"]["region"] == "footer-bar" # user intent preserved
|
||||
assert e["status"] == "DRIFTING"
|
||||
assert e["identities"]["lock"] is True
|
||||
# maintenance item records the violation for user review
|
||||
assert ss.entity(doc, "maintenance:layout-drift-ui:status_bar") is not None
|
||||
|
||||
|
||||
# H. version-display stays scaffold-provenance until explicit confirmation
|
||||
def test_h_version_display_not_user_locked():
|
||||
doc = _doc_with_layouts()
|
||||
e = ss.entity(doc, "layout:ui:version_display")
|
||||
assert e["identities"]["provenance"] == "scaffold"
|
||||
assert e["identities"]["lock"] is False # pending user confirmation (§21)
|
||||
# any attempt to lock it without user provenance is refused
|
||||
with pytest.raises(ValueError, match="user provenance"):
|
||||
lc.bind_component(doc, "version_display", "footer-bar", lock=True, provenance="builder")
|
||||
Reference in new issue
Block a user