feat(behaviour_ledger): port to FULL Rust — dichotomy wave
This commit is contained in:
1 parent
7c83200c2e
commit
44b25064c9
10 files changed
+6766
-94
No files matched your search
@@ -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())
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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})
|
||||
Generated
+23
@@ -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",
|
||||
|
||||
@@ -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
@@ -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))))
|
||||
}
|
||||
Reference in new issue
Block a user