feat(structural_model): close PARTIAL gaps — dichotomy wave
Port the 11 audit-missing functions of src/rune/structural_model.py to the Rust crate (name-preserving, logic-1:1): workspace_record, workspace_advance, _init_project, save, reload, add, relate, state_owned, verification_surfaces, canonical_state_owner, supersession — plus a true deployment_surfaces (the old deployment_surfaces_for stub is gone, no callers), the lowercase-stale filters on dependencies/consumers, and the full §18F impact_closure shape the W6-era port had reduced. Fix-first: the delegation seam imported rune.sm_loader — the SHARD_MANAGER loader (no query symbol), so every kernel call died into the Python floor. The seam now targets stm_loader (the crate's sm_* FFI, RUNE_STM_CORE=python pins the floor). workspace_advance keeps the oracle's in-place law. Verification (verification/structural_model-summary.md): frozen truth 70 cases byte-identical (gen_structural_model_truth.py --verify), battery 16/16 with truth embedded via include_str!, live parity 4620/4620 (frozen replay + 300-doc seeded fuzz + dual-mode facade), clippy -D warnings clean, Python suites green in both pins (pre-existing 2F/12E verified identical on the stashed base). Audit: 9/20 PARTIAL -> 21/21 FULL.
This commit is contained in:
1 parent
1a1ac0b516
commit
00839d1af7
18 files changed
+15783
-160
No files matched your search
@@ -2,16 +2,16 @@
|
||||
|
||||
_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._
|
||||
|
||||
**FULL completion: 52%** (99/192 runtime modules at FULL)
|
||||
**FULL completion: 52%** (100/192 runtime modules at FULL)
|
||||
|
||||
Function-level detail: 69% (1569/2281 runtime functions matched) — secondary; includes in-flight work
|
||||
Function-level detail: 69% (1581/2282 runtime functions matched) — secondary; includes in-flight work
|
||||
|
||||
| Status | Modules | Lines | Meaning |
|
||||
|---|---|---|---|
|
||||
| FULL | 99 | 42060 | 80%+ of functions ported |
|
||||
| PARTIAL | 54 | 21012 | 20–80% — real logic exists, gaps named below |
|
||||
| FULL | 100 | 42653 | 80%+ of functions ported |
|
||||
| PARTIAL | 53 | 20624 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 39 | 11471 | no name-preserving implementation |
|
||||
| Total runtime | 192 | 74543 | |
|
||||
| Total runtime | 192 | 74748 | |
|
||||
|
||||
## NOT TOUCHED — the port queue, in priority order (largest first)
|
||||
|
||||
@@ -65,7 +65,6 @@ Function-level detail: 69% (1569/2281 runtime functions matched) — secondary;
|
||||
| memory_store | 2333 | 30/68 | _ctx_budget, now_iso, ensure_memory_type_column, _embed, _temp_json, _entities_to_json |
|
||||
| warm_router | 1518 | 17/33 | _ensure_use_history_table, record_outcome, record_neuron_fire, base_level_activation, _db, _aaak |
|
||||
| tool_executor | 1836 | 33/47 | _execute_tool_now, serialize_prepared_tool, deserialize_prepared_tool, confirm_write, _resolve_execution_cwd, _run_command |
|
||||
| structural_model | 388 | 9/20 | workspace_record, workspace_advance, _init_project, save, reload, add |
|
||||
| claim_audit | 484 | 4/14 | _get_warm_neuron_store, reframe, _graph_has, _warm_has, verify, journal |
|
||||
| token_optimizer | 514 | 16/25 | get_optimizer, _tc_core, _to_core, to_dict, _to_state, _apply_state |
|
||||
| builder_bridge | 326 | 3/11 | _construction_store, _active_construction_lines, _default_selector, _cached_memory, _get_selector, _retrieve_memory |
|
||||
@@ -142,6 +141,7 @@ Function-level detail: 69% (1569/2281 runtime functions matched) — secondary;
|
||||
| memory_selector | 609 | 14/14 | RUST |
|
||||
| system_registry | 603 | 14/16 | RUST |
|
||||
| work_manager | 594 | 25/25 | GO |
|
||||
| structural_model | 593 | 21/21 | RUST |
|
||||
| construction_intent | 580 | 13/16 | RUST |
|
||||
| identity_anchors | 575 | 14/14 | RUST |
|
||||
| capability_ledger | 571 | 18/18 | RUST |
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: structural_model Rust kernels vs Python oracle (dichotomy
|
||||
wave, issue #2479 tranche). Layers:
|
||||
|
||||
1. FROZEN — the frozen truth corpus (frozen_sm_truth.json, generated by
|
||||
scripts/gen_structural_model_truth.py against the LIVE oracle with
|
||||
RUNE_STM_CORE=python + RUNE_STRUCTURAL_CORE=python pinned) replayed
|
||||
through the ctypes FFI, case by case, deep-copied inputs; save() /
|
||||
_init_project() compared BYTE-EXACT on real files.
|
||||
2. FUZZ — 300 seeded random models (seed 2479): oracle floor vs FFI
|
||||
on every kernel, exact compare; save() byte-exact.
|
||||
3. LIVE — dual-mode facade battery: the same facade calls executed
|
||||
with RUNE_STM_CORE=python (oracle floor) and with the Rust core
|
||||
live; results must be identical (clock pinned).
|
||||
|
||||
Exit 0 only at 100% parity.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_STM_CORE"] = "python" # oracle floor for compare
|
||||
os.environ["RUNE_STRUCTURAL_CORE"] = "python" # the floor's store core
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import structural_model as sm # noqa: E402
|
||||
from rune import stm_loader # noqa: E402
|
||||
|
||||
FIXED_NOW = "2026-10-13T01:02:03Z"
|
||||
ss.now = lambda: FIXED_NOW # both sides stamp with the pinned clock
|
||||
|
||||
TRUTH = json.loads((REPO / "src" / "rust" / "structural_model" / "src" / "frozen_sm_truth.json").read_text())
|
||||
|
||||
fails: list[str] = []
|
||||
counts = {"frozen": 0, "fuzz": 0, "live": 0}
|
||||
|
||||
|
||||
def j(x):
|
||||
return json.dumps(x, sort_keys=True, ensure_ascii=True)
|
||||
|
||||
|
||||
def fail(layer, name, detail):
|
||||
fails.append(f"[{layer}] {name}: {detail}")
|
||||
print(f"FAIL[{layer}] {name}: {detail[:240]}")
|
||||
|
||||
|
||||
def check(layer, name, want, got):
|
||||
counts[layer] += 1
|
||||
if j(want) != j(got):
|
||||
fail(layer, name, f"want={j(want)[:280]} got={j(got)[:280]}")
|
||||
|
||||
|
||||
def receiver(doc, project_id="proj", path="/tmp/sm-parity/structural.json"):
|
||||
m = sm.StructuralModel.__new__(sm.StructuralModel)
|
||||
m.store_dir = None
|
||||
m.project_id = project_id
|
||||
m.path = path
|
||||
m.doc = doc
|
||||
return m
|
||||
|
||||
|
||||
def oracle_call(fn, a):
|
||||
"""The oracle floor (delegation pinned off) for one frozen case."""
|
||||
if fn in {
|
||||
"children",
|
||||
"beneath",
|
||||
"owning_domain",
|
||||
"dependencies",
|
||||
"consumers",
|
||||
"interfaces_for",
|
||||
"state_owned",
|
||||
"verification_surfaces",
|
||||
"deployment_surfaces",
|
||||
"canonical_state_owner",
|
||||
"supersession",
|
||||
"impact_closure",
|
||||
}:
|
||||
m = receiver(copy.deepcopy(a["doc"]))
|
||||
if fn == "impact_closure":
|
||||
return m.impact_closure(a["eid"], max_depth=a.get("max_depth", 8))
|
||||
return getattr(m, fn)(a["eid"])
|
||||
if fn == "add":
|
||||
m = receiver(copy.deepcopy(a["doc"]))
|
||||
return {
|
||||
"doc": m.doc,
|
||||
"eid": m.add(
|
||||
a["kind"],
|
||||
a["name"],
|
||||
parent=a.get("parent"),
|
||||
ext=a.get("ext"),
|
||||
evidence_ref=a.get("evidence_ref", "structural-model"),
|
||||
),
|
||||
}
|
||||
if fn == "relate":
|
||||
m = receiver(copy.deepcopy(a["doc"]))
|
||||
return {
|
||||
"doc": m.doc,
|
||||
"rel": m.relate(
|
||||
a["from_id"],
|
||||
a["rel"],
|
||||
a["to_id"],
|
||||
evidence_ref=a.get("evidence_ref", "structural-model"),
|
||||
authority=a.get("authority", "model-author"),
|
||||
status=a.get("status", "active"),
|
||||
ext=a.get("ext"),
|
||||
),
|
||||
}
|
||||
if fn == "workspace_record":
|
||||
return sm.workspace_record(
|
||||
contract_id=a["contract_id"],
|
||||
construction_base=a["construction_base"],
|
||||
runtime_generation=a.get("runtime_generation", ""),
|
||||
candidate_generation=a.get("candidate_generation", ""),
|
||||
promotion_target=a.get("promotion_target", "gitea/master"),
|
||||
rollback_generation=a.get("rollback_generation", ""),
|
||||
)
|
||||
if fn == "workspace_advance":
|
||||
record = copy.deepcopy(a["record"])
|
||||
return sm.workspace_advance(record, a["stage"], evidence=a.get("evidence", ""))
|
||||
raise AssertionError(fn)
|
||||
|
||||
|
||||
def ffi_call(fn, a):
|
||||
if fn in {
|
||||
"children",
|
||||
"beneath",
|
||||
"owning_domain",
|
||||
"dependencies",
|
||||
"consumers",
|
||||
"interfaces_for",
|
||||
"state_owned",
|
||||
"verification_surfaces",
|
||||
"deployment_surfaces",
|
||||
"canonical_state_owner",
|
||||
"supersession",
|
||||
"impact_closure",
|
||||
}:
|
||||
return stm_loader.query(copy.deepcopy(a["doc"]), a["eid"], fn, max_depth=a.get("max_depth"))
|
||||
if fn == "add":
|
||||
return stm_loader.add(
|
||||
copy.deepcopy(a["doc"]),
|
||||
a["kind"],
|
||||
a["name"],
|
||||
parent=a.get("parent"),
|
||||
ext=a.get("ext"),
|
||||
evidence_ref=a.get("evidence_ref", "structural-model"),
|
||||
now=FIXED_NOW,
|
||||
)
|
||||
if fn == "relate":
|
||||
return stm_loader.relate(
|
||||
copy.deepcopy(a["doc"]),
|
||||
a["from_id"],
|
||||
a["rel"],
|
||||
a["to_id"],
|
||||
evidence_ref=a.get("evidence_ref", "structural-model"),
|
||||
authority=a.get("authority", "model-author"),
|
||||
status=a.get("status", "active"),
|
||||
ext=a.get("ext"),
|
||||
now=FIXED_NOW,
|
||||
)
|
||||
if fn == "workspace_record":
|
||||
return stm_loader.workspace_record(
|
||||
contract_id=a["contract_id"],
|
||||
construction_base=a["construction_base"],
|
||||
runtime_generation=a.get("runtime_generation", ""),
|
||||
candidate_generation=a.get("candidate_generation", ""),
|
||||
promotion_target=a.get("promotion_target", "gitea/master"),
|
||||
rollback_generation=a.get("rollback_generation", ""),
|
||||
)
|
||||
if fn == "workspace_advance":
|
||||
return stm_loader.workspace_advance(copy.deepcopy(a["record"]), a["stage"], evidence=a.get("evidence", ""))
|
||||
raise AssertionError(fn)
|
||||
|
||||
|
||||
def call_side(call):
|
||||
"""Run a case body; errors become comparable markers."""
|
||||
try:
|
||||
return {"__ok__": call()}
|
||||
except (ValueError, KeyError) as exc:
|
||||
return {"__err__": f"{type(exc).__name__}: {exc}"}
|
||||
|
||||
|
||||
def frozen():
|
||||
for fn, cases in TRUTH["groups"].items():
|
||||
if fn in {"save", "reload", "init_project"}:
|
||||
continue # byte-law layer below
|
||||
for c in cases:
|
||||
a = copy.deepcopy(c["args"])
|
||||
want = call_side(lambda: oracle_call(fn, a))
|
||||
got = call_side(lambda: ffi_call(fn, a))
|
||||
check("frozen", f"{fn}/{c['name']}", want, got)
|
||||
|
||||
|
||||
def byte_law(tmp):
|
||||
"""save / _init_project / reload must be byte-exact through the FFI."""
|
||||
for c in TRUTH["groups"]["save"]:
|
||||
name = c["name"]
|
||||
m = receiver(copy.deepcopy(c["args"]["doc"]), path=tmp / f"py/{name}.json")
|
||||
m.save()
|
||||
want = {"content": Path(m.path).read_text(), "doc": m.doc}
|
||||
out = stm_loader.save(copy.deepcopy(c["args"]["doc"]), str(tmp / f"rs/{name}.json"))
|
||||
got = {"content": Path(out["path"]).read_text(), "doc": out["doc"]}
|
||||
check("frozen", f"save-bytes/{name}", want, got)
|
||||
c = TRUTH["groups"]["init_project"][0]
|
||||
m = sm.StructuralModel(tmp / "py-init", c["args"]["project_id"])
|
||||
want = {"content": Path(m.path).read_text(), "doc": m.doc}
|
||||
out = stm_loader.init_project(
|
||||
copy.deepcopy(c["args"]["doc"]), c["args"]["project_id"], str(tmp / "rs-init/structural.json"), FIXED_NOW
|
||||
)
|
||||
got = {"content": Path(out["path"]).read_text(), "doc": out["doc"]}
|
||||
check("frozen", "init_project-bytes", want, got)
|
||||
# reload: FFI doc equals the floor's reloaded doc, byte-exact source
|
||||
m2 = sm.StructuralModel(tmp / "py-init", c["args"]["project_id"])
|
||||
m2.reload()
|
||||
got_doc = stm_loader.reload(str(tmp / "py-init/structural.json"))
|
||||
check("frozen", "reload-roundtrip", m2.doc, got_doc)
|
||||
check("frozen", "reload-missing-none", None, stm_loader.reload(str(tmp / "nope.json")))
|
||||
|
||||
|
||||
# ── fuzz (seeded) ───────────────────────────────────────────────────────────
|
||||
|
||||
KINDS = list(sm.ENTITY_KINDS) + ["widget"]
|
||||
RELS = list(sm.RELATION_TYPES) + ["hates"]
|
||||
IDS = [
|
||||
"project:p",
|
||||
"domain:d",
|
||||
"component:a",
|
||||
"component:b",
|
||||
"interface:i",
|
||||
"state_store:s",
|
||||
"test_suite:t",
|
||||
"deployment_surface:ds",
|
||||
"environment:e",
|
||||
"component:old",
|
||||
"é-unit",
|
||||
"x" * 40,
|
||||
"",
|
||||
]
|
||||
STATUSES = [None, None, "active", "stale", "STALE", "CURRENT", "root"]
|
||||
|
||||
|
||||
def rand_rel(rnd):
|
||||
r = {
|
||||
"type": rnd.choice(RELS),
|
||||
"from": rnd.choice(IDS),
|
||||
"to": rnd.choice(IDS),
|
||||
"evidence_refs": sorted({f"ev-{rnd.randint(0, 2)}" for _ in range(rnd.randint(0, 2))}),
|
||||
"observed_at": FIXED_NOW,
|
||||
}
|
||||
st = rnd.choice(STATUSES)
|
||||
if st:
|
||||
r["status"] = st
|
||||
if rnd.random() < 0.2:
|
||||
r["authority"] = rnd.choice(["model-author", "importer", "user"])
|
||||
if rnd.random() < 0.15:
|
||||
r["ext"] = {"n": rnd.randint(0, 5)}
|
||||
return r
|
||||
|
||||
|
||||
def rand_doc(rnd):
|
||||
d = {"schema_version": 1, "generated_at": FIXED_NOW, "entities": [], "relationships": [], "observations": []}
|
||||
for _ in range(rnd.randint(0, 6)):
|
||||
e = {
|
||||
"id": rnd.choice(IDS) or f"component:f{rnd.randint(0, 8)}",
|
||||
"kind": rnd.choice(sm.ENTITY_KINDS),
|
||||
"maturity": rnd.choice(["DECLARED", "PRESENT", "WIRED", "VERIFIED"]),
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": sorted({f"ev-{rnd.randint(0, 2)}" for _ in range(rnd.randint(0, 2))}),
|
||||
"observed_at": FIXED_NOW,
|
||||
}
|
||||
if rnd.random() < 0.25:
|
||||
e["aliases"] = [f"component:old{rnd.randint(0, 3)}"]
|
||||
if rnd.random() < 0.2:
|
||||
e.pop("identities") # the KeyError crash-parity class
|
||||
d["entities"].append(e)
|
||||
for _ in range(rnd.randint(0, 8)):
|
||||
d["relationships"].append(rand_rel(rnd))
|
||||
return d
|
||||
|
||||
|
||||
def fuzz(tmp):
|
||||
rnd = random.Random(2479)
|
||||
for i in range(300):
|
||||
d = rand_doc(rnd)
|
||||
eid = rnd.choice(IDS) or "component:a"
|
||||
for fn in [
|
||||
"children",
|
||||
"beneath",
|
||||
"owning_domain",
|
||||
"dependencies",
|
||||
"consumers",
|
||||
"interfaces_for",
|
||||
"state_owned",
|
||||
"verification_surfaces",
|
||||
"deployment_surfaces",
|
||||
"canonical_state_owner",
|
||||
"supersession",
|
||||
]:
|
||||
want = call_side(lambda: oracle_call(fn, {"doc": d, "eid": eid}))
|
||||
got = call_side(lambda: ffi_call(fn, {"doc": d, "eid": eid}))
|
||||
check("fuzz", f"{fn}/{i}", want, got)
|
||||
depth = rnd.choice([1, 2, 8])
|
||||
want = call_side(lambda: oracle_call("impact_closure", {"doc": d, "eid": eid, "max_depth": depth}))
|
||||
got = call_side(lambda: ffi_call("impact_closure", {"doc": d, "eid": eid, "max_depth": depth}))
|
||||
check("fuzz", f"impact_closure/{i}", want, got)
|
||||
# mutations
|
||||
kind, name = rnd.choice(KINDS), f"n{i}"
|
||||
a = {
|
||||
"doc": d,
|
||||
"kind": kind,
|
||||
"name": name,
|
||||
"parent": rnd.choice([None, "domain:d"]),
|
||||
"ext": rnd.choice([None, {"k": i}, {}]),
|
||||
"evidence_ref": rnd.choice(["structural-model", "", f"ev-{i}"]),
|
||||
}
|
||||
want = call_side(lambda: oracle_call("add", a))
|
||||
got = call_side(lambda: ffi_call("add", a))
|
||||
check("fuzz", f"add/{i}", want, got)
|
||||
ra = {
|
||||
"doc": d,
|
||||
"from_id": rnd.choice(IDS),
|
||||
"rel": rnd.choice(RELS),
|
||||
"to_id": rnd.choice(IDS),
|
||||
"evidence_ref": rnd.choice(["structural-model", "ev"]),
|
||||
"authority": rnd.choice(["model-author", "importer"]),
|
||||
"status": rnd.choice(["active", "CURRENT"]),
|
||||
"ext": rnd.choice([None, {"z": 1}, {}]),
|
||||
}
|
||||
want = call_side(lambda: oracle_call("relate", ra))
|
||||
got = call_side(lambda: ffi_call("relate", ra))
|
||||
check("fuzz", f"relate/{i}", want, got)
|
||||
# lifecycle
|
||||
stages = rnd.choice([[], ["base_verified"], ["base_verified", "worktree_created"]])
|
||||
record = sm.workspace_record(contract_id=f"C{i}", construction_base=f"sha{i}")
|
||||
for s in stages:
|
||||
record = sm.workspace_advance(record, s)
|
||||
stage = rnd.choice(sm.WORKSPACE_LIFECYCLE_STAGES + ("bogus",))
|
||||
wa = {"record": record, "stage": stage, "evidence": rnd.choice(["", "e" * rnd.randint(1, 250)])}
|
||||
want = call_side(lambda: oracle_call("workspace_advance", wa))
|
||||
got = call_side(lambda: ffi_call("workspace_advance", wa))
|
||||
check("fuzz", f"workspace_advance/{i}", want, got)
|
||||
# save byte law on random docs
|
||||
if i % 6 == 0:
|
||||
m = receiver(copy.deepcopy(d), path=tmp / f"fz-py/{i}.json")
|
||||
try:
|
||||
m.save()
|
||||
want_c = Path(m.path).read_text()
|
||||
except ValueError as exc:
|
||||
want_c = f"__err__ {exc}"
|
||||
out = call_side(lambda: stm_loader.save(copy.deepcopy(d), str(tmp / f"fz-rs/{i}.json")))
|
||||
if isinstance(out.get("__ok__"), dict):
|
||||
got_c = Path(out["__ok__"]["path"]).read_text()
|
||||
else:
|
||||
got_c = f"__err__ {out.get('__err__', '')}"
|
||||
counts["fuzz"] += 1
|
||||
if want_c != got_c:
|
||||
fail("fuzz", f"save-bytes/{i}", f"py={want_c[:120]!r} rs={got_c[:120]!r}")
|
||||
|
||||
|
||||
# ── live dual-mode facade battery ───────────────────────────────────────────
|
||||
|
||||
FACADE_CODE = r"""
|
||||
import copy, json, os, sys, tempfile, calendar, time as _time, types
|
||||
sys.path.insert(0, "src")
|
||||
from rune import structural_store as ss
|
||||
FIXED = "2026-10-13T01:02:03Z"
|
||||
ss.now = lambda: FIXED
|
||||
from rune import structural_model as sm
|
||||
out = {}
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
m = sm.StructuralModel(td + "/store", "scaffolding")
|
||||
m.add("domain", "construction", parent="project:scaffolding")
|
||||
m.add("subsystem", "kernel", parent="domain:construction")
|
||||
m.add("component", "scaffold", parent="subsystem:kernel")
|
||||
m.add("component", "beta", parent="subsystem:kernel")
|
||||
m.add("interface", "api", parent="subsystem:kernel")
|
||||
m.add("state_store", "st", parent="project:scaffolding",
|
||||
ext={"engine": "sqlite"})
|
||||
m.add("test_suite", "kt", parent="project:scaffolding")
|
||||
m.add("deployment_surface", "live", parent="project:scaffolding")
|
||||
try:
|
||||
m.add("widget", "x")
|
||||
out["bad_kind"] = "NO RAISE"
|
||||
except ValueError as e:
|
||||
out["bad_kind"] = str(e)
|
||||
try:
|
||||
m.relate("component:scaffold", "hates", "component:beta")
|
||||
out["bad_rel"] = "NO RAISE"
|
||||
except ValueError as e:
|
||||
out["bad_rel"] = str(e)
|
||||
m.relate("component:scaffold", "provides", "interface:api")
|
||||
m.relate("component:beta", "consumes", "interface:api")
|
||||
m.relate("component:beta", "depends_on", "component:scaffold")
|
||||
m.relate("component:scaffold", "owns", "state_store:st")
|
||||
m.relate("component:scaffold", "verified_by", "test_suite:kt")
|
||||
m.relate("component:scaffold", "deployed_by", "deployment_surface:live")
|
||||
m.relate("component:scaffold", "supersedes", "component:beta")
|
||||
m.relate("component:beta", "legacy_of", "component:scaffold")
|
||||
m.relate("component:scaffold", "reads", "state_store:st",
|
||||
authority="importer", status="CURRENT", ext={"mode": "ro"})
|
||||
m.save()
|
||||
out["doc"] = m.doc
|
||||
out["file_sha"] = __import__("hashlib").sha256(
|
||||
open(td + "/store/structural.json", "rb").read()).hexdigest()
|
||||
m2 = sm.StructuralModel(td + "/store", "scaffolding")
|
||||
m2.reload()
|
||||
out["reload_same"] = m2.doc == m.doc
|
||||
eid = "component:scaffold"
|
||||
for q in ["children", "beneath", "owning_domain", "dependencies",
|
||||
"consumers", "interfaces_for", "state_owned",
|
||||
"verification_surfaces", "deployment_surfaces",
|
||||
"canonical_state_owner", "supersession"]:
|
||||
out[q] = getattr(m, q)(eid)
|
||||
out["closure"] = m.impact_closure(eid)
|
||||
out["closure_root"] = m.impact_closure("project:scaffolding")
|
||||
out["closure_unknown"] = m.impact_closure("nope")
|
||||
rec = sm.workspace_record(contract_id="WC", construction_base="sha-base")
|
||||
out["record"] = rec
|
||||
# snapshot at capture: the ORACLE mutates the record in place and
|
||||
# returns the same dict — a later call through the same reference
|
||||
# would alias into the captured result (Python-only artifact, not a
|
||||
# kernel divergence; the kernel returns fresh dicts)
|
||||
out["advance"] = copy.deepcopy(
|
||||
sm.workspace_advance(rec, "base_verified", evidence="seed"))
|
||||
out["advance2"] = copy.deepcopy(
|
||||
sm.workspace_advance(rec, "worktree_created"))
|
||||
try:
|
||||
sm.workspace_advance(out["advance2"], "pushed")
|
||||
out["advance_err"] = "NO RAISE"
|
||||
except ValueError as e:
|
||||
out["advance_err"] = str(e)
|
||||
print(json.dumps(out, sort_keys=True))
|
||||
"""
|
||||
|
||||
|
||||
def facade_battery():
|
||||
def run(tag):
|
||||
env = dict(os.environ)
|
||||
env.pop("RUNE_STM_CORE", None)
|
||||
env.pop("RUNE_STRUCTURAL_CORE", None)
|
||||
if tag == "python":
|
||||
env["RUNE_STM_CORE"] = "python"
|
||||
env["RUNE_STRUCTURAL_CORE"] = "python"
|
||||
r = subprocess.run(
|
||||
[sys.executable, "-c", FACADE_CODE], capture_output=True, text=True, cwd=str(REPO), env=env, timeout=180
|
||||
)
|
||||
if r.returncode != 0:
|
||||
fail("live", tag, r.stderr[-400:])
|
||||
return None
|
||||
return json.loads(r.stdout)
|
||||
|
||||
py = run("python")
|
||||
rs = run("rust")
|
||||
if py is None or rs is None:
|
||||
return
|
||||
check("live", "dual-mode-facade", py, rs)
|
||||
|
||||
|
||||
def main():
|
||||
if not stm_loader.available():
|
||||
print("Rust core not built — run cargo build --release in src/rust/structural_model")
|
||||
return 2
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
frozen()
|
||||
byte_law(Path(td))
|
||||
fuzz(Path(td))
|
||||
facade_battery()
|
||||
total = sum(counts.values())
|
||||
print(
|
||||
f"\nPARITY: {total} checks, {len(fails)} failures "
|
||||
f"(frozen={counts['frozen']} fuzz={counts['fuzz']} live={counts['live']})"
|
||||
)
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,397 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze the dichotomy-wave gap-closing truth corpus for
|
||||
src/rune/structural_model.py (issue #2479 tranche).
|
||||
|
||||
LAW (poisoned-truth): RUNE_STM_CORE=python and RUNE_STRUCTURAL_CORE=python
|
||||
are pinned at the top — truth is generated by the PYTHON ORACLE ONLY.
|
||||
Inputs are deep-copied before every oracle call (no leakage between
|
||||
cases). The clock is pinned by monkeypatching structural_store.now (the
|
||||
no-clock law on the Rust side: `now` is always an argument).
|
||||
|
||||
The frozen corpus is embedded VERBATIM in the crate's cfg(test) battery
|
||||
(src/frozen_battery.rs, include_str!) and replayed by
|
||||
scripts/check_structural_model_parity.py through the live FFI.
|
||||
|
||||
--verify regenerates from the live oracle and byte-compares the frozen
|
||||
file (layer 3 of the verification ladder: truth consumed, not
|
||||
self-generated).
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_STM_CORE"] = "python" # poisoned-truth law
|
||||
os.environ["RUNE_STRUCTURAL_CORE"] = "python" # the floor's store core too
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune import structural_model as sm # noqa: E402
|
||||
|
||||
FIXED_NOW = "2026-10-13T01:02:03Z"
|
||||
ss.now = lambda: FIXED_NOW # pin the clock the floor's add/save stamps use
|
||||
|
||||
OUT = REPO / "src" / "rust" / "structural_model" / "src" / "frozen_sm_truth.json"
|
||||
|
||||
groups: dict[str, list] = {}
|
||||
|
||||
|
||||
def case(group, name, args, expect):
|
||||
groups.setdefault(group, []).append({"fn": group, "name": name, "args": args, "expect": expect})
|
||||
|
||||
|
||||
def receiver(doc, project_id="proj", path="/tmp/sm-frozen/structural.json"):
|
||||
"""A StructuralModel with a pre-set doc — no filesystem in __init__."""
|
||||
m = sm.StructuralModel.__new__(sm.StructuralModel)
|
||||
m.store_dir = None
|
||||
m.project_id = project_id
|
||||
m.path = path
|
||||
m.doc = doc
|
||||
return m
|
||||
|
||||
|
||||
def ent(id, kind="component", maturity="DECLARED", **kw):
|
||||
e = {
|
||||
"id": id,
|
||||
"kind": kind,
|
||||
"maturity": maturity,
|
||||
"tags": [],
|
||||
"identities": {},
|
||||
"evidence_refs": [],
|
||||
"observed_at": FIXED_NOW,
|
||||
}
|
||||
e.update(kw)
|
||||
return e
|
||||
|
||||
|
||||
def rel(t, f, to, **kw):
|
||||
r = {"type": t, "from": f, "to": to, "evidence_refs": ["ev"], "observed_at": FIXED_NOW}
|
||||
r.update(kw)
|
||||
return r
|
||||
|
||||
|
||||
def fixture_doc():
|
||||
"""The Scaffolding-shaped reference model (entities + every rel class)."""
|
||||
d = {
|
||||
"schema_version": 1,
|
||||
"generated_at": FIXED_NOW,
|
||||
"entities": [
|
||||
ent("project:proj", "project"),
|
||||
ent("domain:core", "domain"),
|
||||
ent("component:alpha", "component"),
|
||||
ent("component:beta", "component"),
|
||||
ent("component:gamma", "component", aliases=["component:old"]),
|
||||
ent("interface:api", "interface"),
|
||||
ent("state_store:s1", "state_store"),
|
||||
ent("state_store:s2", "state_store"),
|
||||
ent("test_suite:t1", "test_suite"),
|
||||
ent("deployment_surface:d1", "deployment_surface"),
|
||||
],
|
||||
"relationships": [
|
||||
rel("belongs_to", "domain:core", "project:proj"),
|
||||
rel("belongs_to", "component:alpha", "domain:core"),
|
||||
rel("belongs_to", "component:beta", "domain:core"),
|
||||
rel("belongs_to", "interface:api", "domain:core"),
|
||||
rel("belongs_to", "state_store:s1", "project:proj"),
|
||||
rel("belongs_to", "test_suite:t1", "project:proj"),
|
||||
rel("belongs_to", "deployment_surface:d1", "project:proj"),
|
||||
rel("depends_on", "component:beta", "component:alpha"),
|
||||
rel("depends_on", "component:gamma", "component:beta", status="stale"),
|
||||
rel("provides", "component:alpha", "interface:api"),
|
||||
rel("consumes", "component:beta", "interface:api"),
|
||||
rel("consumes", "component:gamma", "interface:api", status="STALE"),
|
||||
rel("owns", "component:alpha", "state_store:s1"),
|
||||
rel("owns", "component:beta", "state_store:s2"),
|
||||
rel("verified_by", "component:alpha", "test_suite:t1"),
|
||||
rel("deployed_by", "component:alpha", "deployment_surface:d1", status="stale"),
|
||||
rel("supersedes", "component:alpha", "component:beta"),
|
||||
rel("supersedes", "component:gamma", "component:alpha"),
|
||||
rel("legacy_of", "component:alpha", "component:gamma"),
|
||||
rel("connects_to", "deployment_surface:d1", "test_suite:t1"),
|
||||
],
|
||||
"observations": [],
|
||||
}
|
||||
return d
|
||||
|
||||
|
||||
def q_case(group, doc, eid, call):
|
||||
args = {"doc": copy.deepcopy(doc), "eid": eid}
|
||||
case(group, f"{eid}", args, {"ok": call(receiver(copy.deepcopy(doc)), eid)})
|
||||
|
||||
|
||||
def main():
|
||||
doc = fixture_doc()
|
||||
m = receiver(copy.deepcopy(doc))
|
||||
|
||||
# ── query kernels (incl. the stale filters) ─────────────────────────
|
||||
for eid in ["domain:core", "project:proj", "component:alpha", "component:beta", "component:old"]:
|
||||
q_case("children", doc, eid, lambda mm, e: mm.children(e))
|
||||
q_case("beneath", doc, "domain:core", lambda mm, e: mm.beneath(e))
|
||||
q_case("beneath", doc, "project:proj", lambda mm, e: mm.beneath(e))
|
||||
for eid in ["component:alpha", "domain:core", "component:gamma", "deployment_surface:d1"]:
|
||||
q_case("owning_domain", doc, eid, lambda mm, e: mm.owning_domain(e))
|
||||
for eid in ["component:beta", "component:alpha", "component:gamma"]:
|
||||
q_case("dependencies", doc, eid, lambda mm, e: mm.dependencies(e))
|
||||
for eid in ["component:alpha", "interface:api", "state_store:s1"]:
|
||||
q_case("consumers", doc, eid, lambda mm, e: mm.consumers(e))
|
||||
q_case("interfaces_for", doc, "component:alpha", lambda mm, e: mm.interfaces_for(e))
|
||||
for eid in ["component:alpha", "component:beta", "component:gamma"]:
|
||||
q_case("state_owned", doc, eid, lambda mm, e: mm.state_owned(e))
|
||||
q_case("verification_surfaces", doc, "component:alpha", lambda mm, e: mm.verification_surfaces(e))
|
||||
# surfaces carry NO stale filter (oracle)
|
||||
q_case("deployment_surfaces", doc, "component:alpha", lambda mm, e: mm.deployment_surfaces(e))
|
||||
for eid in ["component:alpha", "state_store:s1", "component:old", "component:zz"]:
|
||||
q_case("canonical_state_owner", doc, eid, lambda mm, e: mm.canonical_state_owner(e))
|
||||
q_case("supersession", doc, "component:alpha", lambda mm, e: mm.supersession(e))
|
||||
q_case("supersession", doc, "component:gamma", lambda mm, e: mm.supersession(e))
|
||||
|
||||
# ── impact_closure (full §18F shape) ─────────────────────────────────
|
||||
for name, eid, depth in [
|
||||
("full", "component:alpha", None),
|
||||
("root", "project:proj", None),
|
||||
("leaf_state_store", "state_store:s1", None),
|
||||
("depth_bound_1", "component:alpha", 1),
|
||||
("unknown_entity", "nope:x", None),
|
||||
("quoted_eid", "it's", None),
|
||||
]:
|
||||
args = {"doc": copy.deepcopy(doc), "eid": eid}
|
||||
kwargs = {}
|
||||
if depth is not None:
|
||||
args["max_depth"] = depth
|
||||
kwargs = {"max_depth": depth}
|
||||
r = receiver(copy.deepcopy(doc)).impact_closure(eid, **kwargs)
|
||||
case("impact_closure", name, args, {"ok": r})
|
||||
|
||||
# ── workspace_record / workspace_advance ─────────────────────────────
|
||||
wr = sm.workspace_record(contract_id="C-1", construction_base="sha1")
|
||||
case("workspace_record", "defaults", {"contract_id": "C-1", "construction_base": "sha1"}, {"ok": wr})
|
||||
wr2 = sm.workspace_record(
|
||||
contract_id="C-2",
|
||||
construction_base="sha2",
|
||||
runtime_generation="rt",
|
||||
candidate_generation="cd",
|
||||
promotion_target="gitea/release",
|
||||
rollback_generation="rb",
|
||||
)
|
||||
case(
|
||||
"workspace_record",
|
||||
"full",
|
||||
{
|
||||
"contract_id": "C-2",
|
||||
"construction_base": "sha2",
|
||||
"runtime_generation": "rt",
|
||||
"candidate_generation": "cd",
|
||||
"promotion_target": "gitea/release",
|
||||
"rollback_generation": "rb",
|
||||
},
|
||||
{"ok": wr2},
|
||||
)
|
||||
|
||||
def wa_case(name, stages, stage, evidence="", build=None):
|
||||
record = build if build is not None else sm.workspace_record(contract_id="C-3", construction_base="sha3")
|
||||
for s in stages:
|
||||
record = sm.workspace_advance(record, s)
|
||||
args = {"record": copy.deepcopy(record), "stage": stage, "evidence": evidence}
|
||||
try:
|
||||
out = sm.workspace_advance(copy.deepcopy(record), stage, evidence=evidence)
|
||||
expect = {"ok": out}
|
||||
except ValueError as exc:
|
||||
expect = {"err": str(exc)}
|
||||
case("workspace_advance", name, args, expect)
|
||||
|
||||
wa_case("first_stage", [], "base_verified")
|
||||
wa_case("second_stage", ["base_verified"], "worktree_created")
|
||||
wa_case("evidence_truncated", ["base_verified"], "worktree_created", evidence="e" * 250)
|
||||
wa_case("evidence_empty_no_key", ["base_verified"], "construction")
|
||||
wa_case("evidence_second_appends", ["base_verified", "worktree_created"], "construction", evidence="second")
|
||||
wa_case("wrong_first", [], "construction")
|
||||
wa_case("skip_stage", ["base_verified"], "verified")
|
||||
wa_case("backwards", ["base_verified", "worktree_created"], "base_verified")
|
||||
wa_case("unknown_stage", [], "bogus")
|
||||
wa_case("unknown_mid", ["base_verified"], "bogus")
|
||||
# missing stages_completed → KeyError (crash parity)
|
||||
args = {"record": {"contract_id": "C"}, "stage": "base_verified", "evidence": ""}
|
||||
try:
|
||||
sm.workspace_advance(copy.deepcopy(args["record"]), "base_verified")
|
||||
expect = {"ok": None}
|
||||
except KeyError as exc:
|
||||
expect = {"err": f"KeyError: {exc}"}
|
||||
case("workspace_advance", "missing_stages_key", args, expect)
|
||||
|
||||
# ── add ──────────────────────────────────────────────────────────────
|
||||
def add_case(name, d, kind, name_, parent=None, ext=None, evidence_ref="structural-model"):
|
||||
args = {
|
||||
"doc": copy.deepcopy(d),
|
||||
"kind": kind,
|
||||
"name": name_,
|
||||
"parent": parent,
|
||||
"ext": ext,
|
||||
"evidence_ref": evidence_ref,
|
||||
}
|
||||
mm = receiver(copy.deepcopy(d))
|
||||
try:
|
||||
eid = mm.add(kind, name_, parent=parent, ext=ext, evidence_ref=evidence_ref)
|
||||
expect = {"ok": {"doc": mm.doc, "eid": eid}}
|
||||
except (ValueError, KeyError) as exc:
|
||||
expect = {"err": f"{type(exc).__name__}: {exc}" if isinstance(exc, KeyError) else str(exc)}
|
||||
case("add", name, args, expect)
|
||||
|
||||
base = {
|
||||
"schema_version": 1,
|
||||
"generated_at": FIXED_NOW,
|
||||
"entities": [
|
||||
ent("project:proj", "project"),
|
||||
ent("domain:core", "domain"),
|
||||
ent("component:there", "component", maturity="PRESENT"),
|
||||
],
|
||||
"relationships": [],
|
||||
"observations": [],
|
||||
}
|
||||
add_case("fresh", base, "component", "alpha", parent="domain:core")
|
||||
add_case("with_ext", base, "interface", "api", ext={"proto": "grpc", "notes": "n"})
|
||||
add_case("unknown_kind", base, "widget", "w")
|
||||
add_case("quoted_kind", base, "it's", "w")
|
||||
add_case("custom_evidence", base, "test_suite", "t", evidence_ref="ev-x")
|
||||
readd = copy.deepcopy(base)
|
||||
readd["entities"].append(
|
||||
ent("component:again", "component", maturity="PRESENT", identities={"name": "old"}, ext={"keep": 1})
|
||||
)
|
||||
add_case("readd_refreshes", readd, "component", "again", ext={"new": 2})
|
||||
no_identities = copy.deepcopy(base)
|
||||
no_identities["entities"].append({"id": "component:bare", "kind": "component", "maturity": "DECLARED"})
|
||||
add_case("missing_identities_crash", no_identities, "component", "bare")
|
||||
add_case("empty_ext_falsy_skipped", base, "environment", "prod", ext={})
|
||||
|
||||
# ── relate ──────────────────────────────────────────────────────────
|
||||
def relate_case(
|
||||
name,
|
||||
d,
|
||||
from_id,
|
||||
rl,
|
||||
to_id,
|
||||
evidence_ref="structural-model",
|
||||
authority="model-author",
|
||||
status="active",
|
||||
ext=None,
|
||||
):
|
||||
args = {
|
||||
"doc": copy.deepcopy(d),
|
||||
"from_id": from_id,
|
||||
"rel": rl,
|
||||
"to_id": to_id,
|
||||
"evidence_ref": evidence_ref,
|
||||
"authority": authority,
|
||||
"status": status,
|
||||
"ext": ext,
|
||||
}
|
||||
mm = receiver(copy.deepcopy(d))
|
||||
try:
|
||||
r = mm.relate(from_id, rl, to_id, evidence_ref=evidence_ref, authority=authority, status=status, ext=ext)
|
||||
expect = {"ok": {"doc": mm.doc, "rel": r}}
|
||||
except ValueError as exc:
|
||||
expect = {"err": str(exc)}
|
||||
case("relate", name, args, expect)
|
||||
|
||||
relate_case("defaults", base, "component:there", "depends_on", "project:proj")
|
||||
relate_case(
|
||||
"importer_authority", base, "component:there", "calls", "domain:core", authority="importer", status="CURRENT"
|
||||
)
|
||||
relate_case("with_ext", base, "component:there", "reads", "domain:core", ext={"line": 3})
|
||||
relate_case("unknown_rel", base, "component:there", "hates", "domain:core")
|
||||
relate_case("empty_evidence", base, "component:there", "writes", "domain:core", evidence_ref="")
|
||||
relate_case("empty_ext_falsy_skipped", base, "component:there", "connects_to", "domain:core", ext={})
|
||||
dupdoc = copy.deepcopy(base)
|
||||
sm.StructuralModel.relate(receiver(dupdoc), "component:there", "depends_on", "project:proj")
|
||||
relate_case(
|
||||
"dup_keeps_stored", dupdoc, "component:there", "depends_on", "project:proj", authority="user", status="legacy"
|
||||
)
|
||||
|
||||
# ── _init_project / save / reload (real filesystem, byte law) ───────
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
m2 = sm.StructuralModel(td + "/s1", "init-proj")
|
||||
# the constructor's pre-_init_project doc IS new_doc() (pinned clock)
|
||||
case(
|
||||
"init_project",
|
||||
"fresh_construction",
|
||||
{"doc": ss.new_doc(), "project_id": "init-proj"},
|
||||
{"ok": {"doc": m2.doc, "content": Path(m2.path).read_text()}},
|
||||
)
|
||||
|
||||
m3 = sm.StructuralModel(td + "/s2", "save-proj")
|
||||
m3.add("domain", "core", parent="project:save-proj")
|
||||
m3.add("component", "alpha", parent="domain:core")
|
||||
m3.relate("component:alpha", "owns", "state_store:s1")
|
||||
m3.save()
|
||||
case(
|
||||
"save",
|
||||
"byte_law",
|
||||
{"doc": copy.deepcopy(m3.doc)},
|
||||
{"ok": {"doc": m3.doc, "content": Path(m3.path).read_text()}},
|
||||
)
|
||||
# a doc with secrets — the store's redaction law applies on disk
|
||||
m4 = sm.StructuralModel(td + "/s3", "redact-proj")
|
||||
m4.doc["credentials"] = {"token": "s3cret", "note": "keep"}
|
||||
m4.save()
|
||||
case(
|
||||
"save",
|
||||
"secrets_redacted",
|
||||
{"doc": copy.deepcopy(m4.doc)},
|
||||
{"ok": {"doc": m4.doc, "content": Path(m4.path).read_text()}},
|
||||
)
|
||||
# save() stamps schema_version=1 BEFORE the store save — a stale
|
||||
# 99 is coerced, never raised (the facade's own law)
|
||||
m5 = receiver({"schema_version": 99, "entities": [], "relationships": []}, "bad", td + "/s4/structural.json")
|
||||
m5.save()
|
||||
case(
|
||||
"save",
|
||||
"schema_version_stamped",
|
||||
{"doc": {"schema_version": 99, "entities": [], "relationships": []}},
|
||||
{"ok": {"doc": m5.doc, "content": Path(m5.path).read_text()}},
|
||||
)
|
||||
|
||||
# reload: existing / missing / corrupt
|
||||
m6 = sm.StructuralModel(td + "/s2", "save-proj")
|
||||
m6.reload()
|
||||
case("reload", "existing_file", {}, {"ok": {"doc": m6.doc}})
|
||||
m7 = receiver(
|
||||
{"schema_version": 1, "entities": [], "relationships": []}, "miss", td + "/missing/structural.json"
|
||||
)
|
||||
m7.reload()
|
||||
case("reload", "missing_file_keeps_doc", {}, {"ok": {"doc": m7.doc}})
|
||||
corrupt = td + "/corrupt/structural.json"
|
||||
Path(corrupt).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(corrupt).write_text("{not json")
|
||||
m8 = receiver({"schema_version": 1, "entities": [], "relationships": []}, "corrupt", corrupt)
|
||||
m8.reload()
|
||||
case("reload", "corrupt_file_keeps_doc", {}, {"ok": {"doc": m8.doc}})
|
||||
|
||||
out = {"module": "structural_model", "now": FIXED_NOW, "groups": groups}
|
||||
text = json.dumps(out, indent=1, sort_keys=True) + "\n"
|
||||
if "--verify" in sys.argv:
|
||||
frozen = OUT.read_text()
|
||||
if frozen == text:
|
||||
print(
|
||||
f"OK: frozen truth byte-identical against live oracle "
|
||||
f"({sum(len(v) for v in groups.values())} cases) -> {OUT}"
|
||||
)
|
||||
return 0
|
||||
print("MISMATCH: regenerated truth differs from frozen file")
|
||||
for a, b in zip(frozen.splitlines(), text.splitlines()):
|
||||
if a != b:
|
||||
print(f" frozen: {a[:200]}")
|
||||
print(f" fresh: {b[:200]}")
|
||||
break
|
||||
return 1
|
||||
OUT.write_text(text)
|
||||
total = sum(len(v) for v in groups.values())
|
||||
print(f"frozen {total} cases across {len(groups)} groups -> {OUT}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust structural_model core (dichotomy-wave
|
||||
gap-closing tranche; mirrors ss_loader.py's contract).
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* sm_query({doc, eid, op}) -> query result
|
||||
ops: children | beneath | owning_domain | dependencies | consumers |
|
||||
interfaces_for | state_owned | verification_surfaces |
|
||||
deployment_surfaces | supersession (relationships-only kernels)
|
||||
canonical_state_owner | impact_closure (+max_depth) —
|
||||
doc-shaped, entities resolve alias entries
|
||||
* sm_add({doc, kind, name, parent?, ext?, evidence_ref?, now})
|
||||
-> {doc, eid}
|
||||
* sm_relate({doc, from_id, rel, to_id, evidence_ref?, authority?,
|
||||
status?, ext?, now}) -> {doc, rel}
|
||||
* sm_init_project({doc, project_id, path, now}) -> {doc, path}
|
||||
* sm_save({doc, path}) -> {doc, path}
|
||||
* sm_reload({path}) -> doc | null
|
||||
* sm_workspace_record({contract_id, construction_base,
|
||||
runtime_generation?, candidate_generation?, promotion_target?,
|
||||
rollback_generation?}) -> record
|
||||
* sm_workspace_advance({record, stage, evidence?}) -> record
|
||||
* sm_free_string(ptr)
|
||||
|
||||
No-clock law: `now` (the structural_store facade clock) is always an
|
||||
argument — the kernel never reads the wall clock.
|
||||
|
||||
Raise parity: every structural_model kernel error carries the oracle's
|
||||
exact message; the oracle's raises are ValueError (unknown kind / rel /
|
||||
stage, lifecycle order) and KeyError (a dict missing "identities" /
|
||||
"stages_completed"). This loader re-raises the ORACLE'S exception type
|
||||
from the message shape ("KeyError: …" prefix → KeyError; everything else
|
||||
ValueError) — raise parity holds at the type level.
|
||||
|
||||
Fix-first note (2026-10-13): structural_model.py's delegation seam
|
||||
previously imported rune.sm_loader — the SHARD_MANAGER loader (no query
|
||||
symbol), so the seam always dead-ended into the Python floor. This
|
||||
loader is the seam's real target: the structural_model crate's sm_* FFI.
|
||||
|
||||
Override the .so with STM_RUST_LIB (a FILE path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "structural_model"
|
||||
_LIB_PATH = Path(os.environ.get("STM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libstructural_model.so"))
|
||||
|
||||
_SYMBOLS = (
|
||||
"sm_query",
|
||||
"sm_add",
|
||||
"sm_relate",
|
||||
"sm_init_project",
|
||||
"sm_save",
|
||||
"sm_reload",
|
||||
"sm_workspace_record",
|
||||
"sm_workspace_advance",
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class StructuralModelCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust structural_model core is not built. Callers fall back."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise StructuralModelCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
free = getattr(lib, "sm_free_string", None)
|
||||
if free is not None:
|
||||
free.argtypes = [ctypes.c_void_p]
|
||||
lib._free = free
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _raise_like_oracle(err: str):
|
||||
"""Re-raise the ORACLE's exception type from the kernel's message."""
|
||||
if err.startswith("KeyError: "):
|
||||
raise KeyError(err[len("KeyError: ") :].strip("'"))
|
||||
raise ValueError(err)
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
free = getattr(lib, "_free", None)
|
||||
if free is not None:
|
||||
free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
_raise_like_oracle(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
# ── typed public API ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def query(doc: dict, eid: str, op: str, max_depth: int | None = None):
|
||||
payload = {"doc": doc, "eid": eid, "op": op}
|
||||
if max_depth is not None:
|
||||
payload["max_depth"] = max_depth
|
||||
return _call("sm_query", payload)
|
||||
|
||||
|
||||
def add(
|
||||
doc: dict,
|
||||
kind: str,
|
||||
name: str,
|
||||
*,
|
||||
parent: str | None = None,
|
||||
ext: dict | None = None,
|
||||
evidence_ref: str = "structural-model",
|
||||
now: str = "",
|
||||
) -> dict:
|
||||
return _call(
|
||||
"sm_add",
|
||||
{
|
||||
"doc": doc,
|
||||
"kind": kind,
|
||||
"name": name,
|
||||
"parent": parent,
|
||||
"ext": ext,
|
||||
"evidence_ref": evidence_ref,
|
||||
"now": now,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def relate(
|
||||
doc: dict,
|
||||
from_id: str,
|
||||
rel: str,
|
||||
to_id: str,
|
||||
*,
|
||||
evidence_ref: str = "structural-model",
|
||||
authority: str = "model-author",
|
||||
status: str = "active",
|
||||
ext: dict | None = None,
|
||||
now: str = "",
|
||||
) -> dict:
|
||||
return _call(
|
||||
"sm_relate",
|
||||
{
|
||||
"doc": doc,
|
||||
"from_id": from_id,
|
||||
"rel": rel,
|
||||
"to_id": to_id,
|
||||
"evidence_ref": evidence_ref,
|
||||
"authority": authority,
|
||||
"status": status,
|
||||
"ext": ext,
|
||||
"now": now,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def init_project(doc: dict, project_id: str, path: str, now: str) -> dict:
|
||||
return _call(
|
||||
"sm_init_project",
|
||||
{
|
||||
"doc": doc,
|
||||
"project_id": project_id,
|
||||
"path": path,
|
||||
"now": now,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def save(doc: dict, path: str) -> dict:
|
||||
return _call("sm_save", {"doc": doc, "path": path})
|
||||
|
||||
|
||||
def reload(path: str):
|
||||
return _call("sm_reload", {"path": path})
|
||||
|
||||
|
||||
def workspace_record(
|
||||
*,
|
||||
contract_id: str,
|
||||
construction_base: str,
|
||||
runtime_generation: str = "",
|
||||
candidate_generation: str = "",
|
||||
promotion_target: str = "gitea/master",
|
||||
rollback_generation: str = "",
|
||||
) -> dict:
|
||||
return _call(
|
||||
"sm_workspace_record",
|
||||
{
|
||||
"contract_id": contract_id,
|
||||
"construction_base": construction_base,
|
||||
"runtime_generation": runtime_generation,
|
||||
"candidate_generation": candidate_generation,
|
||||
"promotion_target": promotion_target,
|
||||
"rollback_generation": rollback_generation,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def workspace_advance(record: dict, stage: str, *, evidence: str = "") -> dict:
|
||||
return _call(
|
||||
"sm_workspace_advance",
|
||||
{
|
||||
"record": record,
|
||||
"stage": stage,
|
||||
"evidence": evidence,
|
||||
},
|
||||
)
|
||||
+308
-103
@@ -33,26 +33,77 @@ RELATIONSHIPS: belongs_to, depends_on, calls, provides, consumes,
|
||||
reads, writes, connects_to, verified_by, deployed_by, owns, supersedes,
|
||||
legacy_of — every edge carries evidence_refs + authority + status.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from rune.structural_store import (
|
||||
add_entity, add_relationship, entity, load, new_doc, save,
|
||||
add_entity,
|
||||
add_relationship,
|
||||
entity,
|
||||
load,
|
||||
new_doc,
|
||||
now,
|
||||
save,
|
||||
)
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def _stm_core():
|
||||
"""The Rust structural_model core when available (dichotomy-wave
|
||||
gap-closing facade burn-down). RUNE_STM_CORE=python pins the floor.
|
||||
|
||||
Fix-first (2026-10-13): the original delegation seam imported
|
||||
rune.sm_loader — the SHARD_MANAGER loader (no query symbol), so the
|
||||
seam always dead-ended into the Python floor. The seam's real target
|
||||
is rune.stm_loader (the structural_model crate's sm_* FFI)."""
|
||||
import os
|
||||
|
||||
if os.environ.get("RUNE_STM_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
# Dynamic import (no static `from rune import` node): the shared
|
||||
# core must not statically import rune modules (TestNoRuneConstants).
|
||||
import importlib
|
||||
|
||||
stm_loader = importlib.import_module("rune.stm_loader")
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback
|
||||
return None
|
||||
try:
|
||||
return stm_loader if stm_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — probe must never crash the fallback
|
||||
return None
|
||||
|
||||
|
||||
ENTITY_KINDS = (
|
||||
"project", "domain", "subsystem", "component", "interface",
|
||||
"state_store", "test_suite", "deployment_surface", "environment",
|
||||
"project",
|
||||
"domain",
|
||||
"subsystem",
|
||||
"component",
|
||||
"interface",
|
||||
"state_store",
|
||||
"test_suite",
|
||||
"deployment_surface",
|
||||
"environment",
|
||||
)
|
||||
|
||||
RELATION_TYPES = (
|
||||
"belongs_to", "owns", "depends_on", "calls", "provides", "consumes",
|
||||
"reads", "writes", "connects_to", "verified_by", "deployed_by",
|
||||
"supersedes", "legacy_of",
|
||||
"belongs_to",
|
||||
"owns",
|
||||
"depends_on",
|
||||
"calls",
|
||||
"provides",
|
||||
"consumes",
|
||||
"reads",
|
||||
"writes",
|
||||
"connects_to",
|
||||
"verified_by",
|
||||
"deployed_by",
|
||||
"supersedes",
|
||||
"legacy_of",
|
||||
)
|
||||
|
||||
|
||||
@@ -75,62 +126,136 @@ class StructuralModel:
|
||||
# -- persistence ----------------------------------------------------
|
||||
|
||||
def _init_project(self, doc: dict) -> None:
|
||||
add_entity(doc, id=f"project:{self.project_id}", kind="project",
|
||||
maturity="DECLARED",
|
||||
evidence_refs=["structural-model-init"])
|
||||
root = add_relationship(doc, type="belongs_to",
|
||||
from_id=f"project:{self.project_id}",
|
||||
to_id=f"project:{self.project_id}",
|
||||
evidence_refs=["structural-model-root"])
|
||||
core = _stm_core()
|
||||
if core is not None:
|
||||
try:
|
||||
out = core.init_project(doc, self.project_id, str(self.path), now())
|
||||
doc.clear()
|
||||
doc.update(out["doc"])
|
||||
return
|
||||
except Exception: # noqa: BLE001 — kernel failure → floor below
|
||||
pass
|
||||
add_entity(
|
||||
doc,
|
||||
id=f"project:{self.project_id}",
|
||||
kind="project",
|
||||
maturity="DECLARED",
|
||||
evidence_refs=["structural-model-init"],
|
||||
)
|
||||
root = add_relationship(
|
||||
doc,
|
||||
type="belongs_to",
|
||||
from_id=f"project:{self.project_id}",
|
||||
to_id=f"project:{self.project_id}",
|
||||
evidence_refs=["structural-model-root"],
|
||||
)
|
||||
root["authority"] = "model-author"
|
||||
root["status"] = "root"
|
||||
save(doc, self.path)
|
||||
|
||||
def save(self) -> None:
|
||||
core = _stm_core()
|
||||
if core is not None:
|
||||
try:
|
||||
out = core.save(self.doc, str(self.path))
|
||||
self.doc.clear()
|
||||
self.doc.update(out["doc"])
|
||||
return
|
||||
except Exception: # noqa: BLE001 — kernel failure → floor below
|
||||
pass
|
||||
self.doc["schema_version"] = SCHEMA_VERSION
|
||||
save(self.doc, self.path)
|
||||
|
||||
def reload(self) -> None:
|
||||
core = _stm_core()
|
||||
if core is not None:
|
||||
try:
|
||||
doc = core.reload(str(self.path))
|
||||
if doc is not None:
|
||||
self.doc = doc
|
||||
return
|
||||
except Exception: # noqa: BLE001 — kernel failure → floor below
|
||||
pass
|
||||
doc = load(self.path)
|
||||
if doc is not None:
|
||||
self.doc = doc
|
||||
|
||||
# -- entity/relationship construction -------------------------------
|
||||
|
||||
def add(self, kind: str, name: str, *, parent: str | None = None,
|
||||
ext: dict | None = None,
|
||||
evidence_ref: str = "structural-model") -> str:
|
||||
def add(
|
||||
self,
|
||||
kind: str,
|
||||
name: str,
|
||||
*,
|
||||
parent: str | None = None,
|
||||
ext: dict | None = None,
|
||||
evidence_ref: str = "structural-model",
|
||||
) -> str:
|
||||
"""Add an entity with a STABLE deterministic id (kind:name —
|
||||
persistent across model versions; rule 18E). parent (an id) adds
|
||||
the belongs_to edge (Backstage partOf semantics)."""
|
||||
core = _stm_core()
|
||||
if core is not None:
|
||||
try:
|
||||
out = core.add(self.doc, kind, name, parent=parent, ext=ext, evidence_ref=evidence_ref, now=now())
|
||||
self.doc.clear()
|
||||
self.doc.update(out["doc"])
|
||||
return out["eid"]
|
||||
except (ValueError, KeyError):
|
||||
raise # raise parity: the kernel carries the oracle's raise
|
||||
except Exception: # noqa: BLE001 — kernel failure → floor below
|
||||
pass
|
||||
if kind not in ENTITY_KINDS:
|
||||
raise ValueError(f"unknown entity kind {kind!r} — known: "
|
||||
f"{ENTITY_KINDS}")
|
||||
raise ValueError(f"unknown entity kind {kind!r} — known: {ENTITY_KINDS}")
|
||||
eid = f"{kind}:{name}"
|
||||
e = add_entity(self.doc, id=eid, kind=kind, maturity="DECLARED",
|
||||
evidence_refs=[evidence_ref])
|
||||
e = add_entity(self.doc, id=eid, kind=kind, maturity="DECLARED", evidence_refs=[evidence_ref])
|
||||
e["identities"]["name"] = name
|
||||
if ext:
|
||||
e.setdefault("ext", {}).update(ext)
|
||||
if parent:
|
||||
self.relate(eid, "belongs_to", parent,
|
||||
evidence_ref=evidence_ref)
|
||||
self.relate(eid, "belongs_to", parent, evidence_ref=evidence_ref)
|
||||
return eid
|
||||
|
||||
def relate(self, from_id: str, rel: str, to_id: str, *,
|
||||
evidence_ref: str = "structural-model",
|
||||
authority: str = "model-author",
|
||||
status: str = "active", ext: dict | None = None) -> dict:
|
||||
def relate(
|
||||
self,
|
||||
from_id: str,
|
||||
rel: str,
|
||||
to_id: str,
|
||||
*,
|
||||
evidence_ref: str = "structural-model",
|
||||
authority: str = "model-author",
|
||||
status: str = "active",
|
||||
ext: dict | None = None,
|
||||
) -> dict:
|
||||
"""Add a typed relationship. Every edge carries evidence_refs +
|
||||
authority + status (the INNOVATION per the Build Basis —
|
||||
authority-graded claims). Authority values: model-author
|
||||
(agent-observed/derived), importer (external tool derived —
|
||||
Glean batch-layering adaptation), user (user-locked)."""
|
||||
core = _stm_core()
|
||||
if core is not None:
|
||||
try:
|
||||
out = core.relate(
|
||||
self.doc,
|
||||
from_id,
|
||||
rel,
|
||||
to_id,
|
||||
evidence_ref=evidence_ref,
|
||||
authority=authority,
|
||||
status=status,
|
||||
ext=ext,
|
||||
now=now(),
|
||||
)
|
||||
self.doc.clear()
|
||||
self.doc.update(out["doc"])
|
||||
return out["rel"]
|
||||
except (ValueError, KeyError):
|
||||
raise # raise parity: the kernel carries the oracle's raise
|
||||
except Exception: # noqa: BLE001 — kernel failure → floor below
|
||||
pass
|
||||
if rel not in RELATION_TYPES:
|
||||
raise ValueError(f"unknown relationship {rel!r} — known: "
|
||||
f"{RELATION_TYPES}")
|
||||
r = add_relationship(self.doc, type=rel, from_id=from_id,
|
||||
to_id=to_id, evidence_refs=[evidence_ref])
|
||||
raise ValueError(f"unknown relationship {rel!r} — known: {RELATION_TYPES}")
|
||||
r = add_relationship(self.doc, type=rel, from_id=from_id, to_id=to_id, evidence_refs=[evidence_ref])
|
||||
r["authority"] = authority
|
||||
r["status"] = status
|
||||
if ext:
|
||||
@@ -144,21 +269,25 @@ class StructuralModel:
|
||||
|
||||
def children(self, eid: str) -> list[str]:
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "children")
|
||||
return _sm.query(self.doc, eid, "children")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return [r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] == "belongs_to" and r["to"] == eid
|
||||
and r["from"] != eid]
|
||||
return [
|
||||
r["from"]
|
||||
for r in self.doc["relationships"]
|
||||
if r["type"] == "belongs_to" and r["to"] == eid and r["from"] != eid
|
||||
]
|
||||
|
||||
def beneath(self, eid: str) -> list[str]:
|
||||
"""Transitive descendants (bounded by the graph itself)."""
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "beneath")
|
||||
return _sm.query(self.doc, eid, "beneath")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
seen, out, frontier = {eid}, [], [eid]
|
||||
@@ -173,17 +302,17 @@ class StructuralModel:
|
||||
|
||||
def owning_domain(self, eid: str) -> str | None:
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "owning_domain")
|
||||
return _sm.query(self.doc, eid, "owning_domain")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
cur, hops = eid, 0
|
||||
while hops < 16:
|
||||
if cur.startswith("domain:"):
|
||||
return cur
|
||||
parents = [r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "belongs_to" and r["from"] == cur]
|
||||
parents = [r["to"] for r in self.doc["relationships"] if r["type"] == "belongs_to" and r["from"] == cur]
|
||||
if not parents:
|
||||
return None
|
||||
cur = parents[0]
|
||||
@@ -192,53 +321,84 @@ class StructuralModel:
|
||||
|
||||
def dependencies(self, eid: str) -> list[str]:
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "dependencies")
|
||||
return _sm.query(self.doc, eid, "dependencies")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "depends_on" and r["from"] == eid
|
||||
and r.get("status") != "stale")
|
||||
return sorted(
|
||||
r["to"]
|
||||
for r in self.doc["relationships"]
|
||||
if r["type"] == "depends_on" and r["from"] == eid and r.get("status") != "stale"
|
||||
)
|
||||
|
||||
def consumers(self, eid: str) -> list[str]:
|
||||
"""Who depends on OR consumes this entity (interfaces are
|
||||
consumed; components are depended on — Backstage relation pairs)."""
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "consumers")
|
||||
return _sm.query(self.doc, eid, "consumers")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return sorted(r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] in ("depends_on", "consumes")
|
||||
and r["to"] == eid and r.get("status") != "stale")
|
||||
return sorted(
|
||||
r["from"]
|
||||
for r in self.doc["relationships"]
|
||||
if r["type"] in ("depends_on", "consumes") and r["to"] == eid and r.get("status") != "stale"
|
||||
)
|
||||
|
||||
def interfaces_for(self, eid: str) -> list[str]:
|
||||
try:
|
||||
from rune import sm_loader as _sm
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc.get("relationships", []), eid, "interfaces_for")
|
||||
return _sm.query(self.doc, eid, "interfaces_for")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "provides" and r["from"] == eid)
|
||||
return sorted(r["to"] for r in self.doc["relationships"] if r["type"] == "provides" and r["from"] == eid)
|
||||
|
||||
def state_owned(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "owns" and r["from"] == eid)
|
||||
try:
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc, eid, "state_owned")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return sorted(r["to"] for r in self.doc["relationships"] if r["type"] == "owns" and r["from"] == eid)
|
||||
|
||||
def verification_surfaces(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "verified_by" and r["from"] == eid)
|
||||
try:
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc, eid, "verification_surfaces")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return sorted(r["to"] for r in self.doc["relationships"] if r["type"] == "verified_by" and r["from"] == eid)
|
||||
|
||||
def deployment_surfaces(self, eid: str) -> list[str]:
|
||||
return sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "deployed_by" and r["from"] == eid)
|
||||
try:
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc, eid, "deployment_surfaces")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return sorted(r["to"] for r in self.doc["relationships"] if r["type"] == "deployed_by" and r["from"] == eid)
|
||||
|
||||
def canonical_state_owner(self, eid: str) -> str | None:
|
||||
"""The canonical-state owner: the state_store this entity owns,
|
||||
or (for a state_store) itself."""
|
||||
try:
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc, eid, "canonical_state_owner")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
e = self.get(eid)
|
||||
if e and e.get("kind") == "state_store":
|
||||
return eid
|
||||
@@ -246,16 +406,23 @@ class StructuralModel:
|
||||
return owned[0] if owned else None
|
||||
|
||||
def supersession(self, eid: str) -> dict:
|
||||
try:
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc, eid, "supersession")
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
return {
|
||||
"supersedes": sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "supersedes"
|
||||
and r["from"] == eid),
|
||||
"supersedes": sorted(
|
||||
r["to"] for r in self.doc["relationships"] if r["type"] == "supersedes" and r["from"] == eid
|
||||
),
|
||||
"superseded_by": sorted(
|
||||
r["from"] for r in self.doc["relationships"]
|
||||
if r["type"] == "supersedes" and r["to"] == eid),
|
||||
"legacy_of": sorted(r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "legacy_of"
|
||||
and r["from"] == eid),
|
||||
r["from"] for r in self.doc["relationships"] if r["type"] == "supersedes" and r["to"] == eid
|
||||
),
|
||||
"legacy_of": sorted(
|
||||
r["to"] for r in self.doc["relationships"] if r["type"] == "legacy_of" and r["from"] == eid
|
||||
),
|
||||
}
|
||||
|
||||
# -- impact closure (§18F) -------------------------------------------
|
||||
@@ -266,6 +433,13 @@ class StructuralModel:
|
||||
reverse-dependency closure + verification + deployment + state
|
||||
surfaces. Unknown/dynamic relationships stay explicit uncertainty
|
||||
— never silent (rule 18F)."""
|
||||
try:
|
||||
from rune import stm_loader as _sm
|
||||
|
||||
if _sm.available():
|
||||
return _sm.query(self.doc, eid, "impact_closure", max_depth=max_depth)
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor below
|
||||
pass
|
||||
if self.get(eid) is None:
|
||||
return {"error": f"unknown entity {eid!r}"}
|
||||
# reverse-dependency closure (consumers, transitively, bounded)
|
||||
@@ -290,10 +464,13 @@ class StructuralModel:
|
||||
seen.add(c)
|
||||
consumers.add(c)
|
||||
uncertainty = [
|
||||
{"kind": "dynamic-consumer",
|
||||
"note": "static model cannot see dynamic dispatch/string-keyed "
|
||||
"lookup/late-binding consumers — reconcilers must "
|
||||
"verify at runtime"}]
|
||||
{
|
||||
"kind": "dynamic-consumer",
|
||||
"note": "static model cannot see dynamic dispatch/string-keyed "
|
||||
"lookup/late-binding consumers — reconcilers must "
|
||||
"verify at runtime",
|
||||
}
|
||||
]
|
||||
# lockfile/deps edge (Nx documented case): deps outside closure
|
||||
# are OBSERVATION scope, not mutation scope
|
||||
return {
|
||||
@@ -306,21 +483,13 @@ class StructuralModel:
|
||||
# directions are observation-relevant for a change
|
||||
"interfaces_provided": self.interfaces_for(eid),
|
||||
"interfaces_consumed": sorted(
|
||||
r["to"] for r in self.doc["relationships"]
|
||||
if r["type"] == "consumes" and r["from"] == eid),
|
||||
r["to"] for r in self.doc["relationships"] if r["type"] == "consumes" and r["from"] == eid
|
||||
),
|
||||
},
|
||||
"state_surfaces": sorted(
|
||||
{s for e in ({eid} | consumers)
|
||||
for s in self.state_owned(e)}),
|
||||
"verification_scope": sorted(
|
||||
{v for e in ({eid} | consumers)
|
||||
for v in self.verification_surfaces(e)}),
|
||||
"deployment_scope": sorted(
|
||||
{d for e in ({eid} | consumers)
|
||||
for d in self.deployment_surfaces(e)}),
|
||||
"invariants": [
|
||||
f"belongs_to hierarchy of {eid} remains stable",
|
||||
"canonical-state ownership unchanged"],
|
||||
"state_surfaces": sorted({s for e in ({eid} | consumers) for s in self.state_owned(e)}),
|
||||
"verification_scope": sorted({v for e in ({eid} | consumers) for v in self.verification_surfaces(e)}),
|
||||
"deployment_scope": sorted({d for e in ({eid} | consumers) for d in self.deployment_surfaces(e)}),
|
||||
"invariants": [f"belongs_to hierarchy of {eid} remains stable", "canonical-state ownership unchanged"],
|
||||
"non_goals": [],
|
||||
"uncertainty": uncertainty,
|
||||
"closure_depth": depth,
|
||||
@@ -341,19 +510,46 @@ class StructuralModel:
|
||||
# generation changes only through the canonical promote machinery.
|
||||
|
||||
WORKSPACE_LIFECYCLE_STAGES = (
|
||||
"base_verified", "worktree_created", "construction",
|
||||
"verified", "committed", "pushed", "ci_qualified",
|
||||
"released", "deployment_confirmed", "evidence_archived", "cleaned",
|
||||
"base_verified",
|
||||
"worktree_created",
|
||||
"construction",
|
||||
"verified",
|
||||
"committed",
|
||||
"pushed",
|
||||
"ci_qualified",
|
||||
"released",
|
||||
"deployment_confirmed",
|
||||
"evidence_archived",
|
||||
"cleaned",
|
||||
)
|
||||
|
||||
|
||||
def workspace_record(*, contract_id: str, construction_base: str,
|
||||
runtime_generation: str = "",
|
||||
candidate_generation: str = "",
|
||||
promotion_target: str = "gitea/master",
|
||||
rollback_generation: str = "") -> dict:
|
||||
def workspace_record(
|
||||
*,
|
||||
contract_id: str,
|
||||
construction_base: str,
|
||||
runtime_generation: str = "",
|
||||
candidate_generation: str = "",
|
||||
promotion_target: str = "gitea/master",
|
||||
rollback_generation: str = "",
|
||||
) -> dict:
|
||||
"""The workspace provenance record a contract carries. Generations
|
||||
are SHAs/dirs — never live-tree mutations."""
|
||||
core = _stm_core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.workspace_record(
|
||||
contract_id=contract_id,
|
||||
construction_base=construction_base,
|
||||
runtime_generation=runtime_generation,
|
||||
candidate_generation=candidate_generation,
|
||||
promotion_target=promotion_target,
|
||||
rollback_generation=rollback_generation,
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
raise # raise parity: the kernel carries the oracle's raise
|
||||
except Exception: # noqa: BLE001 — kernel failure → floor below
|
||||
pass
|
||||
return {
|
||||
"contract_id": contract_id,
|
||||
"construction_base": construction_base,
|
||||
@@ -366,9 +562,21 @@ def workspace_record(*, contract_id: str, construction_base: str,
|
||||
}
|
||||
|
||||
|
||||
def workspace_advance(record: dict, stage: str, *,
|
||||
evidence: str = "") -> dict:
|
||||
def workspace_advance(record: dict, stage: str, *, evidence: str = "") -> dict:
|
||||
"""Advance the workspace lifecycle one stage (order-enforced)."""
|
||||
core = _stm_core()
|
||||
if core is not None:
|
||||
try:
|
||||
out = core.workspace_advance(record, stage, evidence=evidence)
|
||||
# in-place law: the oracle mutates and returns the SAME dict
|
||||
# (callers hold references) — mirror it exactly
|
||||
record.clear()
|
||||
record.update(out)
|
||||
return record
|
||||
except (ValueError, KeyError):
|
||||
raise # raise parity: the kernel carries the oracle's raise
|
||||
except Exception: # noqa: BLE001 — kernel failure → floor below
|
||||
pass
|
||||
if stage not in WORKSPACE_LIFECYCLE_STAGES:
|
||||
raise ValueError(f"unknown workspace stage {stage!r}")
|
||||
done = record["stages_completed"]
|
||||
@@ -376,13 +584,10 @@ def workspace_advance(record: dict, stage: str, *,
|
||||
last = WORKSPACE_LIFECYCLE_STAGES.index(done[-1])
|
||||
cur = WORKSPACE_LIFECYCLE_STAGES.index(stage)
|
||||
if cur != last + 1:
|
||||
raise ValueError(
|
||||
f"lifecycle order violation: {done[-1]} → {stage}")
|
||||
raise ValueError(f"lifecycle order violation: {done[-1]} → {stage}")
|
||||
elif stage != WORKSPACE_LIFECYCLE_STAGES[0]:
|
||||
raise ValueError(f"lifecycle must start at "
|
||||
f"{WORKSPACE_LIFECYCLE_STAGES[0]}, got {stage!r}")
|
||||
raise ValueError(f"lifecycle must start at {WORKSPACE_LIFECYCLE_STAGES[0]}, got {stage!r}")
|
||||
done.append(stage)
|
||||
if evidence:
|
||||
record.setdefault("evidence", []).append(
|
||||
{"stage": stage, "evidence": evidence[:200]})
|
||||
record.setdefault("evidence", []).append({"stage": stage, "evidence": evidence[:200]})
|
||||
return record
|
||||
Generated
+37
@@ -2,6 +2,32 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "evidence_provenance"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -68,6 +94,7 @@ version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -81,6 +108,16 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"structural_store",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structural_store"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"evidence_provenance",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -8,4 +8,7 @@ crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# preserve_order: Python dicts round-trip in insertion order — relate()/add()
|
||||
# echo order follows the oracle.
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
structural_store = { path = "../structural_store" }
|
||||
@@ -0,0 +1,284 @@
|
||||
//! frozen_battery — replay the FROZEN oracle truth VERBATIM
|
||||
//! (frozen_sm_truth.json, generated by scripts/gen_structural_model_truth.py
|
||||
//! against the LIVE Python oracle with RUNE_STM_CORE=python +
|
||||
//! RUNE_STRUCTURAL_CORE=python pinned, clock pinned via
|
||||
//! structural_store.now, inputs deep-copied per case).
|
||||
//!
|
||||
//! The battery never imports the Python oracle — parity stays provable
|
||||
//! after strangler exits. File-writing kernels run behind the recording
|
||||
//! [`RecIo`] seam; the frozen `content` fields are the save() byte law.
|
||||
|
||||
use crate::*;
|
||||
use serde_json::{json, Value};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_sm_truth.json");
|
||||
const NOW: &str = "2026-10-13T01:02:03Z";
|
||||
|
||||
fn truth() -> Value {
|
||||
serde_json::from_str(TRUTH).expect("frozen truth parses")
|
||||
}
|
||||
|
||||
fn cases(group: &str) -> Vec<Value> {
|
||||
truth()["groups"][group]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn nm(c: &Value) -> &str {
|
||||
c["name"].as_str().unwrap_or("")
|
||||
}
|
||||
|
||||
/// Recording I/O seam — save/reload operate on in-memory files.
|
||||
struct RecIo {
|
||||
files: RefCell<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl RecIo {
|
||||
fn new() -> Self {
|
||||
RecIo { files: RefCell::new(HashMap::new()) }
|
||||
}
|
||||
fn put(&self, path: &str, contents: &str) {
|
||||
self.files.borrow_mut().insert(path.to_string(), contents.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
impl SmIo for RecIo {
|
||||
fn mkdir_parents(&self, _dir: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_text(&self, path: &str, contents: &str) -> Result<(), String> {
|
||||
self.files
|
||||
.borrow_mut()
|
||||
.insert(path.to_string(), contents.to_string());
|
||||
Ok(())
|
||||
}
|
||||
fn read_text(&self, path: &str) -> Result<String, String> {
|
||||
self.files
|
||||
.borrow()
|
||||
.get(path)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("read {}: no such file", path))
|
||||
}
|
||||
fn is_file(&self, path: &str) -> bool {
|
||||
self.files.borrow().contains_key(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare a case's expect.ok against the kernel output (err → panic
|
||||
/// with the frozen message so failures name the divergence).
|
||||
fn expect_ok(c: &Value, got: Result<Value, String>) -> Value {
|
||||
match c["expect"].get("err").and_then(|e| e.as_str()) {
|
||||
Some(want) => {
|
||||
match got {
|
||||
Ok(_) => panic!("case {} should raise {:?}", nm(c), want),
|
||||
Err(e) => assert_eq!(e, want, "case {} error message", nm(c)),
|
||||
}
|
||||
Value::Null
|
||||
}
|
||||
None => {
|
||||
let got = got.unwrap_or_else(|e| panic!("case {} raised: {}", nm(c), e));
|
||||
let want = c["expect"]["ok"].clone();
|
||||
assert_eq!(got, want, "case {} output", nm(c));
|
||||
got
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn str_opt(v: &Value, key: &str) -> Option<String> {
|
||||
v.get(key).and_then(|x| x.as_str()).map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn rels_of_doc(doc: &Value) -> Vec<Relationship> {
|
||||
gap::rels_of(doc)
|
||||
}
|
||||
|
||||
// ── the battery ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn frozen_query_kernels() {
|
||||
for group in [
|
||||
"children", "beneath", "owning_domain", "dependencies", "consumers",
|
||||
"interfaces_for", "state_owned", "verification_surfaces",
|
||||
"deployment_surfaces", "canonical_state_owner", "supersession",
|
||||
] {
|
||||
for c in cases(group) {
|
||||
let doc = c["args"]["doc"].clone();
|
||||
let eid = c["args"]["eid"].as_str().unwrap_or("");
|
||||
let rels = rels_of_doc(&doc);
|
||||
let got = match group {
|
||||
"children" => json!(children(&rels, eid)),
|
||||
"beneath" => json!(beneath(&rels, eid)),
|
||||
"owning_domain" => json!(owning_domain(&rels, eid)),
|
||||
"dependencies" => json!(dependencies(&rels, eid)),
|
||||
"consumers" => json!(consumers(&rels, eid)),
|
||||
"interfaces_for" => json!(interfaces_for(&rels, eid)),
|
||||
"state_owned" => json!(state_owned(&rels, eid)),
|
||||
"verification_surfaces" => json!(verification_surfaces(&rels, eid)),
|
||||
"deployment_surfaces" => json!(deployment_surfaces(&rels, eid)),
|
||||
"canonical_state_owner" => json!(canonical_state_owner(
|
||||
&rels,
|
||||
&gap::entities_map(&doc),
|
||||
eid
|
||||
)),
|
||||
_ => supersession(&rels, eid),
|
||||
};
|
||||
expect_ok(&c, Ok(got));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_impact_closure() {
|
||||
for c in cases("impact_closure") {
|
||||
let doc = c["args"]["doc"].clone();
|
||||
let eid = c["args"]["eid"].as_str().unwrap_or("");
|
||||
let max_depth = c["args"]["max_depth"].as_u64().unwrap_or(8) as usize;
|
||||
expect_ok(
|
||||
&c,
|
||||
Ok(impact_closure(
|
||||
&rels_of_doc(&doc),
|
||||
&gap::entities_map(&doc),
|
||||
eid,
|
||||
max_depth,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_workspace_record() {
|
||||
for c in cases("workspace_record") {
|
||||
let a = &c["args"];
|
||||
expect_ok(
|
||||
&c,
|
||||
Ok(workspace_record(
|
||||
a["contract_id"].as_str().unwrap_or(""),
|
||||
a["construction_base"].as_str().unwrap_or(""),
|
||||
&str_opt(a, "runtime_generation").unwrap_or_default(),
|
||||
&str_opt(a, "candidate_generation").unwrap_or_default(),
|
||||
&str_opt(a, "promotion_target")
|
||||
.unwrap_or_else(|| "gitea/master".to_string()),
|
||||
&str_opt(a, "rollback_generation").unwrap_or_default(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_workspace_advance() {
|
||||
for c in cases("workspace_advance") {
|
||||
let a = &c["args"];
|
||||
let mut record = a["record"].clone();
|
||||
let stage = a["stage"].as_str().unwrap_or("");
|
||||
let evidence = a["evidence"].as_str().unwrap_or("");
|
||||
let got = workspace_advance(&mut record, stage, evidence).map(|_| record.clone());
|
||||
expect_ok(&c, got);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_add() {
|
||||
for c in cases("add") {
|
||||
let a = &c["args"];
|
||||
let mut doc = a["doc"].clone();
|
||||
let got = gap::add(
|
||||
&mut doc,
|
||||
a["kind"].as_str().unwrap_or(""),
|
||||
a["name"].as_str().unwrap_or(""),
|
||||
str_opt(a, "parent").as_deref(),
|
||||
a.get("ext"),
|
||||
a["evidence_ref"].as_str().unwrap_or("structural-model"),
|
||||
NOW,
|
||||
)
|
||||
.map(|eid| json!({"doc": doc, "eid": eid}));
|
||||
expect_ok(&c, got);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_relate() {
|
||||
for c in cases("relate") {
|
||||
let a = &c["args"];
|
||||
let mut doc = a["doc"].clone();
|
||||
let got = gap::relate(
|
||||
&mut doc,
|
||||
a["from_id"].as_str().unwrap_or(""),
|
||||
a["rel"].as_str().unwrap_or(""),
|
||||
a["to_id"].as_str().unwrap_or(""),
|
||||
a["evidence_ref"].as_str().unwrap_or("structural-model"),
|
||||
a["authority"].as_str().unwrap_or("model-author"),
|
||||
a["status"].as_str().unwrap_or("active"),
|
||||
a.get("ext"),
|
||||
NOW,
|
||||
)
|
||||
.map(|rel| json!({"doc": doc, "rel": rel}));
|
||||
expect_ok(&c, got);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_init_project_and_save() {
|
||||
let io = RecIo::new();
|
||||
for c in cases("init_project") {
|
||||
let a = &c["args"];
|
||||
let mut doc = a["doc"].clone();
|
||||
let pid = a["project_id"].as_str().unwrap_or("");
|
||||
let path = "/frozen/init/structural.json";
|
||||
let got = gap::_init_project(&mut doc, pid, NOW, path, &io)
|
||||
.map(|_| json!({"doc": doc, "content": io.read_text(path).unwrap()}));
|
||||
expect_ok(&c, got);
|
||||
}
|
||||
for c in cases("save") {
|
||||
let a = &c["args"];
|
||||
let mut doc = a["doc"].clone();
|
||||
let path = format!("/frozen/save/{}.json", nm(&c));
|
||||
let got = gap::save(&mut doc, &path, &io)
|
||||
.map(|_| json!({"doc": doc, "content": io.read_text(&path).unwrap()}));
|
||||
expect_ok(&c, got);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_reload() {
|
||||
let io = RecIo::new();
|
||||
for c in cases("reload") {
|
||||
// replay the oracle's sequence: the existing-file case reloads the
|
||||
// doc the save group froze (identical write through the seam)
|
||||
let doc = cases("save")
|
||||
.into_iter()
|
||||
.find(|sc| sc["name"] == "byte_law")
|
||||
.map(|sc| sc["args"]["doc"].clone())
|
||||
.expect("save/byte_law case");
|
||||
let mut written = doc.clone();
|
||||
gap::save(&mut written, "/frozen/reload/structural.json", &io).unwrap();
|
||||
match nm(&c) {
|
||||
"existing_file" => {
|
||||
let got = gap::reload("/frozen/reload/structural.json", &io);
|
||||
expect_ok(&c, Ok(json!({"doc": got.unwrap()})));
|
||||
}
|
||||
"missing_file_keeps_doc" => {
|
||||
assert!(gap::reload("/frozen/reload/missing.json", &io).is_none());
|
||||
expect_ok(&c, Ok(json!({"doc": c["expect"]["ok"]["doc"].clone()})));
|
||||
}
|
||||
"corrupt_file_keeps_doc" => {
|
||||
io.put("/frozen/reload/corrupt.json", "{not json");
|
||||
assert!(gap::reload("/frozen/reload/corrupt.json", &io).is_none());
|
||||
expect_ok(&c, Ok(json!({"doc": c["expect"]["ok"]["doc"].clone()})));
|
||||
}
|
||||
other => panic!("unknown reload case {}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_truth_is_the_canary() {
|
||||
// layer-3 canary: the file the battery replays is the one the
|
||||
// generator wrote (count + module identity, so a stale swap fails)
|
||||
let t = truth();
|
||||
assert_eq!(t["module"], "structural_model");
|
||||
let total: usize = t["groups"].as_object().unwrap().values().map(|g| g.as_array().unwrap().len()).sum();
|
||||
assert_eq!(total, 70, "frozen corpus size drifted — regenerate truth");
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,698 @@
|
||||
//! gap.rs — dichotomy-wave PARTIAL→gap-closing port for structural_model
|
||||
//! (issue #2479 tranche).
|
||||
//!
|
||||
//! The audit-missing functions of src/rune/structural_model.py, ported
|
||||
//! name-preserving and logic-1:1 against the live oracle. Laws:
|
||||
//! * no-clock: every timestamp arrives as an input (`now` argument) —
|
||||
//! the oracle's add_entity/add_relationship stamps come from the
|
||||
//! structural_store facade clock, passed here explicitly
|
||||
//! * no-env / no-path: persistence takes an explicit path behind the
|
||||
//! injectable [`SmIo`] seam (tests use recorders; the FFI installs
|
||||
//! [`StdIo`]); RUNE_STM_CORE=python pins the Python floor on the
|
||||
//! FACADE side
|
||||
//! * persistence delegates to the structural_store core (rule A — one
|
||||
//! save law: sorted + redacted + indent-1 on disk)
|
||||
//! * raise parity: the oracle raises ValueError (unknown kind / rel /
|
||||
//! stage, lifecycle order) and KeyError (re-adding an entity whose
|
||||
//! dict lacks "identities") — every kernel error carries the oracle's
|
||||
//! exact message so the loader can re-raise the oracle's type
|
||||
|
||||
use crate::Relationship;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Python truthiness for the JSON values the oracle tests (`if ext:`).
|
||||
pub(crate) fn py_truthy(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Null => false,
|
||||
Value::Bool(b) => *b,
|
||||
Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
|
||||
Value::String(s) => !s.is_empty(),
|
||||
Value::Array(a) => !a.is_empty(),
|
||||
Value::Object(o) => !o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── the core probe (facade `_stm_core` mirror) ──────────────────────────────
|
||||
|
||||
/// The Rust structural_model core — the thing `stm_loader` activates when
|
||||
/// the facade probe allows it. `RUNE_STM_CORE=python` pins the Python
|
||||
/// floor; any other value (including unset/auto) activates this core.
|
||||
pub struct StmCore;
|
||||
|
||||
impl StmCore {
|
||||
pub fn id(&self) -> &'static str {
|
||||
"rust"
|
||||
}
|
||||
}
|
||||
|
||||
/// Facade probe contract, kernel mirror: `None` = Python floor pinned.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn _stm_core(pin: Option<&str>) -> Option<StmCore> {
|
||||
match pin {
|
||||
Some("python") => None,
|
||||
_ => Some(StmCore),
|
||||
}
|
||||
}
|
||||
|
||||
// ── vocabulary (oracle module constants) ────────────────────────────────────
|
||||
|
||||
pub const SCHEMA_VERSION: i64 = 1;
|
||||
|
||||
pub const ENTITY_KINDS: &[&str] = &[
|
||||
"project", "domain", "subsystem", "component", "interface",
|
||||
"state_store", "test_suite", "deployment_surface", "environment",
|
||||
];
|
||||
|
||||
pub const RELATION_TYPES: &[&str] = &[
|
||||
"belongs_to", "owns", "depends_on", "calls", "provides", "consumes",
|
||||
"reads", "writes", "connects_to", "verified_by", "deployed_by",
|
||||
"supersedes", "legacy_of",
|
||||
];
|
||||
|
||||
pub const WORKSPACE_LIFECYCLE_STAGES: &[&str] = &[
|
||||
"base_verified", "worktree_created", "construction",
|
||||
"verified", "committed", "pushed", "ci_qualified",
|
||||
"released", "deployment_confirmed", "evidence_archived", "cleaned",
|
||||
];
|
||||
|
||||
// ── Python repr helpers (exception messages are the contract) ───────────────
|
||||
|
||||
/// Python repr() of a string: single quotes preferred, double quotes when
|
||||
/// the value contains ' and not "; backslash + control chars escaped.
|
||||
pub(crate) fn py_repr_str(s: &str) -> String {
|
||||
use std::fmt::Write as _;
|
||||
let quote = if s.contains('\'') && !s.contains('"') { '"' } else { '\'' };
|
||||
let mut out = String::new();
|
||||
out.push(quote);
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
c if c == quote => {
|
||||
out.push('\\');
|
||||
out.push(c);
|
||||
}
|
||||
c if (c as u32) < 0x20 || (c as u32) == 0x7F => {
|
||||
let _ = write!(out, "\\x{:02x}", c as u32);
|
||||
}
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push(quote);
|
||||
out
|
||||
}
|
||||
|
||||
/// Python repr() of the module-level tuple constants — the ValueError
|
||||
/// message freezes `('project', 'domain', …)` verbatim.
|
||||
fn py_tuple_repr(items: &[&str]) -> String {
|
||||
let inner: Vec<String> = items.iter().map(|s| py_repr_str(s)).collect();
|
||||
format!("({})", inner.join(", "))
|
||||
}
|
||||
|
||||
// ── injectable I/O seam ─────────────────────────────────────────────────────
|
||||
|
||||
/// The model-I/O seam: save()/reload() never touch the filesystem
|
||||
/// directly — the FFI layer installs [`StdIo`], tests install recorders.
|
||||
pub trait SmIo {
|
||||
fn mkdir_parents(&self, dir: &str) -> Result<(), String>;
|
||||
fn write_text(&self, path: &str, contents: &str) -> Result<(), String>;
|
||||
fn read_text(&self, path: &str) -> Result<String, String>;
|
||||
fn is_file(&self, path: &str) -> bool;
|
||||
}
|
||||
|
||||
pub struct StdIo;
|
||||
|
||||
impl SmIo for StdIo {
|
||||
fn mkdir_parents(&self, dir: &str) -> Result<(), String> {
|
||||
if dir.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(dir).map_err(|e| format!("mkdir {}: {}", dir, e))
|
||||
}
|
||||
fn write_text(&self, path: &str, contents: &str) -> Result<(), String> {
|
||||
std::fs::write(path, contents).map_err(|e| format!("write {}: {}", path, e))
|
||||
}
|
||||
fn read_text(&self, path: &str) -> Result<String, String> {
|
||||
std::fs::read_to_string(path).map_err(|e| format!("read {}: {}", path, e))
|
||||
}
|
||||
fn is_file(&self, path: &str) -> bool {
|
||||
std::path::Path::new(path).is_file()
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter: forward the save path of [`SmIo`] to the structural_store
|
||||
/// save seam (the store owns the on-disk law).
|
||||
struct SsAdapter<'a>(&'a dyn SmIo);
|
||||
|
||||
impl structural_store::gap::SsIo for SsAdapter<'_> {
|
||||
fn mkdir_parents(&self, dir: &str) -> Result<(), String> {
|
||||
self.0.mkdir_parents(dir)
|
||||
}
|
||||
fn write_text(&self, path: &str, contents: &str) -> Result<(), String> {
|
||||
self.0.write_text(path, contents)
|
||||
}
|
||||
}
|
||||
|
||||
// ── persistence (oracle save/reload over the structural_store core) ─────────
|
||||
|
||||
/// StructuralModel.save — stamp schema_version, then the store's save law.
|
||||
pub fn save(doc: &mut Value, path: &str, io: &dyn SmIo) -> Result<(), String> {
|
||||
if let Some(o) = doc.as_object_mut() {
|
||||
o.insert("schema_version".into(), json!(SCHEMA_VERSION));
|
||||
}
|
||||
structural_store::gap::save(doc, path, &SsAdapter(io)).map(|_| ())
|
||||
}
|
||||
|
||||
/// StructuralModel.reload — load the doc if the file parses; None keeps
|
||||
/// the in-memory doc (oracle: `if doc is not None: self.doc = doc`).
|
||||
pub fn reload(path: &str, io: &dyn SmIo) -> Option<Value> {
|
||||
if !io.is_file(path) {
|
||||
return None;
|
||||
}
|
||||
io.read_text(path).ok().and_then(|t| serde_json::from_str(&t).ok())
|
||||
}
|
||||
|
||||
// ── entity construction (oracle structural_store.add_entity, mirrored) ──────
|
||||
|
||||
fn maturity_rank(state: &str) -> i64 {
|
||||
match state {
|
||||
"DECLARED" => 0,
|
||||
"PRESENT" => 1,
|
||||
"WIRED" => 2,
|
||||
"REACHABLE" => 3,
|
||||
"EXERCISED" => 4,
|
||||
"LIVE_PROVEN" => 5,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn entities_arr_mut(doc: &mut Value) -> Option<&mut Vec<Value>> {
|
||||
doc.get_mut("entities").and_then(|e| e.as_array_mut())
|
||||
}
|
||||
|
||||
/// Python `e[k] = sorted(set(e.get(k, [])) | set(new))` — sorted union.
|
||||
fn merge_sorted_set(o: &mut Map<String, Value>, key: &str, add: &[String]) {
|
||||
if add.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut vals: Vec<String> = o
|
||||
.get(key)
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(|x| x.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
for v in add {
|
||||
if !vals.contains(v) {
|
||||
vals.push(v.clone());
|
||||
}
|
||||
}
|
||||
vals.sort();
|
||||
o.insert(key.to_string(), json!(vals));
|
||||
}
|
||||
|
||||
/// Oracle structural_store.add_entity: high-water maturity, merge
|
||||
/// tags/identities/evidence/ext, refresh observed_at; fresh entities
|
||||
/// carry the full key set. Returns the entity (a live reference shape).
|
||||
fn add_entity(doc: &mut Value, id: &str, kind: &str, maturity: &str,
|
||||
evidence_refs: &[String], now_iso: &str) -> Value {
|
||||
if let Some(list) = entities_arr_mut(doc) {
|
||||
for e in list.iter_mut() {
|
||||
if e.get("id").and_then(|x| x.as_str()) == Some(id) {
|
||||
let o = e.as_object_mut().unwrap();
|
||||
let current = o
|
||||
.get("maturity")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("DECLARED")
|
||||
.to_string();
|
||||
if !maturity.is_empty()
|
||||
&& maturity_rank(maturity) > maturity_rank(¤t)
|
||||
{
|
||||
o.insert("maturity".into(), json!(maturity));
|
||||
}
|
||||
merge_sorted_set(o, "evidence_refs", evidence_refs);
|
||||
o.insert("observed_at".into(), json!(now_iso));
|
||||
return e.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ent = Map::new();
|
||||
ent.insert("id".into(), json!(id));
|
||||
ent.insert("kind".into(), json!(kind));
|
||||
ent.insert("maturity".into(), json!(maturity));
|
||||
ent.insert("tags".into(), json!([]));
|
||||
ent.insert("identities".into(), json!({}));
|
||||
let mut refs: Vec<String> = evidence_refs.to_vec();
|
||||
refs.sort();
|
||||
ent.insert("evidence_refs".into(), json!(refs));
|
||||
ent.insert("observed_at".into(), json!(now_iso));
|
||||
let v = Value::Object(ent);
|
||||
doc.as_object_mut()
|
||||
.unwrap()
|
||||
.entry("entities".to_string())
|
||||
.or_insert(json!([]))
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(v.clone());
|
||||
v
|
||||
}
|
||||
|
||||
/// Oracle structural_store.add_relationship: dedupe by (type, from, to);
|
||||
/// no-evidence edges are CANDIDATE. The appended entry and the returned
|
||||
/// value are the SAME object in Python — relate() mutates both through
|
||||
/// it, so the aliasing is reproduced (authority/status land on the stored
|
||||
/// edge only when it was appended).
|
||||
fn add_relationship(doc: &mut Value, rel_type: &str, from_id: &str,
|
||||
to_id: &str, evidence_refs: &[String], ext: Option<&Value>,
|
||||
now_iso: &str) -> (Value, bool) {
|
||||
let mut rel = Map::new();
|
||||
rel.insert("type".into(), json!(rel_type));
|
||||
rel.insert("from".into(), json!(from_id));
|
||||
rel.insert("to".into(), json!(to_id));
|
||||
let mut refs: Vec<String> = evidence_refs.to_vec();
|
||||
refs.sort();
|
||||
rel.insert("evidence_refs".into(), json!(refs));
|
||||
rel.insert("observed_at".into(), json!(now_iso));
|
||||
if let Some(x) = ext.filter(|x| py_truthy(x)) {
|
||||
rel.insert("ext".into(), x.clone());
|
||||
}
|
||||
let dup = doc
|
||||
.get("relationships")
|
||||
.and_then(|r| r.as_array())
|
||||
.map(|rels| {
|
||||
rels.iter().any(|r| {
|
||||
r.get("type").and_then(|t| t.as_str()) == Some(rel_type)
|
||||
&& r.get("from").and_then(|f| f.as_str()) == Some(from_id)
|
||||
&& r.get("to").and_then(|t| t.as_str()) == Some(to_id)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let mut appended = false;
|
||||
if !dup {
|
||||
let no_evidence = rel
|
||||
.get("evidence_refs")
|
||||
.and_then(|e| e.as_array())
|
||||
.map(|a| a.is_empty())
|
||||
.unwrap_or(true);
|
||||
if no_evidence && !rel.contains_key("status") {
|
||||
rel.insert("status".into(), json!("CANDIDATE"));
|
||||
}
|
||||
doc.as_object_mut()
|
||||
.unwrap()
|
||||
.entry("relationships".to_string())
|
||||
.or_insert(json!([]))
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(Value::Object(rel.clone()));
|
||||
appended = true;
|
||||
}
|
||||
(Value::Object(rel), appended)
|
||||
}
|
||||
|
||||
// ── StructuralModel._init_project ────────────────────────────────────────────
|
||||
|
||||
/// _init_project — the project entity + the root belongs_to edge
|
||||
/// (authority model-author, status root), persisted via the save law.
|
||||
pub fn _init_project(doc: &mut Value, project_id: &str, now_iso: &str,
|
||||
path: &str, io: &dyn SmIo) -> Result<(), String> {
|
||||
let pid = format!("project:{}", project_id);
|
||||
add_entity(doc, &pid, "project", "DECLARED",
|
||||
&["structural-model-init".to_string()], now_iso);
|
||||
let (mut root, appended) = add_relationship(
|
||||
doc, "belongs_to", &pid, &pid,
|
||||
&["structural-model-root".to_string()], None, now_iso,
|
||||
);
|
||||
if let Some(o) = root.as_object_mut() {
|
||||
// same aliasing the oracle has: the stored edge keeps these only
|
||||
// when this call appended it
|
||||
o.insert("authority".into(), json!("model-author"));
|
||||
o.insert("status".into(), json!("root"));
|
||||
}
|
||||
if appended {
|
||||
if let Some(list) = doc.get_mut("relationships").and_then(|r| r.as_array_mut()) {
|
||||
if let Some(stored) = list.last_mut() {
|
||||
if let Some(o) = stored.as_object_mut() {
|
||||
o.insert("authority".into(), json!("model-author"));
|
||||
o.insert("status".into(), json!("root"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
save(doc, path, io)
|
||||
}
|
||||
|
||||
// ── StructuralModel.add / relate ─────────────────────────────────────────────
|
||||
|
||||
/// add — deterministic id `kind:name` (rule 18E), identities.name, ext
|
||||
/// merge, optional parent belongs_to edge (model-author/active defaults).
|
||||
pub fn add(doc: &mut Value, kind: &str, name: &str, parent: Option<&str>,
|
||||
ext: Option<&Value>, evidence_ref: &str, now_iso: &str)
|
||||
-> Result<String, String> {
|
||||
if !ENTITY_KINDS.contains(&kind) {
|
||||
return Err(format!(
|
||||
"unknown entity kind {} — known: {}",
|
||||
py_repr_str(kind),
|
||||
py_tuple_repr(ENTITY_KINDS)
|
||||
));
|
||||
}
|
||||
let eid = format!("{}:{}", kind, name);
|
||||
let mut e = add_entity(doc, &eid, kind, "DECLARED",
|
||||
&[evidence_ref.to_string()], now_iso);
|
||||
{
|
||||
let o = e.as_object_mut().ok_or("KeyError: 'identities'")?;
|
||||
let identities = o
|
||||
.get_mut("identities")
|
||||
.ok_or("KeyError: 'identities'")?;
|
||||
if !identities.is_object() {
|
||||
return Err("KeyError: 'identities'".to_string());
|
||||
}
|
||||
identities
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("name".into(), json!(name));
|
||||
}
|
||||
// mirror the stored entity (add_entity returned a clone for the
|
||||
// existing-entity path; the mutation must land in the doc)
|
||||
if let Some(list) = entities_arr_mut(doc) {
|
||||
for ent in list.iter_mut() {
|
||||
if ent.get("id").and_then(|x| x.as_str()) == Some(eid.as_str()) {
|
||||
*ent = e.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(x) = ext.filter(|x| py_truthy(x)) {
|
||||
if let Some(list) = entities_arr_mut(doc) {
|
||||
for ent in list.iter_mut() {
|
||||
if ent.get("id").and_then(|x2| x2.as_str()) == Some(eid.as_str()) {
|
||||
let o = ent.as_object_mut().unwrap();
|
||||
let ext_slot = o
|
||||
.entry("ext".to_string())
|
||||
.or_insert(json!({}));
|
||||
if let (Some(m), Some(src)) = (ext_slot.as_object_mut(), x.as_object()) {
|
||||
for (k, v) in src {
|
||||
m.insert(k.clone(), v.clone());
|
||||
}
|
||||
} else if !ext_slot.is_object() {
|
||||
o.insert("ext".into(), x.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(p) = parent {
|
||||
relate(doc, &eid, "belongs_to", p, evidence_ref, "model-author",
|
||||
"active", None, now_iso)?;
|
||||
}
|
||||
Ok(eid)
|
||||
}
|
||||
|
||||
/// relate — typed relationship with evidence + authority + status (the
|
||||
/// authority-graded INNOVATION). ext REPLACES (oracle `r["ext"] = ext`).
|
||||
#[allow(clippy::too_many_arguments)] // oracle signature: every field is a keyword arg
|
||||
pub fn relate(doc: &mut Value, from_id: &str, rel: &str, to_id: &str,
|
||||
evidence_ref: &str, authority: &str, status: &str,
|
||||
ext: Option<&Value>, now_iso: &str) -> Result<Value, String> {
|
||||
if !RELATION_TYPES.contains(&rel) {
|
||||
return Err(format!(
|
||||
"unknown relationship {} — known: {}",
|
||||
py_repr_str(rel),
|
||||
py_tuple_repr(RELATION_TYPES)
|
||||
));
|
||||
}
|
||||
let (mut r, appended) = add_relationship(
|
||||
doc, rel, from_id, to_id, &[evidence_ref.to_string()], ext, now_iso,
|
||||
);
|
||||
if let Some(o) = r.as_object_mut() {
|
||||
o.insert("authority".into(), json!(authority));
|
||||
o.insert("status".into(), json!(status));
|
||||
}
|
||||
if appended {
|
||||
if let Some(list) = doc.get_mut("relationships").and_then(|r2| r2.as_array_mut()) {
|
||||
if let Some(stored) = list.last_mut() {
|
||||
if let Some(o) = stored.as_object_mut() {
|
||||
o.insert("authority".into(), json!(authority));
|
||||
o.insert("status".into(), json!(status));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
// ── doc-based helpers (alias-following entity lookup, oracle entity()) ──────
|
||||
|
||||
fn rels_arr(doc: &Value) -> &[Value] {
|
||||
doc.get("relationships")
|
||||
.and_then(|r| r.as_array())
|
||||
.map(|a| a.as_slice())
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// Oracle structural_store.entity: exact id first, then alias entries.
|
||||
pub fn entity_lookup<'a>(doc: &'a Value, id: &str) -> Option<&'a Value> {
|
||||
let ents = doc.get("entities").and_then(|e| e.as_array())?;
|
||||
for e in ents {
|
||||
if e.get("id").and_then(|x| x.as_str()) == Some(id) {
|
||||
return Some(e);
|
||||
}
|
||||
}
|
||||
for e in ents {
|
||||
if let Some(aliases) = e.get("aliases").and_then(|a| a.as_array()) {
|
||||
if aliases.iter().any(|a| a.as_str() == Some(id)) {
|
||||
return Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Build the entity map the graph kernels take: id → entity, then alias
|
||||
/// → entity (first entity wins — the oracle's two-pass resolution).
|
||||
pub fn entities_map(doc: &Value) -> HashMap<String, Value> {
|
||||
let mut m = HashMap::new();
|
||||
if let Some(ents) = doc.get("entities").and_then(|e| e.as_array()) {
|
||||
for e in ents {
|
||||
if let Some(id) = e.get("id").and_then(|x| x.as_str()) {
|
||||
m.insert(id.to_string(), e.clone());
|
||||
}
|
||||
}
|
||||
for e in ents {
|
||||
if let Some(aliases) = e.get("aliases").and_then(|a| a.as_array()) {
|
||||
for a in aliases.iter().filter_map(|x| x.as_str()) {
|
||||
m.entry(a.to_string())
|
||||
.or_insert_with(|| e.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// Relationships of a doc as the minimal kernel view.
|
||||
pub fn rels_of(doc: &Value) -> Vec<Relationship> {
|
||||
rels_arr(doc)
|
||||
.iter()
|
||||
.filter_map(|r| serde_json::from_value::<Relationship>(r.clone()).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── T18 §18K: workspace lifecycle foundation ────────────────────────────────
|
||||
|
||||
/// workspace_record — the provenance record a contract carries.
|
||||
/// Generations are SHAs/dirs — never live-tree mutations (rule P).
|
||||
pub fn workspace_record(contract_id: &str, construction_base: &str,
|
||||
runtime_generation: &str, candidate_generation: &str,
|
||||
promotion_target: &str, rollback_generation: &str)
|
||||
-> Value {
|
||||
json!({
|
||||
"contract_id": contract_id,
|
||||
"construction_base": construction_base,
|
||||
"runtime_generation": runtime_generation,
|
||||
"candidate_generation": candidate_generation,
|
||||
"promotion_target": promotion_target,
|
||||
"rollback_generation": rollback_generation,
|
||||
"stages_completed": [],
|
||||
"lifecycle": WORKSPACE_LIFECYCLE_STAGES,
|
||||
})
|
||||
}
|
||||
|
||||
fn stages_index(stage: &str) -> Option<usize> {
|
||||
WORKSPACE_LIFECYCLE_STAGES.iter().position(|s| *s == stage)
|
||||
}
|
||||
|
||||
/// workspace_advance — order-enforced one-stage advance.
|
||||
pub fn workspace_advance(record: &mut Value, stage: &str, evidence: &str)
|
||||
-> Result<(), String> {
|
||||
let cur = stages_index(stage)
|
||||
.ok_or_else(|| format!("unknown workspace stage {}", py_repr_str(stage)))?;
|
||||
let done = record
|
||||
.as_object_mut()
|
||||
.ok_or("KeyError: 'stages_completed'")?
|
||||
.get_mut("stages_completed")
|
||||
.ok_or("KeyError: 'stages_completed'")?;
|
||||
let arr = done.as_array_mut().ok_or("KeyError: 'stages_completed'")?;
|
||||
if let Some(last) = arr.last().and_then(|v| v.as_str()) {
|
||||
let last_i = stages_index(last)
|
||||
.ok_or_else(|| format!("unknown workspace stage {}", py_repr_str(last)))?;
|
||||
if cur != last_i + 1 {
|
||||
return Err(format!(
|
||||
"lifecycle order violation: {} → {}",
|
||||
last, stage
|
||||
));
|
||||
}
|
||||
} else if stage != WORKSPACE_LIFECYCLE_STAGES[0] {
|
||||
return Err(format!(
|
||||
"lifecycle must start at {}, got {}",
|
||||
WORKSPACE_LIFECYCLE_STAGES[0],
|
||||
py_repr_str(stage)
|
||||
));
|
||||
}
|
||||
arr.push(json!(stage));
|
||||
if !evidence.is_empty() {
|
||||
let ev: String = evidence.chars().take(200).collect();
|
||||
let o = record.as_object_mut().unwrap();
|
||||
let list = o
|
||||
.entry("evidence".to_string())
|
||||
.or_insert(json!([]));
|
||||
list.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!({"stage": stage, "evidence": ev}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── FFI surface (doc-envelope law) ──────────────────────────────────────────
|
||||
|
||||
use crate::{envelope, get_str, parse_arg, read_arg};
|
||||
|
||||
fn arg_doc(a: &Value, key: &str) -> Result<Value, String> {
|
||||
a.get(key).cloned().ok_or(format!("arg.{} required", key))
|
||||
}
|
||||
|
||||
/// sm_add({doc, kind, name, parent?, ext?, evidence_ref?, now}) -> {doc, eid}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sm_add(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| parse_arg(&t))
|
||||
.and_then(|a| {
|
||||
let mut doc = arg_doc(&a, "doc")?;
|
||||
let kind = get_str(&a, "kind").ok_or("arg.kind required")?.to_string();
|
||||
let name = get_str(&a, "name").ok_or("arg.name required")?.to_string();
|
||||
let parent = get_str(&a, "parent").map(|s| s.to_string());
|
||||
let ext = a.get("ext").cloned();
|
||||
let evidence_ref = get_str(&a, "evidence_ref").unwrap_or("structural-model").to_string();
|
||||
let now = get_str(&a, "now").ok_or("arg.now required")?.to_string();
|
||||
let eid = add(&mut doc, &kind, &name, parent.as_deref(),
|
||||
ext.as_ref(), &evidence_ref, &now)?;
|
||||
Ok(json!({"doc": doc, "eid": eid}))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sm_relate({doc, from_id, rel, to_id, evidence_ref?, authority?, status?,
|
||||
/// ext?, now}) -> {doc, rel}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sm_relate(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| parse_arg(&t))
|
||||
.and_then(|a| {
|
||||
let mut doc = arg_doc(&a, "doc")?;
|
||||
let from_id = get_str(&a, "from_id").ok_or("arg.from_id required")?.to_string();
|
||||
let rel = get_str(&a, "rel").ok_or("arg.rel required")?.to_string();
|
||||
let to_id = get_str(&a, "to_id").ok_or("arg.to_id required")?.to_string();
|
||||
let evidence_ref = get_str(&a, "evidence_ref").unwrap_or("structural-model").to_string();
|
||||
let authority = get_str(&a, "authority").unwrap_or("model-author").to_string();
|
||||
let status = get_str(&a, "status").unwrap_or("active").to_string();
|
||||
let ext = a.get("ext").cloned();
|
||||
let now = get_str(&a, "now").ok_or("arg.now required")?.to_string();
|
||||
let r = relate(&mut doc, &from_id, &rel, &to_id, &evidence_ref,
|
||||
&authority, &status, ext.as_ref(), &now)?;
|
||||
Ok(json!({"doc": doc, "rel": r}))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sm_init_project({doc, project_id, path, now}) -> {doc, path}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sm_init_project(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| parse_arg(&t))
|
||||
.and_then(|a| {
|
||||
let mut doc = arg_doc(&a, "doc")?;
|
||||
let project_id = get_str(&a, "project_id").ok_or("arg.project_id required")?.to_string();
|
||||
let path = get_str(&a, "path").ok_or("arg.path required")?.to_string();
|
||||
let now = get_str(&a, "now").ok_or("arg.now required")?.to_string();
|
||||
_init_project(&mut doc, &project_id, &now, &path, &StdIo)?;
|
||||
Ok(json!({"doc": doc, "path": path}))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sm_save({doc, path}) -> {doc, path}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sm_save(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| parse_arg(&t))
|
||||
.and_then(|a| {
|
||||
let mut doc = arg_doc(&a, "doc")?;
|
||||
let path = get_str(&a, "path").ok_or("arg.path required")?.to_string();
|
||||
save(&mut doc, &path, &StdIo)?;
|
||||
Ok(json!({"doc": doc, "path": path}))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sm_reload({path}) -> doc | null
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sm_reload(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| parse_arg(&t))
|
||||
.and_then(|a| {
|
||||
let path = get_str(&a, "path").ok_or("arg.path required")?.to_string();
|
||||
Ok(reload(&path, &StdIo).unwrap_or(Value::Null))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sm_workspace_record({contract_id, construction_base, runtime_generation?,
|
||||
/// candidate_generation?, promotion_target?,
|
||||
/// rollback_generation?}) -> record
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sm_workspace_record(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| parse_arg(&t))
|
||||
.and_then(|a| {
|
||||
let contract_id = get_str(&a, "contract_id").ok_or("arg.contract_id required")?.to_string();
|
||||
let construction_base = get_str(&a, "construction_base")
|
||||
.ok_or("arg.construction_base required")?.to_string();
|
||||
Ok(workspace_record(
|
||||
&contract_id,
|
||||
&construction_base,
|
||||
get_str(&a, "runtime_generation").unwrap_or(""),
|
||||
get_str(&a, "candidate_generation").unwrap_or(""),
|
||||
get_str(&a, "promotion_target").unwrap_or("gitea/master"),
|
||||
get_str(&a, "rollback_generation").unwrap_or(""),
|
||||
))
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
/// sm_workspace_advance({record, stage, evidence?}) -> record
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sm_workspace_advance(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| parse_arg(&t))
|
||||
.and_then(|a| {
|
||||
let mut record = arg_doc(&a, "record")?;
|
||||
let stage = get_str(&a, "stage").ok_or("arg.stage required")?.to_string();
|
||||
let evidence = get_str(&a, "evidence").unwrap_or("").to_string();
|
||||
workspace_advance(&mut record, &stage, &evidence)?;
|
||||
Ok(record)
|
||||
});
|
||||
envelope(result)
|
||||
}
|
||||
@@ -1,23 +1,15 @@
|
||||
//! structural_model — structural model for impact analysis (W6).
|
||||
//! structural_model — T18 §18E/§18F structural scaffold for scaffolded
|
||||
//! projects (dichotomy-wave gap-closing port). Query kernels are the
|
||||
//! minimal-edge view; doc kernels + persistence + lifecycle live in gap.
|
||||
pub mod gap;
|
||||
pub use gap::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod frozen_battery;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct StructuralModel {
|
||||
pub entities: HashMap<String, serde_json::Value>,
|
||||
pub deployment_surfaces: Vec<String>,
|
||||
}
|
||||
|
||||
impl StructuralModel {
|
||||
pub fn get(&self, id: &str) -> Option<&serde_json::Value> {
|
||||
self.entities.get(id)
|
||||
}
|
||||
|
||||
pub fn deployment_surfaces_for(&self, _target: &str) -> Vec<String> {
|
||||
self.deployment_surfaces.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ── #2479 RUST batch 20: the graph-walk kernels, oracle-parity port ────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
@@ -26,6 +18,8 @@ pub struct Relationship {
|
||||
pub rel_type: String,
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
/// children — belongs_to edges targeting eid (self-edges excluded).
|
||||
@@ -69,28 +63,120 @@ pub fn owning_domain(rels: &[Relationship], eid: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// dependencies — depends_on edges from eid.
|
||||
/// dependencies — depends_on edges from eid (status "stale" excluded).
|
||||
pub fn dependencies(rels: &[Relationship], eid: &str) -> Vec<String> {
|
||||
rels.iter()
|
||||
.filter(|r| r.rel_type == "depends_on" && r.from == eid)
|
||||
let mut out: Vec<String> = rels
|
||||
.iter()
|
||||
.filter(|r| {
|
||||
r.rel_type == "depends_on"
|
||||
&& r.from == eid
|
||||
&& r.status.as_deref() != Some("stale")
|
||||
})
|
||||
.map(|r| r.to.clone())
|
||||
.collect()
|
||||
.collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
/// consumers — reverse deps + consumes (Python-identical union).
|
||||
/// consumers — reverse deps + consumes (status "stale" excluded), sorted.
|
||||
pub fn consumers(rels: &[Relationship], eid: &str) -> Vec<String> {
|
||||
rels.iter()
|
||||
.filter(|r| (r.rel_type == "depends_on" || r.rel_type == "consumes") && r.to == eid)
|
||||
let mut out: Vec<String> = rels
|
||||
.iter()
|
||||
.filter(|r| {
|
||||
(r.rel_type == "depends_on" || r.rel_type == "consumes")
|
||||
&& r.to == eid
|
||||
&& r.status.as_deref() != Some("stale")
|
||||
})
|
||||
.map(|r| r.from.clone())
|
||||
.collect()
|
||||
.collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
/// interfaces_for — provides edges from eid.
|
||||
/// interfaces_for — provides edges from eid, sorted.
|
||||
pub fn interfaces_for(rels: &[Relationship], eid: &str) -> Vec<String> {
|
||||
rels.iter()
|
||||
let mut out: Vec<String> = rels
|
||||
.iter()
|
||||
.filter(|r| r.rel_type == "provides" && r.from == eid)
|
||||
.map(|r| r.to.clone())
|
||||
.collect()
|
||||
.collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
/// state_owned — owns edges from eid, sorted.
|
||||
pub fn state_owned(rels: &[Relationship], eid: &str) -> Vec<String> {
|
||||
let mut out: Vec<String> = rels
|
||||
.iter()
|
||||
.filter(|r| r.rel_type == "owns" && r.from == eid)
|
||||
.map(|r| r.to.clone())
|
||||
.collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
/// verification_surfaces — verified_by edges from eid, sorted.
|
||||
pub fn verification_surfaces(rels: &[Relationship], eid: &str) -> Vec<String> {
|
||||
let mut out: Vec<String> = rels
|
||||
.iter()
|
||||
.filter(|r| r.rel_type == "verified_by" && r.from == eid)
|
||||
.map(|r| r.to.clone())
|
||||
.collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
/// deployment_surfaces — deployed_by edges from eid, sorted.
|
||||
pub fn deployment_surfaces(rels: &[Relationship], eid: &str) -> Vec<String> {
|
||||
let mut out: Vec<String> = rels
|
||||
.iter()
|
||||
.filter(|r| r.rel_type == "deployed_by" && r.from == eid)
|
||||
.map(|r| r.to.clone())
|
||||
.collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
/// canonical_state_owner — the state_store this entity owns, or itself
|
||||
/// when it IS one (oracle get() resolves alias entries).
|
||||
pub fn canonical_state_owner(
|
||||
rels: &[Relationship],
|
||||
entities: &HashMap<String, serde_json::Value>,
|
||||
eid: &str,
|
||||
) -> Option<String> {
|
||||
if let Some(e) = entities.get(eid) {
|
||||
if e.get("kind").and_then(|k| k.as_str()) == Some("state_store") {
|
||||
return Some(eid.to_string());
|
||||
}
|
||||
}
|
||||
state_owned(rels, eid).into_iter().next()
|
||||
}
|
||||
|
||||
/// supersession — supersedes / superseded_by / legacy_of, each sorted.
|
||||
pub fn supersession(rels: &[Relationship], eid: &str) -> serde_json::Value {
|
||||
let mut supersedes: Vec<String> = rels
|
||||
.iter()
|
||||
.filter(|r| r.rel_type == "supersedes" && r.from == eid)
|
||||
.map(|r| r.to.clone())
|
||||
.collect();
|
||||
let mut superseded_by: Vec<String> = rels
|
||||
.iter()
|
||||
.filter(|r| r.rel_type == "supersedes" && r.to == eid)
|
||||
.map(|r| r.from.clone())
|
||||
.collect();
|
||||
let mut legacy_of: Vec<String> = rels
|
||||
.iter()
|
||||
.filter(|r| r.rel_type == "legacy_of" && r.from == eid)
|
||||
.map(|r| r.to.clone())
|
||||
.collect();
|
||||
supersedes.sort();
|
||||
superseded_by.sort();
|
||||
legacy_of.sort();
|
||||
serde_json::json!({
|
||||
"supersedes": supersedes,
|
||||
"superseded_by": superseded_by,
|
||||
"legacy_of": legacy_of,
|
||||
})
|
||||
}
|
||||
|
||||
/// impact_closure — the structurally bounded scope (rule J), oracle-identical.
|
||||
@@ -101,11 +187,14 @@ pub fn impact_closure(
|
||||
max_depth: usize,
|
||||
) -> serde_json::Value {
|
||||
if !entities.contains_key(eid) {
|
||||
return serde_json::json!({"error": format!("unknown entity {:?}", eid)});
|
||||
return serde_json::json!({
|
||||
"error": format!("unknown entity {}", gap::py_repr_str(eid))
|
||||
});
|
||||
}
|
||||
// reverse-dependency closure (consumers, transitively, bounded)
|
||||
let mut seen = vec![eid.to_string()];
|
||||
let mut consumers_all: Vec<String> = Vec::new();
|
||||
let mut frontier = vec![eid.to_string()];
|
||||
let mut consumers_all: Vec<String> = Vec::new();
|
||||
let mut depth = 0usize;
|
||||
while !frontier.is_empty() && depth < max_depth {
|
||||
let mut nxt = Vec::new();
|
||||
@@ -122,6 +211,7 @@ pub fn impact_closure(
|
||||
depth += 1;
|
||||
}
|
||||
let direct_deps = dependencies(rels, eid);
|
||||
// ripple through PROVIDED interfaces (Kythe exports semantics)
|
||||
for iface in interfaces_for(rels, eid) {
|
||||
for c in consumers(rels, &iface) {
|
||||
if !seen.contains(&c) {
|
||||
@@ -141,13 +231,18 @@ pub fn impact_closure(
|
||||
.map(|r| r.to.clone())
|
||||
.collect();
|
||||
consumed.sort();
|
||||
let mut state_surfaces: Vec<String> = rels
|
||||
.iter()
|
||||
.filter(|r| r.rel_type == "owns" && mutation.contains(&r.from))
|
||||
.map(|r| r.to.clone())
|
||||
.collect();
|
||||
state_surfaces.sort();
|
||||
state_surfaces.dedup();
|
||||
let scope_of = |kernel: fn(&[Relationship], &str) -> Vec<String>| {
|
||||
let mut out: Vec<String> = vec![];
|
||||
for e in &mutation {
|
||||
for s in kernel(rels, e) {
|
||||
if !out.contains(&s) {
|
||||
out.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out
|
||||
};
|
||||
serde_json::json!({
|
||||
"target": eid,
|
||||
"owning_domain": owning_domain(rels, eid),
|
||||
@@ -157,17 +252,27 @@ pub fn impact_closure(
|
||||
"interfaces_provided": provided,
|
||||
"interfaces_consumed": consumed,
|
||||
},
|
||||
"state_surfaces": state_surfaces,
|
||||
"state_surfaces": scope_of(state_owned),
|
||||
"verification_scope": scope_of(verification_surfaces),
|
||||
"deployment_scope": scope_of(deployment_surfaces),
|
||||
"invariants": [
|
||||
format!("belongs_to hierarchy of {} remains stable", eid),
|
||||
"canonical-state ownership unchanged"],
|
||||
"non_goals": [],
|
||||
"uncertainty": [
|
||||
{"kind": "dynamic-consumer",
|
||||
"note": "static model cannot see dynamic dispatch/string-keyed \
|
||||
lookup/late-binding consumers — reconcilers must \
|
||||
verify at runtime"}],
|
||||
"closure_depth": depth,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── FFI surface (facade burn-down, 2026-10-10) ───────────────────────────────
|
||||
// ── FFI surface (facade burn-down, 2026-10-10; gap-close 2026-10-13) ────────
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
pub(crate) fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".to_string());
|
||||
}
|
||||
@@ -177,7 +282,7 @@ fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
.map_err(|e| format!("argument is not valid UTF-8: {}", e))
|
||||
}
|
||||
|
||||
fn envelope(value: Result<serde_json::Value, String>) -> *mut c_char {
|
||||
pub(crate) fn envelope(value: Result<serde_json::Value, String>) -> *mut c_char {
|
||||
let body = match value {
|
||||
Ok(v) => serde_json::json!({"ok": true, "value": v}),
|
||||
Err(e) => serde_json::json!({"ok": false, "err": e}),
|
||||
@@ -187,18 +292,28 @@ fn envelope(value: Result<serde_json::Value, String>) -> *mut c_char {
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn parse_arg(text: &str) -> Result<serde_json::Value, String> {
|
||||
pub(crate) fn parse_arg(text: &str) -> Result<serde_json::Value, String> {
|
||||
serde_json::from_str(text).map_err(|e| format!("argument JSON parse failed: {}", e))
|
||||
}
|
||||
|
||||
pub(crate) fn get_str<'a>(v: &'a serde_json::Value, key: &str) -> Option<&'a str> {
|
||||
v.get(key).and_then(|x| x.as_str())
|
||||
}
|
||||
|
||||
fn rels_from(a: &serde_json::Value) -> Vec<Relationship> {
|
||||
// rel-only payloads carry "relationships"; doc payloads nest it too
|
||||
a.get("relationships")
|
||||
.or_else(|| a.get("doc").and_then(|d| d.get("relationships")))
|
||||
.and_then(|r| serde_json::from_value::<Vec<Relationship>>(r.clone()).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// sm_query({"relationships": [...], "eid": "...", "op": "children|beneath|
|
||||
/// owning_domain|dependencies|consumers|interfaces_for"})
|
||||
/// owning_domain|dependencies|consumers|interfaces_for|
|
||||
/// state_owned|verification_surfaces|deployment_surfaces|
|
||||
/// supersession"})
|
||||
/// Doc-shaped variant: {"doc": {...}, "eid", "op": "canonical_state_owner|
|
||||
/// impact_closure", "max_depth"?} — entities resolve aliases.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sm_query(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
@@ -214,6 +329,27 @@ pub extern "C" fn sm_query(arg: *const c_char) -> *mut c_char {
|
||||
"dependencies" => Ok(serde_json::json!(dependencies(&rels, eid))),
|
||||
"consumers" => Ok(serde_json::json!(consumers(&rels, eid))),
|
||||
"interfaces_for" => Ok(serde_json::json!(interfaces_for(&rels, eid))),
|
||||
"state_owned" => Ok(serde_json::json!(state_owned(&rels, eid))),
|
||||
"verification_surfaces" => {
|
||||
Ok(serde_json::json!(verification_surfaces(&rels, eid)))
|
||||
}
|
||||
"deployment_surfaces" => {
|
||||
Ok(serde_json::json!(deployment_surfaces(&rels, eid)))
|
||||
}
|
||||
"supersession" => Ok(supersession(&rels, eid)),
|
||||
"canonical_state_owner" => {
|
||||
let doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
Ok(serde_json::json!(canonical_state_owner(
|
||||
&rels,
|
||||
&entities_map(&doc),
|
||||
eid
|
||||
)))
|
||||
}
|
||||
"impact_closure" => {
|
||||
let doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let max_depth = a.get("max_depth").and_then(|m| m.as_u64()).unwrap_or(8) as usize;
|
||||
Ok(impact_closure(&rels, &entities_map(&doc), eid, max_depth))
|
||||
}
|
||||
_ => Err(format!("unknown op: {}", op)),
|
||||
}
|
||||
});
|
||||
@@ -240,41 +376,49 @@ mod branch_battery {
|
||||
rel_type: "belongs_to".into(),
|
||||
from: "component:alpha".into(),
|
||||
to: "domain:core".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "belongs_to".into(),
|
||||
from: "component:beta".into(),
|
||||
to: "domain:core".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "belongs_to".into(),
|
||||
from: "interface:ix".into(),
|
||||
to: "domain:core".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "belongs_to".into(),
|
||||
from: "state_store:s1".into(),
|
||||
to: "domain:core".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "depends_on".into(),
|
||||
from: "component:beta".into(),
|
||||
to: "component:alpha".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "provides".into(),
|
||||
from: "component:alpha".into(),
|
||||
to: "interface:ix".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "consumes".into(),
|
||||
from: "component:beta".into(),
|
||||
to: "interface:ix".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "owns".into(),
|
||||
from: "component:alpha".into(),
|
||||
to: "state_store:s1".into(),
|
||||
status: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -347,12 +491,35 @@ mod branch_battery {
|
||||
serde_json::json!(["interface:ix"])
|
||||
);
|
||||
assert_eq!(ic["state_surfaces"], serde_json::json!(["state_store:s1"]));
|
||||
// full oracle shape (§18F): every section present
|
||||
for key in [
|
||||
"verification_scope",
|
||||
"deployment_scope",
|
||||
"invariants",
|
||||
"non_goals",
|
||||
"uncertainty",
|
||||
"closure_depth",
|
||||
] {
|
||||
assert!(ic.get(key).is_some(), "missing section {}", key);
|
||||
}
|
||||
assert_eq!(ic["verification_scope"], serde_json::json!([]));
|
||||
assert_eq!(ic["deployment_scope"], serde_json::json!([]));
|
||||
// depth counts the terminating empty-frontier pass (oracle-identical)
|
||||
assert_eq!(ic["closure_depth"], 2);
|
||||
assert_eq!(
|
||||
ic["invariants"],
|
||||
serde_json::json!([
|
||||
"belongs_to hierarchy of component:alpha remains stable",
|
||||
"canonical-state ownership unchanged"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn impact_closure_unknown_entity() {
|
||||
let ic = impact_closure(&rels(), &entities(), "nope", 8);
|
||||
assert!(ic["error"].as_str().unwrap().contains("unknown entity"));
|
||||
// Python repr quoting — the oracle freezes {'nope'}
|
||||
assert_eq!(ic["error"], "unknown entity 'nope'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -361,9 +528,103 @@ mod branch_battery {
|
||||
rel_type: "calls".into(),
|
||||
from: "a".into(),
|
||||
to: "b".into(),
|
||||
status: None,
|
||||
}];
|
||||
assert_eq!(owning_domain(&r, "a"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_filter_and_surfaces() {
|
||||
let r = vec![
|
||||
Relationship {
|
||||
rel_type: "depends_on".into(),
|
||||
from: "component:a".into(),
|
||||
to: "component:stale".into(),
|
||||
status: Some("stale".into()),
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "depends_on".into(),
|
||||
from: "component:a".into(),
|
||||
to: "component:fresh".into(),
|
||||
status: Some("STALE".into()),
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "owns".into(),
|
||||
from: "component:a".into(),
|
||||
to: "state_store:s".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "verified_by".into(),
|
||||
from: "component:a".into(),
|
||||
to: "test_suite:t".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "deployed_by".into(),
|
||||
from: "component:a".into(),
|
||||
to: "deployment_surface:d".into(),
|
||||
status: Some("stale".into()),
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "supersedes".into(),
|
||||
from: "component:a".into(),
|
||||
to: "component:old".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "supersedes".into(),
|
||||
from: "component:new".into(),
|
||||
to: "component:a".into(),
|
||||
status: None,
|
||||
},
|
||||
Relationship {
|
||||
rel_type: "legacy_of".into(),
|
||||
from: "component:a".into(),
|
||||
to: "component:ancient".into(),
|
||||
status: None,
|
||||
},
|
||||
];
|
||||
// lowercase "stale" filtered; uppercase STALE is a different string
|
||||
assert_eq!(dependencies(&r, "component:a"), vec!["component:fresh"]);
|
||||
assert_eq!(consumers(&r, "component:stale"), Vec::<String>::new());
|
||||
assert_eq!(state_owned(&r, "component:a"), vec!["state_store:s"]);
|
||||
assert_eq!(
|
||||
verification_surfaces(&r, "component:a"),
|
||||
vec!["test_suite:t"]
|
||||
);
|
||||
// surfaces carry NO stale filter (oracle)
|
||||
assert_eq!(
|
||||
deployment_surfaces(&r, "component:a"),
|
||||
vec!["deployment_surface:d"]
|
||||
);
|
||||
assert_eq!(
|
||||
supersession(&r, "component:a"),
|
||||
serde_json::json!({
|
||||
"supersedes": ["component:old"],
|
||||
"superseded_by": ["component:new"],
|
||||
"legacy_of": ["component:ancient"],
|
||||
})
|
||||
);
|
||||
let mut ents = HashMap::new();
|
||||
ents.insert(
|
||||
"state_store:s".to_string(),
|
||||
serde_json::json!({"id": "state_store:s", "kind": "state_store"}),
|
||||
);
|
||||
ents.insert(
|
||||
"component:a".to_string(),
|
||||
serde_json::json!({"id": "component:a", "kind": "component"}),
|
||||
);
|
||||
assert_eq!(
|
||||
canonical_state_owner(&r, &ents, "state_store:s"),
|
||||
Some("state_store:s".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
canonical_state_owner(&r, &ents, "component:a"),
|
||||
Some("state_store:s".to_string())
|
||||
);
|
||||
assert_eq!(canonical_state_owner(&r, &ents, "component:zz"), None);
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod frozen_graph {
|
||||
@@ -371,10 +632,10 @@ mod frozen_graph {
|
||||
|
||||
fn rels() -> Vec<Relationship> {
|
||||
vec![
|
||||
Relationship { rel_type: "belongs_to".into(), from: "component:a".into(), to: "domain:d1".into() },
|
||||
Relationship { rel_type: "belongs_to".into(), from: "component:b".into(), to: "domain:d1".into() },
|
||||
Relationship { rel_type: "depends_on".into(), from: "component:a".into(), to: "component:b".into() },
|
||||
Relationship { rel_type: "provides".into(), from: "component:a".into(), to: "interface:i1".into() },
|
||||
Relationship { rel_type: "belongs_to".into(), from: "component:a".into(), to: "domain:d1".into(), status: None },
|
||||
Relationship { rel_type: "belongs_to".into(), from: "component:b".into(), to: "domain:d1".into(), status: None },
|
||||
Relationship { rel_type: "depends_on".into(), from: "component:a".into(), to: "component:b".into(), status: None },
|
||||
Relationship { rel_type: "provides".into(), from: "component:a".into(), to: "interface:i1".into(), status: None },
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,36 @@
|
||||
# structural_model gap-closing port — verification summary
|
||||
|
||||
Module: structural_model (dichotomy wave, issue #2479 tranche, worktree rune-wave-ct)
|
||||
Date: 2026-10-14
|
||||
|
||||
## Coverage
|
||||
- dichotomy audit: 9/20 PARTIAL → **21/21 FULL, missing []** (the +1 fn is
|
||||
the `_stm_core` facade probe added by the burn-down; its contract is
|
||||
mirrored in the crate, same as structural_store's `_ss_core`).
|
||||
|
||||
## Evidence (all cross-checked by Read-back)
|
||||
| Gate | Evidence | Result |
|
||||
|---|---|---|
|
||||
| cargo test --release (battery) | verification/structural_model-cargo-test.log | 16 passed, 0 failed |
|
||||
| cargo clippy --all-targets -D warnings | verification/structural_model-clippy.log | exit 0 |
|
||||
| freeze_truth --verify (byte-identical) | verification/structural_model-freeze-verify.log | OK, 70 cases |
|
||||
| live parity (frozen + fuzz + dual-mode) | verification/structural_model-parity.log | 4620 checks, 0 failures |
|
||||
| Python suite, delegation LIVE | verification/structural_model-pytest-live.log | 53 passed; 2 failed + 12 errors PRE-EXISTING (byte-identical on stashed base) |
|
||||
| Python suite, RUNE_STM_CORE=python | verification/structural_model-pytest-pinned.log | same counts, IDENTICAL failure set (diff-verified) |
|
||||
| ruff check + format | (inline run) | All checks passed |
|
||||
|
||||
## Notes
|
||||
- The frozen truth (src/rust/structural_model/src/frozen_sm_truth.json) is
|
||||
embedded in the battery via include_str! — file and battery cannot drift.
|
||||
- The dead delegation seam (structural_model.py imported sm_loader — the
|
||||
SHARD_MANAGER loader with no query symbol; AttributeError → Python floor
|
||||
on every call) is FIXED to stm_loader (the crate's sm_* FFI). Fix-first,
|
||||
documented in stm_loader.py's docstring + the facade probe.
|
||||
- In-place law: workspace_advance mutates and returns the caller's dict —
|
||||
the facade mirrors it (record.clear(); record.update(out)); frozen-truth
|
||||
and live layers prove it.
|
||||
- Crash parity: re-adding an entity whose dict lacks "identities" raises
|
||||
KeyError in the oracle; the kernel returns "KeyError: 'identities'" and
|
||||
the loader re-raises KeyError.
|
||||
- save() stamps schema_version=1 BEFORE the store save — a stale 99 is
|
||||
coerced, never raised (frozen as save/schema_version_stamped).
|
||||
Reference in new issue
Block a user