From c8b003e8fe7d962c68cb496b883b2fb7443ed415 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 13 Oct 2026 10:50:07 +0100 Subject: [PATCH] =?UTF-8?q?feat(growth=5Fdelta):=20close=20PARTIAL=20gaps?= =?UTF-8?q?=20=E2=80=94=20dichotomy=20wave?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parity: every growth_delta kernel ported name-preserving into the cognition_core broker — _parse_json (fence strip, brace extraction, the _parse_error degrade), _load_deltas/_save_deltas (parse-or-default, the exact json.dump indent=2 bytes), _apply_profile (first-near-dup replace, distinct append), _apply_one (200-char evidence, now-as-argument), reflect's gate pipeline ([:3] slice, 12-char floor, behavioural-only, 0.6 gate), apply's validation (invalid-json/short/wrong-target/low-conf branches), handle_hook passthrough, _llm's exact Ollama payload, _write_store's memory_store argv, _ctx_budget's #28 arithmetic, and the dict-aware py_near_dup. fix-first: the facade's _near_dup fed DICTS into the string-only growth_near_dup kernel, which answered False for every dict pair — live core and pinned fallback DISAGREED (test_growth_delta_error_paths only ran pinned, masking it). The new cc_py_near_dup kernel takes dicts-or- strings; both pins now agree and the previously-masked assertion holds live. doc-envelope FFI via cc_free_string (never re-exported); cc_loader pins argtypes+restype for all 12 new symbols; facade delegation with the RUNE_COGNITION_CORE=python pin. Battery: 9 tests replay 24 pure + 22 state frozen-oracle cases VERBATIM (include_str!). Live parity 86/86 facade checks both pins; freeze_truth --verify byte-identical both pins. clippy --all-targets -D warnings 0 (incl. two pre-existing lints the new toolchain gates: branch_battery module inception, parse_probe format!). Audit: growth_delta 5/20 → 17/20; the 3 remaining (_store_db env-path, _cc_core loader probe, _py_growth_profile fallback oracle) stay Python by law, same calibration as drift_cache. --- scripts/check_growth_delta_parity.py | 234 ++++++++++ scripts/gen_growth_delta_truth.py | 362 ++++++++++++++++ src/rune/cc_loader.py | 62 ++- src/rune/growth_delta.py | 147 +++++-- src/rust/cognition_core/src/branch_battery.rs | 6 +- src/rust/cognition_core/src/broker.rs | 305 ++++++++++++- .../src/frozen_growth_delta_state.json | 404 ++++++++++++++++++ .../src/frozen_growth_delta_truth.json | 291 +++++++++++++ src/rust/cognition_core/src/growth_battery.rs | 191 +++++++++ src/rust/cognition_core/src/lib.rs | 171 ++++++++ src/rust/cognition_core/tests/parse_probe.rs | 2 +- 11 files changed, 2132 insertions(+), 43 deletions(-) create mode 100644 scripts/check_growth_delta_parity.py create mode 100644 scripts/gen_growth_delta_truth.py create mode 100644 src/rust/cognition_core/src/frozen_growth_delta_state.json create mode 100644 src/rust/cognition_core/src/frozen_growth_delta_truth.json create mode 100644 src/rust/cognition_core/src/growth_battery.rs diff --git a/scripts/check_growth_delta_parity.py b/scripts/check_growth_delta_parity.py new file mode 100644 index 00000000..20ca963c --- /dev/null +++ b/scripts/check_growth_delta_parity.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +"""check_growth_delta_parity.py — live parity: growth_delta facade under +BOTH core pins vs the frozen oracle truth (dichotomy wave). + +Per pin (RUNE_COGNITION_CORE=python and unset → Rust via cc_loader) it +replays every frozen case through the LIVE facade. File seams are tmp +DELTAS_FILE dirs; subprocess/urlopen are stubbed at parity time (nothing +touches the machine store); the clock is frozen. + +Usage: python3 scripts/check_growth_delta_parity.py +Exit 0 iff every case matches its frozen expected in BOTH pins. +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +import urllib.request +from datetime import datetime +from pathlib import Path +from types import SimpleNamespace + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) + +import rune.growth_delta as gd # noqa: E402 + +CRATE = Path(__file__).resolve().parents[1] / "src" / "rust" / "cognition_core" / "src" +EMPTY = {"applied": [], "pending": [], "last_read_back": None} + + +# NOTE (no-clock law): the facade's wall-clock `applied_at` is NOT logic — +# the apply_one case is normalized to the frozen `now` before comparing; +# apply_ok compares only clock-free fields. The kernel takes `now` as an +# argument, so the Rust battery is exact with zero masking. + +FAILS = [] +CHECKS = [0] + + +def call_core(core, fn, *args, **kwargs): + old = os.environ.get("RUNE_COGNITION_CORE") + os.environ["RUNE_COGNITION_CORE"] = core + try: + return getattr(gd, fn)(*args, **kwargs) + finally: + if old is None: + os.environ.pop("RUNE_COGNITION_CORE", None) + else: + os.environ["RUNE_COGNITION_CORE"] = old + + +def with_file(state, core, fn, *args): + with tempfile.TemporaryDirectory() as td: + path = os.path.join(td, "deltas.json") + if state is not None: + with open(path, "w") as f: + if isinstance(state, dict) and "__raw__" in state: + f.write(state["__raw__"]) + else: + json.dump(state, f) + orig = gd.DELTAS_FILE + gd.DELTAS_FILE = path + try: + result = call_core(core, fn, *args) + try: + with open(path) as f: + after = f.read() + except OSError: + after = None + finally: + gd.DELTAS_FILE = orig + return result, after + + +def check(name, got, want): + CHECKS[0] += 1 + g, w = json.dumps(got, sort_keys=True), json.dumps(want, sort_keys=True) + if g == w: + return + FAILS.append(name) + print(f"MISMATCH [{name}]\n got: {g[:240]}\n want: {w[:240]}") + + +def main() -> int: + for core in ("python", "auto"): + live = call_core(core, "_cc_core") is not None + tag = f"{core}:{'rust' if live else 'py'}" + print(f"== {tag} ==") + + truth = json.loads((CRATE / "frozen_growth_delta_truth.json").read_text()) + for c in truth: + got = call_core(core, c["fn"], *c["args"], **c["kwargs"]) + check(f"{tag}:{c['fn']}", got, c["expected"]) + + state = json.loads((CRATE / "frozen_growth_delta_state.json").read_text())["cases"] + + for c in state["load_deltas"]: + raw = c["input"]["raw"] + seed = None if raw is None else {"__raw__": raw} + got, _ = with_file(seed, core, "_load_deltas") + check(f"{tag}:load:{c['name']}", got, c["expected"]) + + for c in state["save_deltas"]: + st = json.loads(json.dumps(c["input"]["state"])) + _, after = with_file(None, core, "_save_deltas", st) + check(f"{tag}:save:{c['name']}", after, c["expected"]) + + for c in state["apply_profile"]: + prof = json.loads(json.dumps(c["input"]["profile"])) + delta = json.loads(json.dumps(c["input"]["delta"])) + action, after = with_file( + {**EMPTY, "profile": prof}, core, "_apply_profile", delta) + check(f"{tag}:profile:{c['name']}", + {"profile": json.loads(after)["profile"], "action": action}, + c["expected"]) + + for c in state["apply_one"]: + i = c["input"] + got, _ = with_file(dict(EMPTY), core, "_apply_one", + i["change"], i["evidence"], i["conf"], + i["target"]) + got = {**got, "applied_at": i["now"]} # no-clock law: the + # facade's wall-clock stamp is not logic — normalize to the + # frozen `now` before comparing + check(f"{tag}:apply_one", got, c["expected"]) + + for c in state["reflect"]: + orig_llm = gd._llm + gd._llm = lambda m: json.dumps({"deltas": c["input"]["deltas"]}) + try: + res, _ = with_file(dict(EMPTY), core, "reflect", "material", True) + finally: + gd._llm = orig_llm + check(f"{tag}:reflect:{c['name']}", res["would_apply"], + c["expected"]) + + c = state["llm"][0] + captured = {} + + class Resp: + def read(self): + return b"{}" + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + orig_open = urllib.request.urlopen + orig_ctx = gd._ctx_budget + gd._ctx_budget = lambda mid: c["input"]["num_ctx"] + urllib.request.urlopen = lambda req, timeout=0: ( + captured.update(data=req.data.decode()), Resp())[1] + try: + call_core(core, "_llm", c["input"]["prompt"]) + finally: + urllib.request.urlopen = orig_open + gd._ctx_budget = orig_ctx + check(f"{tag}:llm", captured.get("data"), c["expected"]) + + c = state["write_store"][0] + argv_cap = {} + orig_run = subprocess.run + subprocess.run = lambda argv, **kw: ( + argv_cap.update(argv=list(argv)), + SimpleNamespace(returncode=0))[1] + try: + call_core(core, "_write_store", c["input"]["text"], + c["input"]["topic"]) + finally: + subprocess.run = orig_run + check(f"{tag}:store", argv_cap.get("argv", [])[2:], c["expected"][2:]) + + # ctx_budget through the LIVE facade: the oracle signature only + # exposes model_id (task_kind always "default" there — the + # task-kind branches are kernel-covered by the Rust battery). + # Rows: unknown model (default), cloud, local, small, zero-window. + rows = [("unknown-model", 8192, True), + ("cloud-model", 32768, False), + ("local-model", 32768, True), + ("small-local", 2048, True), + ("zero-window", 8192, True)] + for name, window, local in rows: + want = next(c_["expected"] for c_ in state["ctx_budget"] + if c_["input"]["window"] == window + and c_["input"]["local"] == local + and c_["input"]["task_kind"] == "default") + reg = SimpleNamespace() + if window == 8192 and local: + reg.profiles = {} + else: + reg.profiles = {"t": SimpleNamespace( + id=name, context_window=window, local=local)} + import rune + from rune import model_registry as mr + import types as _t + stub = _t.ModuleType("rune.model_registry") + stub.ModelRegistry = lambda: reg + # the real resolve defaults registry=None → its OWN module's + # ModelRegistry; force the stub row explicitly + stub.resolve_context_budget = ( + lambda mid, tk="default", registry=None, _r=reg: + mr.resolve_context_budget(mid, tk, registry=_r)) + saved = sys.modules.get("rune.model_registry") + rune.model_registry = stub + sys.modules["rune.model_registry"] = stub + try: + got = call_core(core, "_ctx_budget", name) + finally: + if saved is not None: + rune.model_registry = saved + sys.modules["rune.model_registry"] = saved + check(f"{tag}:ctx:{name}", got, want) + + res, _ = with_file(dict(EMPTY), core, "apply", json.dumps({ + "change": "a valid longer change here", "confidence": 0.9, + "target": "delivery", "evidence": "ev"})) + check(f"{tag}:apply_ok", + [{"change": r["change"], "target": r["target"], + "confidence": r["confidence"]} for r in res.get("applied", [])], + [{"change": "a valid longer change here", "target": "delivery", + "confidence": 0.9}]) + + bad = len(FAILS) + print(f"{'PASS' if bad == 0 else 'FAIL'}: {CHECKS[0] - bad}/{CHECKS[0]}") + return 1 if bad else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/gen_growth_delta_truth.py b/scripts/gen_growth_delta_truth.py new file mode 100644 index 00000000..09a41f43 --- /dev/null +++ b/scripts/gen_growth_delta_truth.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +"""gen_growth_delta_truth.py — freeze growth_delta oracle truth for the +cognition_core gap-close battery. + +Two truth artifacts (both embedded VERBATIM into the Rust battery): + +1. frozen_growth_delta_truth.json — the freeze_truth harness format + ([{fn, args, kwargs, expected}]), restricted to the PURE, replayable + oracle surface (_parse_json, _py_near_dup, _profile_key, handle_hook, + apply gate branches). This is the file the mandated + `freeze_truth.py --verify` command re-freezes byte-identically. + +2. frozen_growth_delta_state.json — injected-state kernel cases + (drift_resume battery format): the file/LLM/subprocess I/O seams are + patched at capture (tmp DELTAS_FILE, stubbed urlopen/subprocess.run), + so the recorded (fn,args) pairs are NOT re-derivable by --verify; the + battery replays them against the pure kernels with `now` and the I/O + seeds passed as explicit arguments (no-clock law). + +Poisoned-truth law: RUNE_COGNITION_CORE=python is pinned for the whole +capture — the oracle runs pure Python everywhere. +""" +from __future__ import annotations + +import copy +import json +import os +import sys +import tempfile +import urllib.request +from pathlib import Path +from types import SimpleNamespace + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) +os.environ["RUNE_COGNITION_CORE"] = "python" # poisoned-truth law: pure oracle + +from freeze_truth import freeze, write as freeze_write # noqa: E402 + +CRATE = Path(__file__).resolve().parents[1] / "src" / "rust" / "cognition_core" / "src" + +import rune.growth_delta as gd # noqa: E402 + + +# ── 1. pure, replayable surface ───────────────────────────────────────────── + +PURE_CASES = [ + # _parse_json — every branch: plain, ```json fence, ``` fence, junk + # around the object, garbage (→ _parse_error), JSON string, fence with + # trailing junk (brace extraction), JSON array, non-json fence. + ("_parse_json", ('{"deltas": [{"change": "lead with the summary", "confidence": 0.9}]}',), {}), + ("_parse_json", ('```json\n{"deltas": []}\n```',), {}), + ("_parse_json", ('```\n{"deltas": [{"change": "plain fence"}]}\n```',), {}), + ("_parse_json", ('junk before {"deltas": [{"change": "after junk"}]} junk after',), {}), + ("_parse_json", ("total garbage, no braces here",), {}), + ("_parse_json", ('"just a json string"',), {}), + ("_parse_json", ('```json {"a": 1} ``` trailing',), {}), + ("_parse_json", ("[1, 2, 3]",), {}), + ("_parse_json", ("```python\ncode not json\n```",), {}), + ("_parse_json", ('{"deltas": [{"change": "café unicode \u00e9\u4e2d"}]}',), {}), + # _py_near_dup — dicts, strings, mixed, empty keys, the 0.25 boundary, + # punctuation + case normalisation. + ("_py_near_dup", ( + {"change": "deploy qualification resolves runner labels"}, + {"change": "deploy qualification resolves labels"},), {}), + ("_py_near_dup", ( + {"change": "always summarise the ticket before the standup"}, + {"change": "water the plants on tuesdays morning"},), {}), + ("_py_near_dup", ("same idea here", "same idea here"), {}), + ("_py_near_dup", ( + "deploy qualification resolves runner labels", + {"change": "deploy qualification resolves labels"},), {}), + ("_py_near_dup", ({"change": ""}, {"change": "anything at all here"}), {}), + ("_py_near_dup", ( + {"change": "alpha bravo charlie delta"}, + {"change": "alpha echo foxtrot golf"},), {}), + ("_py_near_dup", ( + {"change": "Alpha, BETA! (gamma)"}, + {"change": "alpha beta gamma"},), {}), + ("_py_near_dup", ( + {"change": "one two three four five"}, + {"change": "one six seven eight"},), {}), + # _profile_key returns a Python SET (not JSON-freezable, and --verify + # re-freeze would compare set-vs-list) — the fingerprint logic stays + # battery-covered through the _py_near_dup cases that exercise it. + # handle_hook — the microkernel passthrough. + ("handle_hook", ("session_end", {"k": 1}), {}), + ("handle_hook", ("unknown", None), {}), + # apply — the four gate error branches (the success branch journals and + # writes the store: side-effectful, battery-covered via apply_one). + ("apply", ("not json at all",), {}), + ("apply", ('{"change": "short", "confidence": 0.9, "target": "delivery"}',), {}), + ("apply", ('{"change": "a valid longer change here", "confidence": 0.9, "target": "constitution"}',), {}), + ("apply", ('{"change": "a valid longer change here", "confidence": 0.5, "target": "delivery"}',), {}), +] + + +# ── 2. injected-state kernel cases ────────────────────────────────────────── + +def _with_deltas_file(state, fn, *args, **kwargs): + """Run an oracle fn with DELTAS_FILE pointed at a tmp file seeded with + `state` (or absent when state is None); return (result, file_after).""" + with tempfile.TemporaryDirectory() as td: + path = os.path.join(td, "deltas.json") + if state is not None: + with open(path, "w") as f: + if isinstance(state, dict) and "__raw__" in state: + f.write(state["__raw__"]) # corrupt-file seeding + else: + json.dump(state, f) + orig = gd.DELTAS_FILE + gd.DELTAS_FILE = path + try: + result = fn(*args, **kwargs) + try: + with open(path) as f: + after = f.read() + except OSError: + after = None + finally: + gd.DELTAS_FILE = orig + return result, after + + +def state_cases(): + groups: dict[str, list[dict]] = {} + + # ── load_deltas: missing file → default; valid → parsed; corrupt → default + valid = {"applied": [{"change": "lead with the summary", "evidence": "e1", + "confidence": 0.9, "target": "delivery", + "applied_at": "2026-10-13T04:00:00+00:00"}], + "pending": [], "last_read_back": None, "profile": [ + {"change": "keep answers tight for delivery", "target": "delivery"}]} + groups["load_deltas"] = [ + {"name": "missing_file_default", "input": {"raw": None}, + "expected": copy.deepcopy( + _with_deltas_file(None, gd._load_deltas)[0])}, + {"name": "valid_state", "input": {"raw": json.dumps(valid)}, + "expected": copy.deepcopy( + _with_deltas_file(valid, gd._load_deltas)[0])}, + {"name": "corrupt_default", "input": {"raw": "{corrupt"}, + "expected": copy.deepcopy( + _with_deltas_file({"__raw__": "{corrupt"}, gd._load_deltas)[0])}, + ] + + # ── save_deltas: the exact json.dump(indent=2) bytes + states = [ + {"applied": [], "pending": [], "last_read_back": None}, + {"applied": [{"change": "lead with the summary", "evidence": "user said X", + "confidence": 0.9, "target": "delivery", + "applied_at": "2026-10-13T04:00:00+00:00"}], + "pending": [], "last_read_back": None, + "profile": [{"change": "café guidance unicode \u00e9", "target": "persona"}]}, + ] + cases = [] + for i, st in enumerate(states): + _, after = _with_deltas_file(None, gd._save_deltas, copy.deepcopy(st)) + cases.append({"name": f"save_deltas_{i}", "input": {"state": st}, + "expected": after}) + groups["save_deltas"] = cases + + # ── apply_profile: near-dup merge (same guidance, newer wins) and + # distinct append, both through the ORACLE merge loop. + base_a = {"change": "deploy qualification resolves runner labels", + "evidence": "e1", "confidence": 0.9, "target": "delivery"} + newer = {"change": "deploy qualification resolves labels quickly", + "evidence": "e2", "confidence": 0.8, "target": "delivery"} + distinct = {"change": "water the plants on tuesdays morning", + "evidence": "e3", "confidence": 0.7, "target": "persona"} + for name, seed_profile, delta in ( + ("merge_near_dup", [base_a], newer), + ("append_distinct", [base_a], distinct)): + action, after = _with_deltas_file( + {"applied": [], "pending": [], "last_read_back": None, + "profile": copy.deepcopy(seed_profile)}, + gd._apply_profile, copy.deepcopy(delta)) + groups.setdefault("apply_profile", []).append({ + "name": name, + "input": {"profile": seed_profile, "delta": delta}, + "expected": {"profile": json.loads(after)["profile"], + "action": action}}) + + # ── apply_one: the journal record (now enters the kernel as an argument). + rec, _ = _with_deltas_file({"applied": [], "pending": [], + "last_read_back": None}, + gd._apply_one, "lead with the summary", + "e" * 300, 0.9, "delivery") + groups["apply_one"] = [ + {"name": "journal_record_evidence_truncated", + "input": {"change": "lead with the summary", "evidence": "e" * 300, + "conf": 0.9, "target": "delivery", + "now": rec["applied_at"]}, + "expected": rec}, + ] + + # ── reflect gate pipeline: dry_run=True keeps the oracle side-effect + # free; _llm stubbed per case. The kernel replays would_apply from + # the already-[:3]-sliced delta list. + def reflect_case(name, deltas): + orig_llm = gd._llm + gd._llm = lambda material: json.dumps({"deltas": deltas}) + try: + res, _ = _with_deltas_file({"applied": [], "pending": [], + "last_read_back": None}, + gd.reflect, "material", True) + finally: + gd._llm = orig_llm + return {"name": name, "input": {"deltas": deltas}, + "expected": res["would_apply"]} + + groups["reflect"] = [ + reflect_case("mixed_gates", [ + {"change": "relay in plain english always for delivery", + "confidence": 0.9, "target": "delivery", "evidence": "e1"}, + {"change": "tiny", "confidence": 0.9, "target": "delivery"}, + {"change": "route permanent rules through the gate", + "confidence": 0.9, "target": "constitution", "evidence": "e3"}, + ]), + reflect_case("confidence_gate_and_string_conf", [ + {"change": "keep answers tight for delivery", + "confidence": "0.7", "target": "delivery", "evidence": "s"}, + {"change": "never below the gate for delivery", + "confidence": 0.59, "target": "delivery", "evidence": "s"}, + {"change": "boundary confidence passes the gate", + "confidence": 0.6, "target": "operating", "evidence": "s"}, + ]), + reflect_case("missing_and_falsy_confidence", [ + {"change": "no confidence field here at all", + "target": "delivery", "evidence": "s"}, + {"change": "zero confidence is filtered too", + "confidence": 0, "target": "delivery", "evidence": "s"}, + {"change": "long change with default target", + "confidence": 0.8, "evidence": "e" * 250}, + ]), + ] + + # ── llm payload: the exact request body json.dumps (urlopen stubbed; + # num_ctx pinned so the case is registry-independent). + class _FakeResp: + def __init__(self, body): + self._body = body + + def read(self): + return json.dumps({"response": "ok"}).encode() + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + captured = {} + orig_ctx = gd._ctx_budget + + def fake_urlopen(req, timeout=0): + captured["data"] = req.data.decode() + return _FakeResp(None) + + gd._ctx_budget = lambda model_id: 8192 + orig_open = urllib.request.urlopen + urllib.request.urlopen = fake_urlopen + try: + gd._llm("extract the deltas from this material") + finally: + urllib.request.urlopen = orig_open + gd._ctx_budget = orig_ctx + groups["llm"] = [ + {"name": "request_payload", + "input": {"prompt": "extract the deltas from this material", + "model": gd.DELTA_MODEL, "num_ctx": 8192}, + "expected": captured["data"]}, + ] + + # ── write_store argv: subprocess.run stubbed, argv captured. + captured_argv = {} + import subprocess as _sp + + def fake_run(argv, **kwargs): + captured_argv["argv"] = argv + return SimpleNamespace(returncode=0) + + orig_run = _sp.run + _sp.run = fake_run + try: + gd._write_store("lead with the summary", "delivery") + finally: + _sp.run = orig_run + argv = captured_argv["argv"] + groups["write_store"] = [ + {"name": "argv", + "input": {"py": argv[0], "store_py": argv[1], + "text": "lead with the summary", "topic": "delivery"}, + "expected": argv}, + ] + + # ── ctx_budget: the #28 arithmetic over a STUB registry + # (resolve_context_budget takes registry= — fully pure). + from rune import model_registry as mr + + class _P(SimpleNamespace): + pass + + def reg_of(profiles): + reg = SimpleNamespace() + reg.profiles = profiles + return reg + + m1 = _P(id="cloud-model", context_window=32768, local=False) + m2 = _P(id="local-model", context_window=32768, local=True) + m3 = _P(id="small-local", context_window=2048, local=True) + m4 = _P(id="zero-window", context_window=None, local=True) + + def row(prof, model_id, task_kind): + expected = mr.resolve_context_budget( + model_id, task_kind, registry=reg_of({"t": prof} if prof else {}) + )["num_ctx"] + if prof is None: + window, local = 8192, True + else: + window = prof.context_window or 8192 + local = prof.local + return {"window": window, "local": local, "task_kind": task_kind, + "expected": expected} + + ctx_rows = [ + row(None, "unknown-model", "default"), + row(m1, "cloud-model", "default"), + row(m1, "cloud-model", "verify"), + row(m1, "cloud-model", "summarize"), + row(m1, "cloud-model", "embed"), + row(m1, "cloud-model", "exotic-kind"), + row(m2, "local-model", "default"), + row(m3, "small-local", "default"), + row(m4, "zero-window", "default"), + ] + groups["ctx_budget"] = [ + {"name": f"ctx_budget_{i}", "input": {k: r[k] for k in + ("window", "local", "task_kind")}, + "expected": r["expected"]} + for i, r in enumerate(ctx_rows) + ] + + return groups + + +def main(): + truth = freeze("growth_delta", PURE_CASES, oracle_module="rune.growth_delta") + freeze_write(truth, CRATE / "frozen_growth_delta_truth.json") + print(f"pure truth: {len(truth)} cases") + + state = {"module": "growth_delta", + "captured": "scripts/gen_growth_delta_truth.py", + "pin": "RUNE_COGNITION_CORE=python", + "cases": state_cases()} + (CRATE / "frozen_growth_delta_state.json").write_text( + json.dumps(state, indent=2, ensure_ascii=False) + "\n") + total = sum(len(v) for v in state["cases"].values()) + print(f"state truth: {total} cases across {len(state['cases'])} groups") + + +if __name__ == "__main__": + main() diff --git a/src/rune/cc_loader.py b/src/rune/cc_loader.py index 00b4aa8c..118e7e15 100644 --- a/src/rune/cc_loader.py +++ b/src/rune/cc_loader.py @@ -58,7 +58,11 @@ def _load() -> ctypes.CDLL: "cc_witness_signals", "cc_safety_get_params", "cc_safety_update_pattern", "cc_abstraction_extract_facts", "cc_consolidation_memory", "cc_consolidation_neuron", - "cc_liveness_canary", "cc_reconcile"): + "cc_liveness_canary", "cc_reconcile", + "cc_py_near_dup", "cc_parse_json", "cc_load_deltas", + "cc_save_deltas", "cc_apply_profile", "cc_apply_one", + "cc_reflect", "cc_apply", "cc_handle_hook", "cc_llm", + "cc_write_store", "cc_ctx_budget"): fn = getattr(lib, sym) fn.argtypes = [ctypes.c_char_p] fn.restype = ctypes.c_void_p @@ -134,6 +138,62 @@ def growth_profile(state: dict, order: list, entry_chars: int, "state": state, "order": order, "entry_chars": entry_chars, "max_tokens": max_tokens}) +# ── growth_delta gap-close kernels (dichotomy wave) ───────────────────────── + +def py_near_dup(a, b) -> bool: + """Dict-or-string same-guidance test (the dict-aware near-dup the + facade's _near_dup feeds — growth_near_dup only takes strings).""" + return bool(_call("cc_py_near_dup", {"a": a, "b": b})) + +def parse_json(text: str): + """Best-effort LLM response parse with the _parse_error degrade.""" + return _call("cc_parse_json", {"text": text}) + +def load_deltas(raw: str | None) -> dict: + """Parse-or-default for the raw deltas file text.""" + return _call("cc_load_deltas", {"raw": raw}) + +def save_deltas(state: dict) -> str: + """The exact json.dump(indent=2) bytes for the deltas file.""" + return str(_call("cc_save_deltas", {"state": state})) + +def apply_profile(profile: list, delta: dict) -> dict: + """Near-dup merge/append decision → {profile, action}.""" + return _call("cc_apply_profile", {"profile": profile, "delta": delta}) + +def apply_one(change: str, evidence: str, conf: float, target: str, + now: str) -> dict: + """The journal record (now is an argument — no-clock law).""" + return _call("cc_apply_one", {"change": change, "evidence": evidence, + "conf": conf, "target": target, "now": now}) + +def reflect(deltas: list) -> list: + """The reflect() gate pipeline → would_apply records.""" + return _call("cc_reflect", {"deltas": deltas}) + +def apply(delta_json: str) -> dict: + """apply() validation → {error} | {params}.""" + return _call("cc_apply", {"delta": delta_json}) + +def handle_hook(event: str) -> dict: + """The microkernel registration passthrough.""" + return _call("cc_handle_hook", {"event": event}) + +def llm_payload(prompt: str, model: str, num_ctx: int) -> str: + """The exact json.dumps request body for the Ollama generate call.""" + return str(_call("cc_llm", {"prompt": prompt, "model": model, + "num_ctx": num_ctx})) + +def write_store_argv(py: str, store_py: str, text: str, topic: str) -> list: + """The memory_store.py add argv (subprocess stays in the facade).""" + return _call("cc_write_store", {"py": py, "store_py": store_py, + "text": text, "topic": topic}) + +def ctx_budget(window: int, local: bool, task_kind: str = "default") -> int: + """The #28 budget arithmetic (profile lookup stays in the facade).""" + return int(_call("cc_ctx_budget", {"window": window, "local": local, + "task_kind": task_kind})) + def pins_recall(pins: list, term: str, limit: int = 4) -> list: return _call("cc_pins_recall", {"pins": pins, "term": term, "limit": limit}) diff --git a/src/rune/growth_delta.py b/src/rune/growth_delta.py index 1833bd57..b71fd302 100644 --- a/src/rune/growth_delta.py +++ b/src/rune/growth_delta.py @@ -53,6 +53,19 @@ def _ctx_budget(model_id: str) -> int: from rune import model_registry as _mr except ImportError: import model_registry as _mr + core = _cc_core() + if core is not None: + profile = None + for _tier, p in (_mr.ModelRegistry().profiles or {}).items(): + if p.id == model_id: + profile = p + break + if profile is None: + window, local = 8192, True + else: + window = profile.context_window or 8192 + local = profile.local + return core.ctx_budget(window, local) return _mr.resolve_context_budget(model_id)["num_ctx"] def _store_db(): @@ -85,21 +98,28 @@ Output ONLY JSON: def _llm(prompt): import urllib.request - payload = json.dumps( - { - "model": DELTA_MODEL, - "prompt": prompt, - "stream": False, - "think": False, - "options": {"temperature": 0.4, "num_predict": 800, "num_ctx": _ctx_budget(DELTA_MODEL)}, - } - ).encode() + core = _cc_core() + if core is not None: + payload = core.llm_payload(prompt, DELTA_MODEL, _ctx_budget(DELTA_MODEL)).encode() + else: + payload = json.dumps( + { + "model": DELTA_MODEL, + "prompt": prompt, + "stream": False, + "think": False, + "options": {"temperature": 0.4, "num_predict": 800, "num_ctx": _ctx_budget(DELTA_MODEL)}, + } + ).encode() req = urllib.request.Request(OLLAMA + "/api/generate", data=payload, headers={"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=120) as r: return json.loads(r.read()).get("response", "") def _parse_json(text): + core = _cc_core() + if core is not None: + return core.parse_json(text) t = text.strip() if t.startswith("```"): t = t.strip("`") @@ -123,6 +143,14 @@ def _parse_json(text): def _load_deltas(): + core = _cc_core() + if core is not None: + try: + with open(DELTAS_FILE) as f: + raw = f.read() + except Exception: # noqa: BLE001 — deliberate: missing file is the default path (logged or continued); crash would be worse + raw = None + return core.load_deltas(raw) try: with open(DELTAS_FILE) as f: return json.load(f) @@ -134,7 +162,11 @@ def _save_deltas(d): os.makedirs(os.path.dirname(DELTAS_FILE), exist_ok=True) tmp = DELTAS_FILE + ".tmp" with open(tmp, "w") as f: - json.dump(d, f, indent=2) + core = _cc_core() + if core is not None: + f.write(core.save_deltas(d)) + else: + json.dump(d, f, indent=2) os.replace(tmp, DELTAS_FILE) @@ -180,7 +212,9 @@ def _cc_core(): def _near_dup(a, b): core = _cc_core() if core is not None: - return core.growth_near_dup(a, b) + # the dict-aware kernel — the deltas here ARE dicts, and the old + # string-only delegation answered False for every dict pair + return core.py_near_dup(a, b) return _py_near_dup(a, b) @@ -203,6 +237,12 @@ def _apply_profile(delta): profile stays active, not per-session.""" state = _load_deltas() profile = state.get("profile", []) + core = _cc_core() + if core is not None: + res = core.apply_profile(profile, delta) + state["profile"] = res["profile"] + _save_deltas(state) + return res["action"] for i, existing in enumerate(profile): if _near_dup(existing, delta): profile[i] = delta # same guidance, newer wins @@ -262,8 +302,11 @@ def _write_store(text, topic): try: py = memory_env.python_bin() store_py = os.path.join(_SD, "memory_store.py") - subprocess.run( - [ + core = _cc_core() + if core is not None: + argv = core.write_store_argv(py, store_py, text, topic) + else: + argv = [ py, store_py, "add", @@ -276,7 +319,9 @@ def _write_store(text, topic): "growth_delta", "--method", "agent", - ], + ] + subprocess.run( + argv, capture_output=True, text=True, timeout=30, @@ -290,13 +335,23 @@ def _apply_one(change, evidence, conf, target): and merge it into the ACTIVE growth profile. The single high-confidence gate is enforced by the caller. The profile updates on the same write — growth is active, not a per-session read-back.""" - rec = { - "change": change, - "evidence": (evidence or "")[:200], - "confidence": conf, - "target": target, - "applied_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), - } + core = _cc_core() + if core is not None: + rec = core.apply_one( + change, + (evidence or ""), + conf, + target, + datetime.now(timezone.utc).isoformat(timespec="seconds"), + ) + else: + rec = { + "change": change, + "evidence": (evidence or "")[:200], + "confidence": conf, + "target": target, + "applied_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), + } state = _load_deltas() state["applied"].append(rec) _save_deltas(state) @@ -313,30 +368,43 @@ def reflect(material, dry_run=False): out = _llm(DELTA_PROMPT + "\n\nMaterial:\n" + material) parsed = _parse_json(out) deltas = parsed.get("deltas", [])[:3] # spec: up to 3 per pass - state = _load_deltas() + core = _cc_core() + if core is not None: + would_apply = core.reflect(deltas) + else: + # pinned fallback + parity oracle: short changes, non-behavioural + # targets and sub-0.6 confidence never apply + would_apply = [] + for d in deltas: + change = (d.get("change") or "").strip() + conf = float(d.get("confidence") or 0) + target = d.get("target") or "delivery" + if not change or len(change) < 12: + continue + if target not in BEHAVIOURAL_TOPICS: + continue # never route behavioural deltas to permanent topics + # SINGLE high-confidence gate for behavioural growth. + if conf < 0.6: + continue + would_apply.append( + {"change": change, "evidence": (d.get("evidence") or "")[:200], "confidence": conf, "target": target} + ) applied = [] - would_apply = [] - for d in deltas: - change = (d.get("change") or "").strip() - conf = float(d.get("confidence") or 0) - target = d.get("target") or "delivery" - if not change or len(change) < 12: - continue - if target not in BEHAVIOURAL_TOPICS: - continue # never route behavioural deltas to permanent topics - # SINGLE high-confidence gate for behavioural growth. - if conf < 0.6: - continue - would_apply.append( - {"change": change, "evidence": (d.get("evidence") or "")[:200], "confidence": conf, "target": target} - ) + for d in would_apply: if not dry_run: - applied.append(_apply_one(change, (d.get("evidence") or ""), conf, target)) + applied.append(_apply_one(d["change"], d["evidence"], d["confidence"], d["target"])) return {"extracted": deltas, "applied": applied, "would_apply": would_apply, "dry_run": dry_run} def apply(delta_json): """Apply a behavioural delta directly (spec: apply ).""" + core = _cc_core() + if core is not None: + res = core.apply(delta_json) + if "error" in res: + return {"applied": [], "error": res["error"]} + p = res["params"] + return {"applied": [_apply_one(p["change"], p["evidence"], p["confidence"], p["target"])]} try: d = json.loads(delta_json) except Exception: # noqa: BLE001 — deliberate: best-effort degrade (logged or continued); crash would be worse @@ -363,6 +431,9 @@ def handle_hook(event, data=None): """Hook handler — growth deltas are extracted during sleep-time, not on individual events. This passthrough allows the microkernel to register the plugin without error; actual work happens in reflect().""" + core = _cc_core() + if core is not None: + return core.handle_hook(event) return {"status": "ok", "event": event, "handler": "growth_delta"} diff --git a/src/rust/cognition_core/src/branch_battery.rs b/src/rust/cognition_core/src/branch_battery.rs index c67d5ebd..eb6b6c48 100644 --- a/src/rust/cognition_core/src/branch_battery.rs +++ b/src/rust/cognition_core/src/branch_battery.rs @@ -3,9 +3,11 @@ //! these are the deterministic kernel transforms (pure, zero-IO); Python //! parity lives in the cc_loader family + the existing FFI batteries. -#[path = "branch_battery.rs"] +// (the outer `mod branch_battery;` declaration lives in lib.rs; the +// historical inline wrapper name is kept as `cases` to avoid module +// inception under the current clippy gate) #[cfg(test)] -mod branch_battery { +mod cases { use crate::broker::*; use crate::pyjson::Json; diff --git a/src/rust/cognition_core/src/broker.rs b/src/rust/cognition_core/src/broker.rs index bb7a1465..657b8578 100644 --- a/src/rust/cognition_core/src/broker.rs +++ b/src/rust/cognition_core/src/broker.rs @@ -1,7 +1,7 @@ //! context_broker deterministic kernel (R2 W3.1 parity port). //! Pure functions; the clock/env gate arrives via inputs (no-clock law). -use crate::pyjson::Json; +use crate::pyjson::{self, Json}; pub fn default_estimator(text: &str) -> i64 { (text.chars().count() / 4) as i64 // CODE POINTS: Python len() parity @@ -1365,3 +1365,306 @@ pub fn reconcile(desired: &Json, actual: &Json, tolerance: &[String], stage_id: ("delta".into(), Json::Obj(delta)), ]) } + +// ── growth_delta gap-close kernels (dichotomy wave) ──────────────────────── +// Name-preserving ports of the growth_delta.py kernels the honest audit +// named as missing. The FILE IO (DELTAS_FILE), the Ollama HTTP call, the +// memory_store subprocess and the loader probe stay in the Python facade; +// every deterministic decision is here. + +/// The behavioural (fast single-signal) topic set — permanent topics +/// (constitution/identity/project/human) NEVER route through this path. +fn is_behavioural_topic(target: &str) -> bool { + matches!(target, "delivery" | "operating" | "persona") +} + +/// `d.get("target") or "delivery"`: a MISSING/empty/null target falls back +/// to "delivery"; any other non-string value can never be behavioural. +fn target_of(d: &Json) -> String { + match get(d, "target") { + Some(Json::Str(s)) if !s.is_empty() => s.clone(), + Some(Json::Str(_)) | Some(Json::Null) | None => "delivery".to_string(), + Some(_) => String::new(), + } +} + +/// `float(d.get("confidence") or 0)`: falsy (0/0.0/""/None/false) → 0.0; +/// numeric strings parse; bools are Python floats (True → 1.0). The oracle +/// raises ValueError on non-numeric strings — this kernel degrades to the +/// falsy branch instead (documented divergence; excluded from truth). +fn confidence_of(d: &Json) -> f64 { + match get(d, "confidence") { + Some(Json::Num(n)) => *n, + Some(Json::Bool(b)) => { + if *b { + 1.0 + } else { + 0.0 + } + } + Some(Json::Str(s)) => s.trim().parse::().unwrap_or(0.0), + _ => 0.0, + } +} + +/// _profile_key: the distinctive-word fingerprint of a delta's `change` — +/// strip ".,;:!?()[]{}\"'" then lowercase, keep words longer than 4 CHARS +/// (Python len parity). +fn profile_key(change: &str) -> std::collections::BTreeSet { + let mut out = std::collections::BTreeSet::new(); + for w in change.split_whitespace() { + let w = w + .trim_matches(|c: char| ".,;:!?()[]{}\"'".contains(c)) + .to_lowercase(); + if w.chars().count() > 4 { + out.insert(w); + } + } + out +} + +/// _py_near_dup: same-guidance test over delta dicts OR raw change strings +/// (raw → wrapped). Near when |A∩B| / max(|A|,|B|) >= 0.25; empty keys → +/// false. The dict-aware sibling of growth_near_dup — the facade's +/// _near_dup feeds it DICTS (the old string-only delegation answered False +/// for every dict pair). +pub fn py_near_dup(a: &Json, b: &Json) -> bool { + let change = |v: &Json| match v { + Json::Str(s) => s.clone(), + other => str_or(get(other, "change"), ""), + }; + let (ka, kb) = (profile_key(&change(a)), profile_key(&change(b))); + if ka.is_empty() || kb.is_empty() { + return false; + } + let inter = ka.intersection(&kb).count() as f64; + (inter / ka.len().max(kb.len()) as f64) >= 0.25 +} + +/// _parse_json: best-effort LLM response parse — ``` fences (json-tagged +/// or bare), full-string JSON, then first-{..last-} brace extraction, then +/// the {"deltas": [], "_parse_error": true} degrade (never a crash). +pub fn parse_json(text: &str) -> Json { + let mut t = text.trim(); + if t.starts_with("```") { + t = t.trim_matches('`'); + if t.starts_with("json") { + t = &t[4..]; + } + t = t.trim(); + } + if let Ok(v) = pyjson::parse(t) { + return v; + } + if let (Some(s), Some(e)) = (t.find('{'), t.rfind('}')) { + if e > s { + if let Ok(v) = pyjson::parse(&t[s..=e]) { + return v; + } + } + } + Json::Obj(vec![ + ("deltas".into(), Json::Arr(vec![])), + ("_parse_error".into(), Json::Bool(true)), + ]) +} + +/// _load_deltas: parse the raw deltas file text (arriving as an argument — +/// the read stays in the facade); missing/corrupt → the empty state. +pub fn load_deltas(raw: Option<&str>) -> Json { + if let Some(text) = raw { + if let Ok(v) = pyjson::parse(text) { + return v; + } + } + Json::Obj(vec![ + ("applied".into(), Json::Arr(vec![])), + ("pending".into(), Json::Arr(vec![])), + ("last_read_back".into(), Json::Null), + ]) +} + +/// _save_deltas: the exact `json.dump(d, indent=2)` bytes (ensure_ascii, +/// no trailing newline) — the tmp-write/replace stays in the facade. +pub fn save_deltas(state: &Json) -> String { + pyjson::dump_indent2(state) +} + +/// _apply_profile: merge a new delta into the profile — the FIRST near-dup +/// position is replaced in place (same guidance, newer wins), distinct +/// guidance appends. Returns (profile_after, "merged"|"added"). +pub fn apply_profile(profile: &[Json], delta: &Json) -> (Vec, &'static str) { + let mut out: Vec = Vec::with_capacity(profile.len() + 1); + let mut merged = false; + for existing in profile { + if !merged && py_near_dup(existing, delta) { + out.push(delta.clone()); + merged = true; + } else { + out.push(existing.clone()); + } + } + if !merged { + out.push(delta.clone()); + } + (out, if merged { "merged" } else { "added" }) +} + +/// _apply_one: the journal record — evidence truncated to 200 CHARS, the +/// timestamp arrives as an argument (no-clock law). +pub fn apply_one(change: &str, evidence: &str, conf: f64, target: &str, now: &str) -> Json { + Json::Obj(vec![ + ("change".into(), Json::Str(change.to_string())), + ( + "evidence".into(), + Json::Str(evidence.chars().take(200).collect()), + ), + ("confidence".into(), Json::Num(conf)), + ("target".into(), Json::Str(target.to_string())), + ("applied_at".into(), Json::Str(now.to_string())), + ]) +} + +/// reflect's deterministic gate pipeline (the LLM call and the _apply_one +/// journaling stay in the facade): up to the already-sliced delta list, +/// drop short changes (<12 chars), non-behavioural targets, and +/// sub-0.6-confidence signals; survivors become would_apply records. +pub fn reflect(deltas: &[Json]) -> Vec { + let mut out = Vec::new(); + for d in deltas { + let change = str_or(get(d, "change"), "").trim().to_string(); + let conf = confidence_of(d); + let target = target_of(d); + if change.is_empty() || change.chars().count() < 12 { + continue; + } + if !is_behavioural_topic(&target) { + continue; // never route behavioural deltas to permanent topics + } + if conf < 0.6 { + continue; + } + let evidence = match get(d, "evidence") { + Some(Json::Str(s)) if !s.is_empty() => s.clone(), + _ => String::new(), + }; + out.push(Json::Obj(vec![ + ("change".into(), Json::Str(change)), + ( + "evidence".into(), + Json::Str(evidence.chars().take(200).collect()), + ), + ("confidence".into(), Json::Num(conf)), + ("target".into(), Json::Str(target)), + ])); + } + out +} + +/// apply's deterministic validation: parse the delta JSON and run the +/// single-signal gates. Returns {"error": msg} or +/// {"params": {change, evidence, confidence, target}} — the facade journals +/// the params through apply_one. +pub fn apply(delta_json: &str) -> Json { + let err = |msg: String| { + Json::Obj(vec![("error".into(), Json::Str(msg))]) + }; + let d = match pyjson::parse(delta_json) { + Ok(v) => v, + Err(_) => return err("invalid delta json".to_string()), + }; + let change = str_or(get(&d, "change"), "").trim().to_string(); + let conf = confidence_of(&d); + let target = target_of(&d); + if change.is_empty() || change.chars().count() < 12 { + return err("delta too short to be actionable".to_string()); + } + if !is_behavioural_topic(&target) { + return err(format!("not a behavioural target: {target}")); + } + if conf < 0.6 { + return err(format!( + "confidence {conf} below the 0.6 single-signal gate" + )); + } + let evidence = match get(&d, "evidence") { + Some(Json::Str(s)) if !s.is_empty() => s.clone(), + _ => String::new(), + }; + Json::Obj(vec![( + "params".into(), + Json::Obj(vec![ + ("change".into(), Json::Str(change)), + ("evidence".into(), Json::Str(evidence)), + ("confidence".into(), Json::Num(conf)), + ("target".into(), Json::Str(target)), + ]), + )]) +} + +/// handle_hook: the microkernel registration passthrough (data is accepted +/// and unused, exactly like the oracle body). +pub fn handle_hook(event: &str) -> Json { + Json::Obj(vec![ + ("status".into(), Json::Str("ok".into())), + ("event".into(), Json::Str(event.into())), + ("handler".into(), Json::Str("growth_delta".into())), + ]) +} + +/// _llm's request body: the exact `json.dumps` of the Ollama generate +/// payload (model, prompt, stream/think off, temperature 0.4, 800-token +/// prediction, model-aware num_ctx) — the HTTP round-trip stays in the +/// facade. +pub fn llm_payload(prompt: &str, model: &str, num_ctx: f64) -> String { + let payload = Json::Obj(vec![ + ("model".into(), Json::Str(model.to_string())), + ("prompt".into(), Json::Str(prompt.to_string())), + ("stream".into(), Json::Bool(false)), + ("think".into(), Json::Bool(false)), + ( + "options".into(), + Json::Obj(vec![ + ("temperature".into(), Json::Num(0.4)), + ("num_predict".into(), Json::Num(800.0)), + ("num_ctx".into(), Json::Num(num_ctx)), + ]), + ), + ]); + pyjson::dumps(&payload) +} + +/// _write_store's argv: the memory_store.py `add` invocation (importance +/// 0.5, source growth_delta, method agent) — subprocess.run stays in the +/// facade. +pub fn write_store_argv(py: &str, store_py: &str, text: &str, topic: &str) -> Vec { + vec![ + py.to_string(), + store_py.to_string(), + "add".to_string(), + text.to_string(), + "--topic".to_string(), + topic.to_string(), + "--importance".to_string(), + "0.5".to_string(), + "--source".to_string(), + "growth_delta".to_string(), + "--method".to_string(), + "agent".to_string(), + ] +} + +/// _ctx_budget's #28 arithmetic: window minus the task-kind output reserve +/// (default 2048, verify 512, summarize 1024, embed 0) minus the 1024 +/// tool-result reserve for non-local models, floored at 1024. The profile +/// lookup (config-shaped) stays in the facade. +pub fn ctx_budget(window: f64, local: bool, task_kind: &str) -> f64 { + let output_reserve = match task_kind { + "verify" => 512.0, + "summarize" => 1024.0, + "embed" => 0.0, + _ => 2048.0, + }; + let tool_reserve = if local { 0.0 } else { 1024.0 }; + (window - output_reserve - tool_reserve).max(1024.0) +} diff --git a/src/rust/cognition_core/src/frozen_growth_delta_state.json b/src/rust/cognition_core/src/frozen_growth_delta_state.json new file mode 100644 index 00000000..691d1e0f --- /dev/null +++ b/src/rust/cognition_core/src/frozen_growth_delta_state.json @@ -0,0 +1,404 @@ +{ + "module": "growth_delta", + "captured": "scripts/gen_growth_delta_truth.py", + "pin": "RUNE_COGNITION_CORE=python", + "cases": { + "load_deltas": [ + { + "name": "missing_file_default", + "input": { + "raw": null + }, + "expected": { + "applied": [], + "pending": [], + "last_read_back": null + } + }, + { + "name": "valid_state", + "input": { + "raw": "{\"applied\": [{\"change\": \"lead with the summary\", \"evidence\": \"e1\", \"confidence\": 0.9, \"target\": \"delivery\", \"applied_at\": \"2026-10-13T04:00:00+00:00\"}], \"pending\": [], \"last_read_back\": null, \"profile\": [{\"change\": \"keep answers tight for delivery\", \"target\": \"delivery\"}]}" + }, + "expected": { + "applied": [ + { + "change": "lead with the summary", + "evidence": "e1", + "confidence": 0.9, + "target": "delivery", + "applied_at": "2026-10-13T04:00:00+00:00" + } + ], + "pending": [], + "last_read_back": null, + "profile": [ + { + "change": "keep answers tight for delivery", + "target": "delivery" + } + ] + } + }, + { + "name": "corrupt_default", + "input": { + "raw": "{corrupt" + }, + "expected": { + "applied": [], + "pending": [], + "last_read_back": null + } + } + ], + "save_deltas": [ + { + "name": "save_deltas_0", + "input": { + "state": { + "applied": [], + "pending": [], + "last_read_back": null + } + }, + "expected": "{\n \"applied\": [],\n \"pending\": [],\n \"last_read_back\": null\n}" + }, + { + "name": "save_deltas_1", + "input": { + "state": { + "applied": [ + { + "change": "lead with the summary", + "evidence": "user said X", + "confidence": 0.9, + "target": "delivery", + "applied_at": "2026-10-13T04:00:00+00:00" + } + ], + "pending": [], + "last_read_back": null, + "profile": [ + { + "change": "café guidance unicode é", + "target": "persona" + } + ] + } + }, + "expected": "{\n \"applied\": [\n {\n \"change\": \"lead with the summary\",\n \"evidence\": \"user said X\",\n \"confidence\": 0.9,\n \"target\": \"delivery\",\n \"applied_at\": \"2026-10-13T04:00:00+00:00\"\n }\n ],\n \"pending\": [],\n \"last_read_back\": null,\n \"profile\": [\n {\n \"change\": \"caf\\u00e9 guidance unicode \\u00e9\",\n \"target\": \"persona\"\n }\n ]\n}" + } + ], + "apply_profile": [ + { + "name": "merge_near_dup", + "input": { + "profile": [ + { + "change": "deploy qualification resolves runner labels", + "evidence": "e1", + "confidence": 0.9, + "target": "delivery" + } + ], + "delta": { + "change": "deploy qualification resolves labels quickly", + "evidence": "e2", + "confidence": 0.8, + "target": "delivery" + } + }, + "expected": { + "profile": [ + { + "change": "deploy qualification resolves labels quickly", + "evidence": "e2", + "confidence": 0.8, + "target": "delivery" + } + ], + "action": "merged" + } + }, + { + "name": "append_distinct", + "input": { + "profile": [ + { + "change": "deploy qualification resolves runner labels", + "evidence": "e1", + "confidence": 0.9, + "target": "delivery" + } + ], + "delta": { + "change": "water the plants on tuesdays morning", + "evidence": "e3", + "confidence": 0.7, + "target": "persona" + } + }, + "expected": { + "profile": [ + { + "change": "deploy qualification resolves runner labels", + "evidence": "e1", + "confidence": 0.9, + "target": "delivery" + }, + { + "change": "water the plants on tuesdays morning", + "evidence": "e3", + "confidence": 0.7, + "target": "persona" + } + ], + "action": "added" + } + } + ], + "apply_one": [ + { + "name": "journal_record_evidence_truncated", + "input": { + "change": "lead with the summary", + "evidence": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "conf": 0.9, + "target": "delivery", + "now": "2026-10-13T05:51:01+00:00" + }, + "expected": { + "change": "lead with the summary", + "evidence": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "confidence": 0.9, + "target": "delivery", + "applied_at": "2026-10-13T05:51:01+00:00" + } + } + ], + "reflect": [ + { + "name": "mixed_gates", + "input": { + "deltas": [ + { + "change": "relay in plain english always for delivery", + "confidence": 0.9, + "target": "delivery", + "evidence": "e1" + }, + { + "change": "tiny", + "confidence": 0.9, + "target": "delivery" + }, + { + "change": "route permanent rules through the gate", + "confidence": 0.9, + "target": "constitution", + "evidence": "e3" + } + ] + }, + "expected": [ + { + "change": "relay in plain english always for delivery", + "evidence": "e1", + "confidence": 0.9, + "target": "delivery" + } + ] + }, + { + "name": "confidence_gate_and_string_conf", + "input": { + "deltas": [ + { + "change": "keep answers tight for delivery", + "confidence": "0.7", + "target": "delivery", + "evidence": "s" + }, + { + "change": "never below the gate for delivery", + "confidence": 0.59, + "target": "delivery", + "evidence": "s" + }, + { + "change": "boundary confidence passes the gate", + "confidence": 0.6, + "target": "operating", + "evidence": "s" + } + ] + }, + "expected": [ + { + "change": "keep answers tight for delivery", + "evidence": "s", + "confidence": 0.7, + "target": "delivery" + }, + { + "change": "boundary confidence passes the gate", + "evidence": "s", + "confidence": 0.6, + "target": "operating" + } + ] + }, + { + "name": "missing_and_falsy_confidence", + "input": { + "deltas": [ + { + "change": "no confidence field here at all", + "target": "delivery", + "evidence": "s" + }, + { + "change": "zero confidence is filtered too", + "confidence": 0, + "target": "delivery", + "evidence": "s" + }, + { + "change": "long change with default target", + "confidence": 0.8, + "evidence": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + } + ] + }, + "expected": [ + { + "change": "long change with default target", + "evidence": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "confidence": 0.8, + "target": "delivery" + } + ] + } + ], + "llm": [ + { + "name": "request_payload", + "input": { + "prompt": "extract the deltas from this material", + "model": "qwen3:14b", + "num_ctx": 8192 + }, + "expected": "{\"model\": \"qwen3:14b\", \"prompt\": \"extract the deltas from this material\", \"stream\": false, \"think\": false, \"options\": {\"temperature\": 0.4, \"num_predict\": 800, \"num_ctx\": 8192}}" + } + ], + "write_store": [ + { + "name": "argv", + "input": { + "py": "/usr/bin/python3", + "store_py": "/mnt/workdrive/rune-wave-bl/src/rune/memory_store.py", + "text": "lead with the summary", + "topic": "delivery" + }, + "expected": [ + "/usr/bin/python3", + "/mnt/workdrive/rune-wave-bl/src/rune/memory_store.py", + "add", + "lead with the summary", + "--topic", + "delivery", + "--importance", + "0.5", + "--source", + "growth_delta", + "--method", + "agent" + ] + } + ], + "ctx_budget": [ + { + "name": "ctx_budget_0", + "input": { + "window": 8192, + "local": true, + "task_kind": "default" + }, + "expected": 6144 + }, + { + "name": "ctx_budget_1", + "input": { + "window": 32768, + "local": false, + "task_kind": "default" + }, + "expected": 29696 + }, + { + "name": "ctx_budget_2", + "input": { + "window": 32768, + "local": false, + "task_kind": "verify" + }, + "expected": 31232 + }, + { + "name": "ctx_budget_3", + "input": { + "window": 32768, + "local": false, + "task_kind": "summarize" + }, + "expected": 30720 + }, + { + "name": "ctx_budget_4", + "input": { + "window": 32768, + "local": false, + "task_kind": "embed" + }, + "expected": 31744 + }, + { + "name": "ctx_budget_5", + "input": { + "window": 32768, + "local": false, + "task_kind": "exotic-kind" + }, + "expected": 29696 + }, + { + "name": "ctx_budget_6", + "input": { + "window": 32768, + "local": true, + "task_kind": "default" + }, + "expected": 30720 + }, + { + "name": "ctx_budget_7", + "input": { + "window": 2048, + "local": true, + "task_kind": "default" + }, + "expected": 1024 + }, + { + "name": "ctx_budget_8", + "input": { + "window": 8192, + "local": true, + "task_kind": "default" + }, + "expected": 6144 + } + ] + } +} diff --git a/src/rust/cognition_core/src/frozen_growth_delta_truth.json b/src/rust/cognition_core/src/frozen_growth_delta_truth.json new file mode 100644 index 00000000..e336a5a2 --- /dev/null +++ b/src/rust/cognition_core/src/frozen_growth_delta_truth.json @@ -0,0 +1,291 @@ +[ + { + "fn": "_parse_json", + "args": [ + "{\"deltas\": [{\"change\": \"lead with the summary\", \"confidence\": 0.9}]}" + ], + "kwargs": {}, + "expected": { + "deltas": [ + { + "change": "lead with the summary", + "confidence": 0.9 + } + ] + } + }, + { + "fn": "_parse_json", + "args": [ + "```json\n{\"deltas\": []}\n```" + ], + "kwargs": {}, + "expected": { + "deltas": [] + } + }, + { + "fn": "_parse_json", + "args": [ + "```\n{\"deltas\": [{\"change\": \"plain fence\"}]}\n```" + ], + "kwargs": {}, + "expected": { + "deltas": [ + { + "change": "plain fence" + } + ] + } + }, + { + "fn": "_parse_json", + "args": [ + "junk before {\"deltas\": [{\"change\": \"after junk\"}]} junk after" + ], + "kwargs": {}, + "expected": { + "deltas": [ + { + "change": "after junk" + } + ] + } + }, + { + "fn": "_parse_json", + "args": [ + "total garbage, no braces here" + ], + "kwargs": {}, + "expected": { + "deltas": [], + "_parse_error": true + } + }, + { + "fn": "_parse_json", + "args": [ + "\"just a json string\"" + ], + "kwargs": {}, + "expected": "just a json string" + }, + { + "fn": "_parse_json", + "args": [ + "```json {\"a\": 1} ``` trailing" + ], + "kwargs": {}, + "expected": { + "a": 1 + } + }, + { + "fn": "_parse_json", + "args": [ + "[1, 2, 3]" + ], + "kwargs": {}, + "expected": [ + 1, + 2, + 3 + ] + }, + { + "fn": "_parse_json", + "args": [ + "```python\ncode not json\n```" + ], + "kwargs": {}, + "expected": { + "deltas": [], + "_parse_error": true + } + }, + { + "fn": "_parse_json", + "args": [ + "{\"deltas\": [{\"change\": \"café unicode é中\"}]}" + ], + "kwargs": {}, + "expected": { + "deltas": [ + { + "change": "café unicode é中" + } + ] + } + }, + { + "fn": "_py_near_dup", + "args": [ + { + "change": "deploy qualification resolves runner labels" + }, + { + "change": "deploy qualification resolves labels" + } + ], + "kwargs": {}, + "expected": true + }, + { + "fn": "_py_near_dup", + "args": [ + { + "change": "always summarise the ticket before the standup" + }, + { + "change": "water the plants on tuesdays morning" + } + ], + "kwargs": {}, + "expected": false + }, + { + "fn": "_py_near_dup", + "args": [ + "same idea here", + "same idea here" + ], + "kwargs": {}, + "expected": false + }, + { + "fn": "_py_near_dup", + "args": [ + "deploy qualification resolves runner labels", + { + "change": "deploy qualification resolves labels" + } + ], + "kwargs": {}, + "expected": true + }, + { + "fn": "_py_near_dup", + "args": [ + { + "change": "" + }, + { + "change": "anything at all here" + } + ], + "kwargs": {}, + "expected": false + }, + { + "fn": "_py_near_dup", + "args": [ + { + "change": "alpha bravo charlie delta" + }, + { + "change": "alpha echo foxtrot golf" + } + ], + "kwargs": {}, + "expected": true + }, + { + "fn": "_py_near_dup", + "args": [ + { + "change": "Alpha, BETA! (gamma)" + }, + { + "change": "alpha beta gamma" + } + ], + "kwargs": {}, + "expected": true + }, + { + "fn": "_py_near_dup", + "args": [ + { + "change": "one two three four five" + }, + { + "change": "one six seven eight" + } + ], + "kwargs": {}, + "expected": false + }, + { + "fn": "handle_hook", + "args": [ + "session_end", + { + "k": 1 + } + ], + "kwargs": {}, + "expected": { + "status": "ok", + "event": "session_end", + "handler": "growth_delta" + } + }, + { + "fn": "handle_hook", + "args": [ + "unknown", + null + ], + "kwargs": {}, + "expected": { + "status": "ok", + "event": "unknown", + "handler": "growth_delta" + } + }, + { + "fn": "apply", + "args": [ + "not json at all" + ], + "kwargs": {}, + "expected": { + "applied": [], + "error": "invalid delta json" + } + }, + { + "fn": "apply", + "args": [ + "{\"change\": \"short\", \"confidence\": 0.9, \"target\": \"delivery\"}" + ], + "kwargs": {}, + "expected": { + "applied": [], + "error": "delta too short to be actionable" + } + }, + { + "fn": "apply", + "args": [ + "{\"change\": \"a valid longer change here\", \"confidence\": 0.9, \"target\": \"constitution\"}" + ], + "kwargs": {}, + "expected": { + "applied": [], + "error": "not a behavioural target: constitution" + } + }, + { + "fn": "apply", + "args": [ + "{\"change\": \"a valid longer change here\", \"confidence\": 0.5, \"target\": \"delivery\"}" + ], + "kwargs": {}, + "expected": { + "applied": [], + "error": "confidence 0.5 below the 0.6 single-signal gate" + } + } +] diff --git a/src/rust/cognition_core/src/growth_battery.rs b/src/rust/cognition_core/src/growth_battery.rs new file mode 100644 index 00000000..345743ee --- /dev/null +++ b/src/rust/cognition_core/src/growth_battery.rs @@ -0,0 +1,191 @@ +//! growth_delta gap-close battery — parity vs the frozen Python-oracle +//! truth, embedded VERBATIM (never transcribed): +//! - frozen_growth_delta_truth.json: the freeze_truth harness format — +//! the PURE, replayable oracle surface (_parse_json, _py_near_dup, +//! handle_hook, apply gate branches). Re-freezable via +//! `freeze_truth.py --verify` (byte-identical law). +//! - frozen_growth_delta_state.json: injected-state kernel cases — the +//! file/LLM/subprocess seams were patched at capture (tmp DELTAS_FILE, +//! stubbed urlopen/subprocess.run), so `now` and the I/O seeds enter +//! the kernels as explicit arguments (no-clock law). +//! +//! Frozen 2026-10-13 via scripts/gen_growth_delta_truth.py with +//! RUNE_COGNITION_CORE=python pinned (poisoned-truth law). + +use crate::broker; +use crate::pyjson::{self, Json}; + +const TRUTH: &str = include_str!("frozen_growth_delta_truth.json"); +const STATE: &str = include_str!("frozen_growth_delta_state.json"); + +fn j(text: &str) -> Json { + pyjson::parse(text).expect("embedded truth must parse") +} + +fn s(v: &Json, key: &str) -> String { + match v.get(key) { + Some(Json::Str(x)) => x.clone(), + _ => String::new(), + } +} + +fn f(v: &Json, key: &str) -> f64 { + match v.get(key) { + Some(Json::Num(x)) => *x, + _ => 0.0, + } +} + +fn as_str(v: &Json) -> &str { + match v { + Json::Str(s) => s, + _ => "", + } +} + +fn cases(group: &str) -> Vec { + match j(STATE).get("cases").and_then(|c| c.get(group)) { + Some(Json::Arr(items)) => items.clone(), + _ => panic!("missing group {group}"), + } +} + +fn replay_state(group: &str, f_case: impl Fn(&Json) -> Json) { + for case in cases(group) { + let got = f_case(&case.get("input").cloned().unwrap_or(Json::Null)); + assert_eq!( + &got, + case.get("expected").unwrap(), + "[{group}] case '{}' diverged", + s(&case, "name") + ); + } +} + +#[test] +fn pure_truth_battery() { + let truth = j(TRUTH); + let arr = match &truth { + Json::Arr(items) => items.clone(), + _ => panic!("pure truth is a list"), + }; + for case in &arr { + let name = s(case, "fn"); + let args = match case.get("args") { + Some(Json::Arr(items)) => items.clone(), + _ => vec![], + }; + let expected = case.get("expected").cloned().unwrap_or(Json::Null); + let got = match name.as_str() { + "_parse_json" => broker::parse_json(as_str(&args[0])), + "_py_near_dup" => Json::Bool(broker::py_near_dup(&args[0], &args[1])), + "handle_hook" => broker::handle_hook(as_str(&args[0])), + "apply" => { + let res = broker::apply(as_str(&args[0])); + // the oracle returns {"applied": [], "error": msg} on the + // gate branches; the kernel returns {"error": msg} + match res.get("error") { + Some(Json::Str(msg)) => Json::Obj(vec![ + ("applied".into(), Json::Arr(vec![])), + ("error".into(), Json::Str(msg.clone())), + ]), + _ => res, + } + } + other => panic!("no kernel dispatch for {other}"), + }; + assert_eq!(&got, &expected, "pure case diverged: {name}"); + } +} + +#[test] +fn load_deltas_battery() { + replay_state("load_deltas", |i| { + let raw = match i.get("raw") { + Some(Json::Str(x)) => Some(x.as_str()), + _ => None, + }; + broker::load_deltas(raw) + }); +} + +#[test] +fn save_deltas_battery() { + replay_state("save_deltas", |i| { + Json::Str(broker::save_deltas(i.get("state").unwrap_or(&Json::Null))) + }); +} + +#[test] +fn apply_profile_battery() { + replay_state("apply_profile", |i| { + let profile = match i.get("profile") { + Some(Json::Arr(x)) => x.clone(), + _ => vec![], + }; + let delta = i.get("delta").cloned().unwrap_or(Json::Null); + let (profile, action) = broker::apply_profile(&profile, &delta); + Json::Obj(vec![ + ("profile".into(), Json::Arr(profile)), + ("action".into(), Json::Str(action.to_string())), + ]) + }); +} + +#[test] +fn apply_one_battery() { + replay_state("apply_one", |i| { + broker::apply_one( + &s(i, "change"), + &s(i, "evidence"), + f(i, "conf"), + &s(i, "target"), + &s(i, "now"), + ) + }); +} + +#[test] +fn reflect_battery() { + replay_state("reflect", |i| { + let deltas = match i.get("deltas") { + Some(Json::Arr(x)) => x.clone(), + _ => vec![], + }; + Json::Arr(broker::reflect(&deltas)) + }); +} + +#[test] +fn llm_payload_battery() { + replay_state("llm", |i| { + Json::Str(broker::llm_payload( + &s(i, "prompt"), + &s(i, "model"), + f(i, "num_ctx"), + )) + }); +} + +#[test] +fn write_store_battery() { + replay_state("write_store", |i| { + Json::Arr( + broker::write_store_argv(&s(i, "py"), &s(i, "store_py"), &s(i, "text"), &s(i, "topic")) + .into_iter() + .map(Json::Str) + .collect(), + ) + }); +} + +#[test] +fn ctx_budget_battery() { + replay_state("ctx_budget", |i| { + Json::Num(broker::ctx_budget( + f(i, "window"), + matches!(i.get("local"), Some(Json::Bool(true))), + &s(i, "task_kind"), + )) + }); +} diff --git a/src/rust/cognition_core/src/lib.rs b/src/rust/cognition_core/src/lib.rs index 61ae87eb..f828bf54 100644 --- a/src/rust/cognition_core/src/lib.rs +++ b/src/rust/cognition_core/src/lib.rs @@ -9,6 +9,9 @@ pub mod pyjson; #[cfg(test)] mod branch_battery; +#[cfg(test)] +mod growth_battery; + use std::ffi::{CStr, CString}; use std::os::raw::c_char; @@ -552,6 +555,174 @@ pub extern "C" fn cc_reconcile(arg: *const c_char) -> *mut c_char { envelope(result) } +// ── growth_delta gap-close kernels (dichotomy wave) ──────────────────────── + +#[no_mangle] +pub extern "C" fn cc_py_near_dup(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let x = get(&a, "a").cloned().unwrap_or(pyjson::Json::Null); + let y = get(&a, "b").cloned().unwrap_or(pyjson::Json::Null); + pyjson::Json::Bool(broker::py_near_dup(&x, &y)) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_parse_json(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let text = match get(&a, "text") { + Some(pyjson::Json::Str(s)) => s.clone(), + _ => String::new(), + }; + broker::parse_json(&text) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_load_deltas(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let raw = match get(&a, "raw") { + Some(pyjson::Json::Str(s)) => Some(s.as_str()), + _ => None, + }; + broker::load_deltas(raw) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_save_deltas(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let state = get(&a, "state") + .cloned() + .unwrap_or(pyjson::Json::Obj(vec![])); + pyjson::Json::Str(broker::save_deltas(&state)) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_apply_profile(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let profile: Vec = match get(&a, "profile") { + Some(pyjson::Json::Arr(x)) => x.clone(), + _ => vec![], + }; + let delta = get(&a, "delta") + .cloned() + .unwrap_or(pyjson::Json::Obj(vec![])); + let (profile, action) = broker::apply_profile(&profile, &delta); + pyjson::Json::Obj(vec![ + ("profile".to_string(), pyjson::Json::Arr(profile)), + ("action".to_string(), pyjson::Json::Str(action.to_string())), + ]) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_apply_one(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let s = |k: &str| match get(&a, k) { + Some(pyjson::Json::Str(x)) => x.clone(), + _ => String::new(), + }; + let conf = match get(&a, "conf") { + Some(pyjson::Json::Num(n)) => *n, + _ => 0.0, + }; + broker::apply_one(&s("change"), &s("evidence"), conf, &s("target"), &s("now")) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_reflect(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let deltas: Vec = match get(&a, "deltas") { + Some(pyjson::Json::Arr(x)) => x.clone(), + _ => vec![], + }; + pyjson::Json::Arr(broker::reflect(&deltas)) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_apply(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let delta_json = match get(&a, "delta") { + Some(pyjson::Json::Str(s)) => s.clone(), + _ => String::new(), + }; + broker::apply(&delta_json) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_handle_hook(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let event = match get(&a, "event") { + Some(pyjson::Json::Str(s)) => s.clone(), + _ => String::new(), + }; + broker::handle_hook(&event) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_llm(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let s = |k: &str| match get(&a, k) { + Some(pyjson::Json::Str(x)) => x.clone(), + _ => String::new(), + }; + let num_ctx = match get(&a, "num_ctx") { + Some(pyjson::Json::Num(n)) => *n, + _ => 0.0, + }; + pyjson::Json::Str(broker::llm_payload(&s("prompt"), &s("model"), num_ctx)) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_write_store(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let s = |k: &str| match get(&a, k) { + Some(pyjson::Json::Str(x)) => x.clone(), + _ => String::new(), + }; + pyjson::Json::Arr( + broker::write_store_argv(&s("py"), &s("store_py"), &s("text"), &s("topic")) + .into_iter() + .map(pyjson::Json::Str) + .collect(), + ) + }); + envelope(result) +} + +#[no_mangle] +pub extern "C" fn cc_ctx_budget(arg: *const c_char) -> *mut c_char { + let result = read_arg(arg).and_then(|t| arg_json(&t)).map(|a| { + let window = match get(&a, "window") { + Some(pyjson::Json::Num(n)) => *n, + _ => 8192.0, + }; + let local = matches!(get(&a, "local"), Some(pyjson::Json::Bool(true))); + let task_kind = match get(&a, "task_kind") { + Some(pyjson::Json::Str(s)) => s.clone(), + _ => String::new(), + }; + pyjson::Json::Num(broker::ctx_budget(window, local, &task_kind)) + }); + envelope(result) +} + #[allow(clippy::not_unsafe_ptr_arg_deref)] // pinned: #[no_mangle] extern "C" — the Survivor loader ABI requires this exact safe-fn signature; the raw pointer IS the FFI contract #[no_mangle] diff --git a/src/rust/cognition_core/tests/parse_probe.rs b/src/rust/cognition_core/tests/parse_probe.rs index ae5d2704..c1b31c61 100644 --- a/src/rust/cognition_core/tests/parse_probe.rs +++ b/src/rust/cognition_core/tests/parse_probe.rs @@ -14,7 +14,7 @@ fn tier_recency_works() { .unwrap(); let t = cognition_core::broker::tier_get(&e, 7.0, 30.0, "2026-10-05T12:00:00+00:00"); assert!( - format!("{}", cognition_core::pyjson::dumps(&t)).contains("hot"), + cognition_core::pyjson::dumps(&t).contains("hot"), "expected hot, got {}", cognition_core::pyjson::dumps(&t) );