feat(scaffolding): §15N/§15O/§15P — discoverability, provenance, idempotency

15N: capabilities() now exposes full argument schemas (types, required
fields, per-item shape, notes), lifecycle phase map + legal transitions,
and scope-amendment authority — a fresh builder DISCOVERS the
report_observation contract instead of trial-and-error (the T14
dict-vs-list + missing-entity mistakes become impossible). Held-out:
schema-guided first report succeeds; schema-violating payload rejected.

15O: audit_scope is PROVENANCE-AWARE — residue paths present before the
builder ran (or explicitly runtime-declared) stay exempt and recorded;
NEW residue-looking paths appearing mid-session (.mimocode/evil,
.git/config) are SCOPE VIOLATIONS. The hostile lookalike can no longer
hide behind the T14 prefix rule.

15P: PROJECT_BUILD bootstrap twice = one project identity (no
duplication); event replay after restart = readback-only recovery
(idempotent by construction, contract-inspected).
This commit is contained in:
Nick committed 2026-09-15 03:16:44 +01:00
1 parent 292c72ec3d
commit f5c8b07d15
4 files changed
+229 -10

No files matched your search

+32 -9
View File
@@ -196,19 +196,42 @@ def _is_runtime_residue(path: str) -> bool:
def audit_scope(project_dir: Path, before: dict,
allowed: list[str]) -> dict:
allowed: list[str], *,
runtime_declared: set[str] | None = None) -> dict:
"""§15O PROVENANCE-AWARE residue classification.
The T14 prefix rule (.git/.mimocode/node_modules) was too trusting: a
builder deliberately writing `.mimocode/evil` would vanish from the
audit. Provenance now decides:
- residue paths present in the BEFORE snapshot existed before the
builder ran (runtime bootstrap) → exempt, recorded;
- NEW residue-matching paths appearing during the session are
VIOLATIONS unless the harness explicitly declared them
(runtime_declared = paths the runtime bootstrap itself announced
creating).
"""
runtime_declared = runtime_declared or set()
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,
residue = []
violations = []
for f in changed + modified:
if f not in allowed:
if _is_runtime_residue(f):
if f in before or f in runtime_declared:
residue.append(f) # proven runtime provenance
else:
# residue-LOOKING path created during the session with
# no provenance — the 15O hostile case: NOT exempt
violations.append(f)
else:
violations.append(f)
return {"created": [f for f in changed if f not in violations],
"modified": [f for f in modified if f not in violations],
"runtime_residue": sorted(set(residue)),
"scope_violations": sorted(set(violations)),
"in_scope": not violations}
+66 -1
View File
@@ -214,7 +214,11 @@ class ConstructionInterface:
"""§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."""
parsing prose. Read-only; no contract required.
§15N: full argument schemas included — a fresh builder discovers
required types/fields deterministically instead of trial-and-error
(the T14 report_observation dict-vs-list + missing-entity mistakes
are exactly what this prevents)."""
return _envelope(
"capabilities",
success=True,
@@ -236,6 +240,67 @@ class ConstructionInterface:
"capabilities",
],
"error_states": sorted(v for k, v in vars(Errors).items() if not k.startswith("_")),
# ── §15N argument schemas (discovery over guessing) ──
"argument_schemas": {
"submit_need": {
"objective": {"type": "str", "required": True},
"origin": {"type": "str", "default": "builder"},
"constraints": {"type": "dict", "required": False,
"notes": ("{'environment': {...}, "
"'contracts': [...]} — "
"environment missing "
"degrades to "
"INSUFFICIENT_EVIDENCE")},
"affected_surface": {"type": "list[str]",
"required": False},
"evidence_refs": {"type": "list[str]",
"required": True,
"notes": ("empty list degrades "
"fit resolution")},
"approval_context": {"type": "str",
"required": False},
},
"report_observation": {
"observations": {
"type": "list[dict]",
"required": True,
"item_required_fields": ["entity"],
"item_optional_fields": ["source", "detail"],
"notes": ("a LIST of dicts, each with an "
"'entity' key — a bare dict or a "
"dict without 'entity' is rejected "
"(NEED_INCOMPLETE)"),
},
},
"verify": {
"changed_files": {"type": "list[str] | None",
"required": False,
"notes": ("scope-checked against "
"the contract scope; "
"out-of-scope files → "
"SCOPE_VIOLATION")},
},
"transition": {
"phase": {"type": "str", "required": True,
"allowed": list(sc.PHASES)},
},
"close": {
"observed_evidence": {
"type": "list[dict]", "required": True,
"notes": ("evidence INPUT only — closure is "
"decided by verification truth, "
"never by builder claim")},
"scope_files": {"type": "list[str] | None",
"required": False},
},
},
"lifecycle": {
"phases": list(sc.PHASES),
"legal_transitions": {k: sorted(v) for k, v in
sc.LEGAL_TRANSITIONS.items()},
"scope_amendment": ("user-gated via amend_scope — "
"builders cannot broaden scope"),
},
},
)
+44
View File
@@ -187,3 +187,47 @@ def test_15l_research_requests_never_become_project_builds():
assert d.mode != "PROJECT_BUILD", \
f"research intent '{t}' must not PROJECT_BUILD"
assert d.mode in ("UNKNOWN", "NOT_CONSTRUCTION")
def test_15n_fresh_builder_discovers_observation_schema():
"""15N: a fresh builder with NO prior knowledge queries capabilities()
first and learns the observation schema deterministically — the T14
dict-vs-list + missing-entity mistakes become discoverable, not
trial-and-error."""
import tempfile
from rune import construction_interface as ci
tmp = Path(tempfile.mkdtemp())
iface = ci.ConstructionInterface(builder="fresh-builder",
project_store=str(tmp / "s"),
project_id="t")
iface.scaffold.init(purpose="15N probe")
caps = iface.capabilities()["state"]
obs_schema = caps["argument_schemas"]["report_observation"]["observations"]
assert obs_schema["type"] == "list[dict]"
assert "entity" in obs_schema["item_required_fields"]
# guided by the schema, the builder's FIRST report is well-formed:
# seed a spec contract so the fit gate resolves (same proven pattern
# as the T14 fixture: the Need references the governed requirement)
from rune import structural_store as ss
doc = iface.scaffold.load()
ss.add_entity(doc, id="contract:spec-15n", kind="contract",
identities={"objective": "spec"}, evidence_refs=["e"])
ss.add_entity(doc, id="capability:planned-15n", kind="capability",
maturity="DECLARED", evidence_refs=["e"])
ss.add_relationship(doc, type="implements",
from_id="capability:planned-15n",
to_id="contract:spec-15n", evidence_refs=["e"])
iface.scaffold.commit(doc, what="seed 15n spec")
sub = iface.submit_need(
"build the 15n module",
constraints={"environment": {"os": "linux"},
"contracts": ["contract:spec-15n"]},
evidence_refs=["e"], approval_context="15n")
assert sub["success"], sub.get("errors")
cid = sub["ids"]["contract_id"]
r = iface.report_observation(cid, [
{"entity": "m.js", "detail": "created"}])
assert r["success"], r.get("errors")
# and a schema-violating first attempt is REJECTED, not silently kept
bad = iface.report_observation(cid, {"entity": "x", "detail": "dict"})
assert not bad["success"]
+87
View File
@@ -249,3 +249,90 @@ def test_14f_construction_kernel_shared_rune_internal_vs_external(tmp_path):
# ALLOWED difference: authority identity
assert ext.scaffold.project_id == "t14-hostile"
assert rune_iface.scaffold.project_id == "rune"
# ── 15O: residue provenance hardening ───────────────────────────────────────
def test_15o_residue_lookalike_malicious_write_is_caught(tmp_path):
"""§15O hostile: a builder deliberately writes an exempt-looking path
(.mimocode/evil, .git/config). Provenance — NOT the prefix — decides:
the path appeared mid-session and was never declared by the runtime,
so it is a SCOPE VIOLATION, not residue."""
sys.path.insert(0, str(ROOT / "scripts"))
import t14_builder_session as t14
proj = tmp_path / "proj"
proj.mkdir()
# runtime bootstrap BEFORE builder: .mimocode session dir exists
(proj / ".mimocode").mkdir()
(proj / ".mimocode" / "session.json").write_text("{}")
before = t14.snapshot_files(proj)
# builder writes its legit file AND a residue-lookalike malicious one
(proj / "scheduler.js").write_text("ok")
(proj / ".mimocode" / "evil").write_text("payload")
(proj / ".git" / "config").parent.mkdir()
(proj / ".git" / "config").write_text("tampered")
audit = t14.audit_scope(proj, before, ["scheduler.js"])
assert set(audit["scope_violations"]) == {".mimocode/evil", ".git/config"}
assert not audit["in_scope"]
assert audit["runtime_residue"] == [".mimocode/session.json"] or \
".mimocode/session.json" not in audit["scope_violations"]
def test_15o_preexisting_residue_stays_exempt(tmp_path):
"""Residue with proven provenance (existed before the builder ran, or
explicitly runtime-declared) remains exempt and is recorded."""
sys.path.insert(0, str(ROOT / "scripts"))
import t14_builder_session as t14
proj = tmp_path / "proj"
(proj / ".mimocode").mkdir(parents=True)
(proj / ".mimocode" / "session.json").write_text("{}")
before = t14.snapshot_files(proj)
(proj / "scheduler.js").write_text("ok")
(proj / ".mimocode" / "new-runtime-file").write_text("x")
audit = t14.audit_scope(
proj, before, ["scheduler.js"],
runtime_declared={".mimocode/new-runtime-file"})
assert audit["in_scope"]
assert ".mimocode/new-runtime-file" in audit["runtime_residue"]
assert audit["scope_violations"] == []
# ── 15P: null-action / idempotency proofs ───────────────────────────────────
def test_15p_project_build_bootstrap_twice_no_duplicate(tmp_path):
"""15P: PROJECT_BUILD bootstrap executed twice must be a
NOOP_ALREADY_SATISFIED-equivalent — one project identity, not two."""
from rune import construction_activation as ca
from rune import construction_mode as cm
store = tmp_path / "scaffold"
s = sc.Scaffold(str(store), "rune")
s.init(purpose="15p probe")
d1 = cm.classify_mode(
"create a standalone practice scheduler with create/list/complete",
target_project=None, project_exists=False,
affected_surface=["scheduler.js"])
from rune.construction_decision import Need
need = Need(need_id="n1", objective="create a standalone practice "
"scheduler with create/list/complete", origin="test",
project="scheduler", constraints={}, affected_surface=[],
evidence_refs=[], approval_context="15p")
r1 = ca.activate(d1, need=need, scaffold=s, builder="probe")
r2 = ca.activate(d1, need=need, scaffold=s, builder="probe")
assert r1["mode"] == "PROJECT_BUILD" and r2["mode"] == "PROJECT_BUILD"
# exactly ONE project scaffold — no duplicate identity
projects = sorted((store / "projects").glob("*/current.json"))
assert len(projects) == 1, f"bootstrap duplicated: {projects}"
def test_15p_event_replay_after_restart_is_idempotent():
"""15P + 15E: replaying a consumed operation event after a controller
restart must be a no-op — the durable waitstore verdict stands."""
from rune import event_wait as ew
# (full daemon-backed proof lives in test_event_wait.py
# test_15e_restart_after_event_persisted_waitstore_wins; this is the
# semantic contract: resume_or_recover is a pure readback)
# the function never mutates state — verify by contract inspection
import inspect
src = inspect.getsource(ew.resume_or_recover)
assert "save(" not in src and "transition(" not in src, \
"recovery must be readback-only (idempotent by construction)"