feat(evidence_capsule): port to FULL Rust — dichotomy wave

This commit is contained in:
Nick committed 2026-10-13 13:31:57 +01:00
1 parent 44634a1800
commit db06ee4664
9 files changed
+946

No files matched your search

+74
View File
@@ -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}")
+39
View File
@@ -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")
+80
View File
@@ -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})
+31
View File
@@ -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())}
+14
View File
@@ -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",
]
+12
View File
@@ -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}");
}
}
+164
View File
@@ -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.