Compare commits

...
15 Commits
Author SHA1 Message Date
conductor ce328a8080 chore(release): auto-bump version to 0.66.45 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 15s
CI Pipeline / lint (push) Successful in 38s
CI Pipeline / unit-tests (push) Successful in 1m9s
CI Pipeline / canary (push) Successful in 29s
CI Pipeline / best-practices (push) Successful in 5s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 45s
Promotion record: (unknown)
2026-09-14 01:11:16 +01:00
Nick 4df1f6f23f feat(release): evidence-derived release history + T8 evidence correction (SCAFFOLD-TRANCHE-009)
CI Pipeline / best-practices (push) Successful in 12s
CI Pipeline / documentation (push) Successful in 6s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / lint (push) Successful in 42s
Auto-version and Release / release (push) Successful in 1m3s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 24s
CI Pipeline / unit-tests (push) Successful in 1m8s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 2m52s
- release_notes.py: deterministic extraction from governed records →
  structured ReleaseNote → Markdown → Gitea API. Idempotent (PATCH update
  for existing tags — never duplicates). Version/tag/release-commit/
  converged-deployed-identity distinction exposed honestly (§6).
- §24F unsupported-claim filter: extras AND extracted facts pass the same
  exclusion surface — the retracted T8 close-refusal claim cannot enter
  generated notes (test-proven).
- §1/§23: T8 evidence correction recorded with full provenance (original
  claim, correction, reason, evidence); process-defect:unsupported-
  causal-claim logged. MiMo maturity survives on supported evidence only.
- §18 backfill: 0.66.35→0.66.43 published from closed-contract
  release_evidence records, marked historical backfill. Pre-0.66.35
  skipped (weak evidence — honest limitation).
- §19: 0.66.44 notes published to real Gitea (release 89): tag commit
  f86b2d1eec vs final deployed d84060cc02 distinction, capabilities
  improvement, bounded MiMo adoption proof (corrected basis), lock-drift
  fix, external-project limitation.
- Gitea 400-tolerance: plain-text error bodies handled (actual behavior,
  not assumed GitHub semantics).

Tests: 17 new (§24/§25/§26 plans); suite 1608 green; henxels rc=0;
pyright clean.
2026-09-14 01:10:42 +01:00
Nick 7484e175be chore(scaffolding): T8 evidence correction — close-refusal claim retracted (durable checkpoints + live output show direct verifying→closed success); process defect recorded
Auto-version and Release / release (push) Successful in 11s
CI Pipeline / documentation (push) Successful in 6s
CI Pipeline / canary (push) Successful in 34s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / unit-tests (push) Successful in 1m7s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 1m15s
2026-09-14 00:56:08 +01:00
Nick d84060cc02 chore(deps): refresh coverage pin 7.16.0 -> 7.16.1 — lock drift ratchet (upstream release between runs)
Auto-version and Release / release (push) Successful in 14s
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / canary (push) Successful in 33s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / lint (push) Successful in 43s
CI Pipeline / unit-tests (push) Successful in 1m7s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / required-checks (push) Successful in 2s
deploy / deploy (push) Successful in 44s
2026-09-13 21:52:48 +01:00
Nick b102225d4e chore(scaffolding): SCAFFOLD-TRANCHE-008 — interface governance record, TUI projection NONE, builder interaction log
Auto-version and Release / release (push) Successful in 17s
CI Pipeline / best-practices (push) Successful in 5s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / lint (push) Failing after 29s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / unit-tests (push) Successful in 1m4s
CI Pipeline / required-checks (push) Failing after 1s
deploy / deploy (push) Failing after 48s
2026-09-13 21:37:42 +01:00
conductor f86b2d1eec chore(release): auto-bump version to 0.66.44 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 15s
CI Pipeline / lint (push) Failing after 27s
CI Pipeline / unit-tests (push) Successful in 1m3s
CI Pipeline / canary (push) Successful in 29s
CI Pipeline / best-practices (push) Successful in 5s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Failing after 1s
deploy / deploy (push) Failing after 46s
Promotion record: (unknown)
2026-09-13 21:34:13 +01:00
Nick 307b0bf814 feat(scaffolding): capabilities operation — builder schema negotiation (SCAFFOLD-TRANCHE-008 live task)
CI Pipeline / documentation (push) Successful in 6s
CI Pipeline / lint (push) Failing after 27s
CI Pipeline / canary (push) Successful in 36s
Auto-version and Release / release (push) Successful in 1m3s
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / unit-tests (push) Successful in 1m10s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / required-checks (push) Failing after 1s
deploy / deploy (push) Failing after 2m59s
Closes maintenance:interface-capabilities-op — T7 brief §28 required
builders to detect the supported interface schema before use; the envelope
carried the schema field passively but no operation exposed it.

- ConstructionInterface.capabilities(): schema_version, supported
  operations, error-state vocabulary, mode + project identity. Read-only,
  no contract required.
- CLI dispatch: capabilities command (transport parity — builders detect
  schema without the Python API).
- New-behavior proof: 2 tests (op + CLI roundtrip). Old-behavior
  preservation: existing 26 interface tests untouched and green; full
  suite 1591 green.

Governed through the Construction Interface itself (SCAFFOLD-TRANCHE-008):
Need submitted via submit_need, scope/obligations/patterns consumed before
implementation, observations reported via report_observation, phase map
completed through the interface (verifying → superseding → close).
2026-09-13 21:33:42 +01:00
Nick d776f286de chore(scaffolding): SCAFFOLD-TRANCHE-007 closed — TUI backlog (12 entries + projection seam), interface quality accounting, scope amendment with provenance
CI Pipeline / best-practices (push) Successful in 9s
Auto-version and Release / release (push) Successful in 20s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / canary (push) Successful in 37s
CI Pipeline / lint (push) Successful in 50s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / unit-tests (push) Successful in 1m16s
CI Pipeline / required-checks (push) Successful in 0s
deploy / deploy (push) Successful in 41s
2026-09-13 19:53:03 +01:00
conductor 621f88c425 chore(release): auto-bump version to 0.66.43 — earned by # (patch: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 12s
CI Pipeline / required-checks (push) Successful in 2s
CI Pipeline / lint (push) Successful in 43s
deploy / deploy (push) Successful in 55s
CI Pipeline / unit-tests (push) Successful in 1m11s
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 17s
Promotion record: (unknown)
2026-09-13 19:33:06 +01:00
Nick ef6811ff58 fix(scaffolding): annotate report_observation payload as Any — runtime isinstance guards are genuine for CLI JSON, not redundant (type-check ratchet)
CI Pipeline / best-practices (push) Successful in 9s
CI Pipeline / documentation (push) Successful in 6s
Auto-version and Release / release (push) Successful in 37s
CI Pipeline / canary (push) Successful in 41s
CI Pipeline / lint (push) Successful in 45s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 22s
CI Pipeline / unit-tests (push) Successful in 1m18s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 4m7s
2026-09-13 19:32:42 +01:00
Nick a4f02f227c refactor(scaffolding): Construction Interface methods in class body — removes class-attribute binding warnings (pyright ratchet)
CI Pipeline / best-practices (push) Successful in 10s
Auto-version and Release / release (push) Successful in 19s
CI Pipeline / documentation (push) Successful in 6s
CI Pipeline / canary (push) Successful in 41s
CI Pipeline / lint (push) Successful in 51s
CI Pipeline / type-check (push) Failing after 31s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 1m19s
CI Pipeline / required-checks (push) Failing after 1s
deploy / deploy (push) Failing after 39s
2026-09-13 19:16:34 +01:00
conductor 5b315ac7c7 chore(release): auto-bump version to 0.66.42 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 10s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / unit-tests (push) Successful in 1m3s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Failing after 28s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / required-checks (push) Failing after 1s
deploy / deploy (push) Failing after 46s
Promotion record: (unknown)
2026-09-13 17:23:46 +01:00
Nick 90bd2b3285 feat(scaffolding): Construction Interface — builder adapter, TUI backlog, pattern-precision repair (SCAFFOLD-TRANCHE-007)
CI Pipeline / best-practices (push) Successful in 9s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / canary (push) Successful in 38s
Auto-version and Release / release (push) Successful in 44s
CI Pipeline / lint (push) Successful in 45s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / type-check (push) Failing after 30s
CI Pipeline / unit-tests (push) Successful in 1m14s
CI Pipeline / required-checks (push) Failing after 1s
deploy / deploy (push) Failing after 2m55s
§0 PATTERN FALSE-POSITIVE REPAIR (investigated, not assumed):
- Root cause layer 1: raw-substring signal matching — 'ui' matched
  'patterns_bu_i_l_t_in.py'. Boundary-aware matchers: short (<4 char)
  signals match path TOKENS / entity-namespace PREFIXES only; longer
  signals may contain.
- Root cause layer 2: ui-component-identity-stable was keyed on
  path_contains ['tui','ui','widget'] — re-keyed to entities_match
  ui:/region:/layout: with namespace-prefix semantics (structural kinds
  carry meaning in their prefix).
- Verified all four directions: T6 false positive gone; tui_backlog path
  no longer activates identity obligation; genuine UI work still derives;
  genuine deploy hits survive. Recorded as
  process-defect:raw-substring-signal-match + TUI registry entry for
  tui_backlog.py (ledger, honestly marked not-a-surface).

- construction_interface.py: ONE builder-facing boundary (Rune-internal +
  external-project modes). submit_need → inspect_decision → get_scope →
  get_obligations → report_observation → verify → transition → close →
  resume + amend_scope. Structured JSON envelope (schema v1: success/ids/
  state/errors/warnings); explicit error states (UNKNOWN_CONTRACT,
  SCOPE_VIOLATION, APPROVAL_REQUIRED, VERIFICATION_FAILED,
  STORE_WRITE_FAILED...); builder identity recorded for audit, semantics
  identical for any builder (§17); NO structural-authority writes exposed;
  scope amendment is user-gated; closure cannot be self-certified; CLI
  transport with --builder/--project-store flags.
- tui_backlog.py: durable TUI Upgrade Backlog (§22-25) — projection
  classification (NONE/INSPECTION/CONTROL/BOTH/UNKNOWN), auto-capture seam
  record_tui_projection_decision for contract closures, backlog NEVER
  structural authority (frontier propagation empty, test-proven).

REAL PROOF §33: builder session via interface-only subprocess (no internal
imports): submit→decision→scope→obligations→transition→observe→verify→
close→FRESH-PROCESS RESUME recovering need/decision/scope/obligations/
next-action from contract id alone. Scope green, quality green.

Tests: 33 new (interface §29/§30, pattern precision §31, backlog §32);
suite 1589 green; henxels rc=0; pyright clean.
2026-09-13 17:23:13 +01:00
Nick 1bb4ea86b8 chore(scaffolding): SCAFFOLD-TRANCHE-006 scope amendment + regeneration-sequencing honesty note (RED retained)
Auto-version and Release / release (push) Successful in 12s
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / canary (push) Successful in 27s
CI Pipeline / documentation (push) Successful in 10s
CI Pipeline / unit-tests (push) Successful in 59s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 24s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 39s
2026-09-13 15:04:11 +01:00
Nick f98416e94a chore(scaffolding): SCAFFOLD-TRANCHE-006 trace regenerated through pattern-aware path + closed via save_verified (dogfooded Guard A)
CI Pipeline / best-practices (push) Successful in 7s
Auto-version and Release / release (push) Successful in 17s
CI Pipeline / documentation (push) Successful in 6s
CI Pipeline / canary (push) Successful in 33s
CI Pipeline / lint (push) Successful in 39s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 1m8s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 1m55s
2026-09-13 15:03:47 +01:00
20 changed files with 13876 additions and 171 deletions

No files matched your search

+1 -1
View File
@@ -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.**
+4 -1
View File
@@ -1,7 +1,7 @@
{
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
"_generated_by_spine": true,
"generated_at": "2026-09-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": [
+47
View File
@@ -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).
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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:
+2
View File
@@ -872,6 +872,8 @@ def generate_contract(
"completeness",
"affected_surfaces",
"iterations",
"patterns_considered",
"pattern_obligations",
)
},
# distinct impact surfaces (Tranche 5 §13) — file scope != structural
+767
View File
@@ -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())
+42 -11
View File
@@ -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"]))
+1 -1
View File
@@ -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=[
+336
View File
@@ -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))
+2 -2
View File
@@ -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
+125
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+386 -124
View File
File diff suppressed because it is too large. Load diff
+551
View File
@@ -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"]
+308
View File
@@ -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
+118
View File
@@ -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")