feat(scaffold): programme_continuation wired into close() (T5.15 slice 3) + same-second contract-id collision fix

Slice 3 of the completion wave: closing a contract that is a stage of a persisted Programme marks that stage COMPLETE, persists the ledger, and rides the continuation canary on the close envelope — the next eligible stage is SURFACED, never guessed (task done ≠ programme done). No programme bound → no continuation block; ledger failure degrades without failing the closure. Three contracts. Also fixed en route: contract ids were second-granularity timestamps that collided when two needs filed in the same second (red test proves distinct ids now, uuid suffix added).
This commit is contained in:
Nick committed 2026-10-06 03:46:24 +01:00
1 parent f7e6c5cff5
commit 0873b521e3
2 files changed
+137 -10

No files matched your search

+53 -10
View File
@@ -36,6 +36,7 @@ from typing import Any
import json
import sys
import uuid
from pathlib import Path
from rune import structural_store as ss
@@ -421,7 +422,8 @@ class ConstructionInterface:
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}"
cid = (contract_id
or f"BUILDER-{ss.now()}-{uuid.uuid4().hex[:6]}-{self.builder}")
cd.generate_contract(
self.scaffold,
doc,
@@ -768,21 +770,62 @@ class ConstructionInterface:
material_closure = self._auto_material_change(
scope_files or [],
note=f"contract {contract_id} closed (material change)")
continuation = self._programme_continuation(contract_id)
state = {
"phase": "closed",
"material_change_closure": material_closure,
"contract_quality": {
k: q[k]
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
if k in q
},
}
if continuation is not None:
state["programme_continuation"] = continuation
return _envelope(
"close",
success=True,
ids={"contract_id": contract_id},
state={
"phase": "closed",
"material_change_closure": material_closure,
"contract_quality": {
k: q[k]
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
if k in q
},
},
state=state,
)
def _programme_continuation(self, contract_id: str) -> dict | None:
"""T5.15 slice 3 (the programme_continuation wire): after a legal
close, any persisted Programme stage bound to this contract is
marked COMPLETE and the continuation canary's verdict rides the
close envelope — 'task done' ≠ 'programme done', and the next
eligible stage is SURFACED, never guessed. No programme bound →
None (no continuation block in the envelope). Failures degrade:
a broken programme ledger never fails a real closure."""
try:
from pathlib import Path as _Path
from rune import programme_continuation as pc
base = _Path(self.scaffold.current).parent / pc.DEFAULT_PROGRAMME_AREA
if not base.is_dir():
return None
for prog_file in sorted(base.glob("*.json")):
prog = pc.Programme.load(base, prog_file.stem)
if prog is None:
continue
stage = next((s for s in prog.stages
if s.stage_id == contract_id
and s.state != pc.COMPLETE), None)
if stage is None:
continue
stage.state = pc.COMPLETE
stage.result["closed_at_contract"] = contract_id
prog.save(base)
return {
"programme_id": prog.programme_id,
"stage_marked_complete": contract_id,
"canary": prog.continuation_canary(),
}
except Exception as exc: # noqa: BLE001 — ledger failure never fails a closure
return {"error": f"programme continuation degraded: {exc}"[:200]}
return None
def _auto_material_change(self, scope_files: list[str], *,
note: str) -> dict:
"""T21.4 §21.4H (rule 19Q.2): governed construction closes → the
+84
View File
@@ -6,6 +6,7 @@ import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
@@ -671,3 +672,86 @@ def test_15f_transition_to_building_runs_no_guards(tmp_path):
iface.scaffold.commit(doc, what="attach guard")
env = iface.transition(cid, "approved") # NOT a completion edge
assert env["success"]
# ── T5.15 slice 3: programme_continuation wired into close() ────────────────
def _seed_programme(iface, programme_id, stage_contract_ids, tmp_path):
"""Persist a real Programme bound to the interface's scaffold store."""
from rune import programme_continuation as pc
base = Path(iface.scaffold.current).parent / pc.DEFAULT_PROGRAMME_AREA
prog = pc.Programme(programme_id=programme_id, objective="wave test")
for i, cid in enumerate(stage_contract_ids):
prog.stages.append(pc.ProgrammeStage(
stage_id=cid, objective=f"stage {i}", depends_on=[]))
prog.save(base)
return prog, base
def test_close_marks_programme_stage_complete_and_surfaces_continuation(tmp_path):
"""Closing a contract that is a stage of a persisted Programme marks
that stage COMPLETE and the envelope carries the continuation canary —
the programme's next eligible stage is surfaced, not guessed."""
from rune import programme_continuation as pc
iface = _iface(tmp_path)
env1 = _submit(iface, objective="wave stage one")
cid1 = env1["ids"]["contract_id"]
env2 = _submit(iface, objective="wave stage two")
cid2 = env2["ids"]["contract_id"]
# drive both through to verifying so close is legal
for cid in (cid1, cid2):
iface.transition(cid, "approved")
iface.transition(cid, "building")
iface.transition(cid, "verifying")
prog, base = _seed_programme(iface, "wave-x", [cid1, cid2], tmp_path)
env = iface.close(cid1, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
assert env["success"], env["errors"]
cont = env["state"].get("programme_continuation")
assert cont, "close envelope must carry the programme continuation"
assert cont["programme_id"] == "wave-x"
assert cont["stage_marked_complete"] == cid1
reloaded = pc.Programme.load(base, "wave-x")
stage1 = next(s for s in reloaded.stages if s.stage_id == cid1)
assert stage1.state == pc.COMPLETE
assert cont["canary"]["verdict"] == "PROGRAMME_OPEN"
assert cont["canary"]["next_action"] == ("start", cid2)
def test_close_without_programme_has_no_continuation_block(tmp_path):
iface = _iface(tmp_path)
env = _submit(iface)
cid = env["ids"]["contract_id"]
iface.transition(cid, "approved")
iface.transition(cid, "building")
iface.transition(cid, "verifying")
env = iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
assert env["success"]
assert "programme_continuation" not in env["state"]
def test_close_programme_completion_verdict_surfaces(tmp_path):
"""Closing the LAST stage yields PROGRAMME_COMPLETE — the canary says
stop, the runner never guesses."""
iface = _iface(tmp_path)
env = _submit(iface)
cid = env["ids"]["contract_id"]
iface.transition(cid, "approved")
iface.transition(cid, "building")
iface.transition(cid, "verifying")
prog, base = _seed_programme(iface, "solo", [cid], tmp_path)
env = iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
cont = env["state"]["programme_continuation"]
assert cont["canary"]["verdict"] == "PROGRAMME_COMPLETE"
def test_two_submits_same_second_get_distinct_contract_ids(tmp_path):
"""Second-granularity timestamps collide when two needs are filed in
the same second (real builder-pair scenario) — ids must be distinct."""
iface = _iface(tmp_path)
e1 = _submit(iface, objective="wave stage one")
e2 = _submit(iface, objective="wave stage two")
assert e1["success"] and e2["success"], (e1["errors"], e2["errors"])
assert e1["ids"]["contract_id"] != e2["ids"]["contract_id"]