feat(spine): close PARTIAL gaps — dichotomy wave
Port the 5 audit-missing fns name-preserving into src/rust/spine/src/gap.rs (_module_imports, _gather_meta_inputs, _deployed_source_files, _gather_drift_inputs, _fresh_doc; IO behind injectable seams) plus machinery twins over the proven rune_spine kernels, with doc-envelope sp_* FFI (freed via cs_free_string). Audit spine 20/25 -> 25/25 (fn-level 1335 -> 1340). Battery replays the 21-case harness truth (byte-verified vs live oracle) + 35-case clocked battery VERBATIM; live parity 74/74 through the real FFI surface; clippy -D warnings clean; pytest no new failures either pin. Fixes pre-landing: clock-pin args leak (structural_store.now passes gmtime() positionally), malformed-import SyntaxError branch, test_ prefix in drift tested stems, kernel_absent battery dispatch.
This commit is contained in:
1 parent
96eb8c64e3
commit
b472eeb786
10 files changed
+5026
No files matched your search
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: the spine gap kernels over their REAL FFI surface (the
|
||||
src/rust/spine cdylib, sp_* doc-envelope symbols) vs the live Python
|
||||
oracle pinned RUNE_SPINE_CORE=python. Dichotomy wave #2479.
|
||||
|
||||
Replays the SAME corpus the frozen battery replays (the harness cases +
|
||||
the clocked sandbox cases from gen_spine_gap_truth.py — regenerated LIVE
|
||||
here, not read from the frozen file: ladder layer 4 wants port vs oracle,
|
||||
frozen files prove layer 3). Returned pointers are freed through
|
||||
cs_free_string (capability_scoring owns the free symbol — loader ABI law;
|
||||
both cdylibs resolve the same process allocator).
|
||||
|
||||
Comparison law (branch_battery_gap.rs): key-order-insensitive canon
|
||||
(spine._py_sorted on both sides) + byte-equal dumps; array order exact;
|
||||
error messages exact modulo the loader's "sp_* core error: " envelope
|
||||
prefix, which the oracle adds when the stub routes through spine_loader.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
# Importing the generator pins RUNE_SPINE_CORE=python (poisoned-truth law)
|
||||
# and freezes the process clock at FROZEN_NOW — the oracle side below runs
|
||||
# under exactly the pins that froze the truth.
|
||||
import gen_spine_gap_truth as gen # noqa: E402
|
||||
|
||||
from rune import spine # noqa: E402
|
||||
|
||||
SPINE_SO = REPO / "src" / "rust" / "spine" / "target" / "release" / "libspine.so"
|
||||
CS_SO = (
|
||||
REPO
|
||||
/ "src"
|
||||
/ "rust"
|
||||
/ "capability_scoring"
|
||||
/ "target"
|
||||
/ "release"
|
||||
/ "libcapability_scoring.so"
|
||||
)
|
||||
|
||||
SYMS = (
|
||||
"sp_module_imports", "sp_gather_meta_inputs", "sp_deployed_source_files",
|
||||
"sp_gather_drift_inputs", "sp_fresh_doc", "sp_rust_core",
|
||||
"sp_py_sorted", "sp_py_declared_surfaces", "sp_py_project_meta_surfaces",
|
||||
"sp_py_ingest_umbrellas_data", "sp_py_entity", "sp_py_build_from_inputs",
|
||||
"sp_py_drift_from_inputs", "sp_py_export_umbrellas", "sp_py_query",
|
||||
)
|
||||
|
||||
_spine = ctypes.CDLL(str(SPINE_SO))
|
||||
_cs = ctypes.CDLL(str(CS_SO))
|
||||
_cs.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
_cs.cs_free_string.restype = None
|
||||
for _s in SYMS:
|
||||
_fn = getattr(_spine, _s)
|
||||
_fn.argtypes = [ctypes.c_char_p]
|
||||
_fn.restype = ctypes.c_void_p # char* — never the default c_int
|
||||
|
||||
|
||||
def call(sym: str, payload: dict):
|
||||
fn = getattr(_spine, sym)
|
||||
ptr = fn(json.dumps(payload).encode("utf-8"))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned a null pointer")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
_cs.cs_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(env.get("err"))
|
||||
return env["value"]
|
||||
|
||||
|
||||
def canon(v) -> str:
|
||||
return json.dumps(spine._py_sorted(v), sort_keys=True)
|
||||
|
||||
|
||||
def strip_loader_prefix(msg: str) -> str:
|
||||
for p in ("sp_project core error: ", "sp_drift core error: ",
|
||||
"sp_export core error: ", "sp_query core error: "):
|
||||
if msg.startswith(p):
|
||||
return msg[len(p):]
|
||||
return msg
|
||||
|
||||
|
||||
PASS = 0
|
||||
FAIL: list[str] = []
|
||||
|
||||
|
||||
def check(label: str, ok: bool, detail: str = ""):
|
||||
global PASS
|
||||
if ok:
|
||||
PASS += 1
|
||||
else:
|
||||
FAIL.append(f"{label}: {detail}")
|
||||
|
||||
|
||||
def compare_case(label: str, rust_out, case: dict):
|
||||
if "error" in case:
|
||||
want = case["error"]["message"]
|
||||
if isinstance(rust_out, RuntimeError):
|
||||
check(label, strip_loader_prefix(want) == str(rust_out),
|
||||
f"oracle={want!r} rust={rust_out!r}")
|
||||
else:
|
||||
check(label, False, f"oracle raised {want!r}, rust returned {rust_out!r}")
|
||||
return
|
||||
want = case["expected"]
|
||||
if "post_doc" in case:
|
||||
# doc-mutating: oracle return is the count; the doc mutation is
|
||||
# frozen separately — compare the kernel's {count, doc} wrapper.
|
||||
check(label, isinstance(rust_out, dict) and "count" in rust_out and "doc" in rust_out,
|
||||
f"rust wrapper shape: {rust_out!r}"[:200])
|
||||
if isinstance(rust_out, dict) and "count" in rust_out:
|
||||
check(label + "/count", rust_out["count"] == want,
|
||||
f"oracle={want!r} rust={rust_out['count']!r}")
|
||||
check(label + "/doc", canon(rust_out["doc"]) == canon(case["post_doc"]),
|
||||
"doc divergence")
|
||||
return
|
||||
check(label, canon(rust_out) == canon(want),
|
||||
f"oracle={canon(want)[:300]} rust={canon(rust_out)[:300]}")
|
||||
|
||||
|
||||
def sandbox(files: dict) -> Path:
|
||||
root = Path(tempfile.mkdtemp(prefix="spine-gap-parity-"))
|
||||
for rel, content in files.items():
|
||||
p = root / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(content)
|
||||
return root
|
||||
|
||||
|
||||
def rust_replay(case: dict):
|
||||
"""Mirror of the battery dispatch — through the FFI symbols."""
|
||||
fn, name, args = case["fn"], case["name"], case["args"]
|
||||
root = sandbox(case.get("files") or {})
|
||||
try:
|
||||
if fn == "_module_imports":
|
||||
module = args[0]
|
||||
text = (root / "src" / "rune" / f"{module}.py").read_text() \
|
||||
if (root / "src" / "rune" / f"{module}.py").is_file() else None
|
||||
return call("sp_module_imports",
|
||||
{"module": module, "src_text": text})
|
||||
if fn == "_gather_meta_inputs":
|
||||
return call("sp_gather_meta_inputs", {"root": str(root)})
|
||||
if fn == "_deployed_source_files":
|
||||
return call("sp_deployed_source_files",
|
||||
{"root": str(root / "deployed")})
|
||||
if fn == "_rust_core":
|
||||
# both frozen branches: loader importable, .so absent
|
||||
return call("sp_rust_core",
|
||||
{"pin": "auto", "loader_importable": True,
|
||||
"so_present": False})
|
||||
if fn == "_py_project_meta_surfaces":
|
||||
return call("sp_py_project_meta_surfaces",
|
||||
{"doc": args[0], "inputs": args[1],
|
||||
"now": gen.STAMP})
|
||||
if fn == "_py_ingest_umbrellas_data":
|
||||
return call("sp_py_ingest_umbrellas_data",
|
||||
{"doc": args[0], "data": args[1], "now": gen.STAMP})
|
||||
if fn == "_py_build_from_inputs":
|
||||
core_up = name != "kernel_absent_raises"
|
||||
return call("sp_py_build_from_inputs",
|
||||
{"inputs": args[0], "core_available": core_up})
|
||||
if fn == "_gather_drift_inputs":
|
||||
tested = sorted(
|
||||
p.stem[len("test_"):]
|
||||
for p in (root / "tests").glob("test_*.py")
|
||||
)
|
||||
src_stems = [p.stem for p in (root / "src" / "rune").glob("*.py")]
|
||||
return call("sp_gather_drift_inputs", {
|
||||
"root": str(root),
|
||||
"registry": ["canary_alpha", "canary_beta"],
|
||||
"graph": {"alpha": ["json", "rune"], "beta": ["sys"]},
|
||||
"tested": tested,
|
||||
"src_stems": src_stems,
|
||||
"deployed_obs": None,
|
||||
})
|
||||
if fn == "_py_drift_from_inputs":
|
||||
return call("sp_py_drift_from_inputs",
|
||||
{"doc": args[0], "inputs": args[1],
|
||||
"core_available": False})
|
||||
if fn == "_py_export_umbrellas":
|
||||
return call("sp_py_export_umbrellas",
|
||||
{"doc": args[0], "now": gen.STAMP,
|
||||
"core_available": False})
|
||||
if fn == "_py_query":
|
||||
return call("sp_py_query",
|
||||
{"doc": args[0], "id": args[1],
|
||||
"core_available": False})
|
||||
if fn == "_fresh_doc":
|
||||
return call("sp_fresh_doc", {"root": str(root)})
|
||||
raise AssertionError(f"unknown fn {fn}")
|
||||
except RuntimeError as e:
|
||||
return e
|
||||
finally:
|
||||
import shutil
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# 1. harness corpus (arg-pure) — oracle re-run LIVE via run_oracle.
|
||||
for fn_name, args, kwargs in gen.harness_cases():
|
||||
case = gen.run_oracle(fn_name, list(args), dict(kwargs))
|
||||
case = {"expected": case.get("expected"), **case}
|
||||
label = f"harness/{fn_name}:{json.dumps(gen.snap(list(args)))[:60]}"
|
||||
if fn_name == "_py_sorted":
|
||||
rust = call("sp_py_sorted", {"v": args[0]})
|
||||
elif fn_name == "_py_declared_surfaces":
|
||||
rust = call("sp_py_declared_surfaces", {"manifest": args[0]})
|
||||
elif fn_name == "_py_entity":
|
||||
rust = call("sp_py_entity", {"doc": args[0], "id": args[1]})
|
||||
elif fn_name == "_rust_core":
|
||||
rust = call("sp_rust_core",
|
||||
{"pin": "python", "loader_importable": True,
|
||||
"so_present": True})
|
||||
else:
|
||||
raise AssertionError(fn_name)
|
||||
compare_case(label, rust, case)
|
||||
|
||||
# 2. clocked corpus — clocked_cases() re-runs the live oracle per case.
|
||||
for case in gen.clocked_cases():
|
||||
label = f"clocked/{case['fn']}/{case['name']}"
|
||||
rust = rust_replay(case)
|
||||
compare_case(label, rust, case)
|
||||
|
||||
# 3. the branches the frozen corpus cannot carry.
|
||||
r = call("sp_rust_core", {"pin": "auto", "loader_importable": True,
|
||||
"so_present": True})
|
||||
check("extra/rust_core_auto_present", r == "spine_loader", repr(r))
|
||||
r = call("sp_rust_core", {"pin": "auto", "loader_importable": False,
|
||||
"so_present": True})
|
||||
check("extra/rust_core_no_loader", r is None, repr(r))
|
||||
|
||||
# 4. _fresh_doc build-and-save seam: injected built doc returned AND
|
||||
# persisted; the persisted bytes cross-checked against the PYTHON
|
||||
# oracle's canonical save text (spine.save under the pin).
|
||||
root = sandbox({})
|
||||
built = gen.seeded_doc()
|
||||
got = call("sp_fresh_doc", {"root": str(root), "built": built})
|
||||
check("extra/fresh_doc_builds", canon(got) == canon(built), "doc mismatch")
|
||||
persisted = (root / "store" / "spine" / "current.json").read_text()
|
||||
oracle_path = root / "oracle_save.json"
|
||||
spine.save(gen.snap(built), oracle_path)
|
||||
check("extra/fresh_doc_save_text", persisted == oracle_path.read_text(),
|
||||
"persisted text != python oracle save text")
|
||||
import shutil
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
|
||||
total = PASS + len(FAIL)
|
||||
print(f"spine gap live parity: {PASS}/{total} pass"
|
||||
f"{'' if not FAIL else ' — FAILURES:'}")
|
||||
for f in FAIL:
|
||||
print(" FAIL", f)
|
||||
return 1 if FAIL else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,515 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_spine_gap_truth.py — spine gap-closing truth battery (contract
|
||||
BUILDER-2026-10-13T08:11:47Z-9b1f32-factory-gap-sp).
|
||||
|
||||
Closes the PARTIAL audit gaps: the 8 audit-named fns (_py_sorted,
|
||||
_module_imports, _py_declared_surfaces, _py_project_meta_surfaces,
|
||||
_gather_meta_inputs, _rust_core, _deployed_source_files,
|
||||
_py_build_from_inputs) plus their 7 remaining audit-missing siblings
|
||||
(_py_ingest_umbrellas_data, _gather_drift_inputs, _py_drift_from_inputs,
|
||||
_py_export_umbrellas, _py_query, _py_entity, _fresh_doc) — 35/35.
|
||||
|
||||
TWO artifacts, two laws:
|
||||
|
||||
1. frozen_spine_gap_truth.json — SHARED-HARNESS truth (scripts/
|
||||
freeze_truth.py): arg-pure, stamp-free, JSON-safe cases only. The
|
||||
harness re-runs the live oracle per case with NO monkeypatching, so
|
||||
only functions of the recorded args qualify. Byte-verified:
|
||||
|
||||
PYTHONPATH=src python3 scripts/freeze_truth.py \
|
||||
--verify src/rust/spine/frozen_spine_gap_truth.json \
|
||||
--module rune.spine
|
||||
|
||||
2. frozen_spine_gap_clocked.json — the sandbox/clock/error battery
|
||||
(rune_spine facade-battery + spine_language law): cases whose oracle
|
||||
needs module-global IO roots (SRC_DIR/ROOT/WORKFLOWS_DIR/DEPLOYED_LINK
|
||||
→ per-case temp sandboxes, fixture bytes recorded back as `files`),
|
||||
the pinned process clock (structural_store.now → time.strftime
|
||||
patched to FROZEN_NOW), set→sorted-list conversion (_module_imports),
|
||||
and exception capture as {"error": {"type", "message"}}. Replayed
|
||||
VERBATIM by src/rust/spine/src/branch_battery_gap.rs.
|
||||
|
||||
RUNE_SPINE_CORE=python pins the pure-oracle path (poisoned-truth law);
|
||||
the _py_build_from_inputs group runs UNPINNED — that stub is
|
||||
delegation-only since 2026-10-10 and raises without the kernel.
|
||||
|
||||
Usage: python3 scripts/gen_spine_gap_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from freeze_truth import freeze, write_envelope # noqa: E402
|
||||
|
||||
FROZEN_NOW = "2026-10-13T08:30:00"
|
||||
STAMP = FROZEN_NOW + "Z"
|
||||
|
||||
os.environ["RUNE_FROZEN_NOW"] = FROZEN_NOW
|
||||
os.environ["RUNE_SPINE_CORE"] = "python" # poisoned-truth law (pure oracle)
|
||||
|
||||
import time # noqa: E402
|
||||
|
||||
import rune.structural_store as ss # noqa: E402
|
||||
from rune import spine # noqa: E402
|
||||
|
||||
# Pin the wall clock at the source: structural_store.now/add_entity stamp
|
||||
# observed_at via time.strftime — freeze it process-wide so clocked cases
|
||||
# are byte-stable (other formats pass through to the real clock).
|
||||
_real_strftime = time.strftime
|
||||
|
||||
|
||||
def _pinned_strftime(fmt="%a %b %d %H:%M:%S %Y", *args):
|
||||
# structural_store.now/spine.now pass time.gmtime() positionally —
|
||||
# the pin must hold for the stamp format REGARDLESS of args, or the
|
||||
# frozen truth carries the real wall clock (byte-unstable).
|
||||
if fmt == "%Y-%m-%dT%H:%M:%SZ":
|
||||
return STAMP
|
||||
if args:
|
||||
return _real_strftime(fmt, *args)
|
||||
return _real_strftime(fmt)
|
||||
|
||||
|
||||
time.strftime = _pinned_strftime
|
||||
|
||||
GENERATED_VIEW_MARKER = spine.GENERATED_VIEW_MARKER
|
||||
|
||||
# ── fixtures ────────────────────────────────────────────────────────────
|
||||
|
||||
ALPHA_SRC = '''"""Alpha module."""
|
||||
import json
|
||||
import os.path
|
||||
from rune import spine
|
||||
from . import sibling
|
||||
from .helpers import thing
|
||||
'''
|
||||
BETA_SRC = '''import sys # trailing comment
|
||||
from rune.structural_store import add_entity as ae, promote
|
||||
try:
|
||||
import optional_dep
|
||||
except ImportError:
|
||||
optional_dep = None
|
||||
'''
|
||||
GAMMA_SRC = "x = 1\nimport [oops\n"
|
||||
EMPTY_SRC = ""
|
||||
PLAIN_SRC = "VALUE = 42\n"
|
||||
SEMI_SRC = "import json; import os.path\nfrom rune import spine, query\n"
|
||||
CONT_SRC = (
|
||||
"from rune.structural_store import (\n"
|
||||
" add_entity,\n"
|
||||
" promote,\n"
|
||||
")\n"
|
||||
"import a.b as c\n"
|
||||
)
|
||||
|
||||
SRC_FILES = {
|
||||
"src/rune/alpha.py": ALPHA_SRC,
|
||||
"src/rune/beta.py": BETA_SRC,
|
||||
"src/rune/gamma.py": GAMMA_SRC,
|
||||
"src/rune/empty.py": EMPTY_SRC,
|
||||
"src/rune/plain.py": PLAIN_SRC,
|
||||
"src/rune/semi.py": SEMI_SRC,
|
||||
"src/rune/cont.py": CONT_SRC,
|
||||
}
|
||||
|
||||
|
||||
def umbrella_manifest() -> dict:
|
||||
return {"umbrellas": {
|
||||
"cognition": {
|
||||
"description": "thinking parts",
|
||||
"modules": ["alpha"],
|
||||
"rust_crates": ["kernel_a", "ghost_crate"],
|
||||
"go_surfaces": ["rune-entry"],
|
||||
"configs": ["go-version.txt", "phantom.toml"],
|
||||
},
|
||||
"governance": {"description": "meta"},
|
||||
}}
|
||||
|
||||
|
||||
def meta_tree() -> dict:
|
||||
return {
|
||||
"config/umbrellas.json": json.dumps(umbrella_manifest()),
|
||||
"src/rust/kernel_a/lib.rs": "// crate\n",
|
||||
"src/rust/kernel_a/canary_kernel_a.py": "# canary\n",
|
||||
"src/go/rune-entry/main.go": "package main\n",
|
||||
"go-version.txt": "1.24\n",
|
||||
".gitea/workflows/ci.yml": "on: push\n",
|
||||
}
|
||||
|
||||
|
||||
def deployed_tree() -> dict:
|
||||
return {
|
||||
"src/rune/alpha.py": ALPHA_SRC,
|
||||
"src/rust/kernel_a/canary_kernel_a.py": "# canary\n",
|
||||
"src/rust/kernel_a/lib.rs": "// not a canary\n",
|
||||
"README.md": "# not a source\n",
|
||||
}
|
||||
|
||||
|
||||
def drift_tree() -> dict:
|
||||
return {
|
||||
"pyproject.toml": '[project]\nname = "rune"\nversion = "9.9.9"\n',
|
||||
"src/rune/alpha.py": ALPHA_SRC,
|
||||
"src/rune/beta.py": BETA_SRC,
|
||||
"tests/test_alpha.py": "# test\n",
|
||||
"tests/test_beta.py": "# test\n",
|
||||
}
|
||||
|
||||
|
||||
def fresh_doc() -> dict:
|
||||
doc = ss.new_doc()
|
||||
doc["generated_at"] = STAMP # deterministic under the pinned clock
|
||||
return doc
|
||||
|
||||
|
||||
def meta_inputs(over: dict | None = None) -> dict:
|
||||
base = {
|
||||
"config_manifest": umbrella_manifest(),
|
||||
"crate_dirs": ["kernel_a"],
|
||||
"crate_canaries": {"kernel_a": ["canary_kernel_a"]},
|
||||
"go_dirs": ["rune-entry"],
|
||||
"config_files": ["go-version.txt"],
|
||||
"workflows": ["ci.yml"],
|
||||
}
|
||||
base.update(over or {})
|
||||
return base
|
||||
|
||||
|
||||
def build_inputs(over: dict | None = None) -> dict:
|
||||
base = {
|
||||
"now": STAMP,
|
||||
"existing": None,
|
||||
"manifest": umbrella_manifest(),
|
||||
"manifest_path": "config/umbrellas.json",
|
||||
"graph": {"alpha": ["json", "os", "rune"], "beta": ["sys"]},
|
||||
"registry": ["canary_alpha"],
|
||||
"tested": ["alpha"],
|
||||
"main_present": True,
|
||||
"present_modules": ["alpha", "beta"],
|
||||
"deployed_obs": None,
|
||||
"deployed_sources": [],
|
||||
"canary_files": ["canary_alpha"],
|
||||
}
|
||||
base.update(meta_inputs())
|
||||
base.update(over or {})
|
||||
return base
|
||||
|
||||
|
||||
def seeded_doc() -> dict:
|
||||
doc = fresh_doc()
|
||||
doc["relationships"].append({
|
||||
"type": "belongs_to", "from": "module:alpha",
|
||||
"to": "subsystem:cognition", "evidence_refs": ["seed"],
|
||||
"observed_at": STAMP,
|
||||
})
|
||||
return doc
|
||||
|
||||
|
||||
# ── sandbox machinery ───────────────────────────────────────────────────
|
||||
|
||||
_SAVED_GLOBALS = (spine.SRC_DIR, spine.ROOT, spine.WORKFLOWS_DIR,
|
||||
spine.DEPLOYED_LINK, spine.TESTS_DIR, spine.CURRENT,
|
||||
spine.STORE_DIR, spine.SNAPSHOTS)
|
||||
|
||||
|
||||
def _sandbox(files: dict) -> Path:
|
||||
root = Path(tempfile.mkdtemp(prefix="spine-gap-truth-"))
|
||||
for rel, content in files.items():
|
||||
p = root / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(content)
|
||||
return root
|
||||
|
||||
|
||||
def _point_globals_at(root: Path) -> None:
|
||||
spine.ROOT = root
|
||||
spine.SRC_DIR = root / "src" / "rune"
|
||||
spine.WORKFLOWS_DIR = root / ".gitea" / "workflows"
|
||||
spine.TESTS_DIR = root / "tests"
|
||||
spine.DEPLOYED_LINK = root / "deployed"
|
||||
spine.STORE_DIR = root / "store" / "spine"
|
||||
spine.CURRENT = spine.STORE_DIR / "current.json"
|
||||
spine.SNAPSHOTS = spine.STORE_DIR / "snapshots"
|
||||
|
||||
|
||||
def _restore_globals() -> None:
|
||||
(spine.SRC_DIR, spine.ROOT, spine.WORKFLOWS_DIR, spine.DEPLOYED_LINK,
|
||||
spine.TESTS_DIR, spine.CURRENT, spine.STORE_DIR, spine.SNAPSHOTS) = _SAVED_GLOBALS
|
||||
|
||||
|
||||
def snap(value):
|
||||
"""JSON-safe snapshot: sets → sorted lists; everything else deep."""
|
||||
if isinstance(value, (set, frozenset)):
|
||||
return sorted(value)
|
||||
if isinstance(value, dict):
|
||||
return {k: snap(v) for k, v in value.items()}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [snap(v) for v in value]
|
||||
return copy.deepcopy(value)
|
||||
|
||||
|
||||
def run_oracle(fn_name: str, args: list, kwargs: dict, live_doc: dict | None = None):
|
||||
"""Call the oracle fn, capturing results OR exceptions. live_doc (the
|
||||
doc-mutating cases) is passed by reference — the caller snapshots it
|
||||
AFTER the call to freeze the in-place mutation contract."""
|
||||
fn = getattr(spine, fn_name)
|
||||
try:
|
||||
if live_doc is not None:
|
||||
result = fn(live_doc, *copy.deepcopy(args), **copy.deepcopy(kwargs))
|
||||
else:
|
||||
result = fn(*copy.deepcopy(args), **copy.deepcopy(kwargs))
|
||||
return {"expected": snap(result)}
|
||||
except Exception as e: # noqa: BLE001 — the error IS the frozen contract
|
||||
return {"error": {"type": type(e).__name__, "message": str(e)}}
|
||||
|
||||
|
||||
# ── part 1: shared-harness truth (arg-pure cases) ───────────────────────
|
||||
|
||||
def harness_cases() -> list[tuple]:
|
||||
cases: list[tuple] = []
|
||||
|
||||
for v in [
|
||||
{"b": 1, "a": {"z": 2, "c": 3}},
|
||||
[{"a": 2, "b": 1}, {"a": 9}, {"a": 1, "b": 1}],
|
||||
[3, "x", None, True, 1.5, "a"],
|
||||
{"s": "héllo", "i": 7, "f": 2.5, "n": None, "b": False},
|
||||
{"l": [], "d": {}},
|
||||
{"k": [{"z": 1, "a": 2}, {"b": []}]},
|
||||
42,
|
||||
"zzz",
|
||||
None,
|
||||
[{"a": 1}, {"a": 1}, {"a": 2}],
|
||||
]:
|
||||
cases.append(("_py_sorted", (v,), {}))
|
||||
|
||||
for manifest in [
|
||||
umbrella_manifest(),
|
||||
{"umbrellas": {}},
|
||||
{"umbrellas": {"solo": {"description": "d"}}},
|
||||
{},
|
||||
{"umbrellas": {"u": {
|
||||
"rust_crates": ["k"], "go_surfaces": ["g"], "configs": ["c"],
|
||||
"modules": ["m"], "depends_on": ["d"], "future": 1}}},
|
||||
]:
|
||||
cases.append(("_py_declared_surfaces", (manifest,), {}))
|
||||
|
||||
ents_doc = {
|
||||
"entities": [
|
||||
{"id": "module:m_a", "kind": "module", "maturity": "WIRED",
|
||||
"aliases": ["module:m_old"]},
|
||||
{"id": "capability:c1", "kind": "capability", "maturity": "DECLARED"},
|
||||
],
|
||||
"relationships": [],
|
||||
}
|
||||
for eid in ("module:m_a", "module:m_old", "module:ghost", ""):
|
||||
cases.append(("_py_entity", (copy.deepcopy(ents_doc), eid), {}))
|
||||
cases.append(("_py_entity", ({"entities": []}, "x"), {}))
|
||||
|
||||
cases.append(("_rust_core", (), {})) # harness pins RUNE_SPINE_CORE=python
|
||||
return cases
|
||||
|
||||
|
||||
# ── part 2: clocked battery (sandbox/clock/error cases) ─────────────────
|
||||
|
||||
def clocked_cases() -> list[dict]:
|
||||
cases: list[dict] = []
|
||||
|
||||
def add(fn: str, name: str, files: dict, args: list,
|
||||
kwargs: dict | None = None, pin: str | None = None,
|
||||
sandboxed: bool = True, mutate_doc: bool = False,
|
||||
env: dict | None = None):
|
||||
root = _sandbox(files) if sandboxed else None
|
||||
if sandboxed:
|
||||
_point_globals_at(root)
|
||||
saved_pin = os.environ.get("RUNE_SPINE_CORE")
|
||||
saved_env = {k: os.environ.get(k) for k in (env or {})}
|
||||
if pin is not None:
|
||||
os.environ["RUNE_SPINE_CORE"] = pin
|
||||
for k, v in (env or {}).items():
|
||||
os.environ[k] = v
|
||||
try:
|
||||
doc_args = list(args)
|
||||
post_doc = None
|
||||
if mutate_doc:
|
||||
live_doc = copy.deepcopy(args[0])
|
||||
out = run_oracle(fn, args[1:], dict(kwargs or {}),
|
||||
live_doc=live_doc)
|
||||
post_doc = live_doc
|
||||
else:
|
||||
out = run_oracle(fn, copy.deepcopy(args), dict(kwargs or {}))
|
||||
finally:
|
||||
if pin is not None:
|
||||
os.environ["RUNE_SPINE_CORE"] = saved_pin or "auto"
|
||||
for k, v in saved_env.items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
if sandboxed:
|
||||
_restore_globals()
|
||||
entry = {"fn": fn, "name": name, "files": files,
|
||||
"args": snap(doc_args), "kwargs": snap(kwargs or {})}
|
||||
entry.update(out)
|
||||
if mutate_doc:
|
||||
entry["post_doc"] = snap(post_doc)
|
||||
cases.append(entry)
|
||||
|
||||
# _module_imports — set→sorted-list conversion, SRC_DIR sandbox
|
||||
for name, module in [
|
||||
("from_and_plain", "alpha"),
|
||||
("alias_try_except", "beta"),
|
||||
("syntax_error_empties", "gamma"),
|
||||
("no_imports", "plain"),
|
||||
("empty_file", "empty"),
|
||||
("semicolons", "semi"),
|
||||
("paren_continuation", "cont"),
|
||||
("missing_file_empties", "m_ghost_xyz"),
|
||||
]:
|
||||
add("_module_imports", name, SRC_FILES, [module])
|
||||
|
||||
# _py_project_meta_surfaces — clock-pinned stamps, doc mutation
|
||||
for name, doc, inputs in [
|
||||
("full_inputs", fresh_doc(), meta_inputs()),
|
||||
("empty_inputs", fresh_doc(), {}),
|
||||
("null_manifest", fresh_doc(), {"config_manifest": None}),
|
||||
("absent_crate_stays_declared", fresh_doc(), meta_inputs({
|
||||
"crate_dirs": [], "crate_canaries": {}})),
|
||||
("high_water_keeps_wired", None, meta_inputs()),
|
||||
]:
|
||||
if doc is None:
|
||||
doc = fresh_doc()
|
||||
doc["entities"].append({
|
||||
"id": "rust-crate:kernel_a", "kind": "rust-crate",
|
||||
"maturity": "WIRED", "tags": ["code"],
|
||||
"identities": {"source": "src/rust/kernel_a"},
|
||||
"evidence_refs": ["earlier#ref"], "observed_at": STAMP,
|
||||
})
|
||||
add("_py_project_meta_surfaces", name, {}, [doc, inputs],
|
||||
mutate_doc=True)
|
||||
|
||||
# _gather_meta_inputs — ROOT/WORKFLOWS_DIR sandbox
|
||||
add("_gather_meta_inputs", "sandbox_tree", meta_tree(), [])
|
||||
add("_gather_meta_inputs", "empty_tree", {}, [])
|
||||
add("_gather_meta_inputs", "malformed_manifest",
|
||||
{"config/umbrellas.json": "{oops"}, [])
|
||||
|
||||
# _rust_core — extra branches live at the END of clocked_cases (they
|
||||
# poison SP_RUST_LIB; spine_loader caches _LIB_PATH at first import).
|
||||
|
||||
# _deployed_source_files — DEPLOYED_LINK sandbox
|
||||
add("_deployed_source_files", "sandbox_deployed", {
|
||||
"deployed/src/rune/alpha.py": ALPHA_SRC,
|
||||
"deployed/src/rust/kernel_a/canary_kernel_a.py": "# canary\n",
|
||||
"deployed/src/rust/kernel_a/lib.rs": "// not a canary\n",
|
||||
"deployed/README.md": "# not a source\n",
|
||||
}, [])
|
||||
add("_deployed_source_files", "absent_link", {}, [])
|
||||
|
||||
# _py_build_from_inputs — UNPINNED (delegation-only stub needs kernel)
|
||||
add("_py_build_from_inputs", "seeded_carry_forward", {},
|
||||
[build_inputs({"existing": seeded_doc()})], pin="auto",
|
||||
sandboxed=False)
|
||||
add("_py_build_from_inputs", "unseeded_manifest", {},
|
||||
[build_inputs()], pin="auto", sandboxed=False)
|
||||
ghost = umbrella_manifest()
|
||||
ghost[GENERATED_VIEW_MARKER] = True
|
||||
add("_py_build_from_inputs", "generated_view_refused", {},
|
||||
[build_inputs({"manifest": ghost})], pin="auto", sandboxed=False)
|
||||
add("_py_build_from_inputs", "kernel_absent_raises", {},
|
||||
[build_inputs()], pin="python", sandboxed=False)
|
||||
|
||||
# _py_ingest_umbrellas_data — pinned pure oracle, clock-pinned stamps
|
||||
manifest = umbrella_manifest()
|
||||
add("_py_ingest_umbrellas_data", "full_manifest", {}, [fresh_doc(), manifest],
|
||||
mutate_doc=True)
|
||||
add("_py_ingest_umbrellas_data", "empty_umbrellas", {},
|
||||
[fresh_doc(), {"umbrellas": {}}], mutate_doc=True)
|
||||
ghost_manifest = {"umbrellas": {}, GENERATED_VIEW_MARKER: True}
|
||||
add("_py_ingest_umbrellas_data", "generated_view_refused", {},
|
||||
[fresh_doc(), ghost_manifest], mutate_doc=True)
|
||||
|
||||
# _gather_drift_inputs — sandbox + registry seam
|
||||
real_registry = spine._canary_registry
|
||||
spine._canary_registry = lambda: ["canary_alpha", "canary_beta"]
|
||||
real_graph = spine._import_graph
|
||||
spine._import_graph = lambda: {
|
||||
"alpha": {"json", "rune"}, "beta": {"sys"}}
|
||||
try:
|
||||
add("_gather_drift_inputs", "sandbox_tree", drift_tree(), [])
|
||||
add("_gather_drift_inputs", "no_pyproject", {
|
||||
"src/rune/alpha.py": ALPHA_SRC}, [])
|
||||
add("_gather_drift_inputs", "unversioned_pyproject", {
|
||||
"pyproject.toml": "[project]\nname = 'rune'\n",
|
||||
"src/rune/alpha.py": ALPHA_SRC}, [])
|
||||
finally:
|
||||
spine._canary_registry = real_registry
|
||||
spine._import_graph = real_graph
|
||||
|
||||
# _py_drift_from_inputs / _py_export_umbrellas / _py_query —
|
||||
# delegation-only stubs: kernel-absent error pinned; happy path is
|
||||
# the kernel's own frozen battery (frozen_spine_oracle.json).
|
||||
add("_py_drift_from_inputs", "kernel_absent_raises", {},
|
||||
[fresh_doc(), {}], pin="python", sandboxed=False)
|
||||
add("_py_export_umbrellas", "kernel_absent_raises", {},
|
||||
[fresh_doc()], pin="python", sandboxed=False)
|
||||
add("_py_query", "kernel_absent_raises", {},
|
||||
[fresh_doc(), "module:alpha"], pin="python", sandboxed=False)
|
||||
|
||||
# _fresh_doc — load path frozen; the absent-store build path is the
|
||||
# zero-dep seam (the Python gather feeds the kernel): under the pin
|
||||
# build() hits the retired stub and raises — the exact frozen error.
|
||||
# The Rust port's injected-`built` happy path is a crate unit test.
|
||||
store_doc = seeded_doc()
|
||||
add("_fresh_doc", "existing_store_wins", {
|
||||
"store/spine/current.json": json.dumps(store_doc),
|
||||
}, [])
|
||||
add("_fresh_doc", "absent_store_kernel_required", {}, [])
|
||||
|
||||
# _rust_core — extra branches the harness pin can't reach. MUST run
|
||||
# LAST and poison spine_loader._LIB_PATH directly (the module is
|
||||
# already imported by the kernel cases and caches _LIB_PATH at first
|
||||
# import; env alone would not reach it).
|
||||
from rune import spine_loader # noqa: E402 — cached module, path patched below
|
||||
|
||||
saved_lib_path = spine_loader._LIB_PATH
|
||||
spine_loader._LIB_PATH = Path("/nonexistent/librune_spine.so")
|
||||
try:
|
||||
add("_rust_core", "auto_missing_so", {}, [], pin="auto", sandboxed=False)
|
||||
add("_rust_core", "weird_pin_is_auto", {}, [], pin="weird-value",
|
||||
sandboxed=False)
|
||||
finally:
|
||||
spine_loader._LIB_PATH = saved_lib_path
|
||||
# (the auto+present branch returns the loader MODULE — not
|
||||
# JSON-freezable; the Rust battery pins it directly as
|
||||
# Some("spine_loader"))
|
||||
|
||||
return cases
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Part 1 — shared-harness truth.
|
||||
truth = freeze("rune.spine", harness_cases(), "rune.spine")
|
||||
out1 = REPO / "src" / "rust" / "spine" / "frozen_spine_gap_truth.json"
|
||||
write_envelope(FROZEN_NOW, truth, out1)
|
||||
print(f"wrote {out1} ({len(truth)} harness cases)")
|
||||
|
||||
# Part 2 — clocked battery.
|
||||
clocked = clocked_cases()
|
||||
out2 = REPO / "src" / "rust" / "spine" / "frozen_spine_gap_clocked.json"
|
||||
out2.write_text(json.dumps(
|
||||
{"frozen_now": FROZEN_NOW, "cases": clocked},
|
||||
indent=2, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {out2} ({len(clocked)} clocked cases)")
|
||||
|
||||
_restore_globals()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open + walk the #2479 dichotomy-wave construction contract for spine
|
||||
to verifying (with an optional SHA argument: through to closed — the full
|
||||
lifecycle runs in ONE process, shared-store clobber hazard). Post-
|
||||
machinery-exclusion surface: 25 fns, was 20/25.
|
||||
|
||||
GAP-CLOSING PORT: spine (975 lines). Extends src/rust/spine with the
|
||||
audit-missing kernels: _module_imports, _gather_meta_inputs,
|
||||
_deployed_source_files, _gather_drift_inputs, _fresh_doc (+ machinery
|
||||
twins over the proven rune_spine kernels) — name-preserving, IO behind
|
||||
injectable seams. Rust fns + doc-envelope FFI (sp_* / free via
|
||||
cs_free_string — NEVER re-export) + ctypes battery replay.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
iface = ConstructionInterface(builder="factory-gap-sp")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Close spine PARTIAL gaps (dichotomy factory, issue #2479 tranche): "
|
||||
"extend src/rust/spine with the 8 audit-missing fns (_py_sorted, "
|
||||
"_module_imports, _py_declared_surfaces, _py_project_meta_surfaces, "
|
||||
"_gather_meta_inputs, _rust_core, _deployed_source_files, "
|
||||
"_py_build_from_inputs) name-preserving, logic 1:1, IO behind "
|
||||
"injectable seams — target >=28/35 fn coverage. Rust fns + doc-envelope "
|
||||
"FFI (sp_* / free via cs_free_string — NEVER re-export) + ctypes loader "
|
||||
"mirroring broker_loader.py + facade delegation with "
|
||||
"RUNE_SPINE_CORE=python pin. Truth frozen via scripts/freeze_truth.py "
|
||||
"(RUNE_SPINE_CORE=python pinned, deep-copied inputs, JSON truth "
|
||||
"embedded VERBATIM in cfg(test) battery); byte-identical verify. "
|
||||
"Phase B language-advantage pass.",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:300])
|
||||
if not r.get("success"):
|
||||
print("NEED FAILED — aborting"); sys.exit(1)
|
||||
cid = r["ids"]["contract_id"]
|
||||
print("contract_id:", cid)
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, phase)
|
||||
print(f"transition->{phase}:", json.dumps(t)[:200])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
|
||||
# Close with SHA evidence when given one (full lifecycle in ONE process —
|
||||
# close-time re-walk law: DRAFT → approved → building → verifying → closed).
|
||||
if len(sys.argv) > 1:
|
||||
sha = sys.argv[1]
|
||||
c = iface.close(cid, observed_evidence=[{
|
||||
"kind": "port_evidence",
|
||||
"detail": f"spine dichotomy gap-close — {sha} feat(spine): close "
|
||||
"PARTIAL gaps — dichotomy wave (rebased onto "
|
||||
"gitea/master, pushed). Audit spine 20/25 -> 25/25 "
|
||||
"(fn-level 1335 -> 1340): the 5 audit-missing fns "
|
||||
"(_module_imports, _gather_meta_inputs, "
|
||||
"_deployed_source_files, _gather_drift_inputs, _fresh_doc) "
|
||||
"ported name-preserving in src/rust/spine/src/gap.rs with "
|
||||
"doc-envelope FFI (sp_* symbols, freed via cs_free_string). "
|
||||
"Battery 5/5 replays the 21-case harness truth (byte-"
|
||||
"verified against the live oracle) + 35-case clocked "
|
||||
"battery VERBATIM; live parity 74/74 through the real "
|
||||
"FFI surface (ctypes vs pinned oracle, incl. fresh_doc "
|
||||
"save-text cross-check); clippy -D warnings clean; "
|
||||
"pytest no new failures either pin (15+1 pre-existing at "
|
||||
"HEAD, retired-oracle class). Divergences caught "
|
||||
"pre-landing: wall-clock leak in the clocked freeze "
|
||||
"(structural_store.now passes gmtime() positionally — pin "
|
||||
"now holds for the stamp format regardless of args), "
|
||||
"malformed-import SyntaxError branch (empty set, not "
|
||||
"Err), test_ prefix in drift `tested` stems.",
|
||||
}], scope_files=[
|
||||
"src/rust/spine/src/gap.rs",
|
||||
"src/rust/spine/src/lib.rs",
|
||||
"src/rust/spine/src/branch_battery_gap.rs",
|
||||
"src/rust/spine/frozen_spine_gap_truth.json",
|
||||
"src/rust/spine/frozen_spine_gap_clocked.json",
|
||||
"src/rust/spine/Cargo.toml",
|
||||
"src/rust/spine/Cargo.lock",
|
||||
"scripts/gen_spine_gap_truth.py",
|
||||
"scripts/check_spine_gap_parity.py",
|
||||
"scripts/walk_contract_spine.py",
|
||||
])
|
||||
print("close:", json.dumps(c)[:300])
|
||||
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
|
||||
sys.exit(0 if c.get("success") else 1)
|
||||
|
||||
print("CONTRACT", cid, "verifying")
|
||||
Generated
+5
@@ -32,6 +32,10 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rune_spine"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
@@ -79,6 +83,7 @@ dependencies = [
|
||||
name = "spine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rune_spine",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -9,3 +9,4 @@ crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rune_spine = { path = "../rune_spine" }
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,478 @@
|
||||
{
|
||||
"frozen_now": "2026-10-13T08:30:00",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "_py_sorted",
|
||||
"args": [
|
||||
{
|
||||
"b": 1,
|
||||
"a": {
|
||||
"z": 2,
|
||||
"c": 3
|
||||
}
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"a": {
|
||||
"c": 3,
|
||||
"z": 2
|
||||
},
|
||||
"b": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_sorted",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
{
|
||||
"a": 9
|
||||
},
|
||||
{
|
||||
"a": 1,
|
||||
"b": 1
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
{
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
{
|
||||
"a": 9
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_py_sorted",
|
||||
"args": [
|
||||
[
|
||||
3,
|
||||
"x",
|
||||
null,
|
||||
true,
|
||||
1.5,
|
||||
"a"
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
"a",
|
||||
"x",
|
||||
1.5,
|
||||
3,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_py_sorted",
|
||||
"args": [
|
||||
{
|
||||
"s": "héllo",
|
||||
"i": 7,
|
||||
"f": 2.5,
|
||||
"n": null,
|
||||
"b": false
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"b": false,
|
||||
"f": 2.5,
|
||||
"i": 7,
|
||||
"n": null,
|
||||
"s": "héllo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_sorted",
|
||||
"args": [
|
||||
{
|
||||
"l": [],
|
||||
"d": {}
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"d": {},
|
||||
"l": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_sorted",
|
||||
"args": [
|
||||
{
|
||||
"k": [
|
||||
{
|
||||
"z": 1,
|
||||
"a": 2
|
||||
},
|
||||
{
|
||||
"b": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"k": [
|
||||
{
|
||||
"a": 2,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"b": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_sorted",
|
||||
"args": [
|
||||
42
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 42
|
||||
},
|
||||
{
|
||||
"fn": "_py_sorted",
|
||||
"args": [
|
||||
"zzz"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": "zzz"
|
||||
},
|
||||
{
|
||||
"fn": "_py_sorted",
|
||||
"args": [
|
||||
null
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "_py_sorted",
|
||||
"args": [
|
||||
[
|
||||
{
|
||||
"a": 1
|
||||
},
|
||||
{
|
||||
"a": 1
|
||||
},
|
||||
{
|
||||
"a": 2
|
||||
}
|
||||
]
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": [
|
||||
{
|
||||
"a": 1
|
||||
},
|
||||
{
|
||||
"a": 1
|
||||
},
|
||||
{
|
||||
"a": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "_py_declared_surfaces",
|
||||
"args": [
|
||||
{
|
||||
"umbrellas": {
|
||||
"cognition": {
|
||||
"description": "thinking parts",
|
||||
"modules": [
|
||||
"alpha"
|
||||
],
|
||||
"rust_crates": [
|
||||
"kernel_a",
|
||||
"ghost_crate"
|
||||
],
|
||||
"go_surfaces": [
|
||||
"rune-entry"
|
||||
],
|
||||
"configs": [
|
||||
"go-version.txt",
|
||||
"phantom.toml"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"description": "meta"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"rust_crates": [
|
||||
[
|
||||
"kernel_a",
|
||||
"cognition"
|
||||
],
|
||||
[
|
||||
"ghost_crate",
|
||||
"cognition"
|
||||
]
|
||||
],
|
||||
"go_surfaces": [
|
||||
[
|
||||
"rune-entry",
|
||||
"cognition"
|
||||
]
|
||||
],
|
||||
"configs": [
|
||||
[
|
||||
"go-version.txt",
|
||||
"cognition"
|
||||
],
|
||||
[
|
||||
"phantom.toml",
|
||||
"cognition"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_declared_surfaces",
|
||||
"args": [
|
||||
{
|
||||
"umbrellas": {}
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"rust_crates": [],
|
||||
"go_surfaces": [],
|
||||
"configs": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_declared_surfaces",
|
||||
"args": [
|
||||
{
|
||||
"umbrellas": {
|
||||
"solo": {
|
||||
"description": "d"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"rust_crates": [],
|
||||
"go_surfaces": [],
|
||||
"configs": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_declared_surfaces",
|
||||
"args": [
|
||||
{}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"rust_crates": [],
|
||||
"go_surfaces": [],
|
||||
"configs": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_declared_surfaces",
|
||||
"args": [
|
||||
{
|
||||
"umbrellas": {
|
||||
"u": {
|
||||
"rust_crates": [
|
||||
"k"
|
||||
],
|
||||
"go_surfaces": [
|
||||
"g"
|
||||
],
|
||||
"configs": [
|
||||
"c"
|
||||
],
|
||||
"modules": [
|
||||
"m"
|
||||
],
|
||||
"depends_on": [
|
||||
"d"
|
||||
],
|
||||
"future": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"rust_crates": [
|
||||
[
|
||||
"k",
|
||||
"u"
|
||||
]
|
||||
],
|
||||
"go_surfaces": [
|
||||
[
|
||||
"g",
|
||||
"u"
|
||||
]
|
||||
],
|
||||
"configs": [
|
||||
[
|
||||
"c",
|
||||
"u"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_entity",
|
||||
"args": [
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"id": "module:m_a",
|
||||
"kind": "module",
|
||||
"maturity": "WIRED",
|
||||
"aliases": [
|
||||
"module:m_old"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "capability:c1",
|
||||
"kind": "capability",
|
||||
"maturity": "DECLARED"
|
||||
}
|
||||
],
|
||||
"relationships": []
|
||||
},
|
||||
"module:m_a"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"id": "module:m_a",
|
||||
"kind": "module",
|
||||
"maturity": "WIRED",
|
||||
"aliases": [
|
||||
"module:m_old"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_entity",
|
||||
"args": [
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"id": "module:m_a",
|
||||
"kind": "module",
|
||||
"maturity": "WIRED",
|
||||
"aliases": [
|
||||
"module:m_old"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "capability:c1",
|
||||
"kind": "capability",
|
||||
"maturity": "DECLARED"
|
||||
}
|
||||
],
|
||||
"relationships": []
|
||||
},
|
||||
"module:m_old"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"id": "module:m_a",
|
||||
"kind": "module",
|
||||
"maturity": "WIRED",
|
||||
"aliases": [
|
||||
"module:m_old"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "_py_entity",
|
||||
"args": [
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"id": "module:m_a",
|
||||
"kind": "module",
|
||||
"maturity": "WIRED",
|
||||
"aliases": [
|
||||
"module:m_old"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "capability:c1",
|
||||
"kind": "capability",
|
||||
"maturity": "DECLARED"
|
||||
}
|
||||
],
|
||||
"relationships": []
|
||||
},
|
||||
"module:ghost"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "_py_entity",
|
||||
"args": [
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"id": "module:m_a",
|
||||
"kind": "module",
|
||||
"maturity": "WIRED",
|
||||
"aliases": [
|
||||
"module:m_old"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "capability:c1",
|
||||
"kind": "capability",
|
||||
"maturity": "DECLARED"
|
||||
}
|
||||
],
|
||||
"relationships": []
|
||||
},
|
||||
""
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "_py_entity",
|
||||
"args": [
|
||||
{
|
||||
"entities": []
|
||||
},
|
||||
"x"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "_rust_core",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
//! branch_battery_gap.rs — spine gap-closing parity battery (#2479).
|
||||
//!
|
||||
//! Replays the two frozen truth artifacts VERBATIM (include_str! — never
|
||||
//! hand-transcribed):
|
||||
//! * frozen_spine_gap_truth.json — the SHARED freeze_truth harness
|
||||
//! output (byte-verified against the live oracle via
|
||||
//! `scripts/freeze_truth.py --verify ... --module rune.spine`).
|
||||
//! * frozen_spine_gap_clocked.json — sandbox/clock/error cases frozen
|
||||
//! from the live oracle with the process clock pinned and the IO
|
||||
//! roots sandboxed; each case carries its fixture `files` back so
|
||||
//! this battery materializes the identical tree.
|
||||
//!
|
||||
//! Comparison law (rune_spine facade-battery precedent): key-order
|
||||
//! insensitive via store::sorted_deep on BOTH sides, then pyjson::dumps
|
||||
//! byte-equality; array order exact. Error cases must carry the oracle's
|
||||
//! EXACT message; the one documented transformation is the loader
|
||||
//! wrapper: the oracle's delegated-path RuntimeError reads
|
||||
//! "sp_project core error: <kernel message>" — the loader adds that
|
||||
//! prefix when unwrapping the envelope, so it is stripped before the
|
||||
//! kernel-message comparison.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::gap;
|
||||
use rune_spine::pyjson::{self, Json};
|
||||
use rune_spine::store;
|
||||
|
||||
const HARNESS_TRUTH: &str = include_str!("../frozen_spine_gap_truth.json");
|
||||
const CLOCKED_TRUTH: &str = include_str!("../frozen_spine_gap_clocked.json");
|
||||
|
||||
fn parse(s: &str) -> Json {
|
||||
pyjson::parse(s).unwrap_or_else(|e| panic!("parse failed: {} :: {}", e, s))
|
||||
}
|
||||
|
||||
fn canon(v: &Json) -> String {
|
||||
pyjson::dumps(&store::sorted_deep(v))
|
||||
}
|
||||
|
||||
fn expect_field<'a>(case: &'a Json, key: &str) -> &'a Json {
|
||||
case.get(key)
|
||||
.unwrap_or_else(|| panic!("case.{} missing", key))
|
||||
}
|
||||
|
||||
fn materialize(files: &Json, root: &Path) {
|
||||
if let Some(Json::Obj(pairs)) = Some(files) {
|
||||
for (rel, content) in pairs {
|
||||
let p = root.join(rel);
|
||||
fs::create_dir_all(p.parent().unwrap_or(root))
|
||||
.unwrap_or_else(|e| panic!("mkdir {}: {}", p.display(), e));
|
||||
let text = match content {
|
||||
Json::Str(t) => t.clone(),
|
||||
other => pyjson::dumps(other),
|
||||
};
|
||||
fs::write(&p, text).unwrap_or_else(|e| panic!("write {}: {}", p.display(), e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox_dir(idx: usize) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!(
|
||||
"spine-gap-battery-{}-{}",
|
||||
std::process::id(),
|
||||
idx
|
||||
));
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
fs::create_dir_all(&d).expect("sandbox mkdir");
|
||||
d
|
||||
}
|
||||
|
||||
/// Dispatch one harness case (args = oracle positional args, in order).
|
||||
fn replay_harness(case: &Json) -> Result<Json, String> {
|
||||
let fname = store::as_str(expect_field(case, "fn")).to_string();
|
||||
let args = expect_field(case, "args");
|
||||
let items = match args {
|
||||
Json::Arr(items) => items.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
match fname.as_str() {
|
||||
"_py_sorted" => Ok(gap::_py_sorted(&items[0])),
|
||||
"_py_declared_surfaces" => gap::_py_declared_surfaces(&items[0]),
|
||||
"_py_entity" => Ok(gap::_py_entity(&items[0], store::as_str(&items[1]))),
|
||||
"_rust_core" => Ok(match gap::_rust_core("python", true, true) {
|
||||
Some(name) => Json::Str(name.to_string()),
|
||||
None => Json::Null,
|
||||
}),
|
||||
other => Err(format!("unknown fn in harness truth: {}", other)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The loader-wrapper prefix the Python loader adds to delegated-path
|
||||
/// kernel errors (documented battery law — see module docs).
|
||||
fn strip_loader_prefix(msg: &str) -> &str {
|
||||
msg.strip_prefix("sp_project core error: ").unwrap_or(msg)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod truth_battery {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn harness_truth_replays_verbatim() {
|
||||
let doc = parse(HARNESS_TRUTH);
|
||||
let cases = doc.get("cases").expect("envelope.cases");
|
||||
let mut n = 0usize;
|
||||
if let Some(Json::Arr(items)) = Some(cases) {
|
||||
for case in items {
|
||||
let fname = store::as_str(expect_field(case, "fn"));
|
||||
let got = replay_harness(case)
|
||||
.unwrap_or_else(|e| panic!("{} dispatch failed: {}", fname, e));
|
||||
let expect = expect_field(case, "expected");
|
||||
assert_eq!(
|
||||
canon(&got),
|
||||
canon(expect),
|
||||
"case {} parity (canon)",
|
||||
fname
|
||||
);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(n, 21, "harness case count");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clocked_truth_replays_verbatim() {
|
||||
let doc = parse(CLOCKED_TRUTH);
|
||||
let frozen_now = store::as_str(doc.get("frozen_now").expect("frozen_now")).to_string();
|
||||
let stamp = format!("{}Z", frozen_now);
|
||||
let cases = doc.get("cases").expect("cases");
|
||||
let mut n = 0usize;
|
||||
if let Some(Json::Arr(items)) = Some(cases) {
|
||||
for (idx, case) in items.iter().enumerate() {
|
||||
let fname = store::as_str(expect_field(case, "fn")).to_string();
|
||||
let name = store::as_str(expect_field(case, "name")).to_string();
|
||||
let files = case.get("files").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
let root = sandbox_dir(idx);
|
||||
materialize(&files, &root);
|
||||
let args = match expect_field(case, "args") {
|
||||
Json::Arr(items) => items.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
let kwargs = expect_field(case, "kwargs").clone();
|
||||
let _ = kwargs;
|
||||
|
||||
let result: Result<Json, String> = match fname.as_str() {
|
||||
"_module_imports" => {
|
||||
let module = store::as_str(&args[0]).to_string();
|
||||
let src = fs::read_to_string(
|
||||
root.join("src/rune").join(format!("{}.py", module)),
|
||||
)
|
||||
.ok();
|
||||
gap::_module_imports(&module, src.as_deref())
|
||||
.map(|names| Json::Arr(names.into_iter().map(Json::Str).collect()))
|
||||
}
|
||||
"_gather_meta_inputs" => gap::_gather_meta_inputs(&root, None),
|
||||
"_deployed_source_files" => Ok(Json::Arr(
|
||||
gap::_deployed_source_files(&root.join("deployed"))
|
||||
.into_iter()
|
||||
.map(Json::Str)
|
||||
.collect(),
|
||||
)),
|
||||
"_rust_core" => {
|
||||
// both frozen branches: loader importable, .so absent
|
||||
Ok(match gap::_rust_core("auto", true, false) {
|
||||
Some(x) => Json::Str(x.to_string()),
|
||||
None => Json::Null,
|
||||
})
|
||||
}
|
||||
"_py_project_meta_surfaces" => gap::_py_project_meta_surfaces(
|
||||
&args[0],
|
||||
&args[1],
|
||||
&stamp,
|
||||
)
|
||||
.map(|out| {
|
||||
// the frozen expected carries the oracle's return
|
||||
// (the count int); post_doc carries the mutated doc
|
||||
Json::Obj(vec![
|
||||
("result".into(), out.get("count").cloned().unwrap_or(Json::Null)),
|
||||
("doc".into(), out.get("doc").cloned().unwrap_or(Json::Null)),
|
||||
])
|
||||
}),
|
||||
"_py_ingest_umbrellas_data" => {
|
||||
gap::_py_ingest_umbrellas_data(&args[0], &args[1], &stamp).map(|out| {
|
||||
Json::Obj(vec![
|
||||
("result".into(), out.get("count").cloned().unwrap_or(Json::Null)),
|
||||
("doc".into(), out.get("doc").cloned().unwrap_or(Json::Null)),
|
||||
])
|
||||
})
|
||||
}
|
||||
"_py_build_from_inputs" => {
|
||||
// kernel_absent_raises froze under the python pin
|
||||
// (delegation-only stub); the other three cases ran
|
||||
// kernel-live — including generated_view_refused,
|
||||
// whose kernel error the loader prefix-strip below
|
||||
// unprefixes.
|
||||
let core_up = name != "kernel_absent_raises";
|
||||
gap::_py_build_from_inputs(&args[0], core_up)
|
||||
}
|
||||
"_gather_drift_inputs" => {
|
||||
// the Python-side gathers the generator patched in
|
||||
// are case fixtures (the registry/graph seam):
|
||||
let registry = vec!["canary_alpha".to_string(), "canary_beta".to_string()];
|
||||
let graph = parse(
|
||||
r#"{"alpha": ["json", "rune"], "beta": ["sys"]}"#,
|
||||
);
|
||||
// the oracle's _test_backed_modules strips the
|
||||
// test_ prefix (p.stem[5:])
|
||||
let mut tested: Vec<String> = file_stems_under(
|
||||
&root.join("tests"),
|
||||
"test_",
|
||||
".py",
|
||||
)
|
||||
.into_iter()
|
||||
.map(|stem| stem["test_".len()..].to_string())
|
||||
.collect();
|
||||
tested.sort();
|
||||
let src_stems = readdir_stems(&root.join("src/rune"), ".py");
|
||||
gap::_gather_drift_inputs(
|
||||
&root,
|
||||
®istry,
|
||||
&graph,
|
||||
&tested,
|
||||
&src_stems,
|
||||
&Json::Null,
|
||||
)
|
||||
}
|
||||
"_py_drift_from_inputs" => gap::_py_drift_from_inputs(&args[0], &args[1], false),
|
||||
"_py_export_umbrellas" => {
|
||||
gap::_py_export_umbrellas(&args[0], &stamp, false)
|
||||
}
|
||||
"_py_query" => gap::_py_query(&args[0], store::as_str(&args[1]), false),
|
||||
"_fresh_doc" => gap::_fresh_doc(&root, None),
|
||||
other => panic!("unknown fn in clocked truth: {}", other),
|
||||
};
|
||||
|
||||
match (result, case.get("error")) {
|
||||
(Ok(mut got), None) => {
|
||||
// doc-mutating cases: compare {result, doc} against
|
||||
// expected + post_doc
|
||||
if let Some(post) = case.get("post_doc") {
|
||||
let expect = Json::Obj(vec![
|
||||
("result".into(), expect_field(case, "expected").clone()),
|
||||
("doc".into(), post.clone()),
|
||||
]);
|
||||
assert_eq!(
|
||||
canon(&got),
|
||||
canon(&expect),
|
||||
"case {}/{} parity (canon)",
|
||||
fname,
|
||||
name
|
||||
);
|
||||
} else {
|
||||
if let Json::Obj(ref mut pairs) = got {
|
||||
if pairs.len() == 2
|
||||
&& pairs[0].0 == "result"
|
||||
&& pairs[1].0 == "doc"
|
||||
&& matches!(case.get("expected"), Some(Json::Num(_)))
|
||||
{
|
||||
// kernel doc-wrappers on non-mutating
|
||||
// cases are not expected here
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
canon(&got),
|
||||
canon(expect_field(case, "expected")),
|
||||
"case {}/{} parity (canon)",
|
||||
fname,
|
||||
name
|
||||
);
|
||||
}
|
||||
}
|
||||
(Err(err), Some(err_doc)) => {
|
||||
let msg = store::as_str(
|
||||
err_doc.get("message").expect("error.message"),
|
||||
);
|
||||
assert_eq!(
|
||||
err,
|
||||
strip_loader_prefix(msg),
|
||||
"case {}/{} error text",
|
||||
fname,
|
||||
name
|
||||
);
|
||||
}
|
||||
(other, _) => panic!(
|
||||
"case {}/{}: unexpected outcome: {:?}",
|
||||
fname, name, other
|
||||
),
|
||||
}
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(n, 35, "clocked case count");
|
||||
}
|
||||
|
||||
fn file_stems_under(dir: &Path, prefix: &str, suffix: &str) -> Vec<String> {
|
||||
let mut out = vec![];
|
||||
if let Ok(rd) = fs::read_dir(dir) {
|
||||
for e in rd.flatten() {
|
||||
let name = e.file_name().to_string_lossy().into_owned();
|
||||
if name.len() >= prefix.len() + suffix.len()
|
||||
&& name.starts_with(prefix)
|
||||
&& name.ends_with(suffix)
|
||||
{
|
||||
out.push(name[..name.len() - suffix.len()].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn readdir_stems(dir: &Path, suffix: &str) -> Vec<String> {
|
||||
let mut out = vec![];
|
||||
if let Ok(rd) = fs::read_dir(dir) {
|
||||
for e in rd.flatten() {
|
||||
let name = e.file_name().to_string_lossy().into_owned();
|
||||
if name.ends_with(suffix) {
|
||||
out.push(name[..name.len() - suffix.len()].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The auto+present branch the frozen truth cannot carry (the loader
|
||||
/// module is unrepresentable over FFI): the verdict is Some.
|
||||
#[test]
|
||||
fn rust_core_auto_present_branch() {
|
||||
assert_eq!(gap::_rust_core("auto", true, true), Some("spine_loader"));
|
||||
assert_eq!(gap::_rust_core("auto", false, true), None);
|
||||
assert_eq!(gap::_rust_core("python", true, true), None);
|
||||
}
|
||||
|
||||
/// The _fresh_doc build-and-save seam the pinned oracle cannot reach
|
||||
/// (build() gathers in Python): an injected built doc is returned AND
|
||||
/// persisted with spine.save's canonical text.
|
||||
#[test]
|
||||
fn fresh_doc_absent_store_builds_and_saves() {
|
||||
let root = sandbox_dir(9001);
|
||||
let built = parse(
|
||||
r#"{"schema_version": 1, "entities": [{"id": "module:alpha", "kind": "module", "maturity": "DECLARED", "tags": [], "identities": {}, "evidence_refs": [], "observed_at": "2026-10-13T08:30:00Z"}], "relationships": [], "observations": [], "generated_at": "2026-10-13T08:30:00Z"}"#,
|
||||
);
|
||||
let got = gap::_fresh_doc(&root, Some(&built)).expect("fresh_doc builds");
|
||||
assert_eq!(canon(&got), canon(&built));
|
||||
let persisted = fs::read_to_string(root.join("store/spine/current.json")).expect("saved");
|
||||
let expect_text = rune_spine::facade::save_text(&Json::Obj(vec![("doc".into(), built)]));
|
||||
if let Ok(Json::Str(text)) = expect_text {
|
||||
assert_eq!(persisted, text, "canonical save text");
|
||||
} else {
|
||||
panic!("save_text failed");
|
||||
}
|
||||
// second call loads the persisted store (load path wins)
|
||||
let again = gap::_fresh_doc(&root, None).expect("reload");
|
||||
assert_eq!(canon(&again), canon(&got));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,704 @@
|
||||
//! gap.rs — dichotomy-wave gap-closing kernels (issue #2479 tranche,
|
||||
//! contract BUILDER-2026-10-13T08:11:47Z-9b1f32-factory-gap-sp).
|
||||
//!
|
||||
//! Name-preserving ports of the audit-missing spine.py functions. Two
|
||||
//! classes live here:
|
||||
//!
|
||||
//! REAL kernels (new logic, IO behind injectable seams — the zero-dep
|
||||
//! split law: the Python facade gathers what only Python can gather
|
||||
//! (imports of rune.canary_native, the import graph) and passes it in;
|
||||
//! filesystem probes take an explicit root):
|
||||
//! _module_imports(module, src_text) — the AST-import scan over an
|
||||
//! injected source text (the file read stays Python; None = the
|
||||
//! oracle's OSError branch; a malformed import statement is the
|
||||
//! oracle's SyntaxError branch → empty set).
|
||||
//! _gather_meta_inputs(root, workflows_dir) — the meta-surface gather
|
||||
//! (manifest parse + dir/file probes + glob scans, all explicit).
|
||||
//! _deployed_source_files(root) — the deployed-tree source scan.
|
||||
//! readdir order, NOT sorted — the oracle's glob order is the
|
||||
//! contract (both are readdir sequences on the same directory).
|
||||
//! _gather_drift_inputs(root, registry, graph, tested, src_stems,
|
||||
//! deployed_obs) — assembly + the pyproject version extraction; the
|
||||
//! Python-side gathers (registry/graph/tested/src_stems/deployed_obs)
|
||||
//! are injected seams.
|
||||
//! _fresh_doc(root, built) — load-or-build seam: the store read is
|
||||
//! direct; the absent-store branch takes the caller-gathered build
|
||||
//! result (the oracle's build() gathers in Python) or raises the
|
||||
//! retired-stub error exactly like the pinned oracle.
|
||||
//!
|
||||
//! MACHINERY twins (the audit excludes _py_*/_core fns as fallback
|
||||
//! machinery; these wrap the PROVEN rune_spine kernels — no second
|
||||
//! implementation, strangler law):
|
||||
//! _py_sorted → store::sorted_deep (sf_sorted twin)
|
||||
//! _py_declared_surfaces → facade::declared_surfaces
|
||||
//! _py_project_meta_surfaces → facade::project_meta_surfaces_fn
|
||||
//! _py_ingest_umbrellas_data → facade::ingest_umbrellas_data_fn
|
||||
//! _py_entity → store::entity (sf_entity twin)
|
||||
//! _py_build_from_inputs / _py_drift_from_inputs / _py_export_umbrellas
|
||||
//! / _py_query → project/drift/query kernels with the retired-stub
|
||||
//! core-absent error (battery-pinned message)
|
||||
//! _rust_core(pin, loader_importable, so_present) — the availability
|
||||
//! verdict as data (the module itself is unrepresentable over FFI).
|
||||
//!
|
||||
//! Parity: frozen_spine_gap_truth.json (shared freeze_truth harness,
|
||||
//! byte-verified) + frozen_spine_gap_clocked.json (sandbox/clock/error
|
||||
//! battery) — both replayed VERBATIM in branch_battery_gap.rs.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rune_spine::facade;
|
||||
use rune_spine::pyjson::{self, Json};
|
||||
use rune_spine::store;
|
||||
|
||||
pub const KERNEL_NOT_BUILT: &str = "rune_spine kernel not built — run the rune_spine build; the Python oracle was retired";
|
||||
|
||||
fn s(text: &str) -> Json {
|
||||
Json::Str(text.to_string())
|
||||
}
|
||||
|
||||
fn str_arr(items: &[String]) -> Json {
|
||||
Json::Arr(items.iter().map(|x| s(x)).collect())
|
||||
}
|
||||
|
||||
// ── _module_imports — the AST-import scan over injected source text ───────
|
||||
|
||||
/// One physical line reduced to code bytes: comments stripped, string
|
||||
/// literal CONTENTS blanked (quotes kept as markers so token boundaries
|
||||
/// survive), unterminated triple-quotes spanning lines tracked.
|
||||
#[derive(Default)]
|
||||
struct LineStripper {
|
||||
in_triple: Option<char>,
|
||||
}
|
||||
|
||||
impl LineStripper {
|
||||
fn strip(&mut self, line: &str) -> String {
|
||||
let chars: Vec<char> = line.chars().collect();
|
||||
let mut out = String::with_capacity(line.len());
|
||||
let mut i = 0usize;
|
||||
while i < chars.len() {
|
||||
let c = chars[i];
|
||||
if let Some(q) = self.in_triple {
|
||||
// inside a triple-quoted string: look for the closer
|
||||
if c == q && i + 2 < chars.len() + 1 {
|
||||
let triple: String = chars[i..(i + 3).min(chars.len())].iter().collect();
|
||||
if triple.len() == 3 && triple.chars().all(|x| x == q) {
|
||||
self.in_triple = None;
|
||||
out.push_str(&triple);
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(' ');
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
match c {
|
||||
'#' => break, // comment to end of line
|
||||
'"' | '\'' => {
|
||||
// triple quote opener?
|
||||
let triple: String = chars[i..(i + 3).min(chars.len())].iter().collect();
|
||||
if triple.len() == 3 && triple.chars().all(|x| x == c) {
|
||||
self.in_triple = Some(c);
|
||||
out.push_str(&triple);
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
// single-quoted string: blank contents until closer
|
||||
out.push(c);
|
||||
i += 1;
|
||||
while i < chars.len() {
|
||||
if chars[i] == '\\' && i + 1 < chars.len() {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if chars[i] == c {
|
||||
out.push(c);
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
out.push(' ');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
out.push(c);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn is_ident_start(c: char) -> bool {
|
||||
c.is_alphabetic() || c == '_'
|
||||
}
|
||||
|
||||
fn is_ident_char(c: char) -> bool {
|
||||
c.is_alphanumeric() || c == '_'
|
||||
}
|
||||
|
||||
/// Split source text into logical statements (comment/string-stripped,
|
||||
/// bracket- and backslash-continuations joined, `;`-separated).
|
||||
fn logical_statements(src: &str) -> Vec<String> {
|
||||
let mut stripper = LineStripper::default();
|
||||
let mut logicals: Vec<String> = vec![];
|
||||
let mut pending = String::new();
|
||||
let mut depth: i32 = 0;
|
||||
for raw in src.lines() {
|
||||
let line = stripper.strip(raw);
|
||||
let mut piece = line.as_str();
|
||||
let mut joins = false;
|
||||
if let Some(stripped) = piece.strip_suffix('\\') {
|
||||
piece = stripped;
|
||||
joins = true;
|
||||
}
|
||||
for ch in piece.chars() {
|
||||
match ch {
|
||||
'(' | '[' | '{' => depth += 1,
|
||||
')' | ']' | '}' => depth -= 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if !pending.is_empty() {
|
||||
pending.push(' ');
|
||||
}
|
||||
pending.push_str(piece.trim_end());
|
||||
if joins || depth > 0 {
|
||||
continue;
|
||||
}
|
||||
logicals.push(std::mem::take(&mut pending));
|
||||
}
|
||||
if !pending.is_empty() {
|
||||
logicals.push(pending);
|
||||
}
|
||||
// split each logical line on ';' at depth 0
|
||||
let mut out: Vec<String> = vec![];
|
||||
for line in logicals {
|
||||
let mut cur = String::new();
|
||||
let mut d: i32 = 0;
|
||||
for ch in line.chars() {
|
||||
match ch {
|
||||
'(' | '[' | '{' => {
|
||||
d += 1;
|
||||
cur.push(ch);
|
||||
}
|
||||
')' | ']' | '}' => {
|
||||
d -= 1;
|
||||
cur.push(ch);
|
||||
}
|
||||
';' if d == 0 => out.push(std::mem::take(&mut cur)),
|
||||
_ => cur.push(ch),
|
||||
}
|
||||
}
|
||||
if !cur.trim().is_empty() {
|
||||
out.push(cur);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Parse one dotted identifier starting at `chars[i]`; returns (text, next index).
|
||||
fn parse_dotted(chars: &[char], mut i: usize) -> Result<(String, usize), String> {
|
||||
if i >= chars.len() || !is_ident_start(chars[i]) {
|
||||
return Err(format!("expected identifier at {}", i));
|
||||
}
|
||||
let start = i;
|
||||
while i < chars.len() && (is_ident_char(chars[i]) || chars[i] == '.') {
|
||||
i += 1;
|
||||
}
|
||||
// trailing dot is malformed
|
||||
if chars[i - 1] == '.' {
|
||||
return Err("trailing dot in dotted name".to_string());
|
||||
}
|
||||
Ok((chars[start..i].iter().collect(), i))
|
||||
}
|
||||
|
||||
fn skip_ws(chars: &[char], mut i: usize) -> usize {
|
||||
while i < chars.len() && chars[i].is_whitespace() {
|
||||
i += 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
fn expect_keyword(chars: &[char], i: usize, kw: &str) -> Result<usize, String> {
|
||||
let end = skip_ws(chars, i);
|
||||
let kwc: Vec<char> = kw.chars().collect();
|
||||
if chars.len() >= end + kwc.len() && chars[end..end + kwc.len()] == kwc[..] {
|
||||
let after = end + kwc.len();
|
||||
if after >= chars.len() || !is_ident_char(chars[after]) {
|
||||
return Ok(after);
|
||||
}
|
||||
}
|
||||
Err(format!("expected `{}`", kw))
|
||||
}
|
||||
|
||||
/// Parse the names after `import` (comma-separated dotted names, optional
|
||||
/// `as x` aliases). Any malformed token → Err (the SyntaxError branch).
|
||||
fn parse_import_names(chars: &[char], mut i: usize) -> Result<Vec<String>, String> {
|
||||
let mut names = vec![];
|
||||
loop {
|
||||
i = skip_ws(chars, i);
|
||||
let (dotted, next) = parse_dotted(chars, i)?;
|
||||
names.push(dotted);
|
||||
i = skip_ws(chars, next);
|
||||
if i < chars.len() && chars[i] == 'a' {
|
||||
let after = expect_keyword(chars, i, "as")?;
|
||||
i = skip_ws(chars, after);
|
||||
let (_alias, next) = parse_dotted(chars, i)?;
|
||||
i = next;
|
||||
}
|
||||
i = skip_ws(chars, i);
|
||||
if i < chars.len() && chars[i] == ',' {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if i < chars.len() {
|
||||
return Err("trailing tokens after import names".to_string());
|
||||
}
|
||||
break;
|
||||
}
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// The spine._module_imports core: top-level imported module names from
|
||||
/// injected source text. `src_text=None` mirrors the oracle's OSError
|
||||
/// branch; a malformed import statement mirrors its SyntaxError branch
|
||||
/// (empty result). The set is returned as a sorted list (FFI law: sets
|
||||
/// cross the wire sorted).
|
||||
pub fn _module_imports(module: &str, src_text: Option<&str>) -> Result<Vec<String>, String> {
|
||||
let _ = module; // the oracle keys the file read by it; text is injected
|
||||
let text = match src_text {
|
||||
None => return Ok(vec![]),
|
||||
Some(t) => t,
|
||||
};
|
||||
let mut names: Vec<String> = vec![];
|
||||
for stmt in logical_statements(text) {
|
||||
let trimmed = stmt.trim_start();
|
||||
let chars: Vec<char> = trimmed.chars().collect();
|
||||
if chars.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// A malformed import statement mirrors the oracle's SyntaxError
|
||||
// branch: ast.parse refuses the whole file → empty set.
|
||||
if let Ok(after) = expect_keyword(&chars, 0, "import") {
|
||||
let dotted_names = match parse_import_names(&chars, after) {
|
||||
Ok(names) => names,
|
||||
Err(_) => return Ok(vec![]),
|
||||
};
|
||||
for dotted in dotted_names {
|
||||
let top = dotted.split('.').next().unwrap_or("").to_string();
|
||||
if !names.contains(&top) {
|
||||
names.push(top);
|
||||
}
|
||||
}
|
||||
} else if let Ok(after_from) = expect_keyword(&chars, 0, "from") {
|
||||
// module part: leading dots (relative) then a dotted name
|
||||
let mut i = skip_ws(&chars, after_from);
|
||||
while i < chars.len() && chars[i] == '.' {
|
||||
i += 1;
|
||||
}
|
||||
let parsed: Result<(), String> = if i < chars.len() && is_ident_start(chars[i]) {
|
||||
parse_dotted(&chars, i).and_then(|(module_path, next)| {
|
||||
expect_keyword(&chars, next, "import").map(|_| module_path)
|
||||
})
|
||||
.map(|module_path| {
|
||||
// `from x.y import ...` → node.module = "x.y" → "x";
|
||||
// `from .x import ...` → node.module = "x" → "x"
|
||||
let top = module_path.split('.').next().unwrap_or("").to_string();
|
||||
if !names.contains(&top) {
|
||||
names.push(top);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// `from . import x` → node.module = None → skipped by the
|
||||
// oracle; still validate the import keyword + names
|
||||
expect_keyword(&chars, i, "import")
|
||||
.and_then(|after_import| parse_import_names(&chars, after_import).map(|_| ()))
|
||||
};
|
||||
if parsed.is_err() {
|
||||
return Ok(vec![]); // the oracle's SyntaxError branch
|
||||
}
|
||||
}
|
||||
// non-import statements: not validated (a full Python grammar is
|
||||
// out of scope; the oracle's only observable here is the import set)
|
||||
}
|
||||
names.sort();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
// ── _gather_meta_inputs — the meta-surface gather under an explicit root ──
|
||||
|
||||
fn read_manifest(root: &Path) -> Option<Json> {
|
||||
let text = fs::read_to_string(root.join("config").join("umbrellas.json")).ok()?;
|
||||
pyjson::parse(&text).ok()
|
||||
}
|
||||
|
||||
fn dir_entry_names(dir: &Path) -> Vec<String> {
|
||||
let mut names: Vec<String> = match fs::read_dir(dir) {
|
||||
Ok(rd) => rd.filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().into_owned()).collect(),
|
||||
Err(_) => vec![],
|
||||
};
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
fn glob_match(name: &str, prefix: &str, suffix: &str) -> bool {
|
||||
name.len() >= prefix.len() + suffix.len()
|
||||
&& name.starts_with(prefix)
|
||||
&& name.ends_with(suffix)
|
||||
}
|
||||
|
||||
/// The spine._gather_meta_inputs core. `workflows_dir` defaults to
|
||||
/// `<root>/.gitea/workflows` (the facade passes the module global so the
|
||||
/// monkeypatch seam holds). Canaries and workflows are SORTED (the
|
||||
/// oracle's explicit sorted() calls); existence probes are direct.
|
||||
pub fn _gather_meta_inputs(root: &Path, workflows_dir: Option<&Path>) -> Result<Json, String> {
|
||||
let manifest = read_manifest(root);
|
||||
// declared surfaces: tolerant parse (the oracle's except → empty)
|
||||
let declared = facade::declared_surfaces(&Json::Obj(vec![(
|
||||
"config_manifest".into(),
|
||||
manifest.clone().unwrap_or(Json::Null),
|
||||
)]))?;
|
||||
let declared_crates: Vec<(String, String)> = match declared.get("rust_crates") {
|
||||
Some(Json::Arr(items)) => items
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
Json::Arr(pair) if pair.len() == 2 => Some((
|
||||
store::as_str(&pair[0]).to_string(),
|
||||
store::as_str(&pair[1]).to_string(),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
let declared_go: Vec<(String, String)> = match declared.get("go_surfaces") {
|
||||
Some(Json::Arr(items)) => items
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
Json::Arr(pair) if pair.len() == 2 => Some((
|
||||
store::as_str(&pair[0]).to_string(),
|
||||
store::as_str(&pair[1]).to_string(),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
let declared_cfg: Vec<(String, String)> = match declared.get("configs") {
|
||||
Some(Json::Arr(items)) => items
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
Json::Arr(pair) if pair.len() == 2 => Some((
|
||||
store::as_str(&pair[0]).to_string(),
|
||||
store::as_str(&pair[1]).to_string(),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let mut crate_dirs: Vec<String> = vec![];
|
||||
for (c, _) in &declared_crates {
|
||||
if root.join("src").join("rust").join(c).is_dir() {
|
||||
crate_dirs.push(c.clone());
|
||||
}
|
||||
}
|
||||
let mut crate_canaries: Vec<(String, Json)> = vec![];
|
||||
for c in &crate_dirs {
|
||||
let dir = root.join("src").join("rust").join(c);
|
||||
let mut stems: Vec<String> = dir_entry_names(&dir)
|
||||
.into_iter()
|
||||
.filter(|n| glob_match(n, "canary_", ".py"))
|
||||
.map(|n| n.trim_end_matches(".py").to_string())
|
||||
.collect();
|
||||
stems.sort();
|
||||
crate_canaries.push((c.clone(), str_arr(&stems)));
|
||||
}
|
||||
let mut go_dirs: Vec<String> = vec![];
|
||||
for (g, _) in &declared_go {
|
||||
if root.join("src").join("go").join(g).is_dir() {
|
||||
go_dirs.push(g.clone());
|
||||
}
|
||||
}
|
||||
let mut config_files: Vec<String> = vec![];
|
||||
for (cfg, _) in &declared_cfg {
|
||||
if root.join(cfg).is_file() {
|
||||
config_files.push(cfg.clone());
|
||||
}
|
||||
}
|
||||
let wf_dir = workflows_dir
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| root.join(".gitea").join("workflows"));
|
||||
let mut workflows: Vec<String> = vec![];
|
||||
if wf_dir.is_dir() {
|
||||
workflows = dir_entry_names(&wf_dir)
|
||||
.into_iter()
|
||||
.filter(|n| n.ends_with(".yml"))
|
||||
.collect();
|
||||
workflows.sort();
|
||||
}
|
||||
Ok(Json::Obj(vec![
|
||||
("config_manifest".into(), manifest.unwrap_or(Json::Null)),
|
||||
("crate_dirs".into(), str_arr(&crate_dirs)),
|
||||
("crate_canaries".into(), Json::Obj(crate_canaries)),
|
||||
("go_dirs".into(), str_arr(&go_dirs)),
|
||||
("config_files".into(), str_arr(&config_files)),
|
||||
("workflows".into(), str_arr(&workflows)),
|
||||
]))
|
||||
}
|
||||
|
||||
// ── _deployed_source_files — readdir-order source scan ────────────────────
|
||||
|
||||
/// The spine._deployed_source_files core: relative source paths under the
|
||||
/// deployed link that exist as files. Order is the oracle's glob order —
|
||||
/// readdir sequence per directory, NOT sorted (both cores walk the same
|
||||
/// directory, so the sequences agree).
|
||||
pub fn _deployed_source_files(root: &Path) -> Vec<String> {
|
||||
let mut out: Vec<String> = vec![];
|
||||
let bases = [
|
||||
(root.join("src").join("rune"), "*.py" , false),
|
||||
(root.join("src").join("rust"), "*/canary_*.py", true),
|
||||
];
|
||||
for (base, pattern, nested) in bases {
|
||||
if !base.is_dir() {
|
||||
continue;
|
||||
}
|
||||
if !nested {
|
||||
for name in readdir_names(&base) {
|
||||
if name.ends_with(".py") {
|
||||
let p = base.join(&name);
|
||||
if p.is_file() {
|
||||
out.push(rel_posix(root, &p));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for sub in readdir_names(&base) {
|
||||
let sub_dir = base.join(&sub);
|
||||
if !sub_dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
for name in readdir_names(&sub_dir) {
|
||||
if glob_match(&name, "canary_", ".py") {
|
||||
let p = sub_dir.join(&name);
|
||||
if p.is_file() {
|
||||
out.push(rel_posix(root, &p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = pattern;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn readdir_names(dir: &Path) -> Vec<String> {
|
||||
match fs::read_dir(dir) {
|
||||
Ok(rd) => rd
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.collect(),
|
||||
Err(_) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn rel_posix(root: &Path, p: &Path) -> String {
|
||||
p.strip_prefix(root).unwrap_or(p).to_string_lossy().replace('\\', "/")
|
||||
}
|
||||
|
||||
// ── _gather_drift_inputs — assembly + pyproject version extraction ────────
|
||||
|
||||
/// The spine._gather_drift_inputs core. registry/graph/tested/src_stems/
|
||||
/// deployed_obs are the Python-side gathers (injected seams — the registry
|
||||
/// imports rune.canary_native, the graph needs the AST scan); the version
|
||||
/// comes from `<root>/pyproject.toml` via the oracle's anchored regex.
|
||||
/// graph values and tested are sorted here (the oracle's sorted() calls).
|
||||
pub fn _gather_drift_inputs(
|
||||
root: &Path,
|
||||
registry: &[String],
|
||||
graph: &Json,
|
||||
tested: &[String],
|
||||
src_stems: &[String],
|
||||
deployed_obs: &Json,
|
||||
) -> Result<Json, String> {
|
||||
let mut sorted_graph: Vec<(String, Json)> = vec![];
|
||||
if let Some(Json::Obj(pairs)) = Some(graph) {
|
||||
for (m, deps) in pairs {
|
||||
let mut list: Vec<String> = store::as_arr(deps).iter().map(|x| store::as_str(x).to_string()).collect();
|
||||
list.sort();
|
||||
sorted_graph.push((m.clone(), str_arr(&list)));
|
||||
}
|
||||
}
|
||||
let mut sorted_tested: Vec<String> = tested.to_vec();
|
||||
sorted_tested.sort();
|
||||
let src_version = pyproject_version(root);
|
||||
Ok(Json::Obj(vec![
|
||||
("registry".into(), str_arr(registry)),
|
||||
("graph".into(), Json::Obj(sorted_graph)),
|
||||
("tested".into(), str_arr(&sorted_tested)),
|
||||
("src_stems".into(), str_arr(src_stems)),
|
||||
("deployed_obs".into(), deployed_obs.clone()),
|
||||
("src_version".into(), s(&src_version)),
|
||||
]))
|
||||
}
|
||||
|
||||
/// The oracle's `re.search(r'^version\s*=\s*"([^"]+)"', text, re.M)`:
|
||||
/// line-anchored `version` key with a double-quoted value.
|
||||
fn pyproject_version(root: &Path) -> String {
|
||||
let text = match fs::read_to_string(root.join("pyproject.toml")) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
for line in text.split('\n') {
|
||||
if !line.starts_with("version") {
|
||||
continue;
|
||||
}
|
||||
let rest = &line["version".len()..];
|
||||
let mut chars = rest.chars().peekable();
|
||||
while matches!(chars.peek(), Some(c) if c.is_whitespace()) {
|
||||
chars.next();
|
||||
}
|
||||
if chars.next() != Some('=') {
|
||||
continue;
|
||||
}
|
||||
while matches!(chars.peek(), Some(c) if c.is_whitespace()) {
|
||||
chars.next();
|
||||
}
|
||||
if chars.next() != Some('"') {
|
||||
continue;
|
||||
}
|
||||
let mut value = String::new();
|
||||
let mut closed = false;
|
||||
for c in chars {
|
||||
if c == '"' {
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
value.push(c);
|
||||
}
|
||||
if closed {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
// ── _fresh_doc — load-or-build seam ───────────────────────────────────────
|
||||
|
||||
/// The spine._fresh_doc core over an explicit root: the store read is
|
||||
/// direct (missing/unparseable → absent); the absent-store branch takes
|
||||
/// the caller-gathered build result (`built` — the oracle's build()
|
||||
/// gathers in Python) and persists it with spine.save's canonical text;
|
||||
/// with no injected build the retired-stub error is raised exactly like
|
||||
/// the pinned oracle.
|
||||
pub fn _fresh_doc(root: &Path, built: Option<&Json>) -> Result<Json, String> {
|
||||
let store_path = root.join("store").join("spine").join("current.json");
|
||||
if store_path.is_file() {
|
||||
if let Ok(text) = fs::read_to_string(&store_path) {
|
||||
if let Ok(doc) = pyjson::parse(&text) {
|
||||
return Ok(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
match built {
|
||||
Some(doc) => {
|
||||
let text = facade::save_text(&Json::Obj(vec![("doc".into(), doc.clone())]))?;
|
||||
if let Json::Str(body) = text {
|
||||
if let Some(parent) = store_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||
}
|
||||
fs::write(&store_path, body).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(doc.clone())
|
||||
}
|
||||
None => Err(KERNEL_NOT_BUILT.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── _rust_core — the availability verdict as data ─────────────────────────
|
||||
|
||||
/// The spine._rust_core verdict: pin=="python" → None; loader not
|
||||
/// importable → None; loader available → Some (represented as the loader
|
||||
/// name — the module itself is unrepresentable over FFI).
|
||||
pub fn _rust_core(pin: &str, loader_importable: bool, so_present: bool) -> Option<&'static str> {
|
||||
if pin == "python" {
|
||||
return None;
|
||||
}
|
||||
if !loader_importable {
|
||||
return None;
|
||||
}
|
||||
if so_present {
|
||||
Some("spine_loader")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// ── machinery twins — thin routes to the PROVEN rune_spine kernels ────────
|
||||
|
||||
pub fn _py_sorted(v: &Json) -> Json {
|
||||
store::sorted_deep(v)
|
||||
}
|
||||
|
||||
pub fn _py_declared_surfaces(manifest: &Json) -> Result<Json, String> {
|
||||
facade::declared_surfaces(&Json::Obj(vec![(
|
||||
"config_manifest".into(),
|
||||
manifest.clone(),
|
||||
)]))
|
||||
}
|
||||
|
||||
pub fn _py_project_meta_surfaces(doc: &Json, inputs: &Json, now: &str) -> Result<Json, String> {
|
||||
facade::project_meta_surfaces_fn(&Json::Obj(vec![
|
||||
("doc".into(), doc.clone()),
|
||||
("inputs".into(), inputs.clone()),
|
||||
("now".into(), s(now)),
|
||||
]))
|
||||
}
|
||||
|
||||
pub fn _py_ingest_umbrellas_data(doc: &Json, data: &Json, now: &str) -> Result<Json, String> {
|
||||
facade::ingest_umbrellas_data_fn(&Json::Obj(vec![
|
||||
("doc".into(), doc.clone()),
|
||||
("data".into(), data.clone()),
|
||||
("now".into(), s(now)),
|
||||
]))
|
||||
}
|
||||
|
||||
pub fn _py_entity(doc: &Json, id: &str) -> Json {
|
||||
match store::entity(doc, id) {
|
||||
Some(e) => e.clone(),
|
||||
None => Json::Null,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _py_build_from_inputs(inputs: &Json, core_available: bool) -> Result<Json, String> {
|
||||
if !core_available {
|
||||
return Err(KERNEL_NOT_BUILT.to_string());
|
||||
}
|
||||
rune_spine::project::project(inputs)
|
||||
}
|
||||
|
||||
pub fn _py_drift_from_inputs(doc: &Json, inputs: &Json, core_available: bool) -> Result<Json, String> {
|
||||
if !core_available {
|
||||
return Err(KERNEL_NOT_BUILT.to_string());
|
||||
}
|
||||
Ok(rune_spine::drift::drift(doc, inputs))
|
||||
}
|
||||
|
||||
pub fn _py_export_umbrellas(doc: &Json, now: &str, core_available: bool) -> Result<Json, String> {
|
||||
if !core_available {
|
||||
return Err(KERNEL_NOT_BUILT.to_string());
|
||||
}
|
||||
rune_spine::query::export_umbrellas(doc, now)
|
||||
}
|
||||
|
||||
pub fn _py_query(doc: &Json, entity_id: &str, core_available: bool) -> Result<Json, String> {
|
||||
if !core_available {
|
||||
return Err(KERNEL_NOT_BUILT.to_string());
|
||||
}
|
||||
Ok(rune_spine::query::query(doc, entity_id))
|
||||
}
|
||||
@@ -1,7 +1,317 @@
|
||||
//! spine — Rune's self-model spine (W6 deep port).
|
||||
//! PARITY port of the core data structures from src/rune/spine.py.
|
||||
//!
|
||||
//! Dichotomy-wave gap kernels (issue #2479 tranche) live in `gap.rs` and
|
||||
//! are exposed as doc-envelope FFI below: every symbol takes ONE JSON
|
||||
//! argument string and returns {"ok": true, "value": ...} or
|
||||
//! {"ok": false, "err": "..."}. NOTE: no free symbol is exported here —
|
||||
//! capability_scoring (same process) owns cs_free_string; callers free
|
||||
//! every returned pointer through it (loader ABI law, recall_uplift
|
||||
//! precedent). FFI segfault law: the loader pins argtypes + restype
|
||||
//! (c_void_p returns — never the default c_int).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery_gap;
|
||||
|
||||
pub mod gap;
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use rune_spine::pyjson::{self, Json};
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Json, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".to_string());
|
||||
}
|
||||
let text = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| format!("argument is not valid UTF-8: {}", e))?;
|
||||
pyjson::parse(text).map_err(|e| format!("argument JSON parse failed: {}", e))
|
||||
}
|
||||
|
||||
fn envelope(result: Result<Json, String>) -> *mut c_char {
|
||||
let body = match result {
|
||||
Ok(value) => Json::Obj(vec![
|
||||
("ok".to_string(), Json::Bool(true)),
|
||||
("value".to_string(), value),
|
||||
]),
|
||||
Err(err) => Json::Obj(vec![
|
||||
("ok".to_string(), Json::Bool(false)),
|
||||
("err".to_string(), Json::Str(err)),
|
||||
]),
|
||||
};
|
||||
let out = pyjson::dumps(&body);
|
||||
CString::new(out)
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn root_arg(v: &Json, key: &str) -> Result<std::path::PathBuf, String> {
|
||||
match v.get(key).and_then(|x| match x {
|
||||
Json::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
}) {
|
||||
Some(r) if !r.is_empty() => Ok(std::path::PathBuf::from(r)),
|
||||
_ => Err(format!("{} (root path string) is required", key)),
|
||||
}
|
||||
}
|
||||
|
||||
/// sp_module_imports({"module": str, "src_text": str|null}) -> [names]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_module_imports(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let module = a.get("module").and_then(|x| match x {
|
||||
Json::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
}).ok_or("arg.module required")?;
|
||||
let src_text = match a.get("src_text") {
|
||||
Some(Json::Str(t)) => Some(t.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let names = gap::_module_imports(&module, src_text.as_deref())?;
|
||||
Ok(Json::Arr(names.into_iter().map(Json::Str).collect()))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_gather_meta_inputs({"root": str, "workflows_dir"?: str}) -> inputs
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_gather_meta_inputs(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let root = root_arg(&a, "root")?;
|
||||
let wf = a.get("workflows_dir").and_then(|x| match x {
|
||||
Json::Str(s) => Some(std::path::PathBuf::from(s.clone())),
|
||||
_ => None,
|
||||
});
|
||||
gap::_gather_meta_inputs(&root, wf.as_deref())
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_deployed_source_files({"root": str}) -> [relative paths]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_deployed_source_files(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let root = root_arg(&a, "root")?;
|
||||
Ok(Json::Arr(
|
||||
gap::_deployed_source_files(&root).into_iter().map(Json::Str).collect(),
|
||||
))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_gather_drift_inputs({"root","registry","graph","tested","src_stems",
|
||||
/// "deployed_obs"}) -> inputs
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_gather_drift_inputs(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let root = root_arg(&a, "root")?;
|
||||
let registry: Vec<String> = a
|
||||
.get("registry")
|
||||
.and_then(|x| match x {
|
||||
Json::Arr(items) => Some(
|
||||
items
|
||||
.iter()
|
||||
.map(|i| rune_spine::store::as_str(i).to_string())
|
||||
.collect(),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or("arg.registry required")?;
|
||||
let graph = a.get("graph").cloned().ok_or("arg.graph required")?;
|
||||
let tested: Vec<String> = a
|
||||
.get("tested")
|
||||
.and_then(|x| match x {
|
||||
Json::Arr(items) => Some(
|
||||
items
|
||||
.iter()
|
||||
.map(|i| rune_spine::store::as_str(i).to_string())
|
||||
.collect(),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or("arg.tested required")?;
|
||||
let src_stems: Vec<String> = a
|
||||
.get("src_stems")
|
||||
.and_then(|x| match x {
|
||||
Json::Arr(items) => Some(
|
||||
items
|
||||
.iter()
|
||||
.map(|i| rune_spine::store::as_str(i).to_string())
|
||||
.collect(),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or("arg.src_stems required")?;
|
||||
let deployed_obs = a.get("deployed_obs").cloned().unwrap_or(Json::Null);
|
||||
gap::_gather_drift_inputs(
|
||||
&root,
|
||||
®istry,
|
||||
&graph,
|
||||
&tested,
|
||||
&src_stems,
|
||||
&deployed_obs,
|
||||
)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_fresh_doc({"root": str, "built"?: doc}) -> doc
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_fresh_doc(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let root = root_arg(&a, "root")?;
|
||||
let built = a.get("built").cloned();
|
||||
gap::_fresh_doc(&root, built.as_ref())
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_rust_core({"pin","loader_importable","so_present"}) -> null|"spine_loader"
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_rust_core(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let pin = a
|
||||
.get("pin")
|
||||
.and_then(|x| match x {
|
||||
Json::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or("arg.pin required")?;
|
||||
let loader_importable = matches!(a.get("loader_importable"), Some(Json::Bool(true)));
|
||||
let so_present = matches!(a.get("so_present"), Some(Json::Bool(true)));
|
||||
Ok(match gap::_rust_core(&pin, loader_importable, so_present) {
|
||||
Some(name) => Json::Str(name.to_string()),
|
||||
None => Json::Null,
|
||||
})
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_py_sorted({"v": value}) -> normalized value
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_py_sorted(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg)
|
||||
.and_then(|a| a.get("v").cloned().ok_or("arg.v required".to_string()))
|
||||
.map(|v| gap::_py_sorted(&v));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_py_declared_surfaces({"manifest": manifest}) -> declared
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_py_declared_surfaces(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let manifest = a.get("manifest").cloned().ok_or("arg.manifest required")?;
|
||||
gap::_py_declared_surfaces(&manifest)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_py_project_meta_surfaces({"doc","inputs","now"}) -> {"count","doc"}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_py_project_meta_surfaces(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let inputs = a.get("inputs").cloned().ok_or("arg.inputs required")?;
|
||||
let now = a
|
||||
.get("now")
|
||||
.map(rune_spine::store::as_str)
|
||||
.map(|x| x.to_string())
|
||||
.ok_or("arg.now required")?;
|
||||
gap::_py_project_meta_surfaces(&doc, &inputs, &now)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_py_ingest_umbrellas_data({"doc","data","now"}) -> {"count","doc"}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_py_ingest_umbrellas_data(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let data = a.get("data").cloned().ok_or("arg.data required")?;
|
||||
let now = a
|
||||
.get("now")
|
||||
.map(rune_spine::store::as_str)
|
||||
.map(|x| x.to_string())
|
||||
.ok_or("arg.now required")?;
|
||||
gap::_py_ingest_umbrellas_data(&doc, &data, &now)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_py_entity({"doc","id"}) -> entity|null
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_py_entity(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let id = a
|
||||
.get("id")
|
||||
.map(rune_spine::store::as_str)
|
||||
.map(|x| x.to_string())
|
||||
.ok_or("arg.id required")?;
|
||||
Ok(gap::_py_entity(&doc, &id))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
fn core_available(a: &Json) -> bool {
|
||||
matches!(a.get("core_available"), Some(Json::Bool(true)))
|
||||
}
|
||||
|
||||
/// sp_py_build_from_inputs({"inputs","core_available"}) -> doc
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_py_build_from_inputs(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let inputs = a.get("inputs").cloned().ok_or("arg.inputs required")?;
|
||||
gap::_py_build_from_inputs(&inputs, core_available(&a))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_py_drift_from_inputs({"doc","inputs","core_available"}) -> drift list
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_py_drift_from_inputs(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let inputs = a.get("inputs").cloned().ok_or("arg.inputs required")?;
|
||||
gap::_py_drift_from_inputs(&doc, &inputs, core_available(&a))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_py_export_umbrellas({"doc","now","core_available"}) -> umbrella view
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_py_export_umbrellas(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let now = a
|
||||
.get("now")
|
||||
.map(rune_spine::store::as_str)
|
||||
.map(|x| x.to_string())
|
||||
.ok_or("arg.now required")?;
|
||||
gap::_py_export_umbrellas(&doc, &now, core_available(&a))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// sp_py_query({"doc","id","core_available"}) -> query result
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_py_query(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let id = a
|
||||
.get("id")
|
||||
.map(rune_spine::store::as_str)
|
||||
.map(|x| x.to_string())
|
||||
.ok_or("arg.id required")?;
|
||||
gap::_py_query(&doc, &id, core_available(&a))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpineEntity {
|
||||
pub id: String,
|
||||
|
||||
Reference in new issue
Block a user