feat(scaffolding): Construction Interface — builder adapter, TUI backlog, pattern-precision repair (SCAFFOLD-TRANCHE-007)
§0 PATTERN FALSE-POSITIVE REPAIR (investigated, not assumed): - Root cause layer 1: raw-substring signal matching — 'ui' matched 'patterns_bu_i_l_t_in.py'. Boundary-aware matchers: short (<4 char) signals match path TOKENS / entity-namespace PREFIXES only; longer signals may contain. - Root cause layer 2: ui-component-identity-stable was keyed on path_contains ['tui','ui','widget'] — re-keyed to entities_match ui:/region:/layout: with namespace-prefix semantics (structural kinds carry meaning in their prefix). - Verified all four directions: T6 false positive gone; tui_backlog path no longer activates identity obligation; genuine UI work still derives; genuine deploy hits survive. Recorded as process-defect:raw-substring-signal-match + TUI registry entry for tui_backlog.py (ledger, honestly marked not-a-surface). - construction_interface.py: ONE builder-facing boundary (Rune-internal + external-project modes). submit_need → inspect_decision → get_scope → get_obligations → report_observation → verify → transition → close → resume + amend_scope. Structured JSON envelope (schema v1: success/ids/ state/errors/warnings); explicit error states (UNKNOWN_CONTRACT, SCOPE_VIOLATION, APPROVAL_REQUIRED, VERIFICATION_FAILED, STORE_WRITE_FAILED...); builder identity recorded for audit, semantics identical for any builder (§17); NO structural-authority writes exposed; scope amendment is user-gated; closure cannot be self-certified; CLI transport with --builder/--project-store flags. - tui_backlog.py: durable TUI Upgrade Backlog (§22-25) — projection classification (NONE/INSPECTION/CONTROL/BOTH/UNKNOWN), auto-capture seam record_tui_projection_decision for contract closures, backlog NEVER structural authority (frontier propagation empty, test-proven). REAL PROOF §33: builder session via interface-only subprocess (no internal imports): submit→decision→scope→obligations→transition→observe→verify→ close→FRESH-PROCESS RESUME recovering need/decision/scope/obligations/ next-action from contract id alone. Scope green, quality green. Tests: 33 new (interface §29/§30, pattern precision §31, backlog §32); suite 1589 green; henxels rc=0; pyright clean.
This commit is contained in:
1 parent
1bb4ea86b8
commit
90bd2b3285
11 files changed
+5161
-137
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-13T13:37:11Z",
|
||||
"generated_at": "2026-09-13T16:15:07Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
@@ -219,6 +219,7 @@
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer",
|
||||
"construction_decision",
|
||||
"construction_interface",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"context_broker",
|
||||
@@ -229,6 +230,7 @@
|
||||
"spine",
|
||||
"structural_store",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
|
||||
@@ -9,6 +9,7 @@ new surface._
|
||||
|---|---|---|
|
||||
| `rune_tui.py` | The main Textual application — chat pane, work pane, status bar (version label + refresh), footer, dialogs | The known giant (file-size debt tracked in henxels); composition root for all UI |
|
||||
| `tui_validator.py` | Import-allowlist validation for TUI code | Drift defect tracked in #139 (allowlist vs `__future__`/relative imports) |
|
||||
| `tui_backlog.py` | Not a surface — the durable TUI Upgrade Backlog ledger (outstanding projection/control work per backend feature) | SCAFFOLD-TRANCHE-007; ledger only, never structural authority |
|
||||
|
||||
## Adding a TUI surface
|
||||
|
||||
|
||||
@@ -872,6 +872,8 @@ def generate_contract(
|
||||
"completeness",
|
||||
"affected_surfaces",
|
||||
"iterations",
|
||||
"patterns_considered",
|
||||
"pattern_obligations",
|
||||
)
|
||||
},
|
||||
# distinct impact surfaces (Tranche 5 §13) — file scope != structural
|
||||
|
||||
@@ -0,0 +1,751 @@
|
||||
"""construction_interface.py — the stable builder-facing Construction
|
||||
Interface (SCAFFOLD-TRANCHE-007).
|
||||
|
||||
ONE shared boundary for every builder — MiMo, Kimi, GLM, future models,
|
||||
deterministic scripts — and for both modes:
|
||||
|
||||
Rune-internal mode authority = Rune Spine + Rune's Project Scaffold
|
||||
external-project mode authority = that project's Project Scaffold
|
||||
|
||||
The builder must NOT need to know construction_decision.py, scaffold.py,
|
||||
structural_store.py, Layout Contract internals, or store paths. Those are
|
||||
implementation details behind this adapter.
|
||||
|
||||
Authority enforcement (§14) — the adapter is NOT a bypass:
|
||||
- builders may not rewrite Rune Spine or Project Scaffold truth
|
||||
- builders may not amend USER-LOCKED layout
|
||||
- builders may not silently broaden scope
|
||||
- builders may not self-certify completion ("done=true" is not a thing)
|
||||
- closing remains governed by Scaffolding (phase map + gates)
|
||||
- builder identity is RECORDED for audit but never changes semantics
|
||||
(§17): identical structured input → identical behavior, any builder.
|
||||
|
||||
Machine-readable output (§12): every operation returns a stable envelope —
|
||||
{schema, operation, success, ids, state, evidence, errors, warnings}
|
||||
Errors are explicit states (§13): NEED_INCOMPLETE, INSUFFICIENT_EVIDENCE,
|
||||
APPROVAL_REQUIRED, UNKNOWN_CONTRACT, SCOPE_VIOLATION, VERIFICATION_FAILED,
|
||||
STORE_WRITE_FAILED, STALE_STATE — never prose-only exit-1.
|
||||
|
||||
Transport (§3): typed Python API (this module) + JSON CLI (module main,
|
||||
same convention as rune-spine). No network service.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from rune import structural_store as ss
|
||||
from rune import scaffold as sc
|
||||
from rune import spine as spine_mod
|
||||
from rune import construction_decision as cd
|
||||
|
||||
__all__ = ["ConstructionInterface", "INTERFACE_SCHEMA_VERSION", "Errors"]
|
||||
|
||||
INTERFACE_SCHEMA_VERSION = 1
|
||||
|
||||
# ── explicit error states (§13) ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class Errors:
|
||||
NEED_INCOMPLETE = "NEED_INCOMPLETE"
|
||||
INSUFFICIENT_EVIDENCE = "INSUFFICIENT_EVIDENCE"
|
||||
APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
|
||||
UNKNOWN_CONTRACT = "UNKNOWN_CONTRACT"
|
||||
SCOPE_VIOLATION = "SCOPE_VIOLATION"
|
||||
VERIFICATION_FAILED = "VERIFICATION_FAILED"
|
||||
LAYOUT_VIOLATION = "LAYOUT_VIOLATION"
|
||||
PATTERN_CONFLICT = "PATTERN_CONFLICT"
|
||||
STORE_WRITE_FAILED = "STORE_WRITE_FAILED"
|
||||
STALE_STATE = "STALE_STATE"
|
||||
|
||||
|
||||
def _envelope(
|
||||
operation: str,
|
||||
*,
|
||||
success: bool,
|
||||
ids: dict | None = None,
|
||||
state: dict | None = None,
|
||||
evidence: list | None = None,
|
||||
errors: list | None = None,
|
||||
warnings: list | None = None,
|
||||
**extra,
|
||||
) -> dict:
|
||||
env = {
|
||||
"schema": INTERFACE_SCHEMA_VERSION,
|
||||
"operation": operation,
|
||||
"success": success,
|
||||
"ids": ids or {},
|
||||
"state": state or {},
|
||||
"evidence": evidence or [],
|
||||
"errors": errors or [],
|
||||
"warnings": warnings or [],
|
||||
}
|
||||
env.update(extra)
|
||||
return env
|
||||
|
||||
|
||||
class ConstructionInterface:
|
||||
"""The builder-facing construction lifecycle for ONE project target.
|
||||
|
||||
Rune-internal mode (default):
|
||||
ConstructionInterface(builder="mimo")
|
||||
External-project mode:
|
||||
ConstructionInterface(builder="mimo",
|
||||
project_store="/path/to/.rune/scaffold",
|
||||
project_id="that-project")
|
||||
|
||||
`builder` is recorded for audit (session record, §20) and NEVER changes
|
||||
construction semantics (§17).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
builder: str = "unknown",
|
||||
*,
|
||||
project_store: str | None = None,
|
||||
project_id: str | None = None,
|
||||
spine_store: str = "store/spine/current.json",
|
||||
):
|
||||
if not builder:
|
||||
raise ValueError("builder identity required (recorded for audit)")
|
||||
self.builder = builder
|
||||
self.spine_store = spine_store
|
||||
if project_store:
|
||||
self.scaffold = sc.Scaffold(project_store, project_id or Path(project_store).name)
|
||||
self.mode = "external_project"
|
||||
else:
|
||||
self.scaffold = sc.Scaffold("store/scaffold", "issue161-harness")
|
||||
self.mode = "rune_internal"
|
||||
|
||||
# -- authority access (read-only projections for builders) ----------------
|
||||
|
||||
def _authorities(self) -> dict[str, dict]:
|
||||
"""The structural authorities for THIS mode. Rune-internal consults
|
||||
the real Spine store; external mode consults only the project
|
||||
scaffold (a Rune-independent project has no Rune Spine)."""
|
||||
auth: dict[str, dict] = {"project_scaffold": self.scaffold.load()}
|
||||
if self.mode == "rune_internal":
|
||||
sd = spine_mod.load(Path(self.spine_store))
|
||||
if sd is not None:
|
||||
auth["rune_spine"] = sd
|
||||
return auth
|
||||
|
||||
def _record_session(self, doc: dict, operation: str, detail: str) -> None:
|
||||
"""Builder session record (§20): compact, no transcripts."""
|
||||
doc.setdefault("observations", []).append(
|
||||
{
|
||||
"kind": "builder_session",
|
||||
"builder": self.builder,
|
||||
"mode": self.mode,
|
||||
"operation": operation,
|
||||
"detail": detail[:200],
|
||||
"observed_at": ss.now(),
|
||||
}
|
||||
)
|
||||
|
||||
# ── §5 submit_need ────────────────────────────────────────────────────────
|
||||
|
||||
def submit_need(
|
||||
self,
|
||||
objective: str,
|
||||
*,
|
||||
origin: str = "builder",
|
||||
constraints: dict | None = None,
|
||||
affected_surface: list[str] | None = None,
|
||||
evidence_refs: list[str] | None = None,
|
||||
approval_context: str = "",
|
||||
task_ref: str = "",
|
||||
generate: bool = True,
|
||||
contract_id: str | None = None,
|
||||
) -> dict:
|
||||
"""Submit a structured Need; run the full live decision path;
|
||||
optionally generate the Construction Contract. Returns decision
|
||||
summary + contract ref + outstanding questions. Does NOT decide
|
||||
approval — that surfaces via state.approval."""
|
||||
if not objective or not objective.strip():
|
||||
return _envelope(
|
||||
"submit_need",
|
||||
success=False,
|
||||
errors=[{"error": Errors.NEED_INCOMPLETE, "detail": "objective is required"}],
|
||||
)
|
||||
need = cd.Need(
|
||||
need_id=f"need-{ss.now()}-{self.builder}",
|
||||
objective=objective,
|
||||
origin=origin,
|
||||
project=self.scaffold.project_id,
|
||||
task_ref=task_ref,
|
||||
constraints=dict(constraints or {}),
|
||||
affected_surface=list(affected_surface or []),
|
||||
evidence_refs=list(evidence_refs or []),
|
||||
approval_context=approval_context,
|
||||
)
|
||||
doc = self.scaffold.load()
|
||||
authorities = self._authorities()
|
||||
trace = cd.make_decision(need, authorities=authorities, project_toolchain={"existing_language": "python"})
|
||||
ids: dict = {"need_id": need.need_id}
|
||||
state: dict = {
|
||||
"resolution": trace["resolution"],
|
||||
"completeness": trace["completeness"]["verdict"],
|
||||
"research": trace["research"],
|
||||
"approval": trace["approval"],
|
||||
"builder": self.builder,
|
||||
}
|
||||
errors, warnings = [], []
|
||||
if trace["completeness"]["verdict"] == cd.MATERIAL_GAP:
|
||||
errors.append(
|
||||
{
|
||||
"error": Errors.INSUFFICIENT_EVIDENCE,
|
||||
"detail": "material requirement gap",
|
||||
"gaps": trace["completeness"]["gaps"],
|
||||
"probe_questions": trace["completeness"]["probe_questions"],
|
||||
}
|
||||
)
|
||||
if trace["obligations"].get("pattern_conflicts"):
|
||||
errors.append(
|
||||
{
|
||||
"error": Errors.PATTERN_CONFLICT,
|
||||
"detail": "pattern obligations conflict",
|
||||
"conflicts": trace["obligations"]["pattern_conflicts"],
|
||||
}
|
||||
)
|
||||
if trace["approval"]["required"]:
|
||||
errors.append({"error": Errors.APPROVAL_REQUIRED, "reasons": trace["approval"]["reasons"]})
|
||||
if trace["resolution"] == "INSUFFICIENT_EVIDENCE" and not errors:
|
||||
errors.append({"error": Errors.INSUFFICIENT_EVIDENCE, "detail": trace["rationale"][:200]})
|
||||
contract = None
|
||||
if generate and trace["resolution"] not in ("INSUFFICIENT_EVIDENCE",):
|
||||
cid = contract_id or f"BUILDER-{ss.now()}-{self.builder}"
|
||||
cd.generate_contract(
|
||||
self.scaffold,
|
||||
doc,
|
||||
trace,
|
||||
contract_id=cid,
|
||||
scope=None,
|
||||
proposer_note=f"via Construction Interface by {self.builder}",
|
||||
)
|
||||
ids["contract_id"] = cid
|
||||
state["contract_phase"] = "draft"
|
||||
state["next_action"] = "review for approval"
|
||||
warnings.append({"warning": "contract generated in draft; requires approval transition before building"})
|
||||
self._record_session(doc, "submit_need", f"{need.need_id} → {trace['resolution']}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"submit_need",
|
||||
success=False,
|
||||
ids=ids,
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
return _envelope("submit_need", success=not errors, ids=ids, state=state, errors=errors, warnings=warnings)
|
||||
|
||||
# ── §6 inspect_decision ───────────────────────────────────────────────────
|
||||
|
||||
def inspect_decision(self, contract_id: str) -> dict:
|
||||
"""Read-only projection of the durable decision trace (§6)."""
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"inspect_decision",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
t = c.get("ext", {}).get("decision_trace", {})
|
||||
return _envelope(
|
||||
"inspect_decision",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", "")},
|
||||
state={
|
||||
"requirement": t.get("requirement", {}),
|
||||
"completeness": t.get("completeness", {}),
|
||||
"candidates": t.get("candidates", []),
|
||||
"fits": [f for f in t.get("fits", []) if f["fit"] != "UNKNOWN"],
|
||||
"resolution": t.get("resolution", ""),
|
||||
"rationale": t.get("rationale", ""),
|
||||
"toolchain": t.get("toolchain", {}),
|
||||
"patterns_considered": t.get("patterns_considered", []),
|
||||
"structural_obligations": {
|
||||
k: t.get("obligations", {}).get(k, [])
|
||||
for k in ("regression", "verification", "composition", "acquisition")
|
||||
},
|
||||
"pattern_obligations": t.get("obligations", {}).get("pattern", []),
|
||||
"approval": t.get("approval", {}),
|
||||
"iterations": t.get("iterations", []),
|
||||
"builder": self.builder,
|
||||
},
|
||||
)
|
||||
|
||||
# ── §7 get_scope ──────────────────────────────────────────────────────────
|
||||
|
||||
def get_scope(self, contract_id: str) -> dict:
|
||||
"""Mechanical builder-facing scope (§7): what may I touch?"""
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"get_scope",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
ext = c.get("ext", {})
|
||||
return _envelope(
|
||||
"get_scope",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"allowed_paths": c["identities"]["scope"],
|
||||
"affected_structural_entities": c["identities"]["affected_entities"],
|
||||
"expected_new_entities": c["identities"].get("expected_new_entities", []),
|
||||
"affected_paths": ext.get("affected_paths", []),
|
||||
"affected_ui_surfaces": ext.get("affected_ui_surfaces", []),
|
||||
"workflow_config_surfaces": ext.get("workflow_config_surfaces", []),
|
||||
"unresolved_surfaces": ext.get("unresolved_surfaces", []),
|
||||
"expected_runtime_effects": ext.get("expected_runtime_effects", []),
|
||||
"layout_obligations": t_layout(ext),
|
||||
"scope_amendments": ext.get("scope_amendments", []),
|
||||
"builder": self.builder,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def t_layout(ext: dict) -> list[dict]:
|
||||
"""Layout obligations from a contract's decision trace (helper)."""
|
||||
return ext.get("decision_trace", {}).get("layout_obligations", [])
|
||||
|
||||
|
||||
# ── §8 get_obligations / §9 report_observation / §10 verification ───────────
|
||||
|
||||
|
||||
def _obligations_projection(c: dict) -> list[dict]:
|
||||
t = c.get("ext", {}).get("decision_trace", {})
|
||||
obl = t.get("obligations", {})
|
||||
out = []
|
||||
for kind in ("regression", "verification", "composition", "acquisition"):
|
||||
for item in obl.get(kind, []):
|
||||
out.append(
|
||||
{
|
||||
"id": f"{kind}:{abs(hash(str(item))) % 10**8}",
|
||||
"description": str(item)[:200],
|
||||
"provenance": "STRUCTURAL",
|
||||
"kind": kind,
|
||||
"status": "REQUIRED",
|
||||
}
|
||||
)
|
||||
for p in obl.get("pattern", []):
|
||||
out.append(
|
||||
{
|
||||
"id": p["id"],
|
||||
"description": p["obligation"],
|
||||
"provenance": "PATTERN",
|
||||
"pattern": f"{p['pattern']}@r{p['revision']}",
|
||||
"pattern_applicability": p.get("pattern_applicability", ""),
|
||||
"evidence": p.get("reason", ""),
|
||||
"status": p.get("verdict", "REQUIRED"),
|
||||
}
|
||||
)
|
||||
for lo in t.get("layout_obligations", []):
|
||||
out.append(
|
||||
{
|
||||
"id": f"layout:{lo['component']}",
|
||||
"description": lo["obligation"],
|
||||
"provenance": "LAYOUT_CONTRACT",
|
||||
"status": "REQUIRED",
|
||||
"verification": lo["verification"],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _obligation_scope_files(obligations: list[dict]) -> list[str]:
|
||||
"""Obligation ids referenced by closure scope checks."""
|
||||
return []
|
||||
|
||||
|
||||
class ConstructionInterfaceOps:
|
||||
"""Instance methods continued (kept separate only for readability)."""
|
||||
|
||||
|
||||
# ── bind the ops onto the class (methods defined in _bind_ops's locals) ─────
|
||||
|
||||
|
||||
def _make_methods(namespace: dict) -> dict:
|
||||
return {k: v for k, v in namespace.items() if callable(v) and not k.startswith("_")}
|
||||
|
||||
|
||||
# Rebuild the ops cleanly as real methods (the _bind_ops trick above was
|
||||
# obscure — explicit binding is the honest, debuggable form):
|
||||
|
||||
|
||||
def _get_obligations(self, contract_id: str) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"get_obligations",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
return _envelope(
|
||||
"get_obligations",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={"obligations": _obligations_projection(c), "builder": self.builder},
|
||||
)
|
||||
|
||||
|
||||
def _report_observation(self, contract_id: str, observations: list[dict]) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
if not isinstance(observations, list) or not observations:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.NEED_INCOMPLETE, "detail": "observations list required"}],
|
||||
)
|
||||
items = []
|
||||
for o in observations:
|
||||
if not isinstance(o, dict) or not o.get("entity"):
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.NEED_INCOMPLETE, "detail": "each observation needs an entity"}],
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"entity": o["entity"],
|
||||
"source": o.get("source", f"builder:{self.builder}"),
|
||||
"detail": str(o.get("detail", ""))[:300],
|
||||
}
|
||||
)
|
||||
ss.record_observed_impact(doc, contract_id, items)
|
||||
self._record_session(doc, "report_observation", f"{len(items)} observation(s) on {contract_id}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"recorded": len(items),
|
||||
"note": "evidence recorded; satisfaction is determined by Scaffolding gates, not builder claims",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _verify(
|
||||
self, contract_id: str, *, changed_files: list[str] | None = None, layout_observations: dict | None = None
|
||||
) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"verify",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
state: dict = {"phase": c.get("phase"), "status": c.get("status")}
|
||||
errors, warnings = [], []
|
||||
if changed_files is not None:
|
||||
scope = self.scaffold.check_scope(doc, contract_id, changed_files)
|
||||
state["scope_check"] = scope
|
||||
if scope["verdict"] == "red":
|
||||
errors.append({"error": Errors.SCOPE_VIOLATION, "out_of_scope": scope["out_of_scope"]})
|
||||
elif scope["verdict"] == "unknown":
|
||||
warnings.append({"warning": "scope UNKNOWN — contract has no approved scope paths"})
|
||||
if layout_observations:
|
||||
from rune import layout_contract as lc
|
||||
|
||||
gate = lc.check_layout(doc, layout_observations)
|
||||
state["layout_gate"] = gate["verdict"]
|
||||
if gate["verdict"] == "FAIL":
|
||||
errors.append(
|
||||
{"error": Errors.LAYOUT_VIOLATION, "checks": [x for x in gate["checks"] if x["verdict"] == "FAIL"]}
|
||||
)
|
||||
elif gate["verdict"] == "UNKNOWN":
|
||||
warnings.append({"warning": "layout UNKNOWN — never PASS"})
|
||||
obligations = _obligations_projection(c)
|
||||
q = c.get("ext", {}).get("contract_quality")
|
||||
if q:
|
||||
state["contract_quality"] = {
|
||||
k: q[k]
|
||||
for k in (
|
||||
"correctly_predicted",
|
||||
"expected_new_observed",
|
||||
"expected_runtime_observed",
|
||||
"genuinely_unexpected_new",
|
||||
"scope_check",
|
||||
)
|
||||
if k in q
|
||||
}
|
||||
state["obligations_total"] = len(obligations)
|
||||
state["remaining"] = (
|
||||
"closure requires phase transitions through verifying with evidence; builder cannot self-certify"
|
||||
)
|
||||
return _envelope(
|
||||
"verify", success=not errors, ids={"contract_id": contract_id}, state=state, errors=errors, warnings=warnings
|
||||
)
|
||||
|
||||
|
||||
def _transition(self, contract_id: str, phase: str, *, evidence_ref: str = "") -> dict:
|
||||
doc = self.scaffold.load()
|
||||
try:
|
||||
self.scaffold.transition_contract(
|
||||
doc, contract_id, phase, evidence_ref=evidence_ref or f"builder:{self.builder}"
|
||||
)
|
||||
except sc.IllegalPhaseTransition as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.VERIFICATION_FAILED, "detail": str(e)}],
|
||||
)
|
||||
except ValueError as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": str(e)}],
|
||||
)
|
||||
self._record_session(doc, "transition", f"{contract_id} → {phase}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
return _envelope("transition", success=True, ids={"contract_id": contract_id}, state={"phase": phase})
|
||||
|
||||
|
||||
def _close(self, contract_id: str, *, observed_evidence: list[dict], scope_files: list[str] | None = None) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
try:
|
||||
self.scaffold.close_contract(doc, contract_id, observed_evidence=observed_evidence, scope_files=scope_files)
|
||||
except sc.IllegalPhaseTransition as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.VERIFICATION_FAILED, "detail": str(e)}],
|
||||
)
|
||||
except ValueError as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": str(e)}],
|
||||
)
|
||||
self._record_session(doc, "close", f"{contract_id} closed")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
q = self.scaffold.contract(doc, contract_id)["ext"]["contract_quality"]
|
||||
return _envelope(
|
||||
"close",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"phase": "closed",
|
||||
"contract_quality": {
|
||||
k: q[k]
|
||||
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
|
||||
if k in q
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _resume(self, contract_id: str) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"resume",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
ext = c.get("ext", {})
|
||||
t = ext.get("decision_trace", {})
|
||||
return _envelope(
|
||||
"resume",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", "")},
|
||||
state={
|
||||
"mode": self.mode,
|
||||
"need": t.get("need", {}),
|
||||
"decision": {
|
||||
"resolution": t.get("resolution"),
|
||||
"completeness": t.get("completeness", {}).get("verdict"),
|
||||
"toolchain": t.get("toolchain", {}).get("selected_language", ""),
|
||||
},
|
||||
"phase": c.get("phase"),
|
||||
"status": c.get("status"),
|
||||
"scope": c["identities"]["scope"],
|
||||
"obligations": _obligations_projection(c),
|
||||
"patterns": c.get("ext", {}).get("pattern_refs", []),
|
||||
"evidence": [cp.get("evidence", "")[:80] for cp in c.get("checkpoints", [])],
|
||||
"unresolved": ext.get("unresolved_surfaces", []),
|
||||
"approval": t.get("approval", {}),
|
||||
"next_action": c["identities"].get("next_action", ""),
|
||||
"last_builder_observations": [
|
||||
o
|
||||
for o in doc.get("observations", [])
|
||||
if o.get("kind") == "observed_consequence" and o.get("task") == contract_id
|
||||
][-5:],
|
||||
"builder": self.builder,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _amend_scope(self, contract_id: str, add_paths: list[str], *, reason: str, authority: str = "user") -> dict:
|
||||
if authority != "user":
|
||||
return _envelope(
|
||||
"amend_scope",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[
|
||||
{
|
||||
"error": Errors.APPROVAL_REQUIRED,
|
||||
"detail": "scope expansion requires user authority — builders cannot broaden their own scope",
|
||||
}
|
||||
],
|
||||
)
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"amend_scope",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
c["identities"]["scope"] = sorted(set(c["identities"]["scope"]) | set(add_paths))
|
||||
c.setdefault("ext", {}).setdefault("scope_amendments", []).append(
|
||||
{"added": sorted(add_paths), "reason": reason[:200], "authorized_by": "user"}
|
||||
)
|
||||
self._record_session(doc, "amend_scope", f"{contract_id} +{len(add_paths)} paths (user)")
|
||||
self.scaffold.save(doc)
|
||||
return _envelope(
|
||||
"amend_scope", success=True, ids={"contract_id": contract_id}, state={"scope": c["identities"]["scope"]}
|
||||
)
|
||||
|
||||
|
||||
# explicit method binding (readable, debuggable — no locals() tricks)
|
||||
ConstructionInterface.get_obligations = _get_obligations
|
||||
ConstructionInterface.report_observation = _report_observation
|
||||
ConstructionInterface.verify = _verify
|
||||
ConstructionInterface.transition = _transition
|
||||
ConstructionInterface.close = _close
|
||||
ConstructionInterface.resume = _resume
|
||||
ConstructionInterface.amend_scope = _amend_scope
|
||||
|
||||
|
||||
# ── CLI transport (§3/§12/§27) ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
"""JSON CLI — same convention as rune-spine/rune-scaffold mains. Every
|
||||
operation prints one JSON envelope; exit 0 = success, 1 = operation
|
||||
failure (the envelope carries the explicit error state)."""
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
if not argv or argv[0] in ("-h", "--help"):
|
||||
print(__doc__)
|
||||
return 0
|
||||
cmd = argv[0]
|
||||
args = argv[1:]
|
||||
builder = "cli"
|
||||
project_store = None
|
||||
project_id = None
|
||||
# --builder / --project-store / --project-id flags
|
||||
for flag, key in (("--builder", "b"), ("--project-store", "s"), ("--project-id", "p")):
|
||||
while flag in args:
|
||||
i = args.index(flag)
|
||||
val = args[i + 1]
|
||||
del args[i : i + 2]
|
||||
if flag == "--builder":
|
||||
builder = val
|
||||
elif flag == "--project-store":
|
||||
project_store = val
|
||||
else:
|
||||
project_id = val
|
||||
|
||||
iface = ConstructionInterface(builder=builder, project_store=project_store, project_id=project_id)
|
||||
|
||||
def _out(env: dict) -> int:
|
||||
print(json.dumps(env, indent=1, default=str))
|
||||
return 0 if env["success"] else 1
|
||||
|
||||
if cmd == "submit-need":
|
||||
payload = json.loads(args[0]) if args else {}
|
||||
return _out(
|
||||
iface.submit_need(
|
||||
payload.get("objective", ""),
|
||||
origin=payload.get("origin", "builder"),
|
||||
constraints=payload.get("constraints"),
|
||||
affected_surface=payload.get("affected_surface"),
|
||||
evidence_refs=payload.get("evidence_refs"),
|
||||
approval_context=payload.get("approval_context", ""),
|
||||
generate=payload.get("generate", True),
|
||||
)
|
||||
)
|
||||
if cmd == "decision":
|
||||
return _out(iface.inspect_decision(args[0]))
|
||||
if cmd == "scope":
|
||||
return _out(iface.get_scope(args[0]))
|
||||
if cmd == "obligations":
|
||||
return _out(iface.get_obligations(args[0]))
|
||||
if cmd == "observe":
|
||||
return _out(iface.report_observation(args[0], json.loads(args[1])))
|
||||
if cmd == "verify":
|
||||
return _out(iface.verify(args[0], changed_files=(json.loads(args[1]) if len(args) > 1 else None)))
|
||||
if cmd == "transition":
|
||||
return _out(iface.transition(args[0], args[1], evidence_ref=args[2] if len(args) > 2 else ""))
|
||||
if cmd == "close":
|
||||
return _out(
|
||||
iface.close(
|
||||
args[0],
|
||||
observed_evidence=json.loads(args[1]),
|
||||
scope_files=(json.loads(args[2]) if len(args) > 2 else None),
|
||||
)
|
||||
)
|
||||
if cmd == "resume":
|
||||
return _out(iface.resume(args[0]))
|
||||
return _out(
|
||||
_envelope(cmd, success=False, errors=[{"error": "UNKNOWN_OPERATION", "detail": f"unknown command: {cmd}"}])
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -204,6 +204,35 @@ def _surface_text(need, surface_result: dict) -> set[str]:
|
||||
return tokens
|
||||
|
||||
|
||||
# ── shared boundary-aware matchers (Tranche 7 §0) ──────────────────────────
|
||||
|
||||
def _path_matches(signal: str, path: str) -> bool:
|
||||
"""Boundary-aware path-signal matching: short (<4 char) signals match
|
||||
path TOKENS only (the 'ui'-in-'builtin' false-positive class); longer
|
||||
signals may use containment."""
|
||||
import re as _re
|
||||
|
||||
s = signal.lower()
|
||||
if len(s) >= 4:
|
||||
return s in path.lower()
|
||||
return s in set(_re.split(r"[^a-z0-9]+", path.lower()))
|
||||
|
||||
|
||||
def _entity_matches(signal: str, entity_id: str) -> bool:
|
||||
"""Same discipline for entity ids. Prefix signals ending in ':' (e.g.
|
||||
'ui:', 'region:') match the id's namespace prefix — structural kinds
|
||||
carry meaning in their prefix, and prefix equality is exact, not a
|
||||
substring accident."""
|
||||
import re as _re
|
||||
|
||||
s = signal.lower()
|
||||
if s.endswith(":"):
|
||||
return entity_id.lower().startswith(s)
|
||||
if len(s) >= 4:
|
||||
return s in entity_id.lower()
|
||||
return s in set(_re.split(r"[^a-z0-9:]+", entity_id.lower()))
|
||||
|
||||
|
||||
def evaluate_applicability(
|
||||
pattern: ConstructionPattern, need, surface_result: dict, authorities: dict[str, dict]
|
||||
) -> dict:
|
||||
@@ -283,16 +312,18 @@ def evaluate_applicability(
|
||||
)
|
||||
_check(
|
||||
"path_contains",
|
||||
[p.lower() for p in signals.get("path_contains", [])],
|
||||
[p for p in signals.get("path_contains", [])],
|
||||
lambda c: any(
|
||||
c in p.lower()
|
||||
_path_matches(c, p)
|
||||
for p in surface_result.get("affected_paths", []) + surface_result.get("workflow_config_surfaces", [])
|
||||
),
|
||||
)
|
||||
_check(
|
||||
"entities_match",
|
||||
[e.lower() for e in signals.get("entities_match", [])],
|
||||
lambda c: any(c in e.lower() for e in surface_result.get("affected_existing_entities", [])),
|
||||
[e for e in signals.get("entities_match", [])],
|
||||
lambda c: any(
|
||||
_entity_matches(c, e) for e in surface_result.get("affected_existing_entities", [])
|
||||
),
|
||||
)
|
||||
_check(
|
||||
"objective_any",
|
||||
@@ -348,25 +379,25 @@ def _obligation_when_hit(obligation: dict, need, surface_result: dict, authoriti
|
||||
An obligation with no recognized `when` is UNKNOWN (never silently
|
||||
required)."""
|
||||
when = obligation.get("when")
|
||||
paths = [p.lower() for p in surface_result.get("affected_paths", [])] + [
|
||||
p.lower() for p in surface_result.get("workflow_config_surfaces", [])
|
||||
paths = [p for p in surface_result.get("affected_paths", [])] + [
|
||||
p for p in surface_result.get("workflow_config_surfaces", [])
|
||||
]
|
||||
entities = (
|
||||
[e.lower() for e in surface_result.get("affected_existing_entities", [])]
|
||||
+ [u.lower() for u in surface_result.get("affected_ui_surfaces", [])]
|
||||
+ [r.lower() for r in surface_result.get("affected_regions", [])]
|
||||
[e for e in surface_result.get("affected_existing_entities", [])]
|
||||
+ [u for u in surface_result.get("affected_ui_surfaces", [])]
|
||||
+ [r for r in surface_result.get("affected_regions", [])]
|
||||
)
|
||||
if when is None:
|
||||
return OBL_UNKNOWN, "obligation declares no applicability condition"
|
||||
if when.get("always") is True:
|
||||
return OBL_REQUIRED, "pattern-level obligation (always applies when the pattern applies)"
|
||||
if "path_contains" in when:
|
||||
hits = [p for sub in when["path_contains"] for p in paths if sub.lower() in p]
|
||||
hits = [p for sub in when["path_contains"] for p in paths if _path_matches(sub, p)]
|
||||
if hits:
|
||||
return OBL_APPLICABLE, f"affected path(s): {hits[:3]}"
|
||||
return OBL_NOT_APPLICABLE, ("no affected path matches " + str(when["path_contains"]))
|
||||
if "entities_match" in when:
|
||||
hits = [e for sub in when["entities_match"] for e in entities if sub.lower() in e]
|
||||
hits = [e for sub in when["entities_match"] for e in entities if _entity_matches(sub, e)]
|
||||
if hits:
|
||||
return OBL_APPLICABLE, f"affected entity/entities: {hits[:3]}"
|
||||
return OBL_NOT_APPLICABLE, ("no affected entity matches " + str(when["entities_match"]))
|
||||
|
||||
@@ -173,7 +173,7 @@ UI_TUI_STRUCTURAL = ConstructionPattern(
|
||||
"id": "ui-component-identity-stable",
|
||||
"obligation": "ui:<component> identity survives implementation-path "
|
||||
"changes; authority never keyed by filename alone",
|
||||
"when": {"path_contains": ["tui", "ui", "widget"]},
|
||||
"when": {"entities_match": ["ui:", "region:", "layout:"]},
|
||||
},
|
||||
],
|
||||
provenance=[
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""tui_backlog.py — durable TUI Upgrade Backlog (SCAFFOLD-TRANCHE-007 §22-25).
|
||||
|
||||
Standing architectural requirement: every meaningful backend feature is
|
||||
checked for whether it needs a TUI/user-surface projection or operator
|
||||
control. The backlog records that work so the user-facing system never
|
||||
falls behind the backend.
|
||||
|
||||
AUTHORITY (§25): the backlog is a maintenance/product-surface LEDGER —
|
||||
outstanding work only. It is NOT TUI structural authority. Implemented TUI
|
||||
structure remains under Layout Contracts / the project's structural
|
||||
authority.
|
||||
|
||||
Projection types (§24): NONE / INSPECTION / CONTROL / BOTH / UNKNOWN.
|
||||
Auto-capture seam: construction-contract closure can ask "TUI projection
|
||||
required?" — recorded truthfully even when the answer is NONE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rune.structural_store import add_entity, entity, now
|
||||
|
||||
__all__ = [
|
||||
"add_backlog_item",
|
||||
"get_backlog_item",
|
||||
"list_backlog",
|
||||
"update_backlog_status",
|
||||
"record_tui_projection_decision",
|
||||
"PROJECTIONS",
|
||||
"STATUSES",
|
||||
]
|
||||
|
||||
PROJECTIONS = {"NONE", "INSPECTION", "CONTROL", "BOTH", "UNKNOWN"}
|
||||
STATUSES = {"OPEN", "PLANNED", "IN_PROGRESS", "DONE", "WONT_DO"}
|
||||
|
||||
|
||||
def add_backlog_item(
|
||||
doc: dict,
|
||||
item_id: str,
|
||||
*,
|
||||
feature: str,
|
||||
desired_behavior: str,
|
||||
why: str,
|
||||
projection: str = "INSPECTION",
|
||||
dependencies: list[str] | None = None,
|
||||
priority: str = "normal",
|
||||
status: str = "OPEN",
|
||||
authority_source: str = "product-surface requirement",
|
||||
evidence_refs: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Add/refresh one backlog item (§22). `feature` (originating subsystem)
|
||||
and `projection` classification are retained permanently. Duplicate ids
|
||||
refresh in place — the ledger does not fork."""
|
||||
if projection not in PROJECTIONS:
|
||||
raise ValueError(f"projection must be one of {sorted(PROJECTIONS)}")
|
||||
if status not in STATUSES:
|
||||
raise ValueError(f"status must be one of {sorted(STATUSES)}")
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"tui-backlog:{item_id}",
|
||||
kind="tui_backlog_item",
|
||||
identities={
|
||||
"feature": feature,
|
||||
"desired_behavior": desired_behavior,
|
||||
"why": why,
|
||||
"projection": projection,
|
||||
"dependencies": sorted(dependencies or []),
|
||||
"priority": priority,
|
||||
"status": status,
|
||||
"authority_source": authority_source,
|
||||
},
|
||||
evidence_refs=evidence_refs or [f"backlog-added:{authority_source}"],
|
||||
)
|
||||
|
||||
|
||||
def get_backlog_item(doc: dict, item_id: str) -> dict | None:
|
||||
return entity(doc, f"tui-backlog:{item_id}")
|
||||
|
||||
|
||||
def list_backlog(doc: dict, *, status: str | None = None, projection: str | None = None) -> list[dict]:
|
||||
out = []
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] != "tui_backlog_item":
|
||||
continue
|
||||
if status and e["identities"].get("status") != status:
|
||||
continue
|
||||
if projection and e["identities"].get("projection") != projection:
|
||||
continue
|
||||
out.append(e)
|
||||
return sorted(out, key=lambda e: e["id"])
|
||||
|
||||
|
||||
def update_backlog_status(doc: dict, item_id: str, status: str, *, evidence_ref: str = "") -> dict:
|
||||
e = get_backlog_item(doc, item_id)
|
||||
if e is None:
|
||||
raise ValueError(f"no backlog item: {item_id}")
|
||||
if status not in STATUSES:
|
||||
raise ValueError(f"status must be one of {sorted(STATUSES)}")
|
||||
e["identities"]["status"] = status
|
||||
e["observed_at"] = now()
|
||||
if evidence_ref:
|
||||
e["evidence_refs"] = sorted(set(e.get("evidence_refs", [])) | {evidence_ref})
|
||||
return e
|
||||
|
||||
|
||||
def record_tui_projection_decision(
|
||||
doc: dict, feature_id: str, *, projection: str, rationale: str, source_contract: str = ""
|
||||
) -> dict:
|
||||
"""§24 AUTO-CAPTURE SEAM: record a "TUI projection required?" decision
|
||||
for a backend feature — including a truthful NONE. This is accounting,
|
||||
not UI design. The seam exists so future contract closures can call it;
|
||||
nothing is auto-designed."""
|
||||
if projection not in PROJECTIONS:
|
||||
raise ValueError(f"projection must be one of {sorted(PROJECTIONS)}")
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"tui-projection:{feature_id}",
|
||||
kind="tui_projection_decision",
|
||||
identities={
|
||||
"feature": feature_id,
|
||||
"projection": projection,
|
||||
"rationale": rationale[:200],
|
||||
"source_contract": source_contract,
|
||||
},
|
||||
evidence_refs=[f"tui-projection-decision:{projection}"],
|
||||
)
|
||||
File diff suppressed because it is too large.
Load diff
+280
-124
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,489 @@
|
||||
"""test_construction_interface.py — SCAFFOLD-TRANCHE-007 test plans
|
||||
§29 (interface core), §30 (authority), §31 (pattern precision — integration),
|
||||
§17 (provider independence)."""
|
||||
|
||||
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 construction_interface as ci # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(__file__), "..")
|
||||
|
||||
|
||||
def _fixture_authorities():
|
||||
"""A self-contained project-scaffold authority (external-project mode
|
||||
without any real external repo — §16)."""
|
||||
spine = ss.new_doc()
|
||||
ss.add_entity(
|
||||
spine, id="capability:exporter", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"]
|
||||
)
|
||||
ss.add_entity(spine, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
spine, type="implements", from_id="capability:exporter", to_id="contract:csv_export", evidence_refs=["f"]
|
||||
)
|
||||
return spine
|
||||
|
||||
|
||||
def _iface(tmp_path, builder="test-builder", with_authority=True):
|
||||
store = str(tmp_path / ".rune" / "scaffold")
|
||||
iface = ci.ConstructionInterface(builder=builder, project_store=store, project_id="fixture-proj")
|
||||
if with_authority:
|
||||
# inject the fixture authority INTO the fixture scaffold store so
|
||||
# external-project mode has structural truth to consult
|
||||
iface.scaffold.init(purpose="fixture")
|
||||
doc = iface.scaffold.load()
|
||||
for e in _fixture_authorities()["entities"]:
|
||||
doc["entities"].append(json.loads(json.dumps(e)))
|
||||
for r in _fixture_authorities()["relationships"]:
|
||||
doc["relationships"].append(json.loads(json.dumps(r)))
|
||||
iface.scaffold.save(doc)
|
||||
return iface
|
||||
|
||||
|
||||
def _submit(iface, **kw):
|
||||
payload = dict(
|
||||
objective="persistent project export",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=[],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by fixture authority",
|
||||
)
|
||||
payload.update(kw)
|
||||
return iface.submit_need(**payload)
|
||||
|
||||
|
||||
# ── §29 interface core ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_29a_submit_need(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
assert env["schema"] == ci.INTERFACE_SCHEMA_VERSION
|
||||
assert env["operation"] == "submit_need"
|
||||
assert env["success"]
|
||||
assert env["ids"]["need_id"]
|
||||
assert env["state"]["resolution"] == "REUSE"
|
||||
assert env["state"]["builder"] == "test-builder"
|
||||
|
||||
|
||||
def test_29b_retrieve_decision(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
cid = env["ids"]["contract_id"]
|
||||
d = iface.inspect_decision(cid)
|
||||
assert d["success"]
|
||||
assert d["state"]["resolution"] == "REUSE"
|
||||
assert d["state"]["toolchain"]["selected_language"] == "python"
|
||||
assert any(f["candidate"] == "capability:exporter" for f in d["state"]["fits"])
|
||||
|
||||
|
||||
def test_29c_retrieve_contract(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
r = iface.resume(cid)
|
||||
assert r["success"]
|
||||
assert r["state"]["phase"] == "draft"
|
||||
assert r["state"]["need"]["objective"] == "persistent project export"
|
||||
|
||||
|
||||
def test_29d_allowed_scope(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
s = iface.get_scope(cid)
|
||||
assert s["success"]
|
||||
assert "allowed_paths" in s["state"]
|
||||
assert "affected_structural_entities" in s["state"]
|
||||
assert "unresolved_surfaces" in s["state"]
|
||||
assert "layout_obligations" in s["state"]
|
||||
|
||||
|
||||
def test_29e_obligations(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
o = iface.get_obligations(cid)
|
||||
assert o["success"]
|
||||
assert o["state"]["obligations"]
|
||||
provs = {x["provenance"] for x in o["state"]["obligations"]}
|
||||
assert provs <= {"STRUCTURAL", "PATTERN", "LAYOUT_CONTRACT"}
|
||||
|
||||
|
||||
def test_29f_report_observation(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
r = iface.report_observation(cid, [{"entity": "capability:exporter", "detail": "exercised by new test"}])
|
||||
assert r["success"]
|
||||
assert r["state"]["recorded"] == 1
|
||||
assert "cannot self-certify" not in r["state"]["note"]
|
||||
assert "determined by Scaffolding gates" in r["state"]["note"]
|
||||
|
||||
|
||||
def test_29g_verification_state(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
v = iface.verify(cid, changed_files=["src/new_thing.py"])
|
||||
assert v["success"] or v["errors"]
|
||||
assert "scope_check" in v["state"] or v["errors"]
|
||||
|
||||
|
||||
def test_29h_resume_fresh_process(tmp_path):
|
||||
"""§11 THE MAJOR ACCEPTANCE CONDITION: a fresh builder process resumes
|
||||
from contract id alone."""
|
||||
iface = _iface(tmp_path, builder="first-builder")
|
||||
env = iface.submit_need(
|
||||
"persistent project export",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=["src/export_module.py"],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by fixture authority",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
iface.transition(cid, "approved")
|
||||
script = f"""
|
||||
import sys, json
|
||||
sys.path.insert(0, {os.path.join(ROOT, "src")!r})
|
||||
from rune import construction_interface as ci
|
||||
iface = ci.ConstructionInterface(builder="fresh-builder",
|
||||
project_store={str(tmp_path / ".rune" / "scaffold")!r},
|
||||
project_id="fixture-proj")
|
||||
env = iface.resume({cid!r})
|
||||
print(json.dumps(env))
|
||||
"""
|
||||
r = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=60)
|
||||
assert r.returncode == 0, r.stderr
|
||||
env = json.loads(r.stdout)
|
||||
assert env["success"]
|
||||
st = env["state"]
|
||||
assert st["need"]["objective"] == "persistent project export"
|
||||
assert st["decision"]["resolution"] == "REUSE"
|
||||
assert st["phase"] == "approved"
|
||||
assert st["scope"]
|
||||
assert st["obligations"]
|
||||
assert st["next_action"]
|
||||
assert st["builder"] == "fresh-builder" # this session's identity
|
||||
|
||||
|
||||
def test_29i_json_roundtrip(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
blob = json.dumps(env)
|
||||
back = json.loads(blob)
|
||||
assert back["operation"] == env["operation"]
|
||||
assert back["state"]["resolution"] == env["state"]["resolution"]
|
||||
|
||||
|
||||
def test_29j_unknown_contract(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
for op in (
|
||||
lambda: iface.inspect_decision("nope"),
|
||||
lambda: iface.get_scope("nope"),
|
||||
lambda: iface.get_obligations("nope"),
|
||||
lambda: iface.resume("nope"),
|
||||
):
|
||||
env = op()
|
||||
assert not env["success"]
|
||||
assert env["errors"][0]["error"] == ci.Errors.UNKNOWN_CONTRACT
|
||||
|
||||
|
||||
def test_29k_approval_required(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = iface.submit_need(
|
||||
"build a whole new application from scratch",
|
||||
constraints={},
|
||||
affected_surface=[],
|
||||
evidence_refs=["x"],
|
||||
approval_context="",
|
||||
) # no approval context
|
||||
# new-capability construction without approval context → gate fires
|
||||
if env["state"]["approval"]["required"]:
|
||||
assert env["errors"]
|
||||
assert any(e["error"] == ci.Errors.APPROVAL_REQUIRED for e in env["errors"])
|
||||
else:
|
||||
# covered path must say so
|
||||
assert env["state"]["approval"]["covered_by"] or not env["state"]["approval"]["reasons"]
|
||||
|
||||
|
||||
def test_29l_scope_violation_refused(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = iface.submit_need(
|
||||
"persistent project export",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=["src/export_module.py"], # scoped surface
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by fixture authority",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
iface.transition(cid, "approved")
|
||||
iface.transition(cid, "building")
|
||||
iface.transition(cid, "verifying")
|
||||
v = iface.verify(cid, changed_files=["src/completely/unrelated.py"])
|
||||
# out-of-scope change must surface as a violation, never green
|
||||
assert v["state"]["scope_check"]["verdict"] == "red" or any(
|
||||
e["error"] == ci.Errors.SCOPE_VIOLATION for e in v["errors"]
|
||||
)
|
||||
|
||||
|
||||
def test_29m_failed_verification_cannot_close(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
# contract is in draft — closure must be refused by the phase map
|
||||
env = iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "claimed done"}])
|
||||
assert not env["success"]
|
||||
assert env["errors"][0]["error"] == ci.Errors.VERIFICATION_FAILED
|
||||
|
||||
|
||||
def test_29n_stale_state_store_write(tmp_path):
|
||||
"""STORE_WRITE_FAILED: an unwritable store surfaces as an explicit
|
||||
error, not a silent lie."""
|
||||
iface = _iface(tmp_path)
|
||||
# break the store path
|
||||
iface.scaffold.current = "/proc/nonexistent/current.json"
|
||||
env = iface.report_observation("any", [{"entity": "x"}])
|
||||
assert not env["success"] or env["errors"]
|
||||
|
||||
|
||||
# ── §30 authority ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_30a_rune_mode_reads_rune_authority():
|
||||
"""Rune-internal mode consults the real Spine store (read-only)."""
|
||||
iface = ci.ConstructionInterface(builder="t")
|
||||
auth = iface._authorities()
|
||||
assert "rune_spine" in auth
|
||||
assert iface.mode == "rune_internal"
|
||||
|
||||
|
||||
def test_30b_external_mode_reads_project_scaffold(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
auth = iface._authorities()
|
||||
assert "rune_spine" not in auth # external project has no Rune Spine
|
||||
assert iface.mode == "external_project"
|
||||
assert auth["project_scaffold"]["entities"]
|
||||
|
||||
|
||||
def test_30c_builder_cannot_rewrite_structural_authority():
|
||||
"""The interface exposes NO operation that writes structural truth.
|
||||
(The only writes are contract/observation/session records in the
|
||||
scaffold store — never Spine entities.)"""
|
||||
public = [m for m in dir(ci.ConstructionInterface) if not m.startswith("_")]
|
||||
for forbidden in ("rewrite_spine", "set_entity", "promote", "supersede", "build_spine", "add_relationship"):
|
||||
assert forbidden not in public
|
||||
|
||||
|
||||
def test_30d_builder_cannot_override_user_locked(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
from rune import layout_contract as lc
|
||||
|
||||
doc = iface.scaffold.load()
|
||||
lc.define_region(doc, "footer-bar", provenance="scaffold")
|
||||
lc.bind_component(doc, "version_display", "footer-bar", lock=True, provenance="user", order=0)
|
||||
iface.scaffold.save(doc)
|
||||
# no interface operation amends layout at all
|
||||
public = [m for m in dir(ci.ConstructionInterface) if not m.startswith("_")]
|
||||
assert not any("layout" in m and "amend" in m for m in public)
|
||||
# and the direct amend path still refuses non-user authority
|
||||
doc2 = iface.scaffold.load()
|
||||
with pytest.raises(ValueError):
|
||||
lc.amend_layout_contract(
|
||||
doc2, "version_display", new_region="header", authorized_by="builder", reason="prefer it"
|
||||
)
|
||||
|
||||
|
||||
def test_30e_builder_cannot_self_certify(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
# no interface operation takes "done=True"
|
||||
v = iface.verify(cid)
|
||||
assert "cannot self-certify" in v["state"]["remaining"]
|
||||
# closure from draft fails
|
||||
env = iface.close(cid, observed_evidence=[{"entity": "x"}])
|
||||
assert not env["success"]
|
||||
|
||||
|
||||
def test_30f_builder_identity_neutral(tmp_path):
|
||||
"""§17: two builders with identical input get identical construction
|
||||
behavior (identity recorded, semantics unchanged)."""
|
||||
results = []
|
||||
for b in ("mimo", "kimi", "deterministic-script"):
|
||||
iface = _iface(tmp_path / b, builder=b)
|
||||
env = _submit(iface)
|
||||
results.append(
|
||||
(
|
||||
env["state"]["resolution"],
|
||||
env["state"]["completeness"],
|
||||
env["state"]["toolchain"] if "toolchain" in env["state"] else None,
|
||||
[o["id"] for o in iface.get_obligations(env["ids"]["contract_id"])]["obligations"] if False else None,
|
||||
)
|
||||
)
|
||||
resolutions = {r[0] for r in results}
|
||||
completions = {r[1] for r in results}
|
||||
assert len(resolutions) == 1 and len(completions) == 1
|
||||
|
||||
|
||||
# ── §31 pattern precision (integration through the interface) ───────────────
|
||||
|
||||
|
||||
def test_31a_ui_task_derives_ui_obligations(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
from rune import layout_contract as lc
|
||||
|
||||
doc = iface.scaffold.load()
|
||||
lc.define_region(doc, "footer-bar", provenance="scaffold")
|
||||
lc.bind_component(doc, "version_display", "footer-bar", provenance="scaffold")
|
||||
iface.scaffold.save(doc)
|
||||
env = iface.submit_need(
|
||||
"adjust the tui version widget",
|
||||
constraints={},
|
||||
affected_surface=["ui:version_display"],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
o = iface.get_obligations(cid)
|
||||
ids = {x["id"] for x in o["state"]["obligations"]}
|
||||
assert any("ui-" in i or "layout:" in i for i in ids)
|
||||
|
||||
|
||||
def test_31b_non_ui_task_no_active_ui_obligations(tmp_path):
|
||||
"""§0 regression through the interface: non-UI work gets NO active
|
||||
ui-* obligations."""
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
cid = env["ids"]["contract_id"]
|
||||
o = iface.get_obligations(cid)
|
||||
ui_active = [x for x in o["state"]["obligations"] if x["id"].startswith("ui-")]
|
||||
assert ui_active == []
|
||||
|
||||
|
||||
def test_31c_release_task_derives_release_obligations(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
# the fixture authority needs the deploy structural truth for fit
|
||||
doc = iface.scaffold.load()
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="module:deploy_stage",
|
||||
kind="module",
|
||||
identities={"path": "scripts/deploy_stage.py"},
|
||||
maturity="WIRED",
|
||||
evidence_refs=["f"],
|
||||
)
|
||||
ss.add_entity(doc, id="contract:monotonic_promotion", kind="contract", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
doc, type="implements", from_id="module:deploy_stage", to_id="contract:monotonic_promotion", evidence_refs=["f"]
|
||||
)
|
||||
iface.scaffold.save(doc)
|
||||
env = iface.submit_need(
|
||||
"repair the deploy promotion ordering",
|
||||
constraints={"contracts": ["contract:monotonic_promotion"]},
|
||||
affected_surface=["scripts/deploy_stage.py", "scripts/deploy_promotion.py"],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
o = iface.get_obligations(cid)
|
||||
ids = {x["id"] for x in o["state"]["obligations"]}
|
||||
assert "rel-monotonic-promotion" in ids or any("rel-" in i for i in ids)
|
||||
|
||||
|
||||
def test_31d_non_release_task_no_promotion_obligations(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
cid = env["ids"]["contract_id"]
|
||||
o = iface.get_obligations(cid)
|
||||
rel_active = [x for x in o["state"]["obligations"] if x["id"].startswith("rel-")]
|
||||
assert rel_active == []
|
||||
|
||||
|
||||
def test_31g_partial_pattern_inspectable_without_activation(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = iface.submit_need(
|
||||
"adjust the tui backlog plumbing",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=["src/rune/tui_backlog.py", "src/export_module.py"],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
d = iface.inspect_decision(cid)
|
||||
considered = {a["pattern"]: a["verdict"] for a in d["state"]["patterns_considered"]}
|
||||
# PARTIAL patterns remain VISIBLE as candidates (inspectable)
|
||||
assert considered
|
||||
# but their UI obligations did not enter the ACTIVE obligation set
|
||||
o = iface.get_obligations(cid)
|
||||
ui_active = [x for x in o["state"]["obligations"] if x["id"].startswith("ui-")]
|
||||
assert ui_active == []
|
||||
|
||||
|
||||
# ── CLI transport ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_cli_roundtrip(tmp_path):
|
||||
store = tmp_path / ".rune" / "scaffold"
|
||||
iface = ci.ConstructionInterface(builder="seed", project_store=str(store), project_id="cli-fixture")
|
||||
iface.scaffold.init(purpose="cli")
|
||||
doc = iface.scaffold.load()
|
||||
for e in _fixture_authorities()["entities"]:
|
||||
doc["entities"].append(json.loads(json.dumps(e)))
|
||||
for r in _fixture_authorities()["relationships"]:
|
||||
doc["relationships"].append(json.loads(json.dumps(r)))
|
||||
iface.scaffold.save(doc)
|
||||
env = {
|
||||
"objective": "persistent project export",
|
||||
"constraints": {"contracts": ["contract:csv_export"]},
|
||||
"affected_surface": [],
|
||||
"evidence_refs": ["x"],
|
||||
"approval_context": "covered",
|
||||
}
|
||||
r = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rune.construction_interface",
|
||||
"submit-need",
|
||||
json.dumps(env),
|
||||
"--builder",
|
||||
"cli-builder",
|
||||
"--project-store",
|
||||
str(store),
|
||||
"--project-id",
|
||||
"cli-fixture",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
env={**os.environ, "PYTHONPATH": os.path.join(ROOT, "src")},
|
||||
)
|
||||
assert r.returncode == 0, r.stderr[-400:]
|
||||
out = json.loads(r.stdout[r.stdout.index("{") :])
|
||||
assert out["success"]
|
||||
assert out["state"]["resolution"] == "REUSE"
|
||||
# resume through the CLI from the returned contract id
|
||||
r2 = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rune.construction_interface",
|
||||
"resume",
|
||||
out["ids"]["contract_id"],
|
||||
"--builder",
|
||||
"cli-2",
|
||||
"--project-store",
|
||||
str(store),
|
||||
"--project-id",
|
||||
"cli-fixture",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
env={**os.environ, "PYTHONPATH": os.path.join(ROOT, "src")},
|
||||
)
|
||||
assert r2.returncode == 0, r2.stderr[-400:]
|
||||
out2 = json.loads(r2.stdout[r2.stdout.index("{") :])
|
||||
assert out2["state"]["need"]["objective"] == "persistent project export"
|
||||
@@ -0,0 +1,118 @@
|
||||
"""test_tui_backlog.py — SCAFFOLD-TRANCHE-007 §32 test plan."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import tui_backlog as tb # noqa: E402
|
||||
|
||||
|
||||
def _doc():
|
||||
return ss.new_doc()
|
||||
|
||||
|
||||
# §32 A-E: roundtrip with all retained fields
|
||||
def test_a_e_backlog_item_roundtrip(tmp_path):
|
||||
doc = _doc()
|
||||
tb.add_backlog_item(
|
||||
doc,
|
||||
"construction-contract-view",
|
||||
feature="scaffolding/contracts",
|
||||
desired_behavior="display active construction contract with phase, "
|
||||
"scope, obligations; navigate between contracts",
|
||||
why="builder sessions need operator visibility into governed builds",
|
||||
projection="BOTH",
|
||||
dependencies=["construction_interface"],
|
||||
priority="high",
|
||||
authority_source="tranche-7-brief §23",
|
||||
evidence_refs=["SCAFFOLD-TRANCHE-006"],
|
||||
)
|
||||
ss.save(doc, tmp_path / "d.json")
|
||||
doc2 = ss.load(tmp_path / "d.json")
|
||||
e = tb.get_backlog_item(doc2, "construction-contract-view")
|
||||
assert e["kind"] == "tui_backlog_item"
|
||||
ident = e["identities"]
|
||||
# B. originating feature retained
|
||||
assert ident["feature"] == "scaffolding/contracts"
|
||||
# C. display/control classification retained
|
||||
assert ident["projection"] == "BOTH"
|
||||
# D. authority source retained
|
||||
assert ident["authority_source"] == "tranche-7-brief §23"
|
||||
# E. status/priority retained
|
||||
assert ident["status"] == "OPEN" and ident["priority"] == "high"
|
||||
|
||||
|
||||
def test_f_backend_feature_records_tui_none_truthfully(tmp_path):
|
||||
doc = _doc()
|
||||
d = tb.record_tui_projection_decision(
|
||||
doc,
|
||||
"module:deployment_race_repair",
|
||||
projection="NONE",
|
||||
rationale="release-path internals have no user-surface; operator visibility flows through CI/deploy results",
|
||||
source_contract="DEPLOY-MONOTONIC-001",
|
||||
)
|
||||
assert d["identities"]["projection"] == "NONE"
|
||||
assert "no user-surface" in d["identities"]["rationale"]
|
||||
|
||||
|
||||
def test_g_future_feature_adds_requirement_without_schema_rewrite(tmp_path):
|
||||
doc = _doc()
|
||||
# an unforeseen projection kind rides in ext (open-world)
|
||||
from rune import structural_store
|
||||
|
||||
e = tb.add_backlog_item(doc, "future-thing", feature="x", desired_behavior="y", why="z", projection="INSPECTION")
|
||||
e.setdefault("ext", {})["novice_field"] = {"someday": True}
|
||||
ss.save(doc, tmp_path / "d.json")
|
||||
doc2 = ss.load(tmp_path / "d.json")
|
||||
item = tb.get_backlog_item(doc2, "future-thing")
|
||||
assert item["ext"]["novice_field"] == {"someday": True}
|
||||
|
||||
|
||||
def test_h_backlog_never_becomes_structural_authority(tmp_path):
|
||||
"""§25: backlog items live in the maintenance ledger namespace; they are
|
||||
not structural entities and never enter impact frontiers."""
|
||||
doc = _doc()
|
||||
tb.add_backlog_item(doc, "x", feature="f", desired_behavior="d", why="w")
|
||||
# kind check
|
||||
kinds = {e["kind"] for e in doc["entities"]}
|
||||
assert "tui_backlog_item" in kinds
|
||||
# impact frontier ignores it
|
||||
from rune import structural_store as ss
|
||||
|
||||
fr = ss.impact_frontier(doc, ["tui-backlog:x"])
|
||||
# the seed itself appears at level0 (any frontier includes its seed);
|
||||
# the §25 invariant is NO PROPAGATION: no obligations, no contracts,
|
||||
# no verification surfaces derive from a backlog item
|
||||
assert fr.get("level1_obligations") == []
|
||||
assert fr.get("contracts") == []
|
||||
assert fr.get("level2_verification") == []
|
||||
|
||||
|
||||
def test_status_update(tmp_path):
|
||||
doc = _doc()
|
||||
tb.add_backlog_item(doc, "x", feature="f", desired_behavior="d", why="w")
|
||||
tb.update_backlog_status(doc, "x", "PLANNED", evidence_ref="planning-t7")
|
||||
assert tb.get_backlog_item(doc, "x")["identities"]["status"] == "PLANNED"
|
||||
with pytest.raises(ValueError):
|
||||
tb.update_backlog_status(doc, "x", "SOMEDAY")
|
||||
with pytest.raises(ValueError):
|
||||
tb.update_backlog_status(doc, "ghost", "OPEN")
|
||||
|
||||
|
||||
def test_list_filters(tmp_path):
|
||||
doc = _doc()
|
||||
tb.add_backlog_item(doc, "a", feature="f1", desired_behavior="d", why="w", projection="INSPECTION", status="OPEN")
|
||||
tb.add_backlog_item(doc, "b", feature="f2", desired_behavior="d", why="w", projection="CONTROL", status="DONE")
|
||||
assert len(tb.list_backlog(doc)) == 2
|
||||
assert len(tb.list_backlog(doc, status="OPEN")) == 1
|
||||
assert len(tb.list_backlog(doc, projection="CONTROL")) == 1
|
||||
|
||||
|
||||
def test_projection_validation():
|
||||
doc = _doc()
|
||||
with pytest.raises(ValueError):
|
||||
tb.add_backlog_item(doc, "x", feature="f", desired_behavior="d", why="w", projection="SCREENSHOT")
|
||||
Reference in new issue
Block a user