feat(evidence_capsule): port to FULL Rust — dichotomy wave
This commit is contained in:
1 parent
44634a1800
commit
db06ee4664
9 files changed
+946
No files matched your search
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_evidence_capsule_truth.py — freeze the evidence_capsule parity
|
||||
oracle (dichotomy wave #2479 tranche).
|
||||
|
||||
Generator law: RUNE_EC_CORE=python pinned, inputs deep-copied, every
|
||||
public function and branch exercised (pack empty/full/over-full for each
|
||||
slot, defaults for contradictions/limits/raw_ref, exact-boundary fills,
|
||||
truncated-count arithmetic; size incl. missing keys; unpack complete and
|
||||
truncated, missing raw_ref/truncated). Output:
|
||||
src/rust/evidence_capsule/frozen_ec_truth.json — replayed VERBATIM by
|
||||
the crate's cfg(test) battery.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "src" / "rune"))
|
||||
|
||||
os.environ["RUNE_EC_CORE"] = "python"
|
||||
|
||||
import evidence_capsule as ec # noqa: E402
|
||||
|
||||
cases: list = []
|
||||
|
||||
|
||||
def freeze(fn, args):
|
||||
args = copy.deepcopy(args)
|
||||
f = getattr(ec, fn)
|
||||
expected = f(**copy.deepcopy(args))
|
||||
cases.append({"fn": fn, "args": args, "expected": expected})
|
||||
|
||||
|
||||
def items(tag, n):
|
||||
return [f"{tag}{i}" for i in range(n)]
|
||||
|
||||
|
||||
# --- pack: every slot shape ---
|
||||
freeze("pack", {"claims": []}) # all defaults
|
||||
freeze("pack", {"claims": items("c", 3)}) # under-fill
|
||||
freeze("pack", {"claims": items("c", 5)}) # exact claims boundary
|
||||
freeze("pack", {"claims": items("c", 9),
|
||||
"contradictions": ["c1", "c2", "c3", "c4"],
|
||||
"limits": ["l1"], "raw_ref": "run/123"})
|
||||
freeze("pack", {"claims": items("c", 7),
|
||||
"contradictions": items("x", 3), # exact contra boundary
|
||||
"limits": items("l", 5), "raw_ref": "r"}) # limits over-fill
|
||||
freeze("pack", {"claims": items("c", 2), "contradictions": [],
|
||||
"limits": [], "raw_ref": ""}) # explicit empties
|
||||
freeze("pack", {"claims": items("cl", 6), "contradictions": items("k", 4),
|
||||
"limits": items("m", 4), "raw_ref": "run/9"}) # all over-fill
|
||||
|
||||
# --- size: every branch incl. missing keys ---
|
||||
freeze("size", {"capsule": ec.pack(["a", "b"], ["c"], ["d"])})
|
||||
freeze("size", {"capsule": ec.pack([])})
|
||||
freeze("size", {"capsule": {}})
|
||||
freeze("size", {"capsule": {"claims": ["a"]}})
|
||||
freeze("size", {"capsule": ec.pack(items("c", 9), items("x", 9), items("l", 9))})
|
||||
|
||||
# --- unpack: complete, truncated, missing keys ---
|
||||
freeze("unpack", {"capsule": ec.pack(["a"])})
|
||||
freeze("unpack", {"capsule": ec.pack(items("x", 9))})
|
||||
freeze("unpack", {"capsule": {}})
|
||||
freeze("unpack", {"capsule": {"claims": ["a"], "raw_ref": "r/1"}})
|
||||
|
||||
doc = {"module": "evidence_capsule", "core_env": "RUNE_EC_CORE=python",
|
||||
"cases": cases}
|
||||
out = ROOT / "src" / "rust" / "evidence_capsule" / "frozen_ec_truth.json"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(doc, indent=1) + "\n")
|
||||
print(f"frozen {len(cases)} cases -> {out}")
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open + walk the #2479 dichotomy-wave construction contract for
|
||||
evidence_capsule to verifying. Full lifecycle in ONE process."""
|
||||
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-batch-i")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Complete the evidence_capsule dichotomy port to FULL Rust (#2479 "
|
||||
"tranche): the fixed-size evidence-capsule kernel — pack (slot caps "
|
||||
"5/3/3 with explicit truncation counts and None-or-missing -> [] "
|
||||
"defaults) / size (missing keys -> 0) / unpack (raw_ref pointer + "
|
||||
"complete flag; a MISSING truncated map means complete) — as "
|
||||
"doc-envelope FFI (ec_* symbols, free via capability_scoring "
|
||||
"cs_free_string — no free re-export); ec_loader mirroring "
|
||||
"broker_loader.py; facade keeps the Python floor, RUNE_EC_CORE="
|
||||
"python pin; frozen-oracle battery (every branch: empty/full/"
|
||||
"over-full per slot, exact boundary fills, explicit empties, "
|
||||
"missing-key size/unpack); live parity 100% (frozen + fuzz seeds); "
|
||||
"Phase B language-advantage pass (single-pass truncation count).",
|
||||
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)[:150])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
print("CONTRACT", cid, "verifying")
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust evidence_capsule core (dichotomy wave #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* ec_pack({"claims", "contradictions"?, "limits"?, "raw_ref"?})
|
||||
-> capsule dict
|
||||
* ec_size({"capsule"}) -> item count (number)
|
||||
* ec_unpack({"capsule"}) -> {"capsule", "raw_ref", "complete"}
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with EC_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "evidence_capsule"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("EC_RUST_LIB", _CRATE_DIR / "target" / "release" / "libevidence_capsule.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
class EvidenceCapsuleCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust evidence-capsule core is not built. Callers fall back."""
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise EvidenceCapsuleCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("ec_pack", "ec_size", "ec_unpack"):
|
||||
getattr(lib, sym).argtypes = [ctypes.c_void_p]
|
||||
getattr(lib, sym).restype = ctypes.c_void_p
|
||||
try:
|
||||
lib.cs_free_string
|
||||
except AttributeError:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib._free = scoring.cs_free_string
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
free = getattr(lib, "_free", None)
|
||||
if free is not None:
|
||||
free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
def pack(claims: list, contradictions=None, limits=None, raw_ref: str = "") -> dict:
|
||||
return _call("ec_pack", {"claims": claims, "contradictions": contradictions,
|
||||
"limits": limits, "raw_ref": raw_ref})
|
||||
|
||||
def size(capsule: dict) -> int:
|
||||
return _call("ec_size", {"capsule": capsule})
|
||||
|
||||
def unpack(capsule: dict) -> dict:
|
||||
return _call("ec_unpack", {"capsule": capsule})
|
||||
@@ -3,18 +3,43 @@
|
||||
Research findings compress into a fixed-size capsule — the claims,
|
||||
contradictions, and limits — with the full raw evidence one click away
|
||||
(a pointer, not a copy). No extra AI call should be needed to read it.
|
||||
|
||||
Dichotomy wave #2479 tranche: the pure kernel (pack / size / unpack)
|
||||
is ported to FULL Rust (src/rust/evidence_capsule, parity oracle
|
||||
frozen_ec_truth.json) behind ec_loader; the Python implementation below
|
||||
is the strangler fallback, pinned with RUNE_EC_CORE=python.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
MAX_CLAIMS = 5
|
||||
MAX_CONTRADICTIONS = 3
|
||||
MAX_LIMITS = 3
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust evidence-capsule core when available (strangler: the
|
||||
Python floor below is load-bearing). RUNE_EC_CORE=python pins it."""
|
||||
if os.environ.get("RUNE_EC_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import ec_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback
|
||||
return None
|
||||
try:
|
||||
return ec_loader if ec_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe never crashes
|
||||
return None
|
||||
|
||||
|
||||
def pack(claims: list, contradictions: list | None = None,
|
||||
limits: list | None = None, raw_ref: str = "") -> dict:
|
||||
"""Pack findings into a fixed-size capsule (truncation is explicit)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.pack(claims, contradictions, limits, raw_ref)
|
||||
contradictions = contradictions or []
|
||||
limits = limits or []
|
||||
return {
|
||||
@@ -32,6 +57,9 @@ def pack(claims: list, contradictions: list | None = None,
|
||||
|
||||
def size(capsule: dict) -> int:
|
||||
"""The capsule's item count (the fixed-size invariant)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.size(capsule)
|
||||
return (len(capsule.get("claims", []))
|
||||
+ len(capsule.get("contradictions", []))
|
||||
+ len(capsule.get("limits", [])))
|
||||
@@ -39,5 +67,8 @@ def size(capsule: dict) -> int:
|
||||
|
||||
def unpack(capsule: dict) -> dict:
|
||||
"""The full reading: capsule + the raw-evidence pointer."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.unpack(capsule)
|
||||
return {"capsule": capsule, "raw_ref": capsule.get("raw_ref", ""),
|
||||
"complete": not any(capsule.get("truncated", {}).values())}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "capability_scoring"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "evidence_capsule"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"capability_scoring",
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "evidence_capsule"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Fixed-size evidence capsule — dichotomy wave #2479 tranche; pack/size/unpack kernel in Rust; zero-dep (pyjson via capability_scoring)"
|
||||
|
||||
[lib]
|
||||
name = "evidence_capsule"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
capability_scoring = { path = "../capability_scoring" }
|
||||
@@ -0,0 +1,455 @@
|
||||
{
|
||||
"module": "evidence_capsule",
|
||||
"core_env": "RUNE_EC_CORE=python",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "pack",
|
||||
"args": {
|
||||
"claims": []
|
||||
},
|
||||
"expected": {
|
||||
"claims": [],
|
||||
"contradictions": [],
|
||||
"limits": [],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 0,
|
||||
"contradictions": 0,
|
||||
"limits": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "pack",
|
||||
"args": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1",
|
||||
"c2"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1",
|
||||
"c2"
|
||||
],
|
||||
"contradictions": [],
|
||||
"limits": [],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 0,
|
||||
"contradictions": 0,
|
||||
"limits": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "pack",
|
||||
"args": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1",
|
||||
"c2",
|
||||
"c3",
|
||||
"c4"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1",
|
||||
"c2",
|
||||
"c3",
|
||||
"c4"
|
||||
],
|
||||
"contradictions": [],
|
||||
"limits": [],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 0,
|
||||
"contradictions": 0,
|
||||
"limits": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "pack",
|
||||
"args": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1",
|
||||
"c2",
|
||||
"c3",
|
||||
"c4",
|
||||
"c5",
|
||||
"c6",
|
||||
"c7",
|
||||
"c8"
|
||||
],
|
||||
"contradictions": [
|
||||
"c1",
|
||||
"c2",
|
||||
"c3",
|
||||
"c4"
|
||||
],
|
||||
"limits": [
|
||||
"l1"
|
||||
],
|
||||
"raw_ref": "run/123"
|
||||
},
|
||||
"expected": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1",
|
||||
"c2",
|
||||
"c3",
|
||||
"c4"
|
||||
],
|
||||
"contradictions": [
|
||||
"c1",
|
||||
"c2",
|
||||
"c3"
|
||||
],
|
||||
"limits": [
|
||||
"l1"
|
||||
],
|
||||
"raw_ref": "run/123",
|
||||
"truncated": {
|
||||
"claims": 4,
|
||||
"contradictions": 1,
|
||||
"limits": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "pack",
|
||||
"args": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1",
|
||||
"c2",
|
||||
"c3",
|
||||
"c4",
|
||||
"c5",
|
||||
"c6"
|
||||
],
|
||||
"contradictions": [
|
||||
"x0",
|
||||
"x1",
|
||||
"x2"
|
||||
],
|
||||
"limits": [
|
||||
"l0",
|
||||
"l1",
|
||||
"l2",
|
||||
"l3",
|
||||
"l4"
|
||||
],
|
||||
"raw_ref": "r"
|
||||
},
|
||||
"expected": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1",
|
||||
"c2",
|
||||
"c3",
|
||||
"c4"
|
||||
],
|
||||
"contradictions": [
|
||||
"x0",
|
||||
"x1",
|
||||
"x2"
|
||||
],
|
||||
"limits": [
|
||||
"l0",
|
||||
"l1",
|
||||
"l2"
|
||||
],
|
||||
"raw_ref": "r",
|
||||
"truncated": {
|
||||
"claims": 2,
|
||||
"contradictions": 0,
|
||||
"limits": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "pack",
|
||||
"args": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1"
|
||||
],
|
||||
"contradictions": [],
|
||||
"limits": [],
|
||||
"raw_ref": ""
|
||||
},
|
||||
"expected": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1"
|
||||
],
|
||||
"contradictions": [],
|
||||
"limits": [],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 0,
|
||||
"contradictions": 0,
|
||||
"limits": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "pack",
|
||||
"args": {
|
||||
"claims": [
|
||||
"cl0",
|
||||
"cl1",
|
||||
"cl2",
|
||||
"cl3",
|
||||
"cl4",
|
||||
"cl5"
|
||||
],
|
||||
"contradictions": [
|
||||
"k0",
|
||||
"k1",
|
||||
"k2",
|
||||
"k3"
|
||||
],
|
||||
"limits": [
|
||||
"m0",
|
||||
"m1",
|
||||
"m2",
|
||||
"m3"
|
||||
],
|
||||
"raw_ref": "run/9"
|
||||
},
|
||||
"expected": {
|
||||
"claims": [
|
||||
"cl0",
|
||||
"cl1",
|
||||
"cl2",
|
||||
"cl3",
|
||||
"cl4"
|
||||
],
|
||||
"contradictions": [
|
||||
"k0",
|
||||
"k1",
|
||||
"k2"
|
||||
],
|
||||
"limits": [
|
||||
"m0",
|
||||
"m1",
|
||||
"m2"
|
||||
],
|
||||
"raw_ref": "run/9",
|
||||
"truncated": {
|
||||
"claims": 1,
|
||||
"contradictions": 1,
|
||||
"limits": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "size",
|
||||
"args": {
|
||||
"capsule": {
|
||||
"claims": [
|
||||
"a",
|
||||
"b"
|
||||
],
|
||||
"contradictions": [
|
||||
"c"
|
||||
],
|
||||
"limits": [
|
||||
"d"
|
||||
],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 0,
|
||||
"contradictions": 0,
|
||||
"limits": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"expected": 4
|
||||
},
|
||||
{
|
||||
"fn": "size",
|
||||
"args": {
|
||||
"capsule": {
|
||||
"claims": [],
|
||||
"contradictions": [],
|
||||
"limits": [],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 0,
|
||||
"contradictions": 0,
|
||||
"limits": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"expected": 0
|
||||
},
|
||||
{
|
||||
"fn": "size",
|
||||
"args": {
|
||||
"capsule": {}
|
||||
},
|
||||
"expected": 0
|
||||
},
|
||||
{
|
||||
"fn": "size",
|
||||
"args": {
|
||||
"capsule": {
|
||||
"claims": [
|
||||
"a"
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected": 1
|
||||
},
|
||||
{
|
||||
"fn": "size",
|
||||
"args": {
|
||||
"capsule": {
|
||||
"claims": [
|
||||
"c0",
|
||||
"c1",
|
||||
"c2",
|
||||
"c3",
|
||||
"c4"
|
||||
],
|
||||
"contradictions": [
|
||||
"x0",
|
||||
"x1",
|
||||
"x2"
|
||||
],
|
||||
"limits": [
|
||||
"l0",
|
||||
"l1",
|
||||
"l2"
|
||||
],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 4,
|
||||
"contradictions": 6,
|
||||
"limits": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
"expected": 11
|
||||
},
|
||||
{
|
||||
"fn": "unpack",
|
||||
"args": {
|
||||
"capsule": {
|
||||
"claims": [
|
||||
"a"
|
||||
],
|
||||
"contradictions": [],
|
||||
"limits": [],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 0,
|
||||
"contradictions": 0,
|
||||
"limits": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"capsule": {
|
||||
"claims": [
|
||||
"a"
|
||||
],
|
||||
"contradictions": [],
|
||||
"limits": [],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 0,
|
||||
"contradictions": 0,
|
||||
"limits": 0
|
||||
}
|
||||
},
|
||||
"raw_ref": "",
|
||||
"complete": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "unpack",
|
||||
"args": {
|
||||
"capsule": {
|
||||
"claims": [
|
||||
"x0",
|
||||
"x1",
|
||||
"x2",
|
||||
"x3",
|
||||
"x4"
|
||||
],
|
||||
"contradictions": [],
|
||||
"limits": [],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 4,
|
||||
"contradictions": 0,
|
||||
"limits": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"capsule": {
|
||||
"claims": [
|
||||
"x0",
|
||||
"x1",
|
||||
"x2",
|
||||
"x3",
|
||||
"x4"
|
||||
],
|
||||
"contradictions": [],
|
||||
"limits": [],
|
||||
"raw_ref": "",
|
||||
"truncated": {
|
||||
"claims": 4,
|
||||
"contradictions": 0,
|
||||
"limits": 0
|
||||
}
|
||||
},
|
||||
"raw_ref": "",
|
||||
"complete": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "unpack",
|
||||
"args": {
|
||||
"capsule": {}
|
||||
},
|
||||
"expected": {
|
||||
"capsule": {},
|
||||
"raw_ref": "",
|
||||
"complete": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "unpack",
|
||||
"args": {
|
||||
"capsule": {
|
||||
"claims": [
|
||||
"a"
|
||||
],
|
||||
"raw_ref": "r/1"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"capsule": {
|
||||
"claims": [
|
||||
"a"
|
||||
],
|
||||
"raw_ref": "r/1"
|
||||
},
|
||||
"raw_ref": "r/1",
|
||||
"complete": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//! branch_battery — replays frozen_ec_truth.json VERBATIM.
|
||||
|
||||
use super::{pyjson, pack, size, unpack, Json};
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_ec_truth.json");
|
||||
|
||||
fn list_arg(args: &Json, key: &str) -> Vec<Json> {
|
||||
match args.get(key) {
|
||||
Some(Json::Arr(xs)) => xs.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn str_arg(args: &Json, key: &str) -> String {
|
||||
match args.get(key) {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_truth_replays_verbatim() {
|
||||
let doc = pyjson::parse(TRUTH).expect("frozen_ec_truth.json parses");
|
||||
let cases = match doc.get("cases") {
|
||||
Some(Json::Arr(a)) => a,
|
||||
_ => panic!("truth has no cases array"),
|
||||
};
|
||||
assert!(!cases.is_empty(), "truth is empty");
|
||||
for (i, case) in cases.iter().enumerate() {
|
||||
let fname = match case.get("fn") {
|
||||
Some(Json::Str(s)) => s.as_str(),
|
||||
_ => panic!("case {i}: missing fn"),
|
||||
};
|
||||
let args = case.get("args").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
let expected = case.get("expected").cloned().unwrap_or(Json::Null);
|
||||
let got: Json = match fname {
|
||||
"pack" => pack(
|
||||
&list_arg(&args, "claims"),
|
||||
&list_arg(&args, "contradictions"),
|
||||
&list_arg(&args, "limits"),
|
||||
&str_arg(&args, "raw_ref"),
|
||||
),
|
||||
"size" => {
|
||||
let capsule = args.get("capsule").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
Json::Num(size(&capsule) as f64)
|
||||
}
|
||||
"unpack" => {
|
||||
let capsule = args.get("capsule").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
unpack(&capsule)
|
||||
}
|
||||
other => panic!("case {i}: unknown fn {other}"),
|
||||
};
|
||||
let (gs, es) = (pyjson::dumps(&got), pyjson::dumps(&expected));
|
||||
assert_eq!(
|
||||
gs, es,
|
||||
"case {i} ({fname}) diverged:\n got: {gs}\n want: {es}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truth_covers_every_public_fn() {
|
||||
let doc = pyjson::parse(TRUTH).expect("truth parses");
|
||||
let cases = match doc.get("cases") {
|
||||
Some(Json::Arr(a)) => a,
|
||||
_ => panic!("truth has no cases array"),
|
||||
};
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for c in cases {
|
||||
if let Some(Json::Str(s)) = c.get("fn") {
|
||||
seen.insert(s.clone());
|
||||
}
|
||||
}
|
||||
for f in ["pack", "size", "unpack"] {
|
||||
assert!(seen.contains(f), "truth missing coverage for {f}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//! evidence_capsule — fixed-size evidence capsule (dichotomy wave
|
||||
//! #2479 tranche). RUST verdict per classification: the pure kernel
|
||||
//! (pack / size / unpack) lives here.
|
||||
//!
|
||||
//! Oracle: src/rune/evidence_capsule.py (the facade keeps the Python
|
||||
//! floor, RUNE_EC_CORE=python pin).
|
||||
//!
|
||||
//! Semantics mirrored exactly:
|
||||
//! * pack(claims, contradictions=None, limits=None, raw_ref=""):
|
||||
//! None-or-missing -> []; claims[:5], contradictions[:3],
|
||||
//! limits[:3]; truncated counts = max(0, len - cap) per slot.
|
||||
//! Result key order: {claims, contradictions, limits, raw_ref,
|
||||
//! truncated{claims, contradictions, limits}}.
|
||||
//! * size(capsule): len(claims) + len(contradictions) + len(limits),
|
||||
//! missing keys -> 0.
|
||||
//! * unpack(capsule): {capsule, raw_ref (missing -> ""),
|
||||
//! complete: not any(truncated.values())} — a MISSING truncated
|
||||
//! map means complete (any([]) is False).
|
||||
//!
|
||||
//! FFI contract (doc-envelope law — every call returns {"ok","value"|"err"}):
|
||||
//! * ec_pack({"claims", "contradictions"?, "limits"?, "raw_ref"?})
|
||||
//! -> capsule dict
|
||||
//! * ec_size({"capsule"}) -> item count (number)
|
||||
//! * ec_unpack({"capsule"}) -> {"capsule", "raw_ref", "complete"}
|
||||
//!
|
||||
//! Parity oracle: frozen_ec_truth.json (frozen from the live Python
|
||||
//! oracle with RUNE_EC_CORE=python pinned, inputs deep-copied).
|
||||
|
||||
pub use capability_scoring::pyjson;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "branch_battery.rs"]
|
||||
mod branch_battery;
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use pyjson::Json;
|
||||
|
||||
pub const MAX_CLAIMS: usize = 5;
|
||||
pub const MAX_CONTRADICTIONS: usize = 3;
|
||||
pub const MAX_LIMITS: usize = 3;
|
||||
|
||||
fn as_list(v: Option<&Json>) -> Vec<Json> {
|
||||
match v {
|
||||
Some(Json::Arr(xs)) => xs.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_str(v: &Json, key: &str) -> String {
|
||||
match v.get(key) {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn trunc_map(c: i64, x: i64, l: i64) -> Json {
|
||||
Json::Obj(vec![
|
||||
("claims".into(), Json::Num(c as f64)),
|
||||
("contradictions".into(), Json::Num(x as f64)),
|
||||
("limits".into(), Json::Num(l as f64)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn pack(claims: &[Json], contradictions: &[Json], limits: &[Json], raw_ref: &str) -> Json {
|
||||
let keep = |xs: &[Json], cap: usize| -> Vec<Json> { xs.iter().take(cap).cloned().collect() };
|
||||
let tc = 0.max(claims.len() as i64 - MAX_CLAIMS as i64);
|
||||
let tx = 0.max(contradictions.len() as i64 - MAX_CONTRADICTIONS as i64);
|
||||
let tl = 0.max(limits.len() as i64 - MAX_LIMITS as i64);
|
||||
Json::Obj(vec![
|
||||
("claims".into(), Json::Arr(keep(claims, MAX_CLAIMS))),
|
||||
(
|
||||
"contradictions".into(),
|
||||
Json::Arr(keep(contradictions, MAX_CONTRADICTIONS)),
|
||||
),
|
||||
("limits".into(), Json::Arr(keep(limits, MAX_LIMITS))),
|
||||
("raw_ref".into(), Json::Str(raw_ref.into())),
|
||||
("truncated".into(), trunc_map(tc, tx, tl)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn size(capsule: &Json) -> i64 {
|
||||
as_list(capsule.get("claims")).len() as i64
|
||||
+ as_list(capsule.get("contradictions")).len() as i64
|
||||
+ as_list(capsule.get("limits")).len() as i64
|
||||
}
|
||||
|
||||
pub fn unpack(capsule: &Json) -> Json {
|
||||
let raw_ref = get_str(capsule, "raw_ref");
|
||||
let complete = match capsule.get("truncated") {
|
||||
Some(Json::Obj(pairs)) => !pairs.iter().any(|(_, v)| match v {
|
||||
Json::Num(n) => *n != 0.0,
|
||||
Json::Bool(b) => *b,
|
||||
Json::Str(s) => !s.is_empty(),
|
||||
Json::Null => false,
|
||||
Json::Arr(a) => !a.is_empty(),
|
||||
Json::Obj(o) => !o.is_empty(),
|
||||
}),
|
||||
_ => true,
|
||||
};
|
||||
Json::Obj(vec![
|
||||
("capsule".into(), capsule.clone()),
|
||||
("raw_ref".into(), Json::Str(raw_ref)),
|
||||
("complete".into(), Json::Bool(complete)),
|
||||
])
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
]),
|
||||
};
|
||||
CString::new(pyjson::dumps(&v))
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn read_arg(arg: *const c_char) -> Result<Json, String> {
|
||||
if arg.is_null() {
|
||||
return Err("null arg".into());
|
||||
}
|
||||
let raw = unsafe { CStr::from_ptr(arg) }.to_bytes();
|
||||
pyjson::parse(std::str::from_utf8(raw).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ec_pack(arg: *const c_char) -> *mut c_char {
|
||||
let out = read_arg(arg).map(|a| {
|
||||
let claims = as_list(a.get("claims"));
|
||||
let contra = as_list(a.get("contradictions"));
|
||||
let limits = as_list(a.get("limits"));
|
||||
let raw_ref = get_str(&a, "raw_ref");
|
||||
pack(&claims, &contra, &limits, &raw_ref)
|
||||
});
|
||||
envelope(out)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ec_size(arg: *const c_char) -> *mut c_char {
|
||||
let out = read_arg(arg).map(|a| {
|
||||
let capsule = a.get("capsule").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
Json::Num(size(&capsule) as f64)
|
||||
});
|
||||
envelope(out)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ec_unpack(arg: *const c_char) -> *mut c_char {
|
||||
let out = read_arg(arg).map(|a| {
|
||||
let capsule = a.get("capsule").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
unpack(&capsule)
|
||||
});
|
||||
envelope(out)
|
||||
}
|
||||
|
||||
// Free is owned by capability_scoring's cs_free_string (shared process)
|
||||
// — no free re-export here, mirroring the broker_loader contract.
|
||||
Reference in new issue
Block a user