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

- 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:
Nick committed 2026-09-13 04:49:17 +01:00
1 parent da2ec4df0e
commit 0a9b0648cd
8 files changed
+4710 -145

No files matched your search

+2 -1
View File
@@ -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",
+455
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+375
View File
@@ -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"
+150
View File
@@ -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"]
+156
View File
@@ -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")