Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce328a8080 | ||
|
|
4df1f6f23f | ||
|
|
7484e175be | ||
|
|
d84060cc02 | ||
|
|
b102225d4e | ||
|
|
f86b2d1eec | ||
|
|
307b0bf814 | ||
|
|
d776f286de | ||
|
|
621f88c425 | ||
|
|
ef6811ff58 | ||
|
|
a4f02f227c | ||
|
|
5b315ac7c7 | ||
|
|
90bd2b3285 | ||
|
|
1bb4ea86b8 | ||
|
|
f98416e94a |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.41**
|
||||
**Current release: v0.66.45**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
@@ -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-14T00:06:17Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
@@ -219,16 +219,19 @@
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer",
|
||||
"construction_decision",
|
||||
"construction_interface",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"layout_contract",
|
||||
"release_notes",
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"structural_store",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
|
||||
@@ -226,3 +226,50 @@ contract — code adapts.
|
||||
- Scaffolding overall: adoption path live for one bounded flow — NOT
|
||||
general autonomous construction, NOT universal discovery/toolchain
|
||||
intelligence, NOT Scaffolding complete.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Scaffolding Tranche 9 — Release History + Evidence Correction (2026-09-13)
|
||||
|
||||
_Gitea Releases are now an evidence-derived human-readable history of
|
||||
Rune's evolution. Plus the Tranche 8 builder-evidence correction._
|
||||
|
||||
## Release semantics (audited)
|
||||
|
||||
| surface | meaning |
|
||||
|---|---|
|
||||
| VERSION | semver in pyproject/init (e.g. 0.66.44) |
|
||||
| TAG | v<VERSION>, created by auto-version at the BUMP commit |
|
||||
| release commit | the auto-bump SHA — NOT necessarily final state |
|
||||
| converged deployed identity | last commit deployed under the same VERSION |
|
||||
|
||||
Same-version convergence commits (closure/accounting/lock fixes landing and
|
||||
deploying after the bump) are normal; the notes record BOTH the tag commit
|
||||
and the final deployed identity, distinction exposed honestly.
|
||||
|
||||
## Mechanism
|
||||
|
||||
`release_notes.py`: deterministic evidence extraction from governed records
|
||||
(closed contracts, resolved maintenance, contract-quality records) →
|
||||
structured ReleaseNote → Markdown → Gitea API (POST create / PATCH update).
|
||||
Idempotent: publishing twice for a tag UPDATES, never duplicates. Notes are
|
||||
historical documentation — NOT Memory, NOT structural authority.
|
||||
|
||||
## T8 evidence correction (§1)
|
||||
|
||||
The T8 report claimed a close-from-verifying refusal + reactive superseding
|
||||
transition. Durable checkpoints show NO superseding transition and the
|
||||
phase map permits verifying→closed; live output showed first-attempt close
|
||||
success. The claim was RETRACTED with full provenance (original claim,
|
||||
correction, reason, evidence preserved in the contract record) and
|
||||
recorded as process-defect:unsupported-causal-claim. MiMo adoption
|
||||
maturity survives on the SUPPORTED evidence: scope constrained file edits,
|
||||
pattern obligations forced preservation proof, interface-reported
|
||||
observations, fresh resume.
|
||||
|
||||
## Maturity
|
||||
|
||||
Release-note generator: LIVE-PROVEN (real Gitea, 10 releases). Backfill:
|
||||
0.66.35→0.66.44 carrying historical-backfill provenance. Pre-0.66.35
|
||||
history: skipped (weak evidence — honest limitation).
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.41"
|
||||
version = "0.66.45"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ certifi==2026.7.22
|
||||
# httpx
|
||||
chardet==7.6.0
|
||||
# via diff-cover
|
||||
coverage==7.16.0
|
||||
coverage==7.16.1
|
||||
# via rune (pyproject.toml)
|
||||
diff-cover==10.5.1
|
||||
# via rune (pyproject.toml)
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.41"
|
||||
__version__ = "0.66.45"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.66.41"' in rune_version,
|
||||
'__version__ = "0.66.45"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -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,767 @@
|
||||
"""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
|
||||
|
||||
from typing import Any
|
||||
|
||||
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 capabilities(self) -> dict:
|
||||
"""§28 schema negotiation: builders detect the supported interface
|
||||
schema and operation set BEFORE calling operations. Also exposes the
|
||||
error-state vocabulary so builders can switch on states without
|
||||
parsing prose. Read-only; no contract required."""
|
||||
return _envelope(
|
||||
"capabilities",
|
||||
success=True,
|
||||
state={
|
||||
"schema_version": INTERFACE_SCHEMA_VERSION,
|
||||
"mode": self.mode,
|
||||
"project": self.scaffold.project_id,
|
||||
"operations": [
|
||||
"submit_need",
|
||||
"inspect_decision",
|
||||
"get_scope",
|
||||
"get_obligations",
|
||||
"report_observation",
|
||||
"verify",
|
||||
"transition",
|
||||
"close",
|
||||
"resume",
|
||||
"amend_scope",
|
||||
"capabilities",
|
||||
],
|
||||
"error_states": sorted(v for k, v in vars(Errors).items() if not k.startswith("_")),
|
||||
},
|
||||
)
|
||||
|
||||
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 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: Any) -> 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]}],
|
||||
)
|
||||
closed = self.scaffold.contract(doc, contract_id)
|
||||
if closed is None:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": "contract vanished after close"}],
|
||||
)
|
||||
q = closed["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)
|
||||
|
||||
|
||||
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 []
|
||||
|
||||
|
||||
# ── 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 == "capabilities":
|
||||
return _out(iface.capabilities())
|
||||
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,336 @@
|
||||
"""release_notes.py — evidence-derived Gitea release history.
|
||||
|
||||
SCAFFOLD-TRANCHE-009. Prime invariant (tranche brief):
|
||||
|
||||
Git records exact source history. Construction Contracts / maintenance
|
||||
items / tests / deployment records provide governed evidence. Gitea
|
||||
Releases provide the HUMAN-READABLE HISTORICAL ACCOUNT of what actually
|
||||
improved in each Rune release.
|
||||
|
||||
Release notes are evidence-derived history, not marketing prose:
|
||||
- facts come from governed records (closed contracts, resolved
|
||||
maintenance items, contract-quality records, observed evidence,
|
||||
lock/deploy artifacts) in that priority order;
|
||||
- commit messages alone never overrule governed evidence;
|
||||
- model prose may COMPRESS verified facts; it may not invent
|
||||
importance, completion, or maturity (§14 — this generator is fully
|
||||
deterministic; no model step exists here).
|
||||
|
||||
RELEASE SEMANTICS (§6, audited 2026-09-13):
|
||||
VERSION the semver in pyproject.toml / __init__ (e.g. 0.66.44)
|
||||
TAG v<VERSION>, created by auto-version on the BUMP commit
|
||||
release commit (tag SHA) the auto-bump commit — NOT necessarily the
|
||||
final state of the release
|
||||
converged deployed identity the last commit deployed under the same
|
||||
VERSION (same-version convergence commits are normal:
|
||||
closure/accounting/dependency-lock commits land and deploy
|
||||
after the bump without a version change)
|
||||
|
||||
The notes record BOTH: release_sha (tag target) and deployed_sha
|
||||
(converged deployment read from the deployment authority). The
|
||||
distinction is exposed honestly, never papered over (§6).
|
||||
|
||||
RELEASE RANGE for version N: (tag of previous version, tag N] PLUS
|
||||
same-version convergence commits up to the deployed identity (§9/§10).
|
||||
|
||||
PUBLICATION (§15): Gitea API POST /releases to create, PATCH /releases/{id}
|
||||
to update. Idempotent (§17): publishing twice for the same tag UPDATES the
|
||||
existing release — never duplicates. Notes carry provenance (§20-21):
|
||||
historical release documentation, NOT Memory and NOT structural authority.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = [
|
||||
"ReleaseNote",
|
||||
"extract_evidence_facts",
|
||||
"build_note",
|
||||
"render_markdown",
|
||||
"publish_release",
|
||||
"fetch_release",
|
||||
"converged_deployed_sha",
|
||||
]
|
||||
|
||||
# ── structured record (§13) ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ReleaseNote:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
version: str,
|
||||
summary: str,
|
||||
improved: list[str] | None = None,
|
||||
added: list[str] | None = None,
|
||||
fixed: list[str] | None = None,
|
||||
proven: list[str] | None = None,
|
||||
limitations: list[str] | None = None,
|
||||
references: list[str] | None = None,
|
||||
release_sha: str = "",
|
||||
deployed_sha: str = "",
|
||||
provenance: list[str] | None = None,
|
||||
backfill: bool = False,
|
||||
):
|
||||
self.version = version
|
||||
self.summary = summary
|
||||
self.improved = sorted(improved or [])
|
||||
self.added = sorted(added or [])
|
||||
self.fixed = sorted(fixed or [])
|
||||
self.proven = sorted(proven or [])
|
||||
self.limitations = sorted(limitations or [])
|
||||
self.references = sorted(references or [])
|
||||
self.release_sha = release_sha
|
||||
self.deployed_sha = deployed_sha
|
||||
self.provenance = sorted(provenance or [])
|
||||
self.backfill = backfill
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"version": self.version,
|
||||
"summary": self.summary,
|
||||
"improved": self.improved,
|
||||
"added": self.added,
|
||||
"fixed": self.fixed,
|
||||
"proven": self.proven,
|
||||
"limitations": self.limitations,
|
||||
"references": self.references,
|
||||
"release_sha": self.release_sha,
|
||||
"deployed_sha": self.deployed_sha,
|
||||
"provenance": self.provenance,
|
||||
"backfill": self.backfill,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "ReleaseNote":
|
||||
return cls(**d)
|
||||
|
||||
|
||||
def render_markdown(note: ReleaseNote) -> str:
|
||||
"""Human-readable Markdown (§7/§11): concise sections, empty sections
|
||||
omitted, evidence references kept, version/deployed distinction shown."""
|
||||
lines = [f"# Rune v{note.version}", "", note.summary, ""]
|
||||
if note.release_sha and note.deployed_sha:
|
||||
if note.release_sha[:12] == note.deployed_sha[:12]:
|
||||
lines.append(f"_Release commit: `{note.release_sha[:12]}` (deployed: `{note.deployed_sha[:12]}`)_")
|
||||
else:
|
||||
lines.append(
|
||||
f"_Tag commit: `{note.release_sha[:12]}` · "
|
||||
f"**final deployed: `{note.deployed_sha[:12]}`** "
|
||||
f"(same-version convergence commits included)_"
|
||||
)
|
||||
elif note.deployed_sha:
|
||||
lines.append(f"_Deployed: `{note.deployed_sha[:12]}`_")
|
||||
lines.append("")
|
||||
sections = [
|
||||
("Improved", note.improved),
|
||||
("Added", note.added),
|
||||
("Fixed", note.fixed),
|
||||
("Proven", note.proven),
|
||||
("Known limitations", note.limitations),
|
||||
("References", note.references),
|
||||
]
|
||||
for title, items in sections:
|
||||
if not items:
|
||||
continue
|
||||
lines.append(f"## {title}")
|
||||
for item in items:
|
||||
lines.append(f"- {item}")
|
||||
lines.append("")
|
||||
if note.backfill:
|
||||
lines.append("_Historical backfill: derived from surviving governed evidence after the fact._")
|
||||
if note.provenance:
|
||||
lines.append("_Derived from: " + "; ".join(note.provenance) + "_")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
# ── evidence extraction (§8/§24) ─────────────────────────────────────────────
|
||||
|
||||
_CONTRACT_EVIDENCE = "Construction Contract"
|
||||
_MAINT_EVIDENCE = "maintenance item"
|
||||
_DEPLOY_EVIDENCE = "deployment record"
|
||||
_LOCK_EVIDENCE = "dependency-lock record"
|
||||
|
||||
|
||||
def extract_evidence_facts(scaffold_doc: dict) -> dict:
|
||||
"""Deterministic fact extraction from governed records (§8). Returns the
|
||||
raw fact pool the note builder selects from. Commit messages are NOT a
|
||||
source here — governed records only."""
|
||||
facts: dict = {"improved": [], "added": [], "fixed": [], "proven": [], "limitations": [], "references": []}
|
||||
for e in scaffold_doc.get("entities", []):
|
||||
ident = e.get("identities", {})
|
||||
if e["kind"] == "construction_contract" and e.get("status") == "closed":
|
||||
facts["references"].append(f"{e['id']} — {ident.get('need', '')[:90]}")
|
||||
q = e.get("ext", {}).get("contract_quality", {})
|
||||
if q.get("correctly_predicted"):
|
||||
facts["proven"].append(
|
||||
f"{e['id']}: predicted effects independently confirmed ({len(q['correctly_predicted'])} entities)"
|
||||
)
|
||||
elif e["kind"] == "maintenance_item":
|
||||
status = ident.get("status")
|
||||
issue = ident.get("issue", "")[:90]
|
||||
if status == "resolved":
|
||||
facts["fixed"].append(f"{e['id']}: {issue}")
|
||||
elif status == "open":
|
||||
facts["limitations"].append(f"{e['id']}: {issue}")
|
||||
elif e["kind"] == "tui_projection_decision":
|
||||
# recorded projection decisions are release-relevant facts
|
||||
facts["references"].append(f"TUI projection for {ident.get('feature', '')}: {ident.get('projection', '')}")
|
||||
# contract-quality declared maturity evidence
|
||||
for e in scaffold_doc.get("entities", []):
|
||||
if e["kind"] == "construction_contract" and e.get("ext", {}).get("release_evidence"):
|
||||
rel = e["ext"]["release_evidence"]
|
||||
if isinstance(rel, dict) and rel.get("final_deployed") or rel.get("deployed_identity"):
|
||||
facts["proven"].append(
|
||||
f"{e['id']}: release chain proven ({rel.get('final_deployed') or rel.get('deployed_identity')})"
|
||||
)
|
||||
for k in facts:
|
||||
facts[k] = sorted(set(facts[k]))
|
||||
return facts
|
||||
|
||||
|
||||
def build_note(
|
||||
*,
|
||||
version: str,
|
||||
summary: str,
|
||||
scaffold_doc: dict,
|
||||
release_sha: str = "",
|
||||
deployed_sha: str = "",
|
||||
extra: dict | None = None,
|
||||
provenance: list[str] | None = None,
|
||||
backfill: bool = False,
|
||||
exclude_unsupported: list[str] | None = None,
|
||||
) -> ReleaseNote:
|
||||
"""Assemble a ReleaseNote from the governed fact pool + explicit extras
|
||||
(extra items must themselves be evidence-backed — the caller carries the
|
||||
responsibility). `exclude_unsupported` drops fact entries containing any
|
||||
of the given substrings — the §24F unsupported-claim rejection surface."""
|
||||
facts = extract_evidence_facts(scaffold_doc)
|
||||
if exclude_unsupported:
|
||||
|
||||
def _clean(items: list[str]) -> list[str]:
|
||||
return [i for i in items if not any(x.lower() in i.lower() for x in exclude_unsupported)]
|
||||
|
||||
for k in facts:
|
||||
facts[k] = _clean(facts[k])
|
||||
merged = {}
|
||||
for k in ("improved", "added", "fixed", "proven", "limitations", "references"):
|
||||
# §24F: extras pass through the SAME unsupported-claim filter as
|
||||
# extracted facts — a caller cannot smuggle a retracted claim in
|
||||
# via `extra` either.
|
||||
extras = [
|
||||
i for i in (extra or {}).get(k, []) if not any(x.lower() in i.lower() for x in (exclude_unsupported or []))
|
||||
]
|
||||
merged[k] = sorted(set(facts[k]) | set(extras))
|
||||
return ReleaseNote(
|
||||
version=version,
|
||||
summary=summary,
|
||||
release_sha=release_sha,
|
||||
deployed_sha=deployed_sha,
|
||||
provenance=provenance or [],
|
||||
backfill=backfill,
|
||||
**merged,
|
||||
)
|
||||
|
||||
|
||||
# ── Gitea publication (§15/§17/§25) ──────────────────────────────────────────
|
||||
|
||||
|
||||
def _api(url: str, token: str, method: str = "GET", payload: dict | None = None) -> tuple[int, dict]:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
method=method,
|
||||
headers={"Authorization": f"token {token}", "Content-Type": "application/json"},
|
||||
data=json.dumps(payload).encode() if payload else None,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
body = r.read()
|
||||
return r.status, (json.loads(body) if body else {})
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read()
|
||||
text = body.decode("utf-8", "replace")
|
||||
try:
|
||||
return e.code, (json.loads(text) if text else {})
|
||||
except ValueError:
|
||||
# Gitea returns plain-text bodies for some errors (e.g. 404
|
||||
# "page not found" on unknown release-tag lookups)
|
||||
return e.code, {"_raw": text[:200]}
|
||||
except OSError as e:
|
||||
return 0, {"_error": str(e)[:200]}
|
||||
|
||||
|
||||
def fetch_release(server: str, repo: str, token: str, tag: str) -> dict | None:
|
||||
status, data = _api(f"{server}/api/v1/repos/{repo}/releases/tags/{tag}", token)
|
||||
return data if status == 200 else None
|
||||
|
||||
|
||||
def publish_release(server: str, repo: str, token: str, note: ReleaseNote, *, target_commitish: str = "master") -> dict:
|
||||
"""Create or idempotently update the Gitea release for note.version
|
||||
(§17): existing release → PATCH body (same evidence converges, never
|
||||
duplicates); missing release → POST create with the tag target. The tag
|
||||
itself is owned by auto-version — this module never rewrites tags (§30.5).
|
||||
Returns {action: created|updated, release_id, html_url?} or an error
|
||||
dict with `failed: True` (§16 — publication failure is visible)."""
|
||||
tag = f"v{note.version}"
|
||||
body = render_markdown(note)
|
||||
existing = fetch_release(server, repo, token, tag)
|
||||
if existing is not None:
|
||||
status, data = _api(
|
||||
f"{server}/api/v1/repos/{repo}/releases/{existing['id']}",
|
||||
token,
|
||||
"PATCH",
|
||||
{"body": body, "name": f"Rune v{note.version}"},
|
||||
)
|
||||
if status != 200:
|
||||
return {
|
||||
"failed": True,
|
||||
"action": "update",
|
||||
"status": status,
|
||||
"error": data.get("_error") or data.get("message", "")[:200],
|
||||
}
|
||||
return {"action": "updated", "release_id": data.get("id"), "html_url": data.get("html_url", "")}
|
||||
status, data = _api(
|
||||
f"{server}/api/v1/repos/{repo}/releases",
|
||||
token,
|
||||
"POST",
|
||||
{
|
||||
"tag_name": tag,
|
||||
"target_commitish": target_commitish,
|
||||
"name": f"Rune v{note.version}",
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
},
|
||||
)
|
||||
if status not in (200, 201):
|
||||
return {
|
||||
"failed": True,
|
||||
"action": "create",
|
||||
"status": status,
|
||||
"error": data.get("_error") or data.get("message", "")[:200],
|
||||
}
|
||||
return {"action": "created", "release_id": data.get("id"), "html_url": data.get("html_url", "")}
|
||||
|
||||
|
||||
def converged_deployed_sha(deploy_root: str = "/mnt/workdrive/rune-code") -> str:
|
||||
"""Read the converged deployed commit from the deployment authority
|
||||
(the `current` symlink's build identity) — never assumed equal to the
|
||||
tag SHA (§6)."""
|
||||
try:
|
||||
target = Path(os_readlink(Path(deploy_root) / "current"))
|
||||
identity = json.loads((target / "src" / "rune" / "_build_identity.json").read_text(encoding="utf-8"))
|
||||
return str(identity.get("commit", ""))
|
||||
except (OSError, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
def os_readlink(p: Path) -> Path:
|
||||
import os
|
||||
|
||||
return Path(os.readlink(p))
|
||||
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
|
||||
|
||||
This is not an interface. This is how I see, speak, and interact with the world.
|
||||
|
||||
Version: v0.66.41
|
||||
Version: v0.66.45
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -1856,7 +1856,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.66.41 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.66.45 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -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}"],
|
||||
)
|
||||
+11181
-27
File diff suppressed because it is too large.
Load diff
+386
-124
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,551 @@
|
||||
"""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"
|
||||
|
||||
|
||||
# ── §28 capabilities / schema negotiation (SCAFFOLD-TRANCHE-008) ────────────
|
||||
|
||||
|
||||
def test_capabilities_operation(tmp_path):
|
||||
"""Builders can detect the supported interface schema + operations
|
||||
BEFORE calling anything (T7 §28 requirement, closed by this tranche)."""
|
||||
iface = _iface(tmp_path)
|
||||
env = iface.capabilities()
|
||||
assert env["success"]
|
||||
st = env["state"]
|
||||
assert st["schema_version"] == ci.INTERFACE_SCHEMA_VERSION
|
||||
for op in (
|
||||
"submit_need",
|
||||
"inspect_decision",
|
||||
"get_scope",
|
||||
"get_obligations",
|
||||
"report_observation",
|
||||
"verify",
|
||||
"transition",
|
||||
"close",
|
||||
"resume",
|
||||
"capabilities",
|
||||
):
|
||||
assert op in st["operations"]
|
||||
assert ci.Errors.SCOPE_VIOLATION in st["error_states"]
|
||||
assert ci.Errors.UNKNOWN_CONTRACT in st["error_states"]
|
||||
# mode + project identity exposed for builder routing
|
||||
assert st["mode"] == "external_project"
|
||||
assert st["project"] == "fixture-proj"
|
||||
|
||||
|
||||
def test_capabilities_cli_roundtrip(tmp_path):
|
||||
"""The CLI transport exposes the same capability set (§3 transport
|
||||
parity: the builder must not need the Python API for schema detection)."""
|
||||
import subprocess
|
||||
|
||||
env = {"objective": "x", "constraints": {}, "affected_surface": [], "evidence_refs": [], "approval_context": ""}
|
||||
r = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rune.construction_interface",
|
||||
"capabilities",
|
||||
"--builder",
|
||||
"probe",
|
||||
"--project-store",
|
||||
str(tmp_path / ".rune" / "scaffold"),
|
||||
"--project-id",
|
||||
"fixture-proj",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
env={**os.environ, "PYTHONPATH": os.path.join(ROOT, "src")},
|
||||
)
|
||||
assert r.returncode == 0, r.stderr[-300:]
|
||||
out = json.loads(r.stdout[r.stdout.index("{") :])
|
||||
assert out["success"]
|
||||
assert out["state"]["schema_version"] == ci.INTERFACE_SCHEMA_VERSION
|
||||
assert "capabilities" in out["state"]["operations"]
|
||||
@@ -0,0 +1,308 @@
|
||||
"""test_release_notes.py — SCAFFOLD-TRANCHE-009 test plans §24 (extraction),
|
||||
§25 (publication), §26 (T8 correction integration)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
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 release_notes as rn # noqa: E402
|
||||
|
||||
|
||||
def _governed_doc():
|
||||
"""Scaffold doc carrying the governed record shapes release notes draw
|
||||
from (closed contracts, resolved/open maintenance, quality records)."""
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="contract:C1",
|
||||
kind="construction_contract",
|
||||
identities={"need": "add export capability"},
|
||||
maturity="EXERCISED",
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
c = ss.entity(doc, "contract:C1")
|
||||
c["status"] = "closed"
|
||||
c.setdefault("ext", {})["contract_quality"] = {
|
||||
"correctly_predicted": ["module:a", "module:b"],
|
||||
"expected_new_observed": ["module:fresh"],
|
||||
}
|
||||
c["ext"]["release_evidence"] = {"final_deployed": "abc123 / 0.66.44"}
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="contract:C2",
|
||||
kind="construction_contract",
|
||||
identities={"need": "release safety repair"},
|
||||
maturity="LIVE_PROVEN",
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
c2 = ss.entity(doc, "contract:C2")
|
||||
c2["status"] = "closed"
|
||||
c2.setdefault("ext", {})["release_evidence"] = {"deployed_identity": "def456 / 0.66.32"}
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="maintenance:m1",
|
||||
kind="maintenance_item",
|
||||
identities={"issue": "deploy-order race", "status": "resolved"},
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="maintenance:m2",
|
||||
kind="maintenance_item",
|
||||
identities={"issue": "external repo proof pending user", "status": "open"},
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="tui-projection:module:x",
|
||||
kind="tui_projection_decision",
|
||||
identities={"feature": "module:x", "projection": "NONE"},
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
return doc
|
||||
|
||||
|
||||
# ── §24 evidence extraction ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_24a_added_capability_extracted():
|
||||
facts = rn.extract_evidence_facts(_governed_doc())
|
||||
assert any("C1" in r for r in facts["references"])
|
||||
|
||||
|
||||
def test_24b_fix_from_resolved_maintenance():
|
||||
facts = rn.extract_evidence_facts(_governed_doc())
|
||||
assert any("m1" in f and "deploy-order race" in f for f in facts["fixed"])
|
||||
assert not any("m2" in f for f in facts["fixed"])
|
||||
|
||||
|
||||
def test_24c_improvement_not_called_new():
|
||||
"""Closed contracts with correctly-predicted effects = PROVEN evidence,
|
||||
and the note builder places contract work under Improved/Proven, never
|
||||
fabricates 'new'."""
|
||||
facts = rn.extract_evidence_facts(_governed_doc())
|
||||
assert any("C1" in p for p in facts["proven"])
|
||||
note = rn.build_note(version="0.66.44", summary="s", scaffold_doc=_governed_doc())
|
||||
# proven items never leak into added[]
|
||||
assert not any("C1" in a or "C2" in a for a in note.added)
|
||||
assert any("C1" in p for p in note.proven)
|
||||
|
||||
|
||||
def test_24d_maturity_evidence_represented():
|
||||
facts = rn.extract_evidence_facts(_governed_doc())
|
||||
assert any("0.66.44" in p for p in facts["proven"])
|
||||
assert any("0.66.32" in p for p in facts["proven"])
|
||||
|
||||
|
||||
def test_24e_known_limitation_retained():
|
||||
note = rn.build_note(version="0.66.44", summary="s", scaffold_doc=_governed_doc())
|
||||
assert any("m2" in l and "pending user" in l for l in note.limitations)
|
||||
|
||||
|
||||
def test_24f_unsupported_claim_rejected():
|
||||
"""§24F/§12: the T8 close-refusal claim is excluded by the
|
||||
exclude_unsupported surface — it cannot enter generated notes."""
|
||||
doc = _governed_doc()
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="contract:T8",
|
||||
kind="construction_contract",
|
||||
identities={"need": "close-from-verifying refusal reaction proof"},
|
||||
maturity="WIRED",
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
c = ss.entity(doc, "contract:T8")
|
||||
c["status"] = "closed"
|
||||
c.setdefault("ext", {})["contract_quality"] = {"correctly_predicted": ["module:x"]}
|
||||
note = rn.build_note(
|
||||
version="0.66.44",
|
||||
summary="s",
|
||||
scaffold_doc=doc,
|
||||
extra={"proven": ["bounded MiMo close-refusal reaction proven"]},
|
||||
exclude_unsupported=["refusal", "reaction"],
|
||||
)
|
||||
blob = rn.render_markdown(note)
|
||||
assert "refusal" not in blob.lower()
|
||||
assert "reaction" not in blob.lower()
|
||||
|
||||
|
||||
def test_24g_same_version_later_commit_represented():
|
||||
note = rn.build_note(
|
||||
version="0.66.44",
|
||||
summary="s",
|
||||
scaffold_doc=_governed_doc(),
|
||||
release_sha="f86b2d1eecde",
|
||||
deployed_sha="d84060cc027f",
|
||||
)
|
||||
md = rn.render_markdown(note)
|
||||
assert "same-version convergence" in md
|
||||
assert "f86b2d1eecde"[:12] in md and "d84060cc027f"[:12] in md
|
||||
|
||||
|
||||
def test_24h_deployed_distinct_from_release_sha():
|
||||
note = rn.build_note(
|
||||
version="0.66.44",
|
||||
summary="s",
|
||||
scaffold_doc=_governed_doc(),
|
||||
release_sha="f86b2d1eecde039f",
|
||||
deployed_sha="d84060cc027f1a2b",
|
||||
)
|
||||
assert note.release_sha != note.deployed_sha
|
||||
md = rn.render_markdown(note)
|
||||
assert "final deployed" in md # distinction exposed honestly
|
||||
|
||||
|
||||
def test_24i_missing_evidence_no_invented_prose():
|
||||
"""§16/§24I: an empty governed pool produces a note with empty sections
|
||||
(omitted in Markdown) — never invented prose."""
|
||||
note = rn.build_note(version="9.9.9", summary="empty release", scaffold_doc=ss.new_doc())
|
||||
md = rn.render_markdown(note)
|
||||
assert "## Improved" not in md
|
||||
assert "## Added" not in md
|
||||
assert "empty release" in md # only the supplied summary exists
|
||||
|
||||
|
||||
# ── §25 publication (mocked transport; real Gitea verified live in §15) ─────
|
||||
|
||||
|
||||
def _note():
|
||||
return rn.build_note(
|
||||
version="0.66.44",
|
||||
summary="test",
|
||||
scaffold_doc=_governed_doc(),
|
||||
release_sha="f86b2d1eecde",
|
||||
deployed_sha="d84060cc027f",
|
||||
)
|
||||
|
||||
|
||||
def test_25a_create_release():
|
||||
calls = []
|
||||
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
calls.append((method, url))
|
||||
if "tags/" in url:
|
||||
return 404, {"message": "Not Found"}
|
||||
return 201, {"id": 77, "html_url": "http://x/y"}
|
||||
|
||||
with patch.object(rn, "_api", fake_api):
|
||||
result = rn.publish_release("http://s", "o/r", "tok", _note())
|
||||
assert result["action"] == "created"
|
||||
assert any(m == "POST" for m, _ in calls)
|
||||
assert not any(m == "PATCH" for m, _ in calls)
|
||||
|
||||
|
||||
def test_25b_c_retry_is_idempotent_update():
|
||||
"""§17: publishing twice for the same tag UPDATES — never duplicates."""
|
||||
calls = []
|
||||
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
calls.append((method, url.split("?")[0].split("/")[-1] if "releases/" in url else url))
|
||||
if "tags/" in url:
|
||||
return 200, {"id": 77, "body": "old"}
|
||||
return 200, {"id": 77, "html_url": "http://x/y"}
|
||||
|
||||
with patch.object(rn, "_api", fake_api):
|
||||
r1 = rn.publish_release("http://s", "o/r", "tok", _note())
|
||||
r2 = rn.publish_release("http://s", "o/r", "tok", _note())
|
||||
assert r1["action"] == "updated" and r2["action"] == "updated"
|
||||
assert r1["release_id"] == r2["release_id"] == 77
|
||||
posts = [c for c in calls if c[0] == "POST"]
|
||||
assert posts == [] # no duplicate release objects, ever
|
||||
|
||||
|
||||
def test_25d_notes_correspond_to_target_version():
|
||||
note = _note()
|
||||
assert note.version == "0.66.44"
|
||||
md = rn.render_markdown(note)
|
||||
assert "Rune v0.66.44" in md
|
||||
|
||||
|
||||
def test_25e_historical_refs_preserved():
|
||||
md = rn.render_markdown(_note())
|
||||
assert "contract:C1" in md or "contract:C2" in md
|
||||
|
||||
|
||||
def test_25f_publication_failure_detected():
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
if "tags/" in url:
|
||||
return 404, {"message": "Not Found"}
|
||||
return 500, {"message": "server error"}
|
||||
|
||||
with patch.object(rn, "_api", fake_api):
|
||||
result = rn.publish_release("http://s", "o/r", "tok", _note())
|
||||
assert result["failed"] is True
|
||||
assert result["action"] == "create"
|
||||
assert "server error" in result["error"]
|
||||
|
||||
|
||||
def test_25g_convergence_sha_recorded():
|
||||
note = rn.build_note(
|
||||
version="0.66.44",
|
||||
summary="s",
|
||||
scaffold_doc=_governed_doc(),
|
||||
release_sha="f86b2d1eecde039f",
|
||||
deployed_sha="d84060cc027f1a2b",
|
||||
)
|
||||
d = note.to_dict()
|
||||
assert d["release_sha"] != d["deployed_sha"]
|
||||
|
||||
|
||||
# ── §26 T8 correction integration ────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_26_correction_replaces_unsupported_claim():
|
||||
"""The durable T8 record carries BOTH the retracted original (provenance)
|
||||
AND the corrected claim — history preserved, not silently rewritten."""
|
||||
corrections = {
|
||||
"original_claim": "close-from-verifying refusal → completed phase map",
|
||||
"correction": "close succeeded directly from verifying",
|
||||
"reason": "checkpoints show no superseding transition",
|
||||
"evidence": ["checkpoints", "phase map", "live output"],
|
||||
}
|
||||
# the correction structure preserves all four required elements (§1)
|
||||
for key in ("original_claim", "correction", "reason", "evidence"):
|
||||
assert key in corrections
|
||||
# and the corrected claim is the weaker, supported one
|
||||
assert "succeeded directly" in corrections["correction"]
|
||||
assert "refusal" not in corrections["correction"]
|
||||
|
||||
|
||||
def test_26_note_generation_rejects_retracted_claim():
|
||||
"""Integration: building notes from the T8-shaped record with
|
||||
exclude_unsupported removes the retracted narrative (the held-out §19
|
||||
case)."""
|
||||
doc = _governed_doc()
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="contract:T8",
|
||||
kind="construction_contract",
|
||||
identities={"need": "governed builder session"},
|
||||
maturity="EXERCISED",
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
c = ss.entity(doc, "contract:T8")
|
||||
c["status"] = "closed"
|
||||
c.setdefault("ext", {})["builder_session_record"] = {
|
||||
"verification_result_that_changed_builder_behavior": "close-from-verifying refusal → completed phase map",
|
||||
"evidence_correction": {
|
||||
"correction": "close succeeded directly from verifying",
|
||||
},
|
||||
}
|
||||
note = rn.build_note(
|
||||
version="0.66.45",
|
||||
summary="s",
|
||||
scaffold_doc=doc,
|
||||
extra={
|
||||
"proven": ["governed builder session with durable interaction log", "close-from-verifying refusal reaction"]
|
||||
},
|
||||
exclude_unsupported=["refusal", "reaction"],
|
||||
)
|
||||
md = rn.render_markdown(note)
|
||||
assert "refusal" not in md.lower()
|
||||
assert "durable interaction log" in md # supported claim survives
|
||||
@@ -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