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:
Nick committed 2026-10-14 05:53:36 +01:00
1 parent 1a1ac0b516
commit 00839d1af7
18 files changed
+15783 -160

No files matched your search

+6 -6
View File
@@ -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 |
+488
View File
@@ -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())
+397
View File
@@ -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())
+231
View File
@@ -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
View File
@@ -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
+37
View File
@@ -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]]
+4 -1
View File
@@ -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
+698
View File
@@ -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(&current)
{
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)
}
+311 -50
View File
@@ -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
+36
View File
@@ -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).