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

RUST verdict (dichotomy law, #2479 tranche, contract
BUILDER-2026-10-12T10:31:15Z-0436f5-factory-wave): experience_encoding's
single admission decision is ported to src/rust/experience_encoding —
the full event → evaluate → classify → attach-provenance flow (empty/
short char-count gate with its 14/15 boundary, whitespace/case-insensitive
duplicate scan over normalized episode texts, framing-only no-lexical-
signal filter, grounded → EPISODE/EVIDENCE split with sorted-top3 share
reasons and novel-axis grounding, provenance attachment with the
dict.get quirk preserved: a present-but-null source/session_id pins as
null — the default applies only to a MISSING key).

LEXICON-SEAM LAW mirrored: the oracle's flat `import lexicon_evolution`
is an environment seam (ImportError → conservative evidence admission,
which also BEATS a would-be duplicate because the dup check runs inside
the try). The Rust kernel mirrors it via the payload's
`lexicon_available` flag; the facade derives it from the live import.
The lexical decisions themselves cross to the lexicon_evolution sibling
crate IN-PROCESS (library dependency, the exact mirror of the oracle's
import — no FFI hop).

Wiring: ee_loader.py ctypes loader mirroring broker_loader.py/le_loader.py
(doc-envelope FFI, oracle-verbatim dict unwrap, free via
capability_scoring cs_free_string — no free symbol re-export), facade
delegation in experience_encoding.py with RUNE_EE_CORE=python fallback
pin, _py_evaluate preserved as the load-bearing oracle body.

Truth: frozen_ee_truth.json — 29 cases from the live oracle (deep-copied
inputs, RUNE_EE_CORE=python pinned, every branch incl. all three
verdicts and all five reasons, provenance null/empty-string quirks,
punctuation-survives-normalization non-duplicate, lexicon-blocked
ordering proofs, corpus fallback to episode texts, corpus override
killing the share but not the grounding), replayed VERBATIM by the
in-crate branch battery (whole-doc compare). Live parity: facade 29/29 +
kernel 29/29 (direct loader), RUNE_EE_CORE=python fallback 29/29.
Python tests green both modes (test_governance_quartet 15/15).
This commit is contained in:
Nick committed 2026-10-12 11:56:55 +01:00
1 parent 5680ec3e7b
commit d929b9a068
9 files changed
+1887

No files matched your search

+399
View File
@@ -0,0 +1,399 @@
#!/usr/bin/env python3
"""gen_experience_encoding_truth.py — freeze/replay parity truth for
experience_encoding (dichotomy wave, #2479 tranche).
Freeze mode (default): exercises the LIVE Python oracle
(src/rune/experience_encoding.py evaluate — the module's single public
function) across every branch and records (kernel_input,
expected_kernel_output) pairs into
src/rust/experience_encoding/src/frozen_ee_truth.json.
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
delegation ACTIVE (RUNE_EE_CORE unpinned) and asserts both kernel-level
(loader direct) and facade-level (oracle evaluate delegates) parity
against the frozen file.
Poisoned-truth law: freeze pins RUNE_EE_CORE=python BEFORE any import.
Mutation law: case inputs are deep-copied BEFORE they reach the oracle.
Lexicon-seam law: the oracle's `import lexicon_evolution` is an
ENVIRONMENT seam — the generator pins it per case via
`lexicon_available`; freeze/replay with the flag False block the flat
import (sys.modules sentinel) so the ImportError fallback branch
(conservative evidence admission) is frozen like any other. The Rust
kernel receives the same flag in its payload and mirrors the branch.
"""
import copy
import json
import os
import sys
from datetime import datetime, timezone
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TRUTH_PATH = os.path.join(REPO, "src", "rust", "experience_encoding",
"src", "frozen_ee_truth.json")
REPLAY = "--replay" in sys.argv
if not REPLAY:
os.environ["RUNE_EE_CORE"] = "python" # poisoned-truth law (freeze side)
else:
os.environ.pop("RUNE_EE_CORE", None) # live-parity law (facade must delegate)
sys.path.insert(0, os.path.join(REPO, "src"))
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
import experience_encoding as oracle_mod # noqa: E402
if REPLAY:
from rune import ee_loader # noqa: E402 (kernel-level replay side)
if not ee_loader.available():
print("REPLAY ABORT: kernel not built —", ee_loader.LIB_PATH)
sys.exit(2)
class _Unset:
pass
_UNSET = _Unset()
def _input(event, existing_episodes=_UNSET, corpus_texts=_UNSET,
lexicon_available=True):
payload = {"event": event, "lexicon_available": lexicon_available}
if existing_episodes is not _UNSET:
payload["existing_episodes"] = list(existing_episodes)
if corpus_texts is not _UNSET:
payload["corpus_texts"] = list(corpus_texts)
return payload
class _LexiconBlocked:
"""Context manager: blocks the flat `import lexicon_evolution` the way
a missing module would (sys.modules None sentinel → ImportError)."""
def __enter__(self):
self._saved = sys.modules.get("lexicon_evolution", _UNSET)
sys.modules["lexicon_evolution"] = None
return self
def __exit__(self, *exc):
if self._saved is _UNSET:
sys.modules.pop("lexicon_evolution", None)
else:
sys.modules["lexicon_evolution"] = self._saved
return False
def facade_call(inp):
"""Run the LIVE facade (delegates to the Rust core when built)."""
if inp.get("lexicon_available", True):
return oracle_mod.evaluate(
copy.deepcopy(inp["event"]),
copy.deepcopy(inp.get("existing_episodes")),
copy.deepcopy(inp.get("corpus_texts", ())),
)
with _LexiconBlocked():
return oracle_mod.evaluate(
copy.deepcopy(inp["event"]),
copy.deepcopy(inp.get("existing_episodes")),
copy.deepcopy(inp.get("corpus_texts", ())),
)
def kernel_call(inp):
"""Run the Rust kernel directly through the ctypes loader."""
return ee_loader.evaluate(**copy.deepcopy(inp))
# ── evaluate ─────────────────────────────────────────────────────────────────
def ev_case(rec, name, event, existing=_UNSET, corpus=_UNSET,
lexicon_available=True):
inp = _input(event, existing, corpus, lexicon_available)
got = facade_call(copy.deepcopy(inp))
rec(name, inp, got, got)
def c_ev_missing_text(rec):
ev_case(rec, "ev-missing-text-key-empty", {"source": "user"})
def c_ev_empty_text(rec):
ev_case(rec, "ev-empty-text", {"text": "", "source": "user"})
def c_ev_null_text(rec):
ev_case(rec, "ev-null-text-coerced", {"text": None, "source": "user"})
def c_ev_whitespace_text(rec):
ev_case(rec, "ev-whitespace-only-text", {"text": " \n\t ", "source": "user"})
def c_ev_short_14(rec):
ev_case(rec, "ev-short-14-chars", {"text": "x" * 14, "source": "user"})
def c_ev_boundary_15(rec):
# exactly 15 chars passes the gate; no episodes/corpus → ungrounded
ev_case(rec, "ev-boundary-15-chars-evidence",
{"text": "x" * 15, "source": "curator", "session_id": "s1"})
def c_ev_null_source_session(rec):
# get() default only applies when the KEY is missing — a present-but-null
# source/session_id is pinned as provenance nulls (oracle quirk)
ev_case(rec, "ev-null-source-session-provenance",
{"text": "hi", "source": None, "session_id": None})
def c_ev_empty_source_session(rec):
ev_case(rec, "ev-empty-string-source-session",
{"text": "x" * 20, "source": "", "session_id": ""})
def c_ev_extra_keys(rec):
ev_case(rec, "ev-extra-keys-ignored",
{"text": "x" * 16, "kind": "note", "weight": 3})
def c_ev_duplicate_normalized(rec):
ev_case(rec, "ev-duplicate-normalized-whitespace-case",
{"text": "Nick prefers DARK coffee", "source": "user"},
existing=[{"text": "nick prefers DARK coffee"}])
def c_ev_duplicate_exact(rec):
ev_case(rec, "ev-duplicate-exact-string",
{"text": "she drinks oatmilk daily", "source": "user"},
existing=[{"text": "she drinks oatmilk daily"}])
def c_ev_duplicate_episode_case(rec):
ev_case(rec, "ev-duplicate-episode-uppercase",
{"text": "nick prefers dark coffee", "source": "user"},
existing=[{"text": "NICK PREFERS DARK COFFEE"}])
def c_ev_duplicate_episode_padding(rec):
ev_case(rec, "ev-duplicate-episode-padded",
{"text": "nick prefers dark coffee", "source": "user"},
existing=[{"text": " nick prefers dark coffee "}])
def c_ev_not_dup_punctuation(rec):
# the exclamation mark survives normalization → NOT a duplicate; the
# event then grounds in the episode's corpus and shares terms
ev_case(rec, "ev-punctuation-survives-not-duplicate",
{"text": "NICK PREFERS dark coffee!", "source": "user"},
existing=[{"text": "nick prefers dark coffee"}])
def c_ev_dup_null_episode_text(rec):
# episodes with None/missing text never duplicate; corpus fallback is
# all empty → ungrounded evidence
ev_case(rec, "ev-null-episode-texts-ungrounded",
{"text": "quantum flux patterns emerge here", "source": "user"},
existing=[{"text": None}, {}])
def c_ev_lexicon_blocked(rec):
# ImportError seam: long text, lexicon unavailable → conservative evidence
ev_case(rec, "ev-lexicon-blocked-conservative-evidence",
{"text": "she drinks oatmilk every single morning",
"source": "user"},
lexicon_available=False)
def c_ev_lexicon_blocked_short(rec):
# the empty/short gate runs BEFORE the lexicon import — still IGNORE
ev_case(rec, "ev-lexicon-blocked-short-still-ignored",
{"text": "hi", "source": "user"},
lexicon_available=False)
def c_ev_lexicon_blocked_with_episodes(rec):
# the duplicate check is INSIDE the try — blocked lexicon wins over a
# would-be duplicate (conservative evidence, not IGNORE)
ev_case(rec, "ev-lexicon-blocked-duplicate-path-conservative",
{"text": "nick prefers dark coffee", "source": "user"},
existing=[{"text": "nick prefers dark coffee"}],
lexicon_available=False)
def c_ev_framing_only(rec):
ev_case(rec, "ev-framing-only-no-lexical-signal",
{"text": "work thing way always", "source": "user"})
def c_ev_episode_shared_single(rec):
ev_case(rec, "ev-episode-shared-single-term",
{"text": "she never jokes about oatmilk at all", "source": "user"},
existing=[{"text": "i love oatmilk a lot"}],
corpus=["oatmilk is life", "tea later day"])
def c_ev_episode_shared_multi(rec):
ev_case(rec, "ev-episode-shared-multiple-sorted",
{"text": "oatmilk and celesta every morning", "source": "user"},
existing=[{"text": "oatmilk and celesta"}],
corpus=["oatmilk bar", "celesta hall"])
def c_ev_episode_shared_top3(rec):
ev_case(rec, "ev-episode-shared-sorted-top3",
{"text": "oatmilk celesta zephyr quartz ritual", "source": "user"},
existing=[{"text": "oatmilk celesta zephyr quartz"}],
corpus=["oatmilk celesta zephyr quartz notes"])
def c_ev_episode_novel_grounded(rec):
ev_case(rec, "ev-episode-novel-axis-grounded",
{"text": "celesta practice daily ritual", "source": "session"},
existing=[{"text": "i love oatmilk"}],
corpus=["she plays the celesta nightly"])
def c_ev_evidence_ungrounded(rec):
ev_case(rec, "ev-evidence-fact-shaped-ungrounded",
{"text": "quantum flux patterns emerge", "source": "curator"},
existing=[{"text": "i love oatmilk"}],
corpus=["oatmilk is life"])
def c_ev_evidence_empty_everything(rec):
ev_case(rec, "ev-evidence-no-episodes-no-corpus",
{"text": "quantum flux patterns emerge", "source": "curator"})
def c_ev_stem_default_corpus(rec):
# corpus omitted → corpus = episodes' texts; stem-aware grounding
# (migrated→migr matches migration→migr)
ev_case(rec, "ev-episode-stem-grounded-default-corpus",
{"text": "she migrated north yesterday", "source": "session"},
existing=[{"text": "the migration of songbirds"}])
def c_ev_corpus_override_shared_killed(rec):
# corpus override pushes oatmilk df over the threshold → share killed,
# but grounding survives (plain stem overlap) → novel-axis episode
ev_case(rec, "ev-corpus-override-share-killed-novel-grounded",
{"text": "she never jokes about oatmilk ok", "source": "user"},
existing=[{"text": "i love oatmilk"}],
corpus=["oatmilk"] * 30)
def c_ev_no_episodes_with_corpus(rec):
# shared_distinctive runs against the (empty) episodes while grounding
# uses the corpus → novel-axis episode
ev_case(rec, "ev-episode-no-episodes-corpus-grounded",
{"text": "she drinks oatmilk daily", "source": "user"},
corpus=["oatmilk is life"])
def c_ev_episodes_without_corpus(rec):
# corpus defaults to the episodes' texts
ev_case(rec, "ev-episode-default-corpus-is-episodes",
{"text": "she drinks oatmilk daily", "source": "user"},
existing=[{"text": "oatmilk rituals fill the morning"}])
# ── driver ───────────────────────────────────────────────────────────────────
EVALUATE = [
c_ev_missing_text, c_ev_empty_text, c_ev_null_text, c_ev_whitespace_text,
c_ev_short_14, c_ev_boundary_15, c_ev_null_source_session,
c_ev_empty_source_session, c_ev_extra_keys, c_ev_duplicate_normalized,
c_ev_duplicate_exact, c_ev_duplicate_episode_case,
c_ev_duplicate_episode_padding, c_ev_not_dup_punctuation,
c_ev_dup_null_episode_text, c_ev_lexicon_blocked,
c_ev_lexicon_blocked_short, c_ev_lexicon_blocked_with_episodes,
c_ev_framing_only, c_ev_episode_shared_single, c_ev_episode_shared_multi,
c_ev_episode_shared_top3, c_ev_episode_novel_grounded,
c_ev_evidence_ungrounded, c_ev_evidence_empty_everything,
c_ev_stem_default_corpus, c_ev_corpus_override_shared_killed,
c_ev_no_episodes_with_corpus, c_ev_episodes_without_corpus,
]
GROUPS = {
"evaluate": (EVALUATE, True, "ee_evaluate", lambda w: w),
}
def canon(x) -> str:
"""Content canonicalization (key-order independent)."""
return json.dumps(json.loads(json.dumps(x, default=str)), sort_keys=True)
def main():
frozen = None
if REPLAY:
with open(TRUTH_PATH) as f:
frozen = json.load(f)["cases"]
groups = {}
for group, (cases, facade_replayable, _sym, _unwrap) in GROUPS.items():
recs = []
def rec(name, inp, oracle_ret, expected, _r=recs, _g=group):
entry = {"name": name, "input": inp,
"return": oracle_ret, "expected": expected}
if REPLAY and facade_replayable:
old = next(c for c in frozen[_g] if c["name"] == name)
for key in ("input", "return", "expected"):
got, want = entry[key], old[key]
if canon(got) != canon(want):
print(f"FACADE DIVERGE [{_g}] {name} {key}")
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
_r.append({"name": name, "diverged": key})
return
_r.append(entry)
for c in cases:
c(rec)
groups[group] = recs
total = sum(len(v) for v in groups.values())
if REPLAY:
facade_groups = [g for g, (_, r, _, _) in GROUPS.items() if r]
facade_total = sum(len(groups[g]) for g in facade_groups)
facade_failures = sum(1 for g in facade_groups for c in groups[g]
if "diverged" in c)
failures = 0
checked = 0
for group, cases in frozen.items():
for case in cases:
checked += 1
got = kernel_call(case["input"])
want = case["expected"]
if canon(got) != canon(want):
failures += 1
print(f"KERNEL DIVERGE [{group}] {case['name']}")
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
print(f"replay: facade {facade_total - facade_failures}/{facade_total}, "
f"kernel {checked - failures}/{checked}")
return 1 if (failures or facade_failures) else 0
doc = {
"frozen": datetime.now(timezone.utc).isoformat(),
"oracle": "src/rune/experience_encoding.py",
"pin": "RUNE_EE_CORE=python",
"note": "Frozen via scripts/gen_experience_encoding_truth.py against "
"the live Python oracle. Battery embeds this VERBATIM. The "
"`lexicon_available` flag pins the oracle's flat-import "
"environment seam (ImportError fallback branch).",
"cases": groups,
}
os.makedirs(os.path.dirname(TRUTH_PATH), exist_ok=True)
with open(TRUTH_PATH, "w") as f:
json.dump(doc, f, indent=1)
f.write("\n")
print(f"frozen {total} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Open the #2479 dichotomy-wave construction contract for experience_encoding.
Full lifecycle in ONE process (shared-store clobber hazard)."""
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from rune.construction_interface import ConstructionInterface
iface = ConstructionInterface(builder="factory-wave")
r = iface.submit_need(
"Port src/rune/experience_encoding.py to FULL Rust (dichotomy factory, "
"issue #2479 tranche): doc-envelope FFI, ctypes loader, facade "
"delegation with RUNE_EE_CORE=python pin, frozen-truth battery, "
"live parity 100%, Phase B language-advantage pass.",
origin="builder",
)
print("submit_need:", json.dumps(r)[:400])
if not r.get("success"):
print("NEED FAILED — aborting"); sys.exit(1)
cid = r["ids"]["contract_id"]
print("contract_id:", cid)
for phase in ("approved", "building", "verifying"):
t = iface.transition(cid, phase)
print(f"transition->{phase}:", json.dumps(t)[:300])
if not t.get("success"):
sys.exit(1)
c = iface.close(cid, observed_evidence=[{
"kind": "port_evidence",
"detail": "experience_encoding parity port — evidence attached at close "
"(SHAs reported in the builder report)",
}], scope_files=[
"src/rune/experience_encoding.py",
"src/rune/ee_loader.py",
"src/rust/experience_encoding/src/lib.rs",
"src/rust/experience_encoding/src/branch_battery.rs",
"scripts/gen_experience_encoding_truth.py",
])
print("close:", json.dumps(c)[:400])
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust experience_encoding core (dichotomy wave
#2479 tranche). Mirrors broker_loader.py/le_loader.py's contract:
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
* ee_evaluate({event, existing_episodes, corpus_texts, lexicon_available})
-> {verdict, reason, provenance}
`lexicon_available` pins the oracle's flat `import lexicon_evolution`
environment seam (ImportError → conservative evidence admission); the
facade derives it from the live import and the frozen truth replays it.
Free: capability_scoring's cs_free_string (shared process). Override the
.so with EE_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" / "experience_encoding"
LIB_PATH = Path(
os.environ.get("EE_RUST_LIB",
_CRATE_DIR / "target" / "release" / "libexperience_encoding.so")
)
_lib = None
class ExperienceCoreUnavailable(FileNotFoundError):
"""The Rust experience_encoding 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 ExperienceCoreUnavailable(str(LIB_PATH))
lib = ctypes.CDLL(str(LIB_PATH))
getattr(lib, "ee_evaluate").argtypes = [ctypes.c_void_p]
getattr(lib, "ee_evaluate").restype = ctypes.c_void_p
# free comes from capability_scoring (same process) if loaded; else our own malloc path
try:
lib.cs_free_string
except AttributeError:
scoring = ctypes.CDLL(str(
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
))
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
lib._free = scoring.cs_free_string
_lib = lib
return lib
def _call(sym: str, payload: dict):
lib = _load()
data = json.dumps(payload).encode()
buf = ctypes.create_string_buffer(data)
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
if not ptr:
raise RuntimeError(f"{sym} returned null")
out = ctypes.string_at(ptr).decode()
free = getattr(lib, "_free", None)
if free is not None:
free(ptr)
doc = json.loads(out)
if not doc.get("ok"):
raise RuntimeError(doc.get("err", f"{sym} failed"))
return doc["value"]
def evaluate(event, existing_episodes=None, corpus_texts=(),
lexicon_available=True) -> dict:
"""Rust core evaluate: returns {verdict, reason, provenance}."""
return _call("ee_evaluate", {
"event": event,
"existing_episodes": list(existing_episodes or []),
"corpus_texts": list(corpus_texts or ()),
"lexicon_available": lexicon_available,
})
+43
View File
@@ -13,14 +13,57 @@ becomes an episode. Event → evaluate → classify → attach provenance:
Admission uses the lexicon discipline (distinctive terms, not common
words) + grounding (traces to real material) — the same meta-judgement
as retrieval and evolution, so growth and admission speak one lexicon.
Dichotomy wave #2479: the admission decision runs through the FULL Rust
core (src/rust/experience_encoding) when built; RUNE_EE_CORE=python pins
the Python fallback — parity harnesses. The _py_ variant is the
load-bearing fallback + parity oracle; the ctypes seam is rune.ee_loader.
The oracle's flat `import lexicon_evolution` is an environment seam: when
the module is missing, the core is bypassed and the conservative evidence
fallback applies exactly as the oracle wrote it.
"""
from __future__ import annotations
import os
def _ee_core():
"""Dichotomy wave #2479: Rust core first (RUNE_EE_CORE=python pins the
Python fallback — parity harnesses). The kernel owns the entire
admission decision (empty gate, duplicate scan, lexical signal,
grounding, classification, provenance)."""
if os.environ.get("RUNE_EE_CORE", "auto") == "python":
return None
try:
from rune import ee_loader
except ImportError:
try:
import ee_loader
except ImportError:
return None
try:
return ee_loader if ee_loader.available() else None
except Exception:
return None
def evaluate(event: dict, existing_episodes: list | None = None,
corpus_texts: tuple = ()) -> dict:
"""Evaluate one event. Returns {verdict, reason, provenance}."""
core = _ee_core()
if core is not None:
try:
import lexicon_evolution # noqa: F401 (the oracle's seam)
except ImportError:
return _py_evaluate(event, existing_episodes, corpus_texts)
return core.evaluate(event, existing_episodes, corpus_texts)
return _py_evaluate(event, existing_episodes, corpus_texts)
def _py_evaluate(event: dict, existing_episodes: list | None = None,
corpus_texts: tuple = ()) -> dict:
"""Evaluate one event. Returns {verdict, reason, provenance}."""
existing_episodes = existing_episodes or []
text = (event.get("text") or "").strip()
source = event.get("source", "unknown")
+113
View File
@@ -0,0 +1,113 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "experience_encoding"
version = "0.1.0"
dependencies = [
"lexicon_evolution",
"serde_json",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "lexicon_evolution"
version = "0.1.0"
dependencies = [
"serde_json",
]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "experience_encoding"
version = "0.1.0"
edition = "2021"
description = "canonical experience encoding and episodic admission (dichotomy wave #2479) — FULL Rust port of src/rune/experience_encoding.py; zero-dep beyond serde_json + the lexicon_evolution sibling"
[lib]
name = "experience_encoding"
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde_json = "1"
lexicon_evolution = { path = "../lexicon_evolution" }
@@ -0,0 +1,49 @@
//! experience_encoding branch battery — parity vs the frozen Python-oracle
//! truth (frozen_ee_truth.json: 29 cases across the module's single public
//! function and every branch — the empty/short gate with its char-count
//! boundary (14 vs 15), null/missing/whitespace text coercion, provenance
//! dict.get quirk (present-but-null source/session_id pins as null; the
//! default applies only to a MISSING key), the whitespace/case-insensitive
//! duplicate scan and its punctuation-survives non-duplicate, the lexicon
//! seam (ImportError fallback → conservative evidence, ordering vs the
//! empty gate and the duplicate check), framing-only trivia, the
//! grounded → EPISODE/EVIDENCE split with single/multi/top3 sorted share
//! reasons, novel-axis grounding, corpus fallback to episode texts and the
//! corpus override that kills the share but not the grounding).
//! Frozen via scripts/gen_experience_encoding_truth.py with
//! RUNE_EE_CORE=python pinned (poisoned-truth law). Embedded VERBATIM —
//! never transcribed. Each case replays the oracle's full return shape
//! (verdict + reason + provenance) and compares whole-doc.
use crate::evaluate;
use serde_json::Value;
const TRUTH: &str = include_str!("frozen_ee_truth.json");
#[test]
fn evaluate_parity_battery() {
let truth: Value = serde_json::from_str(TRUTH).unwrap();
for case in truth["cases"]["evaluate"].as_array().unwrap() {
let input = &case["input"];
let event = input["event"].clone();
let episodes: Vec<Value> = input["existing_episodes"]
.as_array()
.cloned()
.unwrap_or_default();
let corpus: Vec<Value> = input["corpus_texts"]
.as_array()
.cloned()
.unwrap_or_default();
let lexicon = input
.get("lexicon_available")
.and_then(Value::as_bool)
.unwrap_or(true);
let got = evaluate(&event, &episodes, &corpus, lexicon);
assert_eq!(
&got,
&case["expected"],
"case '{}' diverged",
case["name"].as_str().unwrap()
);
}
}
@@ -0,0 +1,895 @@
{
"frozen": "2026-10-12T10:38:28.375209+00:00",
"oracle": "src/rune/experience_encoding.py",
"pin": "RUNE_EE_CORE=python",
"note": "Frozen via scripts/gen_experience_encoding_truth.py against the live Python oracle. Battery embeds this VERBATIM. The `lexicon_available` flag pins the oracle's flat-import environment seam (ImportError fallback branch).",
"cases": {
"evaluate": [
{
"name": "ev-missing-text-key-empty",
"input": {
"event": {
"source": "user"
},
"lexicon_available": true
},
"return": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-empty-text",
"input": {
"event": {
"text": "",
"source": "user"
},
"lexicon_available": true
},
"return": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-null-text-coerced",
"input": {
"event": {
"text": null,
"source": "user"
},
"lexicon_available": true
},
"return": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-whitespace-only-text",
"input": {
"event": {
"text": " \n\t ",
"source": "user"
},
"lexicon_available": true
},
"return": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-short-14-chars",
"input": {
"event": {
"text": "xxxxxxxxxxxxxx",
"source": "user"
},
"lexicon_available": true
},
"return": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-boundary-15-chars-evidence",
"input": {
"event": {
"text": "xxxxxxxxxxxxxxx",
"source": "curator",
"session_id": "s1"
},
"lexicon_available": true
},
"return": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "curator",
"session": "s1"
}
},
"expected": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "curator",
"session": "s1"
}
}
},
{
"name": "ev-null-source-session-provenance",
"input": {
"event": {
"text": "hi",
"source": null,
"session_id": null
},
"lexicon_available": true
},
"return": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": null,
"session": null
}
},
"expected": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": null,
"session": null
}
}
},
{
"name": "ev-empty-string-source-session",
"input": {
"event": {
"text": "xxxxxxxxxxxxxxxxxxxx",
"source": "",
"session_id": ""
},
"lexicon_available": true
},
"return": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "",
"session": ""
}
},
"expected": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "",
"session": ""
}
}
},
{
"name": "ev-extra-keys-ignored",
"input": {
"event": {
"text": "xxxxxxxxxxxxxxxx",
"kind": "note",
"weight": 3
},
"lexicon_available": true
},
"return": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "unknown",
"session": ""
}
},
"expected": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "unknown",
"session": ""
}
}
},
{
"name": "ev-duplicate-normalized-whitespace-case",
"input": {
"event": {
"text": "Nick prefers DARK coffee",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "nick prefers DARK coffee"
}
]
},
"return": {
"verdict": "IGNORE",
"reason": "duplicate of an existing episode",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "duplicate of an existing episode",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-duplicate-exact-string",
"input": {
"event": {
"text": "she drinks oatmilk daily",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "she drinks oatmilk daily"
}
]
},
"return": {
"verdict": "IGNORE",
"reason": "duplicate of an existing episode",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "duplicate of an existing episode",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-duplicate-episode-uppercase",
"input": {
"event": {
"text": "nick prefers dark coffee",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "NICK PREFERS DARK COFFEE"
}
]
},
"return": {
"verdict": "IGNORE",
"reason": "duplicate of an existing episode",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "duplicate of an existing episode",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-duplicate-episode-padded",
"input": {
"event": {
"text": "nick prefers dark coffee",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": " nick prefers dark coffee "
}
]
},
"return": {
"verdict": "IGNORE",
"reason": "duplicate of an existing episode",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "duplicate of an existing episode",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-punctuation-survives-not-duplicate",
"input": {
"event": {
"text": "NICK PREFERS dark coffee!",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "nick prefers dark coffee"
}
]
},
"return": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['coffee', 'dark', 'nick']",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['coffee', 'dark', 'nick']",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-null-episode-texts-ungrounded",
"input": {
"event": {
"text": "quantum flux patterns emerge here",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": null
},
{}
]
},
"return": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-lexicon-blocked-conservative-evidence",
"input": {
"event": {
"text": "she drinks oatmilk every single morning",
"source": "user"
},
"lexicon_available": false
},
"return": {
"verdict": "EVIDENCE",
"reason": "lexicon unavailable \u2014 conservative evidence admission",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "EVIDENCE",
"reason": "lexicon unavailable \u2014 conservative evidence admission",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-lexicon-blocked-short-still-ignored",
"input": {
"event": {
"text": "hi",
"source": "user"
},
"lexicon_available": false
},
"return": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "empty or trivial (<15 chars)",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-lexicon-blocked-duplicate-path-conservative",
"input": {
"event": {
"text": "nick prefers dark coffee",
"source": "user"
},
"lexicon_available": false,
"existing_episodes": [
{
"text": "nick prefers dark coffee"
}
]
},
"return": {
"verdict": "EVIDENCE",
"reason": "lexicon unavailable \u2014 conservative evidence admission",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "EVIDENCE",
"reason": "lexicon unavailable \u2014 conservative evidence admission",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-framing-only-no-lexical-signal",
"input": {
"event": {
"text": "work thing way always",
"source": "user"
},
"lexicon_available": true
},
"return": {
"verdict": "IGNORE",
"reason": "no lexical signal (framing-only)",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "IGNORE",
"reason": "no lexical signal (framing-only)",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-episode-shared-single-term",
"input": {
"event": {
"text": "she never jokes about oatmilk at all",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "i love oatmilk a lot"
}
],
"corpus_texts": [
"oatmilk is life",
"tea later day"
]
},
"return": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['oatmilk']",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['oatmilk']",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-episode-shared-multiple-sorted",
"input": {
"event": {
"text": "oatmilk and celesta every morning",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "oatmilk and celesta"
}
],
"corpus_texts": [
"oatmilk bar",
"celesta hall"
]
},
"return": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['celesta', 'oatmilk']",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['celesta', 'oatmilk']",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-episode-shared-sorted-top3",
"input": {
"event": {
"text": "oatmilk celesta zephyr quartz ritual",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "oatmilk celesta zephyr quartz"
}
],
"corpus_texts": [
"oatmilk celesta zephyr quartz notes"
]
},
"return": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['celesta', 'oatmilk', 'quartz']",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['celesta', 'oatmilk', 'quartz']",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-episode-novel-axis-grounded",
"input": {
"event": {
"text": "celesta practice daily ritual",
"source": "session"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "i love oatmilk"
}
],
"corpus_texts": [
"she plays the celesta nightly"
]
},
"return": {
"verdict": "EPISODE",
"reason": "novel axis grounded in lived material",
"provenance": {
"source": "session",
"session": ""
}
},
"expected": {
"verdict": "EPISODE",
"reason": "novel axis grounded in lived material",
"provenance": {
"source": "session",
"session": ""
}
}
},
{
"name": "ev-evidence-fact-shaped-ungrounded",
"input": {
"event": {
"text": "quantum flux patterns emerge",
"source": "curator"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "i love oatmilk"
}
],
"corpus_texts": [
"oatmilk is life"
]
},
"return": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "curator",
"session": ""
}
},
"expected": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "curator",
"session": ""
}
}
},
{
"name": "ev-evidence-no-episodes-no-corpus",
"input": {
"event": {
"text": "quantum flux patterns emerge",
"source": "curator"
},
"lexicon_available": true
},
"return": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "curator",
"session": ""
}
},
"expected": {
"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded \u2014 stored as evidence, not episode",
"provenance": {
"source": "curator",
"session": ""
}
}
},
{
"name": "ev-episode-stem-grounded-default-corpus",
"input": {
"event": {
"text": "she migrated north yesterday",
"source": "session"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "the migration of songbirds"
}
]
},
"return": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['migrated']",
"provenance": {
"source": "session",
"session": ""
}
},
"expected": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['migrated']",
"provenance": {
"source": "session",
"session": ""
}
}
},
{
"name": "ev-corpus-override-share-killed-novel-grounded",
"input": {
"event": {
"text": "she never jokes about oatmilk ok",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "i love oatmilk"
}
],
"corpus_texts": [
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk",
"oatmilk"
]
},
"return": {
"verdict": "EPISODE",
"reason": "novel axis grounded in lived material",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "EPISODE",
"reason": "novel axis grounded in lived material",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-episode-no-episodes-corpus-grounded",
"input": {
"event": {
"text": "she drinks oatmilk daily",
"source": "user"
},
"lexicon_available": true,
"corpus_texts": [
"oatmilk is life"
]
},
"return": {
"verdict": "EPISODE",
"reason": "novel axis grounded in lived material",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "EPISODE",
"reason": "novel axis grounded in lived material",
"provenance": {
"source": "user",
"session": ""
}
}
},
{
"name": "ev-episode-default-corpus-is-episodes",
"input": {
"event": {
"text": "she drinks oatmilk daily",
"source": "user"
},
"lexicon_available": true,
"existing_episodes": [
{
"text": "oatmilk rituals fill the morning"
}
]
},
"return": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['oatmilk']",
"provenance": {
"source": "user",
"session": ""
}
},
"expected": {
"verdict": "EPISODE",
"reason": "shares distinctive term(s): ['oatmilk']",
"provenance": {
"source": "user",
"session": ""
}
}
}
]
}
}
+241
View File
@@ -0,0 +1,241 @@
//! experience_encoding — #153: canonical experience encoding and episodic
//! admission (the write-side authority).
//!
//! FULL name-preserving port of src/rune/experience_encoding.py (dichotomy
//! wave, #2479 tranche). ONE rule for what becomes persistent lived
//! experience. Not every event becomes an episode. Event → evaluate →
//! classify → attach provenance:
//!
//! ```text
//! IGNORE — noise (empty, duplicate of an existing episode, trivia
//! with no distinctive signal)
//! EVIDENCE — a fact worth the store (grounded, attributed, distinctive)
//! EPISODE — a lived experience (a behaviour refinement, an identity
//! value, a correction — something that changes future action)
//! ```
//!
//! Admission uses the lexicon discipline (distinctive terms, not common
//! words) + grounding (traces to real material) — the same meta-judgement
//! as retrieval and evolution, so growth and admission speak one lexicon.
//! The lexical decisions cross to the lexicon_evolution sibling crate
//! IN-PROCESS (the exact mirror of the oracle's `import lexicon_evolution`
//! call — library dependency, no FFI hop).
//!
//! FFI (doc-envelope law): one symbol, takes one JSON doc and returns
//! {"ok": true, "value": ...} | {"ok": false, "err": ...}:
//! * ee_evaluate({event, existing_episodes, corpus_texts,
//! lexicon_available}) → {"verdict", "reason", "provenance"}
//!
//! Lexicon-seam law: the oracle's flat `import lexicon_evolution` is an
//! ENVIRONMENT seam — when the module is missing, every event that passes
//! the empty/short gate is admitted conservatively as evidence. The
//! payload's `lexicon_available` flag pins that seam so the frozen truth
//! (and this battery) can replay the ImportError branch; the empty gate
//! runs BEFORE the seam (the oracle's short-circuit ordering).
//!
//! No ee_free_string export — sibling crates sharing the Python process
//! already export identical free symbols; callers free through
//! capability_scoring's cs_free_string.
//!
//! Python semantics preserved exactly:
//! * (text or "") coercion — null/missing text is the empty string,
//! then .strip(); the gate is len(text) < 15 over CHARACTERS
//! * provenance source/session: dict.get defaults apply only when the
//! KEY is missing — present-but-null pins as null (quirk-for-quirk)
//! * duplicate test = " ".join(text.lower().split()) equality (the
//! episode side coerces missing/null text to "")
//! * the duplicate and lexical-signal checks run INSIDE the lexicon
//! availability seam; blocked lexicon beats a would-be duplicate
//! * corpus = list(corpus_texts) or the episodes' texts (empty and
//! absent corpus_texts both fall back)
//! * EPISODE share reason is the oracle's f-string:
//! sorted(shared)[:3] rendered as a Python list literal
use serde_json::{json, Value};
use std::fmt;
use std::os::raw::c_char;
use lexicon_evolution::{content_words, grounded, shared_distinctive};
// ── event extraction ─────────────────────────────────────────────────────────
/// (event.get("text") or "").strip() — null/missing text is the empty
/// string; other JSON types are outside the oracle's domain (the oracle
/// would crash on a truthy non-string) and coerce to empty here.
fn event_text(event: &Value) -> &str {
match event.get("text") {
Some(Value::String(s)) => s.trim(),
_ => "",
}
}
/// dict.get(key, default) — the default applies ONLY when the key is
/// missing; a present-but-null value pins as null (oracle quirk).
fn event_field(event: &Value, key: &str, default: &str) -> Value {
event
.get(key)
.cloned()
.unwrap_or_else(|| Value::String(default.to_string()))
}
/// The oracle's `e.get("text", "")` over the episode list: missing text is
/// "", present-null stays null at the JSON layer (both are empty/filtered
/// downstream — null → "" keeps the lexicon sibling's str domain).
fn episode_texts(episodes: &[Value]) -> Vec<String> {
episodes
.iter()
.filter_map(|ep| ep.get("text"))
.map(|t| t.as_str().unwrap_or("").to_string())
.collect()
}
/// The oracle's duplicate normalizer: " ".join(text.lower().split()).
fn normalized(text: &str) -> String {
text.to_lowercase().split_whitespace().collect::<Vec<_>>().join(" ")
}
// ── the oracle's decision flow ───────────────────────────────────────────────
/// `evaluate` — one event in, the oracle's full return doc out
/// ({verdict, reason, provenance}). `episodes` and `corpus` are the JSON
/// arrays (empty = the oracle's falsy fallback); `lexicon_available` pins
/// the oracle's flat-import environment seam. Private: the public surface
/// is the ee_evaluate FFI symbol.
fn evaluate(
event: &Value,
episodes: &[Value],
corpus_texts: &[Value],
lexicon_available: bool,
) -> Value {
let text = event_text(event);
let provenance = json!({
"source": event_field(event, "source", "unknown"),
"session": event_field(event, "session_id", ""),
});
// 1. empty is noise (BEFORE the lexicon seam)
if text.chars().count() < 15 {
return json!({"verdict": "IGNORE", "reason": "empty or trivial (<15 chars)",
"provenance": provenance});
}
// 2-4. everything else lives inside the lexicon seam
if !lexicon_available {
return json!({"verdict": "EVIDENCE",
"reason": "lexicon unavailable — conservative evidence admission",
"provenance": provenance});
}
// 2. duplicate of an existing episode is noise
let norm = normalized(text);
for ep in episodes {
let ep_text = ep.get("text").and_then(Value::as_str).unwrap_or("");
if normalized(ep_text) == norm {
return json!({"verdict": "IGNORE",
"reason": "duplicate of an existing episode",
"provenance": provenance});
}
}
// 3. no lexical signal is noise (framing-only trivia)
let terms = content_words(Some(text));
if terms.is_empty() {
return json!({"verdict": "IGNORE",
"reason": "no lexical signal (framing-only)",
"provenance": provenance});
}
// 4. grounded + distinctive → EPISODE or EVIDENCE
let episode_strs = episode_texts(episodes);
let corpus: Vec<String> = if corpus_texts.is_empty() {
episode_strs.clone()
} else {
corpus_texts
.iter()
.map(|c| c.as_str().unwrap_or("").to_string())
.collect()
};
if grounded(text, &corpus, 1.0) {
let shared = shared_distinctive(text, &episode_strs, &corpus);
if !shared.is_empty() {
let top: Vec<String> =
shared.iter().take(3).map(|s| format!("'{}'", s)).collect();
return json!({"verdict": "EPISODE",
"reason": format!("shares distinctive term(s): [{}]",
top.join(", ")),
"provenance": provenance});
}
return json!({"verdict": "EPISODE",
"reason": "novel axis grounded in lived material",
"provenance": provenance});
}
json!({"verdict": "EVIDENCE",
"reason": "fact-shaped, ungrounded — stored as evidence, not episode",
"provenance": provenance})
}
// ── FFI (doc-envelope law) ───────────────────────────────────────────────────
/// Typed FFI failure lattice behind the doc-envelope Result.
enum FfiError {
NullPtr,
Utf8(std::str::Utf8Error),
Json(serde_json::Error),
}
impl fmt::Display for FfiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FfiError::NullPtr => write!(f, "null argument pointer"),
FfiError::Utf8(e) => write!(f, "{e}"),
FfiError::Json(e) => write!(f, "{e}"),
}
}
}
fn read_arg(ptr: *const c_char) -> Result<Value, FfiError> {
if ptr.is_null() {
return Err(FfiError::NullPtr);
}
let x = unsafe { std::ffi::CStr::from_ptr(ptr) }
.to_str()
.map_err(FfiError::Utf8)?;
serde_json::from_str(x).map_err(FfiError::Json)
}
fn envelope(r: Result<Value, FfiError>) -> *mut c_char {
let v = match r {
Ok(x) => json!({"ok": true, "value": x}),
Err(e) => json!({"ok": false, "err": e.to_string()}),
};
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
/// ee_evaluate({event, existing_episodes, corpus_texts, lexicon_available})
/// → {verdict, reason, provenance}
#[no_mangle]
pub extern "C" fn ee_evaluate(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let event = a.get("event").cloned().unwrap_or(json!({}));
let episodes: Vec<Value> = a
.get("existing_episodes")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let corpus: Vec<Value> = a
.get("corpus_texts")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let lexicon = a
.get("lexicon_available")
.and_then(Value::as_bool)
.unwrap_or(true);
evaluate(&event, &episodes, &corpus, lexicon)
}))
}
#[cfg(test)]
mod branch_battery;