§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.
527 lines
22 KiB
Python
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"
|