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

This commit is contained in:
Nick committed 2026-10-12 12:45:03 +01:00
1 parent 7c83200c2e
commit 44b25064c9
10 files changed
+6766 -94

No files matched your search

+297
View File
@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""Live parity: the FULL Rust behaviour_ledger core (bl_loader) vs the
retired Python oracle (the ep_loader shim path — RUNE_BL_CORE=python),
dichotomy wave #2479. Replays the frozen truth corpus PLUS seeded fuzz
over every public function and branch. The clock is pinned (no-clock
law — the kernel takes `now` in), so nothing is masked.
The facade reads RUNE_BL_CORE per call, so one process can flip between
the pinned oracle and the wave core.
"""
from __future__ import annotations
import copy
import json
import os
import random
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src" / "rune"))
from rune import behaviour_ledger as bl # noqa: E402
TRUTH_PATH = Path(__file__).resolve().parents[1] / (
"src/rust/behaviour_ledger/src/frozen_bl_truth.json")
FROZEN = "2026-10-11T23:00:00Z"
CLOCK = {"n": 0}
bl._now = lambda: f"2026-10-11T23:{CLOCK['n'] // 60:02d}:{CLOCK['n'] % 60:02d}Z" # noqa: E731
def call_core(core, fn, *args, **kwargs):
old = os.environ.get("RUNE_BL_CORE")
os.environ["RUNE_BL_CORE"] = core
try:
return getattr(bl, fn)(*args, **kwargs)
finally:
if old is None:
os.environ.pop("RUNE_BL_CORE", None)
else:
os.environ["RUNE_BL_CORE"] = old
def fresh_ledger(doc=None):
led = bl.BehaviourLedger(Path(tempfile.mkdtemp(prefix="bl_par_")) / "s")
if doc is not None:
led.doc = copy.deepcopy(doc)
return led
def compare(label, ok, failures, detail):
if not ok:
failures.append((label, detail))
print(f"FAIL {label}\n{detail}")
return ok
def scenario(group, name, doc_in, op, failures):
"""Run one op under both cores on identical deep-copied pre-state;
compare (return | ValueError message, doc after)."""
py_ret = py_err = py_doc = None
try:
CLOCK["n"] = 0
led = fresh_ledger(doc_in)
py_ret = op(led)
py_doc = led.doc
except ValueError as e:
py_err = str(e)
rs_ret = rs_err = rs_doc = None
try:
CLOCK["n"] = 0
led = fresh_ledger(doc_in)
rs_ret = op(led)
rs_doc = led.doc
except ValueError as e:
rs_err = str(e)
ok = (json.dumps(py_ret, sort_keys=True, default=str)
== json.dumps(rs_ret, sort_keys=True, default=str)
and py_err == rs_err
and json.dumps(py_doc, sort_keys=True)
== json.dumps(rs_doc, sort_keys=True))
return compare(f"{group}:{name}", ok, failures,
f"py={json.dumps(py_ret, sort_keys=True, default=str)[:300]}"
f"/{py_err}\nrs={json.dumps(rs_ret, sort_keys=True, default=str)[:300]}"
f"/{rs_err}")
# ── fuzz pools ─────────────────────────────────────────────────────────
IDS = ["B-1", "B-2", "behaviour:B-2", "B-3", "GHOST", "behaviour:GHOST",
"UNI-é", "X" * 60, "b", "behaviour:b"]
PROJECTS = ["demo", "other", "démø", ""]
COMPONENTS = ["component:a", "component:b", "component:bridge",
"component:ünï", ""]
ENTS = ["component:a", "component:b", "component:bridge",
"interface:completion", "component:ünï", "interface:x", "domain:d"]
DETAILS = ["ok", "", "x" * 250, "café ✓ ünïcode", "multi\nline", "quote'"]
GENS = ["gen-1", "gen-2", "", "gén-3"]
CONFIRMERS = ["user", "ci-robot", "", "user ", "nick"]
NOTES = ["accepted for v1", "", "n" * 220, "»signed«"]
def rand_params(rng):
return {
"behaviour_id": rng.choice(IDS),
"owning_project": rng.choice(PROJECTS),
"intended_behaviour": rng.choice(DETAILS),
"origin_intent": rng.choice(DETAILS),
"pass_criteria": rng.choice(DETAILS),
"fail_criteria": rng.choice(DETAILS),
"procedure": rng.choice(DETAILS),
"owner_component": rng.choice(COMPONENTS),
"dependencies": rng.sample(ENTS, rng.randint(0, 3)),
"automated_ref": rng.choice(["tests/x.py", ""]),
"environment": rng.choice(["ci", "prod", ""]),
}
def rand_doc(rng):
"""Deterministic pre-state via the pinned-clock oracle (python pin)."""
old = os.environ.get("RUNE_BL_CORE")
os.environ["RUNE_BL_CORE"] = "python"
try:
led = fresh_ledger()
for i in range(rng.randint(0, 4)):
bid = f"B-{i}"
p = rand_params(rng)
p["behaviour_id"] = bid
try:
led.define(**p)
except ValueError:
pass
roll = rng.random()
if roll < 0.45: # full confirmation chain
led.record_auto_verification(
bid, passed=rng.random() < 0.85,
detail=rng.choice(DETAILS), generation=rng.choice(GENS))
if rng.random() < 0.8:
led.record_real_world(
bid, passed=rng.random() < 0.85,
detail=rng.choice(DETAILS),
generation=rng.choice(GENS),
confirmed_by=rng.choice(CONFIRMERS))
elif roll < 0.6:
led.record_auto_verification(
bid, passed=False, detail=rng.choice(DETAILS),
generation=rng.choice(GENS))
elif roll < 0.7:
led.accept_limitation(bid, note=rng.choice(NOTES))
return led.doc
finally:
if old is None:
os.environ.pop("RUNE_BL_CORE", None)
else:
os.environ["RUNE_BL_CORE"] = old
def main() -> int:
failures: list[tuple[str, str]] = []
n_truth = 0
# 1. frozen truth corpus (kernel-level replay lives in the crate's
# branch battery; replayed here at FACADE level, both cores)
truth = json.loads(TRUTH_PATH.read_text())["cases"]
def run_case(group, case):
i = case["input"]
if group == "define":
params = {k: v for k, v in i.items()
if k not in ("doc", "now")}
params["dependencies"] = params.get("dependencies")
op = lambda led: led.define(**params) # noqa: E731
elif group == "get":
op = lambda led: led.get(i["behaviour_id"]) # noqa: E731
elif group == "record_auto_verification":
params = {k: v for k, v in i.items()
if k not in ("doc", "now")}
op = lambda led: led.record_auto_verification( # noqa: E731
params["behaviour_id"], **{k: v for k, v in params.items()
if k != "behaviour_id"})
elif group == "record_real_world":
params = {k: v for k, v in i.items()
if k not in ("doc", "now")}
op = lambda led: led.record_real_world( # noqa: E731
params["behaviour_id"], **{k: v for k, v in params.items()
if k != "behaviour_id"})
elif group == "accept_limitation":
params = {k: v for k, v in i.items()
if k not in ("doc", "now")}
op = lambda led: led.accept_limitation( # noqa: E731
params["behaviour_id"], **{k: v for k, v in params.items()
if k != "behaviour_id"})
elif group == "invalidate_for_change":
params = {k: v for k, v in i.items()
if k not in ("doc", "now")}
params["changed_entities"] = set(params["changed_entities"])
op = lambda led: led.invalidate_for_change(**params) # noqa: E731
elif group == "retest_history":
op = lambda led: led.retest_history(i["behaviour_id"]) # noqa: E731
elif group == "outstanding":
params = {k: v for k, v in i.items() if k != "doc"}
op = lambda led: led.outstanding(**params) # noqa: E731
else: # summary
op = lambda led: led.summary() # noqa: E731
scenario(group, case["name"], i["doc"], op, failures)
for group, cases in truth.items():
for case in cases:
n_truth += 1
run_case(group, case)
# 2. seeded fuzz (seed 2479). ALL randomness is consumed by the
# opmaker ONCE — the closure it returns is deterministic, so both
# core runs see identical args (poisoned-truth law).
rng = random.Random(2479)
n_fuzz = 0
def pick_bid(r, doc):
"""Existing (raw key) or random id — consumed once."""
bids = list(doc.get("behaviours", {}))
if bids and r.random() < 0.7:
return r.choice(bids)
return r.choice(IDS)
def fuzz(group, opmaker, count):
nonlocal n_fuzz
for j in range(count):
n_fuzz += 1
doc = rand_doc(rng)
scenario(group, f"fuzz{j}", doc, opmaker(rng, doc), failures)
fuzz("define", lambda r, d: (lambda led, p=rand_params(r): led.define(**p)),
150)
fuzz("get", lambda r, d: (lambda led, b=r.choice(IDS): led.get(b)), 100)
def auto_op(r, d):
bid = pick_bid(r, d)
passed = r.random() < 0.6
detail = r.choice(DETAILS)
gen = r.choice(GENS)
return lambda led: led.record_auto_verification(
bid, passed=passed, detail=detail, generation=gen)
fuzz("record_auto_verification", auto_op, 150)
def rw_op(r, d):
bid = pick_bid(r, d)
passed = r.random() < 0.6
detail = r.choice(DETAILS)
gen = r.choice(GENS)
by = r.choice(CONFIRMERS)
return lambda led: led.record_real_world(
bid, passed=passed, detail=detail, generation=gen, confirmed_by=by)
fuzz("record_real_world", rw_op, 150)
def lim_op(r, d):
bid = pick_bid(r, d)
note = r.choice(NOTES)
by = r.choice(CONFIRMERS)
return lambda led: led.accept_limitation(bid, note=note, approved_by=by)
fuzz("accept_limitation", lim_op, 100)
def inv_op(r, d):
changed = set(r.sample(ENTS, r.randint(0, 3)))
reasons = ({"behaviour:B-0": "custom reason"} if r.random() < 0.3
else None)
gen = r.choice(GENS)
return lambda led: led.invalidate_for_change(
changed_entities=changed, generation=gen, reasons=reasons)
fuzz("invalidate_for_change", inv_op, 200)
fuzz("retest_history",
lambda r, d: (lambda led, b=r.choice(IDS): led.retest_history(b)),
80)
def out_op(r, d):
kw = {}
if r.random() < 0.5:
kw["component"] = r.choice(COMPONENTS)
if r.random() < 0.5:
kw["project"] = r.choice(PROJECTS)
return lambda led: led.outstanding(**kw)
fuzz("outstanding", out_op, 120)
fuzz("summary", lambda r, d: (lambda led: led.summary()), 50)
total = n_truth + n_fuzz
print(f"parity: {total - len(failures)}/{total} "
f"({n_truth} truth + {n_fuzz} fuzz seed 2479)")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())
+444
View File
@@ -0,0 +1,444 @@
#!/usr/bin/env python3
"""Freeze the parity truth for the behaviour_ledger Rust port (dichotomy
wave #2479 tranche).
Runs the LIVE oracle (src/rune/behaviour_ledger.py — the Phase G shim
serving the evidence_provenance `bl_*` core, retired as the Python
oracle by this port) across EVERY public function and EVERY branch:
define (minimal/full/prefixed id/duplicate/unicode), get (hit both id
forms/miss/empty), record_auto_verification (pass/fail/unknown/200-char
detail truncation/reverify-confirmed), record_real_world (user
confirmation/other-confirmer/fail/unknown), accept_limitation (ok/
custom approver/unknown), invalidate_for_change (dependency overlap
with the default sorted-list-repr reason / owner hit / no overlap →
preserved / custom reasons / reasons key miss / auto-verified-only
skip / mixed multi-behaviour), retest_history (populated generations/
unknown/empty), outstanding (mixed states with the TEST_FAILED-first
sort / component filter / project filter / combined / excluded
states), summary (empty/mixed integer counts).
The clock is pinned (bl._now) so truth is deterministic and
regenerable byte-for-byte. Case input docs are deep-copied BEFORE the
oracle runs (poisoned-truth law). Output:
src/rust/behaviour_ledger/src/frozen_bl_truth.json — embedded VERBATIM
by the cfg(test) branch battery (include_str!).
"""
from __future__ import annotations
import copy
import json
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from rune import behaviour_ledger as bl # noqa: E402
TRUTH_PATH = Path(__file__).resolve().parents[1] / (
"src/rust/behaviour_ledger/src/frozen_bl_truth.json")
class Clock:
"""Pinned clock: %Y-%m-%dT%H:%M:%SZ stamps incrementing by second
(the oracle's _now format), reset per case."""
def __init__(self):
self.n = 0
def now(self) -> str:
self.n += 1
return f"2026-10-11T23:00:{self.n:02d}Z"
CLOCK = Clock()
bl._now = CLOCK.now # pin the facade's clock (the kernel takes `now` in)
def fresh() -> bl.BehaviourLedger:
CLOCK.n = 0
tmp = tempfile.mkdtemp(prefix="bl_truth_")
return bl.BehaviourLedger(Path(tmp) / "store")
def envelope_ok(value) -> dict:
return {"ok": True, "value": value}
def envelope_err(msg: str) -> dict:
return {"ok": False, "err": msg}
CASES: dict[str, list] = {}
def add(group: str, case: dict) -> None:
CASES.setdefault(group, []).append(case)
def snap(led: bl.BehaviourLedger) -> dict:
return copy.deepcopy(led.doc)
def next_now() -> str:
"""The stamp the NEXT pinned _now() call will return."""
return f"2026-10-11T23:00:{CLOCK.n + 1:02d}Z"
def run_mutation(group: str, name: str, led: bl.BehaviourLedger,
params: dict, op) -> None:
"""Run one mutating op (kwargs only); record {input: {doc, now,
**params}, expected: {ok, value: {doc, rec | recs}}} or the
ValueError envelope. The op under test stamps via the pinned clock;
its stamp is recorded in the input (no-clock law — the kernel takes
`now` in)."""
doc_in = snap(led)
stamp = next_now()
try:
out = op(**params)
except ValueError as e:
add(group, {"name": name,
"input": {"doc": doc_in, "now": stamp, **params},
"expected": envelope_err(str(e))})
return
value_key = "recs" if isinstance(out, list) else "rec"
add(group, {"name": name, "input": {"doc": doc_in, "now": stamp,
**params},
"expected": envelope_ok({"doc": led.doc,
value_key: out})})
def seed_confirmed(led: bl.BehaviourLedger, bid: str, owner: str,
deps: list[str]) -> None:
led.define(behaviour_id=bid, owning_project="demo",
intended_behaviour=f"{bid} behaves",
owner_component=owner, dependencies=deps)
led.record_auto_verification(bid, passed=True, generation="gen-1")
led.record_real_world(bid, passed=True, detail="user confirmed",
generation="gen-1")
# ── define ────────────────────────────────────────────────────────────────
led = fresh()
run_mutation("define", "minimal", led, {
"behaviour_id": "MIN-1", "owning_project": "demo",
"intended_behaviour": "does the thing"},
led.define)
led = fresh()
run_mutation("define", "full_kwargs", led, {
"behaviour_id": "FULL-1", "owning_project": "demo",
"intended_behaviour": "bridge completes events",
"origin_intent": "issue #19F", "pass_criteria": "events done",
"fail_criteria": "events dropped", "procedure": "run the suite",
"owner_component": "component:event_bridge",
"dependencies": ["component:event_bridge",
"interface:event_completion"],
"automated_ref": "tests/test_bridge.py", "environment": "ci"},
led.define)
led = fresh()
run_mutation("define", "prefixed_id_preserved", led, {
"behaviour_id": "behaviour:PRE-1", "owning_project": "demo",
"intended_behaviour": "already prefixed"},
led.define)
led = fresh()
run_mutation("define", "unicode_fields", led, {
"behaviour_id": "UNI-1", "owning_project": "démø",
"intended_behaviour": "completes événements — ✓ ünïcode",
"origin_intent": "»quoted«", "pass_criteria": "✓ pass",
"owner_component": "component:kafé",
"dependencies": ["component:kafé", "interface:ünï"],
"environment": "prod-ü"},
led.define)
led = fresh()
led.define(behaviour_id="DUP-1", owning_project="demo",
intended_behaviour="first")
run_mutation("define", "duplicate_err", led, {
"behaviour_id": "DUP-1", "owning_project": "demo",
"intended_behaviour": "second"},
led.define)
led = fresh()
run_mutation("define", "none_dependencies", led, {
"behaviour_id": "NODEP-1", "owning_project": "demo",
"intended_behaviour": "no deps given", "dependencies": None},
led.define)
# ── get ───────────────────────────────────────────────────────────────────
led = fresh()
led.define(behaviour_id="GET-1", owning_project="demo",
intended_behaviour="gettable")
rec = led.get("GET-1")
add("get", {"name": "hit_bare_id",
"input": {"doc": snap(led), "behaviour_id": "GET-1"},
"expected": envelope_ok(rec)})
add("get", {"name": "hit_prefixed_id",
"input": {"doc": snap(led), "behaviour_id": "behaviour:GET-1"},
"expected": envelope_ok(rec)})
add("get", {"name": "miss",
"input": {"doc": snap(led), "behaviour_id": "behaviour:NOPE"},
"expected": envelope_ok(None)})
add("get", {"name": "empty_doc",
"input": {"doc": snap(fresh()), "behaviour_id": "GET-1"},
"expected": envelope_ok(None)})
# ── record_auto_verification ──────────────────────────────────────────────
led = fresh()
led.define(behaviour_id="AUTO-1", owning_project="demo",
intended_behaviour="auto verifiable")
run_mutation("record_auto_verification", "pass_to_auto_verified", led, {
"behaviour_id": "AUTO-1", "passed": True, "detail": "suite green",
"generation": "gen-1"},
led.record_auto_verification)
led = fresh()
led.define(behaviour_id="AUTO-2", owning_project="demo",
intended_behaviour="fails")
run_mutation("record_auto_verification", "fail_to_test_failed", led, {
"behaviour_id": "AUTO-2", "passed": False, "detail": "assert crashed",
"generation": "gen-1"},
led.record_auto_verification)
led = fresh()
run_mutation("record_auto_verification", "unknown_behaviour_err", led, {
"behaviour_id": "GHOST", "passed": True},
led.record_auto_verification)
led = fresh()
led.define(behaviour_id="AUTO-3", owning_project="demo",
intended_behaviour="long detail")
run_mutation("record_auto_verification", "detail_truncated_200", led, {
"behaviour_id": "AUTO-3", "passed": True, "detail": "x" * 250},
led.record_auto_verification)
led = fresh()
seed_confirmed(led, "AUTO-4", "component:c", ["component:c"])
run_mutation("record_auto_verification", "reverify_confirmed", led, {
"behaviour_id": "AUTO-4", "passed": True,
"detail": "regression suite", "generation": "gen-2"},
led.record_auto_verification)
# ── record_real_world ─────────────────────────────────────────────────────
led = fresh()
led.define(behaviour_id="RW-1", owning_project="demo",
intended_behaviour="user confirmed")
run_mutation("record_real_world", "user_pass_confirmed", led, {
"behaviour_id": "RW-1", "passed": True, "detail": "manual test ok",
"generation": "gen-1", "confirmed_by": "user"},
led.record_real_world)
led = fresh()
led.define(behaviour_id="RW-2", owning_project="demo",
intended_behaviour="robot verified")
run_mutation("record_real_world", "other_confirmer_real_world_passed", led, {
"behaviour_id": "RW-2", "passed": True, "detail": "bot checked",
"generation": "gen-1", "confirmed_by": "ci-robot"},
led.record_real_world)
led = fresh()
led.define(behaviour_id="RW-3", owning_project="demo",
intended_behaviour="fails in the field")
run_mutation("record_real_world", "fail_to_test_failed", led, {
"behaviour_id": "RW-3", "passed": False, "detail": "user saw breakage",
"generation": "gen-1"},
led.record_real_world)
led = fresh()
run_mutation("record_real_world", "unknown_behaviour_err", led, {
"behaviour_id": "RW-GHOST", "passed": True},
led.record_real_world)
# ── accept_limitation ─────────────────────────────────────────────────────
led = fresh()
led.define(behaviour_id="LIM-1", owning_project="demo",
intended_behaviour="known gap")
run_mutation("accept_limitation", "ok", led, {
"behaviour_id": "LIM-1", "note": "edge case accepted for v1",
"approved_by": "user"},
led.accept_limitation)
led = fresh()
led.define(behaviour_id="LIM-2", owning_project="demo",
intended_behaviour="approved gap")
run_mutation("accept_limitation", "custom_approver", led, {
"behaviour_id": "LIM-2", "note": "signed off by maintainer",
"approved_by": "nick"},
led.accept_limitation)
led = fresh()
run_mutation("accept_limitation", "unknown_behaviour_err", led, {
"behaviour_id": "LIM-GHOST", "note": "x"},
led.accept_limitation)
# ── invalidate_for_change ─────────────────────────────────────────────────
led = fresh()
seed_confirmed(led, "INV-1", "component:bridge",
["component:bridge", "interface:completion"])
doc_in = snap(led)
stamp = next_now()
recs = led.invalidate_for_change(changed_entities={"component:bridge"},
generation="gen-2")
add("invalidate_for_change", {
"name": "dep_overlap_default_reason",
"input": {"doc": doc_in, "now": stamp, "changed_entities": sorted({"component:bridge"}),
"generation": "gen-2", "reasons": None},
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
led = fresh()
seed_confirmed(led, "INV-2", "component:bridge", [])
doc_in = snap(led)
stamp = next_now()
recs = led.invalidate_for_change(changed_entities={"component:bridge"},
generation="gen-2")
add("invalidate_for_change", {
"name": "owner_hit_only",
"input": {"doc": doc_in, "now": stamp, "changed_entities": sorted({"component:bridge"}),
"generation": "gen-2", "reasons": None},
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
led = fresh()
seed_confirmed(led, "INV-3", "component:unrelated", ["component:unrelated"])
doc_in = snap(led)
stamp = next_now()
recs = led.invalidate_for_change(changed_entities={"component:bridge"},
generation="gen-2")
add("invalidate_for_change", {
"name": "no_overlap_preserved",
"input": {"doc": doc_in, "now": stamp, "changed_entities": sorted({"component:bridge"}),
"generation": "gen-2", "reasons": None},
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
led = fresh()
seed_confirmed(led, "INV-4", "component:bridge",
["component:bridge", "interface:completion"])
doc_in = snap(led)
stamp = next_now()
recs = led.invalidate_for_change(
changed_entities={"component:bridge", "interface:completion"},
generation="gen-2",
reasons={"behaviour:INV-4": "bridge rewrite (issue #42)"})
add("invalidate_for_change", {
"name": "custom_reason",
"input": {"doc": doc_in, "now": stamp,
"changed_entities": sorted({"component:bridge",
"interface:completion"}),
"generation": "gen-2",
"reasons": {"behaviour:INV-4": "bridge rewrite (issue #42)"}},
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
led = fresh()
led.define(behaviour_id="INV-5", owning_project="demo",
intended_behaviour="never confirmed")
doc_in = snap(led)
stamp = next_now()
recs = led.invalidate_for_change(changed_entities={"component:bridge"},
generation="gen-2")
add("invalidate_for_change", {
"name": "unconfirmed_skipped",
"input": {"doc": doc_in, "now": stamp, "changed_entities": sorted({"component:bridge"}),
"generation": "gen-2", "reasons": None},
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
led = fresh()
seed_confirmed(led, "INV-6", "component:bridge",
["component:bridge", "interface:completion"])
seed_confirmed(led, "INV-7", "component:consumer",
["component:consumer", "interface:completion"])
seed_confirmed(led, "INV-8", "component:unrelated", ["component:unrelated"])
doc_in = snap(led)
stamp = next_now()
recs = led.invalidate_for_change(
changed_entities={"interface:completion"}, generation="gen-2")
add("invalidate_for_change", {
"name": "mixed_multi_behaviour",
"input": {"doc": doc_in, "now": stamp,
"changed_entities": sorted({"interface:completion"}),
"generation": "gen-2", "reasons": None},
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
led = fresh()
seed_confirmed(led, "INV-9", "component:bridge", ["component:bridge"])
doc_in = snap(led)
stamp = next_now()
recs = led.invalidate_for_change(changed_entities={"component:bridge"},
generation="gen-2",
reasons={"behaviour:NOPE": "x"})
add("invalidate_for_change", {
"name": "reasons_key_miss_falls_back",
"input": {"doc": doc_in, "now": stamp, "changed_entities": sorted({"component:bridge"}),
"generation": "gen-2", "reasons": {"behaviour:NOPE": "x"}},
"expected": envelope_ok({"doc": led.doc, "recs": recs})})
# ── retest_history ────────────────────────────────────────────────────────
led = fresh()
seed_confirmed(led, "HIST-1", "component:bridge", ["component:bridge"])
led.invalidate_for_change(changed_entities={"component:bridge"},
generation="gen-2")
hist = led.retest_history("HIST-1")
add("retest_history", {"name": "populated_generations",
"input": {"doc": snap(led),
"behaviour_id": "HIST-1"},
"expected": envelope_ok(hist)})
add("retest_history", {"name": "unknown_behaviour_empty",
"input": {"doc": snap(led),
"behaviour_id": "behaviour:GHOST"},
"expected": envelope_ok([])})
led = fresh()
led.define(behaviour_id="HIST-2", owning_project="demo",
intended_behaviour="no events yet")
add("retest_history", {"name": "empty_history",
"input": {"doc": snap(led),
"behaviour_id": "HIST-2"},
"expected": envelope_ok([])})
# ── outstanding ───────────────────────────────────────────────────────────
led = fresh()
add("outstanding", {"name": "empty_doc",
"input": {"doc": snap(led), "component": "",
"project": ""},
"expected": envelope_ok(led.outstanding())})
led = fresh()
led.define(behaviour_id="OUT-1", owning_project="demo",
intended_behaviour="failed", owner_component="component:a")
led.record_auto_verification("OUT-1", passed=False, generation="g")
led.define(behaviour_id="OUT-2", owning_project="demo",
intended_behaviour="unvalidated", owner_component="component:b")
led.define(behaviour_id="OUT-3", owning_project="other",
intended_behaviour="other project", owner_component="component:a")
led.define(behaviour_id="OUT-4", owning_project="demo",
intended_behaviour="confirmed", owner_component="component:b")
led.record_real_world("OUT-4", passed=True, generation="g")
led.define(behaviour_id="OUT-5", owning_project="demo",
intended_behaviour="accepted", owner_component="component:c")
led.accept_limitation("OUT-5", note="known gap")
led.define(behaviour_id="OUT-6", owning_project="demo",
intended_behaviour="n/a", owner_component="component:c")
led.doc["behaviours"]["behaviour:OUT-6"]["state"] = "NOT_APPLICABLE"
doc = snap(led)
add("outstanding", {"name": "mixed_sort_failed_first",
"input": {"doc": doc, "component": "", "project": ""},
"expected": envelope_ok(led.outstanding())})
add("outstanding", {"name": "component_filter",
"input": {"doc": doc, "component": "component:a",
"project": ""},
"expected": envelope_ok(
led.outstanding(component="component:a"))})
add("outstanding", {"name": "project_filter",
"input": {"doc": doc, "component": "",
"project": "other"},
"expected": envelope_ok(led.outstanding(project="other"))})
add("outstanding", {"name": "combined_filters",
"input": {"doc": doc, "component": "component:a",
"project": "demo"},
"expected": envelope_ok(
led.outstanding(component="component:a",
project="demo"))})
# ── summary ───────────────────────────────────────────────────────────────
led = fresh()
add("summary", {"name": "empty_doc", "input": {"doc": snap(led)},
"expected": envelope_ok(led.summary())})
led = fresh()
led.define(behaviour_id="SUM-1", owning_project="demo",
intended_behaviour="confirmed")
led.record_real_world("SUM-1", passed=True, generation="g")
led.define(behaviour_id="SUM-2", owning_project="demo",
intended_behaviour="failed")
led.record_auto_verification("SUM-2", passed=False, generation="g")
led.define(behaviour_id="SUM-3", owning_project="demo",
intended_behaviour="unvalidated")
led.define(behaviour_id="SUM-4", owning_project="demo",
intended_behaviour="confirmed too")
led.record_real_world("SUM-4", passed=True, generation="g")
add("summary", {"name": "mixed_integer_counts",
"input": {"doc": snap(led)},
"expected": envelope_ok(led.summary())})
truth = {"frozen": "2026-10-11T23:00:00Z clock, oracle = live "
"src/rune/behaviour_ledger.py shim (ep bl_* core)",
"cases": CASES}
TRUTH_PATH.write_text(json.dumps(truth, indent=1, ensure_ascii=False) + "\n")
n = sum(len(v) for v in CASES.values())
print(f"frozen {n} cases across {len(CASES)} groups -> {TRUTH_PATH}")
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Open the #2479 dichotomy-wave construction contract for behaviour_ledger.
Full lifecycle in ONE process (shared-store clobber hazard)."""
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-wave")
r = iface.submit_need(
"Port src/rune/behaviour_ledger.py to FULL Rust (dichotomy factory, "
"issue #2479 tranche): doc-envelope FFI, ctypes loader, facade "
"delegation with RUNE_BL_CORE=python pin, frozen-truth battery, "
"live parity 100%, Phase B language-advantage pass.",
origin="builder",
)
print("submit_need:", json.dumps(r)[:400])
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)[:300])
if not t.get("success"):
sys.exit(1)
print("PENDING_CONTRACT", cid)
+39 -18
View File
@@ -17,23 +17,26 @@ reason; unrelated hardened confirmations are preserved.
Storage: <project_store>/behaviour_ledger.json (versioned document).
Phase G strangler slice 2 (port 4): the state machine (define / get /
transitions / selective invalidation / queues / summary) is served
IN-PROCESS by the Rust `evidence_provenance` cdylib through ep_loader.py
— parity 13/13 proven vs this module's Python vintage (full-scenario
doc parity, byte-equal error messages, save render indent=1 sort_keys).
File glue (load/save) stays Python; timestamps are pinned by this shim
(`now`) so the core stays deterministic. State constants and class API
unchanged (git history holds the original).
Dichotomy wave #2479: the state machine (define / get / transitions /
selective invalidation / queues / summary) is served by the FULL Rust
`behaviour_ledger` cdylib through bl_loader.py — the kernel is a pure
function of the parsed ledger doc, timestamps pass IN (`now`, no-clock
law), and parity is pinned by the frozen truth battery embedded in the
crate (frozen_bl_truth.json). RUNE_BL_CORE=python pins the retired
evidence_provenance shim path (ep_loader) for parity harnesses. File
glue (load/save) stays Python. State constants and class API unchanged
(git history holds the original).
"""
from __future__ import annotations
import json
import os
import sys
import time
from pathlib import Path
# ── Rust core wiring (slice-2 port-4 shim pattern) ──────────────────────────
# ── retired shim wiring (RUNE_BL_CORE=python pin; fallback when the
# wave core is not built) ────────────────────────────────────────────────
from rune import paths # noqa: E402 (#41 centralized paths)
_CRATE_DIR = str(paths.crate_dir("evidence_provenance")) # #41 centralized
if _CRATE_DIR not in sys.path:
@@ -63,6 +66,24 @@ STATES = (UNVALIDATED, IMPLEMENTED, AUTO_VERIFIED, REAL_WORLD_PASSED,
CONFIRMED_STATES = (AUTO_VERIFIED, REAL_WORLD_PASSED, USER_CONFIRMED)
def _kernel():
"""Dichotomy wave #2479: the FULL Rust behaviour_ledger core first
(RUNE_BL_CORE=python pins the retired evidence_provenance shim —
parity harnesses). The kernel owns the state machine (define / get /
transitions / selective invalidation / queues / summary); the file
glue and the class API stay here."""
if os.environ.get("RUNE_BL_CORE", "auto") == "python":
return ep_loader
try:
from rune import bl_loader
if bl_loader.available():
return bl_loader
except ImportError:
pass
return ep_loader
def _now() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
@@ -93,7 +114,7 @@ class BehaviourLedger:
dependencies: list[str] | None = None,
automated_ref: str = "",
environment: str = "") -> dict:
self.doc, rec = ep_loader.bl_define(
self.doc, rec = _kernel().bl_define(
self.doc, behaviour_id=behaviour_id,
owning_project=owning_project,
intended_behaviour=intended_behaviour,
@@ -109,7 +130,7 @@ class BehaviourLedger:
return rec
def get(self, behaviour_id: str) -> dict | None:
return ep_loader.bl_get(self.doc, behaviour_id)
return _kernel().bl_get(self.doc, behaviour_id)
# -- state transitions (all recorded in history — rule 19G) -------------
@@ -118,7 +139,7 @@ class BehaviourLedger:
generation: str = "") -> dict:
"""Automated verification. GREEN NEVER IMPLIES USER_CONFIRMED
(rule 19W-22): the ceiling here is AUTO_VERIFIED."""
self.doc, rec = ep_loader.bl_record_auto(
self.doc, rec = _kernel().bl_record_auto(
self.doc, behaviour_id, passed=passed, detail=detail,
generation=generation, now=_now())
return rec
@@ -128,14 +149,14 @@ class BehaviourLedger:
confirmed_by: str = "user") -> dict:
"""Real-world/manual test result (testing mode). USER_CONFIRMED
requires an explicit confirmed_by='user' pass (rule 19M)."""
self.doc, rec = ep_loader.bl_record_real_world(
self.doc, rec = _kernel().bl_record_real_world(
self.doc, behaviour_id, passed=passed, detail=detail,
generation=generation, confirmed_by=confirmed_by, now=_now())
return rec
def accept_limitation(self, behaviour_id: str, *, note: str,
approved_by: str = "user") -> dict:
self.doc, rec = ep_loader.bl_accept_limitation(
self.doc, rec = _kernel().bl_accept_limitation(
self.doc, behaviour_id, note=note,
approved_by=approved_by, now=_now())
return rec
@@ -150,7 +171,7 @@ class BehaviourLedger:
structural dependencies intersect the changed closure. Every
invalidation carries a reason; unrelated confirmations stay.
Returns the invalidated records."""
self.doc, recs = ep_loader.bl_invalidate(
self.doc, recs = _kernel().bl_invalidate(
self.doc, changed_entities=sorted(changed_entities),
generation=generation, reasons=reasons, now=_now())
return recs
@@ -158,7 +179,7 @@ class BehaviourLedger:
def retest_history(self, behaviour_id: str) -> list[dict]:
"""Generation-aware history: all confirmations + invalidations
for one behaviour, oldest first (rule 19S-12)."""
return ep_loader.bl_retest_history(self.doc, behaviour_id)
return _kernel().bl_retest_history(self.doc, behaviour_id)
# -- queue (testing mode, 19L/M) ------------------------------------------
@@ -167,8 +188,8 @@ class BehaviourLedger:
"""Eligible testing items: anything not confirmed/accepted/n.a.
Dynamic — the queue re-derives from states after every fix
(rule 19M)."""
return ep_loader.bl_outstanding(
return _kernel().bl_outstanding(
self.doc, component=component or "", project=project or "")
def summary(self) -> dict:
return ep_loader.bl_summary(self.doc)
return _kernel().bl_summary(self.doc)
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""ctypes loader for the FULL Rust behaviour_ledger core (dichotomy wave,
#2479 tranche). Mirrors broker_loader.py's contract:
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
* bl_define({doc, behaviour_id, owning_project, intended_behaviour,
origin_intent, pass_criteria, fail_criteria, procedure,
owner_component, dependencies, automated_ref, environment, now})
→ {doc, rec}
* bl_get({doc, behaviour_id}) → record | null
* bl_record_auto({doc, behaviour_id, passed, detail, generation, now})
→ {doc, rec}
* bl_record_real_world({doc, behaviour_id, passed, detail, generation,
confirmed_by, now}) → {doc, rec}
* bl_accept_limitation({doc, behaviour_id, note, approved_by, now})
→ {doc, rec}
* bl_invalidate({doc, changed_entities, generation, reasons, now})
→ {doc, recs}
* bl_retest_history({doc, behaviour_id}) → events (oldest-first)
* bl_outstanding({doc, component, project}) → queue
* bl_summary({doc}) → {total, states, outstanding}
Domain errors (unknown behaviour, duplicate define, …) surface as
{"ok": false, "err": <exact Python message>} and raise ValueError —
the same type the retired evidence_provenance shim raised.
No-clock law: `now` (UTC "%Y-%m-%dT%H:%M:%SZ") is always an argument —
the kernel never reads the wall clock.
Free: capability_scoring's cs_free_string (shared process). Override the
.so with BL_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" / "behaviour_ledger"
_LIB_PATH = Path(
os.environ.get("BL_RUST_LIB", _CRATE_DIR / "target" / "release" / "libbehaviour_ledger.so")
)
_lib = None
class BlCoreUnavailable(FileNotFoundError):
"""The Rust behaviour_ledger 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 BlCoreUnavailable(
f"Rust behaviour_ledger core not built: {_LIB_PATH} missing — run "
f"`cargo build --release` in {_CRATE_DIR}"
)
lib = ctypes.CDLL(str(_LIB_PATH))
for sym in (
"bl_define", "bl_get", "bl_record_auto", "bl_record_real_world",
"bl_accept_limitation", "bl_invalidate", "bl_retest_history",
"bl_outstanding", "bl_summary",
):
fn = getattr(lib, sym)
fn.argtypes = [ctypes.c_void_p]
fn.restype = ctypes.c_void_p
_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 via any sibling free symbol already in-process (capability_scoring)
try:
scoring = ctypes.CDLL(str(
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
))
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
scoring.cs_free_string(ptr)
except OSError:
pass # leak-on-fallback only if capability_scoring absent; bounded
doc = json.loads(out)
if not doc.get("ok"):
raise ValueError(doc.get("err", f"{sym} failed"))
return doc["value"]
def bl_define(doc: dict, *, behaviour_id: str, owning_project: str,
intended_behaviour: str, origin_intent: str = "",
pass_criteria: str = "", fail_criteria: str = "",
procedure: str = "", owner_component: str = "",
dependencies: list | None = None, automated_ref: str = "",
environment: str = "", now: str) -> tuple[dict, dict]:
"""define(): returns (new_doc, new_record)."""
v = _call("bl_define", {
"doc": doc, "behaviour_id": behaviour_id,
"owning_project": owning_project,
"intended_behaviour": intended_behaviour,
"origin_intent": origin_intent, "pass_criteria": pass_criteria,
"fail_criteria": fail_criteria, "procedure": procedure,
"owner_component": owner_component,
"dependencies": list(dependencies or []),
"automated_ref": automated_ref, "environment": environment,
"now": now,
})
return v["doc"], v["rec"]
def bl_record_auto(doc: dict, behaviour_id: str, *, passed: bool,
detail: str = "", generation: str = "",
now: str) -> tuple[dict, dict]:
"""record_auto_verification(): returns (new_doc, record)."""
v = _call("bl_record_auto", {
"doc": doc, "behaviour_id": behaviour_id, "passed": bool(passed),
"detail": detail, "generation": generation, "now": now,
})
return v["doc"], v["rec"]
def bl_record_real_world(doc: dict, behaviour_id: str, *, passed: bool,
detail: str = "", generation: str = "",
confirmed_by: str = "user",
now: str) -> tuple[dict, dict]:
"""record_real_world(): returns (new_doc, record)."""
v = _call("bl_record_real_world", {
"doc": doc, "behaviour_id": behaviour_id, "passed": bool(passed),
"detail": detail, "generation": generation,
"confirmed_by": confirmed_by, "now": now,
})
return v["doc"], v["rec"]
def bl_accept_limitation(doc: dict, behaviour_id: str, *, note: str,
approved_by: str = "user",
now: str) -> tuple[dict, dict]:
"""accept_limitation(): returns (new_doc, record)."""
v = _call("bl_accept_limitation", {
"doc": doc, "behaviour_id": behaviour_id, "note": note,
"approved_by": approved_by, "now": now,
})
return v["doc"], v["rec"]
def bl_invalidate(doc: dict, *, changed_entities: list,
generation: str = "", reasons: dict | None = None,
now: str) -> tuple[dict, list]:
"""invalidate_for_change(): returns (new_doc, invalidated records)."""
v = _call("bl_invalidate", {
"doc": doc, "changed_entities": list(changed_entities),
"generation": generation, "reasons": reasons or {}, "now": now,
})
return v["doc"], v["recs"]
def bl_get(doc: dict, behaviour_id: str) -> dict | None:
"""get(): record or None (normalized id)."""
return _call("bl_get", {"doc": doc, "behaviour_id": behaviour_id})
def bl_retest_history(doc: dict, behaviour_id: str) -> list:
"""retest_history(): events oldest-first (empty for unknown)."""
return _call("bl_retest_history", {"doc": doc,
"behaviour_id": behaviour_id})
def bl_outstanding(doc: dict, *, component: str = "",
project: str = "") -> list:
"""outstanding(): filtered queue, TEST_FAILED first."""
return _call("bl_outstanding", {"doc": doc, "component": component,
"project": project})
def bl_summary(doc: dict) -> dict:
"""summary(): {total, states, outstanding}."""
return _call("bl_summary", {"doc": doc})
+23
View File
@@ -10,6 +10,28 @@ dependencies = [
"serde_json",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -76,6 +98,7 @@ version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"indexmap",
"itoa",
"memchr",
"serde",
+4 -1
View File
@@ -8,4 +8,7 @@ crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# preserve_order: Python-dict insertion-order semantics — the behaviours
# map is walked in insertion order (selective invalidation output order,
# queue pre-sort order) exactly like the retired ep core.
serde_json = { version = "1", features = ["preserve_order"] }
@@ -0,0 +1,207 @@
//! behaviour_ledger branch battery — parity vs the frozen Python-oracle
//! truth (frozen_bl_truth.json: 39 cases across all nine kernel-owned
//! public functions and every branch — define minimal/full/prefixed-id/
//! duplicate/unicode/None-deps, get both id forms/miss/empty, automated
//! pass/fail/unknown/200-codepoint detail truncation/reverify-confirmed,
//! real-world user confirmation/other-confirmer/fail/unknown,
//! accept-limitation ok/custom-approver/unknown, selective invalidation
//! dep-overlap default reason/owner-hit/no-overlap preserved/custom
//! reason/reasons key miss/unconfirmed skip/mixed, retest history
//! populated/unknown/empty, outstanding mixed sort + filters, summary
//! empty/mixed integer counts). Frozen via scripts/
//! gen_behaviour_ledger_truth.py with the clock pinned (no-clock law)
//! and inputs deep-copied before the oracle ran (poisoned-truth law).
//! Embedded VERBATIM — never transcribed.
use crate::{
accept_limitation, define, get, invalidate_for_change, outstanding,
retest_history, record_auto_verification, record_real_world, summary,
};
use serde_json::{json, Value};
const TRUTH: &str = include_str!("frozen_bl_truth.json");
fn cases(group: &str) -> Vec<Value> {
let truth: Value = serde_json::from_str(TRUTH).unwrap();
truth["cases"][group]
.as_array()
.unwrap()
.clone()
}
fn replay(group: &str, f: impl Fn(&Value) -> Value) {
for case in cases(group) {
let got = f(&case["input"]);
assert_eq!(
&got,
&case["expected"],
"[{}] case '{}' diverged",
group,
case["name"].as_str().unwrap()
);
}
}
fn ok(v: Value) -> Value {
json!({"ok": true, "value": v})
}
fn err_case(e: String) -> Value {
json!({"ok": false, "err": e})
}
fn doc_of(i: &Value) -> Value {
i["doc"].clone()
}
/// A mutated-op envelope: kernel Ok → {"ok": true, "value": {doc, rec}} /
/// {doc, recs}; kernel Err → the exact-Python-message error envelope.
fn run_mut(
i: &Value,
f: impl FnOnce(&mut Value) -> Result<Value, String>,
) -> Value {
let mut doc = doc_of(i);
match f(&mut doc) {
Ok(rec) => ok(json!({"doc": doc, "rec": rec})),
Err(e) => err_case(e),
}
}
#[test]
fn define_parity_battery() {
replay("define", |i| {
let deps: Vec<String> = i["dependencies"]
.as_array()
.map(|a| {
a.iter()
.filter_map(Value::as_str)
.map(String::from)
.collect()
})
.unwrap_or_default();
let mut doc = doc_of(i);
match define(
&mut doc,
i["behaviour_id"].as_str().unwrap(),
i["owning_project"].as_str().unwrap(),
i["intended_behaviour"].as_str().unwrap(),
i["origin_intent"].as_str().unwrap_or(""),
i["pass_criteria"].as_str().unwrap_or(""),
i["fail_criteria"].as_str().unwrap_or(""),
i["procedure"].as_str().unwrap_or(""),
i["owner_component"].as_str().unwrap_or(""),
&deps,
i["automated_ref"].as_str().unwrap_or(""),
i["environment"].as_str().unwrap_or(""),
i["now"].as_str().unwrap(),
) {
Ok(rec) => ok(json!({"doc": doc, "rec": rec})),
Err(e) => err_case(e),
}
});
}
#[test]
fn get_parity_battery() {
replay("get", |i| ok(get(&doc_of(i), i["behaviour_id"].as_str().unwrap())));
}
#[test]
fn record_auto_verification_parity_battery() {
replay("record_auto_verification", |i| {
run_mut(i, |doc| {
record_auto_verification(
doc,
i["behaviour_id"].as_str().unwrap(),
i["passed"].as_bool().unwrap(),
i["detail"].as_str().unwrap_or(""),
i["generation"].as_str().unwrap_or(""),
i["now"].as_str().unwrap(),
)
})
});
}
#[test]
fn record_real_world_parity_battery() {
replay("record_real_world", |i| {
run_mut(i, |doc| {
record_real_world(
doc,
i["behaviour_id"].as_str().unwrap(),
i["passed"].as_bool().unwrap(),
i["detail"].as_str().unwrap_or(""),
i["generation"].as_str().unwrap_or(""),
i["confirmed_by"].as_str().unwrap_or("user"),
i["now"].as_str().unwrap(),
)
})
});
}
#[test]
fn accept_limitation_parity_battery() {
replay("accept_limitation", |i| {
run_mut(i, |doc| {
accept_limitation(
doc,
i["behaviour_id"].as_str().unwrap(),
i["note"].as_str().unwrap_or(""),
i["approved_by"].as_str().unwrap_or("user"),
i["now"].as_str().unwrap(),
)
})
});
}
#[test]
fn invalidate_for_change_parity_battery() {
replay("invalidate_for_change", |i| {
let changed: Vec<String> = i["changed_entities"]
.as_array()
.map(|a| {
a.iter()
.filter_map(Value::as_str)
.map(String::from)
.collect()
})
.unwrap_or_default();
let mut doc = doc_of(i);
match invalidate_for_change(
&mut doc,
&changed,
i["generation"].as_str().unwrap_or(""),
&i["reasons"],
i["now"].as_str().unwrap(),
) {
Ok(recs) => ok(json!({"doc": doc, "recs": recs})),
Err(e) => err_case(e),
}
});
}
#[test]
fn retest_history_parity_battery() {
replay("retest_history", |i| {
ok(retest_history(
&doc_of(i),
i["behaviour_id"].as_str().unwrap(),
))
});
}
#[test]
fn outstanding_parity_battery() {
replay("outstanding", |i| {
ok(outstanding(
&doc_of(i),
i["component"].as_str().unwrap_or(""),
i["project"].as_str().unwrap_or(""),
))
});
}
#[test]
fn summary_parity_battery() {
replay("summary", |i| ok(summary(&doc_of(i))));
}
File diff suppressed because it is too large. Load diff
+669 -75
View File
@@ -1,80 +1,674 @@
//! behaviour_ledger — behaviour definition and tracking (W6).
//! PARITY port of src/rune/behaviour_ledger.py.
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviourEntry {
pub behaviour_id: String,
pub owning_project: String,
pub description: String,
pub evidence_refs: Vec<String>,
pub defined_at: String,
pub status: String,
}
pub struct BehaviourLedger {
pub behaviours: BTreeMap<String, BehaviourEntry>,
}
impl BehaviourLedger {
pub fn new() -> Self {
BehaviourLedger {
behaviours: BTreeMap::new(),
}
}
pub fn define(
&mut self,
behaviour_id: &str,
owning_project: &str,
description: &str,
evidence_refs: Vec<String>,
) -> BehaviourEntry {
let entry = BehaviourEntry {
behaviour_id: behaviour_id.to_string(),
owning_project: owning_project.to_string(),
description: description.to_string(),
evidence_refs,
defined_at: now(),
status: "defined".to_string(),
};
self.behaviours
.insert(behaviour_id.to_string(), entry.clone());
entry
}
pub fn get(&self, behaviour_id: &str) -> Option<&BehaviourEntry> {
self.behaviours.get(behaviour_id)
}
}
impl Default for BehaviourLedger {
fn default() -> Self {
Self::new()
}
}
fn now() -> String {
format!(
"{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
)
}
//! behaviour_ledger — the canonical Behaviour Validation Ledger (T19
//! §19F/G/I). FULL parity port of src/rune/behaviour_ledger.py
//! (dichotomy wave, issue #2479 tranche).
//!
//! PRIMARY UNIT: the VERSIONED BEHAVIOUR / TEST SPECIFICATION. Automated
//! verification and real-world/user confirmation are DISTINCT truths
//! (rule 19F): tests green ≠ user-confirmed.
//!
//! The kernel owns the state machine — define / get / transitions with
//! generation-aware history (rule 19G) / selective invalidation (rule
//! 19I) / the outstanding queue / the summary — over the parsed ledger
//! doc. File glue (load/save of <store>/behaviour_ledger.json), the
//! state constants and the class API stay in the Python facade.
//!
//! Python semantics preserved exactly (pinned by the frozen oracle
//! truth, frozen_bl_truth.json, replayed VERBATIM by the branch
//! battery):
//! * id normalization: "behaviour:" prefix added unless present;
//! * duplicate define → Err "behaviour exists: behaviour:X";
//! * unknown behaviour → Err "unknown behaviour '<original id>'"
//! (repr'd, NOT normalized);
//! * history entries carry detail truncated to 200 CODEPOINTS;
//! * green never implies USER_CONFIRMED (rule 19W-22) — the
//! automated ceiling is AUTO_VERIFIED;
//! * USER_CONFIRMED requires passed && confirmed_by == "user"
//! (rule 19M); other confirmers land REAL_WORLD_PASSED;
//! * invalidation touches CONFIRMED_STATES whose dependencies
//! intersect the changed closure or whose owner is changed; every
//! invalidation carries a reason — default
//! "structural change touched: ['a', 'b']" (Python list repr,
//! sorted); unrelated confirmations are PRESERVED;
//! * outstanding excludes USER_CONFIRMED / ACCEPTED_LIMITATION /
//! NOT_APPLICABLE, sorts TEST_FAILED first then behaviour_id;
//! * summary counts are Python ints.
//!
//! No-clock law: `now` is always an argument — the kernel never reads
//! the wall clock.
//!
//! FFI (doc-envelope law): every symbol takes ONE JSON doc and returns
//! ONE JSON doc {"ok": true, "value": ...} / {"ok": false, "err": ...}.
//! Mutating ops return the full new doc alongside the record(s).
//! Strings are freed via capability_scoring's cs_free_string — this
//! crate exports NO free symbol of its own (duplicate exports break
//! the cdylib link when sibling crates share one process).
use serde_json::{json, Map, Value};
use std::collections::BTreeSet;
use std::ffi::c_char;
use std::fmt;
#[cfg(test)]
mod tests {
use super::*;
mod branch_battery;
#[test]
fn test_define_and_get() {
let mut ledger = BehaviourLedger::new();
ledger.define("b1", "rune", "test behaviour", vec!["evidence1".into()]);
let b = ledger.get("b1");
assert!(b.is_some());
assert_eq!(b.unwrap().owning_project, "rune");
pub const LEDGER_SCHEMA_VERSION: i64 = 1;
// hardening lifecycle (rule 19G semantics)
pub const UNVALIDATED: &str = "UNVALIDATED";
pub const IMPLEMENTED: &str = "IMPLEMENTED";
pub const AUTO_VERIFIED: &str = "AUTO_VERIFIED";
pub const REAL_WORLD_PASSED: &str = "REAL_WORLD_PASSED";
pub const TEST_FAILED: &str = "TEST_FAILED";
pub const PARTIAL: &str = "PARTIAL";
pub const USER_CONFIRMED: &str = "USER_CONFIRMED";
pub const RETEST_REQUIRED: &str = "RETEST_REQUIRED";
pub const ACCEPTED_LIMITATION: &str = "ACCEPTED_LIMITATION";
pub const NOT_APPLICABLE: &str = "NOT_APPLICABLE";
pub const STATES: [&str; 10] = [
UNVALIDATED,
IMPLEMENTED,
AUTO_VERIFIED,
REAL_WORLD_PASSED,
TEST_FAILED,
PARTIAL,
USER_CONFIRMED,
RETEST_REQUIRED,
ACCEPTED_LIMITATION,
NOT_APPLICABLE,
];
/// states that represent STANDING CONFIDENCE (selective invalidation
/// may reopen exactly these; everything else is already open or n/a)
pub const CONFIRMED_STATES: [&str; 3] =
[AUTO_VERIFIED, REAL_WORLD_PASSED, USER_CONFIRMED];
// ── helpers ─────────────────────────────────────────────────────────────────
/// Python repr of a string: single quotes, escaped backslash/quote.
fn py_repr(s: &str) -> String {
let mut out = String::from("'");
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\'' => out.push_str("\\'"),
other => out.push(other),
}
}
out.push('\'');
out
}
/// Python repr of a list of strings: ['a', 'b'].
fn py_list_repr(items: &[String]) -> String {
let inner: Vec<String> = items.iter().map(|s| py_repr(s)).collect();
format!("[{}]", inner.join(", "))
}
fn norm_id(behaviour_id: &str) -> String {
if behaviour_id.starts_with("behaviour:") {
behaviour_id.to_string()
} else {
format!("behaviour:{}", behaviour_id)
}
}
/// First `n` CODEPOINTS of `s` (Python `s[:200]`).
fn chars_upto(s: &str, n: usize) -> String {
s.chars().take(n).collect()
}
fn get_str(v: &Value, key: &str, default: &str) -> String {
v.get(key)
.and_then(Value::as_str)
.unwrap_or(default)
.to_string()
}
/// doc["behaviours"] pairs (Err when the ledger is malformed).
fn behaviours_pairs(doc: &Value) -> Result<&Map<String, Value>, String> {
match doc.get("behaviours").and_then(Value::as_object) {
Some(m) => Ok(m),
None => Err("corrupt ledger: behaviours is not an object".to_string()),
}
}
/// Fetch a behaviour record (normalized id) from the full doc.
fn get_rec(doc: &Value, bid: &str) -> Option<Value> {
behaviours_pairs(doc)
.ok()?
.get(bid)
.cloned()
}
/// doc["behaviours"][bid] = val (insert or replace).
fn set_rec(doc: &mut Value, bid: &str, val: Value) {
if let Some(bp) = doc.get_mut("behaviours").and_then(Value::as_object_mut) {
bp.insert(bid.to_string(), val);
}
}
/// Append to a record's array field (evidence / history / limitations /
/// invalidations).
fn push_to_arr(rec: &mut Value, key: &str, val: Value) {
if let Some(a) = rec.get_mut(key).and_then(Value::as_array_mut) {
a.push(val);
}
}
// ── transitions ─────────────────────────────────────────────────────────────
/// _transition — history append + state + updated_at; unknown state → Err.
fn transition(
rec: &mut Value,
new_state: &str,
kind: &str,
detail: &str,
generation: &str,
now: &str,
) -> Result<(), String> {
if !STATES.contains(&new_state) {
return Err(format!("unknown validation state {}", py_repr(new_state)));
}
let from = get_str(rec, "state", "");
push_to_arr(
rec,
"history",
json!({
"at": now,
"from": from,
"to": new_state,
"kind": kind,
"detail": chars_upto(detail, 200),
"generation": generation,
}),
);
if let Some(obj) = rec.as_object_mut() {
obj.insert("state".to_string(), json!(new_state));
obj.insert("updated_at".to_string(), json!(now));
}
Ok(())
}
// ── public operations (name-preserving ports of the oracle) ─────────────────
/// define — duplicate → Err; returns the new record.
#[allow(clippy::too_many_arguments)] // pinned: parity signature — the port mirrors the oracle arg-for-arg
pub fn define(
doc: &mut Value,
behaviour_id: &str,
owning_project: &str,
intended_behaviour: &str,
origin_intent: &str,
pass_criteria: &str,
fail_criteria: &str,
procedure: &str,
owner_component: &str,
dependencies: &[String],
automated_ref: &str,
environment: &str,
now: &str,
) -> Result<Value, String> {
let bid = norm_id(behaviour_id);
behaviours_pairs(doc)?;
if get_rec(doc, &bid).is_some() {
return Err(format!("behaviour exists: {}", bid));
}
let rec = json!({
"behaviour_id": bid,
"owning_project": owning_project,
"intended_behaviour": intended_behaviour,
"origin_intent": origin_intent,
"pass_criteria": pass_criteria,
"fail_criteria": fail_criteria,
"procedure": procedure,
"owner_component": owner_component,
"dependencies": dependencies,
"automated_ref": automated_ref,
"environment": environment,
"state": UNVALIDATED,
"evidence": [],
"history": [],
"invalidations": [],
"limitations": [],
"created_at": now,
"updated_at": now,
});
set_rec(doc, &bid, rec.clone());
Ok(rec)
}
/// get — normalized lookup; Null when absent.
pub fn get(doc: &Value, behaviour_id: &str) -> Value {
get_rec(doc, &norm_id(behaviour_id)).unwrap_or(Value::Null)
}
/// record_auto_verification — ceiling AUTO_VERIFIED (green never implies
/// USER_CONFIRMED).
pub fn record_auto_verification(
doc: &mut Value,
behaviour_id: &str,
passed: bool,
detail: &str,
generation: &str,
now: &str,
) -> Result<Value, String> {
let bid = norm_id(behaviour_id);
let mut rec = get_rec(doc, &bid)
.ok_or_else(|| format!("unknown behaviour {}", py_repr(behaviour_id)))?;
push_to_arr(
&mut rec,
"evidence",
json!({
"at": now,
"kind": "automated",
"passed": passed,
"detail": chars_upto(detail, 200),
"generation": generation,
}),
);
let new_state = if passed { AUTO_VERIFIED } else { TEST_FAILED };
transition(&mut rec, new_state, "automated_verification", detail, generation, now)?;
set_rec(doc, &bid, rec.clone());
Ok(rec)
}
/// record_real_world — USER_CONFIRMED iff passed && confirmed_by == "user".
pub fn record_real_world(
doc: &mut Value,
behaviour_id: &str,
passed: bool,
detail: &str,
generation: &str,
confirmed_by: &str,
now: &str,
) -> Result<Value, String> {
let bid = norm_id(behaviour_id);
let mut rec = get_rec(doc, &bid)
.ok_or_else(|| format!("unknown behaviour {}", py_repr(behaviour_id)))?;
push_to_arr(
&mut rec,
"evidence",
json!({
"at": now,
"kind": "real_world",
"passed": passed,
"detail": chars_upto(detail, 200),
"generation": generation,
"by": confirmed_by,
}),
);
let new_state = if passed {
if confirmed_by == "user" {
USER_CONFIRMED
} else {
REAL_WORLD_PASSED
}
} else {
TEST_FAILED
};
transition(&mut rec, new_state, "real_world", detail, generation, now)?;
set_rec(doc, &bid, rec.clone());
Ok(rec)
}
/// accept_limitation — explicitly approved gap.
pub fn accept_limitation(
doc: &mut Value,
behaviour_id: &str,
note: &str,
approved_by: &str,
now: &str,
) -> Result<Value, String> {
let bid = norm_id(behaviour_id);
let mut rec = get_rec(doc, &bid)
.ok_or_else(|| format!("unknown behaviour {}", py_repr(behaviour_id)))?;
push_to_arr(
&mut rec,
"limitations",
json!({
"at": now,
"note": chars_upto(note, 200),
"approved_by": approved_by,
}),
);
transition(&mut rec, ACCEPTED_LIMITATION, "accept_limitation", note, "", now)?;
set_rec(doc, &bid, rec.clone());
Ok(rec)
}
/// invalidate_for_change — selective invalidation (rule 19I); returns the
/// invalidated records. Only CONFIRMED_STATES whose dependency set
/// intersects the changed closure (or whose owner is changed) reopen;
/// every invalidation carries a reason; unrelated confirmations stay.
pub fn invalidate_for_change(
doc: &mut Value,
changed: &[String],
generation: &str,
reasons: &Value,
now: &str,
) -> Result<Value, String> {
let changed_set: BTreeSet<String> = changed.iter().cloned().collect();
let bids: Vec<String> = behaviours_pairs(doc)?
.keys()
.cloned()
.collect();
let mut invalidated: Vec<Value> = Vec::new();
for bid in bids {
let rec = match get_rec(doc, &bid) {
Some(r) => r,
None => continue,
};
let state = get_str(&rec, "state", "");
if !CONFIRMED_STATES.contains(&state.as_str()) {
continue;
}
let deps: BTreeSet<String> = rec
.get("dependencies")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(Value::as_str)
.map(String::from)
.collect()
})
.unwrap_or_default();
let owner = get_str(&rec, "owner_component", "");
let mut overlap: BTreeSet<String> = deps.intersection(&changed_set).cloned().collect();
if !owner.is_empty() && changed_set.contains(&owner) {
overlap.insert(owner.clone());
}
if overlap.is_empty() {
continue; // unrelated hardened behaviour: PRESERVED (19S-5)
}
let overlap_sorted: Vec<String> = overlap.into_iter().collect();
let bid_val = get_str(&rec, "behaviour_id", "");
let because = match reasons.get(&bid_val).and_then(Value::as_str) {
Some(r) => r.to_string(),
None => format!(
"structural change touched: {}",
py_list_repr(&overlap_sorted)
),
};
let mut rec2 = rec.clone();
push_to_arr(
&mut rec2,
"invalidations",
json!({
"at": now,
"generation": generation,
"because": chars_upto(&because, 200),
"was": state,
"changed_entities": overlap_sorted,
}),
);
let detail = format!("was {}; {}", state, because);
transition(
&mut rec2,
RETEST_REQUIRED,
"selective_invalidation",
&detail,
generation,
now,
)?;
set_rec(doc, &bid, rec2.clone());
invalidated.push(rec2);
}
Ok(Value::Array(invalidated))
}
/// retest_history — all events oldest-first for one behaviour (rule
/// 19S-12); empty for unknown.
pub fn retest_history(doc: &Value, behaviour_id: &str) -> Value {
let rec = get(doc, behaviour_id);
let mut events: Vec<Value> = Vec::new();
if let Some(hist) = rec.get("history").and_then(Value::as_array) {
for h in hist {
let from = get_str(h, "from", "");
let to = get_str(h, "to", "");
events.push(json!({
"at": get_str(h, "at", ""),
"generation": get_str(h, "generation", ""),
"event": format!("{}→{}", from, to),
"kind": get_str(h, "kind", ""),
"detail": get_str(h, "detail", ""),
}));
}
}
Value::Array(events)
}
/// outstanding — eligible testing items, TEST_FAILED first then id;
/// confirmed / accepted / n.a. excluded. The queue re-derives from
/// states after every fix (rule 19M).
pub fn outstanding(doc: &Value, component: &str, project: &str) -> Value {
let mut out: Vec<Value> = Vec::new();
if let Ok(pairs) = behaviours_pairs(doc) {
for (_k, rec) in pairs {
let state = get_str(rec, "state", "");
if [USER_CONFIRMED, ACCEPTED_LIMITATION, NOT_APPLICABLE].contains(&state.as_str()) {
continue;
}
if !project.is_empty() && get_str(rec, "owning_project", "") != project {
continue;
}
if !component.is_empty() && get_str(rec, "owner_component", "") != component {
continue;
}
out.push(rec.clone());
}
}
out.sort_by(|a, b| {
let ka = i32::from(get_str(a, "state", "") != TEST_FAILED);
let kb = i32::from(get_str(b, "state", "") != TEST_FAILED);
(ka, get_str(a, "behaviour_id", "")).cmp(&(kb, get_str(b, "behaviour_id", "")))
});
Value::Array(out)
}
/// summary — {total, states: {state: count}, outstanding} with Python
/// int counts.
pub fn summary(doc: &Value) -> Value {
let mut counts: Vec<(String, i64)> = Vec::new();
let mut total: i64 = 0;
if let Ok(pairs) = behaviours_pairs(doc) {
for (_k, rec) in pairs {
let state = get_str(rec, "state", "");
total += 1;
match counts.iter_mut().find(|(k, _)| *k == state) {
Some((_, n)) => *n += 1,
None => counts.push((state, 1)),
}
}
}
let states: Map<String, Value> = counts
.into_iter()
.map(|(k, n)| (k, json!(n)))
.collect();
let out_len = outstanding(doc, "", "").as_array().map(Vec::len).unwrap_or(0);
json!({
"total": total,
"states": states,
"outstanding": out_len,
})
}
// ── FFI (doc-envelope law) ───────────────────────────────────────────────────
/// Typed FFI failure lattice behind the doc-envelope Result — the
/// caller-visible `err` string is unchanged (enum only shapes the Rust
/// side; parity is pinned by the battery).
enum FfiError {
NullPtr,
Utf8(std::str::Utf8Error),
Json(serde_json::Error),
/// Kernel-domain error with the exact Python message.
Domain(String),
}
impl fmt::Display for FfiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FfiError::NullPtr => write!(f, "null argument pointer"),
FfiError::Utf8(e) => write!(f, "{e}"),
FfiError::Json(e) => write!(f, "{e}"),
FfiError::Domain(e) => write!(f, "{e}"),
}
}
}
fn read_arg(ptr: *const c_char) -> Result<Value, FfiError> {
if ptr.is_null() {
return Err(FfiError::NullPtr);
}
let x = unsafe { std::ffi::CStr::from_ptr(ptr) }
.to_str()
.map_err(FfiError::Utf8)?;
serde_json::from_str(x).map_err(FfiError::Json)
}
fn envelope(r: Result<Value, FfiError>) -> *mut c_char {
let v = match r {
Ok(x) => json!({"ok": true, "value": x}),
Err(e) => json!({"ok": false, "err": e.to_string()}),
};
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
/// Kernel-domain error → envelope (the loader raises ValueError with the
/// exact Python message).
fn s<'a>(a: &'a Value, key: &str) -> &'a str {
a.get(key).and_then(Value::as_str).unwrap_or("")
}
#[no_mangle]
pub extern "C" fn bl_define(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
let deps: Vec<String> = a
.get("dependencies")
.and_then(Value::as_array)
.map(|x| x.iter().filter_map(Value::as_str).map(String::from).collect())
.unwrap_or_default();
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
match define(
&mut doc,
s(&a, "behaviour_id"),
s(&a, "owning_project"),
s(&a, "intended_behaviour"),
s(&a, "origin_intent"),
s(&a, "pass_criteria"),
s(&a, "fail_criteria"),
s(&a, "procedure"),
s(&a, "owner_component"),
&deps,
s(&a, "automated_ref"),
s(&a, "environment"),
s(&a, "now"),
) {
Ok(rec) => Ok(json!({"doc": doc, "rec": rec})),
Err(e) => Err(FfiError::Domain(e)),
}
}))
}
#[no_mangle]
pub extern "C" fn bl_get(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
get(a.get("doc").unwrap_or(&Value::Null), s(&a, "behaviour_id"))
}))
}
#[no_mangle]
pub extern "C" fn bl_record_auto(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
match record_auto_verification(
&mut doc,
s(&a, "behaviour_id"),
a.get("passed").and_then(Value::as_bool).unwrap_or(false),
s(&a, "detail"),
s(&a, "generation"),
s(&a, "now"),
) {
Ok(rec) => Ok(json!({"doc": doc, "rec": rec})),
Err(e) => Err(FfiError::Domain(e)),
}
}))
}
#[no_mangle]
pub extern "C" fn bl_record_real_world(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
match record_real_world(
&mut doc,
s(&a, "behaviour_id"),
a.get("passed").and_then(Value::as_bool).unwrap_or(false),
s(&a, "detail"),
s(&a, "generation"),
s(&a, "confirmed_by"),
s(&a, "now"),
) {
Ok(rec) => Ok(json!({"doc": doc, "rec": rec})),
Err(e) => Err(FfiError::Domain(e)),
}
}))
}
#[no_mangle]
pub extern "C" fn bl_accept_limitation(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
match accept_limitation(
&mut doc,
s(&a, "behaviour_id"),
s(&a, "note"),
s(&a, "approved_by"),
s(&a, "now"),
) {
Ok(rec) => Ok(json!({"doc": doc, "rec": rec})),
Err(e) => Err(FfiError::Domain(e)),
}
}))
}
#[no_mangle]
pub extern "C" fn bl_invalidate(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
let changed: Vec<String> = a
.get("changed_entities")
.and_then(Value::as_array)
.map(|x| x.iter().filter_map(Value::as_str).map(String::from).collect())
.unwrap_or_default();
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
let reasons = a.get("reasons").cloned().unwrap_or(Value::Null);
match invalidate_for_change(&mut doc, &changed, s(&a, "generation"), &reasons, s(&a, "now")) {
Ok(recs) => Ok(json!({"doc": doc, "recs": recs})),
Err(e) => Err(FfiError::Domain(e)),
}
}))
}
#[no_mangle]
pub extern "C" fn bl_retest_history(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
retest_history(a.get("doc").unwrap_or(&Value::Null), s(&a, "behaviour_id"))
}))
}
#[no_mangle]
pub extern "C" fn bl_outstanding(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
outstanding(
a.get("doc").unwrap_or(&Value::Null),
s(&a, "component"),
s(&a, "project"),
)
}))
}
#[no_mangle]
pub extern "C" fn bl_summary(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| summary(a.get("doc").unwrap_or(&Value::Null))))
}