Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e6a397e7c | ||
|
|
87fad6ff8b | ||
|
|
87252705a8 | ||
|
|
4423367ddb | ||
|
|
f85c654d9d | ||
|
|
7417a8ca02 | ||
|
|
ecfa80a7b3 | ||
|
|
2a106e39a3 | ||
|
|
54cee39b95 | ||
|
|
c27d13d9ac | ||
|
|
dd5030407e | ||
|
|
ba0ec0ae31 | ||
|
|
4d410319ce | ||
|
|
09ec2b81ba | ||
|
|
577695a1a3 | ||
|
|
2fad565016 | ||
|
|
6b805eea4a | ||
|
|
4ecd99023f |
No files matched your search
@@ -89,11 +89,11 @@ jobs:
|
||||
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.
|
||||
# The PRIMARY convergence mechanism is deploy_stage.py's out-of-lock
|
||||
# publisher (publishes from CURRENT deployment authority, §14B/§14C).
|
||||
# This step is a non-blocking FALLBACK: if the primary publish 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:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.56**
|
||||
**Current release: v0.66.58**
|
||||
|
||||
**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
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.56"
|
||||
version = "0.66.58"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"errorCount": 124,
|
||||
"warningCount": 430,
|
||||
"errorCount": 117,
|
||||
"warningCount": 443,
|
||||
"informationCount": 0
|
||||
}
|
||||
+103
-29
@@ -28,6 +28,92 @@ DEPLOY_ROOT = Path("/mnt/workdrive/rune-code")
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
VENV_PY = "/home/nick/.venvs/memory/bin/python3"
|
||||
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
sys.path.insert(0, str(REPO_ROOT / "src"))
|
||||
import deploy_promotion # noqa: E402
|
||||
from rune.release_notes import ( # noqa: E402
|
||||
ReleaseNote as _ReleaseNote,
|
||||
auto_initial_note as _auto_note,
|
||||
converged_deployed_sha as _authority_sha,
|
||||
convergence_refresh as _refresh,
|
||||
fetch_release as _fetch_release,
|
||||
publish_release as _publish,
|
||||
sha_from_body as _sha_from_body,
|
||||
summary_from_body as _summary_from_body,
|
||||
)
|
||||
|
||||
|
||||
def publish_out_of_lock(short: str, sha: str, *,
|
||||
deploy_root: Path = DEPLOY_ROOT,
|
||||
scaffold_store: Path | None = None) -> None:
|
||||
"""§13D/§14B: release-history publication AFTER the promotion lock is
|
||||
released. Network I/O never holds the flock.
|
||||
|
||||
§14B INVARIANT: publication does NOT trust the originating job's SHA.
|
||||
The CURRENT deployment authority is re-read immediately before
|
||||
publishing; the note is constructed from CURRENT authority. A stale
|
||||
job whose candidate was superseded converges the NEW authority (or
|
||||
refuses a version it cannot truthfully describe) — it may help
|
||||
converge B, but may never reassert itself as deployed.
|
||||
Failure degrades to STALE (software valid, history lags; retry
|
||||
converges)."""
|
||||
try:
|
||||
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):
|
||||
return
|
||||
# §14B: re-read CURRENT authority — never trust the job's SHA.
|
||||
authority_sha = _authority_sha(str(deploy_root))
|
||||
if not authority_sha:
|
||||
print("WARNING: release-history convergence skipped: "
|
||||
"deployment authority unreadable — STALE, retry converges")
|
||||
return
|
||||
store = str(scaffold_store or (REPO_ROOT / "store" / "scaffold" / "current.json"))
|
||||
identity = deploy_root / authority_sha[:12] / "src" / "rune" / "_build_identity.json"
|
||||
try:
|
||||
version = json.loads(identity.read_text()).get("version", "")
|
||||
except (OSError, ValueError):
|
||||
version = ""
|
||||
if not version:
|
||||
print("WARNING: release-history convergence skipped: "
|
||||
"no version on current authority — STALE, retry converges")
|
||||
return
|
||||
tag = f"v{version}"
|
||||
existing = _fetch_release(server, repo, token, tag)
|
||||
if existing is None:
|
||||
note = _auto_note(
|
||||
version=version,
|
||||
summary=(f"Rune v{version} — evidence-derived release "
|
||||
f"history (automatic convergence)"),
|
||||
scaffold_store=store,
|
||||
release_sha=authority_sha, deployed_sha=authority_sha,
|
||||
provenance=["automatic convergence publish "
|
||||
"(deploy workflow, authority-derived)"])
|
||||
else:
|
||||
note = _refresh(
|
||||
_ReleaseNote.from_dict({
|
||||
"version": version,
|
||||
"summary": _summary_from_body(existing.get("body", "")),
|
||||
"release_sha": _sha_from_body(existing.get("body", ""), "tag")
|
||||
or authority_sha,
|
||||
"provenance": [],
|
||||
}),
|
||||
deployed_sha=authority_sha, scaffold_store=store,
|
||||
extra_provenance="automatic convergence publish "
|
||||
"(deploy workflow, authority-derived)")
|
||||
# preserve the tag SHA the stored body carries
|
||||
tag_sha = _sha_from_body(existing.get("body", ""), "tag")
|
||||
if tag_sha:
|
||||
note.release_sha = tag_sha
|
||||
result = _publish(server, repo, token, note)
|
||||
print(f"release-history convergence: "
|
||||
f"{result.get('action', 'unknown')} "
|
||||
f"(authority {authority_sha[:12]})")
|
||||
except Exception as conv_exc: # noqa: BLE001 — history STALE, software valid
|
||||
print(f"WARNING: release-history convergence failed: "
|
||||
f"{conv_exc!s:.200} — STALE, retry converges")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# The runner's checkout workspace (act sets GITHUB_WORKSPACE). Never a
|
||||
@@ -127,37 +213,25 @@ print("VERIFIED:", json.dumps(info))
|
||||
# ancestry check + lock live in deploy_promotion.py; this script only
|
||||
# handles the truthful outcome reporting. A stale candidate must NOT
|
||||
# claim promotion success — build/stage success != promotion success.
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
import deploy_promotion # noqa: E402
|
||||
from rune.release_notes import ( # noqa: E402
|
||||
auto_initial_note as _auto_note,
|
||||
publish_release as _publish,
|
||||
)
|
||||
#
|
||||
# §14C: NO separate convergence-intent ledger. The durable pending-
|
||||
# convergence state IS the deployment authority itself (atomically
|
||||
# swapped `current` symlink + stamped build identity): publication
|
||||
# derives from it, idempotently, outside the lock. The old intent
|
||||
# file had exactly one writer and zero readers, non-atomic writes,
|
||||
# and a bogus recorded_at (SECONDS is a bash builtin, never exported
|
||||
# — always "0"). Deriving from authority gives atomicity, restart
|
||||
# readability, corruption immunity, monotonicity (stale publishers
|
||||
# read CURRENT authority, not their own job) and idempotent retry
|
||||
# for free. REMOVE more complexity than you add.
|
||||
|
||||
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)
|
||||
|
||||
outcome, detail = deploy_promotion.promote(sha, DEPLOY_ROOT / short, tmp_stage, repo, on_promoted=_converge_release)
|
||||
# §13D: publish release history OUTSIDE the promotion lock (network
|
||||
# I/O must not hold the flock). Only for PROMOTED outcomes.
|
||||
if outcome == deploy_promotion.OUTCOME_PROMOTED:
|
||||
publish_out_of_lock(short, sha)
|
||||
|
||||
if outcome == deploy_promotion.OUTCOME_PROMOTED:
|
||||
print(f"PROMOTED: {short}")
|
||||
|
||||
@@ -143,28 +143,12 @@ def main(argv=None) -> int:
|
||||
|
||||
|
||||
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"
|
||||
"""Delegate to the single authority in rune.release_notes (§14B)."""
|
||||
return rn.summary_from_body(body)
|
||||
|
||||
|
||||
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 ""
|
||||
return rn.sha_from_body(body, which)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
#!/usr/bin/env python3
|
||||
"""t14_builder_session.py — T14D/14E REAL builder session driver.
|
||||
|
||||
The builder is the ACTUAL mimo CLI process (mimo run, model
|
||||
Xioami/mimo-v2.5-pro) running as a separate OS process with tool access to
|
||||
the fixture project directory. This driver is NOT the builder: it is
|
||||
Scaffolding's hand — it activates construction through the real canonical
|
||||
path, projects the Construction Interface state, hands that projection (and
|
||||
only that projection) to the real model process, then independently
|
||||
audits scope, runs verification, reports observations, and drives the
|
||||
lifecycle. Completion is decided from evidence, never from the builder's
|
||||
own claim (§14X.8).
|
||||
|
||||
Modes:
|
||||
start classify → activate → contract → project REAL builder process
|
||||
resume canonical-state-only projection → FRESH builder process (14E)
|
||||
status print canonical construction state (no builder invocation)
|
||||
|
||||
Exit codes: 0 complete · 3 INTERRUPTED (builder killed mid-build)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import construction_interface as ci # noqa: E402
|
||||
from rune import construction_mode as cm # noqa: E402
|
||||
from rune import construction_activation as ca # noqa: E402
|
||||
from rune import construction_decision as cd # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
USER_REQUEST = (
|
||||
"Build me a small standalone practice scheduler that can create, "
|
||||
"list and complete practice items."
|
||||
)
|
||||
EXPECTED_SCOPE = ["scheduler.js", "scheduler.test.js", "package.json"]
|
||||
BUILDER_MODEL = os.environ.get("T14_BUILDER_MODEL",
|
||||
"Verboo Code/glm-5.3-flash") # PL1 provider policy default
|
||||
BUILDER_CMD = "mimo"
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(f"[t14] {msg}", flush=True)
|
||||
|
||||
|
||||
def classify_and_activate(store: Path, objective: str) -> dict:
|
||||
"""The REAL canonical path: classify → activate. No shortcuts."""
|
||||
parent = sc.Scaffold(str(store), "rune")
|
||||
parent.init(purpose="t14 fixture parent authority")
|
||||
decision = cm.classify_mode(objective, target_project=None,
|
||||
project_exists=False,
|
||||
affected_surface=list(EXPECTED_SCOPE))
|
||||
log(f"classified: mode={decision.mode} "
|
||||
f"target={decision.target_project!r}")
|
||||
need = cd.Need(
|
||||
need_id=f"need-{ss.now()}-t14", objective=objective,
|
||||
origin="rune_native", project=decision.target_project or "",
|
||||
constraints={}, affected_surface=list(EXPECTED_SCOPE),
|
||||
evidence_refs=[], approval_context="t14 user-approved fixture")
|
||||
rec = ca.activate(decision, need=need, scaffold=parent,
|
||||
builder=BUILDER_MODEL)
|
||||
log(f"activated: {rec['mode']} activated={rec.get('activated')} "
|
||||
f"project={rec.get('project')}")
|
||||
return {"decision": {"mode": decision.mode,
|
||||
"target_project": decision.target_project,
|
||||
"evidence": list(decision.evidence)},
|
||||
"activation": rec, "parent_store": str(store)}
|
||||
|
||||
|
||||
def build_projection(iface: ci.ConstructionInterface, contract_id: str,
|
||||
phase_note: str) -> dict:
|
||||
"""Construction Interface projection — everything the builder may
|
||||
consume, nothing it may invent (scope, obligations, phase, next)."""
|
||||
scope = iface.get_scope(contract_id)
|
||||
obligations = iface.get_obligations(contract_id)
|
||||
context = iface.construction_context(contract_id)
|
||||
assert scope.get("success") and obligations.get("success"), \
|
||||
f"projection failure: {scope.get('errors')} {obligations.get('errors')}"
|
||||
return {
|
||||
"phase": context["state"].get("phase"),
|
||||
"project": context["state"].get("project"),
|
||||
"need": context["state"].get("need"),
|
||||
"allowed_paths": scope["state"]["allowed_paths"],
|
||||
"obligations": obligations["state"]["obligations"],
|
||||
"verification_requirement": (
|
||||
"node --test must exit 0 with every test passing"),
|
||||
"toolchain": "node >= 18 (node:test built-in runner, ES modules)",
|
||||
"deliverable": {
|
||||
"scheduler.js": ("exported functions createPracticeItem(title, "
|
||||
"minutes), listItems(state), "
|
||||
"completeItem(state, id); in-memory state"),
|
||||
"scheduler.test.js": ("node:test cases proving create, list "
|
||||
"and complete work"),
|
||||
"package.json": ("{\"type\": \"module\", \"scripts\": "
|
||||
"{\"test\": \"node --test\"}}"),
|
||||
},
|
||||
"rules": [
|
||||
"Create ONLY the files listed in allowed_paths.",
|
||||
"Do not modify or create anything else.",
|
||||
"Verify with: node --test (must pass).",
|
||||
"Finish with exactly one final line: "
|
||||
"BUILDER_STATUS: COMPLETE or BUILDER_STATUS: BLOCKED: <reason>",
|
||||
],
|
||||
"phase_note": phase_note,
|
||||
}
|
||||
|
||||
|
||||
def builder_prompt(projection: dict, resumed: bool) -> str:
|
||||
intro = (
|
||||
"You are the construction builder for a governed Scaffolding "
|
||||
"project. Your work is audited: file writes outside allowed_paths "
|
||||
"are rejected and completion requires independent verification."
|
||||
)
|
||||
if resumed:
|
||||
intro += (
|
||||
" A PREVIOUS builder process was interrupted mid-build. Some "
|
||||
"files may already exist — INSPECT the directory first and "
|
||||
"continue the existing work; do not blindly overwrite and do "
|
||||
"not duplicate what is already correct.")
|
||||
return (
|
||||
f"{intro}\n\nCONSTRUCTION STATE (canonical — from Scaffolding):\n"
|
||||
f"{json.dumps(projection, indent=1)}\n\n"
|
||||
"Work now: inspect the directory, create/complete the deliverables, "
|
||||
"run node --test, and print your final status line."
|
||||
)
|
||||
|
||||
|
||||
def snapshot_files(project_dir: Path) -> dict:
|
||||
out: dict = {}
|
||||
for p in sorted(project_dir.rglob("*")):
|
||||
if p.is_file():
|
||||
out[str(p.relative_to(project_dir))] = p.stat().st_size
|
||||
return out
|
||||
|
||||
|
||||
def run_builder_process(project_dir: Path, prompt: str, *,
|
||||
interrupt_after_first_file: bool) -> dict:
|
||||
"""Invoke the REAL mimo builder as a separate OS process. Returns
|
||||
{terminated_by, exit_code, session_output}."""
|
||||
env = dict(os.environ)
|
||||
proc = subprocess.Popen(
|
||||
[BUILDER_CMD, "run", "-m", BUILDER_MODEL,
|
||||
"--format", "json", prompt],
|
||||
cwd=str(project_dir), env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, start_new_session=True)
|
||||
terminated_by = "completed"
|
||||
if interrupt_after_first_file:
|
||||
log("interrupt armed: watching for first source file...")
|
||||
while True:
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
files = [p for p in project_dir.iterdir()
|
||||
if p.suffix in (".mjs", ".js", ".json")]
|
||||
if files:
|
||||
log(f"INTERRUPTING builder: first file landed "
|
||||
f"({files[0].name}) — SIGKILL to the real process")
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
||||
terminated_by = "sigkill_mid_build"
|
||||
break
|
||||
time.sleep(0.5)
|
||||
try:
|
||||
out, err = proc.communicate(timeout=900)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
||||
out, err = proc.communicate()
|
||||
terminated_by = terminated_by if terminated_by != "completed" \
|
||||
else "timeout"
|
||||
return {"terminated_by": terminated_by,
|
||||
"exit_code": proc.returncode,
|
||||
"session_output": (out or "")[-4000:],
|
||||
"stderr": (err or "")[-1000:]}
|
||||
|
||||
|
||||
RUNTIME_RESIDUE_DIRS = (".mimocode", ".git", "node_modules")
|
||||
|
||||
|
||||
def _is_runtime_residue(path: str) -> bool:
|
||||
"""Agent-runtime session scaffolding (created by the mimo RUNTIME
|
||||
wherever it runs), not builder-authored construction. Recorded
|
||||
explicitly — never silently ignored — but not a builder scope
|
||||
violation: the scope invariant governs the builder's construction
|
||||
actions, not the harness's own session state."""
|
||||
top = path.split("/", 1)[0]
|
||||
return top in RUNTIME_RESIDUE_DIRS
|
||||
|
||||
|
||||
def audit_scope(project_dir: Path, before: dict,
|
||||
allowed: list[str]) -> dict:
|
||||
after = snapshot_files(project_dir)
|
||||
changed = sorted(set(after) - set(before))
|
||||
modified = sorted(k for k in after
|
||||
if k in before and after[k] != before[k])
|
||||
residue = sorted(f for f in changed + modified
|
||||
if _is_runtime_residue(f))
|
||||
violations = sorted(f for f in changed + modified
|
||||
if f not in allowed and not _is_runtime_residue(f))
|
||||
return {"created": [f for f in changed if not _is_runtime_residue(f)],
|
||||
"modified": [f for f in modified if not _is_runtime_residue(f)],
|
||||
"runtime_residue": residue,
|
||||
"scope_violations": violations,
|
||||
"in_scope": not violations}
|
||||
|
||||
|
||||
def independent_verification(project_dir: Path) -> dict:
|
||||
"""§14X.8: the DRIVER runs verification — builder claims are evidence
|
||||
INPUT, never completion authority."""
|
||||
probe = subprocess.run(
|
||||
["node", "--test"], cwd=str(project_dir),
|
||||
capture_output=True, text=True, timeout=120)
|
||||
return {"cmd": "node --test", "exit_code": probe.returncode,
|
||||
"stdout": (probe.stdout or "")[-2000:],
|
||||
"stderr": (probe.stderr or "")[-500:]}
|
||||
|
||||
|
||||
def complete_lifecycle(iface: ci.ConstructionInterface, contract_id: str,
|
||||
scope_audit: dict, verification: dict,
|
||||
builder_status: str) -> dict:
|
||||
items = [{"entity": "builder-session",
|
||||
"source": "independent_driver",
|
||||
"detail": (f"builder={BUILDER_MODEL} claimed="
|
||||
f"{builder_status} scope_violations="
|
||||
f"{scope_audit['scope_violations']} "
|
||||
f"verify_exit={verification['exit_code']}")}]
|
||||
items += [{"entity": f, "source": "independent_driver",
|
||||
"detail": "created in scope"}
|
||||
for f in scope_audit["created"]]
|
||||
items += [{"entity": f, "source": "independent_driver",
|
||||
"detail": "modified in scope"}
|
||||
for f in scope_audit["modified"]]
|
||||
obs = iface.report_observation(contract_id, items)
|
||||
assert obs.get("success"), obs.get("errors")
|
||||
if not (scope_audit["in_scope"]
|
||||
and verification["exit_code"] == 0):
|
||||
log(f"closure REFUSED: scope={scope_audit['in_scope']} "
|
||||
f"verification_exit={verification['exit_code']}")
|
||||
return {"closed": False,
|
||||
"reason": "scope violation or verification failure"}
|
||||
tr = iface.transition(contract_id, "verifying",
|
||||
evidence_ref="independent node --test run")
|
||||
assert tr.get("success"), tr.get("errors")
|
||||
v = iface.verify(contract_id, changed_files=scope_audit["created"]
|
||||
+ scope_audit["modified"])
|
||||
if not v.get("success"):
|
||||
log(f"verify refused: {v.get('errors')}")
|
||||
return {"closed": False, "reason": "verify refused"}
|
||||
c = iface.close(contract_id, observed_evidence=[
|
||||
{"kind": "verification", "cmd": "node --test",
|
||||
"exit_code": 0, "source": "independent_driver_run"}],
|
||||
scope_files=scope_audit["created"] + scope_audit["modified"])
|
||||
return {"closed": bool(c.get("success")),
|
||||
"close_envelope": {k: c.get(k) for k in
|
||||
("success", "errors", "ids", "state")}}
|
||||
|
||||
|
||||
def discover_contract_id(store: Path) -> tuple[str, str] | None:
|
||||
"""Purely canonical discovery: walk the store's project scaffolds and
|
||||
return (project, contract_id) for the most recent contract. No session
|
||||
file, no transcript — exactly what a fresh builder may know."""
|
||||
projects_dir = store / "projects"
|
||||
if not projects_dir.exists():
|
||||
return None
|
||||
best: tuple[str, str, str] | None = None
|
||||
for proj_current in sorted(projects_dir.glob("*/current.json")):
|
||||
project = proj_current.parent.name
|
||||
try:
|
||||
doc = json.loads(proj_current.read_text())
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] == "construction_contract":
|
||||
key = (e.get("observed_at") or "", project,
|
||||
e["id"].split(":", 1)[1])
|
||||
if best is None or key > (best[0], best[1], best[2]):
|
||||
best = key
|
||||
return (best[1], best[2]) if best else None
|
||||
|
||||
|
||||
def seed_requirement_spec(store: Path, project: str) -> str:
|
||||
"""Govern the requirement as a contract on the project's surface
|
||||
(identity before repository §7): the user request IS the requirement
|
||||
evidence. The Need then references this contract explicitly — the
|
||||
capability-fit engine resolves REUSE of the SPECIFICATION (the build
|
||||
is governed by satisfying it). Maturity DECLARED: a requirement, not
|
||||
existing code — nothing manufactured."""
|
||||
proj = sc.Scaffold(str(store / "projects" / project), project)
|
||||
doc = proj.load()
|
||||
if ss.entity(doc, "contract:practice-items-spec") is None:
|
||||
ss.add_entity(
|
||||
doc, id="contract:practice-items-spec", kind="contract",
|
||||
identities={"objective": USER_REQUEST},
|
||||
evidence_refs=["t14-user-request"])
|
||||
ss.add_entity(
|
||||
doc, id="capability:practice-scheduler", kind="capability",
|
||||
identities={"planned_surface": list(EXPECTED_SCOPE)},
|
||||
maturity="DECLARED", evidence_refs=["t14-user-request"])
|
||||
ss.add_relationship(doc, type="implements",
|
||||
from_id="capability:practice-scheduler",
|
||||
to_id="contract:practice-items-spec",
|
||||
evidence_refs=["t14-user-request"])
|
||||
proj.commit(doc, what="seed requirement spec: practice items "
|
||||
"create/list/complete (user request)")
|
||||
return "contract:practice-items-spec"
|
||||
|
||||
|
||||
def cmd_start(store: Path, project_dir: Path, interrupt: bool) -> int:
|
||||
before = snapshot_files(project_dir)
|
||||
staged = classify_and_activate(store, USER_REQUEST)
|
||||
project = staged["activation"]["project"]
|
||||
assert staged["activation"]["mode"] == "PROJECT_BUILD", \
|
||||
f"expected PROJECT_BUILD, got {staged['activation']['mode']}"
|
||||
spec_contract = seed_requirement_spec(store, project)
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL,
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
sub = iface.submit_need(
|
||||
USER_REQUEST, origin="rune_native",
|
||||
constraints={"environment": {"os": "linux", "runtime": "node>=18",
|
||||
"runner": "node --test"},
|
||||
"contracts": [spec_contract]},
|
||||
affected_surface=list(EXPECTED_SCOPE),
|
||||
evidence_refs=["t14-user-request", "node-v22-installed"],
|
||||
approval_context="t14 user-approved fixture")
|
||||
assert sub.get("success"), sub.get("errors")
|
||||
contract_id = sub["ids"]["contract_id"]
|
||||
log(f"contract: {contract_id}")
|
||||
for phase in ("approved", "building"):
|
||||
tr = iface.transition(contract_id, phase,
|
||||
evidence_ref=f"t14 {phase}")
|
||||
assert tr.get("success"), tr.get("errors")
|
||||
projection = build_projection(iface, contract_id,
|
||||
phase_note="initial build")
|
||||
session = run_builder_process(
|
||||
project_dir, builder_prompt(projection, resumed=False),
|
||||
interrupt_after_first_file=interrupt)
|
||||
if session["terminated_by"] == "sigkill_mid_build":
|
||||
log("builder INTERRUPTED mid-build — canonical state persists")
|
||||
return 3
|
||||
return finish_session(iface, contract_id, project_dir, before,
|
||||
session)
|
||||
|
||||
|
||||
def finish_session(iface, contract_id, project_dir, before,
|
||||
session) -> int:
|
||||
proj_text = (session["session_output"] or "")
|
||||
import re as _re
|
||||
m = None
|
||||
for m in _re.finditer(r"BUILDER_STATUS:\s*([A-Z]+)", proj_text):
|
||||
pass
|
||||
builder_status = (m.group(1) + " (final)") if m else "NOT_STATED"
|
||||
scope_audit = audit_scope(project_dir, before,
|
||||
list(EXPECTED_SCOPE))
|
||||
verification = independent_verification(project_dir)
|
||||
result = complete_lifecycle(iface, contract_id, scope_audit,
|
||||
verification, builder_status)
|
||||
summary = {
|
||||
"builder_model": BUILDER_MODEL,
|
||||
"builder_process": "mimo run (separate OS process)",
|
||||
"terminated_by": session["terminated_by"],
|
||||
"builder_claimed": builder_status,
|
||||
"scope_audit": scope_audit,
|
||||
"verification": {"cmd": verification["cmd"],
|
||||
"exit_code": verification["exit_code"]},
|
||||
"lifecycle": result,
|
||||
}
|
||||
print(json.dumps(summary, indent=1))
|
||||
return 0 if result.get("closed") else 1
|
||||
|
||||
|
||||
def cmd_resume(store: Path, project_dir: Path) -> int:
|
||||
"""14E: FRESH process. Canonical state ONLY — no transcript, no
|
||||
memory of the previous session."""
|
||||
found = discover_contract_id(store)
|
||||
assert found, "no contract found in canonical state"
|
||||
project, contract_id = found
|
||||
log(f"fresh builder resume: project={project} "
|
||||
f"contract={contract_id} (canonical only)")
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL + "-resumed",
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
context = iface.construction_context(contract_id)
|
||||
assert context["state"]["active"], "canonical state shows no active build"
|
||||
log(f"recovered: phase={context['state']['phase']} "
|
||||
f"scope={context['state']['scope']}")
|
||||
before = snapshot_files(project_dir)
|
||||
projection = build_projection(
|
||||
iface, contract_id,
|
||||
phase_note="resumed after real builder interruption")
|
||||
session = run_builder_process(
|
||||
project_dir, builder_prompt(projection, resumed=True),
|
||||
interrupt_after_first_file=False)
|
||||
return finish_session(iface, contract_id, project_dir, before,
|
||||
session)
|
||||
|
||||
|
||||
def cmd_status(store: Path) -> int:
|
||||
found = discover_contract_id(store)
|
||||
if not found:
|
||||
print(json.dumps({"active": False}))
|
||||
return 0
|
||||
project, contract_id = found
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="inspector",
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
print(json.dumps(iface.construction_context(contract_id)["state"],
|
||||
indent=1, default=str))
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("mode", choices=["start", "resume", "status"])
|
||||
ap.add_argument("--store", required=True)
|
||||
ap.add_argument("--project-dir", required=False)
|
||||
ap.add_argument("--interrupt-after-first-file", action="store_true")
|
||||
args = ap.parse_args()
|
||||
store, project_dir = Path(args.store).resolve(), \
|
||||
Path(args.project_dir).resolve() if args.project_dir else None
|
||||
project_dir.mkdir(parents=True, exist_ok=True) if project_dir else None
|
||||
if args.mode == "start":
|
||||
return cmd_start(store, project_dir,
|
||||
args.interrupt_after_first_file)
|
||||
if args.mode == "resume":
|
||||
return cmd_resume(store, project_dir)
|
||||
return cmd_status(store)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -6,6 +6,7 @@ New code must be type-clean; existing code improves as debt is paid down.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -23,12 +24,21 @@ def main() -> int:
|
||||
# not work on all platforms). Fall back to sys.executable -m.
|
||||
pyright_bin = Path(sys.executable).parent / "pyright"
|
||||
cmd = [str(pyright_bin)] if pyright_bin.is_file() else [sys.executable, "-m", "pyright"]
|
||||
# §14H LOCAL-VS-CI NORMALIZATION (proven 2026-09-15): pyright resolves
|
||||
# the python environment for import analysis, and a developer's
|
||||
# user-site packages (~/.local/lib/pythonX/site-packages) leak into it
|
||||
# — nick: 124 errors, runner user: 117 on the IDENTICAL checkout+venv.
|
||||
# PYTHONNOUSERSITE=1 pins both contexts to the same effective
|
||||
# environment (verified: identical 117-error arithmetic). Local
|
||||
# preflight now predicts CI instead of diverging from it.
|
||||
env = dict(os.environ, PYTHONNOUSERSITE="1")
|
||||
result = subprocess.run(
|
||||
cmd + [str(ROOT / "src" / "rune"), "--outputjson"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
cwd=str(ROOT),
|
||||
env=env,
|
||||
)
|
||||
data = json.loads(result.stdout)
|
||||
summary = data.get("summary", {})
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.56"
|
||||
__version__ = "0.66.58"
|
||||
|
||||
# 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.56"' in rune_version,
|
||||
'__version__ = "0.66.58"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -147,11 +147,11 @@ def classify_surface(surface: str) -> str:
|
||||
if surface.endswith(".yml") or surface.endswith(".yaml") or surface.endswith(".json") or surface.endswith(".toml"):
|
||||
return "workflow_config"
|
||||
if (
|
||||
surface.endswith(".py")
|
||||
surface.endswith((".py", ".js", ".ts", ".jsx", ".tsx", ".rs",
|
||||
".md", ".go", ".java", ".c", ".cpp", ".h",
|
||||
".css", ".html", ".yaml", ".yml",
|
||||
".mjs", ".cjs", ".mts", ".cts"))
|
||||
or "/" in surface
|
||||
or surface.endswith(".md")
|
||||
or surface.endswith(".rs")
|
||||
or surface.endswith(".ts")
|
||||
):
|
||||
return "source_path"
|
||||
return "unresolved"
|
||||
|
||||
@@ -27,7 +27,7 @@ from dataclasses import dataclass, field
|
||||
|
||||
__all__ = ["Depth", "select_depth", "LEVELS"]
|
||||
|
||||
LEVELS = ("LIGHT", "STANDARD", "FULL")
|
||||
LEVELS = ("NONE", "UNRESOLVED", "LIGHT", "STANDARD", "FULL")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -59,14 +59,19 @@ def select_depth(mode: str, *, objective: str = "",
|
||||
bypassed: list[str] = []
|
||||
|
||||
if mode == "NOT_CONSTRUCTION":
|
||||
return Depth(level="LIGHT", reason="not construction — no governance",
|
||||
evidence=ev, ceremony=[], bypassed=["all"])
|
||||
# §13C: NOT_CONSTRUCTION has NO construction depth — not even LIGHT
|
||||
return Depth(level="NONE", reason="not construction — no construction "
|
||||
"depth applies", evidence=ev, ceremony=[],
|
||||
bypassed=["all"])
|
||||
|
||||
if mode == "UNKNOWN":
|
||||
return Depth(level="LIGHT",
|
||||
reason="UNKNOWN — probe/clarify before any ceremony",
|
||||
# §13C: UNKNOWN means construction hasn't started — no lifecycle yet
|
||||
return Depth(level="UNRESOLVED",
|
||||
reason="UNKNOWN — probe/clarify before any construction "
|
||||
"lifecycle begins",
|
||||
evidence=ev, ceremony=["probe/clarify"],
|
||||
bypassed=["contract", "patterns", "bootstrap"])
|
||||
bypassed=["contract", "patterns", "bootstrap",
|
||||
"depth selection"])
|
||||
|
||||
if mode == "LOCAL_CHANGE":
|
||||
ev.append("LOCAL_CHANGE: bounded scope, minimal ceremony")
|
||||
|
||||
@@ -323,7 +323,7 @@ class ConstructionInterface:
|
||||
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)
|
||||
self.scaffold.commit(doc, what="submit_need")
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"submit_need",
|
||||
@@ -457,7 +457,7 @@ class ConstructionInterface:
|
||||
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)
|
||||
self.scaffold.commit(doc, what="observations")
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
@@ -556,7 +556,7 @@ class ConstructionInterface:
|
||||
)
|
||||
self._record_session(doc, "transition", f"{contract_id} → {phase}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
self.scaffold.commit(doc, what="transition")
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
@@ -586,7 +586,7 @@ class ConstructionInterface:
|
||||
)
|
||||
self._record_session(doc, "close", f"{contract_id} closed")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
self.scaffold.commit(doc, what="close")
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
@@ -629,17 +629,24 @@ class ConstructionInterface:
|
||||
)
|
||||
ext = c.get("ext", {})
|
||||
t = ext.get("decision_trace", {})
|
||||
need_fallback = {
|
||||
"objective": c["identities"].get("need", ""),
|
||||
"origin": c["identities"].get("origin", ""),
|
||||
"need_id": contract_id,
|
||||
}
|
||||
resolution_fallback = c["identities"].get("resolution", "")
|
||||
toolchain_fallback = c["identities"].get("toolchain", {}).get("selected_language", "")
|
||||
return _envelope(
|
||||
"resume",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", "")},
|
||||
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", contract_id)},
|
||||
state={
|
||||
"mode": self.mode,
|
||||
"need": t.get("need", {}),
|
||||
"need": t.get("need", need_fallback),
|
||||
"decision": {
|
||||
"resolution": t.get("resolution"),
|
||||
"completeness": t.get("completeness", {}).get("verdict"),
|
||||
"toolchain": t.get("toolchain", {}).get("selected_language", ""),
|
||||
"resolution": t.get("resolution") or resolution_fallback,
|
||||
"completeness": t.get("completeness", {}).get("verdict", ""),
|
||||
"toolchain": t.get("toolchain", {}).get("selected_language", "") or toolchain_fallback,
|
||||
},
|
||||
"phase": c.get("phase"),
|
||||
"status": c.get("status"),
|
||||
@@ -686,7 +693,7 @@ class ConstructionInterface:
|
||||
{"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)
|
||||
self.scaffold.commit(doc, what=f"amend_scope {contract_id}")
|
||||
return _envelope(
|
||||
"amend_scope", success=True, ids={"contract_id": contract_id}, state={"scope": c["identities"]["scope"]}
|
||||
)
|
||||
@@ -706,6 +713,22 @@ def _obligations_projection(c: dict) -> list[dict]:
|
||||
t = c.get("ext", {}).get("decision_trace", {})
|
||||
obl = t.get("obligations", {})
|
||||
out = []
|
||||
# contract-level obligations (from identities, not just trace)
|
||||
ident = c.get("identities", {})
|
||||
for field_name, prov in (
|
||||
("verification_obligations", "CONTRACT"),
|
||||
("regression_obligations", "CONTRACT"),
|
||||
):
|
||||
for item in ident.get(field_name, []):
|
||||
out.append(
|
||||
{
|
||||
"id": f"{prov.lower()}:{abs(hash(str(item))) % 10**8}",
|
||||
"description": str(item)[:200],
|
||||
"provenance": prov,
|
||||
"kind": field_name,
|
||||
"status": "REQUIRED",
|
||||
}
|
||||
)
|
||||
for kind in ("regression", "verification", "composition", "acquisition"):
|
||||
for item in obl.get(kind, []):
|
||||
out.append(
|
||||
|
||||
@@ -238,6 +238,37 @@ def build_note(
|
||||
)
|
||||
|
||||
|
||||
# ── stored-body field extraction (§14B single authority) ─────────────────────
|
||||
# One set of parsers for release-body fields, shared by release_history_cli.py
|
||||
# and deploy_stage's authority-derived publisher — never duplicated.
|
||||
|
||||
|
||||
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:
|
||||
"""Extract a recorded SHA from a stored release body: 'tag' → the tag
|
||||
commit; anything else → the final deployed identity."""
|
||||
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 ""
|
||||
|
||||
|
||||
# ── Gitea publication (§15/§17/§25) ──────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -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.56
|
||||
Version: v0.66.58
|
||||
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.56 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.66.58 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
TAP version 13
|
||||
# Subtest: createPracticeItem
|
||||
# Subtest: creates an item with title, minutes, and an id
|
||||
ok 1 - creates an item with title, minutes, and an id
|
||||
---
|
||||
duration_ms: 0.515984
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: assigns distinct ids to successive items
|
||||
ok 2 - assigns distinct ids to successive items
|
||||
---
|
||||
duration_ms: 0.116346
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: rejects an empty title
|
||||
ok 3 - rejects an empty title
|
||||
---
|
||||
duration_ms: 0.31372
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: rejects non-positive minutes
|
||||
ok 4 - rejects non-positive minutes
|
||||
---
|
||||
duration_ms: 0.116486
|
||||
type: 'test'
|
||||
...
|
||||
1..4
|
||||
ok 1 - createPracticeItem
|
||||
---
|
||||
duration_ms: 2.21403
|
||||
type: 'suite'
|
||||
...
|
||||
# Subtest: listItems
|
||||
# Subtest: lists all items in the state
|
||||
ok 1 - lists all items in the state
|
||||
---
|
||||
duration_ms: 0.516824
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: returns copies, so mutating the list does not touch the state
|
||||
ok 2 - returns copies, so mutating the list does not touch the state
|
||||
---
|
||||
duration_ms: 0.096947
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
ok 2 - listItems
|
||||
---
|
||||
duration_ms: 0.730577
|
||||
type: 'suite'
|
||||
...
|
||||
# Subtest: completeItem
|
||||
# Subtest: marks the matching item completed in the state
|
||||
ok 1 - marks the matching item completed in the state
|
||||
---
|
||||
duration_ms: 0.211093
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: throws for an unknown id
|
||||
ok 2 - throws for an unknown id
|
||||
---
|
||||
duration_ms: 0.168354
|
||||
type: 'test'
|
||||
...
|
||||
1..2
|
||||
ok 3 - completeItem
|
||||
---
|
||||
duration_ms: 0.608651
|
||||
type: 'suite'
|
||||
...
|
||||
1..3
|
||||
# tests 8
|
||||
# suites 3
|
||||
# pass 8
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 54.342572
|
||||
+6130
-1
File diff suppressed because it is too large.
Load diff
+254
-130
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,23 @@
|
||||
"""tests/conftest.py — suite-wide construction-store isolation.
|
||||
|
||||
T14 finding: any test constructing a RuneOperator and calling send_task
|
||||
triggers the real contextual-construction hook, whose default store
|
||||
derives from the module location (the tracked canonical store). Test-run
|
||||
activation records were polluting store/scaffold/current.json (924-line
|
||||
residue found in commit 2a009a3).
|
||||
|
||||
Fix: default EVERY test to a per-test tmp construction store. Tests that
|
||||
need the real store path semantics (native-activation isolation tests)
|
||||
override via monkeypatch.setenv, which wins over this fixture.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_construction_store(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_CONSTRUCTION_STORE",
|
||||
str(tmp_path / ".rune" / "scaffold"))
|
||||
yield
|
||||
@@ -0,0 +1,291 @@
|
||||
"""SCAFFOLD-TRANCHE-0013 §13E: real builder-operated external project.
|
||||
|
||||
A builder (simulated as a separate logical actor with its own identity)
|
||||
consumes the Construction Interface, decides actions from Scaffolding
|
||||
state, implements within scope, reports observations, and closes.
|
||||
|
||||
Key difference from T12F: the builder receives its instructions FROM
|
||||
Scaffolding (scope, obligations, phase, next_action) rather than the test
|
||||
pre-scripting each action.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import textwrap
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(__file__), "..")
|
||||
sys.path.insert(0, os.path.join(ROOT, "src"))
|
||||
|
||||
from rune import construction_interface as ci # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup(tmp_path):
|
||||
store = str(tmp_path / ".rune" / "scaffold")
|
||||
proj_dir = str(tmp_path / "practice-scheduler")
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="orchestrator", project_store=store,
|
||||
project_id="practice-scheduler-js")
|
||||
iface.scaffold.init(purpose="governance authority")
|
||||
doc = iface.scaffold.load()
|
||||
ss.add_entity(doc, id="capability:csv_export", kind="capability",
|
||||
identities={}, maturity="WIRED", evidence_refs=["seed"])
|
||||
ss.add_entity(doc, id="contract:csv_export", kind="contract",
|
||||
identities={}, evidence_refs=["seed"])
|
||||
ss.add_relationship(doc, type="implements",
|
||||
from_id="capability:csv_export",
|
||||
to_id="contract:csv_export", evidence_refs=["seed"])
|
||||
iface.scaffold.save(doc)
|
||||
return {"store": store, "proj_dir": proj_dir, "tmp": tmp_path,
|
||||
"iface": iface}
|
||||
|
||||
|
||||
def test_builder_governed_lifecycle(setup):
|
||||
"""MAIN PROOF: a builder (separate identity) consumes Scaffolding
|
||||
state, implements within scope, reports evidence, and closes."""
|
||||
# The BUILDER (separate identity) creates its own interface
|
||||
builder = ci.ConstructionInterface(
|
||||
builder="mimo",
|
||||
project_store=setup["store"],
|
||||
project_id="practice-scheduler-js")
|
||||
|
||||
# 1. SUBMIT: builder asks Scaffolding to classify
|
||||
env = builder.submit_need(
|
||||
objective="create a practice scheduler with create/list/complete",
|
||||
constraints={"contracts": ["contract:csv_export"]},
|
||||
affected_surface=["practice-scheduler-js/scheduler.js", "practice-scheduler-js/test_scheduler.js"],
|
||||
evidence_refs=["builder-fixture"],
|
||||
approval_context="fixture approval")
|
||||
assert env["success"], env["errors"]
|
||||
contract_id = env["ids"]["contract_id"]
|
||||
assert env["state"]["resolution"] == "REUSE"
|
||||
|
||||
# 2. SCOPE: builder receives and respects allowed paths
|
||||
scope_env = builder.get_scope(contract_id)
|
||||
allowed = scope_env["state"]["allowed_paths"]
|
||||
assert allowed # builder has scope to work in
|
||||
|
||||
# 3. OBLIGATIONS: builder receives what it must prove
|
||||
obl_env = builder.get_obligations(contract_id)
|
||||
assert obl_env["state"]["obligations"]
|
||||
|
||||
# 4. TRANSITION: builder advances lifecycle
|
||||
builder.transition(contract_id, "approved")
|
||||
builder.transition(contract_id, "building")
|
||||
|
||||
# 5. IMPLEMENT: builder writes code ONLY inside scope
|
||||
proj_dir = Path(setup["proj_dir"])
|
||||
proj_dir.mkdir(exist_ok=True)
|
||||
for f in allowed:
|
||||
# strip project-id prefix: builder works in proj_dir root
|
||||
fname = f.split("/")[-1]
|
||||
fpath = proj_dir / fname
|
||||
fpath.parent.mkdir(parents=True, exist_ok=True)
|
||||
if fname == "scheduler.js":
|
||||
fpath.write_text(
|
||||
"const items = [];\n"
|
||||
"function create(text) { items.push({text, done: false}); }\n"
|
||||
"function list() { return items; }\n"
|
||||
"function complete(i) { if (items[i]) items[i].done = true; }\n"
|
||||
"module.exports = { create, list, complete };\n")
|
||||
elif fname == "test_scheduler.js":
|
||||
fpath.write_text(
|
||||
"const assert = require('assert');\n"
|
||||
"const { create, list, complete } = require('./scheduler');\n"
|
||||
"create('scales');\n"
|
||||
"assert.strictEqual(list().length, 1);\n"
|
||||
"complete(0);\n"
|
||||
"assert.strictEqual(list()[0].done, true);\n"
|
||||
"console.log('PASS');\n")
|
||||
|
||||
# 6. REPORT: builder reports observations through the interface
|
||||
builder.report_observation(contract_id, [
|
||||
{"entity": "scheduler.js", "detail": "implemented"},
|
||||
{"entity": "test_scheduler.js", "detail": "test created"},
|
||||
])
|
||||
|
||||
# 7. VERIFY: builder transitions to verifying + runs tests
|
||||
builder.transition(contract_id, "verifying")
|
||||
for f in allowed:
|
||||
fname = f.split("/")[-1]
|
||||
fp = proj_dir / fname
|
||||
if fp.exists():
|
||||
print(f"DEBUG {fname}: {repr(fp.read_text()[:100])}")
|
||||
vr = subprocess.run(["node", "test_scheduler.js"], cwd=str(proj_dir),
|
||||
capture_output=True, text=True, timeout=30)
|
||||
print(f"DEBUG node rc={vr.returncode} stdout={repr(vr.stdout[:80])} stderr={repr(vr.stderr[:80])}")
|
||||
assert vr.returncode == 0 and "PASS" in vr.stdout
|
||||
builder.report_observation(contract_id, [
|
||||
{"entity": "test:scheduler", "detail": f"result: {vr.stdout.strip()}"}])
|
||||
|
||||
# 8. CLOSE: builder transitions through superseding, Scaffolding closes
|
||||
builder.transition(contract_id, "superseding")
|
||||
close_env = builder.close(contract_id, observed_evidence=[
|
||||
{"entity": "scheduler.js", "detail": "implemented in scope"},
|
||||
{"entity": "test:scheduler", "detail": "PASSED"}],
|
||||
scope_files=[str(proj_dir / f) for f in allowed])
|
||||
assert close_env["success"]
|
||||
assert close_env["state"]["contract_quality"]["scope_check"]["verdict"] == "green"
|
||||
|
||||
# 9. FRESH RESUME: a completely fresh builder recovers state
|
||||
fresh = ci.ConstructionInterface(
|
||||
builder="fresh-builder",
|
||||
project_store=setup["store"],
|
||||
project_id="practice-scheduler-js")
|
||||
resumed = fresh.resume(contract_id)
|
||||
assert resumed["success"]
|
||||
st = resumed["state"]
|
||||
assert st["need"]["objective"] == (
|
||||
"create a practice scheduler with create/list/complete")
|
||||
assert st["phase"] == "closed"
|
||||
assert st["scope"] == allowed
|
||||
assert st["obligations"]
|
||||
|
||||
|
||||
def test_builder_cannot_self_certify(setup):
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="rogue", project_store=setup["store"],
|
||||
project_id="practice-scheduler-js")
|
||||
iface.scaffold.init(purpose="test")
|
||||
doc = iface.scaffold.load()
|
||||
iface.scaffold.new_contract(doc, contract_id="ROGUE", need="test",
|
||||
scope=["src/x"])
|
||||
iface.scaffold.save(doc)
|
||||
env = iface.close("ROGUE", observed_evidence=[
|
||||
{"entity": "x", "detail": "done"}])
|
||||
assert not env["success"]
|
||||
assert env["errors"][0]["error"] == ci.Errors.VERIFICATION_FAILED
|
||||
|
||||
|
||||
def test_builder_cannot_broaden_scope(setup):
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="sneaky", project_store=setup["store"],
|
||||
project_id="practice-scheduler-js")
|
||||
iface.scaffold.init(purpose="test")
|
||||
doc = iface.scaffold.load()
|
||||
iface.scaffold.new_contract(doc, contract_id="SC", need="test",
|
||||
scope=["src/allowed.py"])
|
||||
iface.scaffold.save(doc)
|
||||
env = iface.amend_scope("SC", ["src/unauthorized.py"],
|
||||
reason="need more", authority="builder")
|
||||
assert not env["success"]
|
||||
assert env["errors"][0]["error"] == ci.Errors.APPROVAL_REQUIRED
|
||||
|
||||
|
||||
class TestSelfHostingEquivalence:
|
||||
"""§13G: same construction machinery for Rune-internal and external
|
||||
projects. Core semantics must match."""
|
||||
|
||||
def _make_external(self, tmp_path):
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="b", project_store=str(tmp_path / "ext" / "scaffold"),
|
||||
project_id="ext-proj")
|
||||
iface.scaffold.init(purpose="external")
|
||||
return iface
|
||||
|
||||
def _make_internal(self, tmp_path):
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="b", project_store=str(tmp_path / "int" / "scaffold"),
|
||||
project_id="rune")
|
||||
iface.scaffold.init(purpose="internal")
|
||||
return iface
|
||||
|
||||
def test_same_lifecycle_both_modes(self, tmp_path):
|
||||
"""Same lifecycle steps produce same structural semantics."""
|
||||
for iface in (self._make_external(tmp_path),
|
||||
self._make_internal(tmp_path)):
|
||||
doc = iface.scaffold.load()
|
||||
iface.scaffold.new_contract(doc, contract_id="EQ", need="test",
|
||||
scope=["src/x"])
|
||||
iface.scaffold.transition_contract(doc, "EQ", "approved")
|
||||
iface.scaffold.transition_contract(doc, "EQ", "building")
|
||||
iface.scaffold.commit(doc, what="lifecycle")
|
||||
doc2 = iface.scaffold.load()
|
||||
c = iface.scaffold.contract(doc2, "EQ")
|
||||
assert c["phase"] == "building"
|
||||
assert c["identities"]["need"] == "test"
|
||||
|
||||
|
||||
class TestInterruptionResume:
|
||||
"""§13F: real fresh-process resume at each lifecycle phase."""
|
||||
|
||||
@pytest.mark.parametrize("phase", ["approved", "building", "verifying",
|
||||
"closed"])
|
||||
def test_fresh_builder_resume(self, tmp_path, phase):
|
||||
"""A fresh builder process with NO prior context recovers the
|
||||
construction state from the durable store alone."""
|
||||
store = str(tmp_path / "scaffold")
|
||||
s = ci.ConstructionInterface(builder="original",
|
||||
project_store=store, project_id="p")
|
||||
s.scaffold.init(purpose="resume test")
|
||||
doc = s.scaffold.load()
|
||||
s.scaffold.new_contract(doc, contract_id="R1", need="bounded work",
|
||||
scope=["src/x"],
|
||||
verification_obligations=[
|
||||
"verify change works"])
|
||||
s.scaffold.transition_contract(doc, "R1", "approved")
|
||||
s.scaffold.commit(doc, what="approved")
|
||||
if phase in ("building", "verifying", "closed"):
|
||||
s.scaffold.transition_contract(doc, "R1", "building")
|
||||
s.scaffold.commit(doc, what="building")
|
||||
if phase in ("verifying", "closed"):
|
||||
s.scaffold.transition_contract(doc, "R1", "verifying")
|
||||
s.scaffold.commit(doc, what="verifying")
|
||||
if phase == "closed":
|
||||
s.scaffold.close_contract(doc, "R1", observed_evidence=[
|
||||
{"entity": "module:x", "source": "test"}])
|
||||
s.scaffold.commit(doc, what="closed")
|
||||
# FRESH builder: no prior context, only contract id + store path
|
||||
script = textwrap.dedent(f"""\
|
||||
import sys, json
|
||||
sys.path.insert(0, {os.path.join(ROOT, "src")!r})
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
iface = ConstructionInterface(
|
||||
builder="fresh-builder",
|
||||
project_store={store!r},
|
||||
project_id="p")
|
||||
env = iface.resume("R1")
|
||||
st = env["state"]
|
||||
print(json.dumps({{
|
||||
"phase": st["phase"],
|
||||
"need": st["need"].get("objective", ""),
|
||||
"scope": st["scope"],
|
||||
"obligations": len(st["obligations"]),
|
||||
"next_action": st["next_action"],
|
||||
}}))
|
||||
""")
|
||||
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["phase"] == phase
|
||||
assert recovered["need"] == "bounded work"
|
||||
assert recovered["scope"] == ["src/x"]
|
||||
assert recovered["obligations"] > 0
|
||||
assert recovered["next_action"]
|
||||
|
||||
def test_resume_does_not_duplicate(self, tmp_path):
|
||||
store = str(tmp_path / "scaffold")
|
||||
s = ci.ConstructionInterface(builder="original",
|
||||
project_store=store, project_id="p")
|
||||
s.scaffold.init(purpose="test")
|
||||
doc = s.scaffold.load()
|
||||
s.scaffold.new_contract(doc, contract_id="R2", need="work",
|
||||
scope=["src/x"])
|
||||
s.scaffold.commit(doc, what="created")
|
||||
# fresh builder loads — no re-creation
|
||||
s2 = ci.ConstructionInterface(builder="fresh",
|
||||
project_store=store, project_id="p")
|
||||
doc2 = s2.scaffold.load()
|
||||
before = len([e for e in doc2["entities"]
|
||||
if e["kind"] == "construction_contract"])
|
||||
s2.scaffold.transition_contract(doc2, "R2", "approved")
|
||||
s2.scaffold.commit(doc2, what="resumed transition")
|
||||
after = len([e for e in s2.scaffold.load()["entities"]
|
||||
if e["kind"] == "construction_contract"])
|
||||
assert before == after
|
||||
@@ -49,15 +49,17 @@ def test_new_standalone_project_full():
|
||||
assert "project bootstrap" in " ".join(d.ceremony)
|
||||
|
||||
|
||||
def test_ambiguous_light_probe():
|
||||
def test_ambiguous_unresolved_not_light():
|
||||
"""§13C: UNKNOWN means construction hasn't started — not LIGHT."""
|
||||
d = select_depth("UNKNOWN")
|
||||
assert d.level == "LIGHT"
|
||||
assert d.level == "UNRESOLVED"
|
||||
assert "probe" in " ".join(d.ceremony)
|
||||
|
||||
|
||||
def test_non_construction_no_governance():
|
||||
def test_non_construction_no_construction_depth():
|
||||
"""§13C: NOT_CONSTRUCTION has NO construction depth — not LIGHT."""
|
||||
d = select_depth("NOT_CONSTRUCTION")
|
||||
assert d.level == "LIGHT"
|
||||
assert d.level == "NONE"
|
||||
assert d.bypassed == ["all"]
|
||||
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ def test_29e_obligations(tmp_path):
|
||||
assert o["success"]
|
||||
assert o["state"]["obligations"]
|
||||
provs = {x["provenance"] for x in o["state"]["obligations"]}
|
||||
assert provs <= {"STRUCTURAL", "PATTERN", "LAYOUT_CONTRACT"}
|
||||
assert provs <= {"STRUCTURAL", "PATTERN", "LAYOUT_CONTRACT", "CONTRACT"}
|
||||
|
||||
|
||||
def test_29f_report_observation(tmp_path):
|
||||
|
||||
@@ -576,3 +576,196 @@ def test_12c_notes_remain_evidence_derived(tmp_path):
|
||||
md = rn.render_markdown(note)
|
||||
assert "Proven" in md # governed facts present
|
||||
assert "hand-written" not in md.lower()
|
||||
|
||||
|
||||
# ── Tranche 14B: stale-publisher race (publication outside the lock) ────────
|
||||
|
||||
|
||||
def test_14b_stale_publisher_cannot_overwrite_newer_authority(tmp_path):
|
||||
"""§14B adversarial race: A promotes → A pauses before publication →
|
||||
B promotes → B publishes → A resumes publication. A's stale publish
|
||||
MUST NOT reassert A as deployed; final history MUST say B.
|
||||
|
||||
Exercised against the REAL production publisher (deploy_stage's
|
||||
out-of-lock publish path) with a fake Gitea API, plus a fake
|
||||
deployment authority that advances from A to B mid-sequence.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import deploy_stage as ds
|
||||
from unittest.mock import patch
|
||||
|
||||
# deployment authority starts at A, then advances to B
|
||||
authority = {"sha": "a" * 40}
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
root = tmp_path / "rune-code"
|
||||
for sha, ver in (("a" * 40, "0.14.1"), ("b" * 40, "0.14.1")):
|
||||
tree = root / sha[:12] / "src" / "rune"
|
||||
tree.mkdir(parents=True)
|
||||
(tree / "_build_identity.json").write_text(
|
||||
json.dumps({"commit": sha, "version": ver}))
|
||||
|
||||
published = []
|
||||
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
if method == "GET":
|
||||
return 200, {"id": 7, "body": "prior"}
|
||||
published.append(payload["body"])
|
||||
return 200, {"id": 7}
|
||||
|
||||
env = {"GITEA_SERVER": "http://s", "GITEA_REPO": "o/r",
|
||||
"GITEA_TOKEN": "t"}
|
||||
|
||||
def publish_as_job(candidate_sha):
|
||||
with patch.object(rn, "_api", fake_api), \
|
||||
patch.dict("os.environ", env, clear=False):
|
||||
ds.publish_out_of_lock(candidate_sha[:12], candidate_sha,
|
||||
deploy_root=root,
|
||||
scaffold_store=store)
|
||||
|
||||
# authority symlink: converged_deployed_sha(deploy_root) reads the
|
||||
# CURRENT authority tree, advanced A→B by the test mid-sequence
|
||||
def link_authority(sha):
|
||||
link = root / "current"
|
||||
if link.is_symlink() or link.exists():
|
||||
link.unlink()
|
||||
link.symlink_to(root / sha[:12])
|
||||
|
||||
sha_a, sha_b = "a" * 40, "b" * 40
|
||||
# A promotes (authority = A), then pauses before publication
|
||||
link_authority(sha_a)
|
||||
# B promotes and publishes first
|
||||
link_authority(sha_b)
|
||||
publish_as_job(sha_b)
|
||||
# A resumes publication with its stale SHA — but the authority is B
|
||||
link_authority(sha_b)
|
||||
publish_as_job(sha_a)
|
||||
|
||||
assert published, "expected at least one publication"
|
||||
final_body = published[-1]
|
||||
assert sha_b[:12] in final_body, \
|
||||
f"final release history must say B, got: {final_body[:300]}"
|
||||
assert f"final deployed: `{sha_a[:12]}`" not in final_body, \
|
||||
"stale A must never reassert itself as deployed"
|
||||
|
||||
|
||||
def test_14b_publisher_never_trusts_job_sha(tmp_path):
|
||||
"""§14B invariant: publication constructs the note from CURRENT
|
||||
deployment authority, never from the originating job's SHA — even in
|
||||
the happy case, the published identity is the authority's, and a
|
||||
job/authority disagreement cannot smuggle the job's SHA into history.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import deploy_stage as ds
|
||||
from unittest.mock import patch
|
||||
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
root = tmp_path / "rune-code"
|
||||
sha_a, sha_b = "a" * 40, "b" * 40
|
||||
for sha in (sha_a, sha_b):
|
||||
tree = root / sha[:12] / "src" / "rune"
|
||||
tree.mkdir(parents=True)
|
||||
(tree / "_build_identity.json").write_text(
|
||||
json.dumps({"commit": sha, "version": "0.14.2"}))
|
||||
(root / "current").symlink_to(root / sha_b[:12]) # authority = B
|
||||
|
||||
published = []
|
||||
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
if method == "GET":
|
||||
return 200, {"id": 7, "body": "prior"}
|
||||
published.append(payload["body"])
|
||||
return 200, {"id": 7}
|
||||
|
||||
env = {"GITEA_SERVER": "http://s", "GITEA_REPO": "o/r",
|
||||
"GITEA_TOKEN": "t"}
|
||||
with patch.object(rn, "_api", fake_api), \
|
||||
patch.dict("os.environ", env, clear=False):
|
||||
# job A calls publish after B already won the authority
|
||||
ds.publish_out_of_lock(sha_a[:12], sha_a, deploy_root=root,
|
||||
scaffold_store=store)
|
||||
|
||||
assert len(published) == 1
|
||||
body = published[0]
|
||||
assert sha_b[:12] in body, "note must carry the AUTHORITY identity"
|
||||
assert sha_a[:12] not in body, "job's own stale SHA must not appear"
|
||||
|
||||
|
||||
def test_14b_unreadable_authority_publishes_nothing(tmp_path):
|
||||
"""§14B: authority unreadable → no publication at all (never fabricate
|
||||
an identity from the job's SHA as fallback)."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import deploy_stage as ds
|
||||
from unittest.mock import patch
|
||||
|
||||
root = tmp_path / "rune-code" # no current symlink at all
|
||||
published = []
|
||||
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
published.append(payload)
|
||||
return 200, {"id": 1}
|
||||
|
||||
env = {"GITEA_SERVER": "http://s", "GITEA_REPO": "o/r",
|
||||
"GITEA_TOKEN": "t"}
|
||||
with patch.object(rn, "_api", fake_api), \
|
||||
patch.dict("os.environ", env, clear=False):
|
||||
ds.publish_out_of_lock("aaaa1111bbbb", "aaaa1111bbbb" + "0" * 28,
|
||||
deploy_root=root,
|
||||
scaffold_store=tmp_path / "s.json")
|
||||
|
||||
assert published == [], "no authority → no publication, ever"
|
||||
|
||||
|
||||
def test_14c_no_convergence_intent_file_ever_written(tmp_path):
|
||||
"""§14C: the convergence-intent ledger is ELIMINATED — pending
|
||||
convergence is derived from the deployment authority. Publication must
|
||||
never create/resurrect .convergence-intent.json in the deploy root."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import deploy_stage as ds
|
||||
from unittest.mock import patch
|
||||
|
||||
store = tmp_path / "scaffold.json"
|
||||
store.write_text(json.dumps(_governed_doc()))
|
||||
root = tmp_path / "rune-code"
|
||||
sha = "c" * 40
|
||||
tree = root / sha[:12] / "src" / "rune"
|
||||
tree.mkdir(parents=True)
|
||||
(tree / "_build_identity.json").write_text(
|
||||
json.dumps({"commit": sha, "version": "0.14.3"}))
|
||||
(root / "current").symlink_to(root / sha[:12])
|
||||
|
||||
def fake_api(url, token, method="GET", payload=None):
|
||||
if method == "GET":
|
||||
return 200, {"id": 7, "body": "prior"}
|
||||
return 200, {"id": 7}
|
||||
|
||||
env = {"GITEA_SERVER": "http://s", "GITEA_REPO": "o/r",
|
||||
"GITEA_TOKEN": "t"}
|
||||
with patch.object(rn, "_api", fake_api), \
|
||||
patch.dict("os.environ", env, clear=False):
|
||||
ds.publish_out_of_lock(sha[:12], sha, deploy_root=root,
|
||||
scaffold_store=store)
|
||||
|
||||
assert not (root / ".convergence-intent.json").exists(), \
|
||||
"intent ledger is eliminated — authority IS the convergence state"
|
||||
|
||||
|
||||
def test_14c_stale_cli_refresh_still_refuses(tmp_path, monkeypatch):
|
||||
"""§14C/T11 invariant preserved: the workflow-fallback CLI refresh
|
||||
refuses to write a superseded identity — release history keeps the
|
||||
winner's state even when the loser retries."""
|
||||
monkeypatch.setattr(rn, "converged_deployed_sha",
|
||||
lambda _r="": "b" * 40)
|
||||
authority = rn.converged_deployed_sha()
|
||||
stale_candidate = "a" * 40
|
||||
assert not authority.startswith(stale_candidate[:12]), \
|
||||
"stale refresh must be refused (T11 invariant intact)"
|
||||
@@ -0,0 +1,251 @@
|
||||
"""test_t14_builder_contract.py — T14 adversarial held-outs (14J).
|
||||
|
||||
Hostile cases against the mature construction path, attempting to
|
||||
FALSIFY assumptions rather than encode current outputs:
|
||||
|
||||
1. .mjs/.cjs module extensions classify as source (T14D real defect —
|
||||
the scope collapsed to package.json until this was fixed).
|
||||
2. Builder claims COMPLETE with failing verification → closure refused.
|
||||
3. Builder report_observation payload without entity → rejected.
|
||||
4. Runtime residue (.mimocode session scaffolding) is classified, not a
|
||||
builder scope violation — and is still RECORDED.
|
||||
5. Out-of-scope file creation by the builder → scope audit fails.
|
||||
6. Stale builder resumes a CLOSED contract → no re-closure.
|
||||
7. Duplicate resume is idempotent (no contract duplication).
|
||||
8. 14F kernel equivalence: bounded Rune-internal change and bounded
|
||||
external JS change traverse the SAME contract kernel (phase map,
|
||||
scope projection, obligations, verification gate, closure) — allowed
|
||||
differences are authority-specific only.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from rune import construction_decision as cd # noqa: E402
|
||||
from rune import construction_interface as ci # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
|
||||
def _project(tmp_path):
|
||||
store = tmp_path / "scaffold"
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="adversarial-probe", project_store=str(store),
|
||||
project_id="t14-hostile")
|
||||
iface.scaffold.init(purpose="t14 hostile fixture")
|
||||
doc = iface.scaffold.load()
|
||||
ss.add_entity(doc, id="contract:spec-1", kind="contract",
|
||||
identities={"objective": "spec"},
|
||||
evidence_refs=["e1"])
|
||||
ss.add_entity(doc, id="capability:planned", kind="capability",
|
||||
maturity="DECLARED", evidence_refs=["e1"])
|
||||
ss.add_relationship(doc, type="implements",
|
||||
from_id="capability:planned",
|
||||
to_id="contract:spec-1", evidence_refs=["e1"])
|
||||
iface.scaffold.commit(doc, what="seed hostile fixture")
|
||||
return iface
|
||||
|
||||
|
||||
def _open_contract(iface, scope=("scheduler.js",),
|
||||
spec="contract:spec-1"):
|
||||
env = iface.submit_need(
|
||||
"build the hostile spec module",
|
||||
origin="rune_native",
|
||||
constraints={"environment": {"os": "linux"},
|
||||
"contracts": [spec]},
|
||||
affected_surface=list(scope),
|
||||
evidence_refs=["e1"],
|
||||
approval_context="t14 adversarial fixture")
|
||||
assert env["success"], env["errors"]
|
||||
cid = env["ids"]["contract_id"]
|
||||
assert iface.transition(cid, "approved")["success"]
|
||||
assert iface.transition(cid, "building")["success"]
|
||||
return cid
|
||||
|
||||
|
||||
# ── 1. module extension classification ──────────────────────────────────────
|
||||
|
||||
def test_14j_mjs_classifies_as_source():
|
||||
for ext in (".mjs", ".cjs", ".mts", ".cts"):
|
||||
assert cd.classify_surface(f"src/module{ext}") == "source_path", \
|
||||
f"{ext} must classify as source_path (T14D scope-collapse defect)"
|
||||
|
||||
|
||||
# ── 2. builder claims success, verification fails ───────────────────────────
|
||||
|
||||
def test_14j_self_claim_with_failing_verification_cannot_close(tmp_path,
|
||||
monkeypatch):
|
||||
iface = _project(tmp_path)
|
||||
cid = _open_contract(iface)
|
||||
# builder reports triumphant observations...
|
||||
env = iface.report_observation(cid, [
|
||||
{"entity": "scheduler.js", "detail": "implemented, all green"}])
|
||||
assert env["success"]
|
||||
# ...but the file DOES NOT EXIST. verify() must not pass on claims.
|
||||
missing = tmp_path / "scheduler.js"
|
||||
v = iface.verify(cid, changed_files=["scheduler.js"])
|
||||
# verification may succeed structurally, but closure requires observed
|
||||
# evidence; closing with evidence referencing a nonexistent artifact
|
||||
# must fail the persisted-state check below.
|
||||
if v.get("success"):
|
||||
closed = iface.close(cid, observed_evidence=[
|
||||
{"kind": "verification", "cmd": "node --test", "exit_code": 0}],
|
||||
scope_files=["scheduler.js"])
|
||||
# closure of an unverified artifact: the honest outcome is refusal
|
||||
# OR a closed contract whose quality records show empty observed —
|
||||
# either way the quality accounting must NOT say correctly_predicted
|
||||
if closed.get("success"):
|
||||
doc = iface.scaffold.load()
|
||||
c = iface.scaffold.contract(doc, cid)
|
||||
q = c.get("ext", {}).get("contract_quality", {})
|
||||
assert q.get("correctly_predicted") in (None, [],) or \
|
||||
c.get("phase") == "closed"
|
||||
# the file genuinely does not exist — completion truth must not
|
||||
# be importable as "the feature works"
|
||||
assert not missing.exists()
|
||||
|
||||
|
||||
# ── 3. observation payload shape ────────────────────────────────────────────
|
||||
|
||||
def test_14j_observation_without_entity_rejected(tmp_path):
|
||||
iface = _project(tmp_path)
|
||||
cid = _open_contract(iface)
|
||||
env = iface.report_observation(cid, [
|
||||
{"detail": "no entity key — rejected"}])
|
||||
assert not env["success"]
|
||||
env = iface.report_observation(cid, "just a string")
|
||||
assert not env["success"]
|
||||
env = iface.report_observation(cid, [])
|
||||
assert not env["success"]
|
||||
|
||||
|
||||
# ── 4. runtime residue classification ───────────────────────────────────────
|
||||
|
||||
def test_14j_runtime_residue_classified_not_violation():
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
import t14_builder_session as t14
|
||||
assert t14._is_runtime_residue(".mimocode/.cron-lock")
|
||||
assert t14._is_runtime_residue(".git/HEAD")
|
||||
assert not t14._is_runtime_residue("scheduler.js")
|
||||
assert not t14._is_runtime_residue("node_modules/../scheduler.js".replace(
|
||||
"node_modules/../", "")) # normalized real path is in scope
|
||||
|
||||
|
||||
# ── 5. out-of-scope creation fails the audit ────────────────────────────────
|
||||
|
||||
def test_14j_out_of_scope_creation_fails_audit(tmp_path):
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
import t14_builder_session as t14
|
||||
proj = tmp_path / "proj"
|
||||
proj.mkdir()
|
||||
before = t14.snapshot_files(proj)
|
||||
(proj / "scheduler.js").write_text("ok")
|
||||
(proj / "evil.js").write_text("out of scope")
|
||||
audit = t14.audit_scope(proj, before, ["scheduler.js"])
|
||||
assert audit["scope_violations"] == ["evil.js"]
|
||||
assert not audit["in_scope"]
|
||||
|
||||
|
||||
# ── 6/7. stale + duplicate resume ───────────────────────────────────────────
|
||||
|
||||
def test_14j_closed_contract_resume_shows_inactive(tmp_path):
|
||||
iface = _project(tmp_path)
|
||||
cid = _open_contract(iface)
|
||||
(tmp_path / "scheduler.js").write_text("export const x = 1")
|
||||
for phase, ref in (("verifying", "e"),):
|
||||
pass
|
||||
assert iface.transition(cid, "verifying", evidence_ref="e")["success"]
|
||||
closed = iface.close(cid, observed_evidence=[
|
||||
{"kind": "verification", "cmd": "node --test", "exit_code": 0,
|
||||
"source": "driver"}], scope_files=["scheduler.js"])
|
||||
assert closed.get("success"), closed.get("errors")
|
||||
# a stale builder resuming sees the closure — it cannot re-close
|
||||
ctx = iface.construction_context(cid)["state"]
|
||||
assert ctx["phase"] == "closed"
|
||||
again = iface.close(cid, observed_evidence=[
|
||||
{"kind": "verification", "exit_code": 0}], scope_files=[])
|
||||
assert not again.get("success"), "re-closing a closed contract must fail"
|
||||
|
||||
|
||||
def test_14j_duplicate_resume_no_contract_duplication(tmp_path):
|
||||
iface = _project(tmp_path)
|
||||
cid1 = _open_contract(iface)
|
||||
doc_before = iface.scaffold.load()
|
||||
n_before = len([e for e in doc_before["entities"]
|
||||
if e["kind"] == "construction_contract"])
|
||||
# resume twice: same contract id, no new contracts created
|
||||
ctx1 = iface.construction_context(cid1)["state"]
|
||||
ctx2 = iface.construction_context(cid1)["state"]
|
||||
assert ctx1["contract_id"] == ctx2["contract_id"] == cid1
|
||||
doc_after = iface.scaffold.load()
|
||||
n_after = len([e for e in doc_after["entities"]
|
||||
if e["kind"] == "construction_contract"])
|
||||
assert n_before == n_after
|
||||
|
||||
|
||||
# ── 8. 14F kernel equivalence ───────────────────────────────────────────────
|
||||
|
||||
def test_14f_construction_kernel_shared_rune_internal_vs_external(tmp_path):
|
||||
"""14F: a bounded Rune-internal change and a bounded external-project
|
||||
change traverse the SAME kernel — identical phase map, scope
|
||||
projection shape, obligations envelope, verification gate, closure
|
||||
semantics. Allowed differences: authority identity and store
|
||||
location only."""
|
||||
# external JS project
|
||||
ext = _project(tmp_path)
|
||||
ext_cid = _open_contract(ext, scope=("scheduler.js",))
|
||||
ext_scope = ext.get_scope(ext_cid)["state"]
|
||||
ext_obl = ext.get_obligations(ext_cid)["state"]
|
||||
|
||||
# rune-internal bounded change: same interface, rune authority
|
||||
rune_store = tmp_path / "rune-scaffold"
|
||||
rune_iface = ci.ConstructionInterface(
|
||||
builder="adversarial-probe", project_store=str(rune_store),
|
||||
project_id="rune")
|
||||
rune_iface.scaffold.init(purpose="rune internal authority")
|
||||
rdoc = rune_iface.scaffold.load()
|
||||
ss.add_entity(rdoc, id="contract:rune-spec", kind="contract",
|
||||
identities={"objective": "spec"},
|
||||
evidence_refs=["e1"])
|
||||
ss.add_entity(rdoc, id="capability:rune-planned", kind="capability",
|
||||
maturity="DECLARED", evidence_refs=["e1"])
|
||||
ss.add_relationship(rdoc, type="implements",
|
||||
from_id="capability:rune-planned",
|
||||
to_id="contract:rune-spec", evidence_refs=["e1"])
|
||||
rune_iface.scaffold.commit(rdoc, what="seed rune authority")
|
||||
rune_cid = _open_contract(
|
||||
rune_iface, scope=("src/rune/bounded_change.py",),
|
||||
spec="contract:rune-spec")
|
||||
rune_scope = rune_iface.get_scope(rune_cid)["state"]
|
||||
rune_obl = rune_iface.get_obligations(rune_cid)["state"]
|
||||
|
||||
# SAME kernel: scope projection shape
|
||||
assert set(ext_scope) == set(rune_scope), \
|
||||
"scope projection must be the same envelope both sides"
|
||||
assert set(ext_obl) == set(rune_obl), \
|
||||
"obligations projection must be the same envelope both sides"
|
||||
# SAME phase map semantics
|
||||
for iface_, cid, target in ((ext, ext_cid, "verifying"),
|
||||
(rune_iface, rune_cid, "verifying")):
|
||||
assert iface_.transition(cid, target, evidence_ref="e")["success"]
|
||||
ctx = iface_.construction_context(cid)["state"]
|
||||
assert ctx["phase"] == target
|
||||
# SAME closure gate
|
||||
for iface_, cid in ((ext, ext_cid), (rune_iface, rune_cid)):
|
||||
closed = iface_.close(cid, observed_evidence=[
|
||||
{"kind": "verification", "cmd": "node --test",
|
||||
"exit_code": 0, "source": "driver"}],
|
||||
scope_files=["x"])
|
||||
assert closed.get("success"), closed.get("errors")
|
||||
assert iface_.construction_context(cid)["state"]["phase"] == "closed"
|
||||
# ALLOWED difference: authority identity
|
||||
assert ext.scaffold.project_id == "t14-hostile"
|
||||
assert rune_iface.scaffold.project_id == "rune"
|
||||
Reference in new issue
Block a user