Files
rune/tests/test_construction_mode.py
Nick 08c782fe91 feat(scaffolding): contextual activation + PROJECT_BUILD bootstrap + project-existence authority + release-output gate repair (SCAFFOLD-TRANCHE-0011)
§0 RELEASE-OUTPUT GATE CORRECTION (audited against real Gitea/job logs):
T10's convergence refresh gated on vars.promoted — a repo CONFIG variable,
not a step output. Repo variables list is EMPTY, so the condition was
always false and the refresh NEVER ran: v0.66.47's promoted deploy (job
17917, PROMOTED + RUNTIME VERIFIED in log) executed zero refresh lines,
leaving the release without its deployed identity. CORRECTED:
- promotion step carries id: promotion_truth
- refresh gated on steps.promotion_truth.outputs.promoted == 'true'
  (the actual supported Gitea Actions step-output mechanism)
- deployed identity read from the deployment authority (build identity
  readback) with authority-vs-candidate agreement enforced before refresh
- recorded as process-defect:vars-vs-steps-output-syntax

§1 PROJECT-EXISTENCE AUTHORITY: T10 inferred exists=(project!='rune') —
invalid. Now _project_exists_evidence derives existence from structural
evidence only: Rune via Spine authority; external via bound Project
Scaffold entity; arbitrary names with no scaffold → False; missing
identity → None (UNKNOWN, never manufactured). Scoped change to a
proven-nonexistent project clarifies instead of claiming existing
semantics.

§3-§10 ACTIVATION DISPATCHER (construction_activation.py — NOT another
router/planner): dispatches the evidence-backed mode to the correct
lifecycle. NOT_CONSTRUCTION/UNKNOWN touch nothing (UNKNOWN returns probes,
never activates). LOCAL_CHANGE → bounded governance, no scaffold
bootstrap. EXISTING_PROJECT_CHANGE → attaches to existing authority.
PROJECT_BUILD → ACTUALLY BOOTSTRAPS: provisional Project Scaffold
initialized with explicit UNKNOWN fields (repository: undecided,
toolchain: unresolved, deployment: unknown, requirement gaps recorded) —
identity before repository, nothing manufactured, survives restart
(fresh-process resume test-proven). Rune-originated Needs derive
provisional project ids; same classifier, same dispatch.

§21 FORGOTTEN-SAVE PROCEDURAL REPAIR: Scaffold.commit() = mutation + save
+ readback as ONE boundary; accounting only AFTER commit. Regression test
proves mutation survives accounting failures.

Fixed mid-implementation: activation store-path hardcoded a relative repo
path (now derives from parent scaffold), init's returned doc was stale
(reload-after-persist discipline), ConstructionMode.from_dict added for
durable-trance dispatch.

Tests: 40 new (§1 existence, §6-13 activation/bootstrap/non-activation,
§17 resume, §18 Rune-originated, §21 transactional); suite 1662 green;
henxels rc=0; pyright clean.
2026-09-14 04:27:40 +01:00

527 lines
22 KiB
Python

"""test_construction_mode.py — SCAFFOLD-TRANCHE-0010: §19 classification
matrix, §20 context-sensitivity, §3 BUILD_NEW != PROJECT_BUILD, §23
override provenance, shared-context projection."""
import json
import os
import subprocess
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_mode as cm # noqa: E402
from rune import structural_store as ss # noqa: E402
from rune import scaffold as sc # noqa: E402
# ── §19 classification matrix ───────────────────────────────────────────────
def test_19a_typo_fix_is_local_change():
d = cm.classify_mode("fix this typo in the existing module", project_exists=True)
assert d.mode in ("LOCAL_CHANGE", "EXISTING_PROJECT_CHANGE")
assert d.mode != "PROJECT_BUILD"
def test_19b_add_csv_export_existing_app():
d = cm.classify_mode("add CSV export to this existing app", project_exists=True)
assert d.mode == "EXISTING_PROJECT_CHANGE"
def test_19c_schema_replacement_migration():
d = cm.classify_mode("replace the storage schema in this existing project", project_exists=True)
assert d.mode == "EXISTING_PROJECT_CHANGE"
def test_19d_breaking_api_change_stays_existing_project():
d = cm.classify_mode("make a breaking change to the existing public api", project_exists=True)
assert d.mode == "EXISTING_PROJECT_CHANGE"
def test_19e_standalone_scheduler_is_project_build():
d = cm.classify_mode("build me a new standalone practice scheduler app", project_exists=False)
assert d.mode == "PROJECT_BUILD"
assert d.bootstrap_required is True
def test_19f_new_web_service_is_project_build():
d = cm.classify_mode("create a new web service for session notes", project_exists=False)
assert d.mode == "PROJECT_BUILD"
def test_19g_build_confidence_not_construction():
d = cm.classify_mode("build my confidence with guitar improvisation")
assert d.mode == "NOT_CONSTRUCTION"
def test_19h_make_a_list_not_project_build():
d = cm.classify_mode("make me a list of chord substitutions")
assert d.mode == "NOT_CONSTRUCTION"
def test_19i_ambiguous_is_unknown_not_project_build():
d = cm.classify_mode("build something to help with this")
assert d.mode == "UNKNOWN"
assert d.clarification_required is True
assert d.probe_questions
def test_19j_capability_build_new_inside_rune_is_existing_project_change():
"""§3 THE KEY DISTINCTION: capability resolution may be BUILD_NEW while
construction mode remains EXISTING_PROJECT_CHANGE — new capability
INSIDE the existing Rune project."""
d = cm.classify_mode("implement a new parser module for Rune", target_project="rune", project_exists=True)
assert d.mode == "EXISTING_PROJECT_CHANGE"
assert d.mode != "PROJECT_BUILD"
# ── §20 context-sensitivity: same request, different structure ──────────────
def test_20_same_request_resolves_by_context():
obj = "add appointment booking"
a = cm.classify_mode(obj, target_project="scheduler-app", project_exists=True)
b = cm.classify_mode(obj, target_project=None, project_exists=False)
assert a.mode == "EXISTING_PROJECT_CHANGE"
assert b.mode in ("UNKNOWN", "PROJECT_BUILD")
assert a.mode != b.mode
def test_20b_existing_project_contradicts_standalone_language():
"""§5: project evidence wins over standalone wording — contradictory
evidence → UNKNOWN with clarification, never silent PROJECT_BUILD."""
d = cm.classify_mode("build a standalone export tool", target_project="rune", project_exists=True)
assert d.mode == "UNKNOWN"
assert d.clarification_required is True
# ── UNKNOWN discipline ───────────────────────────────────────────────────────
def test_unknown_never_silently_escalates():
for obj in ("build something to help with this", "create a thing for the workflow", "implement improvements"):
d = cm.classify_mode(obj)
assert d.mode in ("UNKNOWN",)
assert d.clarification_required
def test_empty_objective_unknown():
d = cm.classify_mode("")
assert d.mode == "UNKNOWN"
assert d.probe_questions
# ── §23 manual override ──────────────────────────────────────────────────────
def test_23_override_from_unknown():
d = cm.classify_mode("build something to help with this")
assert d.mode == "UNKNOWN"
forced = cm.classify_mode(
"build something to help with this", explicit_override="PROJECT_BUILD", override_authority="user"
)
assert forced.mode == "PROJECT_BUILD"
assert forced.override == {"authority": "user", "from_mode": None}
assert forced.bootstrap_required is True
def test_23_override_to_existing_project_validated():
d = cm.classify_mode("build a standalone export tool", project_exists=True)
assert d.mode == "UNKNOWN"
forced = cm.classify_mode(
"build a standalone export tool",
project_exists=True,
target_project="rune",
explicit_override="EXISTING_PROJECT_CHANGE",
override_authority="operator",
)
assert forced.mode == "EXISTING_PROJECT_CHANGE"
assert forced.override["authority"] == "operator"
def test_23_override_invalid_mode_rejected():
with pytest.raises(ValueError):
cm.classify_mode("anything", explicit_override="MAGIC_MODE")
# ── model proposal boundary ──────────────────────────────────────────────────
def test_model_proposal_recorded_never_authority():
prop = {"pattern_id": "x", "provider": "p", "confidence": 0.4}
d = cm.classify_mode("fix this typo in the existing module", project_exists=True, model_proposal=prop)
assert d.mode == "LOCAL_CHANGE" # deterministic evidence decided
assert d.proposed_by_model == prop # recorded, not governing
# ── to_dict roundtrip ────────────────────────────────────────────────────────
def test_mode_decision_serializable():
d = cm.classify_mode("add CSV export to this existing app", target_project="garden", project_exists=True)
blob = d.to_dict()
assert blob["mode"] == "EXISTING_PROJECT_CHANGE"
assert blob["target_project"] == "garden"
assert isinstance(blob["evidence"], list)
# ── Tranche 10: interface consumers + shared context ────────────────────────
def test_interface_classify_equivalence(tmp_path):
"""§24: ONE classifier — the Construction Interface and the direct
call produce identical semantics for identical input."""
from rune import construction_interface as ci
iface = ci.ConstructionInterface(
builder="mimo", project_store=str(tmp_path / ".rune" / "scaffold"), project_id="fx"
)
iface.scaffold.init(purpose="t")
via_interface = iface.classify("build me a new standalone practice scheduler app", project_exists=False)
direct = cm.classify_mode("build me a new standalone practice scheduler app", project_exists=False)
assert via_interface["state"]["mode"] == direct.mode == "PROJECT_BUILD"
# builder identity recorded, semantics unchanged
assert via_interface["state"]["builder"] == "mimo"
def test_interface_classify_unknown_carries_probes(tmp_path):
from rune import construction_interface as ci
iface = ci.ConstructionInterface(
builder="mimo", project_store=str(tmp_path / ".rune" / "scaffold"), project_id="fx"
)
iface.scaffold.init(purpose="t")
env = iface.classify("build something to help with this")
assert env["state"]["mode"] == "UNKNOWN"
assert env["state"]["probe_questions"]
assert not env["success"] # UNKNOWN is a non-success that asks
def test_construction_context_projection(tmp_path):
from rune import construction_interface as ci
iface = ci.ConstructionInterface(builder="b", project_store=str(tmp_path / ".rune" / "scaffold"), project_id="fx")
iface.scaffold.init(purpose="t")
# seed the structural truth the Need's fit requires
from rune import structural_store as ss
doc0 = iface.scaffold.load()
ss.add_entity(
doc0, id="capability:exporter", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"]
)
ss.add_entity(doc0, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
ss.add_relationship(
doc0, type="implements", from_id="capability:exporter", to_id="contract:csv_export", evidence_refs=["f"]
)
iface.scaffold.save(doc0)
# no active construction
empty = iface.construction_context()
assert empty["state"]["active"] is False
# with an active contract
env = iface.submit_need(
"persistent project export",
constraints={"contracts": ["contract:csv_export"]},
affected_surface=["src/export.py"],
evidence_refs=["x"],
approval_context="covered",
)
cid = env["ids"]["contract_id"]
ctx = iface.construction_context(cid)
st = ctx["state"]
assert st["active"] is True
assert st["need"] == "persistent project export"
assert st["phase"] == "draft"
assert st["scope"] == ["src/export.py"]
assert st["next_action"] == "review for approval"
def test_construction_mode_in_decision_trace(tmp_path):
"""make_decision carries the contextual mode — ONE classifier behind
both the interface and the decision path."""
from rune import construction_interface as ci
iface = ci.ConstructionInterface(builder="b", project_store=str(tmp_path / ".rune" / "scaffold"), project_id="fx")
iface.scaffold.init(purpose="t")
doc = iface.scaffold.load()
from rune import structural_store as ss
ss.add_entity(
doc, id="capability:exporter", kind="capability", identities={}, maturity="WIRED", evidence_refs=["f"]
)
ss.add_entity(doc, id="contract:csv_export", kind="contract", identities={}, evidence_refs=["f"])
ss.add_relationship(
doc, type="implements", from_id="capability:exporter", to_id="contract:csv_export", evidence_refs=["f"]
)
iface.scaffold.save(doc)
env = iface.submit_need(
"persistent project export",
constraints={"contracts": ["contract:csv_export"]},
affected_surface=[],
evidence_refs=["x"],
approval_context="covered",
)
cid = env["ids"]["contract_id"]
doc2 = iface.scaffold.load()
c = iface.scaffold.contract(doc2, cid)
mode = c["ext"]["decision_trace"]["construction_mode"]
assert mode["mode"] == "EXISTING_PROJECT_CHANGE" # rune-internal project
# ── Tranche 11 §1: project-existence authority ──────────────────────────────
def test_11_rune_project_exists():
from rune import construction_decision as cd
exists, ev = cd._project_exists_evidence("rune", {})
assert exists is True
assert any("Rune Spine" in e for e in ev)
def test_11_arbitrary_name_no_scaffold_is_false():
from rune import construction_decision as cd
doc = ss.new_doc() # empty authority — no project:foo anywhere
exists, ev = cd._project_exists_evidence("foo", {"spine": doc})
assert exists is False
assert any("no structural evidence" in e for e in ev)
def test_11_bound_scaffold_entity_is_true():
from rune import construction_decision as cd
doc = ss.new_doc()
ss.add_entity(doc, id="project:scheduler", kind="project",
identities={"purpose": "scheduling"}, evidence_refs=["e"])
exists, ev = cd._project_exists_evidence("scheduler", {"scaffold": doc})
assert exists is True
assert any("project:scheduler" in e for e in ev)
def test_11_missing_project_identity_is_unknown():
from rune import construction_decision as cd
exists, ev = cd._project_exists_evidence(None, {})
assert exists is None
assert any("UNKNOWN" in e for e in ev)
def test_11_classifier_receives_evidence_not_invention():
"""make_decision must pass STRUCTURAL existence to the classifier —
an arbitrary project name with no scaffold reads exists=False, never
invented True. Confirmed-nonexistence + standalone language is
CONSISTENT evidence → PROJECT_BUILD (T10 §19e matrix holds)."""
from rune import construction_decision as cd
need = cd.Need("x", "build a standalone export tool", project="foo",
constraints={}, affected_surface=[], evidence_refs=["e"],
approval_context="c")
doc = ss.new_doc() # empty: project:foo does NOT exist
trace = cd.make_decision(need, authorities={"spine": doc},
project_toolchain={"existing_language": "python"})
mode = trace["construction_mode"]
assert trace["project_existence"]["exists"] is False # evidenced, not invented
assert mode["mode"] == "PROJECT_BUILD" # consistent: standalone + no existing project
assert any("no structural evidence" in e
for e in mode["evidence"]) # evidence carried through
def test_11_scoped_change_against_nonexistent_project_does_not_manufacture():
"""The REAL manufactured-existence danger: a change scoped to a project
that has NO structural evidence must not silently claim
EXISTING_PROJECT_CHANGE against it — the scope evidence is contradicted
by proven non-existence, so UNKNOWN/clarify (T10 §5)."""
from rune import construction_decision as cd
need = cd.Need("y", "add CSV export to the project foo",
project="foo",
constraints={}, affected_surface=[], evidence_refs=["e"],
approval_context="c")
doc = ss.new_doc() # project:foo does NOT exist anywhere
trace = cd.make_decision(need, authorities={"spine": doc},
project_toolchain={"existing_language": "python"})
assert trace["project_existence"]["exists"] is False
mode = trace["construction_mode"]
assert mode["mode"] in ("UNKNOWN", "PROJECT_BUILD")
if mode["mode"] == "EXISTING_PROJECT_CHANGE":
pytest.fail("manufactured existence: scoped change to a proven-"
"nonexistent project claimed existing-project semantics")
def test_11_scoped_change_to_existing_project_is_existing_change():
from rune import construction_decision as cd
doc = ss.new_doc()
ss.add_entity(doc, id="project:scheduler", kind="project",
identities={"purpose": "scheduling"}, evidence_refs=["e"])
need = cd.Need("z", "add CSV export to the project scheduler",
project="scheduler",
constraints={}, affected_surface=[], evidence_refs=["e"],
approval_context="c")
trace = cd.make_decision(need, authorities={"spine": doc},
project_toolchain={"existing_language": "python"})
assert trace["project_existence"]["exists"] is True
assert trace["construction_mode"]["mode"] == "EXISTING_PROJECT_CHANGE"
# ── Tranche 11 §6-13: activation dispatcher + bootstrap + non-activation ────
from rune import construction_activation as ca # noqa: E402
from rune import construction_decision as cd # noqa: E402
def _need(project=None, obj="build a standalone export tool"):
return cd.Need("n", obj, project=project, constraints={},
affected_surface=[], evidence_refs=["e"],
approval_context="c")
def _mode(obj, project=None, exists=None):
return cm.classify_mode(obj, target_project=project,
project_exists=exists)
def _s(tmp_path):
s = sc.Scaffold(tmp_path / ".rune" / "scaffold", "rune")
s.init(purpose="rune")
return s
def test_project_build_bootstraps_scaffold(tmp_path):
"""§6/§24A: PROJECT_BUILD actually initializes a Project Scaffold with
explicit UNKNOWN fields — nothing manufactured."""
s = _s(tmp_path)
d = _mode("build a new standalone practice scheduler app", exists=False)
rec = ca.activate(d, need=_need("scheduler"), scaffold=s)
assert rec["activated"] and rec["lifecycle"] == "project_build"
ps = sc.Scaffold(tmp_path / ".rune" / "scaffold" / "projects" /
"scheduler", "scheduler")
pdoc = ps.load()
p = ss.entity(pdoc, "project:scheduler")
assert p is not None # scaffold initialized
ext = p["ext"]
assert ext["bootstrap"] == "initialized"
assert ext["repository"] == "undecided" # §7: identity before repo
assert ext["toolchain"] == "unresolved" # nothing manufactured
assert ext["deployment"] == "unknown"
def test_web_service_bootstraps_too(tmp_path):
s = _s(tmp_path)
d = _mode("create a new web service for session notes", exists=False)
rec = ca.activate(d, need=_need("session-notes"), scaffold=s)
assert rec["activated"]
assert (tmp_path / ".rune" / "scaffold" / "projects" /
"session-notes" / "current.json").exists()
def test_existing_change_never_bootstraps_new_project(tmp_path):
"""§24C: existing-project change must NOT create a new Project
Scaffold."""
s = _s(tmp_path)
d = _mode("add CSV export to this existing app", project="rune",
exists=True)
rec = ca.activate(d, need=_need("rune", obj=d.mode), scaffold=s)
assert rec["activated"] and rec["lifecycle"] == "proportional"
assert not (tmp_path / ".rune" / "scaffold" / "projects" /
"scheduler").exists()
def test_ambiguous_never_activates(tmp_path):
"""§24E/§9: UNKNOWN → no Project Scaffold, probes returned."""
s = _s(tmp_path)
d = _mode("build something to help with this")
assert d.mode == "UNKNOWN"
rec = ca.activate(d, need=_need(None, obj=d.mode and
"build something to help with this"),
scaffold=s)
assert rec["activated"] is False
assert rec["probe_questions"]
assert not (tmp_path / ".rune" / "scaffold" / "projects").exists()
def test_non_construction_touches_nothing(tmp_path):
"""§24F/§10: NOT_CONSTRUCTION → zero construction state."""
s = _s(tmp_path)
d = cm.classify_mode("make me a list of chord substitutions")
rec = ca.activate(d, need=_need(None, obj=d.mode and
"make me a list"), scaffold=s)
assert rec["activated"] is False
assert (tmp_path / ".rune" / "scaffold" / "projects").exists() is False
def test_same_name_with_scaffold_is_existing_change(tmp_path):
"""§24G: same project name WITH a bound scaffold → existing semantics."""
proj_dir = tmp_path / ".rune" / "scaffold" / "projects" / "scheduler"
ps = sc.Scaffold(proj_dir, "scheduler")
ps.init(purpose="scheduling")
d = _mode("add CSV export to the scheduler app", project="scheduler",
exists=True)
rec = ca.activate(d, need=_need("scheduler", obj=d.mode and
"add CSV export"), scaffold=_s(tmp_path))
assert rec["lifecycle"] == "proportional"
def test_same_name_without_evidence_not_manufactured_existing(tmp_path):
"""§24H: same project name WITHOUT structural evidence must not become
existing merely because a string was supplied."""
d = cm.classify_mode("add CSV export to the scheduler app",
target_project="scheduler", project_exists=False)
assert d.mode in ("UNKNOWN", "PROJECT_BUILD")
if d.mode == "EXISTING_PROJECT_CHANGE":
pytest.fail("manufactured existence from a bare string")
def test_dispatch_from_decision(tmp_path):
"""§15: dispatch reads the mode from the make_decision trace."""
s = _s(tmp_path)
need = _need("scheduler")
auth = {"spine": ss.new_doc()}
trace = cd.make_decision(need, authorities=auth,
project_toolchain={"existing_language":
"python"})
from rune.construction_mode import ConstructionMode
md = ConstructionMode.from_dict(trace["construction_mode"])
rec = ca.activate(md, need=need, scaffold=s)
assert rec["mode"] == trace["construction_mode"]["mode"]
def test_fresh_process_project_build_resume(tmp_path):
"""§17: PROJECT_BUILD survives restart — durable state recovers mode,
bootstrap, gaps, next action without transcript."""
s = _s(tmp_path)
d = _mode("build a new standalone practice scheduler app", exists=False)
rec = ca.activate(d, need=_need("scheduler"), scaffold=s)
assert rec["activated"]
# fresh process: only durable state
script = f'''
import sys, json
sys.path.insert(0, {os.path.join(os.path.dirname(__file__), "..", "src")!r})
from rune import scaffold as sc, structural_store as ss
ps = sc.Scaffold({str(tmp_path / ".rune" / "scaffold" / "projects" / "scheduler")!r},
"scheduler")
pdoc = ps.load()
p = ss.entity(pdoc, "project:scheduler")
ext = p["ext"]
print(json.dumps({{"purpose": p["identities"]["purpose"],
"bootstrap": ext["bootstrap"], "repository": ext["repository"],
"toolchain": ext["toolchain"], "gaps": ext["requirement_gaps"]}}))
'''
r = subprocess.run([sys.executable, "-c", script],
capture_output=True, text=True, timeout=60)
assert r.returncode == 0, r.stderr
recovered = json.loads(r.stdout)
assert recovered["bootstrap"] == "initialized"
assert recovered["repository"] == "undecided"
assert recovered["gaps"]
def test_rune_originated_need_same_classifier(tmp_path):
"""§18: Rune-originated Need — same classifier, same dispatch. The
origin is provenance, not a different path."""
need = cd.Need("rune-originated", "build a new standalone test corpus "
"manager", project=None,
origin="rune",
constraints={}, affected_surface=[],
evidence_refs=["approved task determined standalone "
"managed project warranted"],
approval_context="approval-gated: no consequential action")
d = cm.classify_mode(need.objective, project_exists=False)
assert d.mode == "PROJECT_BUILD"
rec = ca.activate(d, need=need, scaffold=_s(tmp_path))
assert rec["activated"] and rec["lifecycle"] == "project_build"