Compare commits

...
22 Commits
Author SHA1 Message Date
conductor c9260ace18 chore(release): auto-bump version to 0.66.46 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 02:07:28 +01:00
Nick 2a7e9dcfcf feat(release): automatic release-history wiring — init on version/tag, convergence refresh on same-version deploy (SCAFFOLD-TRANCHE-009B)
§1 HELD-OUT CONVERGENCE DEFECT confirmed live: v0.66.45 release recorded
deployed ce328a80 while the deployment authority had converged to
e3263ef9/0.66.45 — repaired via the new refresh path; the release now
states BOTH honestly (tag commit ce328a80 · final deployed e3263ef9).

§2 audit answers: NO workflow invoked release_notes.py; release creation
used PR title/body paste (empty '##' bodies on direct pushes); nothing
refreshed after same-version deploys. T9 proved the mechanism, not
automatic adoption — recorded, history not rewritten.

§4 WIRING (one note-generation authority, no YAML logic):
- auto-version.yml: old PR-paste release step REMOVED (§6); replaced by
  release_history_cli.py init — evidence-derived notes from the governed
  scaffold store, fail-closed on publication failure (§5/§16: Rune cannot
  silently create another empty historical release)
- deploy.yml: convergence refresh after successful promotion — same
  release PATCHed with the converged deployed identity + newly-governed
  facts; visible non-blocking failure (documentation failure never rolls
  back valid software; retry converges)
- release_notes.py: auto_initial_note (no hand-written prose, §7) +
  convergence_refresh (evidence accumulates, tag SHA never changes,
  idempotent) + build_from_deployment
- §11 ledger-only: maintenance:builder-interaction-ledger created;
  §16: release-history backlog entry amended (health indicator), no
  duplicates

Tests: 10 new (§13 A-L incl. 0.66.45 convergence fixture, idempotence,
failure visibility, retry recovery, no-duplicate guarantee, unsupported-
claim exclusion through the auto path); suite 1620 green.
2026-09-14 02:07:06 +01:00
Nick e3263ef9b1 chore(scaffolding): T9 closure accounting + release-history TUI backlog entry 2026-09-14 01:29:02 +01:00
conductor ce328a8080 chore(release): auto-bump version to 0.66.45 — earned by # (minor: converged release (push-to-master bump))
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)
- 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 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) 2026-09-13 21:52:48 +01:00
Nick b102225d4e chore(scaffolding): SCAFFOLD-TRANCHE-008 — interface governance record, TUI projection NONE, builder interaction log 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))
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)
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 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))
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) 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) 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))
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)
§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) 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) 2026-09-13 15:03:47 +01:00
conductor b3465176f8 chore(release): auto-bump version to 0.66.41 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 14:48:45 +01:00
Nick aab5159ccc feat(scaffolding): Construction Pattern core — evidence-backed applicability, filtered obligation derivation, process guards (SCAFFOLD-TRANCHE-006)
Also §0 pre-flight: SCAFFOLD-TRANCHE-005 found approved/open (parent-
lifecycle neglect — child closed, parent forgotten); closed with real
existing evidence only, provenance recorded, process-defect entity added.

- construction_pattern.py: open-world ConstructionPattern (signals,
  obligations with when-conditions, provenance, revision + history,
  maturity, ext); evaluate_applicability — deterministic signal groups,
  verdicts APPLIES/PARTIAL/DOES_NOT_APPLY/UNKNOWN, name similarity
  establishes nothing, project exclusions override, model proposals stay
  PENDING; derive_pattern_obligations — obligations FILTERED by project
  evidence (REQUIRED/APPLICABLE/NOT_APPLICABLE/UNKNOWN + EXCEPTED with
  provenance retained, §18); combine_pattern_obligations — dedupe with
  also_from, conflicts surfaced never silently resolved (§26-27);
  authority boundary: pattern = guidance, project evidence wins (§3)
- construction_patterns_builtin.py: 4 evidence-grounded patterns, all
  appropriately immature (§15):
  * capability-module-adaptation (UNIT-PROVEN; build-but-don't-break;
    provenance T1/T3/T4 contracts)
  * release-deployment-change (UNIT-PROVEN; 9 obligations incl.
    fail-closed qualification, exact identity, staging!=promotion,
    promotion truthfulness, monotonic ordering, negative+forward proofs,
    deployed-identity verification; provenance deploy-order-race
    incident + DEPLOY-MONOTONIC-001 + red-promotion incidents)
  * ui-tui-structural-change (UNIT-PROVEN; layout inheritance, USER-LOCKED
    preservation, UNKNOWN-never-PASS; provenance issue-105 + T3 real proof)
  * store-schema-evolution (CANDIDATE; backward readability, roundtrip,
    authority preservation, migration-only-if-needed; provenance T2/T4/T5)
- make_decision integration: APPLIES+PARTIAL patterns consulted (§7 —
  per-obligation when-conditions do the precision work); provenance
  PATTERN distinct from STRUCTURAL derivations (§8); pattern refs +
  revisions carried into generated contracts (§17/§19)
- construction_guards.py + Scaffold.save_verified/verify_on_disk (§21-22):
  Guard A durable-mutation readback — verify_on_disk is the pure no-write
  readback that catches the forgotten-save lie (T5 defect class A);
  Guard B source-edit postcondition — zero-match is a truthful no-op
  (EditNoOp), expected-count + postcondition enforced (T5 defect class B)
- REAL PROOFS: §23 read-only replay of DEPLOY-MONOTONIC-001 Need —
  release pattern APPLIES on evidence, 9 obligations derived WITHOUT the
  Need listing them (negative/forward/monotonic/deployed-identity all
  pattern-derived, provenance carried); resolution COMPOSE matches the
  original repair; §24 live change save_verified/verify_on_disk — the
  store pattern contributed store-roundtrip, an obligation the raw Need
  did not state; §25 held-out — webhook-config-only deploy change does
  NOT demand promotion rework but keeps release regression verification

Tests: 30 new (§28-§32 plans + held-out + live); suite 1556 green;
henxels rc=0; pyright clean.
2026-09-13 14:48:28 +01:00
Nick 80f10b9be7 chore(scaffolding): §0 pre-flight — SCAFFOLD-TRANCHE-005 closed with real evidence; parent-lifecycle-neglect process defect recorded 2026-09-13 14:24:15 +01:00
Nick 9d75e51b22 chore(spine): canonical rebuild — observation refresh 2026-09-13 14:02:23 +01:00
26 changed files with 22960 additions and 166 deletions

No files matched your search

+11 -32
View File
@@ -213,38 +213,17 @@ jobs:
env.write(f"RELEASE_VERSION={NEW}\n")
env.write("AUTO_BUMPED=true\n")
PYBUMP
- name: Create release from merged pull request
- name: Create evidence-derived release (SCAFFOLD-TRANCHE-009B)
env:
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
PR_TITLE: ${{ gitea.event.pull_request.title }}
PR_BODY: ${{ gitea.event.pull_request.body }}
RELEASE_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases
LIST_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases?limit=50
RELEASE_SERVER_URL: ${{ gitea.server_url }}
RELEASE_REPO: ${{ gitea.repository }}
run: |
# Skip idempotently if a release for this tag already exists (retry/replay safety).
if curl --fail --silent "$LIST_API_URL" \
-H "Authorization: token $GITEA_TOKEN" \
| python3 -c 'import json,sys,os; v=os.environ["RELEASE_VERSION"]; exists=[r for r in json.load(sys.stdin) if r.get("tag_name")=="v"+v]; sys.exit(0 if exists else 1)'; then
echo "Release v$RELEASE_VERSION already exists — skipping creation"
exit 0
fi
python3 - <<'PY' > /tmp/rune-release.json
import json, os
version = os.environ["RELEASE_VERSION"]
body = f"## {os.environ['PR_TITLE']}\n\n{os.environ.get('PR_BODY', '')}"
print(json.dumps({
"tag_name": f"v{version}",
"target_commitish": "master",
"name": f"Rune v{version}",
"body": body,
"draft": False,
"prerelease": False,
}))
PY
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @/tmp/rune-release.json \
"$RELEASE_API_URL"
rm -f /tmp/rune-release.json
# ONE note-generation authority: the tested release_notes module.
# The old PR-title/body paste path (which produced empty '##'
# bodies on direct-push releases) is REMOVED — governed evidence
# from the scaffold store is the source, not PR prose.
python3 scripts/release_history_cli.py init \
--version "$RELEASE_VERSION" \
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
--store store/scaffold/current.json
+23
View File
@@ -54,6 +54,7 @@ jobs:
CURRENT_TARGET=$(readlink /mnt/workdrive/rune-code/current || true)
if [ "$CURRENT_TARGET" = "/mnt/workdrive/rune-code/${SHA:0:12}" ]; then
echo "PROMOTED: current == candidate ($SHA)"
echo "promoted=true" >> "$GITEA_OUTPUT"
else
# Not promoted. deploy_stage exit 0 + non-matching current =
# NOOP_ALREADY_CURRENT or STALE_SUPERSEDED — truthful non-error
@@ -61,6 +62,7 @@ jobs:
# promotion; the runtime-verification step below only runs for
# real promotions.
echo "NOT PROMOTED: current -> $CURRENT_TARGET (superseded/noop — current is authoritative)"
echo "promoted=false" >> "$GITEA_OUTPUT"
fi
- name: Post-deploy runtime verification (only for real promotions)
@@ -83,3 +85,24 @@ jobs:
assert rune.__version__ == info['version']
print('RUNTIME VERIFIED:', info)
"
- name: Release-history convergence refresh (SCAFFOLD-TRANCHE-009B)
if: ${{ success() }}
env:
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
RELEASE_SERVER_URL: ${{ gitea.server_url }}
RELEASE_REPO: ${{ gitea.repository }}
run: |
# §3/§10: a later SHA of the SAME version successfully became
# deployment:current → refresh the SAME Gitea release with the
# converged identity + newly-governed facts. Idempotent. Failure
# is VISIBLE (non-zero) but does NOT roll back valid software —
# documentation failure never outweighs deployment validity (§5);
# retry converges.
VERSION=$(python3 -c "import sys; sys.path.insert(0,'src'); import rune; print(rune.__version__)")
python3 scripts/release_history_cli.py refresh \
--version "$VERSION" \
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
--store "$GITHUB_WORKSPACE/store/scaffold/current.json" \
--release-sha "$(git rev-parse HEAD)" \
--deployed-sha "$(git rev-parse HEAD)" \
|| echo "::warning::release-history refresh FAILED — release may be stale; retry converges (deployment unaffected)"
+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.66.40**
**Current release: v0.66.46**
**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.**
+6 -1
View File
@@ -1,7 +1,7 @@
{
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
"_generated_by_spine": true,
"generated_at": "2026-09-13T03:43:44Z",
"generated_at": "2026-09-14T00:06:17Z",
"umbrellas": {
"capability": {
"description": "Tool selection and execution authority",
@@ -219,14 +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.40"
version = "0.66.46"
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)
+123
View File
@@ -0,0 +1,123 @@
"""construction_guards.py — deterministic guards for observed process
defects (SCAFFOLD-TRANCHE-006 §21-22).
Two real defect classes from recent sessions:
A. DURABLE-STORE MUTATION WITHOUT PERSISTENCE (Tranche 5: closure script
mutated the contract in memory, never called save(); the next read saw
stale state). Guard: a durable-boundary operation verifies by
RELOAD/READBACK, not by successful in-memory mutation.
B. BLIND SOURCE-EDIT NO-OP (multiple sessions: python str.replace() on
ruff-reformatted files silently matched nothing; the script reported
success while changing nothing). Guard: an automated edit verifies its
POSTCONDITION — expected text present / old text absent — and a
zero-match replacement is an explicit, truthful no-op, never success.
Not a generic editor framework. Two small, deterministic postcondition
checks at the boundaries where the defects actually occurred.
"""
from __future__ import annotations
import json
from pathlib import Path
__all__ = ["verify_persisted", "assert_persisted", "apply_source_edit", "EditNoOp", "EditPostconditionFailed"]
class EditNoOp(Exception):
"""The edit matched zero targets — an explicit, truthful no-op."""
class EditPostconditionFailed(Exception):
"""The edit claimed success but the expected postcondition is absent."""
# ── Guard A: durable mutation verified by readback ──────────────────────────
def verify_persisted(path: Path | str, selector, expect) -> bool:
"""Reload the durable store from disk and check the expectation against
the RELOADED state. `selector(reloaded_doc) -> value`; pass == value
equals `expect` (or expect(value) is truthy when callable).
The reloaded document is parsed as JSON when the file is JSON-shaped;
otherwise the raw text is passed to the selector (plain-text stores).
This is the Tranche 5 defect guard: an in-memory mutation that was
never save()d fails here, because the disk state is what gets read.
"""
raw = Path(path).read_text(encoding="utf-8")
try:
doc = json.loads(raw)
except ValueError:
doc = raw
value = selector(doc)
if callable(expect):
return bool(expect(value))
return value == expect
def assert_persisted(path: Path | str, selector, expect, *, what: str = "durable state") -> None:
"""verify_persisted that raises on mismatch — for use right after a
save() at durable boundaries."""
if not verify_persisted(path, selector, expect):
raise AssertionError(
f"persistence verification FAILED for {what}: reloaded state "
f"does not match expectation — the mutation may not have been "
f"saved (Tranche 5 defect class A)"
)
# ── Guard B: source edit with postcondition ─────────────────────────────────
def apply_source_edit(
text: str,
old: str,
new: str,
*,
expected_count: int | None = None,
postcondition: str | None = None,
allow_noop: bool = False,
) -> str:
"""Apply a source edit WITH postcondition verification (Guard B).
- zero matches: raises EditNoOp (truthful no-op) unless allow_noop=True
- expected_count given: match count must equal it
- postcondition given: the RESULT must contain it, else
EditPostconditionFailed
Returns the new text. Never claims success for a change that did not
happen."""
count = text.count(old)
if count == 0:
if allow_noop:
return text # truthful, explicit no-op — caller records it
raise EditNoOp(
f"edit matched 0 targets — nothing changed (Tranche 5 defect "
f"class B: str.replace silently no-ops on reformatted files)"
)
if expected_count is not None and count != expected_count:
raise EditPostconditionFailed(f"edit matched {count} targets, expected {expected_count}")
result = text.replace(old, new)
if postcondition and postcondition not in result:
raise EditPostconditionFailed(
"postcondition text absent from result — edit did not achieve its expected effect"
)
return result
def apply_source_edit_file(
path: Path | str, old: str, new: str, *, expected_count: int | None = None, postcondition: str | None = None
) -> int:
"""File-level convenience: read, apply_source_edit, write back, then
VERIFY BY READBACK (both guards composed). Returns match count."""
p = Path(path)
text = p.read_text(encoding="utf-8")
result = apply_source_edit(text, old, new, expected_count=expected_count, postcondition=postcondition)
p.write_text(result, encoding="utf-8")
# Guard A composed: the written file must contain the postcondition
if postcondition:
assert_persisted(p, lambda t: t, lambda t: postcondition in t, what=f"edit of {p.name}")
return text.count(old)
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""release_history_cli.py — thin workflow entry points for release history.
SCAFFOLD-TRANCHE-009B §4: the workflows invoke THIS tested module; no
note-building logic lives in YAML. Two moments:
init RELEASE INITIALIZATION — auto-version creates the evidence-
derived Gitea release when the version/tag appears.
refresh RELEASE CONVERGENCE REFRESH — deploy promotes a later SHA of
the same version; the SAME release is updated with the final
deployed identity + newly-governed facts. Idempotent.
Fail-closed boundary (§5/§16): `init` exits non-zero on publication
failure — Rune cannot silently create another empty historical release.
`refresh` ALSO exits non-zero (visible failure + retry converges), but the
workflow marks it non-blocking for deployment validity: valid software is
never rolled back over documentation. Failure state is visible either way.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
from rune import release_notes as rn # noqa: E402
def _token() -> str:
tok = os.environ.get("GITEA_TOKEN", "")
if tok:
return tok
# local/dev fallback: git credential store (runner uses GITEA_TOKEN)
cred = Path.home() / ".git-credentials"
if cred.exists():
import re
m = re.search(r"https://[^:]+:([^@]+)@", cred.read_text())
if m:
return m.group(1)
return ""
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__)
sub = ap.add_subparsers(dest="cmd", required=True)
p_init = sub.add_parser("init", help="release initialization")
p_init.add_argument("--version", required=True)
p_init.add_argument("--server", required=True)
p_init.add_argument("--repo", required=True)
p_init.add_argument("--store", default="store/scaffold/current.json")
p_init.add_argument("--release-sha", default="")
p_init.add_argument("--deployed-sha", default="")
p_ref = sub.add_parser("refresh", help="convergence refresh")
p_ref.add_argument("--version", required=True)
p_ref.add_argument("--server", required=True)
p_ref.add_argument("--repo", required=True)
p_ref.add_argument("--store", default="store/scaffold/current.json")
p_ref.add_argument("--release-sha", default="")
p_ref.add_argument("--deployed-sha", required=True,
help="converged deployed commit (from the deployment "
"authority — required, never assumed)")
args = ap.parse_args(argv)
token = _token()
if not token:
print(json.dumps({"failed": True,
"error": "no Gitea token available"}))
return 1
if args.cmd == "init":
note = rn.auto_initial_note(
version=args.version,
summary=f"Rune v{args.version} — evidence-derived release "
f"history (automatic initialization)",
scaffold_store=args.store,
release_sha=args.release_sha,
deployed_sha=args.deployed_sha or args.release_sha,
)
result = rn.publish_release(args.server, args.repo, token, note)
print(json.dumps(result))
# fail-closed: initialization failure must be visible so Rune cannot
# silently create another empty historical release (§5/§16)
return 0 if not result.get("failed") else 1
if args.cmd == "refresh":
existing = rn.fetch_release(args.server, args.repo, token,
f"v{args.version}")
if existing is None:
# no release yet for this version — init instead (convergence
# may legitimately arrive before/without an init if the
# auto-version run failed); idempotent recovery path
note = rn.build_from_deployment(
version=args.version, deployed_sha=args.deployed_sha,
scaffold_store=args.store, release_sha=args.release_sha)
else:
note = rn.convergence_refresh(
rn.ReleaseNote.from_dict({
"version": args.version,
"summary": _summary_from_body(existing.get("body", "")),
"provenance": [],
}),
deployed_sha=args.deployed_sha, scaffold_store=args.store,
extra_provenance="automatic convergence refresh (deploy "
"workflow)")
# preserve structural fields the stored body carries
note.release_sha = _sha_from_body(existing.get("body", ""),
"tag") or args.release_sha
result = rn.publish_release(args.server, args.repo, token, note)
print(json.dumps(result))
# visible failure; retry converges (§10). Deployment validity is
# NOT rolled back over documentation — the workflow marks this
# step appropriately.
return 0 if not result.get("failed") else 1
return 2
def _summary_from_body(body: str) -> str:
"""Extract the summary paragraph (between the H1 and the first other
heading/italic line) from a stored release body."""
lines = body.splitlines()
grab = []
for ln in lines[1:]:
s = ln.strip()
if not s:
if grab:
break
continue
if s.startswith(("#", "_", "##", "-")):
break
grab.append(s)
return " ".join(grab) or f"Rune — evidence-derived release history"
def _sha_from_body(body: str, which: str) -> str:
import re
pat = ("tag commit: `([0-9a-f]+)`" if which == "tag"
else r"\*\*final deployed: `([0-9a-f]+)`\*\*")
m = re.search(pat, body, re.I)
return m.group(1) if m else ""
if __name__ == "__main__":
sys.exit(main())
+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.40"
__version__ = "0.66.46"
# 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.40"' in rune_version,
'__version__ = "0.66.46"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+46
View File
@@ -512,6 +512,7 @@ def make_decision(
toolchain_constraints: tc.ToolchainConstraints | None = None,
project_toolchain: dict | None = None,
prior_iterations: list[dict] | None = None,
extra_patterns: list | None = None,
) -> dict:
"""Run NEED → decision trace. `authorities` maps authority name → store
doc (e.g. {"rune_spine": spine_doc, "project_scaffold": scaffold_doc}).
@@ -628,9 +629,52 @@ def make_decision(
# research routing (§6, honest seam)
trace["research"] = research_route(req, merged)
# PATTERN SELECTION (Tranche 6 §4-7): evidence-backed applicability over
# the builtin registry + caller-supplied patterns; obligations filtered
# per project evidence; conflicts surfaced, never silently resolved.
# Project evidence > pattern assumption (§3): a pattern that contradicts
# recorded project exclusions becomes DOES_NOT_APPLY.
from rune.construction_pattern import ( # noqa: E402 — local: pattern layer
combine_pattern_obligations, derive_pattern_obligations,
evaluate_applicability)
from rune.construction_patterns_builtin import BUILTIN_PATTERNS # noqa: E402
pattern_candidates = list(BUILTIN_PATTERNS) + list(
extra_patterns or [])
patterns_considered = []
derivations = []
for pattern in pattern_candidates:
app = evaluate_applicability(pattern, need, surface_result, authorities)
patterns_considered.append(app)
# APPLIES and PARTIAL both reach obligation derivation: applicability
# gates whether the pattern is CONSULTED; the per-obligation `when`
# conditions do the precision filtering (Tranche 6 §7). The
# applicability verdict is recorded on every derived obligation so
# weaker evidence stays visible.
if app["verdict"] in ("APPLIES", "PARTIAL"):
d = derive_pattern_obligations(
pattern, need, surface_result, authorities,
project_exceptions=need.constraints.get(
"pattern_exceptions", []))
for entry in d["accepted"] + d["rejected"] + d["unresolved"]:
entry["pattern_applicability"] = app["verdict"]
derivations.append(d)
combined = combine_pattern_obligations(derivations) if derivations else {
"obligations": [], "conflicts": []}
trace["patterns_considered"] = patterns_considered
trace["pattern_obligations"] = combined
# approval gate (§13)
trace["approval"] = needs_user_approval(merged["resolution"], tdec, lay, need)
trace["obligations"] = obligations_from_trace(trace, req)
# pattern-derived obligations join the contract's verification set (§19):
# provenance stays PATTERN (distinct from STRUCTURAL derivations in
# obligations_from_trace — §8 provenance separation)
trace["obligations"]["pattern"] = combined["obligations"]
trace["obligations"]["pattern_conflicts"] = combined["conflicts"]
for po in combined["obligations"]:
trace["obligations"]["verification"].append(
f"[pattern:{po['pattern']}@r{po['revision']}] {po['obligation']}")
return trace
@@ -828,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())
+496
View File
@@ -0,0 +1,496 @@
"""construction_pattern.py — reusable evidence about HOW a class of thing
is normally built.
SCAFFOLD-TRANCHE-006. Canonical distinction (tranche brief):
Construction Pattern = reusable knowledge for a CLASS of construction
Project Scaffold = structural truth for THIS project
Construction Contract = obligations for THIS change
A pattern is GUIDANCE, not authority (§3): project evidence always wins.
A pattern must never overwrite contradictory project evidence — if the
pattern assumes HTTP services and the project evidence says message-queue-
only, the pattern adapts or becomes non-applicable.
Applicability (§4-5) is EVIDENCE-BACKED and deterministic: signals are
checked against the affected surface + structural authorities. Verdicts:
APPLIES / PARTIAL / DOES_NOT_APPLY / UNKNOWN. UNKNOWN never becomes APPLIES
because a name sounds similar; name similarity establishes nothing. Model
reasoning may PROPOSE a pattern (recorded, PENDING) — it may not certify
applicability.
Obligation derivation (§6-7) is FILTERED by this project: every pattern
obligation carries a `when` condition evaluated against project evidence;
verdicts REQUIRED / APPLICABLE / NOT_APPLICABLE / UNKNOWN with the reason.
Patterns REDUCE false-positive obligation expansion, never amplify it.
Provenance (§16): every rule answers "why is this here" with real Rune
evidence — incidents, contracts, tests, user requirements. Patterns
accumulate verified engineering knowledge, not model folklore.
Revision (§17): lightweight revision identity; contracts record the
revision consumed so "which builds used the old rule" stays answerable.
"""
from __future__ import annotations
from rune import structural_store as ss
__all__ = [
"ConstructionPattern",
"evaluate_applicability",
"derive_pattern_obligations",
"record_pattern_proposal",
"APPLIES",
"PARTIAL",
"DOES_NOT_APPLY",
"UNKNOWN",
"OBL_REQUIRED",
"OBL_APPLICABLE",
"OBL_NOT_APPLICABLE",
"OBL_UNKNOWN",
]
APPLIES = "APPLIES"
PARTIAL = "PARTIAL"
DOES_NOT_APPLY = "DOES_NOT_APPLY"
UNKNOWN = "UNKNOWN"
OBL_REQUIRED = "REQUIRED"
OBL_APPLICABLE = "APPLICABLE"
OBL_NOT_APPLICABLE = "NOT_APPLICABLE"
OBL_UNKNOWN = "UNKNOWN"
# ── pattern object (open-world) ─────────────────────────────────────────────
class ConstructionPattern:
"""One reusable construction pattern. Open-world: `ext` carries anything
the schema above does not name. NOT structural truth — guidance."""
def __init__(
self,
pattern_id: str,
purpose: str,
*,
revision: int = 1,
maturity: str = "CANDIDATE",
signals: dict | None = None,
obligations: list[dict] | None = None,
provenance: list[str] | None = None,
exclusions: list[str] | None = None,
toolchain_notes: list[str] | None = None,
runtime_effects: list[str] | None = None,
**ext,
):
if not pattern_id:
raise ValueError("pattern_id required")
if not purpose:
raise ValueError("purpose required")
for o in obligations or []:
for key in ("id", "obligation", "when"):
if key not in o:
raise ValueError(f"obligation missing {key!r}: {o}")
self.pattern_id = pattern_id
self.purpose = purpose
self.revision = revision
self.maturity = maturity
self.signals = dict(signals or {})
self.obligations = list(obligations or [])
self.provenance = sorted(provenance or [])
self.exclusions = sorted(exclusions or [])
self.toolchain_notes = sorted(toolchain_notes or [])
self.runtime_effects = sorted(runtime_effects or [])
self.ext = dict(ext)
def to_dict(self) -> dict:
return {
"pattern_id": self.pattern_id,
"purpose": self.purpose,
"revision": self.revision,
"maturity": self.maturity,
"signals": dict(self.signals),
"obligations": [dict(o) for o in self.obligations],
"provenance": list(self.provenance),
"exclusions": list(self.exclusions),
"toolchain_notes": list(self.toolchain_notes),
"runtime_effects": list(self.runtime_effects),
**self.ext,
}
@classmethod
def from_dict(cls, d: dict) -> "ConstructionPattern":
src = dict(d) # never mutate the caller's dict
known = {
k: src.pop(k)
for k in (
"pattern_id",
"purpose",
"revision",
"maturity",
"signals",
"obligations",
"provenance",
"exclusions",
"toolchain_notes",
"runtime_effects",
)
if k in src
}
return cls(**known, **src)
def revise(self, *, reason: str, evidence: list[str], changes: dict | None = None) -> "ConstructionPattern":
"""Revision with provenance (§17): prior meaning not invisibly
mutated — returns a NEW pattern at revision+1 recording the change."""
d = self.to_dict()
d.pop("pattern_id")
rev = {**d, **(changes or {})}
rev["revision"] = self.revision + 1
rev.setdefault("revision_history", []).append(
{
"prior_revision": self.revision,
"reason": reason[:200],
"evidence": sorted(evidence),
"changed": sorted((changes or {}).keys()),
}
)
return ConstructionPattern(self.pattern_id, **rev)
# ── model proposal seam (§5 — recorded, never authoritative) ────────────────
def record_pattern_proposal(
pattern_id: str, *, provider: str, model: str, evidence: list[str], confidence: float
) -> dict:
"""A model may PROPOSE a pattern; it may not certify applicability.
Recorded with verification state PENDING until deterministic project
evidence, research evidence, proven construction evidence, or an
explicit user decision accepts it."""
if not provider or not model:
raise ValueError("pattern proposal requires provider+model")
if not 0.0 <= confidence <= 1.0:
raise ValueError("confidence must be in [0,1]")
return {
"kind": "pattern_proposal",
"pattern_id": pattern_id,
"provider": provider,
"model": model,
"evidence": sorted(evidence),
"confidence": confidence,
"verification_state": "PENDING",
}
# ── applicability (§4 — deterministic signals only) ─────────────────────────
def _surface_text(need, surface_result: dict) -> set[str]:
"""Everything the affected surface offers as evidence: paths, entities,
ui surfaces, regions, workflow configs, the objective text tokens."""
tokens = set()
for k in (
"affected_paths",
"affected_existing_entities",
"affected_ui_surfaces",
"affected_regions",
"workflow_config_surfaces",
):
tokens.update(surface_result.get(k, []))
tokens.update(need.affected_surface)
# objective word tokens (deterministic split — not semantics)
tokens.update(w.lower() for w in need.objective.replace("/", " ").replace("-", " ").split())
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:
"""Deterministic applicability verdict from the pattern's declared
signals (§4). Signals (all optional; evidence-checked against the
affected surface and structural authorities):
surface_kinds subset of {source_path, workflow_config,
ui_component, layout_region,
structural_entity}
path_contains any-of substrings expected in affected paths
entities_match any-of entity-id prefixes/substrings
objective_any any-of lowercase words expected in the objective
contracts_match any-of contract ids implemented by affected entities
Verdict: count how many DECLARED signal groups produce evidence.
all declared groups hit -> APPLIES
some hit -> PARTIAL (or DOES_NOT_APPLY when the
pattern's `strong` groups all miss)
none / no signals -> UNKNOWN (never APPLIES by name)
Project evidence overrides (§3): exclusions evaluated first — if the
need explicitly declares the pattern non-applicable, DOES_NOT_APPLY
with the exclusion recorded.
"""
# §3/§18: PROJECT evidence overrides the pattern. Exclusions may be
# declared by the project/need (pattern_exclusions constraint) or by the
# pattern itself (non-applicability domains).
declared_exclusions = list(pattern.exclusions) + list(need.constraints.get("pattern_exclusions", []))
for ex in declared_exclusions:
if ex.lower() in need.objective.lower() or ex in need.constraints.get("pattern_exclusions", []):
return {
"pattern": pattern.pattern_id,
"verdict": DOES_NOT_APPLY,
"reason": f"declared exclusion matched: {ex}",
"evidence": {"exclusion": ex, "source": "project/need declaration"},
}
signals = pattern.signals
if not signals:
return {
"pattern": pattern.pattern_id,
"verdict": UNKNOWN,
"reason": "pattern declares no applicability signals — nothing deterministic to check",
"evidence": {},
}
tokens = _surface_text(need, surface_result)
groups_hit, groups_declared = [], 0
evidence: dict = {}
def _check(group: str, candidates: list[str], present) -> None:
nonlocal groups_declared
if not candidates:
return
groups_declared += 1
hits = [c for c in candidates if present(c)]
if hits:
groups_hit.append(group)
evidence[group] = hits[:5]
_check(
"surface_kinds",
signals.get("surface_kinds", []),
lambda c: (
bool(
surface_result.get(
{
"source_path": "affected_paths",
"workflow_config": "workflow_config_surfaces",
"ui_component": "affected_ui_surfaces",
"layout_region": "affected_regions",
}.get(c, ""),
[],
)
)
or any(r["kind"] == c for r in surface_result.get("resolutions", []))
),
)
_check(
"path_contains",
[p for p in signals.get("path_contains", [])],
lambda c: any(
_path_matches(c, p)
for p in surface_result.get("affected_paths", []) + surface_result.get("workflow_config_surfaces", [])
),
)
_check(
"entities_match",
[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",
[w.lower() for w in signals.get("objective_any", [])],
lambda c: c in {w.lower() for w in need.objective.replace("/", " ").split()},
)
contracts_match = signals.get("contracts_match", [])
if contracts_match:
groups_declared += 1
hits = []
for name, doc in authorities.items():
for e in surface_result.get("affected_existing_entities", []):
for r in ss.relationships(doc, from_id=e):
if r["type"] in {"implements", "exposes", "provides", "serves"} and r["to"] in contracts_match:
hits.append(f"{e}->{r['to']}")
if hits:
groups_hit.append("contracts_match")
evidence["contracts_match"] = hits[:5]
if groups_declared == 0:
return {
"pattern": pattern.pattern_id,
"verdict": UNKNOWN,
"reason": "no applicable signal groups declared",
"evidence": {},
}
if len(groups_hit) == groups_declared:
verdict = APPLIES
elif groups_hit:
verdict = PARTIAL
else:
verdict = DOES_NOT_APPLY
return {
"pattern": pattern.pattern_id,
"verdict": verdict,
"reason": f"{len(groups_hit)}/{groups_declared} signal groups "
f"produced evidence: {sorted(groups_hit) or 'none'}",
"evidence": evidence,
}
# ── obligation derivation (§6-7 — filtered by project evidence) ─────────────
def _obligation_when_hit(obligation: dict, need, surface_result: dict, authorities: dict[str, dict]) -> tuple[str, str]:
"""Evaluate one obligation's `when` condition against project evidence.
Returns (verdict, reason). `when` shapes (all deterministic):
path_contains: [substrings] — any affected/config path matches
entities_match: [substrings] — any affected entity matches
always: true — applies whenever the pattern applies
contracts_touched: [ids] — contract in requirement or implemented
by affected entities
An obligation with no recognized `when` is UNKNOWN (never silently
required)."""
when = obligation.get("when")
paths = [p for p in surface_result.get("affected_paths", [])] + [
p for p in surface_result.get("workflow_config_surfaces", [])
]
entities = (
[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 _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 _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"]))
if "contracts_touched" in when:
touched = set(need.constraints.get("contracts", []))
for name, doc in authorities.items():
for e in entities:
for r in ss.relationships(doc, from_id=e):
if r["type"] in {"implements", "exposes", "provides", "serves"}:
touched.add(r["to"])
hits = [c for c in when["contracts_touched"] if c in touched]
if hits:
return OBL_APPLICABLE, f"contract(s) touched: {hits}"
return OBL_NOT_APPLICABLE, ("touched contracts do not include " + str(when["contracts_touched"]))
return OBL_UNKNOWN, f"unrecognized when-condition shape: {sorted(when)}"
def derive_pattern_obligations(
pattern: ConstructionPattern,
need,
surface_result: dict,
authorities: dict[str, dict],
project_exceptions: list[dict] | None = None,
) -> dict:
"""Derive this pattern's obligations FILTERED by project evidence (§7).
Every returned obligation carries: pattern, revision, obligation text,
verdict, reason/evidence, and provenance kind PATTERN (distinct from
Tranche 5's STRUCTURAL derivation — §8 provenance separation).
Project exceptions (§18) suppress with provenance retained."""
project_exceptions = list(project_exceptions or [])
out = {
"pattern": pattern.pattern_id,
"revision": pattern.revision,
"accepted": [],
"rejected": [],
"unresolved": [],
}
for o in pattern.obligations:
entry = {
"id": o["id"],
"obligation": o["obligation"],
"pattern": pattern.pattern_id,
"revision": pattern.revision,
"provenance": "PATTERN",
"pattern_provenance": list(pattern.provenance),
}
# project exception check first (§18) — suppress with provenance
exc = next((x for x in project_exceptions if x.get("obligation") == o["id"]), None)
if exc:
entry.update(
{
"verdict": "EXCEPTED",
"reason": exc.get("reason", ""),
"exception_evidence": exc.get("evidence", ""),
"exception_authority": exc.get("authority", "user"),
}
)
out["rejected"].append(entry)
continue
verdict, reason = _obligation_when_hit(o, need, surface_result, authorities)
entry["verdict"] = verdict
entry["reason"] = reason
if verdict in (OBL_REQUIRED, OBL_APPLICABLE):
out["accepted"].append(entry)
elif verdict == OBL_NOT_APPLICABLE:
out["rejected"].append(entry)
else:
out["unresolved"].append(entry)
return out
def combine_pattern_obligations(derivations: list[dict]) -> dict:
"""Combine multiple patterns' derivations (§26-27): duplicates of the
same obligation id collapse (provenance lists merge); conflicting
obligations (same id, contradictory verdicts across patterns) surface
as conflicts requiring evidence/user decision — never silently chosen."""
merged: dict[str, dict] = {}
conflicts: list[dict] = []
for d in derivations:
for entry in d["accepted"]:
oid = entry["id"]
if oid in merged:
prev = merged[oid]
if prev["obligation"] != entry["obligation"]:
conflicts.append(
{
"obligation_id": oid,
"conflict": "same id, different text",
"sources": [prev["pattern"], entry["pattern"]],
}
)
continue
prev.setdefault("also_from", []).append(entry["pattern"])
continue
merged[oid] = dict(entry)
return {"obligations": sorted(merged.values(), key=lambda x: x["id"]), "conflicts": conflicts}
+249
View File
@@ -0,0 +1,249 @@
"""construction_patterns_builtin.py — the initial evidence-grounded pattern
set (SCAFFOLD-TRANCHE-006 §10-14: 4 patterns, each derived from REAL Rune
evidence, each appropriately immature).
This module is DATA, not a framework: pattern provenance answers "why is
this rule here" with real incidents/contracts/tests. Maturity stays
CANDIDATE/UNIT-PROVEN — existence is not trust (§15).
"""
from __future__ import annotations
from rune.construction_pattern import ConstructionPattern
__all__ = ["BUILTIN_PATTERNS", "get_pattern", "PATTERN_REGISTRY_REVISION"]
# Bumped when any builtin pattern's obligations change meaningfully.
PATTERN_REGISTRY_REVISION = 1
CAPABILITY_ADAPTATION = ConstructionPattern(
"capability-module-adaptation",
"Adapting or extending an existing capability/module: prove the NEW "
"behavior, preserve the OLD behavior (BUILD BUT DON'T BREAK).",
maturity="UNIT-PROVEN",
signals={
"entities_match": ["module:", "capability:"],
"path_contains": ["src/"],
"objective_any": ["adapt", "extend", "modify", "preserve", "existing", "capability", "repair"],
},
obligations=[
{
"id": "adapt-new-behavior-proof",
"obligation": "new behavior proven by tests before closure",
"when": {"always": True},
},
{
"id": "adapt-old-behavior-preserved",
"obligation": "existing capability behavior preserved; original contracts remain valid",
"when": {"always": True},
},
{
"id": "adapt-consumers-verified",
"obligation": "existing consumers of the adapted capability verified against their contracts",
"when": {"entities_match": ["module:", "capability:"]},
},
{
"id": "adapt-migration-detected",
"obligation": "migration obligations detected and recorded if any consumer-visible interface changes",
"when": {"path_contains": ["src/"]},
},
],
provenance=[
"SCAFFOLD-TRANCHE-001 (build-but-don't-break established)",
"SCAFFOLD-TRANCHE-003 (adaptation two-proof rule)",
"construction_decision.obligations_from_trace (auto-derived since T4)",
"LIVE-CONTRACT-QUALITY-001 (adaptation resolved + closed live)",
],
)
RELEASE_DEPLOYMENT = ConstructionPattern(
"release-deployment-change",
"Changes to the release/deployment path: fail-closed qualification, "
"exact candidate identity, staging != promotion, truthful promotion "
"outcomes, deployed-identity verification, negative + forward proofs.",
maturity="UNIT-PROVEN",
signals={
"path_contains": [
"deploy",
"promotion",
"qualify",
"release.yml",
"ci.yml",
"deploy_qualify",
"deploy_stage",
"deploy_promotion",
],
"objective_any": ["deploy", "release", "promot", "qualif", "candidate", "current", "monotonic"],
"workflow_config_signals": True, # informational; surface kinds carry it
},
obligations=[
{
"id": "rel-fail-closed-qualification",
"obligation": "CI qualification remains fail-closed (exact SHA, "
"two-leg verdict); red candidates never deploy",
"when": {"path_contains": ["deploy", "release", "qualify", "ci.yml"]},
},
{
"id": "rel-exact-candidate-identity",
"obligation": "deployment addresses the EXACT pushed revision, never whatever HEAD is at deploy time",
"when": {"path_contains": ["deploy", "release"]},
},
{
"id": "rel-staging-promotion-distinction",
"obligation": "staged candidate != promoted candidate; promotion outcome must be independently verifiable",
"when": {"path_contains": ["deploy", "promotion", "stage"]},
},
{
"id": "rel-promotion-truthfulness",
"obligation": "promotion outcomes are truthful (PROMOTED / "
"NOOP_ALREADY_CURRENT / STALE_SUPERSEDED / REFUSED_*); "
"stale candidates never claim success",
"when": {"path_contains": ["promotion", "deploy_stage", "deploy_promotion"]},
},
{
"id": "rel-monotonic-promotion",
"obligation": "promotion ordering: an older candidate finishing "
"later cannot regress current; ancestry check + "
"fenced critical section",
"when": {"path_contains": ["deploy_promotion", "deploy_stage", "promotion"]},
},
{
"id": "rel-negative-candidate-proof",
"obligation": "negative proof: red/unqualified candidate is refused; stale candidate cannot promote",
"when": {"path_contains": ["deploy", "promotion", "qualify"]},
},
{
"id": "rel-forward-candidate-proof",
"obligation": "forward proof: a green newer candidate still promotes normally",
"when": {"path_contains": ["deploy", "promotion"]},
},
{
"id": "rel-deployed-identity-verification",
"obligation": "deployed identity verified from the deployment "
"authority (current symlink + build identity), not "
"from job artifacts alone",
"when": {"always": True},
},
{
"id": "rel-release-regression-verification",
"obligation": "existing release-safety guarantees re-proven (the "
"guarantees that already exist must not silently "
"weaken)",
"when": {"always": True},
},
],
provenance=[
"maintenance:deploy-order-race (live incident: deploys 7355/7359)",
"DEPLOY-MONOTONIC-001 (monotonic promotion + truthful outcomes, live-proven)",
"release-safety repair 2026-09-13 (red-promotion incidents 0.66.28/29: required-checks aggregator lied)",
"scripts/deploy_promotion.py (ancestry + fenced critical section)",
"tests/test_deploy_stage_race.py (22-test matrix incl. two-process live race)",
],
)
UI_TUI_STRUCTURAL = ConstructionPattern(
"ui-tui-structural-change",
"UI/TUI structural changes: component identity, Layout Contract "
"inheritance, USER-LOCKED preservation, deterministic verification.",
maturity="UNIT-PROVEN",
signals={
"surface_kinds": ["ui_component", "layout_region"],
"path_contains": ["tui", "ui", "widget"],
"entities_match": ["ui:", "region:", "layout:"],
},
obligations=[
{
"id": "ui-layout-contract-inherited",
"obligation": "affected UI components inherit their Layout Contract "
"obligations; layout_contract_check must PASS",
"when": {"entities_match": ["ui:", "region:", "layout:"]},
},
{
"id": "ui-user-locked-preserved",
"obligation": "USER-LOCKED placement is user authority: preserved "
"exactly; changes require explicit user authorization",
"when": {"entities_match": ["ui:", "region:"]},
},
{
"id": "ui-unknown-never-pass",
"obligation": "layout verification UNKNOWN (no adapter evidence) must never be reported as PASS",
"when": {"entities_match": ["ui:", "region:", "layout:"]},
},
{
"id": "ui-component-identity-stable",
"obligation": "ui:<component> identity survives implementation-path "
"changes; authority never keyed by filename alone",
"when": {"entities_match": ["ui:", "region:", "layout:"]},
},
],
provenance=[
"issue #105 + plans/2026-09-03-tui-footer.md (version display)",
"Tranche 3 real TUI proof (gate PASS -> violation FAIL -> restore)",
"tests/test_layout_contract.py + tests/test_layout_contract_real_proof.py",
"maintenance:version-display-user-lock (USER-LOCK upgrade pending "
"user confirmation — provenance rule honored, not fabricated)",
],
)
STORE_SCHEMA_EVOLUTION = ConstructionPattern(
"store-schema-evolution",
"Durable store/schema changes: backward readability, authority "
"preservation, deterministic roundtrip, migration only when necessary.",
maturity="CANDIDATE",
signals={
"path_contains": ["store", "schema", "structural_store", "scaffold", "spine"],
"entities_match": ["module:structural_store", "module:scaffold", "module:spine"],
"objective_any": ["store", "schema", "record", "persist", "field", "migration"],
},
obligations=[
{
"id": "store-backward-readability",
"obligation": "old records without new fields read as empty/unknown, never crash; legacy keys preserved",
"when": {"path_contains": ["store", "structural_store", "scaffold", "spine", "schema"]},
},
{
"id": "store-roundtrip",
"obligation": "deterministic save/load roundtrip incl. unknown entity/relation kinds and open-world fields",
"when": {"path_contains": ["store", "structural_store", "scaffold", "schema"]},
},
{
"id": "store-authority-preservation",
"obligation": "structural facts only change through their owning "
"projector/authority; canonical rebuild after "
"registration (no hand-edited structural facts)",
"when": {"path_contains": ["spine", "scaffold", "store/"]},
},
{
"id": "store-migration-only-if-needed",
"obligation": "no mandatory migration unless semantics truly change; "
"ext/open-world fields preferred over schema churn",
"when": {"path_contains": ["schema", "store", "structural_store"]},
},
],
provenance=[
"Tranche 2 structural-store extraction (behavior-neutrality proof)",
"Tranche 4 contract-quality repair (legacy keys preserved, old contracts readable — test_h)",
"Tranche 5 expected-runtime-effects extension (old contracts close without new fields)",
"Tranche 2 CI lesson: hand-edited spine store diverged from canonical rebuild (deploy-identity edges)",
],
)
BUILTIN_PATTERNS = [CAPABILITY_ADAPTATION, RELEASE_DEPLOYMENT, UI_TUI_STRUCTURAL, STORE_SCHEMA_EVOLUTION]
_REGISTRY: dict[str, ConstructionPattern] = {p.pattern_id: p for p in BUILTIN_PATTERNS}
def get_pattern(pattern_id: str, revision: int | None = None) -> ConstructionPattern | None:
"""Fetch a builtin pattern; a specific revision raises if the registry
no longer carries it (contracts reference the revision they consumed —
§17)."""
p = _REGISTRY.get(pattern_id)
if p is None:
return None
if revision is not None and p.revision != revision:
raise ValueError(
f"pattern {pattern_id} revision {revision} not in registry "
f"(current: {p.revision}) — the consuming contract records which "
f"revision it used"
)
return p
+408
View File
@@ -0,0 +1,408 @@
"""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))
# ── automatic lifecycle entry points (Tranche 9B §4/§7) ─────────────────────
def auto_initial_note(*, version: str, summary: str, scaffold_store: str,
release_sha: str, deployed_sha: str = "",
provenance: list[str] | None = None) -> ReleaseNote:
"""RELEASE INITIALIZATION: build evidence-derived notes with NO
hand-written extra prose (§7). Facts come deterministically from the
governed scaffold store — contracts closed, maintenance resolved/open,
quality records. Routine releases require nothing from the builder."""
try:
doc = json.loads(Path(scaffold_store).read_text(encoding="utf-8"))
except (OSError, ValueError):
doc = {"entities": [], "relationships": [], "observations": []}
facts = extract_evidence_facts(doc)
return ReleaseNote(
version=version, summary=summary,
improved=[], added=[], fixed=[],
proven=facts["proven"], limitations=facts["limitations"],
references=facts["references"],
release_sha=release_sha, deployed_sha=deployed_sha,
provenance=provenance or ["automatic release initialization "
"(auto-version workflow)"])
def convergence_refresh(existing_note: ReleaseNote, *,
deployed_sha: str,
scaffold_store: str,
extra_provenance: str = "") -> ReleaseNote:
"""RELEASE CONVERGENCE REFRESH: a later SHA of the SAME version became
deployment:current — refresh the SAME note with the new deployed
identity and any newly-governed facts (§3/§10). Tag/release SHA never
changes; evidence accumulates; idempotent by construction (same inputs
→ same note)."""
try:
doc = json.loads(Path(scaffold_store).read_text(encoding="utf-8"))
facts = extract_evidence_facts(doc)
except (OSError, ValueError):
facts = {"proven": [], "limitations": [], "references": []}
prov = list(existing_note.provenance)
if extra_provenance and extra_provenance not in prov:
prov.append(extra_provenance)
return ReleaseNote(
version=existing_note.version, summary=existing_note.summary,
improved=existing_note.improved, added=existing_note.added,
fixed=existing_note.fixed,
proven=sorted(set(existing_note.proven) | set(facts["proven"])),
limitations=sorted(set(existing_note.limitations)
| set(facts["limitations"])),
references=sorted(set(existing_note.references)
| set(facts["references"])),
release_sha=existing_note.release_sha, # tag SHA NEVER changes
deployed_sha=deployed_sha, # convergence truth
provenance=prov, backfill=existing_note.backfill)
def build_from_deployment(*, version: str, deployed_sha: str,
scaffold_store: str,
release_sha: str = "",
summary: str = "") -> ReleaseNote:
"""Deploy-side convergence entry: rebuild/refresh notes from the
CURRENT governed store + the deployment authority's identity. No
hand-written prose required."""
note = auto_initial_note(
version=version,
summary=summary or f"Rune v{version} — evidence-derived release "
f"history (converged deployment)",
scaffold_store=scaffold_store, release_sha=release_sha,
deployed_sha=deployed_sha,
provenance=["automatic convergence refresh (deploy workflow)"])
return note
+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.40
Version: v0.66.46
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.40 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.66.46 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+33
View File
@@ -120,6 +120,39 @@ class Scaffold:
def save(self, doc: dict) -> Path:
return save(doc, self.current)
def save_verified(self, doc: dict, selector, expect, *, what: str = "state") -> Path:
"""Save + RELOAD-READBACK confirmation (SCAFFOLD-TRANCHE-006 §22A).
Catches DISK-WRITE failures: the in-memory doc is saved, then the
store is reloaded from disk and checked. This confirms the write
landed — it cannot catch a mutation that was never saved, because
saving is what this method does. For the forgotten-save check use
verify_on_disk (no write)."""
p = self.save(doc)
reloaded = self.load()
value = selector(reloaded)
ok = expect(value) if callable(expect) else value == expect
if not ok:
raise AssertionError(
f"persistence verification FAILED for {what!r}: reloaded "
f"store does not reflect the saved doc (disk-write failure)")
return p
def verify_on_disk(self, selector, expect, *, what: str = "state") -> bool:
"""PURE READBACK guard (SCAFFOLD-TRANCHE-006 §22A, the actual
Tranche 5 defect guard): reload the store from disk WITHOUT saving
and check the expectation. An in-memory mutation that was never
persisted fails here — the disk state is what a future session
would see. This is the forgotten-save lie detector."""
reloaded = self.load()
value = selector(reloaded)
ok = expect(value) if callable(expect) else value == expect
if not ok:
raise AssertionError(
f"durable-state verification FAILED for {what!r}: the on-"
f"disk store does not reflect the claimed mutation — it was "
f"probably never saved (Tranche 5 defect class A)")
return True
# -- project structure (open-world kinds; no fixed vocabulary) ----------
def add_domain(self, doc: dict, name: str, description: str = "") -> dict:
+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}"],
)
File diff suppressed because it is too large. Load diff
+551 -122
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"]
+471
View File
@@ -0,0 +1,471 @@
"""test_construction_pattern.py — SCAFFOLD-TRANCHE-006 test plans §28-§32."""
import json
import os
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
from rune import construction_decision as cd # noqa: E402
from rune import construction_pattern as cp # noqa: E402
from rune import structural_store as ss # noqa: E402
from rune.construction_patterns_builtin import ( # noqa: E402
BUILTIN_PATTERNS,
RELEASE_DEPLOYMENT,
get_pattern,
)
import construction_guards as cg # noqa: E402
def _spine():
spine = ss.new_doc()
ss.add_entity(
spine,
id="module:deploy_stage",
kind="module",
identities={"path": "scripts/deploy_stage.py"},
maturity="WIRED",
evidence_refs=["f"],
)
ss.add_entity(spine, id="contract:monotonic_promotion", kind="contract", identities={}, evidence_refs=["f"])
ss.add_relationship(
spine,
type="implements",
from_id="module:deploy_stage",
to_id="contract:monotonic_promotion",
evidence_refs=["f"],
)
ss.add_entity(
spine,
id="module:deploy_qualify",
kind="module",
identities={"path": "scripts/deploy_qualify.py"},
maturity="WIRED",
evidence_refs=["f"],
)
ss.add_entity(spine, id="contract:fail_closed_deployment", kind="contract", identities={}, evidence_refs=["f"])
ss.add_relationship(
spine,
type="implements",
from_id="module:deploy_qualify",
to_id="contract:fail_closed_deployment",
evidence_refs=["f"],
)
return spine
def _need(**kw):
d = dict(
need_id="n6",
objective="repair the deploy promotion ordering",
origin="user",
constraints={"contracts": ["contract:monotonic_promotion"]},
affected_surface=["scripts/deploy_stage.py", "scripts/deploy_promotion.py"],
evidence_refs=["x"],
approval_context="covered",
)
d.update(kw)
return cd.Need(**d)
def _surface(need):
authorities = {"rune_spine": _spine()}
return cd.resolve_affected_surface(need, authorities), authorities
# ── §28 pattern object ──────────────────────────────────────────────────────
def test_28a_roundtrip():
d = RELEASE_DEPLOYMENT.to_dict()
p2 = cp.ConstructionPattern.from_dict(d)
assert p2.to_dict() == d
def test_28b_open_world_ext():
p = cp.ConstructionPattern("p", "purpose", wibble="future-data")
assert p.to_dict()["wibble"] == "future-data"
assert cp.ConstructionPattern.from_dict(p.to_dict()).ext["wibble"] == "future-data"
def test_28c_revision_preserved_and_advances():
p2 = RELEASE_DEPLOYMENT.revise(
reason="new incident evidence", evidence=["new-incident"], changes={"maturity": "EXERCISED"}
)
assert p2.revision == RELEASE_DEPLOYMENT.revision + 1
hist = p2.ext["revision_history"]
assert hist[-1]["prior_revision"] == RELEASE_DEPLOYMENT.revision
assert hist[-1]["evidence"] == ["new-incident"]
# revision history survives a roundtrip (§17: old-rule traceability)
assert cp.ConstructionPattern.from_dict(p2.to_dict()).ext["revision_history"] == hist
# original untouched (no invisible mutation)
assert RELEASE_DEPLOYMENT.maturity == "UNIT-PROVEN"
def test_28d_provenance_preserved():
d = RELEASE_DEPLOYMENT.to_dict()
# provenance entries are enriched "ref (why)" strings — check by ref
assert any(e.startswith("maintenance:deploy-order-race") for e in d["provenance"])
assert any(e.startswith("DEPLOY-MONOTONIC-001") for e in d["provenance"])
assert cp.ConstructionPattern.from_dict(d).provenance == d["provenance"]
def test_28e_maturity_preserved():
for p in BUILTIN_PATTERNS:
assert p.maturity in ("CANDIDATE", "UNIT-PROVEN", "EXERCISED", "REPEATEDLY-PROVEN", "DEGRADED")
# initial patterns stay appropriately immature (§15)
assert all(p.maturity in ("CANDIDATE", "UNIT-PROVEN") for p in BUILTIN_PATTERNS)
def test_28f_obligation_validation():
with pytest.raises(ValueError):
cp.ConstructionPattern("p", "purpose", obligations=[{"id": "x"}]) # missing text/when
def test_28g_registry_revision_guard():
assert get_pattern("release-deployment-change") is not None
with pytest.raises(ValueError):
get_pattern("release-deployment-change", revision=99)
# ── §29 applicability ───────────────────────────────────────────────────────
def test_29a_matching_evidence_applies():
need = _need()
surface, auth = _surface(need)
app = cp.evaluate_applicability(RELEASE_DEPLOYMENT, need, surface, auth)
assert app["verdict"] == cp.APPLIES
assert app["evidence"] # evidence-backed, not name-based
def test_29b_partial_evidence_partial():
need = _need(objective="release notes wording", affected_surface=["docs/RELEASE.md"])
surface, auth = _surface(need)
app = cp.evaluate_applicability(RELEASE_DEPLOYMENT, need, surface, auth)
assert app["verdict"] in (cp.PARTIAL, cp.DOES_NOT_APPLY)
def test_29c_contradictory_project_evidence_does_not_apply():
"""§3/§29C: project evidence overrides the pattern assumption."""
need = _need(
constraints={"contracts": [], "pattern_exclusions": ["no deployment system in this project"]},
objective="pure algorithm work, no deployment system here",
)
surface, auth = _surface(need)
app = cp.evaluate_applicability(RELEASE_DEPLOYMENT, need, surface, auth)
assert app["verdict"] == cp.DOES_NOT_APPLY
assert "declared exclusion" in app["reason"]
def test_29d_insufficient_evidence_unknown():
p = cp.ConstructionPattern("empty", "no signals declared")
need = _need()
surface, auth = _surface(need)
app = cp.evaluate_applicability(p, need, surface, auth)
assert app["verdict"] == cp.UNKNOWN
def test_29e_name_similarity_alone_never_applies():
"""A pattern whose NAME matches but whose signals find no evidence stays
UNKNOWN/DOES_NOT_APPLY — names establish nothing (§4)."""
p = cp.ConstructionPattern(
"deployment-thing",
"sounds relevant",
signals={"path_contains": ["/nonexistent/xyz/"], "objective_any": ["zzznever"]},
)
need = _need()
surface, auth = _surface(need)
app = cp.evaluate_applicability(p, need, surface, auth)
assert app["verdict"] in (cp.DOES_NOT_APPLY, cp.UNKNOWN)
def test_29f_model_proposal_pending():
prop = cp.record_pattern_proposal("some-pattern", provider="local", model="m", evidence=["hunch"], confidence=0.5)
assert prop["verification_state"] == "PENDING"
with pytest.raises(ValueError):
cp.record_pattern_proposal("x", provider="", model="m", evidence=[], confidence=0.5)
def test_29g_project_evidence_overrides_pattern():
"""The strongest form of §3: pattern signals say APPLIES, but the project
records an explicit exception for the derived obligation — the obligation
is suppressed WITH provenance, not deleted (§18)."""
need = _need()
surface, auth = _surface(need)
deriv = cp.derive_pattern_obligations(
RELEASE_DEPLOYMENT,
need,
surface,
auth,
project_exceptions=[
{
"obligation": "rel-monotonic-promotion",
"reason": "single-runner serialized deploys documented in project policy",
"evidence": "project policy doc",
"authority": "user",
}
],
)
suppressed = [e for e in deriv["rejected"] if e["id"] == "rel-monotonic-promotion"]
assert suppressed and suppressed[0]["verdict"] == "EXCEPTED"
assert suppressed[0]["exception_evidence"] == "project policy doc"
# the pattern's other obligations still derive
assert any(e["id"] == "rel-fail-closed-qualification" for e in deriv["accepted"])
# ── §30 obligation derivation ───────────────────────────────────────────────
def test_30a_applicable_pattern_derives():
need = _need()
surface, auth = _surface(need)
deriv = cp.derive_pattern_obligations(RELEASE_DEPLOYMENT, need, surface, auth)
assert deriv["accepted"]
e = deriv["accepted"][0]
assert e["provenance"] == "PATTERN"
assert e["pattern_provenance"] # evidence chain carried
def test_30b_non_applicable_pattern_derives_nothing():
need = _need(objective="write a poem", affected_surface=["docs/poem.md"])
surface, auth = _surface(need)
deriv = cp.derive_pattern_obligations(RELEASE_DEPLOYMENT, need, surface, auth)
# no obligations accepted as REQUIRED (pattern-level always-obligations
# only fire when the PATTERN applies — a DOES_NOT_APPLY pattern's
# derivation is not consulted at all by make_decision)
assert deriv["accepted"] == [] or all(e["verdict"] == "REQUIRED" for e in deriv["accepted"])
def test_30d_unknown_obligation_not_silently_required():
p = cp.ConstructionPattern(
"weird",
"purpose",
obligations=[{"id": "mystery", "obligation": "do the unknown thing", "when": {"unsupported_shape": True}}],
)
need = _need()
surface, auth = _surface(need)
deriv = cp.derive_pattern_obligations(p, need, surface, auth)
assert deriv["unresolved"] and deriv["unresolved"][0]["verdict"] == cp.OBL_UNKNOWN
assert deriv["accepted"] == []
def test_30e_provenance_distinct():
"""§8: structural derivations and pattern derivations carry different
provenance kinds in the trace."""
need = _need()
trace = cd.make_decision(
need, authorities={"rune_spine": _spine()}, project_toolchain={"existing_language": "python"}
)
obl = trace["obligations"]
assert "pattern" in obl and obl["pattern"]
assert all(o["provenance"] == "PATTERN" for o in obl["pattern"])
# structural obligations (regression etc.) remain their own kind
assert "regression" in obl
def test_30f_duplicate_obligations_collapse():
p1 = cp.ConstructionPattern(
"a", "pa", obligations=[{"id": "shared", "obligation": "regression proof", "when": {"always": True}}]
)
p2 = cp.ConstructionPattern(
"b", "pb", obligations=[{"id": "shared", "obligation": "regression proof", "when": {"always": True}}]
)
d1 = cp.derive_pattern_obligations(p1, _need(), *(_surface(_need())[::-1]))
d2 = cp.derive_pattern_obligations(p2, _need(), *(_surface(_need())[::-1]))
combined = cp.combine_pattern_obligations([d1, d2])
shared = [o for o in combined["obligations"] if o["id"] == "shared"]
assert len(shared) == 1
# first pattern's provenance kept, later duplicates recorded in also_from
assert shared[0]["pattern"] == "a"
assert "b" in shared[0]["also_from"]
def test_30g_conflicting_obligations_surface():
p1 = cp.ConstructionPattern(
"x", "px", obligations=[{"id": "shared", "obligation": "version A of the rule", "when": {"always": True}}]
)
p2 = cp.ConstructionPattern(
"y", "py", obligations=[{"id": "shared", "obligation": "version B of the rule", "when": {"always": True}}]
)
d1 = cp.derive_pattern_obligations(p1, _need(), *(_surface(_need())[::-1]))
d2 = cp.derive_pattern_obligations(p2, _need(), *(_surface(_need())[::-1]))
combined = cp.combine_pattern_obligations([d1, d2])
assert combined["conflicts"]
assert combined["conflicts"][0]["conflict"] == "same id, different text"
# ── §31 pattern-specific ────────────────────────────────────────────────────
def test_31_release_pattern_negative_and_forward():
need = _need()
surface, auth = _surface(need)
deriv = cp.derive_pattern_obligations(RELEASE_DEPLOYMENT, need, surface, auth)
ids = {e["id"] for e in deriv["accepted"]}
assert "rel-negative-candidate-proof" in ids
assert "rel-forward-candidate-proof" in ids
assert "rel-deployed-identity-verification" in ids
def test_31_ui_pattern_layout_inheritance():
scaffold = ss.new_doc()
from rune import layout_contract as lc
lc.define_region(scaffold, "footer-bar", provenance="scaffold")
lc.bind_component(scaffold, "version_display", "footer-bar", provenance="user", lock=True, order=0)
need = cd.Need(
"u",
"adjust the tui version widget",
affected_surface=["ui:version_display"],
constraints={},
evidence_refs=["x"],
approval_context="covered",
)
authorities = {"rune_spine": ss.new_doc(), "project_scaffold": scaffold}
surface = cd.resolve_affected_surface(need, authorities)
from rune.construction_patterns_builtin import UI_TUI_STRUCTURAL
app = cp.evaluate_applicability(UI_TUI_STRUCTURAL, need, surface, authorities)
# ui-surface + entity signals hit; path signal misses (no source paths
# in this fixture) — honest PARTIAL, obligations still derive
assert app["verdict"] in (cp.APPLIES, cp.PARTIAL)
deriv = cp.derive_pattern_obligations(UI_TUI_STRUCTURAL, need, surface, authorities)
ids = {e["id"] for e in deriv["accepted"]}
assert "ui-layout-contract-inherited" in ids
assert "ui-user-locked-preserved" in ids
# and the actual layout obligation confirms the LOCK
lo = [o for o in cd.layout_obligations_for(scaffold, ["ui:version_display"])]
assert lo and lo[0]["lock"] is True
def test_31_store_pattern_backward_readability():
from rune.construction_patterns_builtin import STORE_SCHEMA_EVOLUTION
need = cd.Need(
"s",
"add expected effects to the scaffold store",
affected_surface=["src/rune/scaffold.py"],
constraints={},
evidence_refs=["x"],
approval_context="c",
)
surface, auth = _surface(need)
app = cp.evaluate_applicability(STORE_SCHEMA_EVOLUTION, need, surface, auth)
assert app["verdict"] in (cp.APPLIES, cp.PARTIAL)
deriv = cp.derive_pattern_obligations(STORE_SCHEMA_EVOLUTION, need, surface, auth)
ids = {e["id"] for e in deriv["accepted"]}
assert "store-backward-readability" in ids
# ── §32 process guards ──────────────────────────────────────────────────────
def test_32a_durable_mutation_reload_disagrees(tmp_path):
p = tmp_path / "store.json"
p.write_text(json.dumps({"flag": False}))
doc = json.loads(p.read_text())
doc["flag"] = True # in-memory mutation...
# ...never saved: the guard must catch the lie
assert not cg.verify_persisted(p, lambda d: d["flag"], True)
with pytest.raises(AssertionError):
cg.assert_persisted(p, lambda d: d["flag"], True, what="flag")
def test_32b_durable_mutation_saved_reload_matches(tmp_path):
p = tmp_path / "store.json"
p.write_text(json.dumps({"flag": False}))
doc = json.loads(p.read_text())
doc["flag"] = True
p.write_text(json.dumps(doc)) # actually saved
assert cg.verify_persisted(p, lambda d: d["flag"], True)
cg.assert_persisted(p, lambda d: d["flag"], True, what="flag")
def test_32c_zero_match_edit_is_noop_not_success(tmp_path):
with pytest.raises(cg.EditNoOp):
cg.apply_source_edit("nothing matches here", "ABSENT TEXT", "new")
# intentional no-op is representable truthfully (§32E)
result = cg.apply_source_edit("nothing matches here", "ABSENT TEXT", "new", allow_noop=True)
assert result == "nothing matches here"
def test_32d_edit_postcondition_absent_fails():
with pytest.raises(cg.EditPostconditionFailed):
cg.apply_source_edit("alpha beta", "alpha", "gamma", postcondition="delta")
def test_32e_file_edit_with_readback(tmp_path):
p = tmp_path / "mod.py"
p.write_text("def old_fn():\n pass\n")
n = cg.apply_source_edit_file(p, "old_fn", "new_fn", expected_count=1, postcondition="def new_fn():")
assert n == 1
assert "new_fn" in p.read_text()
# and the readback guard ran inside apply_source_edit_file
# ── §25 HELD-OUT: pattern must not overfit its origin incident ──────────────
def test_25heldout_release_config_change_not_demanding_promotion_rework():
"""Held-out release case: a deployment CONFIGURATION change that does not
touch promotion ordering. The release pattern must NOT demand
monotonic-promotion implementation change, but SHOULD still require
release regression verification (release-level obligation)."""
need = cd.Need(
"heldout",
"change the deploy notification webhook URL",
affected_surface=[".gitea/workflows/deploy.yml"],
constraints={},
evidence_refs=["x"],
approval_context="c",
)
authorities = {"rune_spine": _spine()}
surface = cd.resolve_affected_surface(need, authorities)
app = cp.evaluate_applicability(RELEASE_DEPLOYMENT, need, surface, authorities)
# workflow-config surface hit → the pattern applies to the release class
assert app["verdict"] in (cp.APPLIES, cp.PARTIAL)
deriv = cp.derive_pattern_obligations(RELEASE_DEPLOYMENT, need, surface, authorities)
accepted = {e["id"] for e in deriv["accepted"]}
rejected = {e["id"] for e in deriv["rejected"]}
# promotion-ordering work is NOT applicable to a webhook URL change
assert "rel-monotonic-promotion" in rejected
# but release-level invariants still apply
assert "rel-release-regression-verification" in accepted
assert "rel-deployed-identity-verification" in accepted
# ── §24 LIVE CHANGE: save_verified at the durable boundary ──────────────────
def test_24live_save_verified(tmp_path):
"""The pattern-governed live change: Scaffold.save_verified composes
save + reload-readback. The UNSAVED-mutation lie fails; the saved truth
passes. (Pattern contribution proven separately: store-roundtrip
obligation derived by store-schema-evolution.)"""
from rune import scaffold as sc # local alias for the live test
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "lv")
doc = s.init(purpose="live")
s.new_contract(doc, contract_id="LV1", need="n", scope=["src/x"])
s.save(doc) # contract IS persisted at this point
# the T5 defect shape: mutate in memory, NEVER save, claim persisted —
# verify_on_disk (pure readback, no write) catches the lie
c = s.contract(doc, "LV1")
c["identities"]["next_action"] = "tampered claim"
with pytest.raises(AssertionError, match="probably never saved"):
s.verify_on_disk(lambda d: s.contract(d, "LV1")["identities"][
"next_action"], "tampered claim", what="LV1 next_action")
# save-and-confirm catches disk-write failures (different semantic)
s.transition_contract(doc, "LV1", "approved")
s.save_verified(doc, lambda d: s.contract(d, "LV1")["phase"], "approved",
what="LV1 phase")
# and the forgotten-save check now passes — the state is really durable
s.verify_on_disk(lambda d: s.contract(d, "LV1")["phase"], "approved",
what="LV1 phase")
# and a fresh instance sees it (the actual continuity requirement)
doc2 = sc.Scaffold(tmp_path / ".rune" / "scaffold", "lv").load()
assert sc.Scaffold(tmp_path / ".rune" / "scaffold", "lv").contract(
doc2, "LV1")["phase"] == "approved"
+466
View File
@@ -0,0 +1,466 @@
"""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
# ── Tranche 9B: automatic lifecycle wiring (§13 A-L) ────────────────────────
def test_13l_auto_init_requires_no_extra_prose(tmp_path):
"""§7/§13L: routine releases derive facts from governed records — no
hand-written extra prose."""
doc = _governed_doc()
store = tmp_path / "scaffold.json"
store.write_text(json.dumps(doc))
note = rn.auto_initial_note(version="0.66.46", summary="auto release",
scaffold_store=str(store),
release_sha="aaa111bbb222")
assert note.references # governed facts extracted automatically
assert note.proven # quality records extracted automatically
assert all("hand-written" not in p for p in note.provenance)
def test_13c_d_convergence_refreshes_release_keeps_tag_sha(tmp_path):
"""§13C/D/K: the 0.66.45 fixture — tag ce328a80, later same-version
deploy e3263ef. Refresh updates deployed_sha, NEVER tag SHA."""
existing = rn.ReleaseNote(
version="0.66.45", summary="T9 release",
proven=["initial governed evidence"], release_sha="ce328a808026",
deployed_sha="ce328a808026")
doc = _governed_doc()
store = tmp_path / "scaffold.json"
store.write_text(json.dumps(doc))
refreshed = rn.convergence_refresh(
existing, deployed_sha="e3263ef9b143", scaffold_store=str(store),
extra_provenance="automatic convergence refresh")
assert refreshed.deployed_sha == "e3263ef9b143" # converged truth
assert refreshed.release_sha == "ce328a808026" # tag SHA unchanged
assert "automatic convergence refresh" in " ".join(refreshed.provenance)
md = rn.render_markdown(refreshed)
assert "e3263ef9b143"[:12] in md and "ce328a808026" in md
def test_13e_later_governed_evidence_enters_same_release(tmp_path):
existing = rn.ReleaseNote(version="0.66.45", summary="s",
proven=[], release_sha="ce328a808026",
deployed_sha="ce328a808026")
doc = _governed_doc() # carries C1/C2 quality + m1 resolved records
store = tmp_path / "scaffold.json"
store.write_text(json.dumps(doc))
refreshed = rn.convergence_refresh(existing,
deployed_sha="e3263ef9b143",
scaffold_store=str(store))
assert refreshed.proven # new governed facts accumulated
assert any("C1" in p or "C2" in p for p in refreshed.proven)
def test_13f_refresh_idempotent(tmp_path):
store = tmp_path / "scaffold.json"
store.write_text(json.dumps(_governed_doc()))
existing = rn.ReleaseNote(version="0.66.45", summary="s",
release_sha="ce328a808026",
deployed_sha="ce328a808026")
r1 = rn.convergence_refresh(existing, deployed_sha="e3263ef9b143",
scaffold_store=str(store),
extra_provenance="automatic convergence refresh")
r2 = rn.convergence_refresh(existing, deployed_sha="e3263ef9b143",
scaffold_store=str(store),
extra_provenance="automatic convergence refresh")
assert r1.to_dict() == r2.to_dict() # same inputs → same note
def test_13i_no_duplicate_release_on_refresh():
"""§13I: refresh path PATCHes the existing release (fetch first); the
POST/create path is only reached when no release exists."""
calls = []
def fake_api(url, token, method="GET", payload=None):
calls.append(method)
if "tags/" in url:
return 200, {"id": 90, "body": "old body"}
return 200, {"id": 90, "html_url": "u"}
with patch.object(rn, "_api", fake_api):
result = rn.publish_release("http://s", "o/r", "t", _note())
assert result["action"] == "updated"
assert "POST" not in calls
def test_13g_publication_failure_visible(tmp_path):
store = tmp_path / "scaffold.json"
store.write_text(json.dumps(_governed_doc()))
def fake_api(url, token, method="GET", payload=None):
if "tags/" in url:
return 404, {"message": "nope"}
return 503, {"message": "unavailable"}
with patch.object(rn, "_api", fake_api):
result = rn.publish_release("http://s", "o/r", "t", _note())
assert result["failed"] is True # §13G: visible, not silent
def test_13h_retry_recovers(tmp_path):
"""§13H: first refresh attempt fails (network), retry succeeds."""
store = tmp_path / "scaffold.json"
store.write_text(json.dumps(_governed_doc()))
existing = rn.ReleaseNote(version="0.66.45", summary="s",
release_sha="ce328a808026",
deployed_sha="ce328a808026")
state = {"calls": 0}
def flaky_api(url, token, method="GET", payload=None):
state["calls"] += 1
if "tags/" in url:
return 200, {"id": 90, "body": "old"}
if state["calls"] <= 2:
# first attempt: the PATCH (update) fails with a network error
return 0, {"_error": "connection reset"}
return 200, {"id": 90, "html_url": "u"}
with patch.object(rn, "_api", flaky_api):
r1 = rn.publish_release("http://s", "o/r", "t", existing)
assert r1.get("failed") is True and r1["action"] == "update"
r2 = rn.publish_release("http://s", "o/r", "t", existing)
assert r2["action"] == "updated"
def test_13j_unsupported_claim_excluded_everywhere(tmp_path):
store = tmp_path / "scaffold.json"
store.write_text(json.dumps(_governed_doc()))
note = rn.auto_initial_note(version="0.66.47", summary="s",
scaffold_store=str(store),
release_sha="aaa111bbb222")
md = rn.render_markdown(note)
# no auto path invents the retracted T8 narrative
assert "refusal" not in md.lower()
assert "reaction" not in md.lower()
def test_13b_empty_pr_metadata_cannot_produce_empty_notes():
"""§13B: the removed PR-paste path cannot return; even with no governed
records at all the note carries summary + structure, not bare '##'."""
note = rn.auto_initial_note(version="9.9.9", summary="auto release",
scaffold_store="/nonexistent/store.json",
release_sha="aaa111bbb222")
md = rn.render_markdown(note)
assert md.strip() != "## "
assert "auto release" in md
assert "Rune v9.9.9" in md
def test_13a_init_entry_point_exists_and_builds():
"""§13A: the initialization entry point produces evidence-derived notes
from the store (workflow calls this via release_history_cli.py)."""
store = tmp_store()
note = rn.auto_initial_note(version="0.66.48", summary="s",
scaffold_store=str(store),
release_sha="bbb222ccc333")
assert note.version == "0.66.48"
assert note.release_sha == "bbb222ccc333"
def tmp_store():
import tempfile
from pathlib import Path
p = Path(tempfile.mkdtemp()) / "scaffold.json"
p.write_text(json.dumps(_governed_doc()))
return str(p)
+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")