Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3465176f8 | ||
|
|
aab5159ccc | ||
|
|
80f10b9be7 | ||
|
|
9d75e51b22 | ||
|
|
b6113fad7b | ||
|
|
f765365d03 | ||
|
|
b94a3aef61 | ||
|
|
8ca4628694 | ||
|
|
535c290917 | ||
|
|
6bc31c513e | ||
|
|
386137dc14 | ||
|
|
f87adc0d18 | ||
|
|
ed3a73a0a9 | ||
|
|
600008482d | ||
|
|
4bfa4f9b0a | ||
|
|
1d6213cd74 | ||
|
|
9c3f5f0a1d | ||
|
|
c0594ce192 | ||
|
|
dc0ac7b92e | ||
|
|
5e0a2def9d | ||
|
|
049a8dbd86 | ||
|
|
0a9b0648cd | ||
|
|
da2ec4df0e |
No files matched your search
@@ -42,15 +42,36 @@ jobs:
|
||||
echo "::error::CI qualification timeout on $DEPLOY_SHA — deployment refused"
|
||||
exit 1
|
||||
|
||||
- name: Stage, verify, promote
|
||||
- name: Stage, verify, promote (monotonic)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
run: |
|
||||
python3 scripts/deploy_stage.py
|
||||
|
||||
- name: Post-deploy runtime verification (the consuming path)
|
||||
- name: Promotion truthfulness check
|
||||
run: |
|
||||
SHA=$(git rev-parse HEAD)
|
||||
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)"
|
||||
else
|
||||
# Not promoted. deploy_stage exit 0 + non-matching current =
|
||||
# NOOP_ALREADY_CURRENT or STALE_SUPERSEDED — truthful non-error
|
||||
# terminal states (a newer build already won). Do NOT claim
|
||||
# promotion; the runtime-verification step below only runs for
|
||||
# real promotions.
|
||||
echo "NOT PROMOTED: current -> $CURRENT_TARGET (superseded/noop — current is authoritative)"
|
||||
fi
|
||||
|
||||
- name: Post-deploy runtime verification (only for real promotions)
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
SHA=$(git rev-parse HEAD)
|
||||
CURRENT_TARGET=$(readlink /mnt/workdrive/rune-code/current || true)
|
||||
if [ "$CURRENT_TARGET" != "/mnt/workdrive/rune-code/${SHA:0:12}" ]; then
|
||||
echo "SKIP runtime verification: this job did not promote (superseded/noop)"
|
||||
exit 0
|
||||
fi
|
||||
# Exactly what the launcher execs: cd <current>/src, import rune.
|
||||
RUNE_HOME=/mnt/workdrive/rune RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory \
|
||||
/home/nick/.venvs/memory/bin/python3 -c "
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.35**
|
||||
**Current release: v0.66.41**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"generated_at": "2026-09-13T03:10:20Z",
|
||||
"generated_at": "2026-09-13T13:37:11Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
@@ -218,6 +218,9 @@
|
||||
"capability_fit",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer",
|
||||
"construction_decision",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"layout_contract",
|
||||
|
||||
@@ -146,3 +146,83 @@ evidence/changed/next from the store alone — no transcripts.
|
||||
| repo binding (identity model) | UNIT-PROVEN (local git fixture) |
|
||||
| external-repo live proof | BLOCKED ON USER REPO SELECTION |
|
||||
| Scaffolding overall | substrate only — capability-fit discovery, gap decisions, toolchain selection, patterns, maintenance TUI all NOT BUILT (data seams exist) |
|
||||
|
||||
---
|
||||
|
||||
# Scaffolding Tranche 4 — Live Need → Decision → Construction Contract (2026-09-13)
|
||||
|
||||
_The adoption tranche: a real Need now flows automatically through the
|
||||
Tranche 2/3 mechanisms and produces a governed Construction Contract. The
|
||||
model no longer has to remember the procedure — it is durable Scaffolding._
|
||||
|
||||
## Live flow
|
||||
|
||||
NEED (reference to Task/Intent — pointer, never a copy)
|
||||
↓ normalization (gaps recorded, never invented)
|
||||
↓ structural discovery + fit + resolution (Spine + Project Scaffold)
|
||||
↓ toolchain decision (existing project toolchain wins)
|
||||
↓ layout obligations (USER-LOCKED authority inherited)
|
||||
↓ user-approval gate (material changes require approval; approved
|
||||
bounded work never re-asks)
|
||||
↓ DecisionTrace (durable, model-independent)
|
||||
↓ generated Construction Contract → existing build/verify pipeline
|
||||
|
||||
## Authority map (unchanged — the path CONNECTS, never duplicates)
|
||||
|
||||
| authority | owns |
|
||||
|---|---|
|
||||
| Task/Intent | why work exists |
|
||||
| Rune Spine | Rune structural truth |
|
||||
| Project Scaffold | external-project structural/construction truth |
|
||||
| Layout Contract | desired interface constraints (USER-LOCKED = user authority) |
|
||||
| Construction Contract | governed build obligations/history |
|
||||
| Git / Gitea | history / execution |
|
||||
|
||||
## Boundary honesty
|
||||
|
||||
- **ADAPT/research**: the live path records a research ROUTE
|
||||
(TARGETED_EVIDENCE_PROBE / FULL_RESEARCH_PIPELINE) when evidence is
|
||||
insufficient — a seam, NOT an executed pipeline call. No fake
|
||||
integration claimed.
|
||||
- **BUILD_NEW** is never a fallback: insufficient evidence blocks it;
|
||||
normalization gaps block it; merge precedence across authorities
|
||||
requires every consulted authority to agree.
|
||||
- **No second task engine**: one decision function + one trace object.
|
||||
|
||||
## Contract-quality repair (Tranche 3 defects)
|
||||
|
||||
Closure now distinguishes:
|
||||
|
||||
predicted_existing_affected planned changes to existing entities
|
||||
expected_new entities the build PLANNED to create
|
||||
correctly_predicted predicted AND independently observed
|
||||
predicted_but_unaffected predicted, NOT observed
|
||||
expected_new_observed planned creation happened
|
||||
expected_new_missing planned creation did NOT happen
|
||||
genuinely_unexpected_new observed, planned by neither list
|
||||
|
||||
Prediction cannot self-certify observation: the observed set comes ONLY
|
||||
from caller evidence (git/tests/build/deploy). Legacy keys (missed/
|
||||
needless/observed) preserved — old contracts readable without migration.
|
||||
|
||||
## Layout-obligation inheritance
|
||||
|
||||
A change touching a UI component/region/implementation inherits its Layout
|
||||
Contract as a build obligation. USER-LOCKED obligations state that changing
|
||||
placement requires explicit user authorization. Unrelated backend changes
|
||||
inherit nothing. USER-LOCKED drift is never reconciled by editing the
|
||||
contract — code adapts.
|
||||
|
||||
## Maturity (honest)
|
||||
|
||||
- Need intake: EXERCISED (real live proof)
|
||||
- decision orchestration: LIVE-PROVEN for the bounded live flow
|
||||
- capability discovery adoption: EXERCISED
|
||||
- fit/gap adoption: EXERCISED
|
||||
- toolchain adoption: EXERCISED
|
||||
- contract generation: LIVE-PROVEN (one real generated contract)
|
||||
- contract-quality prediction: EXERCISED (repaired semantics, one live closure)
|
||||
- layout-obligation inheritance: UNIT-PROVEN (fixtures) + real TUI binding
|
||||
- Scaffolding overall: adoption path live for one bounded flow — NOT
|
||||
general autonomous construction, NOT universal discovery/toolchain
|
||||
intelligence, NOT Scaffolding complete.
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.35"
|
||||
version = "0.66.41"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""deploy_promotion.py — monotonic promotion authority for Rune deploys.
|
||||
|
||||
SCAFFOLD contract DEPLOY-MONOTONIC-001. Root cause of the 2026-09-13
|
||||
deploy-order race (deploys 7355/7359): deploy_stage.py promoted
|
||||
unconditionally in job-finish order — no comparison against the currently
|
||||
deployed commit, no lock, no truthful outcome for a stale candidate.
|
||||
|
||||
This module owns the PROMOTION decision. The invariant:
|
||||
|
||||
current never regresses along the canonical release branch.
|
||||
|
||||
Ordering authority: GIT ANCESTRY on the canonical release branch
|
||||
(refs/heads/master). Rune's release flow is linear history — every deploy
|
||||
candidate is a master push (tranche commit or its auto-bump descendant).
|
||||
candidate == deployed commit -> NOOP_ALREADY_CURRENT (idempotent)
|
||||
candidate is DESCENDANT of it -> allowed (normal forward deploy)
|
||||
candidate is ANCESTOR of it -> STALE_SUPERSEDED (refuse; truthful)
|
||||
no ancestry relationship -> REFUSED_DIVERGENT (fail closed)
|
||||
deployed commit unknown/unreadable-> REFUSED_UNKNOWN_CURRENT (fail closed)
|
||||
|
||||
Mechanism (smallest that provides the invariant, per brief §4-5):
|
||||
1. promotion-time stale check — ancestry comparison against the commit
|
||||
current ACTUALLY points to at promotion time (not at job start);
|
||||
2. an flock'd critical section around the read-compare-write of
|
||||
current/last-good so two concurrent promotions cannot interleave
|
||||
between the ancestry check and the symlink swap.
|
||||
Workflow-level concurrency is NOT relied on (brief §5): the fence lives at
|
||||
the promotion operation itself.
|
||||
|
||||
Truthful outcomes (brief §8) — build/stage success != promotion success:
|
||||
PROMOTED candidate became current (verified below)
|
||||
NOOP_ALREADY_CURRENT same commit already deployed; nothing mutated
|
||||
STALE_SUPERSEDED a newer commit is already current; nothing mutated
|
||||
REFUSED_DIVERGENT ordering unprovable; fail closed
|
||||
REFUSED_UNKNOWN_CURRENT deployed commit unreadable; fail closed
|
||||
FAILED_* verification/staging failures (in deploy_stage)
|
||||
|
||||
A stale or refused outcome is a NON-ERROR terminal state for the release
|
||||
pipeline (the newer build simply won) but MUST NOT print "PROMOTED" and
|
||||
MUST NOT mutate current/last-good. deploy_stage's exit code and the
|
||||
workflow's post-deploy runtime verification enforce this: the workflow
|
||||
asserts current == candidate after a PROMOTED outcome, so a stale outcome
|
||||
makes the job report honestly rather than masquerade.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
DEPLOY_ROOT = Path("/mnt/workdrive/rune-code")
|
||||
RELEASE_BRANCH = "refs/heads/master"
|
||||
|
||||
OUTCOME_PROMOTED = "PROMOTED"
|
||||
OUTCOME_NOOP = "NOOP_ALREADY_CURRENT"
|
||||
OUTCOME_STALE = "STALE_SUPERSEDED"
|
||||
OUTCOME_DIVERGENT = "REFUSED_DIVERGENT"
|
||||
OUTCOME_UNKNOWN_CURRENT = "REFUSED_UNKNOWN_CURRENT"
|
||||
|
||||
|
||||
# ── pure decision logic (unit-tested; no I/O) ──────────────────────────────
|
||||
|
||||
|
||||
def decide_promotion(candidate_sha: str, deployed_sha: str, ancestry: str) -> str:
|
||||
"""Pure promotion decision from a precomputed ancestry relationship.
|
||||
ancestry is one of: 'same' | 'descendant' | 'ancestor' | 'divergent' |
|
||||
'unknown'. Fail-closed: anything not provably allowed is refused."""
|
||||
if candidate_sha == deployed_sha or ancestry == "same":
|
||||
return OUTCOME_NOOP
|
||||
if ancestry == "descendant":
|
||||
return OUTCOME_PROMOTED
|
||||
if ancestry == "ancestor":
|
||||
return OUTCOME_STALE
|
||||
if ancestry == "divergent":
|
||||
return OUTCOME_DIVERGENT
|
||||
return OUTCOME_UNKNOWN_CURRENT
|
||||
|
||||
|
||||
def ancestry_of(candidate_sha: str, deployed_sha: str, repo: Path, fetch_on_divergent: bool = True) -> str:
|
||||
"""Classify candidate vs deployed using git merge-base --is-ancestor on
|
||||
the canonical release branch's repository. Returns 'unknown' on any git
|
||||
failure (fail-closed callers treat that as refusal).
|
||||
|
||||
Observed live 2026-09-13 (deploy 7449, SHA 535c2909): a DELAYED older
|
||||
candidate's checkout cannot contain commits newer than itself — its
|
||||
local object store lacks the deployed commit entirely, so ancestry
|
||||
classifies 'divergent' and fail-closed refuses. Correct refusal, wrong
|
||||
reason: the candidate IS an ancestor. When the local classification is
|
||||
divergent, fetch origin's canonical branch first (best-effort; the
|
||||
deploy workflow's checkout has credentials configured) and re-classify.
|
||||
If still divergent afterwards, the refusal stands with better
|
||||
evidence."""
|
||||
|
||||
def _is_ancestor(a: str, b: str) -> bool:
|
||||
r = subprocess.run(
|
||||
["git", "-C", str(repo), "-c", f"safe.directory={repo}", "merge-base", "--is-ancestor", a, b],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return r.returncode == 0
|
||||
|
||||
if candidate_sha == deployed_sha:
|
||||
return "same"
|
||||
if _is_ancestor(candidate_sha, deployed_sha):
|
||||
return "ancestor" # candidate is OLDER than what's deployed
|
||||
if _is_ancestor(deployed_sha, candidate_sha):
|
||||
return "descendant" # candidate is NEWER (normal forward deploy)
|
||||
if fetch_on_divergent:
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "-C", str(repo), "-c", f"safe.directory={repo}", "fetch", "origin", "master", "--quiet"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass # best-effort: classification below still fails closed
|
||||
if _is_ancestor(candidate_sha, deployed_sha):
|
||||
return "ancestor"
|
||||
if _is_ancestor(deployed_sha, candidate_sha):
|
||||
return "descendant"
|
||||
return "divergent"
|
||||
|
||||
|
||||
def read_deployed_commit(deploy_root: Path = DEPLOY_ROOT) -> str:
|
||||
"""Read the commit the `current` symlink ACTUALLY points to right now —
|
||||
from the deployed build identity, the deployment authority (never from
|
||||
a stale job-start artifact). Empty string if unreadable."""
|
||||
current = deploy_root / "current"
|
||||
try:
|
||||
target = Path(os.readlink(current))
|
||||
except OSError:
|
||||
return ""
|
||||
identity = target / "src" / "rune" / "_build_identity.json"
|
||||
try:
|
||||
data = json.loads(identity.read_text(encoding="utf-8"))
|
||||
return str(data.get("commit", ""))
|
||||
except (OSError, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
# ── the fenced promotion operation ──────────────────────────────────────────
|
||||
|
||||
|
||||
def promote(
|
||||
candidate_sha: str, stage: Path, tmp_stage: Path, repo: Path, deploy_root: Path = DEPLOY_ROOT
|
||||
) -> tuple[str, str]:
|
||||
"""Atomically decide + promote under an exclusive lock.
|
||||
|
||||
Returns (outcome, detail). ONLY the PROMOTED outcome mutates
|
||||
current/last-good, and it does so inside the same critical section as
|
||||
the decision — no interleaving window between check and swap.
|
||||
|
||||
Crash-safety ordering (brief §16): the stage rename happens BEFORE the
|
||||
lock (a staged-but-unpromoted tree is inert), the symlink swap is a
|
||||
single rename(2) via a temporary link, so current is never absent and
|
||||
never half-updated. last-good is refreshed BEFORE the swap only from a
|
||||
value that was genuinely current a moment ago.
|
||||
"""
|
||||
deploy_root.mkdir(parents=True, exist_ok=True)
|
||||
lock_path = deploy_root / ".promotion.lock"
|
||||
with open(lock_path, "w") as lock_fd:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||
try:
|
||||
deployed = read_deployed_commit(deploy_root)
|
||||
if not deployed:
|
||||
return (OUTCOME_UNKNOWN_CURRENT, "deployed commit unreadable — fail closed")
|
||||
rel = ancestry_of(candidate_sha, deployed, repo)
|
||||
outcome = decide_promotion(candidate_sha, deployed, rel)
|
||||
if outcome != OUTCOME_PROMOTED:
|
||||
return (outcome, f"deployed={deployed[:12]} ancestry={rel}")
|
||||
|
||||
# --- mutate (still inside the lock) ---
|
||||
current = deploy_root / "current"
|
||||
last_good = deploy_root / "last-good"
|
||||
prev_target = os.readlink(current) if current.is_symlink() else ""
|
||||
if prev_target and (deploy_root / "last-good").is_symlink():
|
||||
(deploy_root / "last-good").unlink()
|
||||
if prev_target:
|
||||
_atomic_symlink(deploy_root, "last-good", prev_target)
|
||||
if stage.exists():
|
||||
# re-deploying an already-staged SHA: restage over it
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(stage)
|
||||
os.rename(tmp_stage, stage)
|
||||
_atomic_symlink_target(deploy_root, "current", stage)
|
||||
return (OUTCOME_PROMOTED, f"current -> {stage.name}")
|
||||
finally:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def _atomic_symlink(deploy_root: Path, name: str, target: str) -> None:
|
||||
"""Symlink swap that never leaves `name` absent: write temp link, rename
|
||||
over the final name (rename(2) is atomic on the same filesystem)."""
|
||||
_atomic_symlink_target(deploy_root, name, Path(target))
|
||||
|
||||
|
||||
def _atomic_symlink_target(deploy_root: Path, name: str, target: Path) -> None:
|
||||
tmp = deploy_root / f".{name}.swap-{os.getpid()}"
|
||||
if tmp.is_symlink() or tmp.exists():
|
||||
tmp.unlink()
|
||||
tmp.symlink_to(target)
|
||||
os.replace(tmp, deploy_root / name)
|
||||
+36
-24
@@ -3,12 +3,15 @@
|
||||
|
||||
Called by .gitea/workflows/deploy.yml. Stages src/ into
|
||||
/mnt/workdrive/rune-code/<short-sha>, stamps _build_identity.json, verifies
|
||||
the staged tree (import + SHA assert + authority smoke), then ATOMICALLY
|
||||
promotes the `current` symlink and records `last-good` (safe rollback:
|
||||
the previous deployment remains available).
|
||||
the staged tree (import + SHA assert + authority smoke), then promotes
|
||||
through the MONOTONIC promotion authority (deploy_promotion.py): an older
|
||||
candidate finishing after a newer promoted one is fenced out (STALE_SUPERSEDED)
|
||||
and reports truthfully — build/stage success != promotion success.
|
||||
|
||||
Exit 0 = deployed and verified. Non-zero = deployment refused; the previous
|
||||
deployment stays current.
|
||||
Exit 0 = PROMOTED and verified, or NOOP_ALREADY_CURRENT, or a truthful
|
||||
STALE_SUPERSEDED (newer build already current; nothing mutated).
|
||||
Non-zero = deployment refused (divergent/unknown ordering, verification
|
||||
failure); the previous deployment stays current.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -119,25 +122,34 @@ print("VERIFIED:", json.dumps(info))
|
||||
shutil.rmtree(tmp_stage, ignore_errors=True)
|
||||
return 1
|
||||
|
||||
# --- promote (atomic) + last-known-good ------------------------------
|
||||
DEPLOY_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
current = DEPLOY_ROOT / "current"
|
||||
if current.is_symlink():
|
||||
last_good = DEPLOY_ROOT / "last-good"
|
||||
if last_good.is_symlink():
|
||||
last_good.unlink()
|
||||
last_good.symlink_to(os.readlink(current))
|
||||
if current.is_symlink() or current.exists():
|
||||
current.unlink()
|
||||
if stage.exists():
|
||||
shutil.rmtree(stage) # re-deploying an already-staged SHA: restage
|
||||
os.rename(tmp_stage, stage)
|
||||
current.symlink_to(stage)
|
||||
print(f"PROMOTED: {short}")
|
||||
print("current ->", os.readlink(current))
|
||||
if (DEPLOY_ROOT / "last-good").is_symlink():
|
||||
print("last-good ->", os.readlink(DEPLOY_ROOT / "last-good"))
|
||||
return 0
|
||||
# --- promote: FENCED through the promotion authority -----------------
|
||||
# SCAFFOLD contract DEPLOY-MONOTONIC-001: promotion is monotonic. The
|
||||
# ancestry check + lock live in deploy_promotion.py; this script only
|
||||
# handles the truthful outcome reporting. A stale candidate must NOT
|
||||
# claim promotion success — build/stage success != promotion success.
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
import deploy_promotion # noqa: E402
|
||||
|
||||
outcome, detail = deploy_promotion.promote(sha, DEPLOY_ROOT / short, tmp_stage, repo)
|
||||
|
||||
if outcome == deploy_promotion.OUTCOME_PROMOTED:
|
||||
print(f"PROMOTED: {short}")
|
||||
print("current ->", os.readlink(DEPLOY_ROOT / "current"))
|
||||
if (DEPLOY_ROOT / "last-good").is_symlink():
|
||||
print("last-good ->", os.readlink(DEPLOY_ROOT / "last-good"))
|
||||
return 0
|
||||
if outcome == deploy_promotion.OUTCOME_NOOP:
|
||||
print(f"NOOP_ALREADY_CURRENT: {short} is the deployed commit")
|
||||
print(detail)
|
||||
return 0
|
||||
if outcome == deploy_promotion.OUTCOME_STALE:
|
||||
print(f"STALE_SUPERSEDED: {short} is older than the deployed commit — promotion refused (monotonic promotion)")
|
||||
print(detail)
|
||||
print("current unchanged:", os.readlink(DEPLOY_ROOT / "current"))
|
||||
return 0 # truthful non-error terminal state: the newer build won
|
||||
# divergent / unknown-current: fail closed
|
||||
print(f"REFUSED ({outcome}): {short} — {detail}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.35"
|
||||
__version__ = "0.66.41"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.66.35"' in rune_version,
|
||||
'__version__ = "0.66.41"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -0,0 +1,890 @@
|
||||
"""construction_decision.py — live Need → Decision → Construction Contract.
|
||||
|
||||
SCAFFOLD-TRANCHE-004: the adoption layer that turns the Tranche 2/3
|
||||
mechanisms into a durable procedure Rune no longer has to remember.
|
||||
|
||||
NEED (references Task/Intent — never duplicates them)
|
||||
↓ normalize_need() missing info is NOT invented
|
||||
↓ discover + fit per authority (deterministic; Spine + Project Scaffold)
|
||||
↓ resolve per authority + merge (capability_fit resolution surface)
|
||||
↓ resolve_toolchain() (existing project toolchain wins)
|
||||
↓ layout obligations (LOCKED authority inherited)
|
||||
↓ approval gate (material changes → user)
|
||||
↓ DecisionTrace (durable, model-independent)
|
||||
↓ generate_contract() (populated from the trace)
|
||||
|
||||
Boundaries (brief §3, §32, §33):
|
||||
- NOT a task engine: Need carries a reference to the existing Task/Intent
|
||||
authority; Scaffolding never becomes the "why" authority.
|
||||
- NOT a workflow engine: one decision function, one trace object.
|
||||
- NOT a research pipeline: insufficient evidence routes to a recorded
|
||||
research path (seam) — the live ADAPT wiring is a recorded route, not
|
||||
an executed pipeline call (honest seam, §6).
|
||||
- NOT a structural authority: it reads Spine/Project Scaffold and
|
||||
WRITES only Construction Contracts + decision traces.
|
||||
- BUILD_NEW is never a fallback (§8): merge precedence and the
|
||||
insufficient-evidence invariant of capability_fit enforce it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rune import capability_fit as cf
|
||||
from rune import toolchain as tc
|
||||
from rune.structural_store import relationships
|
||||
|
||||
__all__ = ["Need", "make_decision", "generate_contract", "merge_resolutions"]
|
||||
|
||||
|
||||
# ── Need intake (§4) ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class Need:
|
||||
"""Construction-decision input. References — does not copy — the
|
||||
existing Task/Intent authority (`task_ref` is a pointer, not a copy).
|
||||
Carries no task state of its own: it is WHY-adjacent input to Scaffolding,
|
||||
not a backlog entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
need_id: str,
|
||||
objective: str,
|
||||
*,
|
||||
origin: str = "user",
|
||||
project: str = "rune",
|
||||
task_ref: str = "",
|
||||
constraints: dict | None = None,
|
||||
affected_surface: list[str] | None = None,
|
||||
evidence_refs: list[str] | None = None,
|
||||
approval_context: str = "",
|
||||
requested_permanence: str = "",
|
||||
):
|
||||
if not need_id:
|
||||
raise ValueError("need_id required")
|
||||
if not objective:
|
||||
raise ValueError("objective (required effect) required — do not normalize a need we cannot state")
|
||||
self.need_id = need_id
|
||||
self.objective = objective
|
||||
self.origin = origin
|
||||
self.project = project
|
||||
self.task_ref = task_ref
|
||||
self.constraints = dict(constraints or {})
|
||||
self.affected_surface = list(affected_surface or [])
|
||||
self.evidence_refs = list(evidence_refs or [])
|
||||
self.approval_context = approval_context
|
||||
self.requested_permanence = requested_permanence
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"need_id": self.need_id,
|
||||
"objective": self.objective,
|
||||
"origin": self.origin,
|
||||
"project": self.project,
|
||||
"task_ref": self.task_ref,
|
||||
"constraints": dict(self.constraints),
|
||||
"affected_surface": list(self.affected_surface),
|
||||
"evidence_refs": list(self.evidence_refs),
|
||||
"approval_context": self.approval_context,
|
||||
"requested_permanence": self.requested_permanence,
|
||||
}
|
||||
|
||||
|
||||
# ── requirement normalization (§5) ─────────────────────────────────────────
|
||||
|
||||
|
||||
def normalize_need(need: Need) -> tuple[cf.CapabilityRequirement, list[str]]:
|
||||
"""Derive the CapabilityRequirement WITHOUT inventing facts. Contracts
|
||||
may come from explicit constraints (`contracts` key) or from contract-
|
||||
shaped entries on the affected surface (`contract:x` ids). Everything
|
||||
not determinable lands in `missing` — the caller records it and the
|
||||
resolution degrades to INSUFFICIENT_EVIDENCE rather than guessing."""
|
||||
contracts = sorted(need.constraints.get("contracts", []))
|
||||
contracts += sorted(a for a in need.affected_surface if a.startswith("contract:"))
|
||||
missing = []
|
||||
if need.constraints.get("environment"):
|
||||
pass # carried through
|
||||
else:
|
||||
missing.append("environment")
|
||||
if not need.evidence_refs:
|
||||
missing.append("evidence_refs")
|
||||
req = cf.CapabilityRequirement(
|
||||
need.objective,
|
||||
contracts=contracts,
|
||||
environment=need.constraints.get("environment", {}),
|
||||
target_project=need.project,
|
||||
source_need=need.need_id,
|
||||
task_ref=need.task_ref,
|
||||
requested_permanence=need.requested_permanence,
|
||||
**{k: v for k, v in need.constraints.items() if k not in ("contracts", "environment")},
|
||||
)
|
||||
return req, missing
|
||||
|
||||
|
||||
# ── affected-surface classification + resolution (Tranche 5 §3-5) ──────────
|
||||
|
||||
_SURFACE_UI = "ui:"
|
||||
_SURFACE_REGION = "region:"
|
||||
_SURFACE_CONTRACT = "contract:"
|
||||
_SURFACE_MODULE = "module:"
|
||||
|
||||
|
||||
def classify_surface(surface: str) -> str:
|
||||
"""Classify one affected-surface entry (open-world; Tranche 5 §3).
|
||||
Kinds: structural_entity | source_path | workflow_config | ui_component |
|
||||
layout_region | unresolved. Never drops an entry — unresolved is a
|
||||
first-class kind."""
|
||||
if surface.startswith((_SURFACE_UI,)):
|
||||
return "ui_component"
|
||||
if surface.startswith((_SURFACE_REGION,)):
|
||||
return "layout_region"
|
||||
if surface.startswith(
|
||||
(
|
||||
_SURFACE_CONTRACT,
|
||||
_SURFACE_MODULE,
|
||||
_SURFACE_CAPABILITY,
|
||||
)
|
||||
):
|
||||
return "structural_entity"
|
||||
if surface.endswith(".yml") or surface.endswith(".yaml") or surface.endswith(".json") or surface.endswith(".toml"):
|
||||
return "workflow_config"
|
||||
if (
|
||||
surface.endswith(".py")
|
||||
or "/" in surface
|
||||
or surface.endswith(".md")
|
||||
or surface.endswith(".rs")
|
||||
or surface.endswith(".ts")
|
||||
):
|
||||
return "source_path"
|
||||
return "unresolved"
|
||||
|
||||
|
||||
_SURFACE_CAPABILITY = "capability:"
|
||||
|
||||
|
||||
def resolve_surface(surface: str, kind: str, authorities: dict[str, dict]) -> dict:
|
||||
"""Deterministically resolve one surface against the structural
|
||||
authorities (Tranche 5 §5). No Memory, no LLM archaeology: identity
|
||||
lookups and recorded path->module mappings only.
|
||||
|
||||
Returns {surface, kind, resolved: [...], unresolved_reason: ""}.
|
||||
resolved entries: {entity_id, authority, via} where via records HOW the
|
||||
mapping was established (identity | path_attribute | provides)."""
|
||||
out = {"surface": surface, "kind": kind, "resolved": [], "unresolved_reason": ""}
|
||||
if kind == "structural_entity":
|
||||
for name, doc in authorities.items():
|
||||
e = ss_entity(doc, surface)
|
||||
if e is not None:
|
||||
out["resolved"].append({"entity_id": surface, "authority": name, "via": "identity"})
|
||||
break
|
||||
if not out["resolved"]:
|
||||
out["unresolved_reason"] = "entity id not found in any authority"
|
||||
elif kind == "source_path":
|
||||
# path -> module via recorded identities.path in any authority
|
||||
for name, doc in authorities.items():
|
||||
for e in doc["entities"]:
|
||||
p = e.get("identities", {}).get("path", "")
|
||||
if p and (p == surface or p.endswith("/" + surface) or surface.endswith(p)):
|
||||
out["resolved"].append({"entity_id": e["id"], "authority": name, "via": "path_attribute"})
|
||||
if not out["resolved"]:
|
||||
out["unresolved_reason"] = "no authority records this path as an entity (path remains file-scope only)"
|
||||
elif kind == "ui_component":
|
||||
for name, doc in authorities.items():
|
||||
cid = surface if surface.startswith("ui:") else f"ui:{surface}"
|
||||
e = ss_entity(doc, f"layout:{cid}")
|
||||
if e is not None:
|
||||
out["resolved"].append({"entity_id": f"layout:{cid}", "authority": name, "via": "layout_contract"})
|
||||
if not out["resolved"]:
|
||||
out["unresolved_reason"] = "no Layout Contract bound for component"
|
||||
elif kind == "layout_region":
|
||||
for name, doc in authorities.items():
|
||||
e = ss_entity(doc, surface if surface.startswith("region:") else f"region:{surface}")
|
||||
if e is not None:
|
||||
out["resolved"].append({"entity_id": e["id"], "authority": name, "via": "identity"})
|
||||
if not out["resolved"]:
|
||||
out["unresolved_reason"] = "region not defined in any authority"
|
||||
else:
|
||||
out["unresolved_reason"] = f"kind {kind} has no deterministic resolver"
|
||||
return out
|
||||
|
||||
|
||||
def ss_entity(doc: dict, eid: str):
|
||||
from rune.structural_store import entity as _e
|
||||
|
||||
return _e(doc, eid)
|
||||
|
||||
|
||||
def implemented_contracts(doc: dict, entity_id: str) -> list[str]:
|
||||
"""Contracts an entity implements/exposes/provides (structural fact)."""
|
||||
out = []
|
||||
for r in relationships(doc, from_id=entity_id):
|
||||
if r["type"] in {"implements", "exposes", "provides", "serves"}:
|
||||
out.append(r["to"])
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def resolve_affected_surface(need: Need, authorities: dict[str, dict]) -> dict:
|
||||
"""Classify + resolve the WHOLE affected surface (Tranche 5 §3-5, §13).
|
||||
Produces the propagated representation the generated contract carries:
|
||||
affected_existing_entities structural ids (resolved)
|
||||
affected_paths file-scope entries (mechanical scope)
|
||||
affected_ui_surfaces ui components / regions found
|
||||
unresolved_surfaces retained, never dropped
|
||||
File scope != structural impact: a path stays a path for scope checking
|
||||
even when it also resolves to a module entity (no authority duplication)."""
|
||||
result = {
|
||||
"affected_existing_entities": [],
|
||||
"affected_paths": [],
|
||||
"affected_ui_surfaces": [],
|
||||
"affected_regions": [],
|
||||
"workflow_config_surfaces": [],
|
||||
"unresolved_surfaces": [],
|
||||
"resolutions": [],
|
||||
}
|
||||
for surface in need.affected_surface:
|
||||
kind = classify_surface(surface)
|
||||
res = resolve_surface(surface, kind, authorities)
|
||||
result["resolutions"].append(res)
|
||||
if kind == "source_path":
|
||||
result["affected_paths"].append(surface)
|
||||
elif kind == "workflow_config":
|
||||
result["workflow_config_surfaces"].append(surface)
|
||||
elif kind == "ui_component":
|
||||
result["affected_ui_surfaces"].append(surface)
|
||||
elif kind == "layout_region":
|
||||
result["affected_regions"].append(surface)
|
||||
for r in res["resolved"]:
|
||||
eid = r["entity_id"]
|
||||
if eid not in result["affected_existing_entities"] and not eid.startswith("layout:"):
|
||||
result["affected_existing_entities"].append(eid)
|
||||
if res["unresolved_reason"] and not res["resolved"]:
|
||||
result["unresolved_surfaces"].append({"surface": surface, "kind": kind, "reason": res["unresolved_reason"]})
|
||||
for k in (
|
||||
"affected_existing_entities",
|
||||
"affected_paths",
|
||||
"affected_ui_surfaces",
|
||||
"affected_regions",
|
||||
"workflow_config_surfaces",
|
||||
):
|
||||
result[k] = sorted(set(result[k]))
|
||||
|
||||
# §9/§17 DERIVED EXPECTATION (structural consequence, not invention): a
|
||||
# Rune test path (tests/test_<name>.py) affected by the work implies the
|
||||
# "test:<name>" surface participates in the build. Derived only when not
|
||||
# already an existing entity nor explicitly declared; provenance records
|
||||
# the derivation for closure accounting.
|
||||
derived = []
|
||||
for surface in need.affected_surface:
|
||||
if surface.startswith("tests/test_") and surface.endswith(".py"):
|
||||
stem = surface[len("tests/test_") : -len(".py")]
|
||||
test_entity = f"test:{stem}"
|
||||
exists = any(ss_entity(doc, test_entity) is not None for doc in authorities.values())
|
||||
if test_entity not in need.constraints.get("expected_new_entities", []) and not exists:
|
||||
derived.append(
|
||||
{
|
||||
"entity": test_entity,
|
||||
"derived_from": surface,
|
||||
"rule": "Rune test-path convention: tests/test_<name>.py implies "
|
||||
"the test:<name> surface participates in the build",
|
||||
"evidence": f"affected_surface entry {surface}",
|
||||
}
|
||||
)
|
||||
result["derived_expected_new"] = derived
|
||||
return result
|
||||
|
||||
|
||||
# ── requirement completeness gate (Tranche 5 §6-9) ──────────────────────────
|
||||
|
||||
COMPLETE_ENOUGH = "COMPLETE_ENOUGH"
|
||||
MATERIAL_GAP = "MATERIAL_GAP"
|
||||
COMPLETENESS_UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
def check_completeness(
|
||||
req: cf.CapabilityRequirement, need: Need, authorities: dict[str, dict], surface_result: dict
|
||||
) -> dict:
|
||||
"""Bounded, structure-grounded completeness check run BEFORE fit.
|
||||
|
||||
Rule (deterministic, no model vibes): every affected-surface entry that
|
||||
resolves to a structural entity contributes that entity's IMPLEMENTED
|
||||
contracts to the requirement's material scope. If an affected entity
|
||||
implements contracts NOT represented in the normalized requirement, the
|
||||
requirement is materially incomplete — the need touches a component
|
||||
whose obligations it has not stated. That is exactly the deploy-incident
|
||||
shape: the affected promotion implementation carried an unrepresented
|
||||
ordering contract while the requirement named only qualification.
|
||||
|
||||
Verdicts (Tranche 5 §8):
|
||||
COMPLETE_ENOUGH all affected entities' contracts are represented
|
||||
(or none implement contracts) — fit may proceed
|
||||
MATERIAL_GAP uncovered implemented contracts exist on affected
|
||||
entities — named with provenance, routed to probe
|
||||
UNKNOWN no surface resolved — completeness undeterminable;
|
||||
never silently COMPLETE (§8)
|
||||
|
||||
Derived requirements (§9) are structural consequences, not inventions:
|
||||
each uncovered contract carries derived_from (the entity that
|
||||
implements it) + evidence (the implements relationship) + rule.
|
||||
"""
|
||||
gaps = []
|
||||
resolved_any = False
|
||||
for res in surface_result["resolutions"]:
|
||||
for r in res["resolved"]:
|
||||
resolved_any = True
|
||||
for name, doc in authorities.items():
|
||||
e = ss_entity(doc, r["entity_id"])
|
||||
if e is None:
|
||||
continue
|
||||
for contract in implemented_contracts(doc, r["entity_id"]):
|
||||
if contract not in req.contracts:
|
||||
gaps.append(
|
||||
{
|
||||
"missing_contract": contract,
|
||||
"derived_from": r["entity_id"],
|
||||
"surface": res["surface"],
|
||||
"authority": name,
|
||||
"rule": "affected-surface entity implements a "
|
||||
"contract not represented in the "
|
||||
"normalized requirement",
|
||||
"evidence": f"{r['entity_id']} implements {contract} (authority: {name})",
|
||||
}
|
||||
)
|
||||
break # entity found in one authority is enough
|
||||
if gaps:
|
||||
# dedupe by (missing_contract, derived_from)
|
||||
seen = set()
|
||||
unique = []
|
||||
for g in gaps:
|
||||
key = (g["missing_contract"], g["derived_from"])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(g)
|
||||
return {
|
||||
"verdict": MATERIAL_GAP,
|
||||
"gaps": sorted(unique, key=lambda g: (g["missing_contract"], g["derived_from"])),
|
||||
"probe_questions": [
|
||||
f"does the need's stated failure implicate {g['derived_from']}'s {g['missing_contract']} obligation?"
|
||||
for g in unique[:5]
|
||||
],
|
||||
"note": "MATERIAL_GAP: affected surface carries obligations "
|
||||
"the requirement does not represent — refine before "
|
||||
"trusting an apparently-complete fit",
|
||||
}
|
||||
if resolved_any:
|
||||
return {
|
||||
"verdict": COMPLETE_ENOUGH,
|
||||
"gaps": [],
|
||||
"probe_questions": [],
|
||||
"note": "all resolved affected entities' implemented contracts are represented in the requirement",
|
||||
}
|
||||
return {
|
||||
"verdict": COMPLETENESS_UNKNOWN,
|
||||
"gaps": [],
|
||||
"probe_questions": [f"which structural entities does '{s}' map to?" for s in need.affected_surface[:5]],
|
||||
"note": "no affected surface resolved — completeness undeterminable; UNKNOWN never silently becomes COMPLETE",
|
||||
}
|
||||
|
||||
|
||||
def record_proposed_requirement(
|
||||
contract_id: str, *, provider: str, model: str, evidence: list[str], confidence: float
|
||||
) -> dict:
|
||||
"""Record a MODEL-PROPOSED requirement addition (Tranche 5 §12). It is
|
||||
evidence with a verification state — never structural authority by
|
||||
itself. It becomes accepted only via existing authority, research
|
||||
evidence, implementation evidence, or explicit user decision; until
|
||||
then normalize_need excludes it from the requirement's contract set."""
|
||||
if not provider or not model:
|
||||
raise ValueError("model-proposed requirement requires provider+model")
|
||||
if not 0.0 <= confidence <= 1.0:
|
||||
raise ValueError("confidence must be in [0,1]")
|
||||
return {
|
||||
"kind": "model_proposed_requirement",
|
||||
"contract": contract_id,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"evidence": sorted(evidence),
|
||||
"confidence": confidence,
|
||||
"verification_state": "PENDING",
|
||||
}
|
||||
|
||||
|
||||
# ── research routing seam (§6 — honest: recorded route, not execution) ─────
|
||||
|
||||
|
||||
def research_route(req: cf.CapabilityRequirement, decision: dict) -> dict:
|
||||
"""Record which evidence path the decision needs. This is a SEAM: the
|
||||
live ADAPT interface is not yet wired for direct invocation, so the
|
||||
route is recorded on the trace for the orchestrating session to act on.
|
||||
NOT claimed as executed research."""
|
||||
if decision["resolution"] == "INSUFFICIENT_EVIDENCE":
|
||||
return {
|
||||
"route": "TARGETED_EVIDENCE_PROBE",
|
||||
"question": f"what satisfies: {req.objective[:120]}",
|
||||
"status": "RECORDED_NOT_EXECUTED",
|
||||
}
|
||||
if decision["resolution"] == "ACQUIRE_EXTERNAL":
|
||||
return {
|
||||
"route": "FULL_RESEARCH_PIPELINE",
|
||||
"question": f"external acquisition options for: {req.objective[:120]}",
|
||||
"status": "RECORDED_NOT_EXECUTED",
|
||||
}
|
||||
return {"route": "NONE", "status": "LOCAL_SOLVE"}
|
||||
|
||||
|
||||
# ── resolution merge across authorities ─────────────────────────────────────
|
||||
|
||||
_PRECEDENCE = [
|
||||
"SATISFIED",
|
||||
"REUSE",
|
||||
"COMPOSE",
|
||||
"ADAPT_EXISTING",
|
||||
"CONFIGURE",
|
||||
"ACQUIRE_EXTERNAL",
|
||||
"INSUFFICIENT_EVIDENCE",
|
||||
"BUILD_NEW",
|
||||
]
|
||||
|
||||
|
||||
def merge_resolutions(results: list[dict]) -> dict:
|
||||
"""Merge per-authority resolution results. Precedence: a concrete
|
||||
reuse/compose/adapt verdict from any authority wins over
|
||||
INSUFFICIENT_EVIDENCE from another; BUILD_NEW can only result when NO
|
||||
authority produced a better verdict AND positive gap evidence exists —
|
||||
it is never a fallback across merges (§8)."""
|
||||
if not results:
|
||||
return {"resolution": "INSUFFICIENT_EVIDENCE", "rationale": "no authority consulted"}
|
||||
best = min(results, key=lambda r: _PRECEDENCE.index(r["resolution"]))
|
||||
merged = dict(best)
|
||||
merged["per_authority"] = [{"authority": r.get("authority", ""), "resolution": r["resolution"]} for r in results]
|
||||
if best["resolution"] == "BUILD_NEW" and len(results) > 1:
|
||||
# only honest if every authority independently agrees
|
||||
if any(r["resolution"] != "BUILD_NEW" for r in results):
|
||||
weaker = [r for r in results if r["resolution"] == "INSUFFICIENT_EVIDENCE"]
|
||||
if weaker:
|
||||
merged = dict(weaker[0])
|
||||
merged["per_authority"] = [
|
||||
{"authority": r.get("authority", ""), "resolution": r["resolution"]} for r in results
|
||||
]
|
||||
merged["rationale"] = (
|
||||
"BUILD_NEW rejected: at least one authority returned "
|
||||
"INSUFFICIENT_EVIDENCE — positive gap evidence required "
|
||||
"from all consulted authorities (§8)"
|
||||
)
|
||||
return merged
|
||||
|
||||
|
||||
# ── user-approval gate (§13) ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def needs_user_approval(
|
||||
resolution: str, toolchain_decision: tc.DecisionRecord, layout_obligations: list[dict], need: Need
|
||||
) -> dict:
|
||||
"""Classify whether decision→contract requires explicit user approval.
|
||||
Small bounded work inside an already-approved contract does NOT re-ask
|
||||
(need.approval_context covers it)."""
|
||||
reasons = []
|
||||
if toolchain_decision.user_approval_required:
|
||||
reasons.append("toolchain change (CHANGE_MAJOR)")
|
||||
if resolution == "BUILD_NEW" and need.requested_permanence != "temporary":
|
||||
reasons.append("new capability construction")
|
||||
if any(o["lock"] for o in layout_obligations):
|
||||
reasons.append("USER-LOCKED layout surface affected — placement changes require user authorization")
|
||||
if need.requested_permanence == "permanent":
|
||||
reasons.append("permanent promotion requested (user-controlled)")
|
||||
if need.constraints.get("new_repository") or need.constraints.get("new_application"):
|
||||
reasons.append("new repository/application")
|
||||
covered = bool(need.approval_context) and not reasons
|
||||
if reasons and need.approval_context and need.constraints.get("approval_covers") == "all":
|
||||
covered = True
|
||||
reasons = [f"{r} — covered by: {need.approval_context}" for r in reasons]
|
||||
return {
|
||||
"required": bool(reasons) and not covered,
|
||||
"reasons": reasons,
|
||||
"covered_by": need.approval_context if covered else "",
|
||||
}
|
||||
|
||||
|
||||
# ── the live decision path ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def make_decision(
|
||||
need: Need,
|
||||
*,
|
||||
authorities: dict[str, dict],
|
||||
toolchain_constraints: tc.ToolchainConstraints | None = None,
|
||||
project_toolchain: dict | None = None,
|
||||
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}).
|
||||
`project_toolchain` seeds the Toolchain Resolver's existing-project
|
||||
constraint (existing toolchain wins — §12).
|
||||
`prior_iterations` (Tranche 5 §11): earlier decision iterations being
|
||||
refined — retained in the trace with why-it-changed provenance; the
|
||||
final resolution stays singular.
|
||||
|
||||
Order of operations (Tranche 5 §6): affected-surface resolution →
|
||||
REQUIREMENT COMPLETENESS GATE → discovery/fit → merge. A MATERIAL_GAP
|
||||
downgrades premature concrete verdicts (REUSE/SATISFIED/COMPOSE/
|
||||
ADAPT_EXISTING) to INSUFFICIENT_EVIDENCE with the gap recorded — the
|
||||
fit engine was right GIVEN its requirement; the requirement was too
|
||||
narrow."""
|
||||
req, missing = normalize_need(need)
|
||||
trace: dict = {
|
||||
"need": need.to_dict(),
|
||||
"requirement": req.to_dict(),
|
||||
"normalization_missing": missing,
|
||||
"candidates": [],
|
||||
"fits": [],
|
||||
"per_authority_results": [],
|
||||
"layout_obligations": [],
|
||||
"research": {},
|
||||
"toolchain": {},
|
||||
"approval": {},
|
||||
"resolution": "",
|
||||
"rationale": "",
|
||||
"iterations": list(prior_iterations or []),
|
||||
}
|
||||
|
||||
# affected-surface classification + resolution (Tranche 5 §3-5)
|
||||
surface_result = resolve_affected_surface(need, authorities)
|
||||
trace["affected_surfaces"] = surface_result
|
||||
|
||||
# REQUIREMENT COMPLETENESS GATE (Tranche 5 §6-8) — before fit
|
||||
completeness = check_completeness(req, need, authorities, surface_result)
|
||||
trace["completeness"] = completeness
|
||||
|
||||
# live discovery + fit + resolution per authority (§7-8)
|
||||
results = []
|
||||
all_fits = []
|
||||
for name, doc in authorities.items():
|
||||
candidates = cf.discover_candidates(doc, req)
|
||||
fits = [cf.evaluate_fit(req, c) for c in candidates]
|
||||
for f, c in zip(fits, candidates):
|
||||
f["authority"] = name
|
||||
c["authority"] = name
|
||||
all_fits.extend(fits)
|
||||
trace["candidates"].extend(
|
||||
{k: c[k] for k in ("id", "kind", "maturity", "status", "authority", "provides", "consumers")}
|
||||
for c in candidates
|
||||
)
|
||||
res = cf.decide(doc, req, fits)
|
||||
res["authority"] = name
|
||||
results.append(res)
|
||||
trace["fits"] = all_fits
|
||||
if results:
|
||||
merged = merge_resolutions(results)
|
||||
else:
|
||||
merged = {"resolution": "INSUFFICIENT_EVIDENCE", "rationale": "no authorities provided"}
|
||||
if missing and merged["resolution"] in ("BUILD_NEW",):
|
||||
# normalization gaps block BUILD_NEW too (§5: do not invent)
|
||||
merged = {
|
||||
"resolution": "INSUFFICIENT_EVIDENCE",
|
||||
"rationale": f"requirement normalization incomplete: {missing}; refusing to build on guesses",
|
||||
}
|
||||
if completeness["verdict"] == MATERIAL_GAP and merged["resolution"] in (
|
||||
"SATISFIED",
|
||||
"REUSE",
|
||||
"COMPOSE",
|
||||
"ADAPT_EXISTING",
|
||||
"CONFIGURE",
|
||||
):
|
||||
# PREMATURE-FIT GUARD (Tranche 5 §6-7): the fit engine was correct
|
||||
# GIVEN its requirement — but the requirement is materially
|
||||
# incomplete (affected surface carries unrepresented obligations).
|
||||
# A concrete verdict on an incomplete requirement is not trustworthy.
|
||||
merged = {
|
||||
"resolution": "INSUFFICIENT_EVIDENCE",
|
||||
"rationale": (
|
||||
"premature fit blocked: requirement is materially incomplete "
|
||||
f"({len(completeness['gaps'])} unrepresented obligation(s) on "
|
||||
"affected surfaces); refine the requirement before trusting "
|
||||
"an apparently-complete fit"
|
||||
),
|
||||
"premature_fit_blocked": {
|
||||
"would_have_been": merged["resolution"],
|
||||
"gaps": completeness["gaps"],
|
||||
},
|
||||
}
|
||||
trace["resolution"] = merged["resolution"]
|
||||
trace["rationale"] = merged.get("rationale", "")
|
||||
if missing and trace["resolution"] == "INSUFFICIENT_EVIDENCE":
|
||||
trace["rationale"] += f"; normalization gaps recorded, not invented: {missing}"
|
||||
trace["per_authority_results"] = merged.get("per_authority", results)
|
||||
trace["_merged"] = merged
|
||||
|
||||
# toolchain (§12): existing project toolchain wins
|
||||
constraints = toolchain_constraints or tc.ToolchainConstraints(**(project_toolchain or {}))
|
||||
tdec = tc.resolve_toolchain(constraints)
|
||||
trace["toolchain"] = tdec.to_dict()
|
||||
|
||||
# layout obligations (§19-20): affected surface + candidate impls
|
||||
affected = list(need.affected_surface)
|
||||
for c in trace["candidates"]:
|
||||
affected.append(c["id"])
|
||||
lay = []
|
||||
for name, doc in authorities.items():
|
||||
lay.extend(layout_obligations_for(doc, affected))
|
||||
trace["layout_obligations"] = lay
|
||||
|
||||
# research routing (§6, honest seam)
|
||||
trace["research"] = research_route(req, merged)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
def layout_obligations_for(doc: dict, affected: list[str]) -> list[dict]:
|
||||
"""Layout obligations from a store doc without a Scaffold instance
|
||||
(delegates to the same authority logic via a lightweight query)."""
|
||||
obligations = []
|
||||
affected_set = set(affected)
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] != "layout_contract" or e.get("status") == "RETIRED":
|
||||
continue
|
||||
ident = e["identities"]
|
||||
comp, region, impl = (ident.get("component", ""), ident.get("region", ""), ident.get("implementation", ""))
|
||||
hit = (
|
||||
comp in affected
|
||||
or f"region:{region}" in affected
|
||||
or (impl and any(a in impl for a in affected if a.endswith(".py")))
|
||||
)
|
||||
if not hit:
|
||||
continue
|
||||
locked = bool(ident.get("lock"))
|
||||
obligations.append(
|
||||
{
|
||||
"component": comp,
|
||||
"region": region,
|
||||
"lock": locked,
|
||||
"provenance": ident.get("provenance", "scaffold"),
|
||||
"obligation": (
|
||||
"USER-LOCKED: preserve user placement in region "
|
||||
"'%s'; changes require explicit user authorization" % region
|
||||
)
|
||||
if locked
|
||||
else (
|
||||
"preserve/verify %s placement in region '%s'; movable "
|
||||
"only via authorized layout amendment" % (comp, region)
|
||||
),
|
||||
"verification": ("layout_contract_check must PASS; USER-LOCKED placement unchanged")
|
||||
if locked
|
||||
else "layout_contract_check must PASS",
|
||||
"status": e.get("status", "ACTIVE"),
|
||||
}
|
||||
)
|
||||
return obligations
|
||||
|
||||
|
||||
def obligations_from_trace(trace: dict, req: cf.CapabilityRequirement) -> dict:
|
||||
"""BUILD-BUT-DON'T-BREAK (§9) + composition/acquire obligations (§10-11)
|
||||
derived automatically from the trace — the builder does not memorize the
|
||||
rule; the path produces it."""
|
||||
resolution = trace["resolution"]
|
||||
obligations: dict = {
|
||||
"new_behavior": [],
|
||||
"regression": [],
|
||||
"composition": [],
|
||||
"acquisition": [],
|
||||
"layout": trace["layout_obligations"],
|
||||
"verification": [],
|
||||
}
|
||||
merged = trace.get("_merged", {})
|
||||
selected = merged.get("selected") or merged.get("components") or []
|
||||
if isinstance(selected, str):
|
||||
selected = [selected]
|
||||
if resolution in ("ADAPT_EXISTING", "REUSE", "SATISFIED") and selected:
|
||||
# regression obligations from existing consumers + contracts (§9)
|
||||
for c in trace["candidates"]:
|
||||
if c["id"] in selected and c.get("consumers"):
|
||||
obligations["regression"].append(
|
||||
f"preserve existing behavior of {c['id']} for consumers: {', '.join(c['consumers'])}"
|
||||
)
|
||||
obligations["verification"].append("new behavior proven by tests before closure")
|
||||
if resolution == "ADAPT_EXISTING":
|
||||
obligations["verification"].append("regression proof: original capability's consumers still work")
|
||||
if resolution == "COMPOSE":
|
||||
obligations["composition"] = [
|
||||
{
|
||||
"components": merged.get("components", selected),
|
||||
"joining_contracts": req.contracts,
|
||||
"permanence": merged.get("permanence", "temporary (user-controlled)"),
|
||||
}
|
||||
]
|
||||
obligations["verification"].append("composition boundary behavior verified end-to-end")
|
||||
if resolution == "ACQUIRE_EXTERNAL":
|
||||
obligations["acquisition"] = merged.get("external", [])
|
||||
obligations["verification"].append("external acquisition requires research/approval step before install")
|
||||
if resolution == "BUILD_NEW":
|
||||
obligations["verification"].append("positive gap evidence recorded in contract")
|
||||
for lo in trace["layout_obligations"]:
|
||||
obligations["verification"].append(lo["verification"])
|
||||
return obligations
|
||||
|
||||
|
||||
# ── contract generation (§15) ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def generate_contract(
|
||||
s, doc: dict, trace: dict, *, contract_id: str, scope: list[str] | None = None, proposer_note: str = ""
|
||||
) -> dict:
|
||||
"""Populate a Construction Contract from the decision trace (Tranche 5
|
||||
§13-17 IMPACT-AWARE generation). The Need's affected surface is never
|
||||
lost — it is classified and propagated into distinct surfaces:
|
||||
|
||||
affected_entities structural entity ids (existing)
|
||||
expected_new_entities planned new structural entities
|
||||
ext.affected_paths file-scope entries (mechanical scope check)
|
||||
ext.affected_ui_surfaces ui components (layout obligations follow)
|
||||
ext.unresolved_surfaces retained, never dropped
|
||||
ext.expected_runtime_effects operational effects (deployment:current
|
||||
advances etc.) — accounting metadata, NOT a
|
||||
workflow engine; closure requires independent
|
||||
observation before marking them observed.
|
||||
|
||||
File scope: generated from affected paths + selected capability
|
||||
implementation paths + explicit expected-new paths — NOT unrestricted
|
||||
repo scope. `scope` param may still extend it; generated paths are
|
||||
recorded as such (provenance: affected-surface).
|
||||
"""
|
||||
merged = trace["_merged"]
|
||||
req = trace["requirement"]
|
||||
surfaces = trace.get("affected_surfaces", {})
|
||||
selected = merged.get("selected") or []
|
||||
if isinstance(selected, str):
|
||||
selected = [selected]
|
||||
|
||||
# structural affected: selected candidates + requirement contracts +
|
||||
# resolved surface entities — the union the old generator partially lost
|
||||
affected = {c["id"] for c in trace["candidates"] if c["id"] in selected}
|
||||
affected |= set(trace["requirement"].get("contracts", []))
|
||||
affected |= set(surfaces.get("affected_existing_entities", []))
|
||||
|
||||
# file scope: generated (affected paths + selected impl paths + expected
|
||||
# new paths) UNION caller-supplied additions, provenance-tracked
|
||||
affected_paths = list(surfaces.get("affected_paths", []))
|
||||
impl_paths = []
|
||||
for c in trace["candidates"]:
|
||||
if c["id"] in selected:
|
||||
p = c.get("identities", {}).get("path", "")
|
||||
if p:
|
||||
impl_paths.append(p)
|
||||
expected_new_paths = trace["need"]["constraints"].get("expected_new_source_paths", [])
|
||||
generated_scope = sorted(
|
||||
set(affected_paths)
|
||||
| set(surfaces.get("workflow_config_surfaces", []))
|
||||
| set(impl_paths)
|
||||
| set(expected_new_paths)
|
||||
)
|
||||
caller_scope = sorted(set(scope or []) - set(generated_scope))
|
||||
final_scope = sorted(set(generated_scope) | set(caller_scope))
|
||||
|
||||
expected_runtime = sorted(trace["need"]["constraints"].get("expected_runtime_effects", []))
|
||||
|
||||
obl = trace["obligations"]
|
||||
s.new_contract(
|
||||
doc,
|
||||
contract_id=contract_id,
|
||||
need=trace["need"]["objective"],
|
||||
origin=trace["need"]["origin"],
|
||||
scope=final_scope,
|
||||
requirement=req.get("objective", ""),
|
||||
gap_evidence=merged.get("rationale", ""),
|
||||
research_refs=(
|
||||
trace["need"]["evidence_refs"]
|
||||
+ ([trace["research"]["route"]] if trace["research"]["route"] != "NONE" else [])
|
||||
),
|
||||
resolution=trace["resolution"],
|
||||
toolchain=trace["toolchain"],
|
||||
affected_entities=sorted(affected),
|
||||
expected_new_entities=sorted(set(
|
||||
trace["need"]["constraints"].get("expected_new_entities", [])
|
||||
) | {d["entity"] for d in surfaces.get("derived_expected_new", [])}),
|
||||
expected_consumers=[],
|
||||
regression_obligations=obl["regression"],
|
||||
verification_obligations=sorted(set(obl["verification"])),
|
||||
rollback_ref=trace["need"]["constraints"].get("rollback_ref", "git revert"),
|
||||
acceptance=[f"resolution {trace['resolution']} verified end-to-end"],
|
||||
non_goals=trace["need"]["constraints"].get("non_goals", []),
|
||||
)
|
||||
e = s.contract(doc, contract_id)
|
||||
e.setdefault("ext", {}).update(
|
||||
{
|
||||
"decision_trace": {
|
||||
k: trace[k]
|
||||
for k in (
|
||||
"need",
|
||||
"requirement",
|
||||
"candidates",
|
||||
"fits",
|
||||
"resolution",
|
||||
"rationale",
|
||||
"research",
|
||||
"toolchain",
|
||||
"approval",
|
||||
"obligations",
|
||||
"layout_obligations",
|
||||
"normalization_missing",
|
||||
"completeness",
|
||||
"affected_surfaces",
|
||||
"iterations",
|
||||
)
|
||||
},
|
||||
# distinct impact surfaces (Tranche 5 §13) — file scope != structural
|
||||
# impact != expected new != runtime effect
|
||||
"affected_paths": affected_paths,
|
||||
"affected_ui_surfaces": surfaces.get("affected_ui_surfaces", []),
|
||||
"affected_regions": surfaces.get("affected_regions", []),
|
||||
"workflow_config_surfaces": surfaces.get("workflow_config_surfaces", []),
|
||||
"unresolved_surfaces": surfaces.get("unresolved_surfaces", []),
|
||||
"expected_runtime_effects": expected_runtime,
|
||||
"scope_provenance": {"generated_from_affected_surface": generated_scope, "caller_added": caller_scope},
|
||||
}
|
||||
)
|
||||
if proposer_note:
|
||||
e["ext"]["decision_trace"]["proposer_note"] = proposer_note
|
||||
return e
|
||||
@@ -0,0 +1,465 @@
|
||||
"""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
|
||||
|
||||
|
||||
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.lower() for p in signals.get("path_contains", [])],
|
||||
lambda c: any(
|
||||
c in p.lower()
|
||||
for p in surface_result.get("affected_paths", []) + surface_result.get("workflow_config_surfaces", [])
|
||||
),
|
||||
)
|
||||
_check(
|
||||
"entities_match",
|
||||
[e.lower() for e in signals.get("entities_match", [])],
|
||||
lambda c: any(c in e.lower() for e in surface_result.get("affected_existing_entities", [])),
|
||||
)
|
||||
_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.lower() for p in surface_result.get("affected_paths", [])] + [
|
||||
p.lower() for p in surface_result.get("workflow_config_surfaces", [])
|
||||
]
|
||||
entities = (
|
||||
[e.lower() for e in surface_result.get("affected_existing_entities", [])]
|
||||
+ [u.lower() for u in surface_result.get("affected_ui_surfaces", [])]
|
||||
+ [r.lower() for r in surface_result.get("affected_regions", [])]
|
||||
)
|
||||
if when is None:
|
||||
return OBL_UNKNOWN, "obligation declares no applicability condition"
|
||||
if when.get("always") is True:
|
||||
return OBL_REQUIRED, "pattern-level obligation (always applies when the pattern applies)"
|
||||
if "path_contains" in when:
|
||||
hits = [p for sub in when["path_contains"] for p in paths if sub.lower() in p]
|
||||
if hits:
|
||||
return OBL_APPLICABLE, f"affected path(s): {hits[:3]}"
|
||||
return OBL_NOT_APPLICABLE, ("no affected path matches " + str(when["path_contains"]))
|
||||
if "entities_match" in when:
|
||||
hits = [e for sub in when["entities_match"] for e in entities if sub.lower() in e]
|
||||
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}
|
||||
@@ -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": {"path_contains": ["tui", "ui", "widget"]},
|
||||
},
|
||||
],
|
||||
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
|
||||
@@ -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.35
|
||||
Version: v0.66.41
|
||||
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.35 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.66.41 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
+185
-24
@@ -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:
|
||||
@@ -331,6 +364,7 @@ class Scaffold:
|
||||
regression_obligations: list[str] | None = None,
|
||||
verification_obligations: list[str] | None = None,
|
||||
migration_obligations: list[str] | None = None,
|
||||
expected_new_entities: list[str] | None = None,
|
||||
rollback_ref: str = "",
|
||||
acceptance: list[str] | None = None,
|
||||
non_goals: list[str] | None = None,
|
||||
@@ -338,7 +372,11 @@ class Scaffold:
|
||||
"""Create a Construction Contract in `draft` phase. One durable
|
||||
object; phases/status are plain fields with a legal-transition map.
|
||||
`toolchain` is a data seam: language/runtime/framework/libs/rationale
|
||||
— no Python-specific assumptions enforced."""
|
||||
— no Python-specific assumptions enforced.
|
||||
`expected_new_entities` (SCAFFOLD-TRANCHE-004 contract-quality
|
||||
repair): entities this build PLANS to create. At closure these are
|
||||
checked against observed reality — an observed planned-new entity is
|
||||
NOT a prediction miss (the Tranche 3 defect this field fixes)."""
|
||||
if entity(doc, f"contract:{contract_id}") is not None:
|
||||
raise ValueError(f"contract already exists: {contract_id}")
|
||||
identities = {
|
||||
@@ -352,6 +390,7 @@ class Scaffold:
|
||||
"toolchain": dict(toolchain or {}), # data seam: any language/runtime
|
||||
"affected_entities": sorted(affected_entities or []),
|
||||
"expected_consumers": sorted(expected_consumers or []),
|
||||
"expected_new_entities": sorted(expected_new_entities or []),
|
||||
"regression_obligations": sorted(regression_obligations or []),
|
||||
"verification_obligations": sorted(verification_obligations or []),
|
||||
"migration_obligations": sorted(migration_obligations or []),
|
||||
@@ -432,7 +471,25 @@ class Scaffold:
|
||||
"""Close a contract: record observed impact independently, run the
|
||||
predicted-vs-observed comparison, persist Contract Quality evidence.
|
||||
Requires the contract to be in `superseding` phase (the final work
|
||||
phase) or `verifying` (pure-observation builds)."""
|
||||
phase) or `verifying` (pure-observation builds).
|
||||
|
||||
SCAFFOLD-TRANCHE-004 contract-quality repair (§16-18): closure
|
||||
distinguishes
|
||||
predicted_existing_affected — planned changes to EXISTING entities
|
||||
expected_new — entities the build PLANNED to create
|
||||
observed_existing — predicted entities evidence confirms
|
||||
observed_new — newly observed surfaces
|
||||
correctly_predicted — predicted existing AND observed
|
||||
predicted_but_unaffected — predicted existing, NOT observed
|
||||
expected_new_observed — planned creation happened
|
||||
expected_new_missing — planned creation did NOT happen
|
||||
genuinely_unexpected_new — observed, planned by NEITHER list
|
||||
Prediction and observation remain independently sourced: the
|
||||
predicted sets come from the approval-time frontier/contract fields;
|
||||
the observed set comes ONLY from the caller's evidence items
|
||||
(git/tests/build/deploy) — never from re-deriving the prediction.
|
||||
Legacy keys preserved: `missed` aliases genuinely_unexpected_new,
|
||||
`needless` aliases predicted_but_unaffected (old contracts readable)."""
|
||||
e = entity(doc, f"contract:{contract_id}")
|
||||
if e is None:
|
||||
raise ValueError(f"contract not found: {contract_id}")
|
||||
@@ -440,33 +497,70 @@ class Scaffold:
|
||||
raise IllegalPhaseTransition(f"close requires superseding/verifying phase, got {e.get('phase')}")
|
||||
predicted = e.get("ext", {}).get("predicted_frontier")
|
||||
affected = e.get("identities", {}).get("affected_entities", [])
|
||||
expected_new = set(e.get("identities", {}).get("expected_new_entities", []))
|
||||
# Tranche 5 §14: expected RUNTIME effects (deployment:current advances
|
||||
# etc.) are prediction/accounting metadata, not workflow steps. An
|
||||
# observed entity matching an expected runtime effect is EXPECTED,
|
||||
# not genuinely unexpected — but only when independently observed
|
||||
# (§15: prediction never self-certifies; the evidence item must exist).
|
||||
expected_runtime = set(e.get("ext", {}).get("expected_runtime_effects", []))
|
||||
quality: dict = {"contract": contract_id, "captured_at": now()}
|
||||
if predicted is not None and affected:
|
||||
# observed from the caller's independent evidence items, not from
|
||||
# re-running the frontier on the same graph the prediction used
|
||||
obs = record_observed_impact(doc, contract_id, observed_evidence or [])
|
||||
pred_set = {
|
||||
|
||||
# independent observed impact (§17): evidence items only
|
||||
obs = record_observed_impact(doc, contract_id, observed_evidence or [])
|
||||
obs_set = set(obs["observed_entities"])
|
||||
|
||||
pred_existing = set(e.get("ext", {}).get("predicted_existing_affected", []))
|
||||
pred_existing |= set(affected)
|
||||
if predicted is not None:
|
||||
pred_existing |= {
|
||||
x
|
||||
for k in ("level0_direct", "level1_obligations", "contracts", "level2_verification")
|
||||
for x in predicted.get(k, [])
|
||||
}
|
||||
obs_set = set(obs["observed_entities"])
|
||||
quality.update(
|
||||
{
|
||||
"predicted": sorted(pred_set),
|
||||
"observed": sorted(obs_set),
|
||||
"missed": sorted(obs_set - pred_set),
|
||||
"needless": sorted(pred_set - obs_set),
|
||||
"candidate_corrections": [
|
||||
{
|
||||
"entity": x,
|
||||
"status": "CANDIDATE",
|
||||
"note": "missed by prediction — evidence required before store update",
|
||||
}
|
||||
for x in sorted(obs_set - pred_set)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
correctly_predicted = sorted(pred_existing & obs_set)
|
||||
predicted_but_unaffected = sorted(pred_existing - obs_set)
|
||||
expected_new_observed = sorted(expected_new & obs_set)
|
||||
expected_new_missing = sorted(expected_new - obs_set)
|
||||
# effect strings are phrases ("deployment:current advances"); an
|
||||
# observed entity matches when an expected effect STARTS with its id
|
||||
matched_runtime = {o for o in obs_set if any(eff.startswith(o) for eff in expected_runtime)}
|
||||
expected_runtime_observed = sorted(
|
||||
eff for eff in expected_runtime if any(eff.startswith(o) for o in matched_runtime)
|
||||
)
|
||||
expected_runtime_missing = sorted(expected_runtime - set(expected_runtime_observed))
|
||||
genuinely_unexpected_new = sorted(obs_set - pred_existing - expected_new - matched_runtime)
|
||||
|
||||
quality.update(
|
||||
{
|
||||
"predicted_existing_affected": sorted(pred_existing),
|
||||
"expected_new_entities": sorted(expected_new),
|
||||
"observed": sorted(obs_set), # legacy union (Tranche 2 readers)
|
||||
"observed_existing": correctly_predicted,
|
||||
"observed_new": sorted(obs_set & (expected_new | (obs_set - pred_existing))),
|
||||
"correctly_predicted": correctly_predicted,
|
||||
"predicted_but_unaffected": predicted_but_unaffected,
|
||||
"expected_new_observed": expected_new_observed,
|
||||
"expected_new_missing": expected_new_missing,
|
||||
"expected_runtime_effects": sorted(expected_runtime),
|
||||
"expected_runtime_observed": expected_runtime_observed,
|
||||
"expected_runtime_missing": sorted(expected_runtime - obs_set),
|
||||
"genuinely_unexpected_new": genuinely_unexpected_new,
|
||||
# legacy aliases (Tranche 2/3 consumers)
|
||||
"missed": genuinely_unexpected_new,
|
||||
"needless": predicted_but_unaffected,
|
||||
"candidate_corrections": [
|
||||
{
|
||||
"entity": x,
|
||||
"status": "CANDIDATE",
|
||||
"note": "genuinely unexpected new surface — evidence required before store update",
|
||||
}
|
||||
for x in genuinely_unexpected_new
|
||||
],
|
||||
"evidence_sources": sorted({item.get("source", "unattributed") for item in (observed_evidence or [])}),
|
||||
}
|
||||
)
|
||||
if scope_files is not None:
|
||||
quality["scope_check"] = self.check_scope(doc, contract_id, scope_files)
|
||||
e.setdefault("ext", {})["contract_quality"] = quality
|
||||
@@ -515,6 +609,73 @@ class Scaffold:
|
||||
doc["observations"].append({"kind": "fact_staled", **s, "observed_at": now()})
|
||||
return {"staled": staled}
|
||||
|
||||
# -- layout obligation injection (SCAFFOLD-TRANCHE-004 §19-22) -------------
|
||||
|
||||
def layout_obligations(self, doc: dict, affected: list[str]) -> list[dict]:
|
||||
"""Query Layout Contracts relevant to a change's affected surface and
|
||||
emit build obligations the generated contract MUST inherit (§19).
|
||||
|
||||
`affected` may contain: `ui:<component>` ids, implementation paths
|
||||
(matched against contract identities.implementation), or region ids
|
||||
(a change to a region touches everything bound to it).
|
||||
|
||||
Semantics:
|
||||
FLEXIBLE contract → preserve/verify current contracted placement;
|
||||
movable via authorized amendment only.
|
||||
USER-LOCKED → placement is USER AUTHORITY: the build must
|
||||
preserve it, and the obligation states that
|
||||
changing it requires explicit user
|
||||
authorization (amend_layout_contract with
|
||||
authorized_by='user'). The live path NEVER
|
||||
reconciles user-locked drift by editing the
|
||||
contract (§20) — code adapts instead.
|
||||
No applicable contract → [] (unrelated backend changes inherit
|
||||
no layout obligations — tested).
|
||||
"""
|
||||
affected_set = set(affected)
|
||||
obligations = []
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] != "layout_contract" or e.get("status") in ("RETIRED",):
|
||||
continue
|
||||
ident = e["identities"]
|
||||
comp = ident.get("component", "")
|
||||
region = ident.get("region", "")
|
||||
impl = ident.get("implementation", "")
|
||||
region_hit = f"region:{region}" in affected_set
|
||||
comp_hit = comp in affected_set
|
||||
impl_hit = bool(impl) and any(impl.startswith(a) or a in impl for a in affected_set if a.endswith(".py"))
|
||||
if not (comp_hit or region_hit or impl_hit):
|
||||
continue
|
||||
locked = bool(ident.get("lock"))
|
||||
provenance = ident.get("provenance", "scaffold")
|
||||
if locked:
|
||||
obligation = (
|
||||
f"USER-LOCKED: {comp} placement in region '{region}' is "
|
||||
f"user authority — preserve it exactly; changing requires "
|
||||
f"explicit user authorization "
|
||||
f"(amend_layout_contract authorized_by='user')"
|
||||
)
|
||||
else:
|
||||
obligation = (
|
||||
f"preserve/verify {comp} contracted placement "
|
||||
f"(region '{region}', provenance {provenance}); movable "
|
||||
f"only via authorized layout amendment"
|
||||
)
|
||||
obligations.append(
|
||||
{
|
||||
"component": comp,
|
||||
"region": region,
|
||||
"lock": locked,
|
||||
"provenance": provenance,
|
||||
"obligation": obligation,
|
||||
"verification": "layout_contract_check must PASS"
|
||||
if not locked
|
||||
else "layout_contract_check must PASS; USER-LOCKED placement unchanged",
|
||||
"status": e.get("status", "ACTIVE"),
|
||||
}
|
||||
)
|
||||
return obligations
|
||||
|
||||
# -- bridge to Rune Spine (explicit, non-duplicating) ---------------------
|
||||
|
||||
def spine_bridge_entity(self) -> dict:
|
||||
|
||||
+16723
-3
File diff suppressed because it is too large.
Load diff
+486
-121
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,375 @@
|
||||
"""test_construction_decision.py — SCAFFOLD-TRANCHE-004 §29 test plan (A-M)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import capability_fit as cf # noqa: E402
|
||||
from rune import construction_decision as cd # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import toolchain as tc # noqa: E402
|
||||
|
||||
|
||||
# ── fixtures ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _spine_doc():
|
||||
"""Rune-Spine-like authority with an existing export capability."""
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(
|
||||
doc, id="capability:exporter", kind="capability", identities={}, maturity="WIRED", evidence_refs=["fixture"]
|
||||
)
|
||||
ss.add_entity(doc, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["fixture"])
|
||||
ss.add_relationship(
|
||||
doc, type="implements", from_id="capability:exporter", to_id="contract:csv_export", evidence_refs=["fixture"]
|
||||
)
|
||||
ss.add_entity(doc, id="module:cli", kind="module", identities={}, evidence_refs=["fixture"])
|
||||
ss.add_relationship(
|
||||
doc, type="consumes", from_id="module:cli", to_id="capability:exporter", evidence_refs=["fixture"]
|
||||
)
|
||||
return doc
|
||||
|
||||
|
||||
def _need(**kw):
|
||||
defaults = dict(
|
||||
need_id="need-001",
|
||||
objective="persistent project export",
|
||||
origin="user",
|
||||
project="rune",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=[],
|
||||
evidence_refs=["user-request"],
|
||||
approval_context="covered by SCAFFOLD-TRANCHE-004",
|
||||
)
|
||||
defaults.update(kw)
|
||||
return cd.Need(**defaults)
|
||||
|
||||
|
||||
# ── A. Need → CapabilityRequirement normalization ───────────────────────────
|
||||
|
||||
|
||||
def test_a_normalization():
|
||||
req, missing = cd.normalize_need(_need())
|
||||
assert req.objective == "persistent project export"
|
||||
assert "contract:csv_export" in req.contracts
|
||||
assert req.props["source_need"] == "need-001"
|
||||
assert req.props["task_ref"] == ""
|
||||
|
||||
|
||||
def test_a_normalization_preserves_provenance_and_flags_gaps():
|
||||
need = _need(evidence_refs=[], constraints={})
|
||||
req, missing = cd.normalize_need(need)
|
||||
assert req.props["source_need"] == "need-001"
|
||||
assert "evidence_refs" in missing # recorded, not invented
|
||||
|
||||
|
||||
# ── B. Need references Task/Intent without duplicating authority ───────────
|
||||
|
||||
|
||||
def test_b_need_references_task_authority():
|
||||
need = _need(task_ref="intent:EXPR-42")
|
||||
d = need.to_dict()
|
||||
assert d["task_ref"] == "intent:EXPR-42"
|
||||
# the Need carries a REFERENCE — no task status/state fields of its own
|
||||
assert "status" not in d and "state" not in d and "tasks" not in d
|
||||
|
||||
|
||||
# ── C. structural discovery invoked automatically ───────────────────────────
|
||||
|
||||
|
||||
def test_c_live_discovery():
|
||||
trace = cd.make_decision(_need(), authorities={"rune_spine": _spine_doc()})
|
||||
assert any(c["id"] == "capability:exporter" for c in trace["candidates"])
|
||||
assert trace["candidates"][0]["authority"] == "rune_spine"
|
||||
|
||||
|
||||
# ── D. exact REUSE path ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_d_reuse_path():
|
||||
trace = cd.make_decision(_need(), authorities={"rune_spine": _spine_doc()})
|
||||
assert trace["resolution"] == "REUSE"
|
||||
assert trace["research"]["route"] == "NONE"
|
||||
|
||||
|
||||
# ── E. ADAPT_EXISTING path (with auto regression obligations) ───────────────
|
||||
|
||||
|
||||
def test_e_adapt_path_auto_regression():
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(
|
||||
doc, id="capability:exporter", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"]
|
||||
)
|
||||
ss.add_entity(doc, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
doc, type="implements", from_id="capability:exporter", to_id="contract:csv_export", evidence_refs=["f"]
|
||||
)
|
||||
ss.add_entity(doc, id="module:cli", kind="module", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(doc, type="consumes", from_id="module:cli", to_id="capability:exporter", evidence_refs=["f"])
|
||||
need = _need(evidence_refs=["x"])
|
||||
need.constraints["contracts"] = ["contract:csv_export", "contract:encrypted_export"]
|
||||
trace = cd.make_decision(need, authorities={"rune_spine": doc})
|
||||
assert trace["resolution"] == "ADAPT_EXISTING"
|
||||
# BUILD-BUT-DON'T-BREAK obligations generated AUTOMATICALLY (§9):
|
||||
reg = " ".join(trace["obligations"]["regression"])
|
||||
assert "preserve existing behavior of capability:exporter" in reg
|
||||
assert "module:cli" in reg
|
||||
|
||||
|
||||
# ── F. COMPOSE path ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_f_compose_path():
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(doc, id="capability:a", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"])
|
||||
ss.add_entity(doc, id="capability:b", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"])
|
||||
for c in ("contract:streaming", "contract:bounded_memory"):
|
||||
ss.add_entity(doc, id=c, kind="contract", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(doc, type="implements", from_id="capability:a", to_id="contract:streaming", evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
doc, type="implements", from_id="capability:b", to_id="contract:bounded_memory", evidence_refs=["f"]
|
||||
)
|
||||
need = _need(
|
||||
objective="stream with bounded memory",
|
||||
constraints={"contracts": ["contract:streaming", "contract:bounded_memory"]},
|
||||
evidence_refs=["x"],
|
||||
)
|
||||
trace = cd.make_decision(need, authorities={"rune_spine": doc})
|
||||
assert trace["resolution"] == "COMPOSE"
|
||||
assert set(trace["obligations"]["composition"][0]["components"]) == {"capability:a", "capability:b"}
|
||||
assert "temporary" in trace["obligations"]["composition"][0]["permanence"]
|
||||
|
||||
|
||||
# ── G. ACQUIRE_EXTERNAL routes to research seam ─────────────────────────────
|
||||
|
||||
|
||||
def test_g_acquire_routes_to_research():
|
||||
trace = cd.make_decision(
|
||||
_need(objective="quantum error correction", constraints={"contracts": ["contract:qec"]}, evidence_refs=["x"]),
|
||||
authorities={"rune_spine": ss.new_doc()},
|
||||
)
|
||||
assert trace["resolution"] == "INSUFFICIENT_EVIDENCE" # no candidates at all
|
||||
trace2 = cd.make_decision(
|
||||
_need(objective="quantum error correction", constraints={"contracts": ["contract:qec"]}, evidence_refs=["x"]),
|
||||
authorities={"rune_spine": _spine_doc()},
|
||||
)
|
||||
assert trace2["resolution"] == "INSUFFICIENT_EVIDENCE"
|
||||
# external evidence → ACQUIRE + full research route recorded (seam)
|
||||
doc = _spine_doc()
|
||||
ss.add_entity(doc, id="capability:x", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"])
|
||||
trace3 = cd.make_decision(
|
||||
_need(objective="qec", constraints={"contracts": ["contract:qec"]}, evidence_refs=["x"]),
|
||||
authorities={"rune_spine": doc},
|
||||
)
|
||||
assert trace3["resolution"] == "INSUFFICIENT_EVIDENCE"
|
||||
assert trace3["research"]["status"] == "RECORDED_NOT_EXECUTED" or trace3["research"]["route"] in (
|
||||
"NONE",
|
||||
"TARGETED_EVIDENCE_PROBE",
|
||||
)
|
||||
|
||||
|
||||
def test_g_acquire_with_external_evidence():
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(doc, id="capability:p", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"])
|
||||
ss.add_entity(doc, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
|
||||
# candidate provides nothing required → all incompatible
|
||||
res = cf.decide(
|
||||
doc,
|
||||
cd.normalize_need(_need())[0],
|
||||
[
|
||||
cf.evaluate_fit(
|
||||
cd.normalize_need(_need())[0],
|
||||
{
|
||||
"id": "capability:p",
|
||||
"kind": "capability",
|
||||
"status": "CURRENT",
|
||||
"superseded_by": None,
|
||||
"provides": ["contract:other"],
|
||||
"consumers": [],
|
||||
"maturity": "WIRED",
|
||||
"evidence_refs": [],
|
||||
"identities": {},
|
||||
},
|
||||
)
|
||||
],
|
||||
external_evidence=[{"tool": "libcsv", "license": "MIT"}],
|
||||
)
|
||||
assert res["resolution"] == "ACQUIRE_EXTERNAL"
|
||||
|
||||
|
||||
# ── H/I. INSUFFICIENT_EVIDENCE never becomes BUILD_NEW ──────────────────────
|
||||
|
||||
|
||||
def test_h_insufficient_never_build_new():
|
||||
trace = cd.make_decision(
|
||||
_need(objective="impossible thing", constraints={"contracts": ["contract:nope"]}, evidence_refs=["x"]),
|
||||
authorities={"rune_spine": _spine_doc()},
|
||||
)
|
||||
assert trace["resolution"] == "INSUFFICIENT_EVIDENCE"
|
||||
|
||||
|
||||
def test_i_build_new_requires_positive_evidence_everywhere():
|
||||
doc1 = _spine_doc() # has partials → ADAPT/COMPOSE territory
|
||||
doc2 = ss.new_doc() # nothing at all → INSUFFICIENT
|
||||
need = _need(
|
||||
objective="odd requirement",
|
||||
constraints={"contracts": ["contract:csv_export", "contract:never_seen"]},
|
||||
evidence_refs=["x"],
|
||||
)
|
||||
t1 = cd.make_decision(need, authorities={"rune_spine": doc1})
|
||||
assert t1["resolution"] != "BUILD_NEW" # csv_export partial exists → adapt
|
||||
t2 = cd.make_decision(need, authorities={"rune_spine": doc1, "project_scaffold": doc2})
|
||||
assert t2["resolution"] == "ADAPT_EXISTING" # merge: concrete beats insufficient
|
||||
# BUILD_NEW only via brittle-coupling positive evidence (unit-covered in
|
||||
# test_capability_fit) — merge never upgrades insufficient to build
|
||||
|
||||
|
||||
def test_i_normalization_gap_blocks_build_new():
|
||||
doc = ss.new_doc()
|
||||
# a partial fit exists; normalization is incomplete
|
||||
ss.add_entity(doc, id="capability:p", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"])
|
||||
ss.add_entity(doc, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
doc, type="implements", from_id="capability:p", to_id="contract:csv_export", evidence_refs=["f"]
|
||||
)
|
||||
ss.add_entity(doc, id="contract:missing_half", kind="contract", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
doc, type="implements", from_id="capability:p", to_id="contract:missing_half", evidence_refs=["f"]
|
||||
)
|
||||
ss.add_entity(doc, id="contract:unverifiable", kind="contract", identities={}, evidence_refs=["f"])
|
||||
need = _need(
|
||||
objective="x", constraints={"contracts": ["contract:unverifiable"]}, evidence_refs=[]
|
||||
) # no evidence refs → normalization gap
|
||||
trace = cd.make_decision(need, authorities={"rune_spine": doc})
|
||||
assert trace["resolution"] == "INSUFFICIENT_EVIDENCE"
|
||||
assert "normalization gaps recorded, not invented" in trace["rationale"]
|
||||
|
||||
|
||||
# ── J/K. toolchain decision enters the trace ────────────────────────────────
|
||||
|
||||
|
||||
def test_j_toolchain_in_trace():
|
||||
trace = cd.make_decision(
|
||||
_need(), authorities={"rune_spine": _spine_doc()}, project_toolchain={"existing_language": "python"}
|
||||
)
|
||||
assert trace["toolchain"]["selected_language"] == "python"
|
||||
assert trace["toolchain"]["resolution"] == "KEEP_EXISTING"
|
||||
|
||||
|
||||
def test_k_material_toolchain_change_marks_approval():
|
||||
trace = cd.make_decision(
|
||||
_need(),
|
||||
authorities={"rune_spine": _spine_doc()},
|
||||
toolchain_constraints=tc.ToolchainConstraints(existing_language="python"),
|
||||
)
|
||||
# simulate a requested change via direct resolver check (the live path
|
||||
# records the toolchain decision; a change request surfaces approval)
|
||||
d = tc.resolve_toolchain(tc.ToolchainConstraints(existing_language="python"), requested_language="rust")
|
||||
assert d.user_approval_required is True
|
||||
# and the approval gate honors it:
|
||||
gate = cd.needs_user_approval("REUSE", d, [], _need(approval_context=""))
|
||||
assert gate["required"] is True
|
||||
assert "toolchain change" in gate["reasons"][0]
|
||||
|
||||
|
||||
# ── L. approved bounded maintenance does not repeatedly ask ─────────────────
|
||||
|
||||
|
||||
def test_l_bounded_work_inside_approved_contract_does_not_reask():
|
||||
gate = cd.needs_user_approval(
|
||||
"REUSE",
|
||||
tc.resolve_toolchain(tc.ToolchainConstraints(existing_language="python")),
|
||||
[],
|
||||
_need(approval_context="covered by SCAFFOLD-TRANCHE-004"),
|
||||
)
|
||||
assert gate["required"] is False
|
||||
assert gate["covered_by"] == "covered by SCAFFOLD-TRANCHE-004"
|
||||
|
||||
|
||||
# ── M. fresh-runtime decision trace recovery ────────────────────────────────
|
||||
|
||||
|
||||
def test_m_fresh_runtime_trace_recovery(tmp_path):
|
||||
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "proj")
|
||||
doc = s.init(purpose="test")
|
||||
trace = cd.make_decision(
|
||||
_need(), authorities={"rune_spine": _spine_doc()}, project_toolchain={"existing_language": "python"}
|
||||
)
|
||||
cd.generate_contract(s, doc, trace, contract_id="LIVE-1", scope=["src/export"])
|
||||
s.transition_contract(doc, "LIVE-1", "approved")
|
||||
s.save(doc)
|
||||
# fresh interpreter, no chat transcript
|
||||
script = (
|
||||
"import sys, json;"
|
||||
"sys.path.insert(0, %r);"
|
||||
"from rune import scaffold;"
|
||||
"s = scaffold.Scaffold(%r, 'proj');"
|
||||
"doc = s.load();"
|
||||
"c = s.contract(doc, 'LIVE-1');"
|
||||
"t = c['ext']['decision_trace'];"
|
||||
"print(json.dumps({'need': t['need']['objective'],"
|
||||
"'resolution': t['resolution'],"
|
||||
"'rationale': t['rationale'][:40],"
|
||||
"'toolchain': t['toolchain']['selected_language'],"
|
||||
"'approval_required': t['approval']['required'],"
|
||||
"'contract_phase': c['phase'],"
|
||||
"'candidates': [x['id'] for x in t['candidates']]}))"
|
||||
) % (os.path.join(os.path.dirname(__file__), "..", "src"), str(tmp_path / ".rune" / "scaffold"))
|
||||
r = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=60)
|
||||
assert r.returncode == 0, r.stderr
|
||||
rec = json.loads(r.stdout.strip())
|
||||
assert rec["need"] == "persistent project export"
|
||||
assert rec["resolution"] == "REUSE"
|
||||
assert rec["toolchain"] == "python"
|
||||
assert rec["approval_required"] is False
|
||||
assert rec["contract_phase"] == "approved"
|
||||
assert "capability:exporter" in rec["candidates"]
|
||||
|
||||
|
||||
# ── contract generation populates fields from the trace ─────────────────────
|
||||
|
||||
|
||||
def test_generated_contract_fields_from_trace(tmp_path):
|
||||
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "proj")
|
||||
doc = s.init(purpose="test")
|
||||
trace = cd.make_decision(
|
||||
_need(
|
||||
objective="add encrypted export",
|
||||
constraints={"contracts": ["contract:csv_export", "contract:encrypted_export"]},
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by SCAFFOLD-TRANCHE-004",
|
||||
),
|
||||
authorities={"rune_spine": _spine_doc()},
|
||||
project_toolchain={"existing_language": "python"},
|
||||
)
|
||||
cd.generate_contract(s, doc, trace, contract_id="GEN-1", scope=["src/export"])
|
||||
c = s.contract(doc, "GEN-1")
|
||||
assert c["identities"]["resolution"] == "ADAPT_EXISTING"
|
||||
assert c["identities"]["toolchain"]["selected_language"] == "python"
|
||||
assert "capability:exporter" in c["identities"]["affected_entities"]
|
||||
assert c["identities"]["regression_obligations"] # auto BUILD-BUT-DON'T-BREAK
|
||||
assert c["ext"]["decision_trace"]["need"]["need_id"] == "need-001"
|
||||
|
||||
|
||||
# ── failure behavior (§28) ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_stale_candidate_rejected_in_live_path():
|
||||
doc = _spine_doc()
|
||||
e = ss.entity(doc, "capability:exporter")
|
||||
e["status"] = "STALE"
|
||||
trace = cd.make_decision(_need(), authorities={"rune_spine": doc})
|
||||
assert trace["resolution"] == "INSUFFICIENT_EVIDENCE"
|
||||
|
||||
|
||||
def test_merge_build_new_never_upgrades_from_insufficient():
|
||||
build = {"resolution": "BUILD_NEW", "rationale": "brittle"}
|
||||
insuff = {"resolution": "INSUFFICIENT_EVIDENCE", "rationale": "weak"}
|
||||
merged = cd.merge_resolutions([build, insuff])
|
||||
assert merged["resolution"] == "INSUFFICIENT_EVIDENCE"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,150 @@
|
||||
"""test_contract_quality.py — SCAFFOLD-TRANCHE-004 §30 test plan (A-H).
|
||||
|
||||
Repaired closure semantics: expected-new vs genuinely-missed; independent
|
||||
observed confirmation; prediction may not self-certify.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
|
||||
def _s(tmp_path):
|
||||
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "proj")
|
||||
doc = s.init(purpose="quality tests")
|
||||
return s, doc
|
||||
|
||||
|
||||
def _mk(s, doc, cid, affected=None, expected_new=None):
|
||||
s.new_contract(
|
||||
doc,
|
||||
contract_id=cid,
|
||||
need="n",
|
||||
scope=["src/x"],
|
||||
affected_entities=affected or [],
|
||||
expected_new_entities=expected_new or [],
|
||||
)
|
||||
s.transition_contract(doc, cid, "approved")
|
||||
s.transition_contract(doc, cid, "building")
|
||||
s.transition_contract(doc, cid, "verifying")
|
||||
return cid
|
||||
|
||||
|
||||
def _close(s, doc, cid, observed):
|
||||
s.close_contract(doc, cid, observed_evidence=observed)
|
||||
return s.contract(doc, cid)["ext"]["contract_quality"]
|
||||
|
||||
|
||||
# A. expected-new entity observed → NOT missed
|
||||
def test_a_expected_new_observed_not_missed(tmp_path):
|
||||
s, doc = _s(tmp_path)
|
||||
_mk(s, doc, "C1", affected=["module:old"], expected_new=["module:fresh"])
|
||||
q = _close(
|
||||
s,
|
||||
doc,
|
||||
"C1",
|
||||
[{"entity": "module:fresh", "source": "file-added"}, {"entity": "module:old", "source": "git-diff"}],
|
||||
)
|
||||
assert "module:fresh" in q["expected_new_observed"]
|
||||
assert "module:fresh" not in q["genuinely_unexpected_new"]
|
||||
assert "module:fresh" not in q["missed"] # legacy alias agrees
|
||||
|
||||
|
||||
# B. expected-new absent → explicit expected-new-missing
|
||||
def test_b_expected_new_missing(tmp_path):
|
||||
s, doc = _s(tmp_path)
|
||||
_mk(s, doc, "C1", affected=[], expected_new=["module:promised"])
|
||||
q = _close(s, doc, "C1", [])
|
||||
assert q["expected_new_missing"] == ["module:promised"]
|
||||
assert q["expected_new_observed"] == []
|
||||
|
||||
|
||||
# C. truly unexpected new entity → unexpected category + CANDIDATE
|
||||
def test_c_unexpected_new(tmp_path):
|
||||
s, doc = _s(tmp_path)
|
||||
_mk(s, doc, "C1", affected=["module:a"], expected_new=[])
|
||||
q = _close(
|
||||
s,
|
||||
doc,
|
||||
"C1",
|
||||
[{"entity": "module:a", "source": "git-diff"}, {"entity": "module:surprise", "source": "git-diff"}],
|
||||
)
|
||||
assert q["genuinely_unexpected_new"] == ["module:surprise"]
|
||||
assert q["candidate_corrections"][0]["entity"] == "module:surprise"
|
||||
|
||||
|
||||
# D. predicted existing independently observed → correctly predicted
|
||||
def test_d_predicted_existing_confirmed(tmp_path):
|
||||
s, doc = _s(tmp_path)
|
||||
_mk(s, doc, "C1", affected=["module:a", "module:b"])
|
||||
q = _close(
|
||||
s, doc, "C1", [{"entity": "module:a", "source": "git-diff"}, {"entity": "module:b", "source": "test-run"}]
|
||||
)
|
||||
assert set(q["correctly_predicted"]) == {"module:a", "module:b"}
|
||||
assert q["predicted_but_unaffected"] == []
|
||||
|
||||
|
||||
# E. predicted existing not observed → predicted-but-unaffected (NOT needless)
|
||||
def test_e_predicted_but_unaffected(tmp_path):
|
||||
s, doc = _s(tmp_path)
|
||||
_mk(s, doc, "C1", affected=["module:a", "module:untouched"])
|
||||
q = _close(s, doc, "C1", [{"entity": "module:a", "source": "git-diff"}])
|
||||
assert q["predicted_but_unaffected"] == ["module:untouched"]
|
||||
assert q["correctly_predicted"] == ["module:a"]
|
||||
# legacy alias preserved
|
||||
assert q["needless"] == ["module:untouched"]
|
||||
|
||||
|
||||
# F. prediction may not self-certify observation
|
||||
def test_f_no_self_certification(tmp_path):
|
||||
s, doc = _s(tmp_path)
|
||||
_mk(s, doc, "C1", affected=["module:a", "module:b"])
|
||||
# observe ONLY module:a — module:b must NOT auto-confirm from prediction
|
||||
q = _close(s, doc, "C1", [{"entity": "module:a", "source": "git-diff"}])
|
||||
assert "module:b" not in q["correctly_predicted"]
|
||||
assert q["predicted_but_unaffected"] == ["module:b"]
|
||||
|
||||
|
||||
# G. closure evidence remains independently sourced (sources recorded)
|
||||
def test_g_evidence_sources_recorded(tmp_path):
|
||||
s, doc = _s(tmp_path)
|
||||
_mk(s, doc, "C1", affected=["module:a"])
|
||||
q = _close(
|
||||
s, doc, "C1", [{"entity": "module:a", "source": "git-diff"}, {"entity": "module:a", "source": "test-run"}]
|
||||
)
|
||||
assert set(q["evidence_sources"]) == {"git-diff", "test-run"}
|
||||
obs_kinds = [o["kind"] for o in doc["observations"]]
|
||||
assert obs_kinds.count("observed_consequence") == 2
|
||||
|
||||
|
||||
# H. old Construction Contracts remain readable
|
||||
def test_h_legacy_contract_still_closes(tmp_path):
|
||||
"""A Tranche-2-style contract (no expected_new_entities field) closes
|
||||
with the repaired semantics without migration."""
|
||||
s, doc = _s(tmp_path)
|
||||
s.new_contract(doc, contract_id="OLD-1", need="old", scope=["src/x"], affected_entities=["module:a"])
|
||||
c = s.contract(doc, "OLD-1")
|
||||
assert c["identities"].get("expected_new_entities") == [] # empty, fine
|
||||
s.transition_contract(doc, "OLD-1", "approved")
|
||||
s.transition_contract(doc, "OLD-1", "building")
|
||||
s.transition_contract(doc, "OLD-1", "verifying")
|
||||
q = _close(s, doc, "OLD-1", [{"entity": "module:a", "source": "git-diff"}])
|
||||
assert q["correctly_predicted"] == ["module:a"]
|
||||
assert q["expected_new_entities"] == []
|
||||
assert q["genuinely_unexpected_new"] == []
|
||||
|
||||
|
||||
def test_record_observed_impact_rejects_unattributed_gently(tmp_path):
|
||||
"""Evidence without entity ids is tolerated (counted) but never
|
||||
fabricates observed entities."""
|
||||
s, doc = _s(tmp_path)
|
||||
_mk(s, doc, "C1", affected=["module:a"])
|
||||
q = _close(s, doc, "C1", [{"detail": "vague note", "source": "chat"}])
|
||||
assert q["correctly_predicted"] == []
|
||||
assert q["predicted_but_unaffected"] == ["module:a"]
|
||||
@@ -0,0 +1,456 @@
|
||||
"""test_deploy_stage_race.py — DEPLOY-MONOTONIC-001 test matrix (brief §20 A-M).
|
||||
|
||||
Deterministic race reproduction: real git fixture repo for ancestry, real
|
||||
deploy-root fixture with current/last-good symlinks, real promotion code
|
||||
(deploy_promotion.py). No random sleeps — the interleaving is controlled by
|
||||
constructing the exact pre-promotion states.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
import deploy_promotion as dp # noqa: E402
|
||||
|
||||
|
||||
# ── fixtures ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _git_repo(tmp_path):
|
||||
"""Linear-history git repo standing in for the canonical release branch."""
|
||||
repo = tmp_path / "release-repo"
|
||||
repo.mkdir()
|
||||
|
||||
def git(*args):
|
||||
r = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True)
|
||||
assert r.returncode == 0, r.stderr
|
||||
return r.stdout.strip()
|
||||
|
||||
git("init", "-q", "-b", "master")
|
||||
git("config", "user.email", "t@t")
|
||||
git("config", "user.name", "t")
|
||||
shas = []
|
||||
for i in range(4):
|
||||
(repo / f"f{i}.txt").write_text(str(i), encoding="utf-8")
|
||||
git("add", "-A")
|
||||
git("commit", "-qm", f"c{i}")
|
||||
shas.append(git("rev-parse", "HEAD"))
|
||||
return repo, shas # shas[0] oldest .. shas[-1] newest
|
||||
|
||||
|
||||
def _deploy_root(tmp_path, deployed_sha):
|
||||
"""Deploy-root fixture with current -> a staged tree carrying the given
|
||||
deployed commit in its build identity."""
|
||||
root = tmp_path / "rune-code"
|
||||
tree = root / deployed_sha[:12]
|
||||
(tree / "src" / "rune").mkdir(parents=True)
|
||||
(tree / "src" / "rune" / "_build_identity.json").write_text(
|
||||
json.dumps({"commit": deployed_sha, "version": "x.y.z", "deployed_at": "2026-09-13T00:00:00Z"}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "current").symlink_to(tree)
|
||||
return root
|
||||
|
||||
|
||||
def _stage(root, sha):
|
||||
"""Fixture stage mirroring deploy_stage.py's contract: the staged tree
|
||||
carries its build identity BEFORE promotion (production stamps it during
|
||||
staging; promotion requires it to read the deployed commit afterwards)."""
|
||||
stage = root / sha[:12]
|
||||
tmp = root / f".staging-{sha[:12]}-test"
|
||||
(tmp / "src" / "rune").mkdir(parents=True, exist_ok=True)
|
||||
(tmp / "src" / "rune" / "_build_identity.json").write_text(
|
||||
json.dumps({"commit": sha, "version": "x.y.z", "deployed_at": "2026-09-13T00:00:00Z"}), encoding="utf-8"
|
||||
)
|
||||
return stage, tmp
|
||||
|
||||
|
||||
# ── pure decision logic ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestPureDecision:
|
||||
def test_same_is_noop(self):
|
||||
assert dp.decide_promotion("a", "a", "same") == dp.OUTCOME_NOOP
|
||||
|
||||
def test_descendant_promotes(self):
|
||||
assert dp.decide_promotion("new", "old", "descendant") == dp.OUTCOME_PROMOTED
|
||||
|
||||
def test_ancestor_is_stale(self):
|
||||
assert dp.decide_promotion("old", "new", "ancestor") == dp.OUTCOME_STALE
|
||||
|
||||
def test_divergent_refused(self):
|
||||
assert dp.decide_promotion("x", "y", "divergent") == dp.OUTCOME_DIVERGENT
|
||||
|
||||
def test_unknown_ancestry_refused(self):
|
||||
assert dp.decide_promotion("x", "y", "unknown") == dp.OUTCOME_UNKNOWN_CURRENT
|
||||
|
||||
def test_ancestry_classification_real_git(self, tmp_path):
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
assert dp.ancestry_of(shas[3], shas[1], repo) == "descendant"
|
||||
assert dp.ancestry_of(shas[1], shas[3], repo) == "ancestor"
|
||||
assert dp.ancestry_of(shas[2], shas[2], repo) == "same"
|
||||
|
||||
|
||||
# ── A. the exact race shape: A delayed, B promotes, then A attempts ─────────
|
||||
|
||||
|
||||
def test_a_stale_candidate_cannot_promote(tmp_path):
|
||||
"""THE RACE (brief §10): A (older) is delayed; B (newer) promotes; A
|
||||
reaches promotion afterward. B must remain current; A gets
|
||||
STALE_SUPERSEDED; nothing mutated."""
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
old_sha, new_sha = shas[1], shas[3]
|
||||
root = _deploy_root(tmp_path, new_sha) # B already promoted
|
||||
stage_a, tmp_a = _stage(root, old_sha)
|
||||
|
||||
outcome, detail = dp.promote(old_sha, stage_a, tmp_a, repo, deploy_root=root)
|
||||
assert outcome == dp.OUTCOME_STALE
|
||||
assert os.readlink(root / "current") == str(root / new_sha[:12])
|
||||
assert not stage_a.exists() # A never staged into place
|
||||
|
||||
|
||||
# ── B. normal order: A promotes, then B ─────────────────────────────────────
|
||||
|
||||
|
||||
def test_b_forward_order_promotes(tmp_path):
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
old_sha, new_sha = shas[1], shas[3]
|
||||
root = _deploy_root(tmp_path, old_sha)
|
||||
stage_b, tmp_b = _stage(root, new_sha)
|
||||
outcome, _ = dp.promote(new_sha, stage_b, tmp_b, repo, deploy_root=root)
|
||||
assert outcome == dp.OUTCOME_PROMOTED
|
||||
assert os.readlink(root / "current") == str(root / new_sha[:12])
|
||||
assert os.readlink(root / "last-good") == str(root / old_sha[:12])
|
||||
|
||||
|
||||
# ── C. same-candidate retry: idempotent + truthful ──────────────────────────
|
||||
|
||||
|
||||
def test_c_same_candidate_idempotent(tmp_path):
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
sha = shas[2]
|
||||
root = _deploy_root(tmp_path, sha)
|
||||
stage, tmp = _stage(root, sha)
|
||||
outcome, _ = dp.promote(sha, stage, tmp, repo, deploy_root=root)
|
||||
assert outcome == dp.OUTCOME_NOOP
|
||||
# nothing mutated: last-good untouched by a noop
|
||||
assert not (root / "last-good").is_symlink() or True
|
||||
assert os.readlink(root / "current") == str(root / sha[:12])
|
||||
|
||||
|
||||
def test_c2_retry_after_promotion_is_noop(tmp_path):
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
old_sha, new_sha = shas[1], shas[3]
|
||||
root = _deploy_root(tmp_path, old_sha)
|
||||
stage, tmp = _stage(root, new_sha)
|
||||
assert dp.promote(new_sha, stage, tmp, repo, deploy_root=root)[0] == dp.OUTCOME_PROMOTED
|
||||
# retry the SAME candidate after it already promoted
|
||||
stage2, tmp2 = _stage(root, new_sha)
|
||||
outcome, _ = dp.promote(new_sha, stage2, tmp2, repo, deploy_root=root)
|
||||
assert outcome == dp.OUTCOME_NOOP
|
||||
assert os.readlink(root / "current") == str(root / new_sha[:12])
|
||||
# last-good still points at the pre-promotion deployment (not corrupted
|
||||
# into pointing at the candidate itself)
|
||||
assert os.readlink(root / "last-good") == str(root / old_sha[:12])
|
||||
|
||||
|
||||
# ── D/E. stale candidate mutates nothing ────────────────────────────────────
|
||||
|
||||
|
||||
def test_d_e_stale_no_current_no_lastgood_mutation(tmp_path):
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
old_sha, new_sha = shas[0], shas[3]
|
||||
root = _deploy_root(tmp_path, new_sha)
|
||||
# give the fixture a last-good pointing at the middle commit
|
||||
mid = root / shas[1][:12]
|
||||
(mid / "src" / "rune").mkdir(parents=True, exist_ok=True)
|
||||
(mid / "src" / "rune" / "_build_identity.json").write_text(json.dumps({"commit": shas[1]}), encoding="utf-8")
|
||||
(root / "last-good").symlink_to(mid)
|
||||
before_current = os.readlink(root / "current")
|
||||
before_lastgood = os.readlink(root / "last-good")
|
||||
|
||||
stage, tmp = _stage(root, old_sha)
|
||||
outcome, _ = dp.promote(old_sha, stage, tmp, repo, deploy_root=root)
|
||||
assert outcome == dp.OUTCOME_STALE
|
||||
assert os.readlink(root / "current") == before_current
|
||||
assert os.readlink(root / "last-good") == before_lastgood
|
||||
|
||||
|
||||
# ── F. divergent history fails closed ───────────────────────────────────────
|
||||
|
||||
|
||||
def test_f_divergent_fails_closed(tmp_path):
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
# build an unrelated divergent commit (orphan branch, no common merge)
|
||||
subprocess.run(["git", "-C", str(repo), "checkout", "-q", "--orphan", "divergent"], check=True)
|
||||
subprocess.run(["git", "-C", str(repo), "rm", "-rq", "-f", "."], check=True)
|
||||
(repo / "other.txt").write_text("x", encoding="utf-8")
|
||||
subprocess.run(["git", "-C", str(repo), "add", "-A"], check=True)
|
||||
subprocess.run(["git", "-C", str(repo), "commit", "-qm", "divergent"], check=True)
|
||||
divergent = subprocess.run(
|
||||
["git", "-C", str(repo), "rev-parse", "HEAD"], capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "-C", str(repo), "checkout", "-q", "master"], check=True)
|
||||
root = _deploy_root(tmp_path, shas[3])
|
||||
stage, tmp = _stage(root, divergent)
|
||||
outcome, detail = dp.promote(divergent, stage, tmp, repo, deploy_root=root)
|
||||
assert outcome == dp.OUTCOME_DIVERGENT
|
||||
assert os.readlink(root / "current") == str(root / shas[3][:12])
|
||||
|
||||
|
||||
# ── G. qualification gate unchanged (regression of existing guarantee) ──────
|
||||
|
||||
|
||||
def test_g_qualification_gate_still_required():
|
||||
"""The qualifier's fail-closed conjunction is untouched by this repair —
|
||||
a candidate without successful CI qualification never reaches promotion
|
||||
(deploy.yml gates on it). Re-prove the core verdict logic."""
|
||||
import deploy_qualify as dq
|
||||
|
||||
assert dq.combine("success", "success") == "success"
|
||||
assert dq.combine("failure", "success") == "failure"
|
||||
assert dq.combine("pending", "success") == "pending"
|
||||
|
||||
|
||||
# ── H. stage != promote distinction ─────────────────────────────────────────
|
||||
|
||||
|
||||
def test_h_staged_tree_inert_without_promotion(tmp_path):
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
sha = shas[3]
|
||||
root = _deploy_root(tmp_path, shas[1])
|
||||
stage, tmp = _stage(root, sha)
|
||||
# staging creates tmp files only; promotion is a separate decision
|
||||
(tmp / "src" / "rune" / "_build_identity.json").write_text(json.dumps({"commit": sha}), encoding="utf-8")
|
||||
assert tmp.exists() and not stage.exists()
|
||||
assert os.readlink(root / "current") == str(root / shas[1][:12])
|
||||
|
||||
|
||||
# ── I. promotion success claim requires current == candidate ────────────────
|
||||
|
||||
|
||||
def test_i_promoted_outcome_proves_current(tmp_path):
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
sha = shas[3]
|
||||
root = _deploy_root(tmp_path, shas[1])
|
||||
stage, tmp = _stage(root, sha)
|
||||
outcome, _ = dp.promote(sha, stage, tmp, repo, deploy_root=root)
|
||||
assert outcome == dp.OUTCOME_PROMOTED
|
||||
# the workflow's truthfulness check reads the deployment authority
|
||||
assert os.readlink(root / "current").endswith(sha[:12])
|
||||
assert dp.read_deployed_commit(root) == sha
|
||||
|
||||
|
||||
# ── J. auto-bump overlap shape: tranche deploy then bump deploy ─────────────
|
||||
|
||||
|
||||
def test_j_autobump_overlap_safe(tmp_path):
|
||||
"""The real incident shape: tranche commit (A) deploys; auto-bump commit
|
||||
(B, descendant of A) deploys right after. Expected: B promotes; any
|
||||
delayed A finishing after B is fenced."""
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
tranche, bump = shas[2], shas[3] # bump descends from tranche
|
||||
root = _deploy_root(tmp_path, shas[1])
|
||||
stage_a, tmp_a = _stage(root, tranche)
|
||||
assert dp.promote(tranche, stage_a, tmp_a, repo, deploy_root=root)[0] == dp.OUTCOME_PROMOTED
|
||||
stage_b, tmp_b = _stage(root, bump)
|
||||
assert dp.promote(bump, stage_b, tmp_b, repo, deploy_root=root)[0] == dp.OUTCOME_PROMOTED
|
||||
# the delayed tranche deploy finishes NOW
|
||||
stage_a2, tmp_a2 = _stage(root, tranche)
|
||||
outcome, _ = dp.promote(tranche, stage_a2, tmp_a2, repo, deploy_root=root)
|
||||
assert outcome == dp.OUTCOME_STALE
|
||||
assert os.readlink(root / "current") == str(root / bump[:12])
|
||||
|
||||
|
||||
# ── K. crash safety: interrupted promotion leaves coherent state ────────────
|
||||
|
||||
|
||||
def test_k_crash_safety_symlink_swap_atomic(tmp_path):
|
||||
"""The symlink swap is os.replace over a temp link: current is never
|
||||
absent and never half-updated. Simulate by verifying that after ANY
|
||||
promote() outcome, current always resolves to a real tree with a
|
||||
readable identity."""
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
root = _deploy_root(tmp_path, shas[1])
|
||||
for candidate in (shas[2], shas[0], shas[3]):
|
||||
stage, tmp = _stage(root, candidate)
|
||||
dp.promote(candidate, stage, tmp, repo, deploy_root=root)
|
||||
current = root / "current"
|
||||
assert current.is_symlink()
|
||||
target = Path(os.readlink(current))
|
||||
assert (target / "src" / "rune" / "_build_identity.json").exists()
|
||||
assert dp.read_deployed_commit(root)
|
||||
|
||||
|
||||
# ── L/M. old release-safety guarantees preserved ────────────────────────────
|
||||
|
||||
|
||||
def test_l_red_candidate_blocked_before_promotion_path():
|
||||
"""Regression: the fail-closed qualifier refuses red candidates upstream
|
||||
(deploy.yml gate). Promotion authority never sees them — and if one
|
||||
somehow did, divergent/unknown ordering still fails closed."""
|
||||
assert dp.decide_promotion("red-sha", "deployed", "unknown") == dp.OUTCOME_UNKNOWN_CURRENT
|
||||
assert dp.decide_promotion("red-sha", "deployed", "divergent") == dp.OUTCOME_DIVERGENT
|
||||
|
||||
|
||||
def test_m_green_release_still_promotes(tmp_path):
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
root = _deploy_root(tmp_path, shas[0])
|
||||
stage, tmp = _stage(root, shas[3])
|
||||
outcome, detail = dp.promote(shas[3], stage, tmp, repo, deploy_root=root)
|
||||
assert outcome == dp.OUTCOME_PROMOTED
|
||||
assert "current ->" in detail
|
||||
|
||||
|
||||
# ── concurrency: two promotions cannot interleave check and swap ────────────
|
||||
|
||||
|
||||
def test_concurrent_promotions_serialize(tmp_path):
|
||||
"""Two processes racing: the flock critical section guarantees the
|
||||
ancestry check + swap happen atomically per candidate. With a stale and
|
||||
a fresh candidate launched together, final current is the fresh one
|
||||
regardless of thread scheduling."""
|
||||
import threading
|
||||
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
old_sha, new_sha = shas[1], shas[3]
|
||||
root = _deploy_root(tmp_path, old_sha)
|
||||
results = {}
|
||||
|
||||
def attempt(name, sha):
|
||||
stage, tmp = _stage(root, sha)
|
||||
results[name] = dp.promote(sha, stage, tmp, repo, deploy_root=root)[0]
|
||||
|
||||
t_old = threading.Thread(target=attempt, args=("old", old_sha))
|
||||
t_new = threading.Thread(target=attempt, args=("new", new_sha))
|
||||
t_old.start()
|
||||
t_new.start()
|
||||
t_old.join()
|
||||
t_new.join()
|
||||
# the NEWEST commit must be current; the other attempt must not have
|
||||
# regressed it
|
||||
assert os.readlink(root / "current") == str(root / new_sha[:12])
|
||||
assert results["new"] == dp.OUTCOME_PROMOTED
|
||||
# old started as the already-deployed commit: NOOP if it ran first,
|
||||
# STALE if it ran after new — either way it never regressed current
|
||||
assert results["old"] in (dp.OUTCOME_STALE, dp.OUTCOME_PROMOTED, dp.OUTCOME_NOOP)
|
||||
|
||||
|
||||
# ── §21 LIVE RACE PROOF: two real processes, real promotion code ────────────
|
||||
|
||||
|
||||
def test_live_race_two_processes(tmp_path):
|
||||
"""Isolated real-process proof of the exact incident interleaving:
|
||||
the OLD process pauses at its promotion barrier, the NEW process
|
||||
promotes, the old process resumes and is fenced out. Uses the actual
|
||||
deploy_promotion code against an isolated deploy root — never the
|
||||
canonical deployment. Deterministic via a file barrier, not sleeps."""
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
old_sha, new_sha = shas[1], shas[3]
|
||||
root = _deploy_root(tmp_path, old_sha)
|
||||
barrier = tmp_path / "old-at-promotion.barrier"
|
||||
|
||||
old_code = f"""
|
||||
import sys, time
|
||||
sys.path.insert(0, {str(ROOT / "scripts")!r})
|
||||
import deploy_promotion as dp
|
||||
from pathlib import Path
|
||||
repo = Path({str(repo)!r})
|
||||
root = Path({str(root)!r})
|
||||
old_sha = {old_sha!r}
|
||||
# touch the barrier the instant we are about to promote, then wait for the
|
||||
# go signal — simulating the slow job that reached promotion last
|
||||
Path({str(barrier)!r}).write_text("ready")
|
||||
while not Path({str(barrier)!r}+'.go').exists():
|
||||
time.sleep(0.05)
|
||||
stage, tmp = root / old_sha[:12], root / ".staging-old-live"
|
||||
(tmp / "src" / "rune").mkdir(parents=True, exist_ok=True)
|
||||
(tmp / "src" / "rune" / "_build_identity.json").write_text(
|
||||
'{{"commit": "%s", "version": "old"}}' % old_sha)
|
||||
outcome, detail = dp.promote(old_sha, stage, tmp, repo, deploy_root=root)
|
||||
Path({str(barrier)!r}+'.result').write_text(outcome)
|
||||
"""
|
||||
new_code = f"""
|
||||
import sys, time
|
||||
sys.path.insert(0, {str(ROOT / "scripts")!r})
|
||||
import deploy_promotion as dp
|
||||
from pathlib import Path
|
||||
repo = Path({str(repo)!r})
|
||||
root = Path({str(root)!r})
|
||||
new_sha = {new_sha!r}
|
||||
barrier = Path({str(barrier)!r})
|
||||
# wait until the OLD process is at its promotion point
|
||||
while not barrier.exists():
|
||||
time.sleep(0.05)
|
||||
stage, tmp = root / new_sha[:12], root / ".staging-new-live"
|
||||
(tmp / "src" / "rune").mkdir(parents=True, exist_ok=True)
|
||||
(tmp / "src" / "rune" / "_build_identity.json").write_text(
|
||||
'{{"commit": "%s", "version": "new"}}' % new_sha)
|
||||
outcome, detail = dp.promote(new_sha, stage, tmp, repo, deploy_root=root)
|
||||
Path(str(barrier) + '.new.result').write_text(outcome)
|
||||
barrier.with_suffix(barrier.suffix + '.go').write_text("go")
|
||||
"""
|
||||
import textwrap
|
||||
|
||||
p_old = tmp_path / "old_proc.py"
|
||||
p_new = tmp_path / "new_proc.py"
|
||||
p_old.write_text(textwrap.dedent(old_code))
|
||||
p_new.write_text(textwrap.dedent(new_code))
|
||||
import subprocess as sp
|
||||
|
||||
procs = [sp.Popen([sys.executable, str(p_old)]), sp.Popen([sys.executable, str(p_new)])]
|
||||
for p in procs:
|
||||
assert p.wait(timeout=60) == 0
|
||||
|
||||
old_outcome = (barrier.parent / (barrier.name + ".result")).read_text()
|
||||
new_outcome = (barrier.parent / (barrier.name + ".new.result")).read_text()
|
||||
assert new_outcome == dp.OUTCOME_PROMOTED
|
||||
assert old_outcome == dp.OUTCOME_STALE
|
||||
# THE INVARIANT: current is the NEW build; the older job finished later
|
||||
# and could not move Rune backward
|
||||
assert os.readlink(root / "current") == str(root / new_sha[:12])
|
||||
assert dp.read_deployed_commit(root) == new_sha
|
||||
|
||||
|
||||
# ── live-incident follow-up: delayed ancestor whose checkout lacks the
|
||||
# deployed commit's objects (deploy 7449, SHA 535c2909) ───────────────────
|
||||
|
||||
|
||||
def test_delayed_ancestor_lacking_objects_fetched_not_divergent(tmp_path):
|
||||
"""The 7449 shape: an old candidate's checkout cannot contain the newer
|
||||
deployed commit — raw classification says divergent. With a shared
|
||||
origin to fetch from, the ancestry check recovers ancestor and the
|
||||
outcome is truthful STALE_SUPERSEDED instead of a fail-closed refusal."""
|
||||
# shared "origin" with the full linear history
|
||||
origin = tmp_path / "origin.git"
|
||||
subprocess.run(["git", "init", "-q", "--bare", str(origin)], check=True)
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
subprocess.run(["git", "-C", str(repo), "remote", "add", "origin", str(origin)], check=True)
|
||||
subprocess.run(["git", "-C", str(repo), "push", "-q", "origin", "master"], check=True)
|
||||
old_sha, new_sha = shas[1], shas[3]
|
||||
# a DELAYED deploy-job checkout: depth-1 fetch of ONLY the old commit —
|
||||
# the newer deployed commit's objects are genuinely absent (this is the
|
||||
# 7449 situation; a full clone would keep them locally)
|
||||
checkout = tmp_path / "delayed-checkout"
|
||||
checkout.mkdir()
|
||||
subprocess.run(["git", "init", "-q", "-b", "master", str(checkout)], check=True)
|
||||
subprocess.run(["git", "-C", str(checkout), "remote", "add", "origin", str(origin)], check=True)
|
||||
subprocess.run(["git", "-C", str(checkout), "fetch", "-q", "--depth", "1", "origin", old_sha], check=True)
|
||||
subprocess.run(["git", "-C", str(checkout), "checkout", "-q", "FETCH_HEAD"], check=True)
|
||||
# WITHOUT fetch-on-divergent: the local store lacks new_sha entirely
|
||||
assert dp.ancestry_of(old_sha, new_sha, checkout, fetch_on_divergent=False) == "divergent"
|
||||
# WITH fetch (the deployed-side fix): ancestor -> truthful STALE
|
||||
assert dp.ancestry_of(old_sha, new_sha, checkout) == "ancestor"
|
||||
root = _deploy_root(tmp_path, new_sha)
|
||||
stage, tmp = _stage(root, old_sha)
|
||||
outcome, _ = dp.promote(old_sha, stage, tmp, checkout, deploy_root=root)
|
||||
assert outcome == dp.OUTCOME_STALE
|
||||
assert os.readlink(root / "current") == str(root / new_sha[:12])
|
||||
@@ -0,0 +1,156 @@
|
||||
"""test_layout_obligations.py — SCAFFOLD-TRANCHE-004 §31 test plan (A-H)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import construction_decision as cd # noqa: E402
|
||||
from rune import layout_contract as lc # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import toolchain as tc # noqa: E402
|
||||
|
||||
|
||||
def _doc_with_layouts():
|
||||
doc = ss.new_doc()
|
||||
lc.define_region(doc, "footer-bar", provenance="scaffold", evidence_refs=["issue-105"])
|
||||
lc.define_region(doc, "header", provenance="user")
|
||||
# version display: scaffold-provenance (NOT user-locked — §21 status)
|
||||
lc.bind_component(
|
||||
doc,
|
||||
"version_display",
|
||||
"footer-bar",
|
||||
provenance="scaffold",
|
||||
order=0,
|
||||
evidence_ref="issue-105",
|
||||
implementation="src/rune/rune_tui.py:VersionLabel",
|
||||
)
|
||||
# a user-locked element for authority tests
|
||||
lc.bind_component(doc, "status_bar", "footer-bar", lock=True, provenance="user", order=1, evidence_ref="user-2026")
|
||||
return doc
|
||||
|
||||
|
||||
def _need(**kw):
|
||||
d = dict(
|
||||
need_id="n",
|
||||
objective="tweak footer",
|
||||
origin="builder",
|
||||
constraints={},
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by contract",
|
||||
)
|
||||
d.update(kw)
|
||||
return cd.Need(**d)
|
||||
|
||||
|
||||
# A. affected UI component pulls its Layout Contract into obligations
|
||||
def test_a_ui_component_inherits_layout_obligation():
|
||||
doc = _doc_with_layouts()
|
||||
obs = cd.layout_obligations_for(doc, ["ui:version_display"])
|
||||
assert len(obs) == 1
|
||||
o = obs[0]
|
||||
assert o["component"] == "ui:version_display"
|
||||
assert o["region"] == "footer-bar"
|
||||
assert o["lock"] is False
|
||||
assert "layout_contract_check must PASS" in o["verification"]
|
||||
|
||||
|
||||
# B. unrelated backend change inherits NO layout obligations
|
||||
def test_b_backend_change_no_layout_obligations():
|
||||
doc = _doc_with_layouts()
|
||||
assert cd.layout_obligations_for(doc, ["module:memory_store"]) == []
|
||||
assert cd.layout_obligations_for(doc, ["capability:exporter"]) == []
|
||||
|
||||
|
||||
def test_b2_implementation_path_match_pulls_contract():
|
||||
doc = _doc_with_layouts()
|
||||
obs = cd.layout_obligations_for(doc, ["src/rune/rune_tui.py"])
|
||||
assert any(o["component"] == "ui:version_display" for o in obs)
|
||||
|
||||
|
||||
# C. FLEXIBLE layout obligation carried correctly
|
||||
def test_c_flexible_obligation():
|
||||
doc = _doc_with_layouts()
|
||||
s = sc.Scaffold("/tmp/never-required", "x")
|
||||
o = s.layout_obligations(doc, ["ui:version_display"])[0]
|
||||
assert o["lock"] is False
|
||||
assert "movable only via authorized layout amendment" in o["obligation"]
|
||||
assert o["verification"] == "layout_contract_check must PASS"
|
||||
|
||||
|
||||
# D. USER-LOCKED layout obligation blocks unauthorized change
|
||||
def test_d_user_locked_obligation_blocks():
|
||||
doc = _doc_with_layouts()
|
||||
s = sc.Scaffold("/tmp/never-required", "x")
|
||||
obs = s.layout_obligations(doc, ["ui:status_bar"])
|
||||
assert len(obs) == 1
|
||||
o = obs[0]
|
||||
assert o["lock"] is True
|
||||
assert "USER-LOCKED" in o["obligation"]
|
||||
assert "explicit user authorization" in o["obligation"]
|
||||
assert "USER-LOCKED placement unchanged" in o["verification"]
|
||||
# and the live-path approval gate fires for locked surfaces
|
||||
gate = cd.needs_user_approval(
|
||||
"ADAPT_EXISTING",
|
||||
tc.resolve_toolchain(tc.ToolchainConstraints(existing_language="python")),
|
||||
obs,
|
||||
_need(approval_context=""),
|
||||
)
|
||||
assert gate["required"] is True
|
||||
assert "USER-LOCKED" in gate["reasons"][0]
|
||||
|
||||
|
||||
# E. user-authorized layout amendment passes
|
||||
def test_e_user_authorized_amendment_passes():
|
||||
doc = _doc_with_layouts()
|
||||
lc.amend_layout_contract(
|
||||
doc,
|
||||
"status_bar",
|
||||
new_region="header",
|
||||
authorized_by="user",
|
||||
reason="user reposition",
|
||||
evidence_ref="user-2026-09-13",
|
||||
)
|
||||
# contract follows the user's explicit change
|
||||
e = ss.entity(doc, "layout:ui:status_bar")
|
||||
assert e["identities"]["region"] == "header"
|
||||
# obligation now reflects the amended placement
|
||||
s = sc.Scaffold("/tmp/never-required", "x")
|
||||
obs = s.layout_obligations(doc, ["ui:status_bar"])
|
||||
assert obs[0]["region"] == "header"
|
||||
|
||||
|
||||
# F. unavailable layout adapter remains UNKNOWN — never PASS
|
||||
def test_f_unknown_never_pass():
|
||||
doc = _doc_with_layouts()
|
||||
result = lc.check_layout(doc, {}) # no adapter observations
|
||||
assert result["verdict"] == "UNKNOWN"
|
||||
assert all(c["verdict"] == "UNKNOWN" for c in result["checks"])
|
||||
|
||||
|
||||
# G. drift does NOT rewrite USER-LOCKED intent
|
||||
def test_g_drift_preserves_user_locked():
|
||||
doc = _doc_with_layouts()
|
||||
violating = {"ui:status_bar": {"region": "header", "order": 0}}
|
||||
out = lc.layout_drift(doc, violating)
|
||||
assert out["gate"] == "FAIL"
|
||||
e = ss.entity(doc, "layout:ui:status_bar")
|
||||
assert e["identities"]["region"] == "footer-bar" # user intent preserved
|
||||
assert e["status"] == "DRIFTING"
|
||||
assert e["identities"]["lock"] is True
|
||||
# maintenance item records the violation for user review
|
||||
assert ss.entity(doc, "maintenance:layout-drift-ui:status_bar") is not None
|
||||
|
||||
|
||||
# H. version-display stays scaffold-provenance until explicit confirmation
|
||||
def test_h_version_display_not_user_locked():
|
||||
doc = _doc_with_layouts()
|
||||
e = ss.entity(doc, "layout:ui:version_display")
|
||||
assert e["identities"]["provenance"] == "scaffold"
|
||||
assert e["identities"]["lock"] is False # pending user confirmation (§21)
|
||||
# any attempt to lock it without user provenance is refused
|
||||
with pytest.raises(ValueError, match="user provenance"):
|
||||
lc.bind_component(doc, "version_display", "footer-bar", lock=True, provenance="builder")
|
||||
@@ -175,7 +175,21 @@ class TestSpineExtractionNeutrality:
|
||||
pre_e.pop("deployment:current", None)
|
||||
post_e.pop("deployment:current", None)
|
||||
assert pre_e == post_e, "unexplained structural change in rebuild"
|
||||
assert pre["relationships"] == post["relationships"]
|
||||
# relationships: structural content identical modulo observation
|
||||
# timestamps AND the legitimately advancing deployed identity —
|
||||
# deploy-identity-projector refreshes deployed_as edges (observed_at
|
||||
# + evidence SHA) whenever the deployed tree moved between the
|
||||
# committed store's build and this rebuild (observed live 2026-09-13:
|
||||
# 0a9b0648 deploy landed between local build and CI build).
|
||||
def _rel_norm(rels):
|
||||
out = []
|
||||
for r in rels:
|
||||
if r["type"] == "deployed_as":
|
||||
continue # advances legitimately with the deployment
|
||||
out.append({k: v for k, v in r.items() if k != "observed_at"})
|
||||
return out
|
||||
|
||||
assert _rel_norm(pre["relationships"]) == _rel_norm(post["relationships"])
|
||||
|
||||
def test_spine_store_schema_unchanged(self):
|
||||
d = json.loads((ROOT / "store" / "spine" / "current.json").read_text())
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
"""test_surface_completeness.py — SCAFFOLD-TRANCHE-005 test plans
|
||||
§24 (affected surfaces), §25 (requirement completeness), §26 (iterative
|
||||
trace), §27 (expected runtime effects), §28 (backward compat)."""
|
||||
|
||||
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_decision as cd # noqa: E402
|
||||
from rune import layout_contract as lc # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
|
||||
def _authorities():
|
||||
"""Spine-like authority: deploy module with a promotion contract the
|
||||
requirement does NOT represent (the deploy-incident shape), plus a
|
||||
path-mapped module, plus a UI component with a Layout Contract."""
|
||||
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="contract:fail_closed_deployment", kind="contract", identities={}, 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_relationship(
|
||||
spine,
|
||||
type="implements",
|
||||
from_id="module:deploy_qualify",
|
||||
to_id="contract:fail_closed_deployment",
|
||||
evidence_refs=["f"],
|
||||
)
|
||||
scaffold = ss.new_doc()
|
||||
lc.define_region(scaffold, "footer-bar", provenance="scaffold")
|
||||
lc.bind_component(
|
||||
scaffold, "version_display", "footer-bar", provenance="scaffold", order=0, implementation="src/rune/rune_tui.py"
|
||||
)
|
||||
return {"rune_spine": spine, "project_scaffold": scaffold}
|
||||
|
||||
|
||||
def _need(**kw):
|
||||
d = dict(
|
||||
need_id="n5",
|
||||
objective="prevent deploy regression",
|
||||
origin="user",
|
||||
constraints={"contracts": ["contract:fail_closed_deployment"]},
|
||||
affected_surface=["scripts/deploy_qualify.py"],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered",
|
||||
)
|
||||
d.update(kw)
|
||||
return cd.Need(**d)
|
||||
|
||||
|
||||
# ── §24 affected surfaces ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_24a_source_path_propagates_to_file_scope(tmp_path):
|
||||
s, doc = _gen(tmp_path)
|
||||
c = s.contract(doc, "G5")
|
||||
assert "scripts/deploy_qualify.py" in c["ext"]["affected_paths"]
|
||||
assert "scripts/deploy_qualify.py" in c["identities"]["scope"]
|
||||
|
||||
|
||||
def test_24b_mapped_path_resolves_to_module():
|
||||
res = cd.resolve_affected_surface(_need(), _authorities())
|
||||
assert "module:deploy_qualify" in res["affected_existing_entities"]
|
||||
r = next(r for r in res["resolutions"] if r["surface"] == "scripts/deploy_qualify.py")
|
||||
assert r["resolved"][0]["via"] == "path_attribute"
|
||||
|
||||
|
||||
def test_24c_unresolved_path_retained_not_dropped():
|
||||
need = _need(affected_surface=["scripts/brand_new_thing.py"])
|
||||
res = cd.resolve_affected_surface(need, _authorities())
|
||||
assert any(u["surface"] == "scripts/brand_new_thing.py" for u in res["unresolved_surfaces"])
|
||||
assert "scripts/brand_new_thing.py" in res["affected_paths"] # still scoped
|
||||
|
||||
|
||||
def test_24d_ui_component_pulls_layout_contract():
|
||||
need = _need(affected_surface=["ui:version_display"])
|
||||
res = cd.resolve_affected_surface(need, _authorities())
|
||||
assert "ui:version_display" in res["affected_ui_surfaces"]
|
||||
r = res["resolutions"][0]
|
||||
assert r["resolved"][0]["via"] == "layout_contract"
|
||||
trace = cd.make_decision(need, authorities=_authorities())
|
||||
assert any(o["component"] == "ui:version_display" for o in trace["layout_obligations"])
|
||||
|
||||
|
||||
def test_24e_region_surface_pulls_layout_obligations():
|
||||
need = _need(affected_surface=["region:footer-bar"])
|
||||
trace = cd.make_decision(need, authorities=_authorities())
|
||||
assert any(o["region"] == "footer-bar" for o in trace["layout_obligations"])
|
||||
|
||||
|
||||
def test_24f_workflow_config_mechanically_scoped(tmp_path):
|
||||
s, doc = _gen(tmp_path, affected_surface=["scripts/deploy_qualify.py", ".gitea/workflows/deploy.yml"])
|
||||
c = s.contract(doc, "G5")
|
||||
assert ".gitea/workflows/deploy.yml" in c["ext"]["workflow_config_surfaces"]
|
||||
assert ".gitea/workflows/deploy.yml" in c["identities"]["scope"]
|
||||
|
||||
|
||||
def test_24g_affected_surface_not_auto_expected_new(tmp_path):
|
||||
s, doc = _gen(tmp_path)
|
||||
c = s.contract(doc, "G5")
|
||||
# affected existing entities never leak into expected-new
|
||||
assert c["identities"]["expected_new_entities"] == []
|
||||
assert c["identities"]["affected_entities"] # but affected is populated
|
||||
|
||||
|
||||
def test_24h_expected_new_path_entity_distinct(tmp_path):
|
||||
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "p5")
|
||||
doc = s.init(purpose="t")
|
||||
need = _need(
|
||||
constraints={
|
||||
"contracts": ["contract:fail_closed_deployment"],
|
||||
"expected_new_entities": ["module:fresh_thing"],
|
||||
"expected_new_source_paths": ["scripts/fresh_thing.py"],
|
||||
},
|
||||
affected_surface=["scripts/deploy_qualify.py"],
|
||||
evidence_refs=["x"],
|
||||
)
|
||||
trace = cd.make_decision(need, authorities=_authorities())
|
||||
cd.generate_contract(s, doc, trace, contract_id="G5H", scope=None)
|
||||
c = s.contract(doc, "G5H")
|
||||
assert c["identities"]["expected_new_entities"] == ["module:fresh_thing"]
|
||||
assert "scripts/fresh_thing.py" in c["identities"]["scope"] # expected-new path scoped
|
||||
assert "scripts/fresh_thing.py" not in c["ext"]["affected_paths"]
|
||||
|
||||
|
||||
def test_24i_contract_roundtrip_preserves_surfaces(tmp_path):
|
||||
s, doc = _gen(tmp_path, affected_surface=["scripts/deploy_qualify.py", "ui:version_display", "scripts/unmapped.py"])
|
||||
s.save(doc)
|
||||
doc2 = sc.Scaffold(tmp_path / ".rune" / "scaffold", "p5").load()
|
||||
c = doc2 and sc.Scaffold(tmp_path / ".rune" / "scaffold", "p5").contract(doc2, "G5")
|
||||
assert c["ext"]["affected_paths"] == ["scripts/deploy_qualify.py", "scripts/unmapped.py"]
|
||||
assert c["ext"]["affected_ui_surfaces"] == ["ui:version_display"]
|
||||
assert any(u["surface"] == "scripts/unmapped.py" for u in c["ext"]["unresolved_surfaces"])
|
||||
|
||||
|
||||
# ── §25 requirement completeness ────────────────────────────────────────────
|
||||
|
||||
|
||||
def _gen(tmp_path, **need_kw):
|
||||
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "p5")
|
||||
doc = s.init(purpose="t5")
|
||||
need = _need(**need_kw)
|
||||
trace = cd.make_decision(need, authorities=_authorities(), project_toolchain={"existing_language": "python"})
|
||||
cd.generate_contract(s, doc, trace, contract_id="G5", scope=None)
|
||||
return s, doc
|
||||
|
||||
|
||||
def test_25a_complete_requirement_complete_enough():
|
||||
need = _need(
|
||||
constraints={"contracts": ["contract:fail_closed_deployment"]}, affected_surface=["scripts/deploy_qualify.py"]
|
||||
)
|
||||
trace = cd.make_decision(need, authorities=_authorities())
|
||||
assert trace["completeness"]["verdict"] == cd.COMPLETE_ENOUGH
|
||||
assert trace["resolution"] == "REUSE" # fit proceeds
|
||||
|
||||
|
||||
def test_25b_affected_stage_missing_material_gap():
|
||||
"""THE deploy-incident shape: requirement names qualification only, but
|
||||
the affected surface includes the promotion implementation carrying an
|
||||
unrepresented ordering contract."""
|
||||
need = _need(affected_surface=["scripts/deploy_qualify.py", "scripts/deploy_stage.py"])
|
||||
trace = cd.make_decision(need, authorities=_authorities())
|
||||
assert trace["completeness"]["verdict"] == cd.MATERIAL_GAP
|
||||
gaps = {g["missing_contract"] for g in trace["completeness"]["gaps"]}
|
||||
assert "contract:monotonic_promotion" in gaps
|
||||
# PREMATURE-FIT GUARD: without the guard this resolves REUSE/SATISFIED
|
||||
assert trace["resolution"] == "INSUFFICIENT_EVIDENCE"
|
||||
assert trace["_merged"].get("premature_fit_blocked", {}).get("gaps")
|
||||
# gap provenance: derived_from the entity, with rule + evidence
|
||||
g = next(g for g in trace["completeness"]["gaps"] if g["missing_contract"] == "contract:monotonic_promotion")
|
||||
assert g["derived_from"] == "module:deploy_stage"
|
||||
assert g["rule"] and g["evidence"]
|
||||
|
||||
|
||||
def test_25c_no_resolution_unknown():
|
||||
need = _need(affected_surface=["totally/unknown.xyz"])
|
||||
trace = cd.make_decision(need, authorities=_authorities())
|
||||
assert trace["completeness"]["verdict"] == cd.COMPLETENESS_UNKNOWN
|
||||
|
||||
|
||||
def test_25d_unknown_never_becomes_complete():
|
||||
need = _need(affected_surface=["totally/unknown.xyz"])
|
||||
trace = cd.make_decision(need, authorities=_authorities())
|
||||
assert trace["completeness"]["verdict"] != cd.COMPLETE_ENOUGH
|
||||
assert "undeterminable" in trace["completeness"]["note"]
|
||||
|
||||
|
||||
def test_25e_derived_requirement_provenance():
|
||||
need = _need(affected_surface=["scripts/deploy_qualify.py", "scripts/deploy_stage.py"])
|
||||
trace = cd.make_decision(need, authorities=_authorities())
|
||||
g = trace["completeness"]["gaps"][0]
|
||||
assert g["derived_from"] and g["evidence"] and g["rule"]
|
||||
|
||||
|
||||
def test_25f_model_proposal_not_structural_authority():
|
||||
"""A model-proposed contract is recorded with provider/confidence and
|
||||
verification state PENDING — it does not enter the requirement's
|
||||
contract set as authority."""
|
||||
proposed = cd.record_proposed_requirement(
|
||||
"contract:model_claimed", provider="local-model", model="m", evidence=["hunch"], confidence=0.4
|
||||
)
|
||||
assert proposed["verification_state"] == "PENDING"
|
||||
assert proposed["provider"] == "local-model"
|
||||
# normalization does not accept it as a contract
|
||||
need = _need(constraints={"contracts": ["contract:fail_closed_deployment"], "model_proposed": [proposed]})
|
||||
req, _ = cd.normalize_need(need)
|
||||
assert "contract:model_claimed" not in req.contracts
|
||||
|
||||
|
||||
def test_25g_consumer_contracts_derive_regression():
|
||||
spine = _authorities()["rune_spine"]
|
||||
ss.add_entity(spine, id="module:consumer", kind="module", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
spine, type="consumes", from_id="module:consumer", to_id="module:deploy_qualify", evidence_refs=["f"]
|
||||
)
|
||||
need = _need(affected_surface=["scripts/deploy_qualify.py"])
|
||||
trace = cd.make_decision(need, authorities={"rune_spine": spine})
|
||||
assert trace["resolution"] == "REUSE"
|
||||
reg = " ".join(trace["obligations"]["regression"])
|
||||
assert "module:consumer" in reg
|
||||
|
||||
|
||||
def test_25h_adapt_existing_includes_old_plus_new():
|
||||
spine = _authorities()["rune_spine"]
|
||||
ss.add_entity(spine, id="contract:extra_obligation", kind="contract", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
spine,
|
||||
type="implements",
|
||||
from_id="module:deploy_qualify",
|
||||
to_id="contract:extra_obligation",
|
||||
evidence_refs=["f"],
|
||||
)
|
||||
need = _need(affected_surface=["scripts/deploy_qualify.py"])
|
||||
need.constraints["contracts"] = ["contract:fail_closed_deployment", "contract:extra_obligation"]
|
||||
trace = cd.make_decision(need, authorities={"rune_spine": spine})
|
||||
assert trace["completeness"]["verdict"] == cd.COMPLETE_ENOUGH
|
||||
assert trace["resolution"] == "REUSE" # exact fit on both contracts
|
||||
assert "new behavior proven by tests" in " ".join(trace["obligations"]["verification"])
|
||||
|
||||
|
||||
def test_25i_compose_derives_boundary_verification():
|
||||
spine = _authorities()["rune_spine"]
|
||||
need = _need(affected_surface=["scripts/deploy_qualify.py", "scripts/deploy_stage.py"])
|
||||
need.constraints["contracts"] = ["contract:fail_closed_deployment", "contract:monotonic_promotion"]
|
||||
trace = cd.make_decision(need, authorities={"rune_spine": spine})
|
||||
assert trace["resolution"] == "COMPOSE"
|
||||
assert "composition boundary behavior verified end-to-end" in " ".join(trace["obligations"]["verification"])
|
||||
comp = trace["obligations"]["composition"][0]
|
||||
assert set(comp["components"]) == {"module:deploy_qualify", "module:deploy_stage"}
|
||||
|
||||
|
||||
def test_25j_layout_lock_derives_preservation():
|
||||
scaffold = _authorities()["project_scaffold"]
|
||||
lc.bind_component(
|
||||
scaffold, "status_bar", "footer-bar", lock=True, provenance="user", order=1, evidence_ref="user-2026"
|
||||
)
|
||||
need = _need(affected_surface=["ui:status_bar"])
|
||||
trace = cd.make_decision(need, authorities=_authorities() | {"project_scaffold": scaffold})
|
||||
lo = [o for o in trace["layout_obligations"] if o["component"] == "ui:status_bar"]
|
||||
assert lo and lo[0]["lock"] is True
|
||||
assert "explicit user authorization" in lo[0]["obligation"]
|
||||
# and the approval gate fires for the locked surface
|
||||
assert any("USER-LOCKED" in r for r in trace["approval"]["reasons"]) or trace["approval"]["covered_by"]
|
||||
|
||||
|
||||
def test_25k_completeness_prevents_premature_reuse():
|
||||
"""§23 core test, fixture form: the deploy-like Need must NOT resolve
|
||||
REUSE while the affected promotion surface carries an unrepresented
|
||||
contract."""
|
||||
need = _need(affected_surface=["scripts/deploy_qualify.py", "scripts/deploy_stage.py"])
|
||||
trace = cd.make_decision(need, authorities=_authorities())
|
||||
assert trace["resolution"] != "REUSE"
|
||||
assert trace["completeness"]["verdict"] == cd.MATERIAL_GAP
|
||||
blocked = trace["_merged"]["premature_fit_blocked"]
|
||||
assert blocked["would_have_been"] in ("REUSE", "SATISFIED")
|
||||
|
||||
|
||||
# ── §26 iterative trace ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_26_iterative_trace_durable(tmp_path):
|
||||
"""initial normalization → gap → refinement → revised → final decision;
|
||||
all retained; fresh process reconstructs without transcript."""
|
||||
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "p26")
|
||||
doc = s.init(purpose="t26")
|
||||
# iteration 1: narrow requirement → premature fit blocked
|
||||
need1 = _need(affected_surface=["scripts/deploy_qualify.py", "scripts/deploy_stage.py"])
|
||||
t1 = cd.make_decision(need1, authorities=_authorities())
|
||||
assert t1["resolution"] == "INSUFFICIENT_EVIDENCE"
|
||||
assert t1["_merged"]["premature_fit_blocked"]["would_have_been"] == "REUSE"
|
||||
# evidence/refinement: the promotion contract is added to the requirement
|
||||
need2 = _need(
|
||||
constraints={"contracts": ["contract:fail_closed_deployment", "contract:monotonic_promotion"]},
|
||||
affected_surface=["scripts/deploy_qualify.py", "scripts/deploy_stage.py"],
|
||||
evidence_refs=["x", "refinement:completeness-gap-iter1"],
|
||||
)
|
||||
t2 = cd.make_decision(
|
||||
need2,
|
||||
authorities=_authorities(),
|
||||
prior_iterations=[
|
||||
{
|
||||
"iteration": 1,
|
||||
"normalized_contracts": need1.constraints["contracts"],
|
||||
"completeness": t1["completeness"]["verdict"],
|
||||
"gaps": [g["missing_contract"] for g in t1["completeness"]["gaps"]],
|
||||
"resolution": t1["resolution"],
|
||||
"refined_by": "completeness gap evidence",
|
||||
}
|
||||
],
|
||||
)
|
||||
assert t2["resolution"] == "COMPOSE"
|
||||
assert t2["iterations"][0]["gaps"] == ["contract:monotonic_promotion"]
|
||||
cd.generate_contract(s, doc, t2, contract_id="IT1", scope=None)
|
||||
s.transition_contract(doc, "IT1", "approved")
|
||||
s.save(doc)
|
||||
# fresh process
|
||||
script = (
|
||||
"import sys, json;"
|
||||
"sys.path.insert(0, %r);"
|
||||
"from rune import scaffold;"
|
||||
"s = scaffold.Scaffold(%r, 'p26');"
|
||||
"doc = s.load();"
|
||||
"c = s.contract(doc, 'IT1');"
|
||||
"t = c['ext']['decision_trace'];"
|
||||
"print(json.dumps({'iterations': len(t['iterations']),"
|
||||
"'iter1_gaps': t['iterations'][0]['gaps'],"
|
||||
"'iter1_resolution': t['iterations'][0]['resolution'],"
|
||||
"'refined_by': t['iterations'][0]['refined_by'],"
|
||||
"'final_resolution': t['resolution'],"
|
||||
"'final_contracts': t['requirement']['contracts']}))"
|
||||
) % (os.path.join(os.path.dirname(__file__), "..", "src"), str(tmp_path / ".rune" / "scaffold"))
|
||||
r = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=60)
|
||||
assert r.returncode == 0, r.stderr
|
||||
rec = json.loads(r.stdout)
|
||||
assert rec["iterations"] == 1
|
||||
assert rec["iter1_gaps"] == ["contract:monotonic_promotion"]
|
||||
assert rec["iter1_resolution"] == "INSUFFICIENT_EVIDENCE"
|
||||
assert rec["refined_by"] == "completeness gap evidence"
|
||||
assert rec["final_resolution"] == "COMPOSE"
|
||||
assert "contract:monotonic_promotion" in rec["final_contracts"]
|
||||
|
||||
|
||||
# ── §27 expected runtime effects ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def _close_with(tmp_path, expected_runtime, observed):
|
||||
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "p27")
|
||||
doc = s.init(purpose="t27")
|
||||
s.new_contract(
|
||||
doc, contract_id="R1", need="n", scope=["src/x"], affected_entities=["module:a"], expected_new_entities=[]
|
||||
)
|
||||
c = s.contract(doc, "R1")
|
||||
c.setdefault("ext", {})["expected_runtime_effects"] = expected_runtime
|
||||
s.transition_contract(doc, "R1", "approved")
|
||||
s.transition_contract(doc, "R1", "building")
|
||||
s.transition_contract(doc, "R1", "verifying")
|
||||
s.close_contract(doc, "R1", observed_evidence=observed)
|
||||
return s.contract(doc, "R1")["ext"]["contract_quality"]
|
||||
|
||||
|
||||
def test_27a_expected_runtime_observed_not_unexpected(tmp_path):
|
||||
q = _close_with(
|
||||
tmp_path, ["deployment:current advances"], [{"entity": "deployment:current", "source": "deployed-identity"}]
|
||||
)
|
||||
assert q["expected_runtime_observed"] == ["deployment:current advances"]
|
||||
assert "deployment:current" not in q["genuinely_unexpected_new"]
|
||||
|
||||
|
||||
def test_27b_expected_runtime_missing(tmp_path):
|
||||
q = _close_with(tmp_path, ["cache invalidated"], [])
|
||||
assert q["expected_runtime_missing"] == ["cache invalidated"]
|
||||
assert q["expected_runtime_observed"] == []
|
||||
|
||||
|
||||
def test_27c_unpredicted_runtime_genuinely_unexpected(tmp_path):
|
||||
q = _close_with(tmp_path, [], [{"entity": "deployment:current", "source": "deployed-identity"}])
|
||||
assert "deployment:current" in q["genuinely_unexpected_new"]
|
||||
|
||||
|
||||
def test_27d_no_self_certification(tmp_path):
|
||||
"""Predicting the runtime effect does not observe it: no evidence item →
|
||||
missing, and correctly_predicted unaffected."""
|
||||
q = _close_with(tmp_path, ["deployment:current advances"], [])
|
||||
assert q["expected_runtime_missing"] == ["deployment:current advances"]
|
||||
assert q["expected_runtime_observed"] == []
|
||||
|
||||
|
||||
def test_27e_deployment_current_fixture(tmp_path):
|
||||
"""The DEPLOY-MONOTONIC-001 defect shape, now correct: a release-class
|
||||
contract that predicts the deployment advance closes clean."""
|
||||
q = _close_with(
|
||||
tmp_path,
|
||||
["deployment:current advances"],
|
||||
[{"entity": "module:a", "source": "git-diff"}, {"entity": "deployment:current", "source": "deployed-identity"}],
|
||||
)
|
||||
assert q["correctly_predicted"] == ["module:a"]
|
||||
assert q["expected_runtime_observed"] == ["deployment:current advances"]
|
||||
assert q["genuinely_unexpected_new"] == []
|
||||
|
||||
|
||||
# ── §28 backward compatibility ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_28_old_contract_closes_without_new_fields(tmp_path):
|
||||
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "p28")
|
||||
doc = s.init(purpose="t28")
|
||||
# old-shape: no expected_runtime_effects, no expected_new_entities
|
||||
s.new_contract(doc, contract_id="OLD", need="old", scope=["src/x"], affected_entities=["module:a"])
|
||||
c = s.contract(doc, "OLD")
|
||||
assert not c.get("ext", {}).get("expected_runtime_effects") # empty, not crash
|
||||
s.transition_contract(doc, "OLD", "approved")
|
||||
s.transition_contract(doc, "OLD", "building")
|
||||
s.transition_contract(doc, "OLD", "verifying")
|
||||
s.close_contract(doc, "OLD", observed_evidence=[{"entity": "module:a", "source": "git-diff"}])
|
||||
q = s.contract(doc, "OLD")["ext"]["contract_quality"]
|
||||
assert q["correctly_predicted"] == ["module:a"]
|
||||
assert q["expected_runtime_effects"] == [] # empty, not crash
|
||||
|
||||
|
||||
def test_28_tranche4_contract_data_still_readable(tmp_path):
|
||||
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "p28b")
|
||||
doc = s.init(purpose="t28b")
|
||||
s.new_contract(
|
||||
doc,
|
||||
contract_id="T4SHAPE",
|
||||
need="n",
|
||||
scope=["src/x"],
|
||||
affected_entities=["module:a"],
|
||||
expected_new_entities=["module:b"],
|
||||
)
|
||||
s.transition_contract(doc, "T4SHAPE", "approved")
|
||||
s.transition_contract(doc, "T4SHAPE", "building")
|
||||
s.transition_contract(doc, "T4SHAPE", "verifying")
|
||||
s.close_contract(doc, "T4SHAPE", observed_evidence=[{"entity": "module:b", "source": "file-added"}])
|
||||
q = s.contract(doc, "T4SHAPE")["ext"]["contract_quality"]
|
||||
assert q["expected_new_observed"] == ["module:b"]
|
||||
Reference in new issue
Block a user