feat(meaningful_events): port to FULL Rust — dichotomy wave
Language verdict: RUST per docs/language-dichotomy-ledger.md (Law 4 — deterministic trigger evaluation; the wall CLOCK is host-side by nature (now passed per op, never null across FFI)). - me_should_fire/me_fire_due/me_mark_fired doc-envelope FFI (me_* symbols; free via capability_scoring cs_free_string — no free re-export); me_loader mirroring broker_loader.py. - Facade delegation with RUNE_ME_CORE=python pin; kernel floor on any kernel error; INT now keeps the Python floor in mark_fired (the oracle stores the value as-is; the kernel wire is float). - Python laws replicated exactly: decision ordering (unknown → stop → absent → rate → fires), state truthiness (0/''/null are absent), falsy last_fired → 0, STRICTLY-LESS rate gate (equal fires), Python int() truncation toward zero INSIDE the reason string, fire_due in table (insertion) order, mark_fired shallow-copy with unknown-trigger recording. - 96-case frozen oracle truth (RUNE_ME_CORE=python pinned, deep-copied inputs, every trigger class × every branch, truncation + boundary + future-last cases) embedded in the crate and replayed VERBATIM by the branch battery; live parity 1296/1296 (96 frozen + 1200 fuzz, seed 2479); pytest 56 passed (kernel auto) and 32 per pin BOTH ways; clippy -D warnings clean.
This commit is contained in:
1 parent
ffa8608f95
commit
a0d0215ee0
10 files changed
+2319
-1
No files matched your search
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: meaningful_events Rust kernel vs Python oracle (dichotomy
|
||||
wave). Layers:
|
||||
1. FROZEN — the 96 frozen-oracle cases replayed through the facade in
|
||||
BOTH core modes (RUNE_ME_CORE=python floor vs auto kernel); exact
|
||||
compare, inputs deep-copied.
|
||||
2. FUZZ — 400 seeded random trigger/state/now triples (seed 2479)
|
||||
driven through both modes; exact compare (should_fire, fire_due,
|
||||
mark_fired round-trips).
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import meaningful_events as me # noqa: E402
|
||||
from rune import me_loader # noqa: E402
|
||||
|
||||
if not me_loader.available():
|
||||
print("FATAL: Rust meaningful_events core not built")
|
||||
sys.exit(2)
|
||||
|
||||
TRUTH = json.loads(
|
||||
(REPO / "src" / "rust" / "meaningful_events" / "frozen_truth.json")
|
||||
.read_text())
|
||||
|
||||
fails = []
|
||||
counts = {"frozen": 0, "fuzz": 0}
|
||||
|
||||
|
||||
def fail(layer, name, detail):
|
||||
fails.append((layer, name, detail))
|
||||
print(f"FAIL[{layer}] {name}: {detail}")
|
||||
|
||||
|
||||
def drive(mode, fn, *args):
|
||||
os.environ["RUNE_ME_CORE"] = mode
|
||||
try:
|
||||
return fn(*copy.deepcopy(args))
|
||||
finally:
|
||||
os.environ["RUNE_ME_CORE"] = "auto"
|
||||
|
||||
|
||||
def frozen():
|
||||
for c in TRUTH["should_fire_cases"]:
|
||||
counts["frozen"] += 1
|
||||
args = (c["args"]["trigger"], c["args"]["state"], c["args"]["now"])
|
||||
py = drive("python", me.should_fire, *args)
|
||||
rs = drive("auto", me.should_fire, *args)
|
||||
if py != rs or py != c["expect"]:
|
||||
fail("frozen", f"should_fire/{c['name']}", f"{py!r} != {rs!r}")
|
||||
for c in TRUTH["fire_due_cases"]:
|
||||
counts["frozen"] += 1
|
||||
args = (c["args"]["state"], c["args"]["now"])
|
||||
py = drive("python", me.fire_due, *args)
|
||||
rs = drive("auto", me.fire_due, *args)
|
||||
if py != rs or py != c["expect"]:
|
||||
fail("frozen", f"fire_due/{c['name']}", f"{py!r} != {rs!r}")
|
||||
for c in TRUTH["mark_fired_cases"]:
|
||||
counts["frozen"] += 1
|
||||
args = (c["args"]["state"], c["args"]["trigger"], c["args"]["now"])
|
||||
py = drive("python", me.mark_fired, *args)
|
||||
rs = drive("auto", me.mark_fired, *args)
|
||||
if py != rs or py != c["expect"]:
|
||||
fail("frozen", f"mark_fired/{c['name']}", f"{py!r} != {rs!r}")
|
||||
|
||||
|
||||
TRIGGER_NAMES = list(me.TRIGGERS) + ["mystery", ""]
|
||||
COND_KEYS = sorted({t[0] for t in me.TRIGGERS.values()})
|
||||
STOP_KEYS = sorted({t[2] for t in me.TRIGGERS.values()})
|
||||
VALUES = [True, False, 0, 1, "", "yes", None, 0.5]
|
||||
|
||||
|
||||
def rand_state(rng):
|
||||
state = {}
|
||||
for k in COND_KEYS + STOP_KEYS:
|
||||
if rng.random() < 0.3:
|
||||
state[k] = rng.choice(VALUES)
|
||||
for name in list(me.TRIGGERS):
|
||||
if rng.random() < 0.3:
|
||||
state[f"last_fired_{name}"] = rng.choice(
|
||||
[0, 0.0, rng.uniform(-500, 1500), 999.0])
|
||||
return state
|
||||
|
||||
|
||||
def fuzz():
|
||||
rng = random.Random(2479)
|
||||
for i in range(400):
|
||||
trigger = rng.choice(TRIGGER_NAMES)
|
||||
state = rand_state(rng)
|
||||
now = round(rng.uniform(-100, 5000), 4)
|
||||
py = drive("python", me.should_fire, trigger, state, now)
|
||||
rs = drive("auto", me.should_fire, trigger, state, now)
|
||||
counts["fuzz"] += 1
|
||||
if py != rs:
|
||||
fail("fuzz", f"fire-{i}",
|
||||
f"{trigger!r}@{now}: {py!r} != {rs!r}")
|
||||
continue
|
||||
py = drive("python", me.fire_due, state, now)
|
||||
rs = drive("auto", me.fire_due, state, now)
|
||||
counts["fuzz"] += 1
|
||||
if py != rs:
|
||||
fail("fuzz", f"due-{i}", f"{py!r} != {rs!r}")
|
||||
continue
|
||||
mark_trigger = rng.choice(TRIGGER_NAMES)
|
||||
py = drive("python", me.mark_fired, state, mark_trigger, now)
|
||||
rs = drive("auto", me.mark_fired, state, mark_trigger, now)
|
||||
counts["fuzz"] += 1
|
||||
if py != rs:
|
||||
fail("fuzz", f"mark-{i}", f"{py!r} != {rs!r}")
|
||||
|
||||
|
||||
frozen()
|
||||
fuzz()
|
||||
total = counts["frozen"] + counts["fuzz"]
|
||||
print(f"parity: {total - len(fails)}/{total} "
|
||||
f"(frozen {counts['frozen']}, fuzz {counts['fuzz']})")
|
||||
sys.exit(1 if fails else 0)
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze the meaningful_events oracle truth (dichotomy wave, #2479).
|
||||
|
||||
Generator law: RUNE_ME_CORE=python pinned, every input deep-copied,
|
||||
every public function exercised across ALL branches:
|
||||
* should_fire — unknown trigger; each of the 8 stop conditions; each
|
||||
of the 8 absent conditions; rate limiting incl. Python int()
|
||||
truncation (fractional remaining), future last_fired, last_fired 0;
|
||||
falsy condition/stop values (0, "", None) vs truthy
|
||||
* fire_due — table-order listing, subsets, none due, stop-blocked
|
||||
* mark_fired — key set on copy (original untouched), unknown trigger
|
||||
still records, explicit now
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
os.environ["RUNE_ME_CORE"] = "python"
|
||||
|
||||
import meaningful_events as me # noqa: E402
|
||||
|
||||
cases_should_fire = []
|
||||
cases_fire_due = []
|
||||
cases_mark_fired = []
|
||||
|
||||
|
||||
def case_fire(name, trigger, state, now):
|
||||
trigger = copy.deepcopy(trigger)
|
||||
state = copy.deepcopy(state)
|
||||
now = copy.deepcopy(now)
|
||||
cases_should_fire.append({
|
||||
"name": name,
|
||||
"args": {"trigger": trigger, "state": state, "now": now},
|
||||
"expect": me.should_fire(copy.deepcopy(trigger),
|
||||
copy.deepcopy(state),
|
||||
copy.deepcopy(now)),
|
||||
})
|
||||
|
||||
|
||||
def case_due(name, state, now):
|
||||
state = copy.deepcopy(state)
|
||||
now = copy.deepcopy(now)
|
||||
cases_fire_due.append({
|
||||
"name": name,
|
||||
"args": {"state": state, "now": now},
|
||||
"expect": me.fire_due(copy.deepcopy(state), copy.deepcopy(now)),
|
||||
})
|
||||
|
||||
|
||||
def case_mark(name, state, trigger, now):
|
||||
state = copy.deepcopy(state)
|
||||
trigger = copy.deepcopy(trigger)
|
||||
now = copy.deepcopy(now)
|
||||
st = copy.deepcopy(state)
|
||||
expect = me.mark_fired(st, copy.deepcopy(trigger), copy.deepcopy(now))
|
||||
cases_mark_fired.append({
|
||||
"name": name,
|
||||
"args": {"state": state, "trigger": trigger, "now": now},
|
||||
"expect": expect,
|
||||
"input_unchanged": st == state,
|
||||
})
|
||||
|
||||
|
||||
# ── should_fire: every branch ───────────────────────────────────────────────
|
||||
case_fire("unknown-trigger", "nope", {}, 1000.0)
|
||||
for trig in me.TRIGGERS:
|
||||
cond, limit, stop = me.TRIGGERS[trig]
|
||||
case_fire(f"fires-{trig}", trig, {cond: True}, 1000.0)
|
||||
case_fire(f"fires-with-old-last-{trig}", trig,
|
||||
{cond: True, f"last_fired_{trig}": 0.0}, 1000.0)
|
||||
case_fire(f"stop-met-{trig}", trig, {cond: True, stop: True}, 1000.0)
|
||||
case_fire(f"stop-truthy-string-{trig}", trig, {cond: True, stop: "yes"}, 1000.0)
|
||||
case_fire(f"stop-falsy-zero-not-met-{trig}", trig,
|
||||
{cond: True, stop: 0}, 1000.0)
|
||||
case_fire(f"condition-absent-{trig}", trig, {}, 1000.0)
|
||||
case_fire(f"condition-falsy-empty-str-{trig}", trig, {cond: ""}, 1000.0)
|
||||
case_fire(f"condition-falsy-null-{trig}", trig, {cond: None}, 1000.0)
|
||||
case_fire(f"rate-limited-{trig}", trig,
|
||||
{cond: True, f"last_fired_{trig}": 1000.0 - (limit - 100.0)}, 1000.0)
|
||||
case_fire(f"rate-limited-exact-boundary-{trig}", trig,
|
||||
{cond: True, f"last_fired_{trig}": 1000.0 - limit}, 1000.0)
|
||||
case_fire("rate-truncation-fractional", "task_blockers",
|
||||
{"task_blocked": True, "last_fired_task_blockers": 400.5}, 1000.0)
|
||||
case_fire("rate-truncation-tiny-remaining", "owner_corrections",
|
||||
{"correction_received": True, "last_fired_owner_corrections": 700.0001},
|
||||
1000.0)
|
||||
case_fire("future-last-rate-limited", "task_blockers",
|
||||
{"task_blocked": True, "last_fired_task_blockers": 5000.0}, 1000.0)
|
||||
case_fire("negative-now-last-fractional", "curiosity_candidates",
|
||||
{"novel_material_present": True, "last_fired_curiosity_candidates": -100.25},
|
||||
1000.0)
|
||||
case_fire("float-now-and-last", "repeated_failure",
|
||||
{"same_failure_3x": True, "last_fired_repeated_failure": 123.456},
|
||||
789.012)
|
||||
|
||||
# ── fire_due: ordering + subsets ────────────────────────────────────────────
|
||||
ALL_DUE = {cond: True for cond, _, _ in me.TRIGGERS.values()}
|
||||
case_due("all-eight-due-table-order", ALL_DUE, 1000.0)
|
||||
case_due("none-due", {}, 1000.0)
|
||||
case_due("subset-due", {"task_blocked": True, "correction_received": 1}, 1000.0)
|
||||
case_due("stop-blocks-some", {**ALL_DUE, "task_unblocked": True}, 1000.0)
|
||||
case_due("rate-limits-block-some",
|
||||
{cond: True for cond, _, _ in me.TRIGGERS.values()}
|
||||
| {"last_fired_owner_corrections": 999.0}, 1000.0)
|
||||
case_due("falsy-conditions-only",
|
||||
{cond: 0 for cond, _, _ in me.TRIGGERS.values()}, 1000.0)
|
||||
|
||||
# ── mark_fired: copy semantics ──────────────────────────────────────────────
|
||||
case_mark("mark-explicit-now", {"task_blocked": True}, "task_blockers", 1000.0)
|
||||
case_mark("mark-unknown-trigger-still-records", {}, "mystery", 55.5)
|
||||
case_mark("mark-overwrites-existing", {"last_fired_task_blockers": 1.0},
|
||||
"task_blockers", 2000.0)
|
||||
case_mark("mark-empty-state", {}, "owner_corrections", 0.25)
|
||||
|
||||
truth = {
|
||||
"module": "meaningful_events",
|
||||
"core_pin": "RUNE_ME_CORE=python",
|
||||
"should_fire_cases": cases_should_fire,
|
||||
"fire_due_cases": cases_fire_due,
|
||||
"mark_fired_cases": cases_mark_fired,
|
||||
}
|
||||
|
||||
out = REPO / "src" / "rust" / "meaningful_events" / "frozen_truth.json"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(truth, indent=1, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
n = len(cases_should_fire) + len(cases_fire_due) + len(cases_mark_fired)
|
||||
print(f"frozen {n} cases -> {out}")
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust meaningful_events core (dichotomy wave,
|
||||
#2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* me_should_fire({trigger, state, now}) -> {fires, reason}
|
||||
* me_fire_due({state, now}) -> [trigger, ...]
|
||||
* me_mark_fired({state, trigger, now}) -> state dict
|
||||
|
||||
The wall CLOCK is host-side by nature (now resolved by the facade —
|
||||
null never crosses); the kernel owns the trigger table and the laws.
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with ME_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" / "meaningful_events"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("ME_RUST_LIB", _CRATE_DIR / "target" / "release" / "libmeaningful_events.so")
|
||||
)
|
||||
|
||||
_ERR_TYPES = {
|
||||
"ValueError": ValueError,
|
||||
"KeyError": KeyError,
|
||||
"AttributeError": AttributeError,
|
||||
"TypeError": TypeError,
|
||||
}
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class EventsCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust meaningful-events core is not built. Callers fall back
|
||||
to Python."""
|
||||
|
||||
|
||||
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 EventsCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("me_should_fire", "me_fire_due", "me_mark_fired"):
|
||||
getattr(lib, sym).argtypes = [ctypes.c_void_p]
|
||||
getattr(lib, sym).restype = ctypes.c_void_p
|
||||
try:
|
||||
lib.cs_free_string
|
||||
except AttributeError:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib._free = scoring.cs_free_string
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
free = getattr(lib, "_free", None)
|
||||
if free is not None:
|
||||
free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
etype = doc.get("type", "RuntimeError")
|
||||
raise _ERR_TYPES.get(etype, RuntimeError)(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def should_fire(trigger: str, state: dict, now: float) -> dict:
|
||||
return _call("me_should_fire", {"trigger": trigger, "state": state,
|
||||
"now": now})
|
||||
|
||||
|
||||
def fire_due(state: dict, now: float) -> list:
|
||||
return _call("me_fire_due", {"state": state, "now": now})
|
||||
|
||||
|
||||
def mark_fired(state: dict, trigger: str, now: float) -> dict:
|
||||
return _call("me_mark_fired", {"state": state, "trigger": trigger,
|
||||
"now": now})
|
||||
@@ -15,6 +15,7 @@ approaching, not because a timer fired.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
# Trigger classes with (condition_key, default_rate_limit_s, stop_key)
|
||||
@@ -30,10 +31,36 @@ TRIGGERS = {
|
||||
}
|
||||
|
||||
|
||||
def _kernel():
|
||||
"""The Rust meaningful-events core (me_loader) when RUNE_ME_CORE
|
||||
allows it, else None — the trigger table, the should_fire law, the
|
||||
fire_due ordering and the mark_fired copy law delegate there first
|
||||
and fall back to the Python body below (same semantics). The wall
|
||||
CLOCK is host-side by nature: `now` is resolved here (never null
|
||||
across FFI), and an INT now keeps the Python floor in mark_fired
|
||||
(the oracle stores the value as-is; the kernel wire is float)."""
|
||||
if os.environ.get("RUNE_ME_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import me_loader
|
||||
|
||||
if me_loader.available():
|
||||
return me_loader
|
||||
except Exception: # noqa: BLE001 — kernel failure → Python floor
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def should_fire(trigger: str, state: dict, now: float | None = None) -> dict:
|
||||
"""Evaluate one trigger against current state. Returns
|
||||
{fires, reason}. Rate-limited, stop-conditioned."""
|
||||
now = now if now is not None else time.time()
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.should_fire(trigger, state, now)
|
||||
except Exception: # noqa: BLE001 — kernel floor
|
||||
pass
|
||||
if trigger not in TRIGGERS:
|
||||
return {"fires": False, "reason": f"unknown trigger: {trigger}"}
|
||||
cond_key, rate_limit, stop_key = TRIGGERS[trigger]
|
||||
@@ -50,12 +77,26 @@ def should_fire(trigger: str, state: dict, now: float | None = None) -> dict:
|
||||
|
||||
def fire_due(state: dict, now: float | None = None) -> list:
|
||||
"""All triggers due right now (the endogenous cognition tick)."""
|
||||
resolved = now if now is not None else time.time()
|
||||
k = _kernel()
|
||||
if k is not None:
|
||||
try:
|
||||
return k.fire_due(state, resolved)
|
||||
except Exception: # noqa: BLE001 — kernel floor
|
||||
pass
|
||||
return [t for t in TRIGGERS
|
||||
if should_fire(t, state, now)["fires"]]
|
||||
if should_fire(t, state, resolved)["fires"]]
|
||||
|
||||
|
||||
def mark_fired(state: dict, trigger: str, now: float | None = None) -> dict:
|
||||
"""Record a firing (for rate limiting). Returns the updated state."""
|
||||
k = _kernel()
|
||||
if k is not None and (now is None or isinstance(now, float)):
|
||||
try:
|
||||
return k.mark_fired(state, trigger,
|
||||
now if now is not None else time.time())
|
||||
except Exception: # noqa: BLE001 — kernel floor
|
||||
pass
|
||||
state = dict(state)
|
||||
state[f"last_fired_{trigger}"] = now if now is not None else time.time()
|
||||
return state
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "evidence_provenance"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "meaningful_events"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"evidence_provenance",
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "meaningful_events"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "FULL parity port of src/rune/meaningful_events.py (dichotomy wave, #2479 tranche)"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
evidence_provenance = { path = "../evidence_provenance" }
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,153 @@
|
||||
//! branch_battery — replay the FROZEN oracle truth VERBATIM
|
||||
//! (frozen_truth.json, generated by scripts/gen_meaningful_events_truth.py
|
||||
//! against the LIVE Python oracle with RUNE_ME_CORE=python pinned and
|
||||
//! deep-copied inputs).
|
||||
|
||||
use crate::{fire_due, mark_fired, should_fire};
|
||||
use evidence_provenance::pyjson::{self, Json};
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_truth.json");
|
||||
|
||||
fn truth() -> Json {
|
||||
pyjson::parse(TRUTH).expect("frozen truth parses")
|
||||
}
|
||||
|
||||
fn cases(group: &str) -> Vec<Json> {
|
||||
match truth().get(group) {
|
||||
Some(Json::Arr(items)) => items.clone(),
|
||||
_ => panic!("truth group {group} missing"),
|
||||
}
|
||||
}
|
||||
|
||||
fn at<'a>(v: &'a Json, path: &[&str]) -> Option<&'a Json> {
|
||||
let mut cur = v;
|
||||
for k in path {
|
||||
cur = match cur {
|
||||
Json::Obj(pairs) => pairs
|
||||
.iter()
|
||||
.find(|(k2, _)| k2 == k)
|
||||
.map(|(_, x)| x)?,
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
Some(cur)
|
||||
}
|
||||
|
||||
fn jstr(v: Option<&Json>) -> String {
|
||||
match v {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn now_of(v: Option<&Json>) -> Option<f64> {
|
||||
match v {
|
||||
Some(Json::Num(n)) | Some(Json::Float(n)) => Some(*n),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Python-value equality on Json (order-insensitive objects).
|
||||
fn peq(a: &Json, b: &Json) -> bool {
|
||||
match (a, b) {
|
||||
(Json::Null, Json::Null) => true,
|
||||
(Json::Bool(x), Json::Bool(y)) => x == y,
|
||||
(Json::Num(x), Json::Num(y)) | (Json::Float(x), Json::Float(y)) => x == y,
|
||||
(Json::Float(x), Json::Num(y)) | (Json::Num(x), Json::Float(y)) => x == y,
|
||||
(Json::Str(x), Json::Str(y)) => x == y,
|
||||
(Json::Arr(x), Json::Arr(y)) => {
|
||||
x.len() == y.len() && x.iter().zip(y).all(|(a, b)| peq(a, b))
|
||||
}
|
||||
(Json::Obj(x), Json::Obj(y)) => {
|
||||
x.len() == y.len()
|
||||
&& x.iter().all(|(k, v)| {
|
||||
y.iter()
|
||||
.find(|(k2, _)| k2 == k)
|
||||
.map(|(_, v2)| peq(v, v2))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_case(name: &str, want: &Json, got: &Json) {
|
||||
assert!(
|
||||
peq(want, got),
|
||||
"case {name} diverged:\n want={}\n got={}",
|
||||
pyjson::dumps(want),
|
||||
pyjson::dumps(got),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_should_fire() {
|
||||
for c in cases("should_fire_cases") {
|
||||
let got = should_fire(
|
||||
&jstr(at(&c, &["args", "trigger"])),
|
||||
at(&c, &["args", "state"]).expect("state"),
|
||||
now_of(at(&c, &["args", "now"])),
|
||||
);
|
||||
assert_case(
|
||||
&jstr(at(&c, &["name"])),
|
||||
at(&c, &["expect"]).expect("expect"),
|
||||
&got,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_fire_due() {
|
||||
for c in cases("fire_due_cases") {
|
||||
let got = fire_due(
|
||||
at(&c, &["args", "state"]).expect("state"),
|
||||
now_of(at(&c, &["args", "now"])),
|
||||
);
|
||||
assert_case(
|
||||
&jstr(at(&c, &["name"])),
|
||||
at(&c, &["expect"]).expect("expect"),
|
||||
&got,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_mark_fired() {
|
||||
for c in cases("mark_fired_cases") {
|
||||
let got = mark_fired(
|
||||
at(&c, &["args", "state"]).expect("state"),
|
||||
&jstr(at(&c, &["args", "trigger"])),
|
||||
now_of(at(&c, &["args", "now"])),
|
||||
);
|
||||
assert_case(
|
||||
&jstr(at(&c, &["name"])),
|
||||
at(&c, &["expect"]).expect("expect"),
|
||||
&got,
|
||||
);
|
||||
// the copy law: the kernel takes &Json and cannot mutate, but
|
||||
// the truth ALSO records the oracle's input-unchanged invariant
|
||||
assert!(
|
||||
matches!(at(&c, &["input_unchanged"]), Some(Json::Bool(true))),
|
||||
"case {} oracle input was mutated",
|
||||
jstr(at(&c, &["name"])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_eight_triggers_in_table_order() {
|
||||
let names: Vec<&str> = crate::TRIGGERS.iter().map(|t| t.name).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![
|
||||
"unresolved_commitments",
|
||||
"approaching_deadlines",
|
||||
"repeated_failure",
|
||||
"contradictions",
|
||||
"curiosity_candidates",
|
||||
"prediction_mismatches",
|
||||
"task_blockers",
|
||||
"owner_corrections",
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
//! meaningful_events — endogenous meaningful-event triggers (#48).
|
||||
//!
|
||||
//! FULL parity port of src/rune/meaningful_events.py (dichotomy wave,
|
||||
//! #2479 tranche). The wall CLOCK is host-side by nature — `now` is
|
||||
//! passed per op (null never reaches the kernel; the facade resolves
|
||||
//! time.time() first). The kernel owns the eight-trigger table, the
|
||||
//! should_fire law, the fire_due ordering and the mark_fired copy law.
|
||||
//!
|
||||
//! FFI (doc-envelope law): every call takes one JSON doc and returns
|
||||
//! {"ok": true, "value": ...} | {"ok": false, "err": ..., "type": ...}.
|
||||
//! * me_should_fire({trigger, state, now}) -> {fires, reason}
|
||||
//! * me_fire_due({state, now}) -> [trigger, ...]
|
||||
//! * me_mark_fired({state, trigger, now}) -> state dict
|
||||
//!
|
||||
//! Python laws replicated exactly:
|
||||
//! * should_fire ordering: unknown → stop → absent → rate → fires
|
||||
//! * truthiness on state values (0, "", null are ABSENT/NOT-MET)
|
||||
//! * last = state["last_fired_{trigger}"] or 0 (falsy → 0)
|
||||
//! * rate gate is STRICTLY `now - last < rate_limit` (equal fires)
|
||||
//! * remaining = int(rate_limit - (now - last)) — Python int()
|
||||
//! truncation TOWARD ZERO (matches `as i64` for this domain)
|
||||
//! * fire_due lists TABLE ORDER (insertion order, not sorted)
|
||||
//! * mark_fired: dict(state) shallow copy + key set (unknown
|
||||
//! triggers still record)
|
||||
//!
|
||||
//! NOTE: no me_free_string export — callers free through capability_
|
||||
//! scoring's cs_free_string (shared process allocator).
|
||||
|
||||
use evidence_provenance::pyjson::{self, Json};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn obj(pairs: Vec<(&str, Json)>) -> Json {
|
||||
Json::Obj(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
|
||||
}
|
||||
|
||||
fn s(v: &str) -> Json {
|
||||
Json::Str(v.to_string())
|
||||
}
|
||||
|
||||
/// Phase B groundwork for the language-advantage pass: the trigger
|
||||
/// table is typed records, not tuples — the compiler names every field.
|
||||
pub struct Trigger {
|
||||
pub name: &'static str,
|
||||
pub condition_key: &'static str,
|
||||
pub rate_limit_s: f64,
|
||||
pub stop_key: &'static str,
|
||||
}
|
||||
|
||||
/// The eight trigger classes, TABLE ORDER (the fire_due listing law).
|
||||
pub const TRIGGERS: [Trigger; 8] = [
|
||||
Trigger { name: "unresolved_commitments", condition_key: "has_open_commitments", rate_limit_s: 3600.0, stop_key: "commitments_resolved" },
|
||||
Trigger { name: "approaching_deadlines", condition_key: "deadline_within_24h", rate_limit_s: 1800.0, stop_key: "deadline_passed_or_met" },
|
||||
Trigger { name: "repeated_failure", condition_key: "same_failure_3x", rate_limit_s: 900.0, stop_key: "failure_pattern_broken" },
|
||||
Trigger { name: "contradictions", condition_key: "contradiction_detected", rate_limit_s: 1800.0, stop_key: "contradiction_resolved" },
|
||||
Trigger { name: "curiosity_candidates", condition_key: "novel_material_present", rate_limit_s: 7200.0, stop_key: "material_absorbed" },
|
||||
Trigger { name: "prediction_mismatches", condition_key: "prediction_wrong", rate_limit_s: 1800.0, stop_key: "model_updated" },
|
||||
Trigger { name: "task_blockers", condition_key: "task_blocked", rate_limit_s: 600.0, stop_key: "task_unblocked" },
|
||||
Trigger { name: "owner_corrections", condition_key: "correction_received", rate_limit_s: 300.0, stop_key: "correction_absorbed" },
|
||||
];
|
||||
|
||||
fn find(trigger: &str) -> Option<&'static Trigger> {
|
||||
TRIGGERS.iter().find(|t| t.name == trigger)
|
||||
}
|
||||
|
||||
/// Python truthiness on a JSON value (state.get() laws).
|
||||
fn truthy(v: Option<&Json>) -> bool {
|
||||
match v {
|
||||
None | Some(Json::Null) => false,
|
||||
Some(Json::Bool(b)) => *b,
|
||||
Some(Json::Num(n)) => *n != 0.0,
|
||||
Some(Json::Float(n)) => *n != 0.0,
|
||||
Some(Json::Str(x)) => !x.is_empty(),
|
||||
Some(Json::Arr(a)) => !a.is_empty(),
|
||||
Some(Json::Obj(o)) => !o.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// as_f64 over state/now values (Num and Float alike).
|
||||
fn f64_of(v: Option<&Json>) -> Option<f64> {
|
||||
match v {
|
||||
Some(Json::Num(n)) | Some(Json::Float(n)) => Some(*n),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// should_fire law. `now` is always Some (facade-resolved clock).
|
||||
pub fn should_fire(trigger: &str, state: &Json, now: Option<f64>) -> Json {
|
||||
let now = now.unwrap_or(0.0);
|
||||
let Some(t) = find(trigger) else {
|
||||
return obj(vec![
|
||||
("fires", Json::Bool(false)),
|
||||
("reason", s(&format!("unknown trigger: {trigger}"))),
|
||||
]);
|
||||
};
|
||||
if truthy(state.get(t.stop_key)) {
|
||||
return obj(vec![
|
||||
("fires", Json::Bool(false)),
|
||||
("reason", s(&format!("stop condition met: {}", t.stop_key))),
|
||||
]);
|
||||
}
|
||||
if !truthy(state.get(t.condition_key)) {
|
||||
return obj(vec![
|
||||
("fires", Json::Bool(false)),
|
||||
("reason", s(&format!("condition absent: {}", t.condition_key))),
|
||||
]);
|
||||
}
|
||||
// last = state.get(f"last_fired_{trigger}", 0) — falsy → 0
|
||||
let last = f64_of(state.get(&format!("last_fired_{trigger}"))).unwrap_or(0.0);
|
||||
if now - last < t.rate_limit_s {
|
||||
let remaining = (t.rate_limit_s - (now - last)) as i64;
|
||||
return obj(vec![
|
||||
("fires", Json::Bool(false)),
|
||||
("reason", s(&format!("rate-limited ({remaining}s left)"))),
|
||||
]);
|
||||
}
|
||||
obj(vec![
|
||||
("fires", Json::Bool(true)),
|
||||
("reason", s(&format!("condition holds: {}", t.condition_key))),
|
||||
])
|
||||
}
|
||||
|
||||
/// fire_due law: TABLE ORDER listing of due triggers.
|
||||
pub fn fire_due(state: &Json, now: Option<f64>) -> Json {
|
||||
let due: Vec<Json> = TRIGGERS
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
should_fire(t.name, state, now)
|
||||
.get("fires")
|
||||
.map(|f| f == &Json::Bool(true))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|t| s(t.name))
|
||||
.collect();
|
||||
Json::Arr(due)
|
||||
}
|
||||
|
||||
/// mark_fired law: dict(state) shallow copy + `last_fired_{trigger}`.
|
||||
pub fn mark_fired(state: &Json, trigger: &str, now: Option<f64>) -> Json {
|
||||
let mut pairs: Vec<(String, Json)> = match state {
|
||||
Json::Obj(pairs) => pairs.clone(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let stamp = Json::Float(now.unwrap_or(0.0));
|
||||
let key = format!("last_fired_{trigger}");
|
||||
if let Some(slot) = pairs.iter_mut().find(|(k, _)| *k == key) {
|
||||
slot.1 = stamp;
|
||||
} else {
|
||||
pairs.push((key, stamp));
|
||||
}
|
||||
Json::Obj(pairs)
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope) ───────────────────────────────────────────────────
|
||||
|
||||
fn envelope(result: Result<Json, (String, String)>) -> *mut c_char {
|
||||
let body = match result {
|
||||
Ok(value) => obj(vec![("ok", Json::Bool(true)), ("value", value)]),
|
||||
Err((err, ty)) => obj(vec![
|
||||
("ok", Json::Bool(false)),
|
||||
("err", Json::Str(err)),
|
||||
("type", Json::Str(ty)),
|
||||
]),
|
||||
};
|
||||
let out = pyjson::dumps(&body);
|
||||
match CString::new(out) {
|
||||
Ok(c) => c.into_raw(),
|
||||
Err(_) => CString::new(
|
||||
r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#,
|
||||
)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_doc(raw: &str) -> Result<Json, (String, String)> {
|
||||
pyjson::parse(raw).map_err(|e| (e, "ValueError".to_string()))
|
||||
}
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<String, (String, String)> {
|
||||
if ptr.is_null() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.map_err(|e| (e.to_string(), "ValueError".to_string()))
|
||||
}
|
||||
|
||||
fn now_of(doc: &Json) -> Result<Option<f64>, (String, String)> {
|
||||
match doc.get("now") {
|
||||
None | Some(Json::Null) => Ok(None),
|
||||
Some(v) => f64_of(Some(v))
|
||||
.map(Some)
|
||||
.ok_or_else(|| ("now must be a number or null".into(), "TypeError".into())),
|
||||
}
|
||||
}
|
||||
|
||||
fn state_of(doc: &Json) -> Result<&Json, (String, String)> {
|
||||
match doc.get("state") {
|
||||
Some(st @ Json::Obj(_)) => Ok(st),
|
||||
_ => Err(("state must be an object".into(), "TypeError".into())),
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! ffi {
|
||||
($name:ident, $body:expr) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn $name(arg: *const c_char) -> *mut c_char {
|
||||
let out: Result<Json, (String, String)> =
|
||||
read_arg(arg).and_then(|raw| $body(&raw));
|
||||
envelope(out)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ffi!(me_should_fire, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let trigger = match doc.get("trigger") {
|
||||
Some(Json::Str(t)) => t.clone(),
|
||||
_ => return Err(("trigger must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let state = state_of(&doc)?.clone();
|
||||
let now = now_of(&doc)?;
|
||||
Ok(should_fire(&trigger, &state, now))
|
||||
});
|
||||
|
||||
ffi!(me_fire_due, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let state = state_of(&doc)?.clone();
|
||||
let now = now_of(&doc)?;
|
||||
Ok(fire_due(&state, now))
|
||||
});
|
||||
|
||||
ffi!(me_mark_fired, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let state = state_of(&doc)?.clone();
|
||||
let trigger = match doc.get("trigger") {
|
||||
Some(Json::Str(t)) => t.clone(),
|
||||
_ => return Err(("trigger must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let now = now_of(&doc)?;
|
||||
Ok(mark_fired(&state, &trigger, now))
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Tests: meaningful_events.py Rust core port (dichotomy wave, #2479).
|
||||
|
||||
Frozen-truth battery: the 96 frozen-oracle cases replayed VERBATIM
|
||||
through the facade in BOTH core modes (RUNE_ME_CORE=python floor vs the
|
||||
Rust kernel), plus loader pin discipline, the fire→mark→quiet loop and
|
||||
input-copy semantics.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "src" / "rune"))
|
||||
|
||||
import meaningful_events as me # noqa: E402
|
||||
from rune import me_loader # noqa: E402
|
||||
|
||||
TRUTH = json.loads(
|
||||
(ROOT / "src" / "rust" / "meaningful_events" / "frozen_truth.json")
|
||||
.read_text())
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def core_built():
|
||||
if not me_loader.available():
|
||||
pytest.skip("rust meaningful_events core not built")
|
||||
return True
|
||||
|
||||
|
||||
def drive(mode, fn, *args):
|
||||
os.environ["RUNE_ME_CORE"] = mode
|
||||
try:
|
||||
return fn(*copy.deepcopy(args))
|
||||
finally:
|
||||
os.environ["RUNE_ME_CORE"] = "auto"
|
||||
|
||||
|
||||
def test_loader_probe_and_pin(core_built, monkeypatch):
|
||||
monkeypatch.delenv("RUNE_ME_CORE", raising=False)
|
||||
assert me_loader.available()
|
||||
assert me._kernel() is not None
|
||||
monkeypatch.setenv("RUNE_ME_CORE", "python")
|
||||
assert me._kernel() is None
|
||||
|
||||
|
||||
def test_frozen_should_fire_both_ways(core_built):
|
||||
for c in TRUTH["should_fire_cases"]:
|
||||
args = (c["args"]["trigger"], c["args"]["state"], c["args"]["now"])
|
||||
py = drive("python", me.should_fire, *args)
|
||||
rs = drive("auto", me.should_fire, *args)
|
||||
assert py == rs == c["expect"], c["name"]
|
||||
|
||||
|
||||
def test_frozen_fire_due_both_ways(core_built):
|
||||
for c in TRUTH["fire_due_cases"]:
|
||||
args = (c["args"]["state"], c["args"]["now"])
|
||||
py = drive("python", me.fire_due, *args)
|
||||
rs = drive("auto", me.fire_due, *args)
|
||||
assert py == rs == c["expect"], c["name"]
|
||||
|
||||
|
||||
def test_frozen_mark_fired_both_ways(core_built):
|
||||
for c in TRUTH["mark_fired_cases"]:
|
||||
args = (c["args"]["state"], c["args"]["trigger"], c["args"]["now"])
|
||||
py = drive("python", me.mark_fired, *args)
|
||||
rs = drive("auto", me.mark_fired, *args)
|
||||
assert py == rs == c["expect"], c["name"]
|
||||
assert c["input_unchanged"] is True, c["name"]
|
||||
|
||||
|
||||
def test_fire_mark_due_loop_both_ways(core_built):
|
||||
for mode in ("python", "auto"):
|
||||
state = {"task_blocked": True}
|
||||
due = drive(mode, me.fire_due, state, 1000.0)
|
||||
assert "task_blockers" in due
|
||||
state = drive(mode, me.mark_fired, state, "task_blockers", 1000.0)
|
||||
assert drive(mode, me.fire_due, state, 1000.0) == []
|
||||
|
||||
|
||||
def test_rate_limit_string_law(core_built):
|
||||
# int() truncation surfaces IN the reason string — verbatim law
|
||||
r = drive("auto", me.should_fire, "task_blockers",
|
||||
{"task_blocked": True, "last_fired_task_blockers": 400.5},
|
||||
1000.0)
|
||||
assert r == {"fires": False, "reason": "rate-limited (0s left)"}
|
||||
Reference in new issue
Block a user