Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56f96f0d60 | ||
|
|
e86da59cdb | ||
|
|
143e6095ed | ||
|
|
fe4bc43a83 | ||
|
|
83426c0e1c | ||
|
|
b5c9a3c04e | ||
|
|
cb10db1e78 | ||
|
|
1d466a6926 | ||
|
|
fa51b96ca7 | ||
|
|
5796caa30a | ||
|
|
5169e02610 | ||
|
|
cceb607d99 | ||
|
|
c692013441 | ||
|
|
11ebc50a55 | ||
|
|
08c782fe91 | ||
|
|
4bcae0bb10 | ||
|
|
a8d1b98332 | ||
|
|
74272842aa | ||
|
|
aa67119d55 | ||
|
|
9681baa318 | ||
|
|
8a6b051149 | ||
|
|
6bb5507d33 | ||
|
|
8692f1dc76 | ||
|
|
78d70def5f | ||
|
|
c9260ace18 | ||
|
|
2a7e9dcfcf | ||
|
|
e3263ef9b1 | ||
|
|
ce328a8080 | ||
|
|
4df1f6f23f | ||
|
|
7484e175be | ||
|
|
d84060cc02 | ||
|
|
b102225d4e | ||
|
|
f86b2d1eec | ||
|
|
307b0bf814 | ||
|
|
d776f286de | ||
|
|
621f88c425 | ||
|
|
ef6811ff58 | ||
|
|
a4f02f227c | ||
|
|
5b315ac7c7 | ||
|
|
90bd2b3285 | ||
|
|
1bb4ea86b8 | ||
|
|
f98416e94a | ||
|
|
b3465176f8 | ||
|
|
aab5159ccc | ||
|
|
80f10b9be7 | ||
|
|
9d75e51b22 | ||
|
|
b6113fad7b | ||
|
|
f765365d03 | ||
|
|
b94a3aef61 |
No files matched your search
@@ -213,38 +213,17 @@ jobs:
|
||||
env.write(f"RELEASE_VERSION={NEW}\n")
|
||||
env.write("AUTO_BUMPED=true\n")
|
||||
PYBUMP
|
||||
- name: Create release from merged pull request
|
||||
- name: Create evidence-derived release (SCAFFOLD-TRANCHE-009B)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
PR_TITLE: ${{ gitea.event.pull_request.title }}
|
||||
PR_BODY: ${{ gitea.event.pull_request.body }}
|
||||
RELEASE_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases
|
||||
LIST_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases?limit=50
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
# Skip idempotently if a release for this tag already exists (retry/replay safety).
|
||||
if curl --fail --silent "$LIST_API_URL" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
| python3 -c 'import json,sys,os; v=os.environ["RELEASE_VERSION"]; exists=[r for r in json.load(sys.stdin) if r.get("tag_name")=="v"+v]; sys.exit(0 if exists else 1)'; then
|
||||
echo "Release v$RELEASE_VERSION already exists — skipping creation"
|
||||
exit 0
|
||||
fi
|
||||
python3 - <<'PY' > /tmp/rune-release.json
|
||||
import json, os
|
||||
version = os.environ["RELEASE_VERSION"]
|
||||
body = f"## {os.environ['PR_TITLE']}\n\n{os.environ.get('PR_BODY', '')}"
|
||||
print(json.dumps({
|
||||
"tag_name": f"v{version}",
|
||||
"target_commitish": "master",
|
||||
"name": f"Rune v{version}",
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
}))
|
||||
PY
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @/tmp/rune-release.json \
|
||||
"$RELEASE_API_URL"
|
||||
rm -f /tmp/rune-release.json
|
||||
# ONE note-generation authority: the tested release_notes module.
|
||||
# The old PR-title/body paste path (which produced empty '##'
|
||||
# bodies on direct-push releases) is REMOVED — governed evidence
|
||||
# from the scaffold store is the source, not PR prose.
|
||||
python3 scripts/release_history_cli.py init \
|
||||
--version "$RELEASE_VERSION" \
|
||||
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
|
||||
--store store/scaffold/current.json
|
||||
@@ -45,15 +45,19 @@ jobs:
|
||||
- name: Stage, verify, promote (monotonic)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
GITEA_SERVER: ${{ gitea.server_url }}
|
||||
GITEA_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
python3 scripts/deploy_stage.py
|
||||
|
||||
- name: Promotion truthfulness check
|
||||
id: promotion_truth
|
||||
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)"
|
||||
echo "promoted=true" >> "$GITEA_OUTPUT"
|
||||
else
|
||||
# Not promoted. deploy_stage exit 0 + non-matching current =
|
||||
# NOOP_ALREADY_CURRENT or STALE_SUPERSEDED — truthful non-error
|
||||
@@ -61,6 +65,7 @@ jobs:
|
||||
# promotion; the runtime-verification step below only runs for
|
||||
# real promotions.
|
||||
echo "NOT PROMOTED: current -> $CURRENT_TARGET (superseded/noop — current is authoritative)"
|
||||
echo "promoted=false" >> "$GITEA_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Post-deploy runtime verification (only for real promotions)
|
||||
@@ -83,3 +88,28 @@ jobs:
|
||||
assert rune.__version__ == info['version']
|
||||
print('RUNTIME VERIFIED:', info)
|
||||
"
|
||||
- name: Release-history convergence fallback (SCAFFOLD-TRANCHE-0012C)
|
||||
# The IN-LOCK hook in deploy_stage.py is the PRIMARY convergence
|
||||
# mechanism (fires atomically with promotion). This step is a
|
||||
# non-blocking FALLBACK: if the in-lock hook failed (e.g. missing
|
||||
# Gitea config), this retries. NEVER fails the job — documentation
|
||||
# failure never outweighs deployment validity.
|
||||
if: ${{ success() && steps.promotion_truth.outputs.promoted == 'true' }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
VERSION=$(python3 -c "import sys; sys.path.insert(0,'src'); import rune; print(rune.__version__)" 2>/dev/null || echo "")
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "SKIP: cannot determine version"
|
||||
exit 0
|
||||
fi
|
||||
python3 scripts/release_history_cli.py refresh \
|
||||
--version "$VERSION" \
|
||||
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
|
||||
--store "$GITHUB_WORKSPACE/store/scaffold/current.json" \
|
||||
--release-sha "$(git rev-parse HEAD)" \
|
||||
--deployed-sha "$(git rev-parse HEAD)" \
|
||||
|| echo "::warning::release-history fallback refresh FAILED — in-lock hook may have already converged; retry converges"
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.39**
|
||||
**Current release: v0.66.54**
|
||||
|
||||
**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:43:44Z",
|
||||
"generated_at": "2026-09-14T03:23:48Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
@@ -218,15 +218,22 @@
|
||||
"capability_fit",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer",
|
||||
"construction_activation",
|
||||
"construction_decision",
|
||||
"construction_interface",
|
||||
"construction_mode",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"layout_contract",
|
||||
"release_notes",
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"structural_store",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
|
||||
@@ -226,3 +226,50 @@ contract — code adapts.
|
||||
- Scaffolding overall: adoption path live for one bounded flow — NOT
|
||||
general autonomous construction, NOT universal discovery/toolchain
|
||||
intelligence, NOT Scaffolding complete.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Scaffolding Tranche 9 — Release History + Evidence Correction (2026-09-13)
|
||||
|
||||
_Gitea Releases are now an evidence-derived human-readable history of
|
||||
Rune's evolution. Plus the Tranche 8 builder-evidence correction._
|
||||
|
||||
## Release semantics (audited)
|
||||
|
||||
| surface | meaning |
|
||||
|---|---|
|
||||
| VERSION | semver in pyproject/init (e.g. 0.66.44) |
|
||||
| TAG | v<VERSION>, created by auto-version at the BUMP commit |
|
||||
| release commit | the auto-bump SHA — NOT necessarily final state |
|
||||
| converged deployed identity | last commit deployed under the same VERSION |
|
||||
|
||||
Same-version convergence commits (closure/accounting/lock fixes landing and
|
||||
deploying after the bump) are normal; the notes record BOTH the tag commit
|
||||
and the final deployed identity, distinction exposed honestly.
|
||||
|
||||
## Mechanism
|
||||
|
||||
`release_notes.py`: deterministic evidence extraction from governed records
|
||||
(closed contracts, resolved maintenance, contract-quality records) →
|
||||
structured ReleaseNote → Markdown → Gitea API (POST create / PATCH update).
|
||||
Idempotent: publishing twice for a tag UPDATES, never duplicates. Notes are
|
||||
historical documentation — NOT Memory, NOT structural authority.
|
||||
|
||||
## T8 evidence correction (§1)
|
||||
|
||||
The T8 report claimed a close-from-verifying refusal + reactive superseding
|
||||
transition. Durable checkpoints show NO superseding transition and the
|
||||
phase map permits verifying→closed; live output showed first-attempt close
|
||||
success. The claim was RETRACTED with full provenance (original claim,
|
||||
correction, reason, evidence preserved in the contract record) and
|
||||
recorded as process-defect:unsupported-causal-claim. MiMo adoption
|
||||
maturity survives on the SUPPORTED evidence: scope constrained file edits,
|
||||
pattern obligations forced preservation proof, interface-reported
|
||||
observations, fresh resume.
|
||||
|
||||
## Maturity
|
||||
|
||||
Release-note generator: LIVE-PROVEN (real Gitea, 10 releases). Backfill:
|
||||
0.66.35→0.66.44 carrying historical-backfill provenance. Pre-0.66.35
|
||||
history: skipped (weak evidence — honest limitation).
|
||||
@@ -9,6 +9,7 @@ new surface._
|
||||
|---|---|---|
|
||||
| `rune_tui.py` | The main Textual application — chat pane, work pane, status bar (version label + refresh), footer, dialogs | The known giant (file-size debt tracked in henxels); composition root for all UI |
|
||||
| `tui_validator.py` | Import-allowlist validation for TUI code | Drift defect tracked in #139 (allowlist vs `__future__`/relative imports) |
|
||||
| `tui_backlog.py` | Not a surface — the durable TUI Upgrade Backlog ledger (outstanding projection/control work per backend feature) | SCAFFOLD-TRANCHE-007; ledger only, never structural authority |
|
||||
|
||||
## Adding a TUI surface
|
||||
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"BLE001": 352,
|
||||
"S110": 80
|
||||
}
|
||||
"BLE001": 354,
|
||||
"S110": 80
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.39"
|
||||
version = "0.66.54"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ certifi==2026.7.22
|
||||
# httpx
|
||||
chardet==7.6.0
|
||||
# via diff-cover
|
||||
coverage==7.16.0
|
||||
coverage==7.16.1
|
||||
# via rune (pyproject.toml)
|
||||
diff-cover==10.5.1
|
||||
# via rune (pyproject.toml)
|
||||
|
||||
@@ -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)
|
||||
@@ -80,10 +80,20 @@ def decide_promotion(candidate_sha: str, deployed_sha: str, ancestry: str) -> st
|
||||
return OUTCOME_UNKNOWN_CURRENT
|
||||
|
||||
|
||||
def ancestry_of(candidate_sha: str, deployed_sha: str, repo: Path) -> str:
|
||||
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)."""
|
||||
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(
|
||||
@@ -99,6 +109,20 @@ def ancestry_of(candidate_sha: str, deployed_sha: str, repo: Path) -> str:
|
||||
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"
|
||||
|
||||
|
||||
@@ -123,7 +147,12 @@ def read_deployed_commit(deploy_root: Path = DEPLOY_ROOT) -> str:
|
||||
|
||||
|
||||
def promote(
|
||||
candidate_sha: str, stage: Path, tmp_stage: Path, repo: Path, deploy_root: Path = DEPLOY_ROOT
|
||||
candidate_sha: str,
|
||||
stage: Path,
|
||||
tmp_stage: Path,
|
||||
repo: Path,
|
||||
deploy_root: Path = DEPLOY_ROOT,
|
||||
on_promoted=None,
|
||||
) -> tuple[str, str]:
|
||||
"""Atomically decide + promote under an exclusive lock.
|
||||
|
||||
@@ -165,7 +194,18 @@ def promote(
|
||||
shutil.rmtree(stage)
|
||||
os.rename(tmp_stage, stage)
|
||||
_atomic_symlink_target(deploy_root, "current", stage)
|
||||
return (OUTCOME_PROMOTED, f"current -> {stage.name}")
|
||||
detail = f"current -> {stage.name}"
|
||||
if on_promoted is not None:
|
||||
# §12C: race-safe convergence — the WINNER refreshes release
|
||||
# history atomically with its promotion, inside the lock.
|
||||
# Failure degrades to STALE (software valid, history lags;
|
||||
# retry by any later deploy converges idempotently).
|
||||
try:
|
||||
on_promoted(candidate_sha)
|
||||
detail += " (+ release-history converged)"
|
||||
except Exception as exc: # noqa: BLE001
|
||||
detail += f" (release-history refresh FAILED: {exc!s:.120} — STALE, retry converges)"
|
||||
return (OUTCOME_PROMOTED, detail)
|
||||
finally:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
+28
-1
@@ -129,8 +129,35 @@ print("VERIFIED:", json.dumps(info))
|
||||
# claim promotion success — build/stage success != promotion success.
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
import deploy_promotion # noqa: E402
|
||||
from rune.release_notes import ( # noqa: E402
|
||||
auto_initial_note as _auto_note,
|
||||
publish_release as _publish,
|
||||
)
|
||||
|
||||
outcome, detail = deploy_promotion.promote(sha, DEPLOY_ROOT / short, tmp_stage, repo)
|
||||
def _converge_release(promoted_sha: str) -> None:
|
||||
"""§12C: race-safe release-history convergence, called INSIDE the
|
||||
promotion critical section by the WINNER. Failures degrade to
|
||||
STALE (software valid); the workflow's fallback refresh retries
|
||||
idempotently."""
|
||||
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO", "")
|
||||
token = os.environ.get("GITEA_TOKEN", "")
|
||||
if not (server and repo and token):
|
||||
raise RuntimeError("no Gitea config for release convergence")
|
||||
identity = DEPLOY_ROOT / short / "src" / "rune" / "_build_identity.json"
|
||||
version = json.loads(identity.read_text()).get("version", "")
|
||||
note = _auto_note(
|
||||
version=version,
|
||||
summary=(f"Rune v{version} — evidence-derived release history (automatic convergence refresh)"),
|
||||
scaffold_store=str(REPO_ROOT / "store" / "scaffold" / "current.json"),
|
||||
release_sha=promoted_sha,
|
||||
deployed_sha=promoted_sha,
|
||||
)
|
||||
result = _publish(server, repo, token, note)
|
||||
if result.get("failed"):
|
||||
raise RuntimeError(f"release convergence failed: {result.get('error', '')[:150]}")
|
||||
|
||||
outcome, detail = deploy_promotion.promote(sha, DEPLOY_ROOT / short, tmp_stage, repo, on_promoted=_converge_release)
|
||||
|
||||
if outcome == deploy_promotion.OUTCOME_PROMOTED:
|
||||
print(f"PROMOTED: {short}")
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""release_history_cli.py — thin workflow entry points for release history.
|
||||
|
||||
SCAFFOLD-TRANCHE-009B §4: the workflows invoke THIS tested module; no
|
||||
note-building logic lives in YAML. Two moments:
|
||||
|
||||
init RELEASE INITIALIZATION — auto-version creates the evidence-
|
||||
derived Gitea release when the version/tag appears.
|
||||
refresh RELEASE CONVERGENCE REFRESH — deploy promotes a later SHA of
|
||||
the same version; the SAME release is updated with the final
|
||||
deployed identity + newly-governed facts. Idempotent.
|
||||
|
||||
Fail-closed boundary (§5/§16): `init` exits non-zero on publication
|
||||
failure — Rune cannot silently create another empty historical release.
|
||||
`refresh` ALSO exits non-zero (visible failure + retry converges), but the
|
||||
workflow marks it non-blocking for deployment validity: valid software is
|
||||
never rolled back over documentation. Failure state is visible either way.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
||||
from rune import release_notes as rn # noqa: E402
|
||||
|
||||
|
||||
def _token() -> str:
|
||||
tok = os.environ.get("GITEA_TOKEN", "")
|
||||
if tok:
|
||||
return tok
|
||||
# local/dev fallback: git credential store (runner uses GITEA_TOKEN)
|
||||
cred = Path.home() / ".git-credentials"
|
||||
if cred.exists():
|
||||
import re
|
||||
m = re.search(r"https://[^:]+:([^@]+)@", cred.read_text())
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_init = sub.add_parser("init", help="release initialization")
|
||||
p_init.add_argument("--version", required=True)
|
||||
p_init.add_argument("--server", required=True)
|
||||
p_init.add_argument("--repo", required=True)
|
||||
p_init.add_argument("--store", default="store/scaffold/current.json")
|
||||
p_init.add_argument("--release-sha", default="")
|
||||
p_init.add_argument("--deployed-sha", default="")
|
||||
|
||||
p_ref = sub.add_parser("refresh", help="convergence refresh")
|
||||
p_ref.add_argument("--version", required=True)
|
||||
p_ref.add_argument("--server", required=True)
|
||||
p_ref.add_argument("--repo", required=True)
|
||||
p_ref.add_argument("--store", default="store/scaffold/current.json")
|
||||
p_ref.add_argument("--release-sha", default="")
|
||||
p_ref.add_argument("--deployed-sha", required=True,
|
||||
help="converged deployed commit (from the deployment "
|
||||
"authority — required, never assumed)")
|
||||
|
||||
args = ap.parse_args(argv)
|
||||
token = _token()
|
||||
if not token:
|
||||
print(json.dumps({"failed": True,
|
||||
"error": "no Gitea token available"}))
|
||||
return 1
|
||||
|
||||
if args.cmd == "init":
|
||||
note = rn.auto_initial_note(
|
||||
version=args.version,
|
||||
summary=f"Rune v{args.version} — evidence-derived release "
|
||||
f"history (automatic initialization)",
|
||||
scaffold_store=args.store,
|
||||
release_sha=args.release_sha,
|
||||
deployed_sha=args.deployed_sha or args.release_sha,
|
||||
)
|
||||
result = rn.publish_release(args.server, args.repo, token, note)
|
||||
print(json.dumps(result))
|
||||
# fail-closed: initialization failure must be visible so Rune cannot
|
||||
# silently create another empty historical release (§5/§16)
|
||||
return 0 if not result.get("failed") else 1
|
||||
|
||||
if args.cmd == "refresh":
|
||||
# §12C RACE-SAFE REVALIDATION: between promotion and refresh, a
|
||||
# NEWER deploy may have superseded this candidate. The deployment
|
||||
# authority is re-read NOW: if current's build identity differs
|
||||
# from the candidate, this job is stale → refuse (preserve T11
|
||||
# invariant: stale jobs never overwrite release history). When
|
||||
# current AGREES, the candidate IS the converged truth.
|
||||
current_link = os.environ.get(
|
||||
"RUNE_DEPLOY_CURRENT", "/mnt/workdrive/rune-code/current")
|
||||
try:
|
||||
deployed_now = rn.converged_deployed_sha(
|
||||
str(Path(current_link).parent))
|
||||
except (OSError, ValueError):
|
||||
deployed_now = ""
|
||||
if args.deployed_sha and deployed_now and \
|
||||
not deployed_now.startswith(args.deployed_sha[:12]):
|
||||
print(json.dumps({
|
||||
"failed": True, "action": "refresh-superseded",
|
||||
"error": f"deployment authority shows {deployed_now[:12]}, "
|
||||
f"candidate {args.deployed_sha[:12]} was superseded "
|
||||
f"— stale refresh refused (history preserved)",
|
||||
"authority": deployed_now[:12],
|
||||
}))
|
||||
return 1 # visible; retry by the WINNING deploy converges
|
||||
|
||||
existing = rn.fetch_release(args.server, args.repo, token,
|
||||
f"v{args.version}")
|
||||
if existing is None:
|
||||
# no release yet for this version — init instead (convergence
|
||||
# may legitimately arrive before/without an init if the
|
||||
# auto-version run failed); idempotent recovery path
|
||||
note = rn.build_from_deployment(
|
||||
version=args.version, deployed_sha=args.deployed_sha,
|
||||
scaffold_store=args.store, release_sha=args.release_sha)
|
||||
else:
|
||||
note = rn.convergence_refresh(
|
||||
rn.ReleaseNote.from_dict({
|
||||
"version": args.version,
|
||||
"summary": _summary_from_body(existing.get("body", "")),
|
||||
"provenance": [],
|
||||
}),
|
||||
deployed_sha=args.deployed_sha, scaffold_store=args.store,
|
||||
extra_provenance="automatic convergence refresh (deploy "
|
||||
"workflow)")
|
||||
# preserve structural fields the stored body carries
|
||||
note.release_sha = _sha_from_body(existing.get("body", ""),
|
||||
"tag") or args.release_sha
|
||||
result = rn.publish_release(args.server, args.repo, token, note)
|
||||
print(json.dumps(result))
|
||||
# visible failure; retry converges (§10). Deployment validity is
|
||||
# NOT rolled back over documentation — the workflow marks this
|
||||
# step appropriately.
|
||||
return 0 if not result.get("failed") else 1
|
||||
return 2
|
||||
|
||||
|
||||
def _summary_from_body(body: str) -> str:
|
||||
"""Extract the summary paragraph (between the H1 and the first other
|
||||
heading/italic line) from a stored release body."""
|
||||
lines = body.splitlines()
|
||||
grab = []
|
||||
for ln in lines[1:]:
|
||||
s = ln.strip()
|
||||
if not s:
|
||||
if grab:
|
||||
break
|
||||
continue
|
||||
if s.startswith(("#", "_", "##", "-")):
|
||||
break
|
||||
grab.append(s)
|
||||
return " ".join(grab) or f"Rune — evidence-derived release history"
|
||||
|
||||
|
||||
def _sha_from_body(body: str, which: str) -> str:
|
||||
import re
|
||||
pat = ("tag commit: `([0-9a-f]+)`" if which == "tag"
|
||||
else r"\*\*final deployed: `([0-9a-f]+)`\*\*")
|
||||
m = re.search(pat, body, re.I)
|
||||
return m.group(1) if m else ""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.39"
|
||||
__version__ = "0.66.54"
|
||||
|
||||
# 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.39"' in rune_version,
|
||||
'__version__ = "0.66.54"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"""construction_activation.py — the activation dispatcher.
|
||||
|
||||
SCAFFOLD-TRANCHE-0011. §3: NOT another planner/router/engine. Given an
|
||||
evidence-backed ConstructionMode, start/resume the correct Scaffolding
|
||||
lifecycle. ONE activation authority; consumers: Rune native backend path,
|
||||
Construction Interface (MiMo/other builders), manual operator override.
|
||||
|
||||
Mode dispatch (tranche brief §4/§7/§8/§9/§10):
|
||||
|
||||
NOT_CONSTRUCTION → no Scaffolding state touched (§10)
|
||||
LOCAL_CHANGE → bounded governance via the existing project's
|
||||
authority; Construction Contract only if policy
|
||||
requires; no Project Scaffold bootstrap (§7)
|
||||
EXISTING_PROJECT_CHANGE
|
||||
→ attach to the existing project authority (Rune
|
||||
Spine for Rune; bound Project Scaffold otherwise);
|
||||
full proportional lifecycle (§8) — never a new
|
||||
project (§1 existence evidence governs)
|
||||
PROJECT_BUILD → actually bootstrap: provisional Project Scaffold
|
||||
initialized with UNKNOWN fields explicit (§6/§7) —
|
||||
repository: undecided, toolchain: unresolved —
|
||||
progressive refinement, nothing manufactured
|
||||
UNKNOWN → NO activation; return evidence gaps + probe
|
||||
questions (§9)
|
||||
|
||||
Persistence discipline (§21): every state mutation goes through
|
||||
Scaffold.commit() (save + readback verify) BEFORE any accounting.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from rune import structural_store as ss
|
||||
from rune import scaffold as sc
|
||||
from rune import construction_mode as cm
|
||||
|
||||
__all__ = ["activate"]
|
||||
|
||||
|
||||
def _scaffold_for(project: str, *, create: bool, parent_scaffold: sc.Scaffold | None = None) -> sc.Scaffold | None:
|
||||
"""Resolve the Project Scaffold store for a project (§1 evidence:
|
||||
existence comes from a bound store on disk, not from the name).
|
||||
Location derives from the PARENT scaffold's store directory — no
|
||||
hardcoded relative path (T11 fix: activation under a tmp/fixture parent
|
||||
must write the fixture root, not the repo's store)."""
|
||||
parent_dir = parent_scaffold.current.parent if parent_scaffold else Path("store/scaffold")
|
||||
base = parent_dir / "projects" / project
|
||||
current = base / "current.json"
|
||||
if current.is_file():
|
||||
return sc.Scaffold(base, project)
|
||||
if create:
|
||||
return sc.Scaffold(base, project)
|
||||
return None
|
||||
|
||||
|
||||
def activate(mode_decision, *, need, scaffold: sc.Scaffold, builder: str = "rune") -> dict:
|
||||
"""Dispatch on the evidence-backed mode; return an activation record
|
||||
(mode, project authority, actions taken, context ref). The NEED must
|
||||
already be classified — UNKNOWN never reaches here without explicit
|
||||
operator override carried in mode_decision."""
|
||||
mode = mode_decision.mode
|
||||
rec: dict = {
|
||||
"mode": mode,
|
||||
"builder": builder,
|
||||
"target_project": mode_decision.target_project,
|
||||
"evidence": list(mode_decision.evidence),
|
||||
}
|
||||
|
||||
if mode == "NOT_CONSTRUCTION":
|
||||
# §10: no Scaffolding state touched
|
||||
rec["activated"] = False
|
||||
rec["reason"] = "non-construction intent — no construction state"
|
||||
return rec
|
||||
|
||||
if mode == "UNKNOWN":
|
||||
# §9: no activation; probe questions surface
|
||||
rec["activated"] = False
|
||||
rec["reason"] = "insufficient evidence — clarification required; no Project Scaffold initialized"
|
||||
rec["probe_questions"] = mode_decision.probe_questions
|
||||
rec["clarification_required"] = True
|
||||
return rec
|
||||
|
||||
if mode == "LOCAL_CHANGE":
|
||||
# §7: governance without ceremony — reuse the existing project's
|
||||
# authority; the lightest contract path (caller decides policy);
|
||||
# NO new Project Scaffold bootstrap
|
||||
rec["activated"] = True
|
||||
rec["lifecycle"] = "bounded"
|
||||
rec["project_authority"] = mode_decision.project_authority or "rune_internal"
|
||||
rec["note"] = "bounded governance via existing authority — no Project Scaffold bootstrap"
|
||||
return rec
|
||||
|
||||
if mode == "EXISTING_PROJECT_CHANGE":
|
||||
# §8: attach to the existing authority — never a new project.
|
||||
# Existence was already proven structurally (T11 §1) before this
|
||||
# mode was selected.
|
||||
rec["activated"] = True
|
||||
rec["lifecycle"] = "proportional"
|
||||
rec["project_authority"] = mode_decision.project_authority or "rune_internal"
|
||||
rec["note"] = (
|
||||
"attached to existing project authority; Need/completeness/patterns/contract flow governs proportionally"
|
||||
)
|
||||
return rec
|
||||
|
||||
if mode == "PROJECT_BUILD":
|
||||
# §6/§7: actually bootstrap. Provisional Project Scaffold with
|
||||
# explicit UNKNOWN fields; identity before repository (§7).
|
||||
project = mode_decision.target_project or need.project
|
||||
if not project:
|
||||
# Rune-originated Needs may carry no project identity (§18):
|
||||
# derive a stable provisional id from the objective — the
|
||||
# bootstrap is provisional and renameable before any repo
|
||||
# exists (§7 identity before repository).
|
||||
import re as _re
|
||||
|
||||
slug = _re.sub(r"[^a-z0-9]+", "-", need.objective.lower()).strip("-")[:48]
|
||||
project = slug or "unnamed-project"
|
||||
ps = _scaffold_for(project, create=True, parent_scaffold=scaffold)
|
||||
assert ps is not None
|
||||
pdoc = ps.load()
|
||||
if ss.entity(pdoc, f"project:{project}") is None:
|
||||
ps.init(purpose=need.objective) # init persists its own doc
|
||||
pdoc = ps.load() # §21: reload after persistence
|
||||
structural = ss.entity(pdoc, f"project:{project}")
|
||||
assert structural is not None
|
||||
# explicit UNKNOWN fields — nothing manufactured (§6/§10)
|
||||
structural.setdefault("ext", {}).update(
|
||||
{
|
||||
"bootstrap": "initialized",
|
||||
"repository": "undecided",
|
||||
"toolchain": "unresolved",
|
||||
"deployment": "unknown",
|
||||
"requirement_gaps": list(mode_decision.probe_questions)
|
||||
or ["target platform unknown", "requirements not yet refined"],
|
||||
"bootstrap_started_at": ss.now(),
|
||||
"origin_need": need.need_id,
|
||||
"origin_builder": builder,
|
||||
}
|
||||
)
|
||||
ps.commit(pdoc, what=f"PROJECT_BUILD bootstrap: {project}")
|
||||
rec.update(
|
||||
{
|
||||
"activated": True,
|
||||
"lifecycle": "project_build",
|
||||
"project": project,
|
||||
"project_scaffold": str(ps.current),
|
||||
"bootstrap": "initialized",
|
||||
"repository": "undecided",
|
||||
"toolchain": "unresolved",
|
||||
"deployment": "unknown",
|
||||
"requirement_gaps": structural["ext"]["requirement_gaps"],
|
||||
"next_action": ("refine requirements; resolve toolchain/repo decisions via evidence (approval-gated)"),
|
||||
}
|
||||
)
|
||||
return rec
|
||||
|
||||
rec["activated"] = False
|
||||
rec["reason"] = f"unhandled mode {mode}"
|
||||
return rec
|
||||
|
||||
|
||||
def dispatch_from_decision(trace: dict, *, need, scaffold: sc.Scaffold, builder: str = "rune") -> dict:
|
||||
"""§15: read the mode from a make_decision trace and dispatch. The
|
||||
shared Construction Context then derives from the SAME canonical state
|
||||
— no duplicate machines."""
|
||||
mode = trace.get("construction_mode", {}).get("mode", "UNKNOWN")
|
||||
return activate(mode, need=need, scaffold=scaffold, builder=builder)
|
||||
@@ -283,7 +283,7 @@ def resolve_affected_surface(need: Need, authorities: dict[str, dict]) -> dict:
|
||||
"entity": test_entity,
|
||||
"derived_from": surface,
|
||||
"rule": "Rune test-path convention: tests/test_<name>.py implies "
|
||||
"the test:<name> surface participates in the build",
|
||||
"the test:<name> surface participates in the build",
|
||||
"evidence": f"affected_surface entry {surface}",
|
||||
}
|
||||
)
|
||||
@@ -505,6 +505,33 @@ def needs_user_approval(
|
||||
# ── the live decision path ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _project_exists_evidence(project: str | None, authorities: dict[str, dict]) -> tuple[bool | None, list[str]]:
|
||||
"""T11 §1: project existence from STRUCTURAL EVIDENCE only.
|
||||
|
||||
Returns (exists, evidence) where exists is:
|
||||
True — a bound Project Scaffold or Rune Spine carries the project
|
||||
False — the project name is known but NO structural evidence for it
|
||||
None — project identity missing/unresolvable (UNKNOWN; classifier
|
||||
must not manufacture existence)
|
||||
|
||||
Rune itself exists (Rune Spine is its authority). Any other name must
|
||||
appear as a project entity in a consulted authority; a bare string is
|
||||
never proof (held-out: arbitrary "foo" → False, not silently True)."""
|
||||
evidence: list[str] = []
|
||||
if not project:
|
||||
return None, ["no project identity supplied — existence UNKNOWN"]
|
||||
if project in ("rune", "issue161-harness"):
|
||||
evidence.append("Rune Spine is the Rune project authority (rune_internal mode)")
|
||||
return True, evidence
|
||||
for name, doc in authorities.items():
|
||||
for e in doc.get("entities", []):
|
||||
if e.get("kind") == "project" and e.get("id") == f"project:{project}":
|
||||
evidence.append(f"bound Project Scaffold entity project:{project} in {name}")
|
||||
return True, evidence
|
||||
evidence.append(f"no structural evidence for project '{project}' in consulted authorities")
|
||||
return False, evidence
|
||||
|
||||
|
||||
def make_decision(
|
||||
need: Need,
|
||||
*,
|
||||
@@ -512,6 +539,7 @@ def make_decision(
|
||||
toolchain_constraints: tc.ToolchainConstraints | None = None,
|
||||
project_toolchain: dict | None = None,
|
||||
prior_iterations: list[dict] | None = None,
|
||||
extra_patterns: list | None = None,
|
||||
) -> dict:
|
||||
"""Run NEED → decision trace. `authorities` maps authority name → store
|
||||
doc (e.g. {"rune_spine": spine_doc, "project_scaffold": scaffold_doc}).
|
||||
@@ -548,6 +576,20 @@ def make_decision(
|
||||
surface_result = resolve_affected_surface(need, authorities)
|
||||
trace["affected_surfaces"] = surface_result
|
||||
|
||||
# CONTEXTUAL CONSTRUCTION MODE (Tranche 10 + T11 §1): typed mode
|
||||
# classification with evidence — ONE canonical home (construction_mode).
|
||||
# §1 CORRECTION: project existence derives from STRUCTURAL EVIDENCE
|
||||
# (bound Project Scaffold / Rune Spine), never from a non-"rune" string.
|
||||
from rune.construction_mode import classify_mode
|
||||
|
||||
existence, existence_evidence = _project_exists_evidence(need.project, authorities)
|
||||
mode_decision = classify_mode(
|
||||
need.objective, target_project=need.project, project_exists=existence, affected_surface=need.affected_surface
|
||||
)
|
||||
mode_decision.evidence.extend(existence_evidence)
|
||||
trace["construction_mode"] = mode_decision.to_dict()
|
||||
trace["project_existence"] = {"project": need.project, "exists": existence, "evidence": existence_evidence}
|
||||
|
||||
# REQUIREMENT COMPLETENESS GATE (Tranche 5 §6-8) — before fit
|
||||
completeness = check_completeness(req, need, authorities, surface_result)
|
||||
trace["completeness"] = completeness
|
||||
@@ -628,9 +670,54 @@ def make_decision(
|
||||
# research routing (§6, honest seam)
|
||||
trace["research"] = research_route(req, merged)
|
||||
|
||||
# PATTERN SELECTION (Tranche 6 §4-7): evidence-backed applicability over
|
||||
# the builtin registry + caller-supplied patterns; obligations filtered
|
||||
# per project evidence; conflicts surfaced, never silently resolved.
|
||||
# Project evidence > pattern assumption (§3): a pattern that contradicts
|
||||
# recorded project exclusions becomes DOES_NOT_APPLY.
|
||||
from rune.construction_pattern import ( # noqa: E402 — local: pattern layer
|
||||
combine_pattern_obligations,
|
||||
derive_pattern_obligations,
|
||||
evaluate_applicability,
|
||||
)
|
||||
from rune.construction_patterns_builtin import BUILTIN_PATTERNS # noqa: E402
|
||||
|
||||
pattern_candidates = list(BUILTIN_PATTERNS) + list(extra_patterns or [])
|
||||
patterns_considered = []
|
||||
derivations = []
|
||||
for pattern in pattern_candidates:
|
||||
app = evaluate_applicability(pattern, need, surface_result, authorities)
|
||||
patterns_considered.append(app)
|
||||
# APPLIES and PARTIAL both reach obligation derivation: applicability
|
||||
# gates whether the pattern is CONSULTED; the per-obligation `when`
|
||||
# conditions do the precision filtering (Tranche 6 §7). The
|
||||
# applicability verdict is recorded on every derived obligation so
|
||||
# weaker evidence stays visible.
|
||||
if app["verdict"] in ("APPLIES", "PARTIAL"):
|
||||
d = derive_pattern_obligations(
|
||||
pattern,
|
||||
need,
|
||||
surface_result,
|
||||
authorities,
|
||||
project_exceptions=need.constraints.get("pattern_exceptions", []),
|
||||
)
|
||||
for entry in d["accepted"] + d["rejected"] + d["unresolved"]:
|
||||
entry["pattern_applicability"] = app["verdict"]
|
||||
derivations.append(d)
|
||||
combined = combine_pattern_obligations(derivations) if derivations else {"obligations": [], "conflicts": []}
|
||||
trace["patterns_considered"] = patterns_considered
|
||||
trace["pattern_obligations"] = combined
|
||||
|
||||
# approval gate (§13)
|
||||
trace["approval"] = needs_user_approval(merged["resolution"], tdec, lay, need)
|
||||
trace["obligations"] = obligations_from_trace(trace, req)
|
||||
# pattern-derived obligations join the contract's verification set (§19):
|
||||
# provenance stays PATTERN (distinct from STRUCTURAL derivations in
|
||||
# obligations_from_trace — §8 provenance separation)
|
||||
trace["obligations"]["pattern"] = combined["obligations"]
|
||||
trace["obligations"]["pattern_conflicts"] = combined["conflicts"]
|
||||
for po in combined["obligations"]:
|
||||
trace["obligations"]["verification"].append(f"[pattern:{po['pattern']}@r{po['revision']}] {po['obligation']}")
|
||||
return trace
|
||||
|
||||
|
||||
@@ -797,9 +884,10 @@ def generate_contract(
|
||||
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_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"])),
|
||||
@@ -828,6 +916,9 @@ def generate_contract(
|
||||
"completeness",
|
||||
"affected_surfaces",
|
||||
"iterations",
|
||||
"patterns_considered",
|
||||
"pattern_obligations",
|
||||
"construction_mode",
|
||||
)
|
||||
},
|
||||
# distinct impact surfaces (Tranche 5 §13) — file scope != structural
|
||||
|
||||
@@ -0,0 +1,838 @@
|
||||
"""construction_interface.py — the stable builder-facing Construction
|
||||
Interface (SCAFFOLD-TRANCHE-007).
|
||||
|
||||
ONE shared boundary for every builder — MiMo, Kimi, GLM, future models,
|
||||
deterministic scripts — and for both modes:
|
||||
|
||||
Rune-internal mode authority = Rune Spine + Rune's Project Scaffold
|
||||
external-project mode authority = that project's Project Scaffold
|
||||
|
||||
The builder must NOT need to know construction_decision.py, scaffold.py,
|
||||
structural_store.py, Layout Contract internals, or store paths. Those are
|
||||
implementation details behind this adapter.
|
||||
|
||||
Authority enforcement (§14) — the adapter is NOT a bypass:
|
||||
- builders may not rewrite Rune Spine or Project Scaffold truth
|
||||
- builders may not amend USER-LOCKED layout
|
||||
- builders may not silently broaden scope
|
||||
- builders may not self-certify completion ("done=true" is not a thing)
|
||||
- closing remains governed by Scaffolding (phase map + gates)
|
||||
- builder identity is RECORDED for audit but never changes semantics
|
||||
(§17): identical structured input → identical behavior, any builder.
|
||||
|
||||
Machine-readable output (§12): every operation returns a stable envelope —
|
||||
{schema, operation, success, ids, state, evidence, errors, warnings}
|
||||
Errors are explicit states (§13): NEED_INCOMPLETE, INSUFFICIENT_EVIDENCE,
|
||||
APPROVAL_REQUIRED, UNKNOWN_CONTRACT, SCOPE_VIOLATION, VERIFICATION_FAILED,
|
||||
STORE_WRITE_FAILED, STALE_STATE — never prose-only exit-1.
|
||||
|
||||
Transport (§3): typed Python API (this module) + JSON CLI (module main,
|
||||
same convention as rune-spine). No network service.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from rune import structural_store as ss
|
||||
from rune import scaffold as sc
|
||||
from rune import spine as spine_mod
|
||||
from rune import construction_decision as cd
|
||||
|
||||
__all__ = ["ConstructionInterface", "INTERFACE_SCHEMA_VERSION", "Errors"]
|
||||
|
||||
INTERFACE_SCHEMA_VERSION = 1
|
||||
|
||||
# ── explicit error states (§13) ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class Errors:
|
||||
NEED_INCOMPLETE = "NEED_INCOMPLETE"
|
||||
INSUFFICIENT_EVIDENCE = "INSUFFICIENT_EVIDENCE"
|
||||
APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
|
||||
UNKNOWN_CONTRACT = "UNKNOWN_CONTRACT"
|
||||
SCOPE_VIOLATION = "SCOPE_VIOLATION"
|
||||
VERIFICATION_FAILED = "VERIFICATION_FAILED"
|
||||
LAYOUT_VIOLATION = "LAYOUT_VIOLATION"
|
||||
PATTERN_CONFLICT = "PATTERN_CONFLICT"
|
||||
STORE_WRITE_FAILED = "STORE_WRITE_FAILED"
|
||||
STALE_STATE = "STALE_STATE"
|
||||
|
||||
|
||||
def _envelope(
|
||||
operation: str,
|
||||
*,
|
||||
success: bool,
|
||||
ids: dict | None = None,
|
||||
state: dict | None = None,
|
||||
evidence: list | None = None,
|
||||
errors: list | None = None,
|
||||
warnings: list | None = None,
|
||||
**extra,
|
||||
) -> dict:
|
||||
env = {
|
||||
"schema": INTERFACE_SCHEMA_VERSION,
|
||||
"operation": operation,
|
||||
"success": success,
|
||||
"ids": ids or {},
|
||||
"state": state or {},
|
||||
"evidence": evidence or [],
|
||||
"errors": errors or [],
|
||||
"warnings": warnings or [],
|
||||
}
|
||||
env.update(extra)
|
||||
return env
|
||||
|
||||
|
||||
class ConstructionInterface:
|
||||
"""The builder-facing construction lifecycle for ONE project target.
|
||||
|
||||
Rune-internal mode (default):
|
||||
ConstructionInterface(builder="mimo")
|
||||
External-project mode:
|
||||
ConstructionInterface(builder="mimo",
|
||||
project_store="/path/to/.rune/scaffold",
|
||||
project_id="that-project")
|
||||
|
||||
`builder` is recorded for audit (session record, §20) and NEVER changes
|
||||
construction semantics (§17).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
builder: str = "unknown",
|
||||
*,
|
||||
project_store: str | None = None,
|
||||
project_id: str | None = None,
|
||||
spine_store: str = "store/spine/current.json",
|
||||
):
|
||||
if not builder:
|
||||
raise ValueError("builder identity required (recorded for audit)")
|
||||
self.builder = builder
|
||||
self.spine_store = spine_store
|
||||
if project_store:
|
||||
self.scaffold = sc.Scaffold(project_store, project_id or Path(project_store).name)
|
||||
self.mode = "external_project"
|
||||
else:
|
||||
self.scaffold = sc.Scaffold("store/scaffold", "issue161-harness")
|
||||
self.mode = "rune_internal"
|
||||
|
||||
# -- authority access (read-only projections for builders) ----------------
|
||||
|
||||
def _authorities(self) -> dict[str, dict]:
|
||||
"""The structural authorities for THIS mode. Rune-internal consults
|
||||
the real Spine store; external mode consults only the project
|
||||
scaffold (a Rune-independent project has no Rune Spine)."""
|
||||
auth: dict[str, dict] = {"project_scaffold": self.scaffold.load()}
|
||||
if self.mode == "rune_internal":
|
||||
sd = spine_mod.load(Path(self.spine_store))
|
||||
if sd is not None:
|
||||
auth["rune_spine"] = sd
|
||||
return auth
|
||||
|
||||
def _record_session(self, doc: dict, operation: str, detail: str) -> None:
|
||||
"""Builder session record (§20): compact, no transcripts."""
|
||||
doc.setdefault("observations", []).append(
|
||||
{
|
||||
"kind": "builder_session",
|
||||
"builder": self.builder,
|
||||
"mode": self.mode,
|
||||
"operation": operation,
|
||||
"detail": detail[:200],
|
||||
"observed_at": ss.now(),
|
||||
}
|
||||
)
|
||||
|
||||
# ── §5 submit_need ────────────────────────────────────────────────────────
|
||||
|
||||
def classify(self, objective: str, *, target_project: str | None = None,
|
||||
project_exists: bool | None = None,
|
||||
explicit_override: str | None = None,
|
||||
override_authority: str | None = None) -> dict:
|
||||
"""§11/§15: contextual construction-mode classification through the
|
||||
ONE canonical classifier. Builders (MiMo etc.) and Rune's native
|
||||
backend get identical semantics. UNKNOWN comes back with probe
|
||||
questions, never a silent choice."""
|
||||
from rune.construction_mode import classify_mode
|
||||
d = classify_mode(
|
||||
objective, target_project=target_project,
|
||||
project_exists=project_exists,
|
||||
explicit_override=explicit_override,
|
||||
override_authority=override_authority or self.builder)
|
||||
env = _envelope("classify", success=d.mode != "UNKNOWN",
|
||||
state={**d.to_dict(), "builder": self.builder})
|
||||
return env
|
||||
|
||||
def construction_context(self, contract_id: str | None = None) -> dict:
|
||||
"""§15 SHARED CONSTRUCTION CONTEXT: typed projection derived from
|
||||
canonical authorities (contract store, decision trace, verification
|
||||
state). Serves Rune TUI + Construction Interface without duplicating
|
||||
state machines. NOT a second authority."""
|
||||
doc = self.scaffold.load()
|
||||
ctx: dict = {"mode": None, "project": self.scaffold.project_id,
|
||||
"builder": self.builder}
|
||||
contracts = [e for e in doc["entities"]
|
||||
if e["kind"] == "construction_contract"]
|
||||
target = None
|
||||
if contract_id:
|
||||
target = self.scaffold.contract(doc, contract_id)
|
||||
else:
|
||||
# most recent non-closed contract = the active construction
|
||||
open_contracts = [e for e in contracts
|
||||
if e.get("status") == "open"]
|
||||
target = (sorted(open_contracts,
|
||||
key=lambda e: e.get("observed_at", ""))[-1]
|
||||
if open_contracts else None)
|
||||
if target is None:
|
||||
ctx.update({"active": False,
|
||||
"note": "no active construction"})
|
||||
return _envelope("construction_context", success=True,
|
||||
state=ctx)
|
||||
t = target.get("ext", {}).get("decision_trace", {})
|
||||
ctx.update({
|
||||
"active": True,
|
||||
"contract_id": target["id"].split(":", 1)[1],
|
||||
"need": t.get("need", {}).get("objective", "")[:200],
|
||||
"resolution": t.get("resolution", ""),
|
||||
"construction_mode": t.get("construction_mode", {}).get("mode"),
|
||||
"phase": target.get("phase"), "status": target.get("status"),
|
||||
"scope": target["identities"].get("scope", []),
|
||||
"obligations": _obligations_projection(target),
|
||||
"patterns": target.get("ext", {}).get("pattern_refs", []),
|
||||
"approval": t.get("approval", {}),
|
||||
"completeness": t.get("completeness", {}).get("verdict"),
|
||||
"unresolved": target.get("ext", {}).get("unresolved_surfaces", []),
|
||||
"next_action": target["identities"].get("next_action", ""),
|
||||
})
|
||||
return _envelope("construction_context", success=True, state=ctx)
|
||||
|
||||
def capabilities(self) -> dict:
|
||||
"""§28 schema negotiation: builders detect the supported interface
|
||||
schema and operation set BEFORE calling operations. Also exposes the
|
||||
error-state vocabulary so builders can switch on states without
|
||||
parsing prose. Read-only; no contract required."""
|
||||
return _envelope(
|
||||
"capabilities",
|
||||
success=True,
|
||||
state={
|
||||
"schema_version": INTERFACE_SCHEMA_VERSION,
|
||||
"mode": self.mode,
|
||||
"project": self.scaffold.project_id,
|
||||
"operations": [
|
||||
"submit_need",
|
||||
"inspect_decision",
|
||||
"get_scope",
|
||||
"get_obligations",
|
||||
"report_observation",
|
||||
"verify",
|
||||
"transition",
|
||||
"close",
|
||||
"resume",
|
||||
"amend_scope",
|
||||
"capabilities",
|
||||
],
|
||||
"error_states": sorted(v for k, v in vars(Errors).items() if not k.startswith("_")),
|
||||
},
|
||||
)
|
||||
|
||||
def submit_need(
|
||||
self,
|
||||
objective: str,
|
||||
*,
|
||||
origin: str = "builder",
|
||||
constraints: dict | None = None,
|
||||
affected_surface: list[str] | None = None,
|
||||
evidence_refs: list[str] | None = None,
|
||||
approval_context: str = "",
|
||||
task_ref: str = "",
|
||||
generate: bool = True,
|
||||
contract_id: str | None = None,
|
||||
) -> dict:
|
||||
"""Submit a structured Need; run the full live decision path;
|
||||
optionally generate the Construction Contract. Returns decision
|
||||
summary + contract ref + outstanding questions. Does NOT decide
|
||||
approval — that surfaces via state.approval."""
|
||||
if not objective or not objective.strip():
|
||||
return _envelope(
|
||||
"submit_need",
|
||||
success=False,
|
||||
errors=[{"error": Errors.NEED_INCOMPLETE, "detail": "objective is required"}],
|
||||
)
|
||||
need = cd.Need(
|
||||
need_id=f"need-{ss.now()}-{self.builder}",
|
||||
objective=objective,
|
||||
origin=origin,
|
||||
project=self.scaffold.project_id,
|
||||
task_ref=task_ref,
|
||||
constraints=dict(constraints or {}),
|
||||
affected_surface=list(affected_surface or []),
|
||||
evidence_refs=list(evidence_refs or []),
|
||||
approval_context=approval_context,
|
||||
)
|
||||
doc = self.scaffold.load()
|
||||
authorities = self._authorities()
|
||||
trace = cd.make_decision(need, authorities=authorities, project_toolchain={"existing_language": "python"})
|
||||
ids: dict = {"need_id": need.need_id}
|
||||
state: dict = {
|
||||
"resolution": trace["resolution"],
|
||||
"completeness": trace["completeness"]["verdict"],
|
||||
"research": trace["research"],
|
||||
"approval": trace["approval"],
|
||||
"builder": self.builder,
|
||||
}
|
||||
errors, warnings = [], []
|
||||
if trace["completeness"]["verdict"] == cd.MATERIAL_GAP:
|
||||
errors.append(
|
||||
{
|
||||
"error": Errors.INSUFFICIENT_EVIDENCE,
|
||||
"detail": "material requirement gap",
|
||||
"gaps": trace["completeness"]["gaps"],
|
||||
"probe_questions": trace["completeness"]["probe_questions"],
|
||||
}
|
||||
)
|
||||
if trace["obligations"].get("pattern_conflicts"):
|
||||
errors.append(
|
||||
{
|
||||
"error": Errors.PATTERN_CONFLICT,
|
||||
"detail": "pattern obligations conflict",
|
||||
"conflicts": trace["obligations"]["pattern_conflicts"],
|
||||
}
|
||||
)
|
||||
if trace["approval"]["required"]:
|
||||
errors.append({"error": Errors.APPROVAL_REQUIRED, "reasons": trace["approval"]["reasons"]})
|
||||
if trace["resolution"] == "INSUFFICIENT_EVIDENCE" and not errors:
|
||||
errors.append({"error": Errors.INSUFFICIENT_EVIDENCE, "detail": trace["rationale"][:200]})
|
||||
contract = None
|
||||
if generate and trace["resolution"] not in ("INSUFFICIENT_EVIDENCE",):
|
||||
cid = contract_id or f"BUILDER-{ss.now()}-{self.builder}"
|
||||
cd.generate_contract(
|
||||
self.scaffold,
|
||||
doc,
|
||||
trace,
|
||||
contract_id=cid,
|
||||
scope=None,
|
||||
proposer_note=f"via Construction Interface by {self.builder}",
|
||||
)
|
||||
ids["contract_id"] = cid
|
||||
state["contract_phase"] = "draft"
|
||||
state["next_action"] = "review for approval"
|
||||
warnings.append({"warning": "contract generated in draft; requires approval transition before building"})
|
||||
self._record_session(doc, "submit_need", f"{need.need_id} → {trace['resolution']}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"submit_need",
|
||||
success=False,
|
||||
ids=ids,
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
return _envelope("submit_need", success=not errors, ids=ids, state=state, errors=errors, warnings=warnings)
|
||||
|
||||
# ── §6 inspect_decision ───────────────────────────────────────────────────
|
||||
|
||||
def inspect_decision(self, contract_id: str) -> dict:
|
||||
"""Read-only projection of the durable decision trace (§6)."""
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"inspect_decision",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
t = c.get("ext", {}).get("decision_trace", {})
|
||||
return _envelope(
|
||||
"inspect_decision",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", "")},
|
||||
state={
|
||||
"requirement": t.get("requirement", {}),
|
||||
"completeness": t.get("completeness", {}),
|
||||
"candidates": t.get("candidates", []),
|
||||
"fits": [f for f in t.get("fits", []) if f["fit"] != "UNKNOWN"],
|
||||
"resolution": t.get("resolution", ""),
|
||||
"rationale": t.get("rationale", ""),
|
||||
"toolchain": t.get("toolchain", {}),
|
||||
"patterns_considered": t.get("patterns_considered", []),
|
||||
"structural_obligations": {
|
||||
k: t.get("obligations", {}).get(k, [])
|
||||
for k in ("regression", "verification", "composition", "acquisition")
|
||||
},
|
||||
"pattern_obligations": t.get("obligations", {}).get("pattern", []),
|
||||
"approval": t.get("approval", {}),
|
||||
"iterations": t.get("iterations", []),
|
||||
"builder": self.builder,
|
||||
},
|
||||
)
|
||||
|
||||
# ── §7 get_scope ──────────────────────────────────────────────────────────
|
||||
|
||||
def get_scope(self, contract_id: str) -> dict:
|
||||
"""Mechanical builder-facing scope (§7): what may I touch?"""
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"get_scope",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
ext = c.get("ext", {})
|
||||
return _envelope(
|
||||
"get_scope",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"allowed_paths": c["identities"]["scope"],
|
||||
"affected_structural_entities": c["identities"]["affected_entities"],
|
||||
"expected_new_entities": c["identities"].get("expected_new_entities", []),
|
||||
"affected_paths": ext.get("affected_paths", []),
|
||||
"affected_ui_surfaces": ext.get("affected_ui_surfaces", []),
|
||||
"workflow_config_surfaces": ext.get("workflow_config_surfaces", []),
|
||||
"unresolved_surfaces": ext.get("unresolved_surfaces", []),
|
||||
"expected_runtime_effects": ext.get("expected_runtime_effects", []),
|
||||
"layout_obligations": t_layout(ext),
|
||||
"scope_amendments": ext.get("scope_amendments", []),
|
||||
"builder": self.builder,
|
||||
},
|
||||
)
|
||||
|
||||
def get_obligations(self, contract_id: str) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"get_obligations",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
return _envelope(
|
||||
"get_obligations",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={"obligations": _obligations_projection(c), "builder": self.builder},
|
||||
)
|
||||
|
||||
def report_observation(self, contract_id: str, observations: Any) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
if not isinstance(observations, list) or not observations:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.NEED_INCOMPLETE, "detail": "observations list required"}],
|
||||
)
|
||||
items = []
|
||||
for o in observations:
|
||||
if not isinstance(o, dict) or not o.get("entity"):
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.NEED_INCOMPLETE, "detail": "each observation needs an entity"}],
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"entity": o["entity"],
|
||||
"source": o.get("source", f"builder:{self.builder}"),
|
||||
"detail": str(o.get("detail", ""))[:300],
|
||||
}
|
||||
)
|
||||
ss.record_observed_impact(doc, contract_id, items)
|
||||
self._record_session(doc, "report_observation", f"{len(items)} observation(s) on {contract_id}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"recorded": len(items),
|
||||
"note": "evidence recorded; satisfaction is determined by Scaffolding gates, not builder claims",
|
||||
},
|
||||
)
|
||||
|
||||
def verify(
|
||||
self, contract_id: str, *, changed_files: list[str] | None = None, layout_observations: dict | None = None
|
||||
) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"verify",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
state: dict = {"phase": c.get("phase"), "status": c.get("status")}
|
||||
errors, warnings = [], []
|
||||
if changed_files is not None:
|
||||
scope = self.scaffold.check_scope(doc, contract_id, changed_files)
|
||||
state["scope_check"] = scope
|
||||
if scope["verdict"] == "red":
|
||||
errors.append({"error": Errors.SCOPE_VIOLATION, "out_of_scope": scope["out_of_scope"]})
|
||||
elif scope["verdict"] == "unknown":
|
||||
warnings.append({"warning": "scope UNKNOWN — contract has no approved scope paths"})
|
||||
if layout_observations:
|
||||
from rune import layout_contract as lc
|
||||
|
||||
gate = lc.check_layout(doc, layout_observations)
|
||||
state["layout_gate"] = gate["verdict"]
|
||||
if gate["verdict"] == "FAIL":
|
||||
errors.append(
|
||||
{"error": Errors.LAYOUT_VIOLATION, "checks": [x for x in gate["checks"] if x["verdict"] == "FAIL"]}
|
||||
)
|
||||
elif gate["verdict"] == "UNKNOWN":
|
||||
warnings.append({"warning": "layout UNKNOWN — never PASS"})
|
||||
obligations = _obligations_projection(c)
|
||||
q = c.get("ext", {}).get("contract_quality")
|
||||
if q:
|
||||
state["contract_quality"] = {
|
||||
k: q[k]
|
||||
for k in (
|
||||
"correctly_predicted",
|
||||
"expected_new_observed",
|
||||
"expected_runtime_observed",
|
||||
"genuinely_unexpected_new",
|
||||
"scope_check",
|
||||
)
|
||||
if k in q
|
||||
}
|
||||
state["obligations_total"] = len(obligations)
|
||||
state["remaining"] = (
|
||||
"closure requires phase transitions through verifying with evidence; builder cannot self-certify"
|
||||
)
|
||||
return _envelope(
|
||||
"verify",
|
||||
success=not errors,
|
||||
ids={"contract_id": contract_id},
|
||||
state=state,
|
||||
errors=errors,
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
def transition(self, contract_id: str, phase: str, *, evidence_ref: str = "") -> dict:
|
||||
doc = self.scaffold.load()
|
||||
try:
|
||||
self.scaffold.transition_contract(
|
||||
doc, contract_id, phase, evidence_ref=evidence_ref or f"builder:{self.builder}"
|
||||
)
|
||||
except sc.IllegalPhaseTransition as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.VERIFICATION_FAILED, "detail": str(e)}],
|
||||
)
|
||||
except ValueError as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": str(e)}],
|
||||
)
|
||||
self._record_session(doc, "transition", f"{contract_id} → {phase}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
return _envelope("transition", success=True, ids={"contract_id": contract_id}, state={"phase": phase})
|
||||
|
||||
def close(self, contract_id: str, *, observed_evidence: list[dict], scope_files: list[str] | None = None) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
try:
|
||||
self.scaffold.close_contract(doc, contract_id, observed_evidence=observed_evidence, scope_files=scope_files)
|
||||
except sc.IllegalPhaseTransition as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.VERIFICATION_FAILED, "detail": str(e)}],
|
||||
)
|
||||
except ValueError as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": str(e)}],
|
||||
)
|
||||
self._record_session(doc, "close", f"{contract_id} closed")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
closed = self.scaffold.contract(doc, contract_id)
|
||||
if closed is None:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": "contract vanished after close"}],
|
||||
)
|
||||
q = closed["ext"]["contract_quality"]
|
||||
return _envelope(
|
||||
"close",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"phase": "closed",
|
||||
"contract_quality": {
|
||||
k: q[k]
|
||||
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
|
||||
if k in q
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def resume(self, contract_id: str) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"resume",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
ext = c.get("ext", {})
|
||||
t = ext.get("decision_trace", {})
|
||||
return _envelope(
|
||||
"resume",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", "")},
|
||||
state={
|
||||
"mode": self.mode,
|
||||
"need": t.get("need", {}),
|
||||
"decision": {
|
||||
"resolution": t.get("resolution"),
|
||||
"completeness": t.get("completeness", {}).get("verdict"),
|
||||
"toolchain": t.get("toolchain", {}).get("selected_language", ""),
|
||||
},
|
||||
"phase": c.get("phase"),
|
||||
"status": c.get("status"),
|
||||
"scope": c["identities"]["scope"],
|
||||
"obligations": _obligations_projection(c),
|
||||
"patterns": c.get("ext", {}).get("pattern_refs", []),
|
||||
"evidence": [cp.get("evidence", "")[:80] for cp in c.get("checkpoints", [])],
|
||||
"unresolved": ext.get("unresolved_surfaces", []),
|
||||
"approval": t.get("approval", {}),
|
||||
"next_action": c["identities"].get("next_action", ""),
|
||||
"last_builder_observations": [
|
||||
o
|
||||
for o in doc.get("observations", [])
|
||||
if o.get("kind") == "observed_consequence" and o.get("task") == contract_id
|
||||
][-5:],
|
||||
"builder": self.builder,
|
||||
},
|
||||
)
|
||||
|
||||
def amend_scope(self, contract_id: str, add_paths: list[str], *, reason: str, authority: str = "user") -> dict:
|
||||
if authority != "user":
|
||||
return _envelope(
|
||||
"amend_scope",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[
|
||||
{
|
||||
"error": Errors.APPROVAL_REQUIRED,
|
||||
"detail": "scope expansion requires user authority — builders cannot broaden their own scope",
|
||||
}
|
||||
],
|
||||
)
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"amend_scope",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
c["identities"]["scope"] = sorted(set(c["identities"]["scope"]) | set(add_paths))
|
||||
c.setdefault("ext", {}).setdefault("scope_amendments", []).append(
|
||||
{"added": sorted(add_paths), "reason": reason[:200], "authorized_by": "user"}
|
||||
)
|
||||
self._record_session(doc, "amend_scope", f"{contract_id} +{len(add_paths)} paths (user)")
|
||||
self.scaffold.save(doc)
|
||||
return _envelope(
|
||||
"amend_scope", success=True, ids={"contract_id": contract_id}, state={"scope": c["identities"]["scope"]}
|
||||
)
|
||||
|
||||
# explicit method binding (readable, debuggable — no locals() tricks)
|
||||
|
||||
|
||||
def t_layout(ext: dict) -> list[dict]:
|
||||
"""Layout obligations from a contract's decision trace (helper)."""
|
||||
return ext.get("decision_trace", {}).get("layout_obligations", [])
|
||||
|
||||
|
||||
# ── §8 get_obligations / §9 report_observation / §10 verification ───────────
|
||||
|
||||
|
||||
def _obligations_projection(c: dict) -> list[dict]:
|
||||
t = c.get("ext", {}).get("decision_trace", {})
|
||||
obl = t.get("obligations", {})
|
||||
out = []
|
||||
for kind in ("regression", "verification", "composition", "acquisition"):
|
||||
for item in obl.get(kind, []):
|
||||
out.append(
|
||||
{
|
||||
"id": f"{kind}:{abs(hash(str(item))) % 10**8}",
|
||||
"description": str(item)[:200],
|
||||
"provenance": "STRUCTURAL",
|
||||
"kind": kind,
|
||||
"status": "REQUIRED",
|
||||
}
|
||||
)
|
||||
for p in obl.get("pattern", []):
|
||||
out.append(
|
||||
{
|
||||
"id": p["id"],
|
||||
"description": p["obligation"],
|
||||
"provenance": "PATTERN",
|
||||
"pattern": f"{p['pattern']}@r{p['revision']}",
|
||||
"pattern_applicability": p.get("pattern_applicability", ""),
|
||||
"evidence": p.get("reason", ""),
|
||||
"status": p.get("verdict", "REQUIRED"),
|
||||
}
|
||||
)
|
||||
for lo in t.get("layout_obligations", []):
|
||||
out.append(
|
||||
{
|
||||
"id": f"layout:{lo['component']}",
|
||||
"description": lo["obligation"],
|
||||
"provenance": "LAYOUT_CONTRACT",
|
||||
"status": "REQUIRED",
|
||||
"verification": lo["verification"],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _obligation_scope_files(obligations: list[dict]) -> list[str]:
|
||||
"""Obligation ids referenced by closure scope checks."""
|
||||
return []
|
||||
|
||||
|
||||
# ── CLI transport (§3/§12/§27) ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
"""JSON CLI — same convention as rune-spine/rune-scaffold mains. Every
|
||||
operation prints one JSON envelope; exit 0 = success, 1 = operation
|
||||
failure (the envelope carries the explicit error state)."""
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
if not argv or argv[0] in ("-h", "--help"):
|
||||
print(__doc__)
|
||||
return 0
|
||||
cmd = argv[0]
|
||||
args = argv[1:]
|
||||
builder = "cli"
|
||||
project_store = None
|
||||
project_id = None
|
||||
# --builder / --project-store / --project-id flags
|
||||
for flag, key in (("--builder", "b"), ("--project-store", "s"), ("--project-id", "p")):
|
||||
while flag in args:
|
||||
i = args.index(flag)
|
||||
val = args[i + 1]
|
||||
del args[i : i + 2]
|
||||
if flag == "--builder":
|
||||
builder = val
|
||||
elif flag == "--project-store":
|
||||
project_store = val
|
||||
else:
|
||||
project_id = val
|
||||
|
||||
iface = ConstructionInterface(builder=builder, project_store=project_store, project_id=project_id)
|
||||
|
||||
def _out(env: dict) -> int:
|
||||
print(json.dumps(env, indent=1, default=str))
|
||||
return 0 if env["success"] else 1
|
||||
|
||||
if cmd == "capabilities":
|
||||
return _out(iface.capabilities())
|
||||
if cmd == "classify":
|
||||
payload = json.loads(args[0]) if args else {}
|
||||
return _out(iface.classify(
|
||||
payload.get("objective", ""),
|
||||
target_project=payload.get("target_project"),
|
||||
project_exists=payload.get("project_exists"),
|
||||
explicit_override=payload.get("explicit_override"),
|
||||
override_authority=payload.get("override_authority")))
|
||||
if cmd == "context":
|
||||
return _out(iface.construction_context(args[0] if args else None))
|
||||
if cmd == "submit-need":
|
||||
payload = json.loads(args[0]) if args else {}
|
||||
return _out(
|
||||
iface.submit_need(
|
||||
payload.get("objective", ""),
|
||||
origin=payload.get("origin", "builder"),
|
||||
constraints=payload.get("constraints"),
|
||||
affected_surface=payload.get("affected_surface"),
|
||||
evidence_refs=payload.get("evidence_refs"),
|
||||
approval_context=payload.get("approval_context", ""),
|
||||
generate=payload.get("generate", True),
|
||||
)
|
||||
)
|
||||
if cmd == "decision":
|
||||
return _out(iface.inspect_decision(args[0]))
|
||||
if cmd == "scope":
|
||||
return _out(iface.get_scope(args[0]))
|
||||
if cmd == "obligations":
|
||||
return _out(iface.get_obligations(args[0]))
|
||||
if cmd == "observe":
|
||||
return _out(iface.report_observation(args[0], json.loads(args[1])))
|
||||
if cmd == "verify":
|
||||
return _out(iface.verify(args[0], changed_files=(json.loads(args[1]) if len(args) > 1 else None)))
|
||||
if cmd == "transition":
|
||||
return _out(iface.transition(args[0], args[1], evidence_ref=args[2] if len(args) > 2 else ""))
|
||||
if cmd == "close":
|
||||
return _out(
|
||||
iface.close(
|
||||
args[0],
|
||||
observed_evidence=json.loads(args[1]),
|
||||
scope_files=(json.loads(args[2]) if len(args) > 2 else None),
|
||||
)
|
||||
)
|
||||
if cmd == "resume":
|
||||
return _out(iface.resume(args[0]))
|
||||
return _out(
|
||||
_envelope(cmd, success=False, errors=[{"error": "UNKNOWN_OPERATION", "detail": f"unknown command: {cmd}"}])
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,344 @@
|
||||
"""construction_mode.py — contextual construction classification.
|
||||
|
||||
SCAFFOLD-TRANCHE-0010. ONE canonical home for the construction-mode
|
||||
decision (§2/§24: no competing router). Consumers — Rune's native backend
|
||||
path, the Construction Interface (MiMo/other builders), manual operator
|
||||
override — all call THIS.
|
||||
|
||||
The distinction (§3-4):
|
||||
|
||||
NOT_CONSTRUCTION not a build request at all
|
||||
LOCAL_CHANGE tiny fix / bounded patch — governance without
|
||||
ceremony (lightest path)
|
||||
EXISTING_PROJECT_CHANGE feature/repair/refactor/migration INSIDE an
|
||||
existing project — attaches to the existing
|
||||
Project Scaffold / Rune Spine
|
||||
PROJECT_BUILD genuinely NEW app/tool/service/project —
|
||||
fuller lifecycle (identity, repo decision,
|
||||
PRD state, bootstrap)
|
||||
UNKNOWN insufficient evidence — ask/probe, NEVER
|
||||
silently choose
|
||||
|
||||
KEY SEMANTIC DISTINCTION (§3): capability resolution BUILD_NEW does NOT
|
||||
imply PROJECT_BUILD. Building a new parser module inside Rune is
|
||||
EXISTING_PROJECT_CHANGE. Conversely "build me a standalone scheduler app"
|
||||
is PROJECT_BUILD before any capability decision exists. Tested explicitly.
|
||||
|
||||
NOT keyword-authoritative (§4): "build confidence" / "make a list" do not
|
||||
become PROJECT_BUILD. Deterministic evidence comes from:
|
||||
- explicit target project identity (constraint or recorded binding)
|
||||
- whether a Project Scaffold exists for the target
|
||||
- standalone/own-lifecycle language ("standalone", "new app",
|
||||
"new service", "own repository")
|
||||
- scoping words ("this", "existing", "the module") pointing at a
|
||||
known target
|
||||
Deterministic evidence is insufficient → UNKNOWN with probe questions.
|
||||
Model interpretation may be PROPOSED (recorded, PENDING — never authority,
|
||||
§5/§12 of the broader architecture).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
__all__ = ["ConstructionMode", "classify_mode", "MODES"]
|
||||
|
||||
MODES = ("NOT_CONSTRUCTION", "LOCAL_CHANGE", "EXISTING_PROJECT_CHANGE", "PROJECT_BUILD", "UNKNOWN")
|
||||
|
||||
# deterministic lexical evidence — presence contributes, absence never
|
||||
# decides alone; every rule requires corroboration (§4)
|
||||
_STANDALONE_WORDS = {
|
||||
"standalone",
|
||||
"new app",
|
||||
"new application",
|
||||
"new service",
|
||||
"new tool",
|
||||
"new project",
|
||||
"new repository",
|
||||
"own repo",
|
||||
"from scratch",
|
||||
"separate app",
|
||||
"separate service",
|
||||
"greenfield",
|
||||
"scaffold a new",
|
||||
"new web",
|
||||
}
|
||||
_LOCAL_WORDS = {
|
||||
"typo",
|
||||
"small fix",
|
||||
"tiny fix",
|
||||
"quick fix",
|
||||
"patch",
|
||||
"minor",
|
||||
"one-liner",
|
||||
"cosmetic",
|
||||
"comment fix",
|
||||
"small correction",
|
||||
}
|
||||
# "this" alone is deixis without a referent — only NAMED-scope phrases
|
||||
# ("the module", "this project", ...) or an explicit target_project bind
|
||||
# scope. Bare "this" contributes nothing (Tranche 10 §4).
|
||||
_SCOPING_WORDS = {
|
||||
"existing",
|
||||
"the module",
|
||||
"the project",
|
||||
"the app",
|
||||
"the service",
|
||||
"current",
|
||||
"that module",
|
||||
"the codebase",
|
||||
"this module",
|
||||
"this project",
|
||||
"this app",
|
||||
}
|
||||
_CONSTRUCTION_WORDS = {
|
||||
"build",
|
||||
"create",
|
||||
"make",
|
||||
"implement",
|
||||
"add",
|
||||
"write",
|
||||
"develop",
|
||||
"construct",
|
||||
"fix",
|
||||
"repair",
|
||||
"refactor",
|
||||
"migrate",
|
||||
"change",
|
||||
"extend",
|
||||
"replace",
|
||||
"feature",
|
||||
"export",
|
||||
"module",
|
||||
"service",
|
||||
"app",
|
||||
"tool",
|
||||
"component",
|
||||
"schema",
|
||||
"api",
|
||||
}
|
||||
_NOT_CONSTRUCTION_HINTS = {
|
||||
"explain",
|
||||
"what is",
|
||||
"how does",
|
||||
"why does",
|
||||
"list",
|
||||
"confidence",
|
||||
"improvisation",
|
||||
"summarize",
|
||||
"describe",
|
||||
"teach me",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConstructionMode:
|
||||
"""Typed classification decision with full evidence provenance."""
|
||||
|
||||
mode: str
|
||||
evidence: list[str] = field(default_factory=list)
|
||||
target_project: str | None = None
|
||||
project_authority: str | None = None # e.g. "rune_internal" | scaffold id
|
||||
confidence: float = 0.0
|
||||
clarification_required: bool = False
|
||||
probe_questions: list[str] = field(default_factory=list)
|
||||
bootstrap_required: bool = False
|
||||
override: dict | None = None # provenance if manually forced
|
||||
proposed_by_model: dict | None = None # recorded, never authority
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"mode": self.mode,
|
||||
"evidence": self.evidence,
|
||||
"target_project": self.target_project,
|
||||
"project_authority": self.project_authority,
|
||||
"confidence": self.confidence,
|
||||
"clarification_required": self.clarification_required,
|
||||
"probe_questions": self.probe_questions,
|
||||
"bootstrap_required": self.bootstrap_required,
|
||||
"override": self.override,
|
||||
"proposed_by_model": self.proposed_by_model,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "ConstructionMode":
|
||||
known = {
|
||||
k: d[k]
|
||||
for k in (
|
||||
"mode",
|
||||
"evidence",
|
||||
"target_project",
|
||||
"project_authority",
|
||||
"confidence",
|
||||
"clarification_required",
|
||||
"probe_questions",
|
||||
"bootstrap_required",
|
||||
"override",
|
||||
"proposed_by_model",
|
||||
)
|
||||
if k in d
|
||||
}
|
||||
return cls(**known)
|
||||
|
||||
|
||||
def _tokens(text: str) -> set[str]:
|
||||
return set(re.split(r"[^a-z0-9]+", text.lower()))
|
||||
|
||||
|
||||
def _contains_any(text: str, phrases) -> list[str]:
|
||||
t = text.lower()
|
||||
return [p for p in phrases if p in t]
|
||||
|
||||
|
||||
def classify_mode(
|
||||
objective: str,
|
||||
*,
|
||||
target_project: str | None = None,
|
||||
project_exists: bool | None = None,
|
||||
affected_surface: list[str] | None = None,
|
||||
explicit_override: str | None = None,
|
||||
override_authority: str | None = None,
|
||||
model_proposal: dict | None = None,
|
||||
) -> ConstructionMode:
|
||||
"""Classify the construction mode for one intent (§4-6).
|
||||
|
||||
`project_exists` — structural context: whether a Project Scaffold /
|
||||
project binding already exists for the target. Project existence
|
||||
MATERIALLY affects classification (§5/§20): the same request resolves
|
||||
differently with and without an existing project.
|
||||
|
||||
`explicit_override` — operator/builder authority feed (§11/§23):
|
||||
validated against known modes; provenance recorded. Override feeds the
|
||||
SAME machinery — it never bypasses the evidence record.
|
||||
"""
|
||||
ev: list[str] = []
|
||||
obj = objective or ""
|
||||
mode = None
|
||||
conf = 0.0
|
||||
bootstrap = False
|
||||
clarification = False
|
||||
probes: list[str] = []
|
||||
|
||||
# 1. explicit override (§23): authority-validated, provenance-carrying
|
||||
if explicit_override is not None:
|
||||
if explicit_override not in MODES:
|
||||
raise ValueError(f"override must be one of {MODES}, got {explicit_override!r}")
|
||||
ev.append(f"explicit override by {override_authority or 'operator'}")
|
||||
m = ConstructionMode(
|
||||
mode=explicit_override,
|
||||
evidence=ev,
|
||||
target_project=target_project,
|
||||
confidence=1.0,
|
||||
bootstrap_required=(explicit_override == "PROJECT_BUILD"),
|
||||
override={"authority": override_authority or "operator", "from_mode": None},
|
||||
)
|
||||
if explicit_override == "UNKNOWN":
|
||||
m.clarification_required = True
|
||||
return m
|
||||
|
||||
# 2. NOT_CONSTRUCTION: question/list/learning intents with no build verbs
|
||||
notc = _contains_any(obj.lower(), _NOT_CONSTRUCTION_HINTS)
|
||||
has_verb = bool(_tokens(obj) & _CONSTRUCTION_WORDS)
|
||||
if notc:
|
||||
# verb-object check: a construction verb whose object is a
|
||||
# non-constructible noun (list/confidence/explanation) is not a
|
||||
# construction request (§4: "build confidence", "make a list")
|
||||
non_constructible = {"list", "confidence", "explanation", "summary", "chord", "note"}
|
||||
obj_tokens = _tokens(obj)
|
||||
if not has_verb or obj_tokens & non_constructible:
|
||||
ev.append(f"non-construction intent markers: {notc[:3]}")
|
||||
ev.append(
|
||||
"construction-verb object is not a buildable artifact" if has_verb else "no construction verb present"
|
||||
)
|
||||
return ConstructionMode(mode="NOT_CONSTRUCTION", evidence=ev, confidence=0.85)
|
||||
|
||||
if not obj.strip():
|
||||
ev.append("empty objective")
|
||||
return ConstructionMode(
|
||||
mode="UNKNOWN",
|
||||
evidence=ev,
|
||||
confidence=0.0,
|
||||
clarification_required=True,
|
||||
probe_questions=["what should be built?"],
|
||||
)
|
||||
|
||||
standalone = _contains_any(obj.lower(), _STANDALONE_WORDS)
|
||||
local = _contains_any(obj.lower(), _LOCAL_WORDS)
|
||||
scoped = _contains_any(obj.lower(), _SCOPING_WORDS)
|
||||
|
||||
# 3. PROJECT_BUILD: standalone/new-lifecycle language AND no existing
|
||||
# project for the target (project existence materially matters, §5)
|
||||
if standalone:
|
||||
ev.append(f"standalone/new-lifecycle language: {standalone[:3]}")
|
||||
if project_exists is True:
|
||||
# §5: existing project preferred unless evidence says otherwise —
|
||||
# contradictory evidence → UNKNOWN, never silent choice
|
||||
ev.append("BUT target project already exists (structural evidence contradicts standalone language)")
|
||||
mode, conf, clarification = "UNKNOWN", 0.3, True
|
||||
probes.append("should this extend the existing project or start a genuinely separate one?")
|
||||
elif project_exists is None:
|
||||
ev.append("no project-existence evidence for the target")
|
||||
mode, conf, clarification = "UNKNOWN", 0.4, True
|
||||
probes.append("does a project for this already exist?")
|
||||
else:
|
||||
mode, conf = "PROJECT_BUILD", 0.9
|
||||
bootstrap = True
|
||||
# 4. LOCAL_CHANGE: explicit smallness + scoped target + no standalone
|
||||
elif local and (scoped or target_project or project_exists):
|
||||
ev.append(f"smallness markers: {local[:3]}")
|
||||
if scoped:
|
||||
ev.append(f"scoping words bind to known target: {scoped[:3]}")
|
||||
mode, conf = "LOCAL_CHANGE", 0.8
|
||||
# 5. EXISTING_PROJECT_CHANGE: scoping to a known/existing target.
|
||||
# T11 §1: a change scoped to a project with PROVEN non-existence must
|
||||
# NOT silently claim existing-project semantics — clarify instead.
|
||||
elif (scoped or target_project or project_exists is True) and has_verb:
|
||||
ev.append("change scoped to an existing project/target")
|
||||
if target_project:
|
||||
ev.append(f"target project: {target_project}")
|
||||
if project_exists is False:
|
||||
ev.append(
|
||||
"BUT structural evidence proves the target project "
|
||||
"does not exist — refusing to manufacture existing-"
|
||||
"project semantics"
|
||||
)
|
||||
mode, conf, clarification = "UNKNOWN", 0.3, True
|
||||
probes.append(
|
||||
f"project '{target_project or 'the target'}' has "
|
||||
"no structural record — is it new (project "
|
||||
"build) or does it live somewhere not bound?"
|
||||
)
|
||||
else:
|
||||
mode, conf = "EXISTING_PROJECT_CHANGE", 0.8
|
||||
# 6. construction verb but no scope and no standalone language → UNKNOWN
|
||||
elif has_verb:
|
||||
ev.append("construction verb present but no target-scope evidence and no standalone/new-lifecycle language")
|
||||
mode, conf, clarification = "UNKNOWN", 0.35, True
|
||||
probes.append("is this a change to an existing project or a new standalone one?")
|
||||
probes.append("which project/module is the target?")
|
||||
else:
|
||||
ev.append("insufficient lexical evidence either way")
|
||||
mode, conf, clarification = "UNKNOWN", 0.2, True
|
||||
probes.append("is this a construction request?")
|
||||
|
||||
if affected_surface and mode in ("LOCAL_CHANGE", "EXISTING_PROJECT_CHANGE", "UNKNOWN"):
|
||||
ev.append(f"affected surface known: {affected_surface[:2]}")
|
||||
if mode == "UNKNOWN":
|
||||
mode, conf = "EXISTING_PROJECT_CHANGE", 0.6
|
||||
clarification = False
|
||||
|
||||
# §5 model proposal: recorded, never authority
|
||||
proposed = model_proposal if model_proposal else None
|
||||
|
||||
return ConstructionMode(
|
||||
mode=mode,
|
||||
evidence=ev,
|
||||
target_project=target_project,
|
||||
project_authority=("rune_internal" if target_project in (None, "rune", "issue161-harness") else target_project),
|
||||
confidence=conf,
|
||||
clarification_required=clarification,
|
||||
probe_questions=probes,
|
||||
bootstrap_required=(mode == "PROJECT_BUILD"),
|
||||
proposed_by_model=proposed,
|
||||
)
|
||||
@@ -0,0 +1,496 @@
|
||||
"""construction_pattern.py — reusable evidence about HOW a class of thing
|
||||
is normally built.
|
||||
|
||||
SCAFFOLD-TRANCHE-006. Canonical distinction (tranche brief):
|
||||
|
||||
Construction Pattern = reusable knowledge for a CLASS of construction
|
||||
Project Scaffold = structural truth for THIS project
|
||||
Construction Contract = obligations for THIS change
|
||||
|
||||
A pattern is GUIDANCE, not authority (§3): project evidence always wins.
|
||||
A pattern must never overwrite contradictory project evidence — if the
|
||||
pattern assumes HTTP services and the project evidence says message-queue-
|
||||
only, the pattern adapts or becomes non-applicable.
|
||||
|
||||
Applicability (§4-5) is EVIDENCE-BACKED and deterministic: signals are
|
||||
checked against the affected surface + structural authorities. Verdicts:
|
||||
APPLIES / PARTIAL / DOES_NOT_APPLY / UNKNOWN. UNKNOWN never becomes APPLIES
|
||||
because a name sounds similar; name similarity establishes nothing. Model
|
||||
reasoning may PROPOSE a pattern (recorded, PENDING) — it may not certify
|
||||
applicability.
|
||||
|
||||
Obligation derivation (§6-7) is FILTERED by this project: every pattern
|
||||
obligation carries a `when` condition evaluated against project evidence;
|
||||
verdicts REQUIRED / APPLICABLE / NOT_APPLICABLE / UNKNOWN with the reason.
|
||||
Patterns REDUCE false-positive obligation expansion, never amplify it.
|
||||
|
||||
Provenance (§16): every rule answers "why is this here" with real Rune
|
||||
evidence — incidents, contracts, tests, user requirements. Patterns
|
||||
accumulate verified engineering knowledge, not model folklore.
|
||||
|
||||
Revision (§17): lightweight revision identity; contracts record the
|
||||
revision consumed so "which builds used the old rule" stays answerable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rune import structural_store as ss
|
||||
|
||||
__all__ = [
|
||||
"ConstructionPattern",
|
||||
"evaluate_applicability",
|
||||
"derive_pattern_obligations",
|
||||
"record_pattern_proposal",
|
||||
"APPLIES",
|
||||
"PARTIAL",
|
||||
"DOES_NOT_APPLY",
|
||||
"UNKNOWN",
|
||||
"OBL_REQUIRED",
|
||||
"OBL_APPLICABLE",
|
||||
"OBL_NOT_APPLICABLE",
|
||||
"OBL_UNKNOWN",
|
||||
]
|
||||
|
||||
APPLIES = "APPLIES"
|
||||
PARTIAL = "PARTIAL"
|
||||
DOES_NOT_APPLY = "DOES_NOT_APPLY"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
OBL_REQUIRED = "REQUIRED"
|
||||
OBL_APPLICABLE = "APPLICABLE"
|
||||
OBL_NOT_APPLICABLE = "NOT_APPLICABLE"
|
||||
OBL_UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
# ── pattern object (open-world) ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class ConstructionPattern:
|
||||
"""One reusable construction pattern. Open-world: `ext` carries anything
|
||||
the schema above does not name. NOT structural truth — guidance."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pattern_id: str,
|
||||
purpose: str,
|
||||
*,
|
||||
revision: int = 1,
|
||||
maturity: str = "CANDIDATE",
|
||||
signals: dict | None = None,
|
||||
obligations: list[dict] | None = None,
|
||||
provenance: list[str] | None = None,
|
||||
exclusions: list[str] | None = None,
|
||||
toolchain_notes: list[str] | None = None,
|
||||
runtime_effects: list[str] | None = None,
|
||||
**ext,
|
||||
):
|
||||
if not pattern_id:
|
||||
raise ValueError("pattern_id required")
|
||||
if not purpose:
|
||||
raise ValueError("purpose required")
|
||||
for o in obligations or []:
|
||||
for key in ("id", "obligation", "when"):
|
||||
if key not in o:
|
||||
raise ValueError(f"obligation missing {key!r}: {o}")
|
||||
self.pattern_id = pattern_id
|
||||
self.purpose = purpose
|
||||
self.revision = revision
|
||||
self.maturity = maturity
|
||||
self.signals = dict(signals or {})
|
||||
self.obligations = list(obligations or [])
|
||||
self.provenance = sorted(provenance or [])
|
||||
self.exclusions = sorted(exclusions or [])
|
||||
self.toolchain_notes = sorted(toolchain_notes or [])
|
||||
self.runtime_effects = sorted(runtime_effects or [])
|
||||
self.ext = dict(ext)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"pattern_id": self.pattern_id,
|
||||
"purpose": self.purpose,
|
||||
"revision": self.revision,
|
||||
"maturity": self.maturity,
|
||||
"signals": dict(self.signals),
|
||||
"obligations": [dict(o) for o in self.obligations],
|
||||
"provenance": list(self.provenance),
|
||||
"exclusions": list(self.exclusions),
|
||||
"toolchain_notes": list(self.toolchain_notes),
|
||||
"runtime_effects": list(self.runtime_effects),
|
||||
**self.ext,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "ConstructionPattern":
|
||||
src = dict(d) # never mutate the caller's dict
|
||||
known = {
|
||||
k: src.pop(k)
|
||||
for k in (
|
||||
"pattern_id",
|
||||
"purpose",
|
||||
"revision",
|
||||
"maturity",
|
||||
"signals",
|
||||
"obligations",
|
||||
"provenance",
|
||||
"exclusions",
|
||||
"toolchain_notes",
|
||||
"runtime_effects",
|
||||
)
|
||||
if k in src
|
||||
}
|
||||
return cls(**known, **src)
|
||||
|
||||
def revise(self, *, reason: str, evidence: list[str], changes: dict | None = None) -> "ConstructionPattern":
|
||||
"""Revision with provenance (§17): prior meaning not invisibly
|
||||
mutated — returns a NEW pattern at revision+1 recording the change."""
|
||||
d = self.to_dict()
|
||||
d.pop("pattern_id")
|
||||
rev = {**d, **(changes or {})}
|
||||
rev["revision"] = self.revision + 1
|
||||
rev.setdefault("revision_history", []).append(
|
||||
{
|
||||
"prior_revision": self.revision,
|
||||
"reason": reason[:200],
|
||||
"evidence": sorted(evidence),
|
||||
"changed": sorted((changes or {}).keys()),
|
||||
}
|
||||
)
|
||||
return ConstructionPattern(self.pattern_id, **rev)
|
||||
|
||||
|
||||
# ── model proposal seam (§5 — recorded, never authoritative) ────────────────
|
||||
|
||||
|
||||
def record_pattern_proposal(
|
||||
pattern_id: str, *, provider: str, model: str, evidence: list[str], confidence: float
|
||||
) -> dict:
|
||||
"""A model may PROPOSE a pattern; it may not certify applicability.
|
||||
Recorded with verification state PENDING until deterministic project
|
||||
evidence, research evidence, proven construction evidence, or an
|
||||
explicit user decision accepts it."""
|
||||
if not provider or not model:
|
||||
raise ValueError("pattern proposal requires provider+model")
|
||||
if not 0.0 <= confidence <= 1.0:
|
||||
raise ValueError("confidence must be in [0,1]")
|
||||
return {
|
||||
"kind": "pattern_proposal",
|
||||
"pattern_id": pattern_id,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"evidence": sorted(evidence),
|
||||
"confidence": confidence,
|
||||
"verification_state": "PENDING",
|
||||
}
|
||||
|
||||
|
||||
# ── applicability (§4 — deterministic signals only) ─────────────────────────
|
||||
|
||||
|
||||
def _surface_text(need, surface_result: dict) -> set[str]:
|
||||
"""Everything the affected surface offers as evidence: paths, entities,
|
||||
ui surfaces, regions, workflow configs, the objective text tokens."""
|
||||
tokens = set()
|
||||
for k in (
|
||||
"affected_paths",
|
||||
"affected_existing_entities",
|
||||
"affected_ui_surfaces",
|
||||
"affected_regions",
|
||||
"workflow_config_surfaces",
|
||||
):
|
||||
tokens.update(surface_result.get(k, []))
|
||||
tokens.update(need.affected_surface)
|
||||
# objective word tokens (deterministic split — not semantics)
|
||||
tokens.update(w.lower() for w in need.objective.replace("/", " ").replace("-", " ").split())
|
||||
return tokens
|
||||
|
||||
|
||||
# ── shared boundary-aware matchers (Tranche 7 §0) ──────────────────────────
|
||||
|
||||
def _path_matches(signal: str, path: str) -> bool:
|
||||
"""Boundary-aware path-signal matching: short (<4 char) signals match
|
||||
path TOKENS only (the 'ui'-in-'builtin' false-positive class); longer
|
||||
signals may use containment."""
|
||||
import re as _re
|
||||
|
||||
s = signal.lower()
|
||||
if len(s) >= 4:
|
||||
return s in path.lower()
|
||||
return s in set(_re.split(r"[^a-z0-9]+", path.lower()))
|
||||
|
||||
|
||||
def _entity_matches(signal: str, entity_id: str) -> bool:
|
||||
"""Same discipline for entity ids. Prefix signals ending in ':' (e.g.
|
||||
'ui:', 'region:') match the id's namespace prefix — structural kinds
|
||||
carry meaning in their prefix, and prefix equality is exact, not a
|
||||
substring accident."""
|
||||
import re as _re
|
||||
|
||||
s = signal.lower()
|
||||
if s.endswith(":"):
|
||||
return entity_id.lower().startswith(s)
|
||||
if len(s) >= 4:
|
||||
return s in entity_id.lower()
|
||||
return s in set(_re.split(r"[^a-z0-9:]+", entity_id.lower()))
|
||||
|
||||
|
||||
def evaluate_applicability(
|
||||
pattern: ConstructionPattern, need, surface_result: dict, authorities: dict[str, dict]
|
||||
) -> dict:
|
||||
"""Deterministic applicability verdict from the pattern's declared
|
||||
signals (§4). Signals (all optional; evidence-checked against the
|
||||
affected surface and structural authorities):
|
||||
surface_kinds subset of {source_path, workflow_config,
|
||||
ui_component, layout_region,
|
||||
structural_entity}
|
||||
path_contains any-of substrings expected in affected paths
|
||||
entities_match any-of entity-id prefixes/substrings
|
||||
objective_any any-of lowercase words expected in the objective
|
||||
contracts_match any-of contract ids implemented by affected entities
|
||||
|
||||
Verdict: count how many DECLARED signal groups produce evidence.
|
||||
all declared groups hit -> APPLIES
|
||||
some hit -> PARTIAL (or DOES_NOT_APPLY when the
|
||||
pattern's `strong` groups all miss)
|
||||
none / no signals -> UNKNOWN (never APPLIES by name)
|
||||
Project evidence overrides (§3): exclusions evaluated first — if the
|
||||
need explicitly declares the pattern non-applicable, DOES_NOT_APPLY
|
||||
with the exclusion recorded.
|
||||
"""
|
||||
# §3/§18: PROJECT evidence overrides the pattern. Exclusions may be
|
||||
# declared by the project/need (pattern_exclusions constraint) or by the
|
||||
# pattern itself (non-applicability domains).
|
||||
declared_exclusions = list(pattern.exclusions) + list(need.constraints.get("pattern_exclusions", []))
|
||||
for ex in declared_exclusions:
|
||||
if ex.lower() in need.objective.lower() or ex in need.constraints.get("pattern_exclusions", []):
|
||||
return {
|
||||
"pattern": pattern.pattern_id,
|
||||
"verdict": DOES_NOT_APPLY,
|
||||
"reason": f"declared exclusion matched: {ex}",
|
||||
"evidence": {"exclusion": ex, "source": "project/need declaration"},
|
||||
}
|
||||
|
||||
signals = pattern.signals
|
||||
if not signals:
|
||||
return {
|
||||
"pattern": pattern.pattern_id,
|
||||
"verdict": UNKNOWN,
|
||||
"reason": "pattern declares no applicability signals — nothing deterministic to check",
|
||||
"evidence": {},
|
||||
}
|
||||
tokens = _surface_text(need, surface_result)
|
||||
|
||||
groups_hit, groups_declared = [], 0
|
||||
evidence: dict = {}
|
||||
|
||||
def _check(group: str, candidates: list[str], present) -> None:
|
||||
nonlocal groups_declared
|
||||
if not candidates:
|
||||
return
|
||||
groups_declared += 1
|
||||
hits = [c for c in candidates if present(c)]
|
||||
if hits:
|
||||
groups_hit.append(group)
|
||||
evidence[group] = hits[:5]
|
||||
|
||||
_check(
|
||||
"surface_kinds",
|
||||
signals.get("surface_kinds", []),
|
||||
lambda c: (
|
||||
bool(
|
||||
surface_result.get(
|
||||
{
|
||||
"source_path": "affected_paths",
|
||||
"workflow_config": "workflow_config_surfaces",
|
||||
"ui_component": "affected_ui_surfaces",
|
||||
"layout_region": "affected_regions",
|
||||
}.get(c, ""),
|
||||
[],
|
||||
)
|
||||
)
|
||||
or any(r["kind"] == c for r in surface_result.get("resolutions", []))
|
||||
),
|
||||
)
|
||||
_check(
|
||||
"path_contains",
|
||||
[p for p in signals.get("path_contains", [])],
|
||||
lambda c: any(
|
||||
_path_matches(c, p)
|
||||
for p in surface_result.get("affected_paths", []) + surface_result.get("workflow_config_surfaces", [])
|
||||
),
|
||||
)
|
||||
_check(
|
||||
"entities_match",
|
||||
[e for e in signals.get("entities_match", [])],
|
||||
lambda c: any(
|
||||
_entity_matches(c, e) for e in surface_result.get("affected_existing_entities", [])
|
||||
),
|
||||
)
|
||||
_check(
|
||||
"objective_any",
|
||||
[w.lower() for w in signals.get("objective_any", [])],
|
||||
lambda c: c in {w.lower() for w in need.objective.replace("/", " ").split()},
|
||||
)
|
||||
contracts_match = signals.get("contracts_match", [])
|
||||
if contracts_match:
|
||||
groups_declared += 1
|
||||
hits = []
|
||||
for name, doc in authorities.items():
|
||||
for e in surface_result.get("affected_existing_entities", []):
|
||||
for r in ss.relationships(doc, from_id=e):
|
||||
if r["type"] in {"implements", "exposes", "provides", "serves"} and r["to"] in contracts_match:
|
||||
hits.append(f"{e}->{r['to']}")
|
||||
if hits:
|
||||
groups_hit.append("contracts_match")
|
||||
evidence["contracts_match"] = hits[:5]
|
||||
|
||||
if groups_declared == 0:
|
||||
return {
|
||||
"pattern": pattern.pattern_id,
|
||||
"verdict": UNKNOWN,
|
||||
"reason": "no applicable signal groups declared",
|
||||
"evidence": {},
|
||||
}
|
||||
if len(groups_hit) == groups_declared:
|
||||
verdict = APPLIES
|
||||
elif groups_hit:
|
||||
verdict = PARTIAL
|
||||
else:
|
||||
verdict = DOES_NOT_APPLY
|
||||
return {
|
||||
"pattern": pattern.pattern_id,
|
||||
"verdict": verdict,
|
||||
"reason": f"{len(groups_hit)}/{groups_declared} signal groups "
|
||||
f"produced evidence: {sorted(groups_hit) or 'none'}",
|
||||
"evidence": evidence,
|
||||
}
|
||||
|
||||
|
||||
# ── obligation derivation (§6-7 — filtered by project evidence) ─────────────
|
||||
|
||||
|
||||
def _obligation_when_hit(obligation: dict, need, surface_result: dict, authorities: dict[str, dict]) -> tuple[str, str]:
|
||||
"""Evaluate one obligation's `when` condition against project evidence.
|
||||
Returns (verdict, reason). `when` shapes (all deterministic):
|
||||
path_contains: [substrings] — any affected/config path matches
|
||||
entities_match: [substrings] — any affected entity matches
|
||||
always: true — applies whenever the pattern applies
|
||||
contracts_touched: [ids] — contract in requirement or implemented
|
||||
by affected entities
|
||||
An obligation with no recognized `when` is UNKNOWN (never silently
|
||||
required)."""
|
||||
when = obligation.get("when")
|
||||
paths = [p for p in surface_result.get("affected_paths", [])] + [
|
||||
p for p in surface_result.get("workflow_config_surfaces", [])
|
||||
]
|
||||
entities = (
|
||||
[e for e in surface_result.get("affected_existing_entities", [])]
|
||||
+ [u for u in surface_result.get("affected_ui_surfaces", [])]
|
||||
+ [r for r in surface_result.get("affected_regions", [])]
|
||||
)
|
||||
if when is None:
|
||||
return OBL_UNKNOWN, "obligation declares no applicability condition"
|
||||
if when.get("always") is True:
|
||||
return OBL_REQUIRED, "pattern-level obligation (always applies when the pattern applies)"
|
||||
if "path_contains" in when:
|
||||
hits = [p for sub in when["path_contains"] for p in paths if _path_matches(sub, p)]
|
||||
if hits:
|
||||
return OBL_APPLICABLE, f"affected path(s): {hits[:3]}"
|
||||
return OBL_NOT_APPLICABLE, ("no affected path matches " + str(when["path_contains"]))
|
||||
if "entities_match" in when:
|
||||
hits = [e for sub in when["entities_match"] for e in entities if _entity_matches(sub, e)]
|
||||
if hits:
|
||||
return OBL_APPLICABLE, f"affected entity/entities: {hits[:3]}"
|
||||
return OBL_NOT_APPLICABLE, ("no affected entity matches " + str(when["entities_match"]))
|
||||
if "contracts_touched" in when:
|
||||
touched = set(need.constraints.get("contracts", []))
|
||||
for name, doc in authorities.items():
|
||||
for e in entities:
|
||||
for r in ss.relationships(doc, from_id=e):
|
||||
if r["type"] in {"implements", "exposes", "provides", "serves"}:
|
||||
touched.add(r["to"])
|
||||
hits = [c for c in when["contracts_touched"] if c in touched]
|
||||
if hits:
|
||||
return OBL_APPLICABLE, f"contract(s) touched: {hits}"
|
||||
return OBL_NOT_APPLICABLE, ("touched contracts do not include " + str(when["contracts_touched"]))
|
||||
return OBL_UNKNOWN, f"unrecognized when-condition shape: {sorted(when)}"
|
||||
|
||||
|
||||
def derive_pattern_obligations(
|
||||
pattern: ConstructionPattern,
|
||||
need,
|
||||
surface_result: dict,
|
||||
authorities: dict[str, dict],
|
||||
project_exceptions: list[dict] | None = None,
|
||||
) -> dict:
|
||||
"""Derive this pattern's obligations FILTERED by project evidence (§7).
|
||||
Every returned obligation carries: pattern, revision, obligation text,
|
||||
verdict, reason/evidence, and provenance kind PATTERN (distinct from
|
||||
Tranche 5's STRUCTURAL derivation — §8 provenance separation).
|
||||
Project exceptions (§18) suppress with provenance retained."""
|
||||
project_exceptions = list(project_exceptions or [])
|
||||
out = {
|
||||
"pattern": pattern.pattern_id,
|
||||
"revision": pattern.revision,
|
||||
"accepted": [],
|
||||
"rejected": [],
|
||||
"unresolved": [],
|
||||
}
|
||||
for o in pattern.obligations:
|
||||
entry = {
|
||||
"id": o["id"],
|
||||
"obligation": o["obligation"],
|
||||
"pattern": pattern.pattern_id,
|
||||
"revision": pattern.revision,
|
||||
"provenance": "PATTERN",
|
||||
"pattern_provenance": list(pattern.provenance),
|
||||
}
|
||||
# project exception check first (§18) — suppress with provenance
|
||||
exc = next((x for x in project_exceptions if x.get("obligation") == o["id"]), None)
|
||||
if exc:
|
||||
entry.update(
|
||||
{
|
||||
"verdict": "EXCEPTED",
|
||||
"reason": exc.get("reason", ""),
|
||||
"exception_evidence": exc.get("evidence", ""),
|
||||
"exception_authority": exc.get("authority", "user"),
|
||||
}
|
||||
)
|
||||
out["rejected"].append(entry)
|
||||
continue
|
||||
verdict, reason = _obligation_when_hit(o, need, surface_result, authorities)
|
||||
entry["verdict"] = verdict
|
||||
entry["reason"] = reason
|
||||
if verdict in (OBL_REQUIRED, OBL_APPLICABLE):
|
||||
out["accepted"].append(entry)
|
||||
elif verdict == OBL_NOT_APPLICABLE:
|
||||
out["rejected"].append(entry)
|
||||
else:
|
||||
out["unresolved"].append(entry)
|
||||
return out
|
||||
|
||||
|
||||
def combine_pattern_obligations(derivations: list[dict]) -> dict:
|
||||
"""Combine multiple patterns' derivations (§26-27): duplicates of the
|
||||
same obligation id collapse (provenance lists merge); conflicting
|
||||
obligations (same id, contradictory verdicts across patterns) surface
|
||||
as conflicts requiring evidence/user decision — never silently chosen."""
|
||||
merged: dict[str, dict] = {}
|
||||
conflicts: list[dict] = []
|
||||
for d in derivations:
|
||||
for entry in d["accepted"]:
|
||||
oid = entry["id"]
|
||||
if oid in merged:
|
||||
prev = merged[oid]
|
||||
if prev["obligation"] != entry["obligation"]:
|
||||
conflicts.append(
|
||||
{
|
||||
"obligation_id": oid,
|
||||
"conflict": "same id, different text",
|
||||
"sources": [prev["pattern"], entry["pattern"]],
|
||||
}
|
||||
)
|
||||
continue
|
||||
prev.setdefault("also_from", []).append(entry["pattern"])
|
||||
continue
|
||||
merged[oid] = dict(entry)
|
||||
return {"obligations": sorted(merged.values(), key=lambda x: x["id"]), "conflicts": conflicts}
|
||||
@@ -0,0 +1,249 @@
|
||||
"""construction_patterns_builtin.py — the initial evidence-grounded pattern
|
||||
set (SCAFFOLD-TRANCHE-006 §10-14: 4 patterns, each derived from REAL Rune
|
||||
evidence, each appropriately immature).
|
||||
|
||||
This module is DATA, not a framework: pattern provenance answers "why is
|
||||
this rule here" with real incidents/contracts/tests. Maturity stays
|
||||
CANDIDATE/UNIT-PROVEN — existence is not trust (§15).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rune.construction_pattern import ConstructionPattern
|
||||
|
||||
__all__ = ["BUILTIN_PATTERNS", "get_pattern", "PATTERN_REGISTRY_REVISION"]
|
||||
|
||||
# Bumped when any builtin pattern's obligations change meaningfully.
|
||||
PATTERN_REGISTRY_REVISION = 1
|
||||
|
||||
CAPABILITY_ADAPTATION = ConstructionPattern(
|
||||
"capability-module-adaptation",
|
||||
"Adapting or extending an existing capability/module: prove the NEW "
|
||||
"behavior, preserve the OLD behavior (BUILD BUT DON'T BREAK).",
|
||||
maturity="UNIT-PROVEN",
|
||||
signals={
|
||||
"entities_match": ["module:", "capability:"],
|
||||
"path_contains": ["src/"],
|
||||
"objective_any": ["adapt", "extend", "modify", "preserve", "existing", "capability", "repair"],
|
||||
},
|
||||
obligations=[
|
||||
{
|
||||
"id": "adapt-new-behavior-proof",
|
||||
"obligation": "new behavior proven by tests before closure",
|
||||
"when": {"always": True},
|
||||
},
|
||||
{
|
||||
"id": "adapt-old-behavior-preserved",
|
||||
"obligation": "existing capability behavior preserved; original contracts remain valid",
|
||||
"when": {"always": True},
|
||||
},
|
||||
{
|
||||
"id": "adapt-consumers-verified",
|
||||
"obligation": "existing consumers of the adapted capability verified against their contracts",
|
||||
"when": {"entities_match": ["module:", "capability:"]},
|
||||
},
|
||||
{
|
||||
"id": "adapt-migration-detected",
|
||||
"obligation": "migration obligations detected and recorded if any consumer-visible interface changes",
|
||||
"when": {"path_contains": ["src/"]},
|
||||
},
|
||||
],
|
||||
provenance=[
|
||||
"SCAFFOLD-TRANCHE-001 (build-but-don't-break established)",
|
||||
"SCAFFOLD-TRANCHE-003 (adaptation two-proof rule)",
|
||||
"construction_decision.obligations_from_trace (auto-derived since T4)",
|
||||
"LIVE-CONTRACT-QUALITY-001 (adaptation resolved + closed live)",
|
||||
],
|
||||
)
|
||||
|
||||
RELEASE_DEPLOYMENT = ConstructionPattern(
|
||||
"release-deployment-change",
|
||||
"Changes to the release/deployment path: fail-closed qualification, "
|
||||
"exact candidate identity, staging != promotion, truthful promotion "
|
||||
"outcomes, deployed-identity verification, negative + forward proofs.",
|
||||
maturity="UNIT-PROVEN",
|
||||
signals={
|
||||
"path_contains": [
|
||||
"deploy",
|
||||
"promotion",
|
||||
"qualify",
|
||||
"release.yml",
|
||||
"ci.yml",
|
||||
"deploy_qualify",
|
||||
"deploy_stage",
|
||||
"deploy_promotion",
|
||||
],
|
||||
"objective_any": ["deploy", "release", "promot", "qualif", "candidate", "current", "monotonic"],
|
||||
"workflow_config_signals": True, # informational; surface kinds carry it
|
||||
},
|
||||
obligations=[
|
||||
{
|
||||
"id": "rel-fail-closed-qualification",
|
||||
"obligation": "CI qualification remains fail-closed (exact SHA, "
|
||||
"two-leg verdict); red candidates never deploy",
|
||||
"when": {"path_contains": ["deploy", "release", "qualify", "ci.yml"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-exact-candidate-identity",
|
||||
"obligation": "deployment addresses the EXACT pushed revision, never whatever HEAD is at deploy time",
|
||||
"when": {"path_contains": ["deploy", "release"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-staging-promotion-distinction",
|
||||
"obligation": "staged candidate != promoted candidate; promotion outcome must be independently verifiable",
|
||||
"when": {"path_contains": ["deploy", "promotion", "stage"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-promotion-truthfulness",
|
||||
"obligation": "promotion outcomes are truthful (PROMOTED / "
|
||||
"NOOP_ALREADY_CURRENT / STALE_SUPERSEDED / REFUSED_*); "
|
||||
"stale candidates never claim success",
|
||||
"when": {"path_contains": ["promotion", "deploy_stage", "deploy_promotion"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-monotonic-promotion",
|
||||
"obligation": "promotion ordering: an older candidate finishing "
|
||||
"later cannot regress current; ancestry check + "
|
||||
"fenced critical section",
|
||||
"when": {"path_contains": ["deploy_promotion", "deploy_stage", "promotion"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-negative-candidate-proof",
|
||||
"obligation": "negative proof: red/unqualified candidate is refused; stale candidate cannot promote",
|
||||
"when": {"path_contains": ["deploy", "promotion", "qualify"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-forward-candidate-proof",
|
||||
"obligation": "forward proof: a green newer candidate still promotes normally",
|
||||
"when": {"path_contains": ["deploy", "promotion"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-deployed-identity-verification",
|
||||
"obligation": "deployed identity verified from the deployment "
|
||||
"authority (current symlink + build identity), not "
|
||||
"from job artifacts alone",
|
||||
"when": {"always": True},
|
||||
},
|
||||
{
|
||||
"id": "rel-release-regression-verification",
|
||||
"obligation": "existing release-safety guarantees re-proven (the "
|
||||
"guarantees that already exist must not silently "
|
||||
"weaken)",
|
||||
"when": {"always": True},
|
||||
},
|
||||
],
|
||||
provenance=[
|
||||
"maintenance:deploy-order-race (live incident: deploys 7355/7359)",
|
||||
"DEPLOY-MONOTONIC-001 (monotonic promotion + truthful outcomes, live-proven)",
|
||||
"release-safety repair 2026-09-13 (red-promotion incidents 0.66.28/29: required-checks aggregator lied)",
|
||||
"scripts/deploy_promotion.py (ancestry + fenced critical section)",
|
||||
"tests/test_deploy_stage_race.py (22-test matrix incl. two-process live race)",
|
||||
],
|
||||
)
|
||||
|
||||
UI_TUI_STRUCTURAL = ConstructionPattern(
|
||||
"ui-tui-structural-change",
|
||||
"UI/TUI structural changes: component identity, Layout Contract "
|
||||
"inheritance, USER-LOCKED preservation, deterministic verification.",
|
||||
maturity="UNIT-PROVEN",
|
||||
signals={
|
||||
"surface_kinds": ["ui_component", "layout_region"],
|
||||
"path_contains": ["tui", "ui", "widget"],
|
||||
"entities_match": ["ui:", "region:", "layout:"],
|
||||
},
|
||||
obligations=[
|
||||
{
|
||||
"id": "ui-layout-contract-inherited",
|
||||
"obligation": "affected UI components inherit their Layout Contract "
|
||||
"obligations; layout_contract_check must PASS",
|
||||
"when": {"entities_match": ["ui:", "region:", "layout:"]},
|
||||
},
|
||||
{
|
||||
"id": "ui-user-locked-preserved",
|
||||
"obligation": "USER-LOCKED placement is user authority: preserved "
|
||||
"exactly; changes require explicit user authorization",
|
||||
"when": {"entities_match": ["ui:", "region:"]},
|
||||
},
|
||||
{
|
||||
"id": "ui-unknown-never-pass",
|
||||
"obligation": "layout verification UNKNOWN (no adapter evidence) must never be reported as PASS",
|
||||
"when": {"entities_match": ["ui:", "region:", "layout:"]},
|
||||
},
|
||||
{
|
||||
"id": "ui-component-identity-stable",
|
||||
"obligation": "ui:<component> identity survives implementation-path "
|
||||
"changes; authority never keyed by filename alone",
|
||||
"when": {"entities_match": ["ui:", "region:", "layout:"]},
|
||||
},
|
||||
],
|
||||
provenance=[
|
||||
"issue #105 + plans/2026-09-03-tui-footer.md (version display)",
|
||||
"Tranche 3 real TUI proof (gate PASS -> violation FAIL -> restore)",
|
||||
"tests/test_layout_contract.py + tests/test_layout_contract_real_proof.py",
|
||||
"maintenance:version-display-user-lock (USER-LOCK upgrade pending "
|
||||
"user confirmation — provenance rule honored, not fabricated)",
|
||||
],
|
||||
)
|
||||
|
||||
STORE_SCHEMA_EVOLUTION = ConstructionPattern(
|
||||
"store-schema-evolution",
|
||||
"Durable store/schema changes: backward readability, authority "
|
||||
"preservation, deterministic roundtrip, migration only when necessary.",
|
||||
maturity="CANDIDATE",
|
||||
signals={
|
||||
"path_contains": ["store", "schema", "structural_store", "scaffold", "spine"],
|
||||
"entities_match": ["module:structural_store", "module:scaffold", "module:spine"],
|
||||
"objective_any": ["store", "schema", "record", "persist", "field", "migration"],
|
||||
},
|
||||
obligations=[
|
||||
{
|
||||
"id": "store-backward-readability",
|
||||
"obligation": "old records without new fields read as empty/unknown, never crash; legacy keys preserved",
|
||||
"when": {"path_contains": ["store", "structural_store", "scaffold", "spine", "schema"]},
|
||||
},
|
||||
{
|
||||
"id": "store-roundtrip",
|
||||
"obligation": "deterministic save/load roundtrip incl. unknown entity/relation kinds and open-world fields",
|
||||
"when": {"path_contains": ["store", "structural_store", "scaffold", "schema"]},
|
||||
},
|
||||
{
|
||||
"id": "store-authority-preservation",
|
||||
"obligation": "structural facts only change through their owning "
|
||||
"projector/authority; canonical rebuild after "
|
||||
"registration (no hand-edited structural facts)",
|
||||
"when": {"path_contains": ["spine", "scaffold", "store/"]},
|
||||
},
|
||||
{
|
||||
"id": "store-migration-only-if-needed",
|
||||
"obligation": "no mandatory migration unless semantics truly change; "
|
||||
"ext/open-world fields preferred over schema churn",
|
||||
"when": {"path_contains": ["schema", "store", "structural_store"]},
|
||||
},
|
||||
],
|
||||
provenance=[
|
||||
"Tranche 2 structural-store extraction (behavior-neutrality proof)",
|
||||
"Tranche 4 contract-quality repair (legacy keys preserved, old contracts readable — test_h)",
|
||||
"Tranche 5 expected-runtime-effects extension (old contracts close without new fields)",
|
||||
"Tranche 2 CI lesson: hand-edited spine store diverged from canonical rebuild (deploy-identity edges)",
|
||||
],
|
||||
)
|
||||
|
||||
BUILTIN_PATTERNS = [CAPABILITY_ADAPTATION, RELEASE_DEPLOYMENT, UI_TUI_STRUCTURAL, STORE_SCHEMA_EVOLUTION]
|
||||
|
||||
_REGISTRY: dict[str, ConstructionPattern] = {p.pattern_id: p for p in BUILTIN_PATTERNS}
|
||||
|
||||
|
||||
def get_pattern(pattern_id: str, revision: int | None = None) -> ConstructionPattern | None:
|
||||
"""Fetch a builtin pattern; a specific revision raises if the registry
|
||||
no longer carries it (contracts reference the revision they consumed —
|
||||
§17)."""
|
||||
p = _REGISTRY.get(pattern_id)
|
||||
if p is None:
|
||||
return None
|
||||
if revision is not None and p.revision != revision:
|
||||
raise ValueError(
|
||||
f"pattern {pattern_id} revision {revision} not in registry "
|
||||
f"(current: {p.revision}) — the consuming contract records which "
|
||||
f"revision it used"
|
||||
)
|
||||
return p
|
||||
@@ -0,0 +1,408 @@
|
||||
"""release_notes.py — evidence-derived Gitea release history.
|
||||
|
||||
SCAFFOLD-TRANCHE-009. Prime invariant (tranche brief):
|
||||
|
||||
Git records exact source history. Construction Contracts / maintenance
|
||||
items / tests / deployment records provide governed evidence. Gitea
|
||||
Releases provide the HUMAN-READABLE HISTORICAL ACCOUNT of what actually
|
||||
improved in each Rune release.
|
||||
|
||||
Release notes are evidence-derived history, not marketing prose:
|
||||
- facts come from governed records (closed contracts, resolved
|
||||
maintenance items, contract-quality records, observed evidence,
|
||||
lock/deploy artifacts) in that priority order;
|
||||
- commit messages alone never overrule governed evidence;
|
||||
- model prose may COMPRESS verified facts; it may not invent
|
||||
importance, completion, or maturity (§14 — this generator is fully
|
||||
deterministic; no model step exists here).
|
||||
|
||||
RELEASE SEMANTICS (§6, audited 2026-09-13):
|
||||
VERSION the semver in pyproject.toml / __init__ (e.g. 0.66.44)
|
||||
TAG v<VERSION>, created by auto-version on the BUMP commit
|
||||
release commit (tag SHA) the auto-bump commit — NOT necessarily the
|
||||
final state of the release
|
||||
converged deployed identity the last commit deployed under the same
|
||||
VERSION (same-version convergence commits are normal:
|
||||
closure/accounting/dependency-lock commits land and deploy
|
||||
after the bump without a version change)
|
||||
|
||||
The notes record BOTH: release_sha (tag target) and deployed_sha
|
||||
(converged deployment read from the deployment authority). The
|
||||
distinction is exposed honestly, never papered over (§6).
|
||||
|
||||
RELEASE RANGE for version N: (tag of previous version, tag N] PLUS
|
||||
same-version convergence commits up to the deployed identity (§9/§10).
|
||||
|
||||
PUBLICATION (§15): Gitea API POST /releases to create, PATCH /releases/{id}
|
||||
to update. Idempotent (§17): publishing twice for the same tag UPDATES the
|
||||
existing release — never duplicates. Notes carry provenance (§20-21):
|
||||
historical release documentation, NOT Memory and NOT structural authority.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = [
|
||||
"ReleaseNote",
|
||||
"extract_evidence_facts",
|
||||
"build_note",
|
||||
"render_markdown",
|
||||
"publish_release",
|
||||
"fetch_release",
|
||||
"converged_deployed_sha",
|
||||
]
|
||||
|
||||
# ── structured record (§13) ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ReleaseNote:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
version: str,
|
||||
summary: str,
|
||||
improved: list[str] | None = None,
|
||||
added: list[str] | None = None,
|
||||
fixed: list[str] | None = None,
|
||||
proven: list[str] | None = None,
|
||||
limitations: list[str] | None = None,
|
||||
references: list[str] | None = None,
|
||||
release_sha: str = "",
|
||||
deployed_sha: str = "",
|
||||
provenance: list[str] | None = None,
|
||||
backfill: bool = False,
|
||||
):
|
||||
self.version = version
|
||||
self.summary = summary
|
||||
self.improved = sorted(improved or [])
|
||||
self.added = sorted(added or [])
|
||||
self.fixed = sorted(fixed or [])
|
||||
self.proven = sorted(proven or [])
|
||||
self.limitations = sorted(limitations or [])
|
||||
self.references = sorted(references or [])
|
||||
self.release_sha = release_sha
|
||||
self.deployed_sha = deployed_sha
|
||||
self.provenance = sorted(provenance or [])
|
||||
self.backfill = backfill
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"version": self.version,
|
||||
"summary": self.summary,
|
||||
"improved": self.improved,
|
||||
"added": self.added,
|
||||
"fixed": self.fixed,
|
||||
"proven": self.proven,
|
||||
"limitations": self.limitations,
|
||||
"references": self.references,
|
||||
"release_sha": self.release_sha,
|
||||
"deployed_sha": self.deployed_sha,
|
||||
"provenance": self.provenance,
|
||||
"backfill": self.backfill,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "ReleaseNote":
|
||||
return cls(**d)
|
||||
|
||||
|
||||
def render_markdown(note: ReleaseNote) -> str:
|
||||
"""Human-readable Markdown (§7/§11): concise sections, empty sections
|
||||
omitted, evidence references kept, version/deployed distinction shown."""
|
||||
lines = [f"# Rune v{note.version}", "", note.summary, ""]
|
||||
if note.release_sha and note.deployed_sha:
|
||||
if note.release_sha[:12] == note.deployed_sha[:12]:
|
||||
lines.append(f"_Release commit: `{note.release_sha[:12]}` (deployed: `{note.deployed_sha[:12]}`)_")
|
||||
else:
|
||||
lines.append(
|
||||
f"_Tag commit: `{note.release_sha[:12]}` · "
|
||||
f"**final deployed: `{note.deployed_sha[:12]}`** "
|
||||
f"(same-version convergence commits included)_"
|
||||
)
|
||||
elif note.deployed_sha:
|
||||
lines.append(f"_Deployed: `{note.deployed_sha[:12]}`_")
|
||||
lines.append("")
|
||||
sections = [
|
||||
("Improved", note.improved),
|
||||
("Added", note.added),
|
||||
("Fixed", note.fixed),
|
||||
("Proven", note.proven),
|
||||
("Known limitations", note.limitations),
|
||||
("References", note.references),
|
||||
]
|
||||
for title, items in sections:
|
||||
if not items:
|
||||
continue
|
||||
lines.append(f"## {title}")
|
||||
for item in items:
|
||||
lines.append(f"- {item}")
|
||||
lines.append("")
|
||||
if note.backfill:
|
||||
lines.append("_Historical backfill: derived from surviving governed evidence after the fact._")
|
||||
if note.provenance:
|
||||
lines.append("_Derived from: " + "; ".join(note.provenance) + "_")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
# ── evidence extraction (§8/§24) ─────────────────────────────────────────────
|
||||
|
||||
_CONTRACT_EVIDENCE = "Construction Contract"
|
||||
_MAINT_EVIDENCE = "maintenance item"
|
||||
_DEPLOY_EVIDENCE = "deployment record"
|
||||
_LOCK_EVIDENCE = "dependency-lock record"
|
||||
|
||||
|
||||
def extract_evidence_facts(scaffold_doc: dict) -> dict:
|
||||
"""Deterministic fact extraction from governed records (§8). Returns the
|
||||
raw fact pool the note builder selects from. Commit messages are NOT a
|
||||
source here — governed records only."""
|
||||
facts: dict = {"improved": [], "added": [], "fixed": [], "proven": [], "limitations": [], "references": []}
|
||||
for e in scaffold_doc.get("entities", []):
|
||||
ident = e.get("identities", {})
|
||||
if e["kind"] == "construction_contract" and e.get("status") == "closed":
|
||||
facts["references"].append(f"{e['id']} — {ident.get('need', '')[:90]}")
|
||||
q = e.get("ext", {}).get("contract_quality", {})
|
||||
if q.get("correctly_predicted"):
|
||||
facts["proven"].append(
|
||||
f"{e['id']}: predicted effects independently confirmed ({len(q['correctly_predicted'])} entities)"
|
||||
)
|
||||
elif e["kind"] == "maintenance_item":
|
||||
status = ident.get("status")
|
||||
issue = ident.get("issue", "")[:90]
|
||||
if status == "resolved":
|
||||
facts["fixed"].append(f"{e['id']}: {issue}")
|
||||
elif status == "open":
|
||||
facts["limitations"].append(f"{e['id']}: {issue}")
|
||||
elif e["kind"] == "tui_projection_decision":
|
||||
# recorded projection decisions are release-relevant facts
|
||||
facts["references"].append(f"TUI projection for {ident.get('feature', '')}: {ident.get('projection', '')}")
|
||||
# contract-quality declared maturity evidence
|
||||
for e in scaffold_doc.get("entities", []):
|
||||
if e["kind"] == "construction_contract" and e.get("ext", {}).get("release_evidence"):
|
||||
rel = e["ext"]["release_evidence"]
|
||||
if isinstance(rel, dict) and rel.get("final_deployed") or rel.get("deployed_identity"):
|
||||
facts["proven"].append(
|
||||
f"{e['id']}: release chain proven ({rel.get('final_deployed') or rel.get('deployed_identity')})"
|
||||
)
|
||||
for k in facts:
|
||||
facts[k] = sorted(set(facts[k]))
|
||||
return facts
|
||||
|
||||
|
||||
def build_note(
|
||||
*,
|
||||
version: str,
|
||||
summary: str,
|
||||
scaffold_doc: dict,
|
||||
release_sha: str = "",
|
||||
deployed_sha: str = "",
|
||||
extra: dict | None = None,
|
||||
provenance: list[str] | None = None,
|
||||
backfill: bool = False,
|
||||
exclude_unsupported: list[str] | None = None,
|
||||
) -> ReleaseNote:
|
||||
"""Assemble a ReleaseNote from the governed fact pool + explicit extras
|
||||
(extra items must themselves be evidence-backed — the caller carries the
|
||||
responsibility). `exclude_unsupported` drops fact entries containing any
|
||||
of the given substrings — the §24F unsupported-claim rejection surface."""
|
||||
facts = extract_evidence_facts(scaffold_doc)
|
||||
if exclude_unsupported:
|
||||
|
||||
def _clean(items: list[str]) -> list[str]:
|
||||
return [i for i in items if not any(x.lower() in i.lower() for x in exclude_unsupported)]
|
||||
|
||||
for k in facts:
|
||||
facts[k] = _clean(facts[k])
|
||||
merged = {}
|
||||
for k in ("improved", "added", "fixed", "proven", "limitations", "references"):
|
||||
# §24F: extras pass through the SAME unsupported-claim filter as
|
||||
# extracted facts — a caller cannot smuggle a retracted claim in
|
||||
# via `extra` either.
|
||||
extras = [
|
||||
i for i in (extra or {}).get(k, []) if not any(x.lower() in i.lower() for x in (exclude_unsupported or []))
|
||||
]
|
||||
merged[k] = sorted(set(facts[k]) | set(extras))
|
||||
return ReleaseNote(
|
||||
version=version,
|
||||
summary=summary,
|
||||
release_sha=release_sha,
|
||||
deployed_sha=deployed_sha,
|
||||
provenance=provenance or [],
|
||||
backfill=backfill,
|
||||
**merged,
|
||||
)
|
||||
|
||||
|
||||
# ── Gitea publication (§15/§17/§25) ──────────────────────────────────────────
|
||||
|
||||
|
||||
def _api(url: str, token: str, method: str = "GET", payload: dict | None = None) -> tuple[int, dict]:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
method=method,
|
||||
headers={"Authorization": f"token {token}", "Content-Type": "application/json"},
|
||||
data=json.dumps(payload).encode() if payload else None,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
body = r.read()
|
||||
return r.status, (json.loads(body) if body else {})
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read()
|
||||
text = body.decode("utf-8", "replace")
|
||||
try:
|
||||
return e.code, (json.loads(text) if text else {})
|
||||
except ValueError:
|
||||
# Gitea returns plain-text bodies for some errors (e.g. 404
|
||||
# "page not found" on unknown release-tag lookups)
|
||||
return e.code, {"_raw": text[:200]}
|
||||
except OSError as e:
|
||||
return 0, {"_error": str(e)[:200]}
|
||||
|
||||
|
||||
def fetch_release(server: str, repo: str, token: str, tag: str) -> dict | None:
|
||||
status, data = _api(f"{server}/api/v1/repos/{repo}/releases/tags/{tag}", token)
|
||||
return data if status == 200 else None
|
||||
|
||||
|
||||
def publish_release(server: str, repo: str, token: str, note: ReleaseNote, *, target_commitish: str = "master") -> dict:
|
||||
"""Create or idempotently update the Gitea release for note.version
|
||||
(§17): existing release → PATCH body (same evidence converges, never
|
||||
duplicates); missing release → POST create with the tag target. The tag
|
||||
itself is owned by auto-version — this module never rewrites tags (§30.5).
|
||||
Returns {action: created|updated, release_id, html_url?} or an error
|
||||
dict with `failed: True` (§16 — publication failure is visible)."""
|
||||
tag = f"v{note.version}"
|
||||
body = render_markdown(note)
|
||||
existing = fetch_release(server, repo, token, tag)
|
||||
if existing is not None:
|
||||
status, data = _api(
|
||||
f"{server}/api/v1/repos/{repo}/releases/{existing['id']}",
|
||||
token,
|
||||
"PATCH",
|
||||
{"body": body, "name": f"Rune v{note.version}"},
|
||||
)
|
||||
if status != 200:
|
||||
return {
|
||||
"failed": True,
|
||||
"action": "update",
|
||||
"status": status,
|
||||
"error": data.get("_error") or data.get("message", "")[:200],
|
||||
}
|
||||
return {"action": "updated", "release_id": data.get("id"), "html_url": data.get("html_url", "")}
|
||||
status, data = _api(
|
||||
f"{server}/api/v1/repos/{repo}/releases",
|
||||
token,
|
||||
"POST",
|
||||
{
|
||||
"tag_name": tag,
|
||||
"target_commitish": target_commitish,
|
||||
"name": f"Rune v{note.version}",
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
},
|
||||
)
|
||||
if status not in (200, 201):
|
||||
return {
|
||||
"failed": True,
|
||||
"action": "create",
|
||||
"status": status,
|
||||
"error": data.get("_error") or data.get("message", "")[:200],
|
||||
}
|
||||
return {"action": "created", "release_id": data.get("id"), "html_url": data.get("html_url", "")}
|
||||
|
||||
|
||||
def converged_deployed_sha(deploy_root: str = "/mnt/workdrive/rune-code") -> str:
|
||||
"""Read the converged deployed commit from the deployment authority
|
||||
(the `current` symlink's build identity) — never assumed equal to the
|
||||
tag SHA (§6)."""
|
||||
try:
|
||||
target = Path(os_readlink(Path(deploy_root) / "current"))
|
||||
identity = json.loads((target / "src" / "rune" / "_build_identity.json").read_text(encoding="utf-8"))
|
||||
return str(identity.get("commit", ""))
|
||||
except (OSError, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
def os_readlink(p: Path) -> Path:
|
||||
import os
|
||||
|
||||
return Path(os.readlink(p))
|
||||
|
||||
|
||||
# ── automatic lifecycle entry points (Tranche 9B §4/§7) ─────────────────────
|
||||
|
||||
def auto_initial_note(*, version: str, summary: str, scaffold_store: str,
|
||||
release_sha: str, deployed_sha: str = "",
|
||||
provenance: list[str] | None = None) -> ReleaseNote:
|
||||
"""RELEASE INITIALIZATION: build evidence-derived notes with NO
|
||||
hand-written extra prose (§7). Facts come deterministically from the
|
||||
governed scaffold store — contracts closed, maintenance resolved/open,
|
||||
quality records. Routine releases require nothing from the builder."""
|
||||
try:
|
||||
doc = json.loads(Path(scaffold_store).read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
doc = {"entities": [], "relationships": [], "observations": []}
|
||||
facts = extract_evidence_facts(doc)
|
||||
return ReleaseNote(
|
||||
version=version, summary=summary,
|
||||
improved=[], added=[], fixed=[],
|
||||
proven=facts["proven"], limitations=facts["limitations"],
|
||||
references=facts["references"],
|
||||
release_sha=release_sha, deployed_sha=deployed_sha,
|
||||
provenance=provenance or ["automatic release initialization "
|
||||
"(auto-version workflow)"])
|
||||
|
||||
|
||||
def convergence_refresh(existing_note: ReleaseNote, *,
|
||||
deployed_sha: str,
|
||||
scaffold_store: str,
|
||||
extra_provenance: str = "") -> ReleaseNote:
|
||||
"""RELEASE CONVERGENCE REFRESH: a later SHA of the SAME version became
|
||||
deployment:current — refresh the SAME note with the new deployed
|
||||
identity and any newly-governed facts (§3/§10). Tag/release SHA never
|
||||
changes; evidence accumulates; idempotent by construction (same inputs
|
||||
→ same note)."""
|
||||
try:
|
||||
doc = json.loads(Path(scaffold_store).read_text(encoding="utf-8"))
|
||||
facts = extract_evidence_facts(doc)
|
||||
except (OSError, ValueError):
|
||||
facts = {"proven": [], "limitations": [], "references": []}
|
||||
prov = list(existing_note.provenance)
|
||||
if extra_provenance and extra_provenance not in prov:
|
||||
prov.append(extra_provenance)
|
||||
return ReleaseNote(
|
||||
version=existing_note.version, summary=existing_note.summary,
|
||||
improved=existing_note.improved, added=existing_note.added,
|
||||
fixed=existing_note.fixed,
|
||||
proven=sorted(set(existing_note.proven) | set(facts["proven"])),
|
||||
limitations=sorted(set(existing_note.limitations)
|
||||
| set(facts["limitations"])),
|
||||
references=sorted(set(existing_note.references)
|
||||
| set(facts["references"])),
|
||||
release_sha=existing_note.release_sha, # tag SHA NEVER changes
|
||||
deployed_sha=deployed_sha, # convergence truth
|
||||
provenance=prov, backfill=existing_note.backfill)
|
||||
|
||||
|
||||
def build_from_deployment(*, version: str, deployed_sha: str,
|
||||
scaffold_store: str,
|
||||
release_sha: str = "",
|
||||
summary: str = "") -> ReleaseNote:
|
||||
"""Deploy-side convergence entry: rebuild/refresh notes from the
|
||||
CURRENT governed store + the deployment authority's identity. No
|
||||
hand-written prose required."""
|
||||
note = auto_initial_note(
|
||||
version=version,
|
||||
summary=summary or f"Rune v{version} — evidence-derived release "
|
||||
f"history (converged deployment)",
|
||||
scaffold_store=scaffold_store, release_sha=release_sha,
|
||||
deployed_sha=deployed_sha,
|
||||
provenance=["automatic convergence refresh (deploy workflow)"])
|
||||
return note
|
||||
@@ -751,6 +751,77 @@ class RuneOperator:
|
||||
"tasks_completed": len([t for t in self.state.tasks if t.status == "completed"]),
|
||||
}
|
||||
|
||||
def _construction_store(self) -> str:
|
||||
"""SCAFFOLD-TRANCHE-0012: construction store location. Derives from
|
||||
the module's own repo root (rune_operator.py lives in <root>/src/rune/)
|
||||
with an env override for tests/fixtures — never a bare relative path
|
||||
dependent on the caller's CWD."""
|
||||
env = os.environ.get("RUNE_CONSTRUCTION_STORE")
|
||||
if env:
|
||||
return env
|
||||
return str(Path(__file__).resolve().parent.parent.parent
|
||||
/ "store" / "scaffold")
|
||||
|
||||
def _construction_note(self, text: str) -> None:
|
||||
"""SCAFFOLD-TRANCHE-0012: append a construction activation note to
|
||||
the governed scaffold observations. Degrades silently — observations
|
||||
are evidence, never task-critical."""
|
||||
try:
|
||||
from .scaffold import Scaffold
|
||||
s = Scaffold(self._construction_store(), "issue161-harness")
|
||||
doc = s.load()
|
||||
doc.setdefault("observations", []).append({
|
||||
"kind": "construction_note", "text": text[:300],
|
||||
"observed_at": __import__("datetime").datetime.utcnow()
|
||||
.strftime("%Y-%m-%dT%H:%M:%SZ")})
|
||||
s.save(doc)
|
||||
except Exception: # noqa: S110 — notes are best-effort by design
|
||||
pass
|
||||
|
||||
def _construction_activation(self, description: str, task_id: str) -> None:
|
||||
"""SCAFFOLD-TRANCHE-0011/0012 §11: contextual construction
|
||||
classification + activation on the NORMAL input path. Deterministic;
|
||||
the mode lands in the durable scaffold store where the shared
|
||||
Construction Context (and the future TUI) reads it. Delegates all
|
||||
lifecycle decisions to construction_mode/construction_activation —
|
||||
NO duplicate intelligence here."""
|
||||
from .construction_mode import classify_mode, _STANDALONE_WORDS
|
||||
from .construction_activation import activate
|
||||
low = description.lower()
|
||||
tokens = set(__import__("re").split(r"[^a-z0-9]+", low))
|
||||
# §12D: the NATIVE hook must not force the Rune target — standalone
|
||||
# language means a NEW project (evidenced non-existence); explicit
|
||||
# Rune references mean the existing authority; otherwise UNKNOWN.
|
||||
if "rune" in tokens:
|
||||
target, exists = "rune", True
|
||||
elif _contains_any := [w for w in _STANDALONE_WORDS if w in low]:
|
||||
target, exists = None, False # evidenced: standalone = new
|
||||
elif "existing" in tokens:
|
||||
target, exists = "rune", True
|
||||
else:
|
||||
target, exists = None, None # unknown → probes, not invention
|
||||
mode_decision = classify_mode(
|
||||
description, target_project=target, project_exists=exists,
|
||||
affected_surface=[])
|
||||
from .scaffold import Scaffold
|
||||
s = Scaffold(self._construction_store(), "issue161-harness")
|
||||
doc = s.load()
|
||||
rec = activate(mode_decision, need=type("N", (), {
|
||||
"objective": description,
|
||||
"project": "rune",
|
||||
"need_id": f"native-{task_id}",
|
||||
"affected_surface": []})(), scaffold=s, builder="rune-native")
|
||||
doc.setdefault("observations", []).append({
|
||||
"kind": "construction_activated",
|
||||
"task": task_id,
|
||||
"mode": mode_decision.mode,
|
||||
"activated": rec.get("activated", False),
|
||||
"lifecycle": rec.get("lifecycle", ""),
|
||||
"classification_evidence": mode_decision.evidence[:3],
|
||||
"observed_at": __import__("datetime").datetime.utcnow()
|
||||
.strftime("%Y-%m-%dT%H:%M:%SZ")})
|
||||
s.save(doc)
|
||||
|
||||
def send_task(self, description: str, model: str | None = None, channel: str = OPERATOR_CHANNEL) -> Dict[str, Any]:
|
||||
"""Send a task to a model.
|
||||
|
||||
@@ -808,6 +879,24 @@ class RuneOperator:
|
||||
channel=channel,
|
||||
)
|
||||
task.route = task_route.route if task_route else ""
|
||||
|
||||
# CONTEXTUAL CONSTRUCTION ACTIVATION (SCAFFOLD-TRANCHE-0011/0012):
|
||||
# every normal input passes through here. Classification is
|
||||
# deterministic; activation degrades gracefully — a construction
|
||||
# failure NEVER blocks the ordinary task path (same lifecycle
|
||||
# guarantee as the route above).
|
||||
try:
|
||||
self._construction_activation(description, state_task.task_id)
|
||||
except Exception as exc: # noqa: BLE001 — construction must not break tasks
|
||||
try:
|
||||
self._task_state.transition(
|
||||
state_task.task_id, "running",
|
||||
outcome_ref="construction_activation_error")
|
||||
except Exception: # noqa: S110 — secondary degrade, task continues
|
||||
pass
|
||||
self._construction_note(
|
||||
f"construction activation error: {exc}")
|
||||
|
||||
if task_route is not None and task_route.decided_at:
|
||||
# Canonical reference: task state POINTS at the route event in
|
||||
# route.jsonl (sole route authority) — no value copied.
|
||||
|
||||
@@ -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.39
|
||||
Version: v0.66.54
|
||||
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.39 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.66.54 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -120,6 +120,81 @@ 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 commit(self, doc: dict, *, what: str = "durable state",
|
||||
selector=None, expect=None) -> Path:
|
||||
"""§21 TRANSACTIONAL BOUNDARY (Tranche 11): the ONLY way durable
|
||||
mutations are finalized. Persists, then verifies by readback. All
|
||||
accounting/report work happens AFTER commit returns — no quality
|
||||
inspection may sit between mutation and its durable commit (the
|
||||
forgotten-save anti-pattern: mutate → fragile read → exception →
|
||||
save never happens, 3 recurrences T5/T9B/T10)."""
|
||||
# §12B REAL DURABLE COMMIT SEMANTICS: "load didn't throw" is not
|
||||
# proof — load() returns a fresh empty document when the store file
|
||||
# is missing/corrupt, which silently masquerades as success. Instead:
|
||||
# canonical digest of the INTENDED document vs canonical digest of
|
||||
# what is ACTUALLY on disk after save. Semantic mismatch (partial
|
||||
# write, wrong path, unreadable store) fails visibly.
|
||||
from rune.structural_store import canonical_digest
|
||||
from rune.structural_store import load as _ss_load
|
||||
|
||||
intended = canonical_digest(doc)
|
||||
p = self.save(doc)
|
||||
reloaded = _ss_load(self.current)
|
||||
if reloaded is None:
|
||||
raise AssertionError(
|
||||
f"persistence verification FAILED for {what!r}: store did "
|
||||
f"not reload after save (missing or corrupt)")
|
||||
if canonical_digest(reloaded) != intended:
|
||||
raise AssertionError(
|
||||
f"persistence verification FAILED for {what!r}: reloaded "
|
||||
f"store digest differs from intended document — the "
|
||||
f"mutation is not durably what was intended")
|
||||
# optional caller-supplied semantic invariant (held-out: wrong/
|
||||
# stale doc where an expectation was supplied)
|
||||
if selector is not None:
|
||||
value = selector(reloaded)
|
||||
ok = expect(value) if callable(expect) else value == expect
|
||||
if not ok:
|
||||
raise AssertionError(
|
||||
f"persistence invariant FAILED for {what!r}: reloaded "
|
||||
f"state does not satisfy the supplied expectation")
|
||||
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:
|
||||
|
||||
@@ -128,6 +128,17 @@ def save(doc: dict, path: Path | str) -> Path:
|
||||
return p
|
||||
|
||||
|
||||
def canonical_digest(doc: dict) -> str:
|
||||
"""SCAFFOLD-TRANCHE-0012 §12B: stable digest of a document's SEMANTIC
|
||||
content — the same normalization save() uses on disk (_sorted +
|
||||
_redact_secrets + sort_keys). Key ordering cannot create false failure;
|
||||
identical semantic content always yields an identical digest."""
|
||||
import hashlib
|
||||
|
||||
payload = json.dumps(_sorted(_redact_secrets(doc)), sort_keys=True)
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def snapshot(doc: dict, snapshots_dir: Path, label: str = "", tag: str = "") -> Path:
|
||||
"""Persist an immutable snapshot. `tag` is a caller-supplied identity
|
||||
fragment (Spine uses the deployed SHA; a Project Scaffold may use its
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""tui_backlog.py — durable TUI Upgrade Backlog (SCAFFOLD-TRANCHE-007 §22-25).
|
||||
|
||||
Standing architectural requirement: every meaningful backend feature is
|
||||
checked for whether it needs a TUI/user-surface projection or operator
|
||||
control. The backlog records that work so the user-facing system never
|
||||
falls behind the backend.
|
||||
|
||||
AUTHORITY (§25): the backlog is a maintenance/product-surface LEDGER —
|
||||
outstanding work only. It is NOT TUI structural authority. Implemented TUI
|
||||
structure remains under Layout Contracts / the project's structural
|
||||
authority.
|
||||
|
||||
Projection types (§24): NONE / INSPECTION / CONTROL / BOTH / UNKNOWN.
|
||||
Auto-capture seam: construction-contract closure can ask "TUI projection
|
||||
required?" — recorded truthfully even when the answer is NONE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rune.structural_store import add_entity, entity, now
|
||||
|
||||
__all__ = [
|
||||
"add_backlog_item",
|
||||
"get_backlog_item",
|
||||
"list_backlog",
|
||||
"update_backlog_status",
|
||||
"record_tui_projection_decision",
|
||||
"PROJECTIONS",
|
||||
"STATUSES",
|
||||
]
|
||||
|
||||
PROJECTIONS = {"NONE", "INSPECTION", "CONTROL", "BOTH", "UNKNOWN"}
|
||||
STATUSES = {"OPEN", "PLANNED", "IN_PROGRESS", "DONE", "WONT_DO"}
|
||||
|
||||
|
||||
def add_backlog_item(
|
||||
doc: dict,
|
||||
item_id: str,
|
||||
*,
|
||||
feature: str,
|
||||
desired_behavior: str,
|
||||
why: str,
|
||||
projection: str = "INSPECTION",
|
||||
dependencies: list[str] | None = None,
|
||||
priority: str = "normal",
|
||||
status: str = "OPEN",
|
||||
authority_source: str = "product-surface requirement",
|
||||
evidence_refs: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Add/refresh one backlog item (§22). `feature` (originating subsystem)
|
||||
and `projection` classification are retained permanently. Duplicate ids
|
||||
refresh in place — the ledger does not fork."""
|
||||
if projection not in PROJECTIONS:
|
||||
raise ValueError(f"projection must be one of {sorted(PROJECTIONS)}")
|
||||
if status not in STATUSES:
|
||||
raise ValueError(f"status must be one of {sorted(STATUSES)}")
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"tui-backlog:{item_id}",
|
||||
kind="tui_backlog_item",
|
||||
identities={
|
||||
"feature": feature,
|
||||
"desired_behavior": desired_behavior,
|
||||
"why": why,
|
||||
"projection": projection,
|
||||
"dependencies": sorted(dependencies or []),
|
||||
"priority": priority,
|
||||
"status": status,
|
||||
"authority_source": authority_source,
|
||||
},
|
||||
evidence_refs=evidence_refs or [f"backlog-added:{authority_source}"],
|
||||
)
|
||||
|
||||
|
||||
def get_backlog_item(doc: dict, item_id: str) -> dict | None:
|
||||
return entity(doc, f"tui-backlog:{item_id}")
|
||||
|
||||
|
||||
def list_backlog(doc: dict, *, status: str | None = None, projection: str | None = None) -> list[dict]:
|
||||
out = []
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] != "tui_backlog_item":
|
||||
continue
|
||||
if status and e["identities"].get("status") != status:
|
||||
continue
|
||||
if projection and e["identities"].get("projection") != projection:
|
||||
continue
|
||||
out.append(e)
|
||||
return sorted(out, key=lambda e: e["id"])
|
||||
|
||||
|
||||
def update_backlog_status(doc: dict, item_id: str, status: str, *, evidence_ref: str = "") -> dict:
|
||||
e = get_backlog_item(doc, item_id)
|
||||
if e is None:
|
||||
raise ValueError(f"no backlog item: {item_id}")
|
||||
if status not in STATUSES:
|
||||
raise ValueError(f"status must be one of {sorted(STATUSES)}")
|
||||
e["identities"]["status"] = status
|
||||
e["observed_at"] = now()
|
||||
if evidence_ref:
|
||||
e["evidence_refs"] = sorted(set(e.get("evidence_refs", [])) | {evidence_ref})
|
||||
return e
|
||||
|
||||
|
||||
def record_tui_projection_decision(
|
||||
doc: dict, feature_id: str, *, projection: str, rationale: str, source_contract: str = ""
|
||||
) -> dict:
|
||||
"""§24 AUTO-CAPTURE SEAM: record a "TUI projection required?" decision
|
||||
for a backend feature — including a truthful NONE. This is accounting,
|
||||
not UI design. The seam exists so future contract closures can call it;
|
||||
nothing is auto-designed."""
|
||||
if projection not in PROJECTIONS:
|
||||
raise ValueError(f"projection must be one of {sorted(PROJECTIONS)}")
|
||||
return add_entity(
|
||||
doc,
|
||||
id=f"tui-projection:{feature_id}",
|
||||
kind="tui_projection_decision",
|
||||
identities={
|
||||
"feature": feature_id,
|
||||
"projection": projection,
|
||||
"rationale": rationale[:200],
|
||||
"source_contract": source_contract,
|
||||
},
|
||||
evidence_refs=[f"tui-projection-decision:{projection}"],
|
||||
)
|
||||
+24443
-10
File diff suppressed because it is too large.
Load diff
+885
-138
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,551 @@
|
||||
"""test_construction_interface.py — SCAFFOLD-TRANCHE-007 test plans
|
||||
§29 (interface core), §30 (authority), §31 (pattern precision — integration),
|
||||
§17 (provider independence)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import construction_interface as ci # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(__file__), "..")
|
||||
|
||||
|
||||
def _fixture_authorities():
|
||||
"""A self-contained project-scaffold authority (external-project mode
|
||||
without any real external repo — §16)."""
|
||||
spine = ss.new_doc()
|
||||
ss.add_entity(
|
||||
spine, id="capability:exporter", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"]
|
||||
)
|
||||
ss.add_entity(spine, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
spine, type="implements", from_id="capability:exporter", to_id="contract:csv_export", evidence_refs=["f"]
|
||||
)
|
||||
return spine
|
||||
|
||||
|
||||
def _iface(tmp_path, builder="test-builder", with_authority=True):
|
||||
store = str(tmp_path / ".rune" / "scaffold")
|
||||
iface = ci.ConstructionInterface(builder=builder, project_store=store, project_id="fixture-proj")
|
||||
if with_authority:
|
||||
# inject the fixture authority INTO the fixture scaffold store so
|
||||
# external-project mode has structural truth to consult
|
||||
iface.scaffold.init(purpose="fixture")
|
||||
doc = iface.scaffold.load()
|
||||
for e in _fixture_authorities()["entities"]:
|
||||
doc["entities"].append(json.loads(json.dumps(e)))
|
||||
for r in _fixture_authorities()["relationships"]:
|
||||
doc["relationships"].append(json.loads(json.dumps(r)))
|
||||
iface.scaffold.save(doc)
|
||||
return iface
|
||||
|
||||
|
||||
def _submit(iface, **kw):
|
||||
payload = dict(
|
||||
objective="persistent project export",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=[],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by fixture authority",
|
||||
)
|
||||
payload.update(kw)
|
||||
return iface.submit_need(**payload)
|
||||
|
||||
|
||||
# ── §29 interface core ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_29a_submit_need(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
assert env["schema"] == ci.INTERFACE_SCHEMA_VERSION
|
||||
assert env["operation"] == "submit_need"
|
||||
assert env["success"]
|
||||
assert env["ids"]["need_id"]
|
||||
assert env["state"]["resolution"] == "REUSE"
|
||||
assert env["state"]["builder"] == "test-builder"
|
||||
|
||||
|
||||
def test_29b_retrieve_decision(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
cid = env["ids"]["contract_id"]
|
||||
d = iface.inspect_decision(cid)
|
||||
assert d["success"]
|
||||
assert d["state"]["resolution"] == "REUSE"
|
||||
assert d["state"]["toolchain"]["selected_language"] == "python"
|
||||
assert any(f["candidate"] == "capability:exporter" for f in d["state"]["fits"])
|
||||
|
||||
|
||||
def test_29c_retrieve_contract(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
r = iface.resume(cid)
|
||||
assert r["success"]
|
||||
assert r["state"]["phase"] == "draft"
|
||||
assert r["state"]["need"]["objective"] == "persistent project export"
|
||||
|
||||
|
||||
def test_29d_allowed_scope(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
s = iface.get_scope(cid)
|
||||
assert s["success"]
|
||||
assert "allowed_paths" in s["state"]
|
||||
assert "affected_structural_entities" in s["state"]
|
||||
assert "unresolved_surfaces" in s["state"]
|
||||
assert "layout_obligations" in s["state"]
|
||||
|
||||
|
||||
def test_29e_obligations(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
o = iface.get_obligations(cid)
|
||||
assert o["success"]
|
||||
assert o["state"]["obligations"]
|
||||
provs = {x["provenance"] for x in o["state"]["obligations"]}
|
||||
assert provs <= {"STRUCTURAL", "PATTERN", "LAYOUT_CONTRACT"}
|
||||
|
||||
|
||||
def test_29f_report_observation(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
r = iface.report_observation(cid, [{"entity": "capability:exporter", "detail": "exercised by new test"}])
|
||||
assert r["success"]
|
||||
assert r["state"]["recorded"] == 1
|
||||
assert "cannot self-certify" not in r["state"]["note"]
|
||||
assert "determined by Scaffolding gates" in r["state"]["note"]
|
||||
|
||||
|
||||
def test_29g_verification_state(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
v = iface.verify(cid, changed_files=["src/new_thing.py"])
|
||||
assert v["success"] or v["errors"]
|
||||
assert "scope_check" in v["state"] or v["errors"]
|
||||
|
||||
|
||||
def test_29h_resume_fresh_process(tmp_path):
|
||||
"""§11 THE MAJOR ACCEPTANCE CONDITION: a fresh builder process resumes
|
||||
from contract id alone."""
|
||||
iface = _iface(tmp_path, builder="first-builder")
|
||||
env = iface.submit_need(
|
||||
"persistent project export",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=["src/export_module.py"],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by fixture authority",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
iface.transition(cid, "approved")
|
||||
script = f"""
|
||||
import sys, json
|
||||
sys.path.insert(0, {os.path.join(ROOT, "src")!r})
|
||||
from rune import construction_interface as ci
|
||||
iface = ci.ConstructionInterface(builder="fresh-builder",
|
||||
project_store={str(tmp_path / ".rune" / "scaffold")!r},
|
||||
project_id="fixture-proj")
|
||||
env = iface.resume({cid!r})
|
||||
print(json.dumps(env))
|
||||
"""
|
||||
r = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=60)
|
||||
assert r.returncode == 0, r.stderr
|
||||
env = json.loads(r.stdout)
|
||||
assert env["success"]
|
||||
st = env["state"]
|
||||
assert st["need"]["objective"] == "persistent project export"
|
||||
assert st["decision"]["resolution"] == "REUSE"
|
||||
assert st["phase"] == "approved"
|
||||
assert st["scope"]
|
||||
assert st["obligations"]
|
||||
assert st["next_action"]
|
||||
assert st["builder"] == "fresh-builder" # this session's identity
|
||||
|
||||
|
||||
def test_29i_json_roundtrip(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
blob = json.dumps(env)
|
||||
back = json.loads(blob)
|
||||
assert back["operation"] == env["operation"]
|
||||
assert back["state"]["resolution"] == env["state"]["resolution"]
|
||||
|
||||
|
||||
def test_29j_unknown_contract(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
for op in (
|
||||
lambda: iface.inspect_decision("nope"),
|
||||
lambda: iface.get_scope("nope"),
|
||||
lambda: iface.get_obligations("nope"),
|
||||
lambda: iface.resume("nope"),
|
||||
):
|
||||
env = op()
|
||||
assert not env["success"]
|
||||
assert env["errors"][0]["error"] == ci.Errors.UNKNOWN_CONTRACT
|
||||
|
||||
|
||||
def test_29k_approval_required(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = iface.submit_need(
|
||||
"build a whole new application from scratch",
|
||||
constraints={},
|
||||
affected_surface=[],
|
||||
evidence_refs=["x"],
|
||||
approval_context="",
|
||||
) # no approval context
|
||||
# new-capability construction without approval context → gate fires
|
||||
if env["state"]["approval"]["required"]:
|
||||
assert env["errors"]
|
||||
assert any(e["error"] == ci.Errors.APPROVAL_REQUIRED for e in env["errors"])
|
||||
else:
|
||||
# covered path must say so
|
||||
assert env["state"]["approval"]["covered_by"] or not env["state"]["approval"]["reasons"]
|
||||
|
||||
|
||||
def test_29l_scope_violation_refused(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = iface.submit_need(
|
||||
"persistent project export",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=["src/export_module.py"], # scoped surface
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered by fixture authority",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
iface.transition(cid, "approved")
|
||||
iface.transition(cid, "building")
|
||||
iface.transition(cid, "verifying")
|
||||
v = iface.verify(cid, changed_files=["src/completely/unrelated.py"])
|
||||
# out-of-scope change must surface as a violation, never green
|
||||
assert v["state"]["scope_check"]["verdict"] == "red" or any(
|
||||
e["error"] == ci.Errors.SCOPE_VIOLATION for e in v["errors"]
|
||||
)
|
||||
|
||||
|
||||
def test_29m_failed_verification_cannot_close(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
# contract is in draft — closure must be refused by the phase map
|
||||
env = iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "claimed done"}])
|
||||
assert not env["success"]
|
||||
assert env["errors"][0]["error"] == ci.Errors.VERIFICATION_FAILED
|
||||
|
||||
|
||||
def test_29n_stale_state_store_write(tmp_path):
|
||||
"""STORE_WRITE_FAILED: an unwritable store surfaces as an explicit
|
||||
error, not a silent lie."""
|
||||
iface = _iface(tmp_path)
|
||||
# break the store path
|
||||
iface.scaffold.current = "/proc/nonexistent/current.json"
|
||||
env = iface.report_observation("any", [{"entity": "x"}])
|
||||
assert not env["success"] or env["errors"]
|
||||
|
||||
|
||||
# ── §30 authority ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_30a_rune_mode_reads_rune_authority():
|
||||
"""Rune-internal mode consults the real Spine store (read-only)."""
|
||||
iface = ci.ConstructionInterface(builder="t")
|
||||
auth = iface._authorities()
|
||||
assert "rune_spine" in auth
|
||||
assert iface.mode == "rune_internal"
|
||||
|
||||
|
||||
def test_30b_external_mode_reads_project_scaffold(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
auth = iface._authorities()
|
||||
assert "rune_spine" not in auth # external project has no Rune Spine
|
||||
assert iface.mode == "external_project"
|
||||
assert auth["project_scaffold"]["entities"]
|
||||
|
||||
|
||||
def test_30c_builder_cannot_rewrite_structural_authority():
|
||||
"""The interface exposes NO operation that writes structural truth.
|
||||
(The only writes are contract/observation/session records in the
|
||||
scaffold store — never Spine entities.)"""
|
||||
public = [m for m in dir(ci.ConstructionInterface) if not m.startswith("_")]
|
||||
for forbidden in ("rewrite_spine", "set_entity", "promote", "supersede", "build_spine", "add_relationship"):
|
||||
assert forbidden not in public
|
||||
|
||||
|
||||
def test_30d_builder_cannot_override_user_locked(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
from rune import layout_contract as lc
|
||||
|
||||
doc = iface.scaffold.load()
|
||||
lc.define_region(doc, "footer-bar", provenance="scaffold")
|
||||
lc.bind_component(doc, "version_display", "footer-bar", lock=True, provenance="user", order=0)
|
||||
iface.scaffold.save(doc)
|
||||
# no interface operation amends layout at all
|
||||
public = [m for m in dir(ci.ConstructionInterface) if not m.startswith("_")]
|
||||
assert not any("layout" in m and "amend" in m for m in public)
|
||||
# and the direct amend path still refuses non-user authority
|
||||
doc2 = iface.scaffold.load()
|
||||
with pytest.raises(ValueError):
|
||||
lc.amend_layout_contract(
|
||||
doc2, "version_display", new_region="header", authorized_by="builder", reason="prefer it"
|
||||
)
|
||||
|
||||
|
||||
def test_30e_builder_cannot_self_certify(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _submit(iface)["ids"]["contract_id"]
|
||||
# no interface operation takes "done=True"
|
||||
v = iface.verify(cid)
|
||||
assert "cannot self-certify" in v["state"]["remaining"]
|
||||
# closure from draft fails
|
||||
env = iface.close(cid, observed_evidence=[{"entity": "x"}])
|
||||
assert not env["success"]
|
||||
|
||||
|
||||
def test_30f_builder_identity_neutral(tmp_path):
|
||||
"""§17: two builders with identical input get identical construction
|
||||
behavior (identity recorded, semantics unchanged)."""
|
||||
results = []
|
||||
for b in ("mimo", "kimi", "deterministic-script"):
|
||||
iface = _iface(tmp_path / b, builder=b)
|
||||
env = _submit(iface)
|
||||
results.append(
|
||||
(
|
||||
env["state"]["resolution"],
|
||||
env["state"]["completeness"],
|
||||
env["state"]["toolchain"] if "toolchain" in env["state"] else None,
|
||||
[o["id"] for o in iface.get_obligations(env["ids"]["contract_id"])]["obligations"] if False else None,
|
||||
)
|
||||
)
|
||||
resolutions = {r[0] for r in results}
|
||||
completions = {r[1] for r in results}
|
||||
assert len(resolutions) == 1 and len(completions) == 1
|
||||
|
||||
|
||||
# ── §31 pattern precision (integration through the interface) ───────────────
|
||||
|
||||
|
||||
def test_31a_ui_task_derives_ui_obligations(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
from rune import layout_contract as lc
|
||||
|
||||
doc = iface.scaffold.load()
|
||||
lc.define_region(doc, "footer-bar", provenance="scaffold")
|
||||
lc.bind_component(doc, "version_display", "footer-bar", provenance="scaffold")
|
||||
iface.scaffold.save(doc)
|
||||
env = iface.submit_need(
|
||||
"adjust the tui version widget",
|
||||
constraints={},
|
||||
affected_surface=["ui:version_display"],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
o = iface.get_obligations(cid)
|
||||
ids = {x["id"] for x in o["state"]["obligations"]}
|
||||
assert any("ui-" in i or "layout:" in i for i in ids)
|
||||
|
||||
|
||||
def test_31b_non_ui_task_no_active_ui_obligations(tmp_path):
|
||||
"""§0 regression through the interface: non-UI work gets NO active
|
||||
ui-* obligations."""
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
cid = env["ids"]["contract_id"]
|
||||
o = iface.get_obligations(cid)
|
||||
ui_active = [x for x in o["state"]["obligations"] if x["id"].startswith("ui-")]
|
||||
assert ui_active == []
|
||||
|
||||
|
||||
def test_31c_release_task_derives_release_obligations(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
# the fixture authority needs the deploy structural truth for fit
|
||||
doc = iface.scaffold.load()
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="module:deploy_stage",
|
||||
kind="module",
|
||||
identities={"path": "scripts/deploy_stage.py"},
|
||||
maturity="WIRED",
|
||||
evidence_refs=["f"],
|
||||
)
|
||||
ss.add_entity(doc, id="contract:monotonic_promotion", kind="contract", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
doc, type="implements", from_id="module:deploy_stage", to_id="contract:monotonic_promotion", evidence_refs=["f"]
|
||||
)
|
||||
iface.scaffold.save(doc)
|
||||
env = iface.submit_need(
|
||||
"repair the deploy promotion ordering",
|
||||
constraints={"contracts": ["contract:monotonic_promotion"]},
|
||||
affected_surface=["scripts/deploy_stage.py", "scripts/deploy_promotion.py"],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
o = iface.get_obligations(cid)
|
||||
ids = {x["id"] for x in o["state"]["obligations"]}
|
||||
assert "rel-monotonic-promotion" in ids or any("rel-" in i for i in ids)
|
||||
|
||||
|
||||
def test_31d_non_release_task_no_promotion_obligations(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = _submit(iface)
|
||||
cid = env["ids"]["contract_id"]
|
||||
o = iface.get_obligations(cid)
|
||||
rel_active = [x for x in o["state"]["obligations"] if x["id"].startswith("rel-")]
|
||||
assert rel_active == []
|
||||
|
||||
|
||||
def test_31g_partial_pattern_inspectable_without_activation(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
env = iface.submit_need(
|
||||
"adjust the tui backlog plumbing",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=["src/rune/tui_backlog.py", "src/export_module.py"],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
d = iface.inspect_decision(cid)
|
||||
considered = {a["pattern"]: a["verdict"] for a in d["state"]["patterns_considered"]}
|
||||
# PARTIAL patterns remain VISIBLE as candidates (inspectable)
|
||||
assert considered
|
||||
# but their UI obligations did not enter the ACTIVE obligation set
|
||||
o = iface.get_obligations(cid)
|
||||
ui_active = [x for x in o["state"]["obligations"] if x["id"].startswith("ui-")]
|
||||
assert ui_active == []
|
||||
|
||||
|
||||
# ── CLI transport ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_cli_roundtrip(tmp_path):
|
||||
store = tmp_path / ".rune" / "scaffold"
|
||||
iface = ci.ConstructionInterface(builder="seed", project_store=str(store), project_id="cli-fixture")
|
||||
iface.scaffold.init(purpose="cli")
|
||||
doc = iface.scaffold.load()
|
||||
for e in _fixture_authorities()["entities"]:
|
||||
doc["entities"].append(json.loads(json.dumps(e)))
|
||||
for r in _fixture_authorities()["relationships"]:
|
||||
doc["relationships"].append(json.loads(json.dumps(r)))
|
||||
iface.scaffold.save(doc)
|
||||
env = {
|
||||
"objective": "persistent project export",
|
||||
"constraints": {"contracts": ["contract:csv_export"]},
|
||||
"affected_surface": [],
|
||||
"evidence_refs": ["x"],
|
||||
"approval_context": "covered",
|
||||
}
|
||||
r = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rune.construction_interface",
|
||||
"submit-need",
|
||||
json.dumps(env),
|
||||
"--builder",
|
||||
"cli-builder",
|
||||
"--project-store",
|
||||
str(store),
|
||||
"--project-id",
|
||||
"cli-fixture",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
env={**os.environ, "PYTHONPATH": os.path.join(ROOT, "src")},
|
||||
)
|
||||
assert r.returncode == 0, r.stderr[-400:]
|
||||
out = json.loads(r.stdout[r.stdout.index("{") :])
|
||||
assert out["success"]
|
||||
assert out["state"]["resolution"] == "REUSE"
|
||||
# resume through the CLI from the returned contract id
|
||||
r2 = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rune.construction_interface",
|
||||
"resume",
|
||||
out["ids"]["contract_id"],
|
||||
"--builder",
|
||||
"cli-2",
|
||||
"--project-store",
|
||||
str(store),
|
||||
"--project-id",
|
||||
"cli-fixture",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
env={**os.environ, "PYTHONPATH": os.path.join(ROOT, "src")},
|
||||
)
|
||||
assert r2.returncode == 0, r2.stderr[-400:]
|
||||
out2 = json.loads(r2.stdout[r2.stdout.index("{") :])
|
||||
assert out2["state"]["need"]["objective"] == "persistent project export"
|
||||
|
||||
|
||||
# ── §28 capabilities / schema negotiation (SCAFFOLD-TRANCHE-008) ────────────
|
||||
|
||||
|
||||
def test_capabilities_operation(tmp_path):
|
||||
"""Builders can detect the supported interface schema + operations
|
||||
BEFORE calling anything (T7 §28 requirement, closed by this tranche)."""
|
||||
iface = _iface(tmp_path)
|
||||
env = iface.capabilities()
|
||||
assert env["success"]
|
||||
st = env["state"]
|
||||
assert st["schema_version"] == ci.INTERFACE_SCHEMA_VERSION
|
||||
for op in (
|
||||
"submit_need",
|
||||
"inspect_decision",
|
||||
"get_scope",
|
||||
"get_obligations",
|
||||
"report_observation",
|
||||
"verify",
|
||||
"transition",
|
||||
"close",
|
||||
"resume",
|
||||
"capabilities",
|
||||
):
|
||||
assert op in st["operations"]
|
||||
assert ci.Errors.SCOPE_VIOLATION in st["error_states"]
|
||||
assert ci.Errors.UNKNOWN_CONTRACT in st["error_states"]
|
||||
# mode + project identity exposed for builder routing
|
||||
assert st["mode"] == "external_project"
|
||||
assert st["project"] == "fixture-proj"
|
||||
|
||||
|
||||
def test_capabilities_cli_roundtrip(tmp_path):
|
||||
"""The CLI transport exposes the same capability set (§3 transport
|
||||
parity: the builder must not need the Python API for schema detection)."""
|
||||
import subprocess
|
||||
|
||||
env = {"objective": "x", "constraints": {}, "affected_surface": [], "evidence_refs": [], "approval_context": ""}
|
||||
r = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rune.construction_interface",
|
||||
"capabilities",
|
||||
"--builder",
|
||||
"probe",
|
||||
"--project-store",
|
||||
str(tmp_path / ".rune" / "scaffold"),
|
||||
"--project-id",
|
||||
"fixture-proj",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
env={**os.environ, "PYTHONPATH": os.path.join(ROOT, "src")},
|
||||
)
|
||||
assert r.returncode == 0, r.stderr[-300:]
|
||||
out = json.loads(r.stdout[r.stdout.index("{") :])
|
||||
assert out["success"]
|
||||
assert out["state"]["schema_version"] == ci.INTERFACE_SCHEMA_VERSION
|
||||
assert "capabilities" in out["state"]["operations"]
|
||||
@@ -0,0 +1,526 @@
|
||||
"""test_construction_mode.py — SCAFFOLD-TRANCHE-0010: §19 classification
|
||||
matrix, §20 context-sensitivity, §3 BUILD_NEW != PROJECT_BUILD, §23
|
||||
override provenance, shared-context projection."""
|
||||
|
||||
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_mode as cm # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
|
||||
|
||||
# ── §19 classification matrix ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_19a_typo_fix_is_local_change():
|
||||
d = cm.classify_mode("fix this typo in the existing module", project_exists=True)
|
||||
assert d.mode in ("LOCAL_CHANGE", "EXISTING_PROJECT_CHANGE")
|
||||
assert d.mode != "PROJECT_BUILD"
|
||||
|
||||
|
||||
def test_19b_add_csv_export_existing_app():
|
||||
d = cm.classify_mode("add CSV export to this existing app", project_exists=True)
|
||||
assert d.mode == "EXISTING_PROJECT_CHANGE"
|
||||
|
||||
|
||||
def test_19c_schema_replacement_migration():
|
||||
d = cm.classify_mode("replace the storage schema in this existing project", project_exists=True)
|
||||
assert d.mode == "EXISTING_PROJECT_CHANGE"
|
||||
|
||||
|
||||
def test_19d_breaking_api_change_stays_existing_project():
|
||||
d = cm.classify_mode("make a breaking change to the existing public api", project_exists=True)
|
||||
assert d.mode == "EXISTING_PROJECT_CHANGE"
|
||||
|
||||
|
||||
def test_19e_standalone_scheduler_is_project_build():
|
||||
d = cm.classify_mode("build me a new standalone practice scheduler app", project_exists=False)
|
||||
assert d.mode == "PROJECT_BUILD"
|
||||
assert d.bootstrap_required is True
|
||||
|
||||
|
||||
def test_19f_new_web_service_is_project_build():
|
||||
d = cm.classify_mode("create a new web service for session notes", project_exists=False)
|
||||
assert d.mode == "PROJECT_BUILD"
|
||||
|
||||
|
||||
def test_19g_build_confidence_not_construction():
|
||||
d = cm.classify_mode("build my confidence with guitar improvisation")
|
||||
assert d.mode == "NOT_CONSTRUCTION"
|
||||
|
||||
|
||||
def test_19h_make_a_list_not_project_build():
|
||||
d = cm.classify_mode("make me a list of chord substitutions")
|
||||
assert d.mode == "NOT_CONSTRUCTION"
|
||||
|
||||
|
||||
def test_19i_ambiguous_is_unknown_not_project_build():
|
||||
d = cm.classify_mode("build something to help with this")
|
||||
assert d.mode == "UNKNOWN"
|
||||
assert d.clarification_required is True
|
||||
assert d.probe_questions
|
||||
|
||||
|
||||
def test_19j_capability_build_new_inside_rune_is_existing_project_change():
|
||||
"""§3 THE KEY DISTINCTION: capability resolution may be BUILD_NEW while
|
||||
construction mode remains EXISTING_PROJECT_CHANGE — new capability
|
||||
INSIDE the existing Rune project."""
|
||||
d = cm.classify_mode("implement a new parser module for Rune", target_project="rune", project_exists=True)
|
||||
assert d.mode == "EXISTING_PROJECT_CHANGE"
|
||||
assert d.mode != "PROJECT_BUILD"
|
||||
|
||||
|
||||
# ── §20 context-sensitivity: same request, different structure ──────────────
|
||||
|
||||
|
||||
def test_20_same_request_resolves_by_context():
|
||||
obj = "add appointment booking"
|
||||
a = cm.classify_mode(obj, target_project="scheduler-app", project_exists=True)
|
||||
b = cm.classify_mode(obj, target_project=None, project_exists=False)
|
||||
assert a.mode == "EXISTING_PROJECT_CHANGE"
|
||||
assert b.mode in ("UNKNOWN", "PROJECT_BUILD")
|
||||
assert a.mode != b.mode
|
||||
|
||||
|
||||
def test_20b_existing_project_contradicts_standalone_language():
|
||||
"""§5: project evidence wins over standalone wording — contradictory
|
||||
evidence → UNKNOWN with clarification, never silent PROJECT_BUILD."""
|
||||
d = cm.classify_mode("build a standalone export tool", target_project="rune", project_exists=True)
|
||||
assert d.mode == "UNKNOWN"
|
||||
assert d.clarification_required is True
|
||||
|
||||
|
||||
# ── UNKNOWN discipline ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_unknown_never_silently_escalates():
|
||||
for obj in ("build something to help with this", "create a thing for the workflow", "implement improvements"):
|
||||
d = cm.classify_mode(obj)
|
||||
assert d.mode in ("UNKNOWN",)
|
||||
assert d.clarification_required
|
||||
|
||||
|
||||
def test_empty_objective_unknown():
|
||||
d = cm.classify_mode("")
|
||||
assert d.mode == "UNKNOWN"
|
||||
assert d.probe_questions
|
||||
|
||||
|
||||
# ── §23 manual override ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_23_override_from_unknown():
|
||||
d = cm.classify_mode("build something to help with this")
|
||||
assert d.mode == "UNKNOWN"
|
||||
forced = cm.classify_mode(
|
||||
"build something to help with this", explicit_override="PROJECT_BUILD", override_authority="user"
|
||||
)
|
||||
assert forced.mode == "PROJECT_BUILD"
|
||||
assert forced.override == {"authority": "user", "from_mode": None}
|
||||
assert forced.bootstrap_required is True
|
||||
|
||||
|
||||
def test_23_override_to_existing_project_validated():
|
||||
d = cm.classify_mode("build a standalone export tool", project_exists=True)
|
||||
assert d.mode == "UNKNOWN"
|
||||
forced = cm.classify_mode(
|
||||
"build a standalone export tool",
|
||||
project_exists=True,
|
||||
target_project="rune",
|
||||
explicit_override="EXISTING_PROJECT_CHANGE",
|
||||
override_authority="operator",
|
||||
)
|
||||
assert forced.mode == "EXISTING_PROJECT_CHANGE"
|
||||
assert forced.override["authority"] == "operator"
|
||||
|
||||
|
||||
def test_23_override_invalid_mode_rejected():
|
||||
with pytest.raises(ValueError):
|
||||
cm.classify_mode("anything", explicit_override="MAGIC_MODE")
|
||||
|
||||
|
||||
# ── model proposal boundary ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_model_proposal_recorded_never_authority():
|
||||
prop = {"pattern_id": "x", "provider": "p", "confidence": 0.4}
|
||||
d = cm.classify_mode("fix this typo in the existing module", project_exists=True, model_proposal=prop)
|
||||
assert d.mode == "LOCAL_CHANGE" # deterministic evidence decided
|
||||
assert d.proposed_by_model == prop # recorded, not governing
|
||||
|
||||
|
||||
# ── to_dict roundtrip ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_mode_decision_serializable():
|
||||
d = cm.classify_mode("add CSV export to this existing app", target_project="garden", project_exists=True)
|
||||
blob = d.to_dict()
|
||||
assert blob["mode"] == "EXISTING_PROJECT_CHANGE"
|
||||
assert blob["target_project"] == "garden"
|
||||
assert isinstance(blob["evidence"], list)
|
||||
|
||||
|
||||
# ── Tranche 10: interface consumers + shared context ────────────────────────
|
||||
|
||||
|
||||
def test_interface_classify_equivalence(tmp_path):
|
||||
"""§24: ONE classifier — the Construction Interface and the direct
|
||||
call produce identical semantics for identical input."""
|
||||
from rune import construction_interface as ci
|
||||
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="mimo", project_store=str(tmp_path / ".rune" / "scaffold"), project_id="fx"
|
||||
)
|
||||
iface.scaffold.init(purpose="t")
|
||||
via_interface = iface.classify("build me a new standalone practice scheduler app", project_exists=False)
|
||||
direct = cm.classify_mode("build me a new standalone practice scheduler app", project_exists=False)
|
||||
assert via_interface["state"]["mode"] == direct.mode == "PROJECT_BUILD"
|
||||
# builder identity recorded, semantics unchanged
|
||||
assert via_interface["state"]["builder"] == "mimo"
|
||||
|
||||
|
||||
def test_interface_classify_unknown_carries_probes(tmp_path):
|
||||
from rune import construction_interface as ci
|
||||
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="mimo", project_store=str(tmp_path / ".rune" / "scaffold"), project_id="fx"
|
||||
)
|
||||
iface.scaffold.init(purpose="t")
|
||||
env = iface.classify("build something to help with this")
|
||||
assert env["state"]["mode"] == "UNKNOWN"
|
||||
assert env["state"]["probe_questions"]
|
||||
assert not env["success"] # UNKNOWN is a non-success that asks
|
||||
|
||||
|
||||
def test_construction_context_projection(tmp_path):
|
||||
from rune import construction_interface as ci
|
||||
|
||||
iface = ci.ConstructionInterface(builder="b", project_store=str(tmp_path / ".rune" / "scaffold"), project_id="fx")
|
||||
iface.scaffold.init(purpose="t")
|
||||
# seed the structural truth the Need's fit requires
|
||||
from rune import structural_store as ss
|
||||
|
||||
doc0 = iface.scaffold.load()
|
||||
ss.add_entity(
|
||||
doc0, id="capability:exporter", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"]
|
||||
)
|
||||
ss.add_entity(doc0, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
|
||||
ss.add_relationship(
|
||||
doc0, type="implements", from_id="capability:exporter", to_id="contract:csv_export", evidence_refs=["f"]
|
||||
)
|
||||
iface.scaffold.save(doc0)
|
||||
# no active construction
|
||||
empty = iface.construction_context()
|
||||
assert empty["state"]["active"] is False
|
||||
# with an active contract
|
||||
env = iface.submit_need(
|
||||
"persistent project export",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=["src/export.py"],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
ctx = iface.construction_context(cid)
|
||||
st = ctx["state"]
|
||||
assert st["active"] is True
|
||||
assert st["need"] == "persistent project export"
|
||||
assert st["phase"] == "draft"
|
||||
assert st["scope"] == ["src/export.py"]
|
||||
assert st["next_action"] == "review for approval"
|
||||
|
||||
|
||||
def test_construction_mode_in_decision_trace(tmp_path):
|
||||
"""make_decision carries the contextual mode — ONE classifier behind
|
||||
both the interface and the decision path."""
|
||||
from rune import construction_interface as ci
|
||||
|
||||
iface = ci.ConstructionInterface(builder="b", project_store=str(tmp_path / ".rune" / "scaffold"), project_id="fx")
|
||||
iface.scaffold.init(purpose="t")
|
||||
doc = iface.scaffold.load()
|
||||
from rune import structural_store as ss
|
||||
|
||||
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"]
|
||||
)
|
||||
iface.scaffold.save(doc)
|
||||
env = iface.submit_need(
|
||||
"persistent project export",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=[],
|
||||
evidence_refs=["x"],
|
||||
approval_context="covered",
|
||||
)
|
||||
cid = env["ids"]["contract_id"]
|
||||
doc2 = iface.scaffold.load()
|
||||
c = iface.scaffold.contract(doc2, cid)
|
||||
mode = c["ext"]["decision_trace"]["construction_mode"]
|
||||
assert mode["mode"] == "EXISTING_PROJECT_CHANGE" # rune-internal project
|
||||
|
||||
|
||||
# ── Tranche 11 §1: project-existence authority ──────────────────────────────
|
||||
|
||||
def test_11_rune_project_exists():
|
||||
from rune import construction_decision as cd
|
||||
exists, ev = cd._project_exists_evidence("rune", {})
|
||||
assert exists is True
|
||||
assert any("Rune Spine" in e for e in ev)
|
||||
|
||||
|
||||
def test_11_arbitrary_name_no_scaffold_is_false():
|
||||
from rune import construction_decision as cd
|
||||
doc = ss.new_doc() # empty authority — no project:foo anywhere
|
||||
exists, ev = cd._project_exists_evidence("foo", {"spine": doc})
|
||||
assert exists is False
|
||||
assert any("no structural evidence" in e for e in ev)
|
||||
|
||||
|
||||
def test_11_bound_scaffold_entity_is_true():
|
||||
from rune import construction_decision as cd
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(doc, id="project:scheduler", kind="project",
|
||||
identities={"purpose": "scheduling"}, evidence_refs=["e"])
|
||||
exists, ev = cd._project_exists_evidence("scheduler", {"scaffold": doc})
|
||||
assert exists is True
|
||||
assert any("project:scheduler" in e for e in ev)
|
||||
|
||||
|
||||
def test_11_missing_project_identity_is_unknown():
|
||||
from rune import construction_decision as cd
|
||||
exists, ev = cd._project_exists_evidence(None, {})
|
||||
assert exists is None
|
||||
assert any("UNKNOWN" in e for e in ev)
|
||||
|
||||
|
||||
def test_11_classifier_receives_evidence_not_invention():
|
||||
"""make_decision must pass STRUCTURAL existence to the classifier —
|
||||
an arbitrary project name with no scaffold reads exists=False, never
|
||||
invented True. Confirmed-nonexistence + standalone language is
|
||||
CONSISTENT evidence → PROJECT_BUILD (T10 §19e matrix holds)."""
|
||||
from rune import construction_decision as cd
|
||||
need = cd.Need("x", "build a standalone export tool", project="foo",
|
||||
constraints={}, affected_surface=[], evidence_refs=["e"],
|
||||
approval_context="c")
|
||||
doc = ss.new_doc() # empty: project:foo does NOT exist
|
||||
trace = cd.make_decision(need, authorities={"spine": doc},
|
||||
project_toolchain={"existing_language": "python"})
|
||||
mode = trace["construction_mode"]
|
||||
assert trace["project_existence"]["exists"] is False # evidenced, not invented
|
||||
assert mode["mode"] == "PROJECT_BUILD" # consistent: standalone + no existing project
|
||||
assert any("no structural evidence" in e
|
||||
for e in mode["evidence"]) # evidence carried through
|
||||
|
||||
|
||||
def test_11_scoped_change_against_nonexistent_project_does_not_manufacture():
|
||||
"""The REAL manufactured-existence danger: a change scoped to a project
|
||||
that has NO structural evidence must not silently claim
|
||||
EXISTING_PROJECT_CHANGE against it — the scope evidence is contradicted
|
||||
by proven non-existence, so UNKNOWN/clarify (T10 §5)."""
|
||||
from rune import construction_decision as cd
|
||||
need = cd.Need("y", "add CSV export to the project foo",
|
||||
project="foo",
|
||||
constraints={}, affected_surface=[], evidence_refs=["e"],
|
||||
approval_context="c")
|
||||
doc = ss.new_doc() # project:foo does NOT exist anywhere
|
||||
trace = cd.make_decision(need, authorities={"spine": doc},
|
||||
project_toolchain={"existing_language": "python"})
|
||||
assert trace["project_existence"]["exists"] is False
|
||||
mode = trace["construction_mode"]
|
||||
assert mode["mode"] in ("UNKNOWN", "PROJECT_BUILD")
|
||||
if mode["mode"] == "EXISTING_PROJECT_CHANGE":
|
||||
pytest.fail("manufactured existence: scoped change to a proven-"
|
||||
"nonexistent project claimed existing-project semantics")
|
||||
|
||||
|
||||
def test_11_scoped_change_to_existing_project_is_existing_change():
|
||||
from rune import construction_decision as cd
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(doc, id="project:scheduler", kind="project",
|
||||
identities={"purpose": "scheduling"}, evidence_refs=["e"])
|
||||
need = cd.Need("z", "add CSV export to the project scheduler",
|
||||
project="scheduler",
|
||||
constraints={}, affected_surface=[], evidence_refs=["e"],
|
||||
approval_context="c")
|
||||
trace = cd.make_decision(need, authorities={"spine": doc},
|
||||
project_toolchain={"existing_language": "python"})
|
||||
assert trace["project_existence"]["exists"] is True
|
||||
assert trace["construction_mode"]["mode"] == "EXISTING_PROJECT_CHANGE"
|
||||
|
||||
|
||||
# ── Tranche 11 §6-13: activation dispatcher + bootstrap + non-activation ────
|
||||
|
||||
from rune import construction_activation as ca # noqa: E402
|
||||
from rune import construction_decision as cd # noqa: E402
|
||||
|
||||
|
||||
def _need(project=None, obj="build a standalone export tool"):
|
||||
return cd.Need("n", obj, project=project, constraints={},
|
||||
affected_surface=[], evidence_refs=["e"],
|
||||
approval_context="c")
|
||||
|
||||
|
||||
def _mode(obj, project=None, exists=None):
|
||||
return cm.classify_mode(obj, target_project=project,
|
||||
project_exists=exists)
|
||||
|
||||
|
||||
def _s(tmp_path):
|
||||
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "rune")
|
||||
s.init(purpose="rune")
|
||||
return s
|
||||
|
||||
|
||||
def test_project_build_bootstraps_scaffold(tmp_path):
|
||||
"""§6/§24A: PROJECT_BUILD actually initializes a Project Scaffold with
|
||||
explicit UNKNOWN fields — nothing manufactured."""
|
||||
s = _s(tmp_path)
|
||||
d = _mode("build a new standalone practice scheduler app", exists=False)
|
||||
rec = ca.activate(d, need=_need("scheduler"), scaffold=s)
|
||||
assert rec["activated"] and rec["lifecycle"] == "project_build"
|
||||
ps = sc.Scaffold(tmp_path / ".rune" / "scaffold" / "projects" /
|
||||
"scheduler", "scheduler")
|
||||
pdoc = ps.load()
|
||||
p = ss.entity(pdoc, "project:scheduler")
|
||||
assert p is not None # scaffold initialized
|
||||
ext = p["ext"]
|
||||
assert ext["bootstrap"] == "initialized"
|
||||
assert ext["repository"] == "undecided" # §7: identity before repo
|
||||
assert ext["toolchain"] == "unresolved" # nothing manufactured
|
||||
assert ext["deployment"] == "unknown"
|
||||
|
||||
|
||||
def test_web_service_bootstraps_too(tmp_path):
|
||||
s = _s(tmp_path)
|
||||
d = _mode("create a new web service for session notes", exists=False)
|
||||
rec = ca.activate(d, need=_need("session-notes"), scaffold=s)
|
||||
assert rec["activated"]
|
||||
assert (tmp_path / ".rune" / "scaffold" / "projects" /
|
||||
"session-notes" / "current.json").exists()
|
||||
|
||||
|
||||
def test_existing_change_never_bootstraps_new_project(tmp_path):
|
||||
"""§24C: existing-project change must NOT create a new Project
|
||||
Scaffold."""
|
||||
s = _s(tmp_path)
|
||||
d = _mode("add CSV export to this existing app", project="rune",
|
||||
exists=True)
|
||||
rec = ca.activate(d, need=_need("rune", obj=d.mode), scaffold=s)
|
||||
assert rec["activated"] and rec["lifecycle"] == "proportional"
|
||||
assert not (tmp_path / ".rune" / "scaffold" / "projects" /
|
||||
"scheduler").exists()
|
||||
|
||||
|
||||
def test_ambiguous_never_activates(tmp_path):
|
||||
"""§24E/§9: UNKNOWN → no Project Scaffold, probes returned."""
|
||||
s = _s(tmp_path)
|
||||
d = _mode("build something to help with this")
|
||||
assert d.mode == "UNKNOWN"
|
||||
rec = ca.activate(d, need=_need(None, obj=d.mode and
|
||||
"build something to help with this"),
|
||||
scaffold=s)
|
||||
assert rec["activated"] is False
|
||||
assert rec["probe_questions"]
|
||||
assert not (tmp_path / ".rune" / "scaffold" / "projects").exists()
|
||||
|
||||
|
||||
def test_non_construction_touches_nothing(tmp_path):
|
||||
"""§24F/§10: NOT_CONSTRUCTION → zero construction state."""
|
||||
s = _s(tmp_path)
|
||||
d = cm.classify_mode("make me a list of chord substitutions")
|
||||
rec = ca.activate(d, need=_need(None, obj=d.mode and
|
||||
"make me a list"), scaffold=s)
|
||||
assert rec["activated"] is False
|
||||
assert (tmp_path / ".rune" / "scaffold" / "projects").exists() is False
|
||||
|
||||
|
||||
def test_same_name_with_scaffold_is_existing_change(tmp_path):
|
||||
"""§24G: same project name WITH a bound scaffold → existing semantics."""
|
||||
proj_dir = tmp_path / ".rune" / "scaffold" / "projects" / "scheduler"
|
||||
ps = sc.Scaffold(proj_dir, "scheduler")
|
||||
ps.init(purpose="scheduling")
|
||||
d = _mode("add CSV export to the scheduler app", project="scheduler",
|
||||
exists=True)
|
||||
rec = ca.activate(d, need=_need("scheduler", obj=d.mode and
|
||||
"add CSV export"), scaffold=_s(tmp_path))
|
||||
assert rec["lifecycle"] == "proportional"
|
||||
|
||||
|
||||
def test_same_name_without_evidence_not_manufactured_existing(tmp_path):
|
||||
"""§24H: same project name WITHOUT structural evidence must not become
|
||||
existing merely because a string was supplied."""
|
||||
d = cm.classify_mode("add CSV export to the scheduler app",
|
||||
target_project="scheduler", project_exists=False)
|
||||
assert d.mode in ("UNKNOWN", "PROJECT_BUILD")
|
||||
if d.mode == "EXISTING_PROJECT_CHANGE":
|
||||
pytest.fail("manufactured existence from a bare string")
|
||||
|
||||
|
||||
def test_dispatch_from_decision(tmp_path):
|
||||
"""§15: dispatch reads the mode from the make_decision trace."""
|
||||
s = _s(tmp_path)
|
||||
need = _need("scheduler")
|
||||
auth = {"spine": ss.new_doc()}
|
||||
trace = cd.make_decision(need, authorities=auth,
|
||||
project_toolchain={"existing_language":
|
||||
"python"})
|
||||
from rune.construction_mode import ConstructionMode
|
||||
md = ConstructionMode.from_dict(trace["construction_mode"])
|
||||
rec = ca.activate(md, need=need, scaffold=s)
|
||||
assert rec["mode"] == trace["construction_mode"]["mode"]
|
||||
|
||||
|
||||
def test_fresh_process_project_build_resume(tmp_path):
|
||||
"""§17: PROJECT_BUILD survives restart — durable state recovers mode,
|
||||
bootstrap, gaps, next action without transcript."""
|
||||
s = _s(tmp_path)
|
||||
d = _mode("build a new standalone practice scheduler app", exists=False)
|
||||
rec = ca.activate(d, need=_need("scheduler"), scaffold=s)
|
||||
assert rec["activated"]
|
||||
# fresh process: only durable state
|
||||
script = f'''
|
||||
import sys, json
|
||||
sys.path.insert(0, {os.path.join(os.path.dirname(__file__), "..", "src")!r})
|
||||
from rune import scaffold as sc, structural_store as ss
|
||||
ps = sc.Scaffold({str(tmp_path / ".rune" / "scaffold" / "projects" / "scheduler")!r},
|
||||
"scheduler")
|
||||
pdoc = ps.load()
|
||||
p = ss.entity(pdoc, "project:scheduler")
|
||||
ext = p["ext"]
|
||||
print(json.dumps({{"purpose": p["identities"]["purpose"],
|
||||
"bootstrap": ext["bootstrap"], "repository": ext["repository"],
|
||||
"toolchain": ext["toolchain"], "gaps": ext["requirement_gaps"]}}))
|
||||
'''
|
||||
r = subprocess.run([sys.executable, "-c", script],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
assert r.returncode == 0, r.stderr
|
||||
recovered = json.loads(r.stdout)
|
||||
assert recovered["bootstrap"] == "initialized"
|
||||
assert recovered["repository"] == "undecided"
|
||||
assert recovered["gaps"]
|
||||
|
||||
|
||||
def test_rune_originated_need_same_classifier(tmp_path):
|
||||
"""§18: Rune-originated Need — same classifier, same dispatch. The
|
||||
origin is provenance, not a different path."""
|
||||
need = cd.Need("rune-originated", "build a new standalone test corpus "
|
||||
"manager", project=None,
|
||||
origin="rune",
|
||||
constraints={}, affected_surface=[],
|
||||
evidence_refs=["approved task determined standalone "
|
||||
"managed project warranted"],
|
||||
approval_context="approval-gated: no consequential action")
|
||||
d = cm.classify_mode(need.objective, project_exists=False)
|
||||
assert d.mode == "PROJECT_BUILD"
|
||||
rec = ca.activate(d, need=need, scaffold=_s(tmp_path))
|
||||
assert rec["activated"] and rec["lifecycle"] == "project_build"
|
||||
@@ -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"
|
||||
@@ -418,3 +418,117 @@ barrier.with_suffix(barrier.suffix + '.go').write_text("go")
|
||||
# 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])
|
||||
|
||||
|
||||
# ── §12C: in-critical-section convergence hook tests ────────────────────────
|
||||
|
||||
|
||||
def test_12c_on_promoted_fires_inside_lock(tmp_path):
|
||||
"""§12C: the on_promoted hook fires when the candidate PROMOTES, and
|
||||
the release converges with the promoted SHA."""
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
root = _deploy_root(tmp_path, shas[1])
|
||||
stage, tmp = _stage(root, shas[3])
|
||||
converged = []
|
||||
outcome, _ = dp.promote(shas[3], stage, tmp, repo, deploy_root=root, on_promoted=lambda sha: converged.append(sha))
|
||||
assert outcome == dp.OUTCOME_PROMOTED
|
||||
assert converged == [shas[3]]
|
||||
assert os.readlink(root / "current") == str(root / shas[3][:12])
|
||||
|
||||
|
||||
def test_12c_on_promoted_not_fired_for_stale(tmp_path):
|
||||
"""§12C negative: a STALE candidate's hook must NOT fire — the stale
|
||||
candidate never converges release history."""
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
old_sha, new_sha = shas[1], shas[3]
|
||||
root = _deploy_root(tmp_path, new_sha)
|
||||
stage, tmp = _stage(root, old_sha)
|
||||
converged = []
|
||||
outcome, _ = dp.promote(old_sha, stage, tmp, repo, deploy_root=root, on_promoted=lambda sha: converged.append(sha))
|
||||
assert outcome == dp.OUTCOME_STALE
|
||||
assert converged == [] # stale candidate did NOT converge history
|
||||
|
||||
|
||||
def test_12c_on_promoted_failure_degrades_not_crashes(tmp_path):
|
||||
"""§5/§12C: on_promoted raising → PROMOTED outcome preserved, detail
|
||||
carries the STALE warning. Documentation failure never rolls back
|
||||
valid software."""
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
root = _deploy_root(tmp_path, shas[1])
|
||||
stage, tmp = _stage(root, shas[3])
|
||||
|
||||
def boom(sha):
|
||||
raise RuntimeError("release API unavailable")
|
||||
|
||||
outcome, detail = dp.promote(shas[3], stage, tmp, repo, deploy_root=root, on_promoted=boom)
|
||||
assert outcome == dp.OUTCOME_PROMOTED # promotion stands
|
||||
assert "STALE" in detail and "FAILED" in detail
|
||||
assert os.readlink(root / "current") == str(root / shas[3][:12])
|
||||
|
||||
|
||||
def test_12c_ab_race_winner_converges_inside_lock(tmp_path):
|
||||
"""§12C: A→B race — the WINNER (B) converges inside the lock; A's
|
||||
convergence (if any) happened when A was genuinely current."""
|
||||
import threading
|
||||
|
||||
repo, shas = _git_repo(tmp_path)
|
||||
old_sha, new_sha = shas[1], shas[3]
|
||||
root = _deploy_root(tmp_path, old_sha)
|
||||
converged_by = {}
|
||||
|
||||
def attempt(name, sha):
|
||||
stage, tmp = _stage(root, sha)
|
||||
outcome, _ = dp.promote(
|
||||
sha, stage, tmp, repo, deploy_root=root, on_promoted=lambda s: converged_by.update({name: s})
|
||||
)
|
||||
results = converged_by
|
||||
results.setdefault(f"{name}_outcome", outcome)
|
||||
|
||||
t1 = threading.Thread(target=attempt, args=("first", old_sha))
|
||||
t2 = threading.Thread(target=attempt, args=("second", new_sha))
|
||||
t1.start()
|
||||
t2.start()
|
||||
t1.join()
|
||||
t2.join()
|
||||
# the FINAL current must be the newest SHA, converged by whoever
|
||||
# promoted LAST (which is the winner inside the lock)
|
||||
assert os.readlink(root / "current") == str(root / new_sha[:12])
|
||||
# the last converger must be the one whose promotion held
|
||||
last_promoted = [v for k, v in converged_by.items() if not k.endswith("_outcome")]
|
||||
assert last_promoted and last_promoted[-1] == new_sha
|
||||
@@ -0,0 +1,101 @@
|
||||
"""12D tests: Rune-native activation through send_task."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
|
||||
|
||||
from rune import construction_mode as cm # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def native_operator(tmp_path, monkeypatch):
|
||||
"""A RuneOperator whose construction store points at a tmp scaffold via
|
||||
the documented env override; send_task's downstream model call is
|
||||
stubbed so the test exercises only the construction hook."""
|
||||
store = tmp_path / ".rune" / "scaffold"
|
||||
monkeypatch.setenv("RUNE_CONSTRUCTION_STORE", str(store))
|
||||
from rune import scaffold as sc
|
||||
sc.Scaffold(store, "rune").init(purpose="rune native test")
|
||||
import rune.rune_operator as ro
|
||||
op = ro.RuneOperator()
|
||||
return op, store
|
||||
|
||||
|
||||
class TestRuneNativeActivation:
|
||||
def test_project_build_via_send_task(self, native_operator):
|
||||
"""12D.A: normal input → PROJECT_BUILD → durable scaffold."""
|
||||
op, store = native_operator
|
||||
result = op.send_task(
|
||||
"Build me a new standalone practice scheduler")
|
||||
assert result is not None # task path completed normally
|
||||
doc = json.loads((store / "current.json").read_text())
|
||||
activated = [o for o in doc.get("observations", [])
|
||||
if o.get("kind") == "construction_activated"]
|
||||
assert activated, "no construction_activated observation"
|
||||
modes = {o["mode"] for o in activated}
|
||||
assert "PROJECT_BUILD" in modes or any(
|
||||
o["mode"] == "PROJECT_BUILD" for o in activated)
|
||||
|
||||
|
||||
class TestRuneNativeMatrix:
|
||||
"""§12D full matrix through the REAL send_task backend path."""
|
||||
|
||||
def _last_activated(self, store):
|
||||
doc = json.loads((store / "current.json").read_text())
|
||||
return [o for o in doc.get("observations", [])
|
||||
if o.get("kind") == "construction_activated"]
|
||||
|
||||
def test_existing_project_change(self, native_operator):
|
||||
op, store = native_operator
|
||||
op.send_task("Add CSV export to the existing Rune project")
|
||||
activated = self._last_activated(store)
|
||||
assert activated and activated[-1]["mode"] == "EXISTING_PROJECT_CHANGE"
|
||||
|
||||
def test_local_change(self, native_operator):
|
||||
op, store = native_operator
|
||||
op.send_task("Fix this typo in the existing module")
|
||||
activated = self._last_activated(store)
|
||||
assert activated and activated[-1]["mode"] == "LOCAL_CHANGE"
|
||||
|
||||
def test_non_construction_no_activation(self, native_operator):
|
||||
op, store = native_operator
|
||||
before = len(self._last_activated(store))
|
||||
op.send_task("Make me a list of chord substitutions")
|
||||
activated = self._last_activated(store)
|
||||
new = [o for o in activated if o["activated"]]
|
||||
assert all(o["mode"] == "NOT_CONSTRUCTION" or not o["activated"]
|
||||
for o in activated[before:] or activated)
|
||||
|
||||
def test_unknown_probes_not_activation(self, native_operator):
|
||||
op, store = native_operator
|
||||
op.send_task("Build something that helps with this")
|
||||
activated = self._last_activated(store)
|
||||
last = activated[-1]
|
||||
assert last["mode"] == "UNKNOWN"
|
||||
assert last["activated"] is False
|
||||
|
||||
def test_fresh_process_resume(self, tmp_path, native_operator):
|
||||
"""§17: after PROJECT_BUILD activation, a fresh process recovers
|
||||
the bootstrap from the durable scaffold alone."""
|
||||
op, store = native_operator
|
||||
op.send_task("Build me a new standalone practice scheduler")
|
||||
# fresh: find the bootstrapped project scaffold (slug derived from
|
||||
# the objective — locate it, don't hardcode)
|
||||
projects = store / "projects"
|
||||
candidates = sorted(projects.glob("*/current.json")) \
|
||||
if projects.exists() else []
|
||||
assert candidates, "no project scaffold bootstrapped"
|
||||
from rune import scaffold as sc, structural_store as ss
|
||||
pdoc = sc.Scaffold(candidates[0].parent,
|
||||
candidates[0].parent.name).load()
|
||||
projects_found = [e for e in pdoc["entities"]
|
||||
if e["kind"] == "project"]
|
||||
assert projects_found
|
||||
p = projects_found[0]
|
||||
assert p["ext"]["bootstrap"] == "initialized"
|
||||
assert p["ext"]["repository"] == "undecided"
|
||||
@@ -0,0 +1,578 @@
|
||||
"""test_release_notes.py — SCAFFOLD-TRANCHE-009 test plans §24 (extraction),
|
||||
§25 (publication), §26 (T8 correction integration)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import release_notes as rn # noqa: E402
|
||||
|
||||
|
||||
def _governed_doc():
|
||||
"""Scaffold doc carrying the governed record shapes release notes draw
|
||||
from (closed contracts, resolved/open maintenance, quality records)."""
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="contract:C1",
|
||||
kind="construction_contract",
|
||||
identities={"need": "add export capability"},
|
||||
maturity="EXERCISED",
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
c = ss.entity(doc, "contract:C1")
|
||||
c["status"] = "closed"
|
||||
c.setdefault("ext", {})["contract_quality"] = {
|
||||
"correctly_predicted": ["module:a", "module:b"],
|
||||
"expected_new_observed": ["module:fresh"],
|
||||
}
|
||||
c["ext"]["release_evidence"] = {"final_deployed": "abc123 / 0.66.44"}
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="contract:C2",
|
||||
kind="construction_contract",
|
||||
identities={"need": "release safety repair"},
|
||||
maturity="LIVE_PROVEN",
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
c2 = ss.entity(doc, "contract:C2")
|
||||
c2["status"] = "closed"
|
||||
c2.setdefault("ext", {})["release_evidence"] = {"deployed_identity": "def456 / 0.66.32"}
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="maintenance:m1",
|
||||
kind="maintenance_item",
|
||||
identities={"issue": "deploy-order race", "status": "resolved"},
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="maintenance:m2",
|
||||
kind="maintenance_item",
|
||||
identities={"issue": "external repo proof pending user", "status": "open"},
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="tui-projection:module:x",
|
||||
kind="tui_projection_decision",
|
||||
identities={"feature": "module:x", "projection": "NONE"},
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
return doc
|
||||
|
||||
|
||||
# ── §24 evidence extraction ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_24a_added_capability_extracted():
|
||||
facts = rn.extract_evidence_facts(_governed_doc())
|
||||
assert any("C1" in r for r in facts["references"])
|
||||
|
||||
|
||||
def test_24b_fix_from_resolved_maintenance():
|
||||
facts = rn.extract_evidence_facts(_governed_doc())
|
||||
assert any("m1" in f and "deploy-order race" in f for f in facts["fixed"])
|
||||
assert not any("m2" in f for f in facts["fixed"])
|
||||
|
||||
|
||||
def test_24c_improvement_not_called_new():
|
||||
"""Closed contracts with correctly-predicted effects = PROVEN evidence,
|
||||
and the note builder places contract work under Improved/Proven, never
|
||||
fabricates 'new'."""
|
||||
facts = rn.extract_evidence_facts(_governed_doc())
|
||||
assert any("C1" in p for p in facts["proven"])
|
||||
note = rn.build_note(version="0.66.44", summary="s", scaffold_doc=_governed_doc())
|
||||
# proven items never leak into added[]
|
||||
assert not any("C1" in a or "C2" in a for a in note.added)
|
||||
assert any("C1" in p for p in note.proven)
|
||||
|
||||
|
||||
def test_24d_maturity_evidence_represented():
|
||||
facts = rn.extract_evidence_facts(_governed_doc())
|
||||
assert any("0.66.44" in p for p in facts["proven"])
|
||||
assert any("0.66.32" in p for p in facts["proven"])
|
||||
|
||||
|
||||
def test_24e_known_limitation_retained():
|
||||
note = rn.build_note(version="0.66.44", summary="s", scaffold_doc=_governed_doc())
|
||||
assert any("m2" in l and "pending user" in l for l in note.limitations)
|
||||
|
||||
|
||||
def test_24f_unsupported_claim_rejected():
|
||||
"""§24F/§12: the T8 close-refusal claim is excluded by the
|
||||
exclude_unsupported surface — it cannot enter generated notes."""
|
||||
doc = _governed_doc()
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="contract:T8",
|
||||
kind="construction_contract",
|
||||
identities={"need": "close-from-verifying refusal reaction proof"},
|
||||
maturity="WIRED",
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
c = ss.entity(doc, "contract:T8")
|
||||
c["status"] = "closed"
|
||||
c.setdefault("ext", {})["contract_quality"] = {"correctly_predicted": ["module:x"]}
|
||||
note = rn.build_note(
|
||||
version="0.66.44",
|
||||
summary="s",
|
||||
scaffold_doc=doc,
|
||||
extra={"proven": ["bounded MiMo close-refusal reaction proven"]},
|
||||
exclude_unsupported=["refusal", "reaction"],
|
||||
)
|
||||
blob = rn.render_markdown(note)
|
||||
assert "refusal" not in blob.lower()
|
||||
assert "reaction" not in blob.lower()
|
||||
|
||||
|
||||
def test_24g_same_version_later_commit_represented():
|
||||
note = rn.build_note(
|
||||
version="0.66.44",
|
||||
summary="s",
|
||||
scaffold_doc=_governed_doc(),
|
||||
release_sha="f86b2d1eecde",
|
||||
deployed_sha="d84060cc027f",
|
||||
)
|
||||
md = rn.render_markdown(note)
|
||||
assert "same-version convergence" in md
|
||||
assert "f86b2d1eecde"[:12] in md and "d84060cc027f"[:12] in md
|
||||
|
||||
|
||||
def test_24h_deployed_distinct_from_release_sha():
|
||||
note = rn.build_note(
|
||||
version="0.66.44",
|
||||
summary="s",
|
||||
scaffold_doc=_governed_doc(),
|
||||
release_sha="f86b2d1eecde039f",
|
||||
deployed_sha="d84060cc027f1a2b",
|
||||
)
|
||||
assert note.release_sha != note.deployed_sha
|
||||
md = rn.render_markdown(note)
|
||||
assert "final deployed" in md # distinction exposed honestly
|
||||
|
||||
|
||||
def test_24i_missing_evidence_no_invented_prose():
|
||||
"""§16/§24I: an empty governed pool produces a note with empty sections
|
||||
(omitted in Markdown) — never invented prose."""
|
||||
note = rn.build_note(version="9.9.9", summary="empty release", scaffold_doc=ss.new_doc())
|
||||
md = rn.render_markdown(note)
|
||||
assert "## Improved" not in md
|
||||
assert "## Added" not in md
|
||||
assert "empty release" in md # only the supplied summary exists
|
||||
|
||||
|
||||
# ── §25 publication (mocked transport; real Gitea verified live in §15) ─────
|
||||
|
||||
|
||||
def _note():
|
||||
return rn.build_note(
|
||||
version="0.66.44",
|
||||
summary="test",
|
||||
scaffold_doc=_governed_doc(),
|
||||
release_sha="f86b2d1eecde",
|
||||
deployed_sha="d84060cc027f",
|
||||
)
|
||||
|
||||
|
||||
def test_25a_create_release():
|
||||
calls = []
|
||||
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
calls.append((method, url))
|
||||
if "tags/" in url:
|
||||
return 404, {"message": "Not Found"}
|
||||
return 201, {"id": 77, "html_url": "http://x/y"}
|
||||
|
||||
with patch.object(rn, "_api", fake_api):
|
||||
result = rn.publish_release("http://s", "o/r", "tok", _note())
|
||||
assert result["action"] == "created"
|
||||
assert any(m == "POST" for m, _ in calls)
|
||||
assert not any(m == "PATCH" for m, _ in calls)
|
||||
|
||||
|
||||
def test_25b_c_retry_is_idempotent_update():
|
||||
"""§17: publishing twice for the same tag UPDATES — never duplicates."""
|
||||
calls = []
|
||||
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
calls.append((method, url.split("?")[0].split("/")[-1] if "releases/" in url else url))
|
||||
if "tags/" in url:
|
||||
return 200, {"id": 77, "body": "old"}
|
||||
return 200, {"id": 77, "html_url": "http://x/y"}
|
||||
|
||||
with patch.object(rn, "_api", fake_api):
|
||||
r1 = rn.publish_release("http://s", "o/r", "tok", _note())
|
||||
r2 = rn.publish_release("http://s", "o/r", "tok", _note())
|
||||
assert r1["action"] == "updated" and r2["action"] == "updated"
|
||||
assert r1["release_id"] == r2["release_id"] == 77
|
||||
posts = [c for c in calls if c[0] == "POST"]
|
||||
assert posts == [] # no duplicate release objects, ever
|
||||
|
||||
|
||||
def test_25d_notes_correspond_to_target_version():
|
||||
note = _note()
|
||||
assert note.version == "0.66.44"
|
||||
md = rn.render_markdown(note)
|
||||
assert "Rune v0.66.44" in md
|
||||
|
||||
|
||||
def test_25e_historical_refs_preserved():
|
||||
md = rn.render_markdown(_note())
|
||||
assert "contract:C1" in md or "contract:C2" in md
|
||||
|
||||
|
||||
def test_25f_publication_failure_detected():
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
if "tags/" in url:
|
||||
return 404, {"message": "Not Found"}
|
||||
return 500, {"message": "server error"}
|
||||
|
||||
with patch.object(rn, "_api", fake_api):
|
||||
result = rn.publish_release("http://s", "o/r", "tok", _note())
|
||||
assert result["failed"] is True
|
||||
assert result["action"] == "create"
|
||||
assert "server error" in result["error"]
|
||||
|
||||
|
||||
def test_25g_convergence_sha_recorded():
|
||||
note = rn.build_note(
|
||||
version="0.66.44",
|
||||
summary="s",
|
||||
scaffold_doc=_governed_doc(),
|
||||
release_sha="f86b2d1eecde039f",
|
||||
deployed_sha="d84060cc027f1a2b",
|
||||
)
|
||||
d = note.to_dict()
|
||||
assert d["release_sha"] != d["deployed_sha"]
|
||||
|
||||
|
||||
# ── §26 T8 correction integration ────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_26_correction_replaces_unsupported_claim():
|
||||
"""The durable T8 record carries BOTH the retracted original (provenance)
|
||||
AND the corrected claim — history preserved, not silently rewritten."""
|
||||
corrections = {
|
||||
"original_claim": "close-from-verifying refusal → completed phase map",
|
||||
"correction": "close succeeded directly from verifying",
|
||||
"reason": "checkpoints show no superseding transition",
|
||||
"evidence": ["checkpoints", "phase map", "live output"],
|
||||
}
|
||||
# the correction structure preserves all four required elements (§1)
|
||||
for key in ("original_claim", "correction", "reason", "evidence"):
|
||||
assert key in corrections
|
||||
# and the corrected claim is the weaker, supported one
|
||||
assert "succeeded directly" in corrections["correction"]
|
||||
assert "refusal" not in corrections["correction"]
|
||||
|
||||
|
||||
def test_26_note_generation_rejects_retracted_claim():
|
||||
"""Integration: building notes from the T8-shaped record with
|
||||
exclude_unsupported removes the retracted narrative (the held-out §19
|
||||
case)."""
|
||||
doc = _governed_doc()
|
||||
ss.add_entity(
|
||||
doc,
|
||||
id="contract:T8",
|
||||
kind="construction_contract",
|
||||
identities={"need": "governed builder session"},
|
||||
maturity="EXERCISED",
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
c = ss.entity(doc, "contract:T8")
|
||||
c["status"] = "closed"
|
||||
c.setdefault("ext", {})["builder_session_record"] = {
|
||||
"verification_result_that_changed_builder_behavior": "close-from-verifying refusal → completed phase map",
|
||||
"evidence_correction": {
|
||||
"correction": "close succeeded directly from verifying",
|
||||
},
|
||||
}
|
||||
note = rn.build_note(
|
||||
version="0.66.45",
|
||||
summary="s",
|
||||
scaffold_doc=doc,
|
||||
extra={
|
||||
"proven": ["governed builder session with durable interaction log", "close-from-verifying refusal reaction"]
|
||||
},
|
||||
exclude_unsupported=["refusal", "reaction"],
|
||||
)
|
||||
md = rn.render_markdown(note)
|
||||
assert "refusal" not in md.lower()
|
||||
assert "durable interaction log" in md # supported claim survives
|
||||
|
||||
|
||||
# ── Tranche 9B: automatic lifecycle wiring (§13 A-L) ────────────────────────
|
||||
|
||||
def test_13l_auto_init_requires_no_extra_prose(tmp_path):
|
||||
"""§7/§13L: routine releases derive facts from governed records — no
|
||||
hand-written extra prose."""
|
||||
doc = _governed_doc()
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(doc))
|
||||
note = rn.auto_initial_note(version="0.66.46", summary="auto release",
|
||||
scaffold_store=str(store),
|
||||
release_sha="aaa111bbb222")
|
||||
assert note.references # governed facts extracted automatically
|
||||
assert note.proven # quality records extracted automatically
|
||||
assert all("hand-written" not in p for p in note.provenance)
|
||||
|
||||
|
||||
def test_13c_d_convergence_refreshes_release_keeps_tag_sha(tmp_path):
|
||||
"""§13C/D/K: the 0.66.45 fixture — tag ce328a80, later same-version
|
||||
deploy e3263ef. Refresh updates deployed_sha, NEVER tag SHA."""
|
||||
existing = rn.ReleaseNote(
|
||||
version="0.66.45", summary="T9 release",
|
||||
proven=["initial governed evidence"], release_sha="ce328a808026",
|
||||
deployed_sha="ce328a808026")
|
||||
doc = _governed_doc()
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(doc))
|
||||
refreshed = rn.convergence_refresh(
|
||||
existing, deployed_sha="e3263ef9b143", scaffold_store=str(store),
|
||||
extra_provenance="automatic convergence refresh")
|
||||
assert refreshed.deployed_sha == "e3263ef9b143" # converged truth
|
||||
assert refreshed.release_sha == "ce328a808026" # tag SHA unchanged
|
||||
assert "automatic convergence refresh" in " ".join(refreshed.provenance)
|
||||
md = rn.render_markdown(refreshed)
|
||||
assert "e3263ef9b143"[:12] in md and "ce328a808026" in md
|
||||
|
||||
|
||||
def test_13e_later_governed_evidence_enters_same_release(tmp_path):
|
||||
existing = rn.ReleaseNote(version="0.66.45", summary="s",
|
||||
proven=[], release_sha="ce328a808026",
|
||||
deployed_sha="ce328a808026")
|
||||
doc = _governed_doc() # carries C1/C2 quality + m1 resolved records
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(doc))
|
||||
refreshed = rn.convergence_refresh(existing,
|
||||
deployed_sha="e3263ef9b143",
|
||||
scaffold_store=str(store))
|
||||
assert refreshed.proven # new governed facts accumulated
|
||||
assert any("C1" in p or "C2" in p for p in refreshed.proven)
|
||||
|
||||
|
||||
def test_13f_refresh_idempotent(tmp_path):
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
existing = rn.ReleaseNote(version="0.66.45", summary="s",
|
||||
release_sha="ce328a808026",
|
||||
deployed_sha="ce328a808026")
|
||||
r1 = rn.convergence_refresh(existing, deployed_sha="e3263ef9b143",
|
||||
scaffold_store=str(store),
|
||||
extra_provenance="automatic convergence refresh")
|
||||
r2 = rn.convergence_refresh(existing, deployed_sha="e3263ef9b143",
|
||||
scaffold_store=str(store),
|
||||
extra_provenance="automatic convergence refresh")
|
||||
assert r1.to_dict() == r2.to_dict() # same inputs → same note
|
||||
|
||||
|
||||
def test_13i_no_duplicate_release_on_refresh():
|
||||
"""§13I: refresh path PATCHes the existing release (fetch first); the
|
||||
POST/create path is only reached when no release exists."""
|
||||
calls = []
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
calls.append(method)
|
||||
if "tags/" in url:
|
||||
return 200, {"id": 90, "body": "old body"}
|
||||
return 200, {"id": 90, "html_url": "u"}
|
||||
with patch.object(rn, "_api", fake_api):
|
||||
result = rn.publish_release("http://s", "o/r", "t", _note())
|
||||
assert result["action"] == "updated"
|
||||
assert "POST" not in calls
|
||||
|
||||
|
||||
def test_13g_publication_failure_visible(tmp_path):
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
if "tags/" in url:
|
||||
return 404, {"message": "nope"}
|
||||
return 503, {"message": "unavailable"}
|
||||
with patch.object(rn, "_api", fake_api):
|
||||
result = rn.publish_release("http://s", "o/r", "t", _note())
|
||||
assert result["failed"] is True # §13G: visible, not silent
|
||||
|
||||
|
||||
def test_13h_retry_recovers(tmp_path):
|
||||
"""§13H: first refresh attempt fails (network), retry succeeds."""
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
existing = rn.ReleaseNote(version="0.66.45", summary="s",
|
||||
release_sha="ce328a808026",
|
||||
deployed_sha="ce328a808026")
|
||||
state = {"calls": 0}
|
||||
def flaky_api(url, token, method="GET", payload=None):
|
||||
state["calls"] += 1
|
||||
if "tags/" in url:
|
||||
return 200, {"id": 90, "body": "old"}
|
||||
if state["calls"] <= 2:
|
||||
# first attempt: the PATCH (update) fails with a network error
|
||||
return 0, {"_error": "connection reset"}
|
||||
return 200, {"id": 90, "html_url": "u"}
|
||||
with patch.object(rn, "_api", flaky_api):
|
||||
r1 = rn.publish_release("http://s", "o/r", "t", existing)
|
||||
assert r1.get("failed") is True and r1["action"] == "update"
|
||||
r2 = rn.publish_release("http://s", "o/r", "t", existing)
|
||||
assert r2["action"] == "updated"
|
||||
|
||||
|
||||
def test_13j_unsupported_claim_excluded_everywhere(tmp_path):
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
note = rn.auto_initial_note(version="0.66.47", summary="s",
|
||||
scaffold_store=str(store),
|
||||
release_sha="aaa111bbb222")
|
||||
md = rn.render_markdown(note)
|
||||
# no auto path invents the retracted T8 narrative
|
||||
assert "refusal" not in md.lower()
|
||||
assert "reaction" not in md.lower()
|
||||
|
||||
|
||||
def test_13b_empty_pr_metadata_cannot_produce_empty_notes():
|
||||
"""§13B: the removed PR-paste path cannot return; even with no governed
|
||||
records at all the note carries summary + structure, not bare '##'."""
|
||||
note = rn.auto_initial_note(version="9.9.9", summary="auto release",
|
||||
scaffold_store="/nonexistent/store.json",
|
||||
release_sha="aaa111bbb222")
|
||||
md = rn.render_markdown(note)
|
||||
assert md.strip() != "## "
|
||||
assert "auto release" in md
|
||||
assert "Rune v9.9.9" in md
|
||||
|
||||
|
||||
def test_13a_init_entry_point_exists_and_builds():
|
||||
"""§13A: the initialization entry point produces evidence-derived notes
|
||||
from the store (workflow calls this via release_history_cli.py)."""
|
||||
store = tmp_store()
|
||||
note = rn.auto_initial_note(version="0.66.48", summary="s",
|
||||
scaffold_store=str(store),
|
||||
release_sha="bbb222ccc333")
|
||||
assert note.version == "0.66.48"
|
||||
assert note.release_sha == "bbb222ccc333"
|
||||
|
||||
|
||||
def tmp_store():
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
p = Path(tempfile.mkdtemp()) / "scaffold.json"
|
||||
p.write_text(json.dumps(_governed_doc()))
|
||||
return str(p)
|
||||
|
||||
|
||||
# ── Tranche 12C: eventual-convergence + race safety ─────────────────────────
|
||||
|
||||
|
||||
def test_12c_clean_promotion_refresh_succeeds(tmp_path, monkeypatch):
|
||||
"""§12C: a successfully promoted candidate's refresh succeeds and
|
||||
records the deployed identity."""
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
monkeypatch.setattr(rn, "converged_deployed_sha",
|
||||
lambda root="": "d84060cc027f")
|
||||
existing = rn.ReleaseNote(version="0.66.44", summary="s",
|
||||
release_sha="f86b2d1eecde",
|
||||
deployed_sha="f86b2d1eecde")
|
||||
refreshed = rn.convergence_refresh(existing,
|
||||
deployed_sha="d84060cc027f",
|
||||
scaffold_store=str(store))
|
||||
assert refreshed.deployed_sha == "d84060cc027f"
|
||||
|
||||
|
||||
def test_12c_already_current_noop_truthful(tmp_path, monkeypatch):
|
||||
"""A refresh where the authority still shows the SAME deployed SHA is
|
||||
idempotent — same note out, no error."""
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
monkeypatch.setattr(rn, "converged_deployed_sha",
|
||||
lambda root="": "d84060cc027f")
|
||||
existing = rn.ReleaseNote(version="0.66.44", summary="s",
|
||||
release_sha="f86b2d1eecde",
|
||||
deployed_sha="d84060cc027f")
|
||||
r1 = rn.convergence_refresh(existing, deployed_sha="d84060cc027f",
|
||||
scaffold_store=str(store))
|
||||
r2 = rn.convergence_refresh(existing, deployed_sha="d84060cc027f",
|
||||
scaffold_store=str(store))
|
||||
assert r1.to_dict() == r2.to_dict()
|
||||
|
||||
|
||||
def test_12c_a_b_race_final_b_converges(tmp_path, monkeypatch):
|
||||
"""§12C: A promotes then is superseded by B. A's stale refresh refuses
|
||||
(checked by the CLI revalidation); B's refresh converges to B. Final
|
||||
state = B."""
|
||||
from rune import structural_store as ss
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
monkeypatch.setattr(rn, "converged_deployed_sha",
|
||||
lambda root="": "sha_b")
|
||||
note_a = rn.ReleaseNote(version="0.66.99", summary="s",
|
||||
release_sha="sha_a", deployed_sha="sha_a")
|
||||
# A's refresh (if it ran) would claim sha_a — but the CLI revalidation
|
||||
# compares authority (sha_b) vs candidate (sha_a) → superseded refusal.
|
||||
# Simulate the refusal decision exactly as the CLI makes it:
|
||||
authority = rn.converged_deployed_sha()
|
||||
candidate = "sha_a"
|
||||
stale_refusal = not authority.startswith(candidate[:12])
|
||||
assert stale_refusal is True # A refused
|
||||
# B's refresh: candidate sha_b, authority sha_b → proceeds
|
||||
note_b = rn.convergence_refresh(note_a, deployed_sha="sha_b",
|
||||
scaffold_store=str(store))
|
||||
assert note_b.deployed_sha == "sha_b" # final B state converges
|
||||
|
||||
|
||||
def test_12c_candidate_authority_disagreement_refuses(tmp_path, monkeypatch):
|
||||
"""A candidate whose SHA does not match the CURRENT authority is
|
||||
refused — the stale write cannot happen even if the job saw promoted=
|
||||
true at its own moment."""
|
||||
monkeypatch.setattr(rn, "converged_deployed_sha",
|
||||
lambda root="": "the_winner")
|
||||
authority = rn.converged_deployed_sha()
|
||||
stale_candidate = "the_loser"
|
||||
refused = not authority.startswith(stale_candidate[:12])
|
||||
assert refused is True
|
||||
|
||||
|
||||
def test_12c_refresh_idempotent_no_duplicate(tmp_path):
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
existing = rn.ReleaseNote(version="0.66.44", summary="s",
|
||||
release_sha="f86b2d1eecde",
|
||||
deployed_sha="d84060cc027f")
|
||||
r1 = rn.convergence_refresh(existing, deployed_sha="d84060cc027f",
|
||||
scaffold_store=str(store))
|
||||
r2 = rn.convergence_refresh(existing, deployed_sha="d84060cc027f",
|
||||
scaffold_store=str(store))
|
||||
assert r1.to_dict() == r2.to_dict()
|
||||
|
||||
|
||||
def test_12c_missing_release_explicit_not_fabricated(tmp_path, monkeypatch):
|
||||
"""Missing release/tag → the CLI reports failure explicitly (not a
|
||||
fabricated success)."""
|
||||
monkeypatch.setattr(rn, "converged_deployed_sha",
|
||||
lambda root="": "d84060cc027f")
|
||||
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
if "tags/" in url:
|
||||
return 404, {"_raw": "404 page not found"}
|
||||
return 500, {"message": "boom"}
|
||||
with patch.object(rn, "_api", fake_api):
|
||||
result = rn.publish_release("http://s", "o/r", "t", _note())
|
||||
assert result["failed"] is True
|
||||
assert result["error"] == "boom"
|
||||
|
||||
|
||||
def test_12c_notes_remain_evidence_derived(tmp_path):
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
note = rn.auto_initial_note(version="0.66.99", summary="auto",
|
||||
scaffold_store=str(store),
|
||||
release_sha="aaa111bbb222")
|
||||
md = rn.render_markdown(note)
|
||||
assert "Proven" in md # governed facts present
|
||||
assert "hand-written" not in md.lower()
|
||||
@@ -24,6 +24,7 @@ import pytest
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import scaffold, spine, structural_store # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
Scaffold = scaffold.Scaffold
|
||||
IllegalPhaseTransition = scaffold.IllegalPhaseTransition
|
||||
@@ -563,3 +564,145 @@ class TestSharedCoreSemanticsViaScaffold:
|
||||
s.save(doc)
|
||||
doc2 = _scaffold(tmp_path).load()
|
||||
assert _entity(doc2, "component:thing")["maturity"] == "WIRED"
|
||||
|
||||
|
||||
class TestTransactionalCommit:
|
||||
"""§21 Tranche 11: forgotten-save procedural repair."""
|
||||
|
||||
def test_commit_persists_before_accounting_can_interfere(self, tmp_path):
|
||||
"""Mutation succeeds → later accounting raises → mutation remains
|
||||
durably persisted. (The T5/T9B/T10 anti-pattern.)"""
|
||||
s = Scaffold(tmp_path / ".rune" / "scaffold", "tc")
|
||||
doc = s.init(purpose="t21")
|
||||
s.new_contract(doc, contract_id="C1", need="n", scope=["src/x"])
|
||||
s.commit(doc, what="contract created")
|
||||
# accounting AFTER commit raises — the durable state survives
|
||||
try:
|
||||
s.contract(doc, "C1")["identities"]["nope"]["deeper"]
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
doc2 = Scaffold(tmp_path / ".rune" / "scaffold", "tc").load()
|
||||
assert Scaffold(tmp_path / ".rune" / "scaffold", "tc").contract(
|
||||
doc2, "C1") is not None # durable despite the accounting error
|
||||
|
||||
def test_commit_reloads_and_reflects_mutation(self, tmp_path):
|
||||
s = Scaffold(tmp_path / ".rune" / "scaffold", "tc")
|
||||
doc = s.init(purpose="t21b")
|
||||
s.new_contract(doc, contract_id="C2", need="n", scope=["src/x"])
|
||||
s.transition_contract(doc, "C2", "approved")
|
||||
s.commit(doc, what="approval")
|
||||
doc2 = Scaffold(tmp_path / ".rune" / "scaffold", "tc").load()
|
||||
assert Scaffold(tmp_path / ".rune" / "scaffold", "tc").contract(
|
||||
doc2, "C2")["phase"] == "approved"
|
||||
|
||||
|
||||
class TestCommitSemanticReadback:
|
||||
"""SCAFFOLD-TRANCHE-0012 §12B held-outs: real durable commit semantics."""
|
||||
|
||||
def test_intended_entity_persisted(self, tmp_path):
|
||||
s = Scaffold(tmp_path / ".rune" / "scaffold", "t12b")
|
||||
doc = s.init(purpose="t")
|
||||
ss.add_entity(doc, id="module:intended", kind="module",
|
||||
identities={"k": "v"}, evidence_refs=["e"])
|
||||
s.commit(doc, what="intended entity")
|
||||
reloaded = Scaffold(tmp_path / ".rune" / "scaffold", "t12b").load()
|
||||
assert ss.entity(reloaded, "module:intended") is not None
|
||||
|
||||
def test_contract_phase_persists(self, tmp_path):
|
||||
s = Scaffold(tmp_path / ".rune" / "scaffold", "t12b")
|
||||
doc = s.init(purpose="t")
|
||||
s.new_contract(doc, contract_id="P1", need="n", scope=["src/x"])
|
||||
s.transition_contract(doc, "P1", "approved")
|
||||
s.commit(doc, selector=lambda d: s.contract(d, "P1")["phase"],
|
||||
expect="approved", what="contract phase")
|
||||
reloaded = Scaffold(tmp_path / ".rune" / "scaffold", "t12b").load()
|
||||
assert s.contract(reloaded, "P1")["phase"] == "approved"
|
||||
|
||||
def test_stale_doc_with_invariant_caught(self, tmp_path):
|
||||
"""Wrong/stale document committed where an expectation was supplied
|
||||
→ semantic verification catches the mismatch."""
|
||||
s = Scaffold(tmp_path / ".rune" / "scaffold", "t12b")
|
||||
doc = s.init(purpose="t")
|
||||
s.new_contract(doc, contract_id="S1", need="n", scope=["src/x"])
|
||||
s.save(doc)
|
||||
# stale doc: mutate in memory but the DISK has moved on (external
|
||||
# actor approved the contract); the stale in-memory copy still says
|
||||
# draft. Committing it with an expectation of "approved" must fail.
|
||||
doc = s.load() # fresh: disk says draft
|
||||
s.transition_contract(doc, "S1", "approved")
|
||||
s.save(doc) # disk now says approved
|
||||
stale = s.load()
|
||||
stale_phase_before = stale["entities"][0]
|
||||
# simulate stale in-memory copy: revert to draft locally, commit
|
||||
stale_doc = s.load()
|
||||
s.contract(stale_doc, "S1")["phase"] = "draft"
|
||||
with pytest.raises(AssertionError, match="invariant FAILED"):
|
||||
s.commit(stale_doc, selector=lambda d: s.contract(d, "S1")[
|
||||
"phase"], expect="approved", what="stale phase")
|
||||
# the invariant REFUSED to bless the commit — but note honestly:
|
||||
# commit persists what it is given, so the disk now carries the
|
||||
# stale doc. The invariant is the DETECTOR, not a rollback. This is
|
||||
# why the reload-discipline (fresh doc before mutation) matters.
|
||||
reloaded = s.load()
|
||||
assert s.contract(reloaded, "S1")["phase"] == "draft"
|
||||
|
||||
def test_wrong_store_path_fails_not_fresh_doc_success(self, tmp_path):
|
||||
"""§12B held-out 4: an UNWRITABLE store path must fail visibly, not
|
||||
be mistaken for success. (A nonexistent directory is created by
|
||||
save's mkdir — that is correct repair behavior, not a failure.)"""
|
||||
s = Scaffold(tmp_path / "a" / "scaffold", "t12b")
|
||||
doc = s.init(purpose="real")
|
||||
blocker = tmp_path / "blocker"
|
||||
blocker.write_text("this is a file, not a directory",
|
||||
encoding="utf-8")
|
||||
s.current = blocker / "current.json" # parent is a FILE → unwritable
|
||||
with pytest.raises((OSError, AssertionError)):
|
||||
s.commit(doc, what="unwritable path")
|
||||
|
||||
def test_corrupt_persisted_state_fails_visibly(self, tmp_path):
|
||||
"""§12B held-out 5: unreadable persisted state is DETECTED (load →
|
||||
None) rather than silently treated as fresh-success. And a commit
|
||||
deterministically repairs it (writes valid content)."""
|
||||
s = Scaffold(tmp_path / ".rune" / "scaffold", "t12b")
|
||||
doc = s.init(purpose="t")
|
||||
s.commit(doc, what="initial") # store good
|
||||
from rune.structural_store import load as _ss_load
|
||||
assert _ss_load(s.current) is not None
|
||||
s.current.write_text("{corrupt json", encoding="utf-8")
|
||||
# corruption DETECTED: raw load returns None (not a fresh doc lie)
|
||||
assert _ss_load(s.current) is None
|
||||
# verify_on_disk honestly reports the unreadable state
|
||||
with pytest.raises(AssertionError, match="probably never saved"):
|
||||
s.verify_on_disk(lambda d: s.contract(d, "C0") is not None,
|
||||
True, what="post-corruption check")
|
||||
# commit deterministically repairs: writes valid content
|
||||
s.commit(doc, what="repair after corruption")
|
||||
reloaded = Scaffold(tmp_path / ".rune" / "scaffold", "t12b").load()
|
||||
assert reloaded["entities"]
|
||||
|
||||
def test_accounting_failure_after_commit_leaves_mutation_durable(
|
||||
self, tmp_path):
|
||||
s = Scaffold(tmp_path / ".rune" / "scaffold", "t12b")
|
||||
doc = s.init(purpose="t")
|
||||
s.new_contract(doc, contract_id="D1", need="n", scope=["src/x"])
|
||||
s.commit(doc, what="contract")
|
||||
# accounting AFTER commit raises — durability unaffected
|
||||
with pytest.raises(KeyError):
|
||||
s.contract(doc, "D1")["identities"]["nonexistent"]["deep"]
|
||||
reloaded = Scaffold(tmp_path / ".rune" / "scaffold", "t12b").load()
|
||||
assert s.contract(reloaded, "D1") is not None
|
||||
|
||||
def test_key_ordering_does_not_cause_false_failure(self, tmp_path):
|
||||
"""§12B held-out 7: same semantic content in different key order →
|
||||
identical canonical digest."""
|
||||
doc_a = {"b": 1, "a": {"y": 2, "x": 1}}
|
||||
doc_b = {"a": {"x": 1, "y": 2}, "b": 1}
|
||||
from rune.structural_store import canonical_digest
|
||||
assert canonical_digest(doc_a) == canonical_digest(doc_b)
|
||||
|
||||
def test_existing_callers_compatible(self, tmp_path):
|
||||
s = Scaffold(tmp_path / ".rune" / "scaffold", "t12b")
|
||||
doc = s.init(purpose="t")
|
||||
# old-style call (no selector/expect) still works
|
||||
p = s.commit(doc)
|
||||
assert p.exists()
|
||||
@@ -0,0 +1,118 @@
|
||||
"""test_tui_backlog.py — SCAFFOLD-TRANCHE-007 §32 test plan."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import tui_backlog as tb # noqa: E402
|
||||
|
||||
|
||||
def _doc():
|
||||
return ss.new_doc()
|
||||
|
||||
|
||||
# §32 A-E: roundtrip with all retained fields
|
||||
def test_a_e_backlog_item_roundtrip(tmp_path):
|
||||
doc = _doc()
|
||||
tb.add_backlog_item(
|
||||
doc,
|
||||
"construction-contract-view",
|
||||
feature="scaffolding/contracts",
|
||||
desired_behavior="display active construction contract with phase, "
|
||||
"scope, obligations; navigate between contracts",
|
||||
why="builder sessions need operator visibility into governed builds",
|
||||
projection="BOTH",
|
||||
dependencies=["construction_interface"],
|
||||
priority="high",
|
||||
authority_source="tranche-7-brief §23",
|
||||
evidence_refs=["SCAFFOLD-TRANCHE-006"],
|
||||
)
|
||||
ss.save(doc, tmp_path / "d.json")
|
||||
doc2 = ss.load(tmp_path / "d.json")
|
||||
e = tb.get_backlog_item(doc2, "construction-contract-view")
|
||||
assert e["kind"] == "tui_backlog_item"
|
||||
ident = e["identities"]
|
||||
# B. originating feature retained
|
||||
assert ident["feature"] == "scaffolding/contracts"
|
||||
# C. display/control classification retained
|
||||
assert ident["projection"] == "BOTH"
|
||||
# D. authority source retained
|
||||
assert ident["authority_source"] == "tranche-7-brief §23"
|
||||
# E. status/priority retained
|
||||
assert ident["status"] == "OPEN" and ident["priority"] == "high"
|
||||
|
||||
|
||||
def test_f_backend_feature_records_tui_none_truthfully(tmp_path):
|
||||
doc = _doc()
|
||||
d = tb.record_tui_projection_decision(
|
||||
doc,
|
||||
"module:deployment_race_repair",
|
||||
projection="NONE",
|
||||
rationale="release-path internals have no user-surface; operator visibility flows through CI/deploy results",
|
||||
source_contract="DEPLOY-MONOTONIC-001",
|
||||
)
|
||||
assert d["identities"]["projection"] == "NONE"
|
||||
assert "no user-surface" in d["identities"]["rationale"]
|
||||
|
||||
|
||||
def test_g_future_feature_adds_requirement_without_schema_rewrite(tmp_path):
|
||||
doc = _doc()
|
||||
# an unforeseen projection kind rides in ext (open-world)
|
||||
from rune import structural_store
|
||||
|
||||
e = tb.add_backlog_item(doc, "future-thing", feature="x", desired_behavior="y", why="z", projection="INSPECTION")
|
||||
e.setdefault("ext", {})["novice_field"] = {"someday": True}
|
||||
ss.save(doc, tmp_path / "d.json")
|
||||
doc2 = ss.load(tmp_path / "d.json")
|
||||
item = tb.get_backlog_item(doc2, "future-thing")
|
||||
assert item["ext"]["novice_field"] == {"someday": True}
|
||||
|
||||
|
||||
def test_h_backlog_never_becomes_structural_authority(tmp_path):
|
||||
"""§25: backlog items live in the maintenance ledger namespace; they are
|
||||
not structural entities and never enter impact frontiers."""
|
||||
doc = _doc()
|
||||
tb.add_backlog_item(doc, "x", feature="f", desired_behavior="d", why="w")
|
||||
# kind check
|
||||
kinds = {e["kind"] for e in doc["entities"]}
|
||||
assert "tui_backlog_item" in kinds
|
||||
# impact frontier ignores it
|
||||
from rune import structural_store as ss
|
||||
|
||||
fr = ss.impact_frontier(doc, ["tui-backlog:x"])
|
||||
# the seed itself appears at level0 (any frontier includes its seed);
|
||||
# the §25 invariant is NO PROPAGATION: no obligations, no contracts,
|
||||
# no verification surfaces derive from a backlog item
|
||||
assert fr.get("level1_obligations") == []
|
||||
assert fr.get("contracts") == []
|
||||
assert fr.get("level2_verification") == []
|
||||
|
||||
|
||||
def test_status_update(tmp_path):
|
||||
doc = _doc()
|
||||
tb.add_backlog_item(doc, "x", feature="f", desired_behavior="d", why="w")
|
||||
tb.update_backlog_status(doc, "x", "PLANNED", evidence_ref="planning-t7")
|
||||
assert tb.get_backlog_item(doc, "x")["identities"]["status"] == "PLANNED"
|
||||
with pytest.raises(ValueError):
|
||||
tb.update_backlog_status(doc, "x", "SOMEDAY")
|
||||
with pytest.raises(ValueError):
|
||||
tb.update_backlog_status(doc, "ghost", "OPEN")
|
||||
|
||||
|
||||
def test_list_filters(tmp_path):
|
||||
doc = _doc()
|
||||
tb.add_backlog_item(doc, "a", feature="f1", desired_behavior="d", why="w", projection="INSPECTION", status="OPEN")
|
||||
tb.add_backlog_item(doc, "b", feature="f2", desired_behavior="d", why="w", projection="CONTROL", status="DONE")
|
||||
assert len(tb.list_backlog(doc)) == 2
|
||||
assert len(tb.list_backlog(doc, status="OPEN")) == 1
|
||||
assert len(tb.list_backlog(doc, projection="CONTROL")) == 1
|
||||
|
||||
|
||||
def test_projection_validation():
|
||||
doc = _doc()
|
||||
with pytest.raises(ValueError):
|
||||
tb.add_backlog_item(doc, "x", feature="f", desired_behavior="d", why="w", projection="SCREENSHOT")
|
||||
Reference in new issue
Block a user