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:
1 parent
5680ec3e7b
commit
d929b9a068
9 files changed
+1887
No files matched your search
@@ -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")
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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")
|
||||
|
||||
Generated
+113
@@ -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"
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user