292 lines
13 KiB
Python
292 lines
13 KiB
Python
"""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
|