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

Full name-preserving port of src/rune/lexicon_evolution.py to
src/rust/lexicon_evolution (RUST verdict, #2479 dichotomy tranche — 7/7 fns,
dichotomy audit match clean):
- content_words: regex-equivalent [a-z]{4,} scanner over the lowered text,
  stop/framing tables 1:1, (text or "") coercion
- distinctive_terms: stem-aware document frequency, df < max(total*0.05, 1.5)
- shared_distinctive: stem-matched share over raw existing values (None → "")
- coherence: conflict/consistent/novel lattice — negation is a SUBSTRING test
  on the lowered fact, the shared-term test is the padded " term " form
- grounded: conjunctive stem hits with the min_distinct counter
- _stem: suffix-rule table quirk-for-quirk (migrate→migrat vs migration→migr;
  retroactive→retroactiv vs retroactively→retroactive) + y→i fold
- promote_eligible: strength gate → empty block → distinctive share → no
  signal → grounded/parked, with the oracle's f-string reasons byte-for-byte
  (py_num_display: ints stay ints, integral floats keep .0)

FFI doc-envelope law ({ok, value|err}), no le_free_string export (siblings
share the process; callers free via capability_scoring cs_free_string).
rune/le_loader.py mirrors broker_loader.py; facade delegation with
RUNE_LE_CORE=python pin, _py_ variants kept verbatim as the load-bearing
fallback. Set-valued fns cross the FFI SORTED; the facade restores set types.

Parity: 76 frozen truth cases (every public fn, every branch) frozen via
scripts/gen_lexicon_evolution_truth.py against the live oracle with
RUNE_LE_CORE=python pinned; battery replays VERBATIM 7/7; live replay
facade 54/54 + kernel 76/76; clippy 0; test_lexicon_contract 15/15 both
modes (kernel active + pinned fallback); test_governance_quartet 15/15;
pre-existing failures in test_constitutional_adversarial (7F) and
test_property_invariants (collection) proven unrelated via stash-dance.
This commit is contained in:
Nick committed 2026-10-12 02:08:12 +01:00
1 parent 53b788f3c6
commit b704f87549
8 files changed
+2944 -7

No files matched your search

+619
View File
@@ -0,0 +1,619 @@
#!/usr/bin/env python3
"""gen_lexicon_evolution_truth.py — freeze/replay parity truth for
lexicon_evolution (dichotomy wave, #2479 tranche).
Freeze mode (default): exercises EVERY public function of the LIVE Python
oracle (src/rune/lexicon_evolution.py) across every branch and records
(kernel_input, expected_kernel_output) pairs into
src/rust/lexicon_evolution/src/frozen_le_truth.json.
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
delegation ACTIVE (RUNE_LE_CORE unpinned) and asserts both kernel-level
(loader direct) and facade-level (oracle functions delegate) parity
against the frozen file.
Poisoned-truth law: freeze pins RUNE_LE_CORE=python BEFORE any import.
Mutation law: case inputs are deep-copied BEFORE they reach the oracle —
the frozen input must be the pre-call input, not a post-call mutation.
Set law: the oracle's set-valued functions (distinctive_terms,
shared_distinctive) are frozen and compared as SORTED lists — Python set
iteration order is not stable across processes; the facade restores set
types on its public surface.
Kernel-only group: `_stem` is private to the oracle (no facade symbol);
its truth group is replayed kernel-side only (the Python _stem body is
frozen verbatim and covered by tests/test_lexicon_contract.py).
"""
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", "lexicon_evolution", "src", "frozen_le_truth.json")
REPLAY = "--replay" in sys.argv
if not REPLAY:
os.environ["RUNE_LE_CORE"] = "python" # poisoned-truth law (freeze side)
else:
os.environ.pop("RUNE_LE_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 lexicon_evolution as oracle_mod # noqa: E402
if REPLAY:
from rune import le_loader # noqa: E402 (kernel-level replay side)
if not le_loader.available():
print("REPLAY ABORT: kernel not built —", le_loader.LIB_PATH)
sys.exit(2)
class Case:
def __init__(self, name, fn):
self.name = name
self.fn = fn
class _Unset:
pass
_UNSET = _Unset()
def _input(text=_UNSET, word=_UNSET, fact=_UNSET, proposal=_UNSET,
corpus_texts=_UNSET, existing_values=_UNSET, source_texts=_UNSET,
min_distinct=_UNSET, min_strength=_UNSET, evidence_strength=_UNSET):
payload = {}
if text is not _UNSET:
payload["text"] = text
if word is not _UNSET:
payload["word"] = word
if fact is not _UNSET:
payload["fact"] = fact
if proposal is not _UNSET:
payload["proposal"] = proposal
if corpus_texts is not _UNSET:
payload["corpus_texts"] = list(corpus_texts)
if existing_values is not _UNSET:
payload["existing_values"] = list(existing_values)
if source_texts is not _UNSET:
payload["source_texts"] = list(source_texts)
if min_distinct is not _UNSET and min_distinct is not None:
payload["min_distinct"] = min_distinct
if min_strength is not _UNSET and min_strength is not None:
payload["min_strength"] = min_strength
if evidence_strength is not _UNSET:
payload["evidence_strength"] = evidence_strength
return payload
def sorted_or(x):
return sorted(x) if isinstance(x, (set, frozenset)) else x
def facade_call(group, inp):
"""Run the LIVE facade (delegates to the Rust core when built)."""
if group == "content_words":
return oracle_mod.content_words(inp["text"])
if group == "distinctive_terms":
return oracle_mod.distinctive_terms(inp["fact"], inp["corpus_texts"])
if group == "shared_distinctive":
return oracle_mod.shared_distinctive(
inp["fact"], inp["existing_values"], inp["corpus_texts"])
if group == "coherence":
return oracle_mod.coherence(
inp["fact"], inp["existing_values"], inp["corpus_texts"])
if group == "grounded":
if "min_distinct" in inp:
return oracle_mod.grounded(inp["fact"], inp["source_texts"],
min_distinct=inp["min_distinct"])
return oracle_mod.grounded(inp["fact"], inp["source_texts"])
if group == "promote_eligible":
return oracle_mod.promote_eligible(
inp["proposal"], inp["existing"], inp["evidence_strength"],
min_strength=inp.get("min_strength", 3),
corpus_texts=inp["corpus_texts"])
raise KeyError(group)
def kernel_call(group, inp):
"""Run the Rust kernel directly through the ctypes loader."""
return getattr(le_loader, group)(**copy.deepcopy(inp))
# ── content_words ────────────────────────────────────────────────────────────
def cw_case(rec, name, text):
inp = _input(text=text)
got = oracle_mod.content_words(copy.deepcopy(text))
rec(name, inp, got, {"words": got})
def c_cw_basic(rec):
cw_case(rec, "cw-basic-stop-framing-filtered",
"The quick brown fox jumps over the lazy dog")
def c_cw_none(rec):
cw_case(rec, "cw-none-coerced-empty", None)
def c_cw_zero(rec):
# falsy non-string → (text or "") → ""
cw_case(rec, "cw-zero-falsy-coerced-empty", 0)
def c_cw_empty(rec):
cw_case(rec, "cw-empty-string", "")
def c_cw_uppercase(rec):
cw_case(rec, "cw-uppercase-folded", "PREFERS Oatmilk AND Celesta")
def c_cw_short(rec):
cw_case(rec, "cw-short-words-dropped", "a an the cat dog ox")
def c_cw_framing(rec):
cw_case(rec, "cw-framing-words-dropped",
"work always way thing value identity persona")
def c_cw_punct(rec):
cw_case(rec, "cw-punctuation-splits", "co-ffee! 1234 espresso's brew.")
def c_cw_mixed(rec):
cw_case(rec, "cw-mixed-stop-framing", "she will make the work better each day")
# ── distinctive_terms ────────────────────────────────────────────────────────
def dt_case(rec, name, fact, corpus):
inp = _input(fact=fact, corpus_texts=corpus)
got = oracle_mod.distinctive_terms(copy.deepcopy(fact), copy.deepcopy(corpus))
rec(name, inp, got, {"terms": sorted(got)})
def c_dt_empty_fact(rec):
dt_case(rec, "dt-empty-fact", "", ["some text here"])
def c_dt_all_stop(rec):
dt_case(rec, "dt-fact-all-stop-framing", "the that this work",
["some text here"])
def c_dt_rare(rec):
dt_case(rec, "dt-rare-terms-distinctive", "she drinks oatmilk daily",
["oatmilk is life", "coffee today", "tea later"])
def c_dt_common(rec):
dt_case(rec, "dt-common-term-not-distinctive", "physics of black holes",
["physics paper one", "physics paper two", "more physics here",
"unrelated text"])
def _filler_corpus(target_word, doc_ids, n=40):
corpus = []
for i in range(n):
if i in doc_ids:
corpus.append(f"the {target_word} session notes")
else:
corpus.append("filler text here")
return corpus
def c_dt_boundary_not_distinctive(rec):
# df 2 at total 40 → threshold max(2.0, 1.5) = 2.0 → 2 < 2 is False
dt_case(rec, "dt-boundary-df2-not-distinctive",
"celesta zephyr quantum", _filler_corpus("celesta", {0, 1}))
def c_dt_boundary_distinctive(rec):
# df 1 at total 40 → 1 < 2.0 → distinctive
dt_case(rec, "dt-boundary-df1-distinctive",
"celesta zephyr", _filler_corpus("celesta", {0}))
def c_dt_boundary_df3(rec):
dt_case(rec, "dt-boundary-df3-not-distinctive",
"celesta zephyr quantum", _filler_corpus("quantum", {0, 1, 2}))
def c_dt_stem_df(rec):
# migrate/migration share the stem "migr" → df counts both docs
dt_case(rec, "dt-stem-aware-document-frequency", "migrate north soon",
["the migration was huge", "another migration happened", "calm day"])
def c_dt_empty_corpus(rec):
dt_case(rec, "dt-empty-corpus-all-distinctive", "oatmilk ritual", [])
def c_dt_case_fold(rec):
dt_case(rec, "dt-case-folding", "OATMILK Rituals", ["oatmilk bar"])
# ── shared_distinctive ───────────────────────────────────────────────────────
def sd_case(rec, name, fact, existing, corpus):
inp = _input(fact=fact, existing_values=existing, corpus_texts=corpus)
got = oracle_mod.shared_distinctive(
copy.deepcopy(fact), copy.deepcopy(existing), copy.deepcopy(corpus))
rec(name, inp, got, {"shared": sorted(got)})
def c_sd_basic(rec):
sd_case(rec, "sd-shared-distinctive-term", "she never jokes about oatmilk",
["i love oatmilk a lot"], ["oatmilk is life", "tea later"])
def c_sd_none_shared(rec):
sd_case(rec, "sd-nothing-shared", "celesta practice daily",
["i love oatmilk"], ["oatmilk is life", "tea later"])
def c_sd_distinct_empty(rec):
# framing-only fact → distinctive_terms empty → short-circuit
sd_case(rec, "sd-distinct-empty-short-circuit", "work always thing",
["whatever corpus text"], ["whatever corpus text"])
def c_sd_stem_shared(rec):
sd_case(rec, "sd-stem-aware-shared", "migration policy debate",
["we migrated north"], ["migration news", "calm"])
def c_sd_none_value(rec):
sd_case(rec, "sd-none-existing-value-filtered", "she never jokes about oatmilk",
[None, "i love oatmilk"], ["oatmilk is life", "tea later"])
def c_sd_empty_existing(rec):
sd_case(rec, "sd-empty-existing", "oatmilk mornings", [], ["oatmilk is life"])
# ── coherence ────────────────────────────────────────────────────────────────
def co_case(rec, name, fact, existing, corpus=None):
inp = _input(fact=fact, existing_values=existing,
corpus_texts=[] if corpus is None else corpus)
if corpus is None:
got = oracle_mod.coherence(copy.deepcopy(fact), copy.deepcopy(existing))
else:
got = oracle_mod.coherence(copy.deepcopy(fact), copy.deepcopy(existing),
copy.deepcopy(corpus))
rec(name, inp, got, {"verdict": got})
def c_co_empty_existing(rec):
co_case(rec, "co-empty-existing-novel", "oatmilk mornings", [])
def c_co_blank_existing(rec):
co_case(rec, "co-blank-existing-filtered-novel", "oatmilk mornings",
["", " ", None])
def c_co_conflict(rec):
co_case(rec, "co-negation-on-shared-distinctive-conflict",
"she never jokes about oatmilk", ["i love oatmilk"],
["oatmilk bar", "tea"])
def c_co_negation_off_shared(rec):
# negation present but the shared term is not a standalone word in the
# fact ("oatmilk-lover" has no trailing space) → consistent
co_case(rec, "co-negation-not-on-shared-term-consistent",
"oatmilk-lover never smiles", ["i love oatmilk"],
["oatmilk bar", "tea"])
def c_co_consistent(rec):
co_case(rec, "co-shared-distinctive-consistent",
"i enjoy oatmilk mornings", ["i love oatmilk"],
["oatmilk bar", "tea"])
def c_co_novel(rec):
co_case(rec, "co-nothing-shared-novel", "celesta practice daily",
["i love oatmilk"], ["oatmilk bar", "tea"])
def c_co_corpus_kills_shared(rec):
# corpus override drives oatmilk df over the threshold → shared empty → novel
co_case(rec, "co-corpus-override-kills-shared-novel",
"she never jokes about oatmilk", ["i love oatmilk"],
["oatmilk"] * 30)
def c_co_default_corpus(rec):
# corpus_texts omitted → corpus = the (filtered) existing values
co_case(rec, "co-default-corpus-is-values", "i enjoy oatmilk mornings",
["i love oatmilk"])
# ── grounded ─────────────────────────────────────────────────────────────────
def gr_case(rec, name, fact, sources, min_distinct=None):
inp = _input(fact=fact, source_texts=sources, min_distinct=min_distinct)
if min_distinct is None:
got = oracle_mod.grounded(copy.deepcopy(fact), copy.deepcopy(sources))
else:
got = oracle_mod.grounded(copy.deepcopy(fact), copy.deepcopy(sources),
min_distinct=min_distinct)
rec(name, inp, got, {"grounded": got})
def c_gr_no_srcs(rec):
gr_case(rec, "gr-empty-sources-false", "she drinks oatmilk daily", [])
def c_gr_blank_srcs(rec):
gr_case(rec, "gr-blank-sources-false", "she drinks oatmilk daily", ["", " "])
def c_gr_no_fact_terms(rec):
gr_case(rec, "gr-fact-no-content-words-false", "the work thing",
["oatmilk is life"])
def c_gr_direct(rec):
gr_case(rec, "gr-direct-word-hit", "she drinks oatmilk daily",
["oatmilk is life"])
def c_gr_stem(rec):
gr_case(rec, "gr-stem-aware-hit", "migrated north",
["the migration of songbirds"])
def c_gr_miss(rec):
gr_case(rec, "gr-no-stem-overlap-false", "quantum flux patterns",
["oatmilk life"])
def c_gr_min2_one_hit(rec):
gr_case(rec, "gr-min-distinct-2-one-hit-false", "she drinks oatmilk with celesta",
["oatmilk life"], min_distinct=2)
def c_gr_min2_two_hits(rec):
gr_case(rec, "gr-min-distinct-2-two-hits-true", "she drinks oatmilk with celesta",
["oatmilk and celesta life"], min_distinct=2)
def c_gr_case(rec):
gr_case(rec, "gr-source-case-folded", "she drinks oatmilk daily",
["OATMILK IS LIFE"])
# ── _stem (kernel-only: private in the oracle) ───────────────────────────────
STEM_WORDS = [
"migration", "migrated", "migrating", "migrate", "migrates",
"retroactively", "retroactive", "honesty", "ability", "abilities",
"running", "kindness", "cats", "dogs", "hope", "oatmilk", "rituals",
"nation", "national", "carefully", "careful", "berries",
]
def st_case(rec, name, word):
inp = _input(word=word)
got = oracle_mod._stem(copy.deepcopy(word))
rec(name, inp, got, {"stem": got})
def c_stem_table(rec):
for w in STEM_WORDS:
st_case(rec, f"stem-{w}", w)
# ── promote_eligible ─────────────────────────────────────────────────────────
def pe_case(rec, name, proposal, existing, strength, min_strength=None, corpus=()):
inp = _input(proposal=proposal, existing_values=existing,
evidence_strength=strength, min_strength=min_strength,
corpus_texts=corpus)
# the oracle/loader param is `existing` (the promote signature)
inp["existing"] = inp.pop("existing_values")
if min_strength is None:
got = oracle_mod.promote_eligible(copy.deepcopy(proposal),
copy.deepcopy(existing), strength)
else:
got = oracle_mod.promote_eligible(
copy.deepcopy(proposal), copy.deepcopy(existing), strength,
min_strength=min_strength, corpus_texts=copy.deepcopy(corpus))
rec(name, inp, got, {"eligible": got["eligible"], "reason": got["reason"]})
def c_pe_below_strength(rec):
pe_case(rec, "pe-strength-gate-first", "oatmilk mornings", ["i love oatmilk"],
2, min_strength=3, corpus=["oatmilk bar"])
def c_pe_float_strength(rec):
pe_case(rec, "pe-float-strength-message", "oatmilk mornings", ["i love oatmilk"],
2.5, min_strength=3, corpus=["oatmilk bar"])
def c_pe_empty_block(rec):
pe_case(rec, "pe-empty-block-first-principle", "oatmilk mornings", [], 3,
min_strength=3)
def c_pe_blank_block(rec):
pe_case(rec, "pe-blank-block-filtered-first-principle", "oatmilk mornings",
["", " "], 3, min_strength=3)
def c_pe_shared(rec):
pe_case(rec, "pe-shared-distinctive-eligible", "i genuinely love oatmilk rituals",
["i love oatmilk"], 3, min_strength=3,
corpus=["oatmilk bar", "tea later"])
def c_pe_shared_multi(rec):
pe_case(rec, "pe-shared-multiple-sorted-top3", "oatmilk celesta mornings",
["oatmilk and celesta"], 3, min_strength=3,
corpus=["oatmilk bar", "celesta hall"])
def c_pe_no_terms(rec):
pe_case(rec, "pe-framing-only-no-lexical-signal", "work always thing way",
["i love oatmilk"], 3, min_strength=3, corpus=["oatmilk bar"])
def c_pe_novel_grounded(rec):
pe_case(rec, "pe-novel-axis-grounded-eligible", "celesta practice daily",
["i love oatmilk"], 3, min_strength=3,
corpus=["she plays celesta nightly"])
def c_pe_novel_parked(rec):
pe_case(rec, "pe-novel-axis-not-grounded-parked", "celesta practice daily",
["i love oatmilk"], 3, min_strength=3, corpus=["oatmilk bar only"])
def c_pe_min_strength_one(rec):
pe_case(rec, "pe-min-strength-1-passes-gate", "oatmilk mornings", [], 1,
min_strength=1)
def c_pe_corpus_override_grounded(rec):
# corpus override pushes oatmilk df over the threshold → shared empty →
# novel axis, but the proposal still grounds in the overridden corpus
pe_case(rec, "pe-corpus-override-novel-grounded", "oatmilk rituals",
["i love oatmilk"], 3, min_strength=3,
corpus=["oatmilk here", "oatmilk there", "oatmilk everywhere"])
def c_pe_default_corpus(rec):
# corpus_texts omitted → corpus = existing values
pe_case(rec, "pe-default-corpus-is-existing", "oatmilk rituals",
["i love oatmilk"], 3, min_strength=3)
# ── driver ───────────────────────────────────────────────────────────────────
CONTENT_WORDS = [c_cw_basic, c_cw_none, c_cw_zero, c_cw_empty, c_cw_uppercase,
c_cw_short, c_cw_framing, c_cw_punct, c_cw_mixed]
DISTINCTIVE = [c_dt_empty_fact, c_dt_all_stop, c_dt_rare, c_dt_common,
c_dt_boundary_not_distinctive, c_dt_boundary_distinctive,
c_dt_boundary_df3, c_dt_stem_df, c_dt_empty_corpus, c_dt_case_fold]
SHARED = [c_sd_basic, c_sd_none_shared, c_sd_distinct_empty, c_sd_stem_shared,
c_sd_none_value, c_sd_empty_existing]
COHERENCE = [c_co_empty_existing, c_co_blank_existing, c_co_conflict,
c_co_negation_off_shared, c_co_consistent, c_co_novel,
c_co_corpus_kills_shared, c_co_default_corpus]
GROUNDED = [c_gr_no_srcs, c_gr_blank_srcs, c_gr_no_fact_terms, c_gr_direct,
c_gr_stem, c_gr_miss, c_gr_min2_one_hit, c_gr_min2_two_hits,
c_gr_case]
STEM = [c_stem_table]
PROMOTE = [c_pe_below_strength, c_pe_float_strength, c_pe_empty_block,
c_pe_blank_block, c_pe_shared, c_pe_shared_multi, c_pe_no_terms,
c_pe_novel_grounded, c_pe_novel_parked, c_pe_min_strength_one,
c_pe_corpus_override_grounded, c_pe_default_corpus]
# group → (cases, facade_replayable, kernel_symbol, unwrap of expected)
GROUPS = {
"content_words": (CONTENT_WORDS, True, "le_content_words",
lambda w: w["words"]),
"distinctive_terms": (DISTINCTIVE, True, "le_distinctive_terms",
lambda w: w["terms"]),
"shared_distinctive": (SHARED, True, "le_shared_distinctive",
lambda w: w["shared"]),
"coherence": (COHERENCE, True, "le_coherence", lambda w: w["verdict"]),
"grounded": (GROUNDED, True, "le_grounded", lambda w: w["grounded"]),
"stem": (STEM, False, "le_stem", lambda w: w["stem"]),
"promote_eligible": (PROMOTE, True, "le_promote_eligible", 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):
entry = {"name": name, "input": inp,
"return": sorted_or(oracle_ret) if group in
("distinctive_terms", "shared_distinctive")
else oracle_ret,
"expected": expected}
if REPLAY and facade_replayable:
old = next(c for c in frozen[group] 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 [{group}] {name} {key}")
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
recs.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():
_sym = GROUPS[group][2]
unwrap = GROUPS[group][3]
for case in cases:
checked += 1
got = kernel_call(group, case["input"])
want = unwrap(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/lexicon_evolution.py",
"pin": "RUNE_LE_CORE=python",
"note": "Frozen via scripts/gen_lexicon_evolution_truth.py against the "
"live Python oracle. Battery embeds this VERBATIM. Group "
"`stem` is kernel-only (_stem is private in the oracle).",
"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())
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust lexicon_evolution core (dichotomy wave
#2479 tranche). Mirrors broker_loader.py's contract:
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
* le_content_words({text}) -> words list
* le_distinctive_terms({fact, corpus_texts}) -> sorted terms list
* le_shared_distinctive({fact, existing_values, corpus_texts}) -> sorted shared list
* le_coherence({fact, existing_values, corpus_texts}) -> verdict str
* le_grounded({fact, source_texts, min_distinct}) -> bool
* le_stem({word}) -> stem str
* le_promote_eligible({proposal, existing, evidence_strength,
min_strength, corpus_texts}) -> {eligible, reason}
Free: capability_scoring's cs_free_string (shared process). Override the
.so with LE_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" / "lexicon_evolution"
LIB_PATH = Path(
os.environ.get("LE_RUST_LIB", _CRATE_DIR / "target" / "release" / "liblexicon_evolution.so")
)
_lib = None
class LexiconUnavailable(FileNotFoundError):
"""The Rust lexicon 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 LexiconUnavailable(str(LIB_PATH))
lib = ctypes.CDLL(str(LIB_PATH))
for sym in ("le_content_words", "le_distinctive_terms", "le_shared_distinctive",
"le_coherence", "le_grounded", "le_stem", "le_promote_eligible"):
getattr(lib, sym).argtypes = [ctypes.c_void_p]
getattr(lib, sym).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 content_words(text) -> list:
return _call("le_content_words", {"text": text})["words"]
def distinctive_terms(fact, corpus_texts) -> list:
return _call("le_distinctive_terms", {
"fact": fact, "corpus_texts": list(corpus_texts),
})["terms"]
def shared_distinctive(fact, existing_values, corpus_texts) -> list:
return _call("le_shared_distinctive", {
"fact": fact, "existing_values": list(existing_values),
"corpus_texts": list(corpus_texts),
})["shared"]
def coherence(fact, existing_values, corpus_texts=()) -> str:
return _call("le_coherence", {
"fact": fact, "existing_values": list(existing_values),
"corpus_texts": list(corpus_texts),
})["verdict"]
def grounded(fact, source_texts, min_distinct=1) -> bool:
return _call("le_grounded", {
"fact": fact, "source_texts": list(source_texts),
"min_distinct": min_distinct,
})["grounded"]
def stem(word) -> str:
return _call("le_stem", {"word": word})["stem"]
def promote_eligible(proposal, existing, evidence_strength, min_strength=3,
corpus_texts=()) -> dict:
return _call("le_promote_eligible", {
"proposal": proposal, "existing": list(existing),
"evidence_strength": evidence_strength, "min_strength": min_strength,
"corpus_texts": list(corpus_texts),
})
+82 -7
View File
@@ -36,6 +36,11 @@ Decisions:
Usage:
from lexicon_evolution import coherence, grounded, promote_eligible
Dichotomy wave #2479: the pure lexical decisions run through the FULL Rust
core (src/rust/lexicon_evolution) when built; RUNE_LE_CORE=python pins the
Python fallback — parity harnesses. The _py_ variants are the load-bearing
fallback + parity oracle; the ctypes seam is rune.le_loader.
"""
import os
@@ -67,7 +72,34 @@ _NEGATION = {"not", "never", "don't", "doesn't", "cannot", "can't", "no",
_DISTINCTIVE_DF = 0.05 # same rare-word threshold as recall/associations
def _le_core():
"""Dichotomy wave #2479: Rust core first (RUNE_LE_CORE=python pins the
Python fallback — parity harnesses). The kernel owns the entire pure
lexical decision surface (tokenize, stem, distinctiveness, verdicts)."""
if os.environ.get("RUNE_LE_CORE", "auto") == "python":
return None
try:
from rune import le_loader
except ImportError:
try:
import le_loader
except ImportError:
return None
try:
return le_loader if le_loader.available() else None
except Exception:
return None
def content_words(text):
"""Significant content words (length > 3, not stop/framing)."""
core = _le_core()
if core is not None:
return core.content_words(text)
return _py_content_words(text)
def _py_content_words(text):
"""Significant content words (length > 3, not stop/framing)."""
return [w for w in re.findall(r"[a-z]{4,}", (text or "").lower())
if w not in _STOP and w not in _FRAMING]
@@ -78,8 +110,16 @@ def distinctive_terms(fact, corpus_texts):
Same signal as the recall gate: a term common in the store ("black",
"physics") is NOT a relation; a rare term ("celesta", "oatmilk") is."""
core = _le_core()
if core is not None:
return set(core.distinctive_terms(fact, corpus_texts))
return _py_distinctive_terms(fact, corpus_texts)
def _py_distinctive_terms(fact, corpus_texts):
"""The terms in `fact` that are RARE across the corpus (< 5% df)."""
total = max(len(corpus_texts), 1)
fact_terms = set(content_words(fact))
fact_terms = set(_py_content_words(fact))
if not fact_terms:
return set()
# stem-aware document frequency (migrate/migration count as one term)
@@ -94,7 +134,15 @@ def distinctive_terms(fact, corpus_texts):
def shared_distinctive(fact, existing_values, corpus_texts):
"""Distinctive terms shared between `fact` and existing values."""
distinct = distinctive_terms(fact, corpus_texts)
core = _le_core()
if core is not None:
return set(core.shared_distinctive(fact, existing_values, corpus_texts))
return _py_shared_distinctive(fact, existing_values, corpus_texts)
def _py_shared_distinctive(fact, existing_values, corpus_texts):
"""Distinctive terms shared between `fact` and existing values."""
distinct = _py_distinctive_terms(fact, corpus_texts)
if not distinct:
return set()
shared = set()
@@ -114,11 +162,19 @@ def coherence(fact, existing_values, corpus_texts=()):
Returns "conflict" | "consistent" | "novel".
"""
core = _le_core()
if core is not None:
return core.coherence(fact, existing_values, corpus_texts)
return _py_coherence(fact, existing_values, corpus_texts)
def _py_coherence(fact, existing_values, corpus_texts=()):
"""Classify a new value against existing ones, lexicon-aware."""
values = [v for v in existing_values if v and v.strip()]
if not values:
return "novel"
corpus = corpus_texts or values
shared = shared_distinctive(fact, values, corpus)
shared = _py_shared_distinctive(fact, values, corpus)
if not shared:
# No distinctive shared term: the value is a NEW axis. This is NOT a
# contradiction (a shared common word like "work" is meaningless), and
@@ -143,10 +199,18 @@ def grounded(fact, source_texts, min_distinct=1):
using a light suffix fold — exact word identity is too strict for natural
sources.
"""
core = _le_core()
if core is not None:
return core.grounded(fact, source_texts, min_distinct)
return _py_grounded(fact, source_texts, min_distinct)
def _py_grounded(fact, source_texts, min_distinct=1):
"""Is `fact` grounded in real material via a distinctive shared term?"""
srcs = [s for s in source_texts if s and s.strip()]
if not srcs:
return False
fact_terms = set(content_words(fact))
fact_terms = set(_py_content_words(fact))
if not fact_terms:
return False
joined = " ".join(s.lower() for s in srcs)
@@ -205,6 +269,17 @@ def promote_eligible(proposal, existing, evidence_strength, min_strength=3,
real absorbed material (the corpus), so arbitrary trivia ("pizza on
fridays") cannot be promoted as a principle no matter the sighting count.
"""
core = _le_core()
if core is not None:
return core.promote_eligible(proposal, existing, evidence_strength,
min_strength, corpus_texts)
return _py_promote_eligible(proposal, existing, evidence_strength,
min_strength, corpus_texts)
def _py_promote_eligible(proposal, existing, evidence_strength, min_strength=3,
corpus_texts=()):
"""Is a persona/constitution promotion eligible, lexicon-aware?"""
if evidence_strength < min_strength:
return {"eligible": False,
"reason": f"evidence strength {evidence_strength} < {min_strength}"}
@@ -212,17 +287,17 @@ def promote_eligible(proposal, existing, evidence_strength, min_strength=3,
if not existing:
return {"eligible": True, "reason": "empty block — first principle"}
corpus = corpus_texts or existing
shared = shared_distinctive(proposal, existing, corpus)
shared = _py_shared_distinctive(proposal, existing, corpus)
if shared:
return {"eligible": True,
"reason": f"shares distinctive term(s): {sorted(shared)[:3]}"}
# No distinctive shared term: a NOVEL axis is eligible only if it is
# GROUNDED in the absorbed corpus via a distinctive term (a real principle
# names something real), and the proposal is not trivial framing.
terms = content_words(proposal)
terms = _py_content_words(proposal)
if not terms:
return {"eligible": False, "reason": "no lexical signal"}
if grounded(proposal, corpus, min_distinct=1):
if _py_grounded(proposal, corpus, min_distinct=1):
return {"eligible": True,
"reason": f"novel axis grounded in corpus ({len(terms)} terms)"}
return {"eligible": False,
+105
View File
@@ -0,0 +1,105 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[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.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "lexicon_evolution"
version = "0.1.0"
edition = "2021"
description = "lexicon meta-judgement for evolution (dichotomy wave #2479) — FULL Rust port of src/rune/lexicon_evolution.py; zero-dep beyond serde_json"
[lib]
name = "lexicon_evolution"
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde_json = "1"
@@ -0,0 +1,141 @@
//! lexicon_evolution branch battery — parity vs the frozen Python-oracle
//! truth (frozen_le_truth.json: 76 cases across every public function and
//! every branch — content-word scanning (regex-equivalent [a-z]{4,} runs,
//! stop/framing tables, null/falsy coercion, punctuation splits), the
//! distinctiveness df threshold with its max(total*0.05, 1.5) boundary
//! (df 1/2/3 at total 40) and stem-aware document frequency, shared-
//! distinctive short-circuits and None-value filtering, the coherence
//! verdict lattice (conflict needs negation ON the padded shared term;
//! corpus override killing the share → novel), grounding with stem hits
//! and the min_distinct counter, the _stem suffix-rule table quirk-for-
//! quirk (migrate→migrat vs migration→migr; retroactive→retroactiv vs
//! retroactively→retroactive), and promote_eligible's strength gate →
//! empty block → distinctive share → no signal → grounded/parked lattice
//! with the oracle's f-string reasons byte-for-byte).
//! Frozen via scripts/gen_lexicon_evolution_truth.py with
//! RUNE_LE_CORE=python pinned (poisoned-truth law). Embedded VERBATIM —
//! never transcribed. The `stem` group is kernel-only (_stem is private
//! in the oracle).
use crate::{
coherence, content_words, distinctive_terms, grounded, promote_eligible,
shared_distinctive, stem,
};
use serde_json::{json, Value};
const TRUTH: &str = include_str!("frozen_le_truth.json");
fn cases(group: &str) -> Vec<Value> {
let truth: Value = serde_json::from_str(TRUTH).unwrap();
truth["cases"][group].as_array().unwrap().clone()
}
fn replay(group: &str, f: impl Fn(&Value) -> Value) {
for case in cases(group) {
let got = f(&case["input"]);
assert_eq!(
&got,
&case["expected"],
"[{}] case '{}' diverged",
group,
case["name"].as_str().unwrap()
);
}
}
#[test]
fn content_words_parity_battery() {
replay("content_words", |i| {
json!({"words": content_words(i["text"].as_str())})
});
}
#[test]
fn distinctive_terms_parity_battery() {
replay("distinctive_terms", |i| {
let fact = i["fact"].as_str().unwrap_or("");
let corpus: Vec<String> = i["corpus_texts"]
.as_array()
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
.unwrap_or_default();
let terms: Vec<String> = distinctive_terms(fact, &corpus).into_iter().collect();
json!({"terms": terms})
});
}
#[test]
fn shared_distinctive_parity_battery() {
replay("shared_distinctive", |i| {
let fact = i["fact"].as_str().unwrap_or("");
let strs = |k: &str| -> Vec<String> {
i[k].as_array()
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
.unwrap_or_default()
};
let existing = strs("existing_values");
let corpus = strs("corpus_texts");
let shared: Vec<String> =
shared_distinctive(fact, &existing, &corpus).into_iter().collect();
json!({"shared": shared})
});
}
#[test]
fn coherence_parity_battery() {
replay("coherence", |i| {
let strs = |k: &str| -> Vec<String> {
i[k].as_array()
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
.unwrap_or_default()
};
let verdict = coherence(
i["fact"].as_str().unwrap_or(""),
&strs("existing_values"),
&strs("corpus_texts"),
);
json!({"verdict": verdict})
});
}
#[test]
fn grounded_parity_battery() {
replay("grounded", |i| {
let sources: Vec<String> = i["source_texts"]
.as_array()
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
.unwrap_or_default();
let min = i.get("min_distinct").and_then(Value::as_f64).unwrap_or(1.0);
json!({"grounded": grounded(i["fact"].as_str().unwrap_or(""), &sources, min)})
});
}
#[test]
fn stem_parity_battery() {
replay("stem", |i| {
json!({"stem": stem(i["word"].as_str().unwrap_or(""))})
});
}
#[test]
fn promote_eligible_parity_battery() {
replay("promote_eligible", |i| {
let existing: Vec<String> = i["existing"]
.as_array()
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
.unwrap_or_default();
let corpus: Vec<String> = i["corpus_texts"]
.as_array()
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
.unwrap_or_default();
let strength = i.get("evidence_strength").cloned().unwrap_or(json!(0));
let min = i.get("min_strength").cloned().unwrap_or(json!(3));
let (eligible, reason) = promote_eligible(
i["proposal"].as_str().unwrap_or(""),
&existing,
&strength,
&min,
&corpus,
);
json!({"eligible": eligible, "reason": reason})
});
}
File diff suppressed because it is too large. Load diff
+556
View File
@@ -0,0 +1,556 @@
//! lexicon_evolution — the lexicon meta-judgement applied to EVOLUTION.
//!
//! FULL name-preserving port of src/rune/lexicon_evolution.py (dichotomy
//! wave, #2479 tranche). Retrieval already applies the meta-judgement ("a
//! shared word is not a relation; a shared DISTINCTIVE word is a signal"):
//! conjunctive matching, rare-word gating, calibrated floors. Evolution
//! decisions (persona coherence, identity grounding, constitution
//! promotion) must use the SAME lexicon discipline — otherwise a
//! personality can "evolve" on a coincidental shared word exactly the way
//! recall used to misfire on "black coffee" vs "black holes".
//!
//! This module provides the shared lexical signal for the curator's three
//! growth paths, using identical definitions to the recall gate (same
//! stopword set, same distinctiveness threshold), so growth and retrieval
//! speak one lexicon.
//!
//! Decisions:
//! coherence(fact, existing_values) → "consistent" | "conflict" | "novel"
//! conflict: shares a DISTINCTIVE term AND a negation marker on it
//! (a real contradiction, e.g. "never jokes" vs "dry humor").
//! consistent: shares a distinctive term without negation (reinforces).
//! novel: shares nothing distinctive — a new, additive axis.
//! A shared COMMON word ("work", "always", "way") is never a relation.
//! grounded(fact, source_texts) → bool
//! Does the fact trace to real absorbed material via a DISTINCTIVE
//! shared term (conjunctive), not just vector proximity?
//! promote_eligible(proposal, existing, evidence_strength, min_strength)
//! Constitution/persona promotion is eligible only when BOTH hold:
//! the evidence threshold AND (distinctive share OR novel axis
//! grounded in the corpus).
//!
//! FFI (doc-envelope law): every symbol takes one JSON doc and returns
//! {"ok": true, "value": ...} | {"ok": false, "err": ...}:
//! * le_content_words({text}) → {"words": [...]}
//! * le_distinctive_terms({fact, corpus_texts}) → {"terms": [...sorted]}
//! * le_shared_distinctive({fact, existing_values, corpus_texts})
//! → {"shared": [...sorted]}
//! * le_coherence({fact, existing_values, corpus_texts}) → {"verdict"}
//! * le_grounded({fact, source_texts, min_distinct}) → {"grounded": bool}
//! * le_stem({word}) → {"stem"}
//! * le_promote_eligible({proposal, existing, evidence_strength,
//! min_strength, corpus_texts})
//! → {"eligible": bool, "reason": str}
//!
//! Set law: the oracle's set-valued functions are serialized SORTED —
//! Python set iteration order is not stable, and sorted() is the frozen
//! canonical form (the Python facade restores set types on its surface).
//!
//! No le_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:
//! * word scan = re.findall(r"[a-z]{4,}", text.lower())
//! * (text or "") coercion — null/missing text is the empty string
//! * `_stem` suffix rules in the oracle's exact order (longest relevant
//! first, fold char appended after stripping) + the y→i fold
//! (len > 4, prev char not a vowel) — quirk-for-quirk: migrate→
//! "migrat" but migration→"migr", retroactive→"retroactiv" but
//! retroactively→"retroactive"
//! * distinctiveness df < max(total * 0.05, 1.5), stem-aware doc freq
//! * negation is a SUBSTRING test on the lowered fact; the shared-term
//! test is the padded " term " form
//! * promote_eligible reason strings are the oracle's f-strings
//! byte-for-byte (int/float formatting per Python repr rules)
use serde_json::{json, Value};
use std::collections::{BTreeSet, HashMap};
use std::fmt;
use std::os::raw::c_char;
// SAME stopword set as memory_store._STOPWORDS (single source of truth
// would be ideal; keep in sync — see memory_store.py).
pub const STOP: &[&str] = &[
"the", "and", "for", "with", "that", "this", "from", "into", "when",
"then", "were", "have", "been", "will", "was", "are", "but", "not",
"you", "your", "also", "its", "his", "her", "him", "over", "under",
"them", "they", "there", "about", "after", "what", "which", "who",
"how", "why", "where", "while", "just", "more", "each", "than", "then",
"very", "such", "some", "only", "prefers", "needs", "wants", "likes",
];
// Hard stop for evolution: these generic framing words never carry a
// relation ("work", "always", "way") — a persona built on them would be a
// coincidence.
pub const FRAMING: &[&str] = &[
"work", "always", "way", "thing", "value", "identity", "persona",
"someone", "something", "never", "always", "before", "after", "then",
"when", "while", "must", "should", "will", "would", "could", "can",
"have", "has", "had", "been", "being", "used", "use", "uses", "make",
"makes", "made", "keep", "keeps", "take", "takes", "took", "get",
"gets", "got", "going",
];
pub const NEGATION: &[&str] = &[
"not", "never", "don't", "doesn't", "cannot", "can't", "no", "nor",
"neither", "without", "avoid", "reject", "against", "refuses",
"refuse", "opposed",
];
// same rare-word threshold as recall/associations
pub const DISTINCTIVE_DF: f64 = 0.05;
// ── tokenization ─────────────────────────────────────────────────────────────
/// Runs of [a-z]{4,} over the lowered text (regex-free scanner; non-ASCII
/// chars are separators exactly like the oracle's character class).
fn word_tokens(text: &str) -> Vec<String> {
let low = text.to_lowercase();
let mut out = Vec::new();
let mut cur = String::new();
for ch in low.chars() {
if ch.is_ascii_lowercase() {
cur.push(ch);
} else {
if cur.chars().count() >= 4 {
out.push(std::mem::take(&mut cur));
}
cur.clear();
}
}
if cur.chars().count() >= 4 {
out.push(cur);
}
out
}
/// `content_words` — significant content words (length > 3, not
/// stop/framing).
pub fn content_words(text: Option<&str>) -> Vec<String> {
match text {
None => Vec::new(),
Some(t) => word_tokens(t)
.into_iter()
.filter(|w| !STOP.contains(&w.as_str()) && !FRAMING.contains(&w.as_str()))
.collect(),
}
}
// ── morphology ───────────────────────────────────────────────────────────────
// (suffix, optional extra fold applied after stripping) — the oracle's
// exact rule order; rules are grouped so the 5-char participle forms
// ('ating', 'ated') are stripped before their 3-char base ('ing', 'ed'),
// keeping migrate/migrating/migrated/migration on one root.
const STEM_RULES: &[(&str, &str)] = &[
("ationally", ""),
("ableness", ""),
("iveness", ""),
("ation", ""),
("ating", ""),
("ative", "e"),
("atively", "e"),
("ability", ""),
("ities", "y"),
("ingly", ""),
("fully", ""),
("ness", ""),
("ment", ""),
("itive", "e"),
("atable", "e"),
("ated", ""),
("ates", "e"),
("ions", ""),
("ings", ""),
("able", "e"),
("ible", "e"),
("ing", "e"),
("ed", ""),
("es", ""),
("ly", ""),
("s", ""),
("e", ""),
];
/// `_stem` — light morphological fold for grounding: drop common English
/// suffixes. Enough to equate migrate/migration and honest/honesty
/// without a full stemmer (no external deps).
pub fn stem(w: &str) -> String {
let mut w = w.to_string();
for (suf, fold) in STEM_RULES {
let sl = suf.chars().count();
if w.chars().count().saturating_sub(sl) >= 4 && w.ends_with(suf) {
let cut = w.len() - suf.len();
w.truncate(cut);
if !fold.is_empty() {
w.push_str(fold);
}
break;
}
}
// y -> i fold (honesty -> honest)
let chars: Vec<char> = w.chars().collect();
if chars.len() > 4
&& chars[chars.len() - 1] == 'y'
&& !"aeiouy".contains(chars[chars.len() - 2])
{
w.pop();
}
w
}
// ── distinctiveness ──────────────────────────────────────────────────────────
/// `distinctive_terms` — the terms in `fact` that are RARE across the
/// corpus (< 5% df), stem-aware (migrate/migration count as one term).
pub fn distinctive_terms(fact: &str, corpus_texts: &[String]) -> BTreeSet<String> {
let total = std::cmp::max(corpus_texts.len(), 1) as f64;
let fact_terms: BTreeSet<String> = content_words(Some(fact)).into_iter().collect();
if fact_terms.is_empty() {
return BTreeSet::new();
}
let mut doc_terms: HashMap<String, u64> = HashMap::new();
for t in corpus_texts {
let mut stems: BTreeSet<String> = BTreeSet::new();
for w in word_tokens(t) {
stems.insert(stem(&w));
}
for s in stems {
*doc_terms.entry(s).or_insert(0) += 1;
}
}
let threshold = (total * DISTINCTIVE_DF).max(1.5);
fact_terms
.into_iter()
.filter(|t| {
let df = doc_terms.get(&stem(t)).copied().unwrap_or(0) as f64;
df < threshold
})
.collect()
}
/// `shared_distinctive` — distinctive terms shared between `fact` and
/// existing values. `existing_values` is RAW (null/blank entries are the
/// oracle's `val or ""`).
pub fn shared_distinctive(
fact: &str,
existing_values: &[String],
corpus_texts: &[String],
) -> BTreeSet<String> {
let distinct = distinctive_terms(fact, corpus_texts);
if distinct.is_empty() {
return BTreeSet::new();
}
let existing_stems: Vec<String> = existing_values
.iter()
.flat_map(|val| {
word_tokens(val)
.into_iter()
.map(|w| stem(&w))
.collect::<Vec<_>>()
})
.collect();
let mut shared = BTreeSet::new();
for term in &distinct {
let ts = stem(term);
if existing_stems.contains(&ts) {
shared.insert(term.clone());
}
}
shared
}
// ── verdicts ─────────────────────────────────────────────────────────────────
/// `coherence` — classify a new value against existing ones, lexicon-aware:
/// "conflict" | "consistent" | "novel".
pub fn coherence(fact: &str, existing_values: &[String], corpus_texts: &[String]) -> &'static str {
let values: Vec<&str> = existing_values
.iter()
.map(String::as_str)
.filter(|v| !v.trim().is_empty())
.collect();
if values.is_empty() {
return "novel";
}
let corpus: Vec<&str> = if corpus_texts.is_empty() {
values.clone()
} else {
corpus_texts.iter().map(String::as_str).collect()
};
let borrowed_existing: Vec<String> = values.iter().map(|s| s.to_string()).collect();
let borrowed_corpus: Vec<String> = corpus.iter().map(|s| s.to_string()).collect();
let shared = shared_distinctive(fact, &borrowed_existing, &borrowed_corpus);
if shared.is_empty() {
// No distinctive shared term: the value is a NEW axis. This is NOT
// a contradiction (a shared common word like "work" is
// meaningless), and NOT a reinforcement — it adds independently.
return "novel";
}
let low = fact.to_lowercase();
let has_neg = NEGATION.iter().any(|n| low.contains(n));
if has_neg
&& shared
.iter()
.any(|t| format!(" {} ", low).contains(&format!(" {} ", t)))
{
return "conflict";
}
"consistent"
}
/// `grounded` — is `fact` grounded in real material via a distinctive
/// shared term? Morphology-aware (stem appears in the sources).
pub fn grounded(fact: &str, source_texts: &[String], min_distinct: f64) -> bool {
let srcs: Vec<&str> = source_texts
.iter()
.map(String::as_str)
.filter(|s| !s.trim().is_empty())
.collect();
if srcs.is_empty() {
return false;
}
let fact_terms: BTreeSet<String> = content_words(Some(fact)).into_iter().collect();
if fact_terms.is_empty() {
return false;
}
let joined = srcs.iter().map(|s| s.to_lowercase()).collect::<Vec<_>>().join(" ");
let stems: BTreeSet<String> = word_tokens(&joined).into_iter().map(|t| stem(&t)).collect();
let mut hits = 0.0f64;
for t in &fact_terms {
if stems.contains(&stem(t)) {
hits += 1.0;
if hits >= min_distinct {
return true;
}
}
}
false
}
/// Python number display for f-string interpolation: ints stay ints
/// (`2`), integral floats keep their `.0` (`2.0`), other floats use the
/// shortest round-trip repr (`2.5`) — matches the oracle's f"{x}" for all
/// values in the frozen-truth domain.
fn py_num_display(n: &Value) -> String {
let num = match n.as_number() {
Some(x) => x,
None => return String::new(),
};
if let Some(i) = num.as_i64() {
return i.to_string();
}
if let Some(u) = num.as_u64() {
return u.to_string();
}
let f = num.as_f64().unwrap_or(0.0);
if f == f.trunc() && f.abs() < 1e16 {
format!("{:.1}", f)
} else {
format!("{}", f)
}
}
/// `promote_eligible` — is a persona/constitution promotion eligible,
/// lexicon-aware? Eligibility = evidence threshold AND (shares a
/// distinctive term with the existing block OR is a genuinely novel axis
/// grounded in the corpus).
pub fn promote_eligible(
proposal: &str,
existing: &[String],
evidence_strength: &Value,
min_strength: &Value,
corpus_texts: &[String],
) -> (bool, String) {
if evidence_strength.as_f64().unwrap_or(0.0) < min_strength.as_f64().unwrap_or(3.0) {
return (
false,
format!(
"evidence strength {} < {}",
py_num_display(evidence_strength),
py_num_display(min_strength)
),
);
}
let existing: Vec<&str> = existing
.iter()
.map(String::as_str)
.filter(|e| !e.trim().is_empty())
.collect();
if existing.is_empty() {
return (true, "empty block — first principle".to_string());
}
let corpus: Vec<&str> = if corpus_texts.is_empty() {
existing.clone()
} else {
corpus_texts.iter().map(String::as_str).collect()
};
let existing_owned: Vec<String> = existing.iter().map(|s| s.to_string()).collect();
let corpus_owned: Vec<String> = corpus.iter().map(|s| s.to_string()).collect();
let shared = shared_distinctive(proposal, &existing_owned, &corpus_owned);
if !shared.is_empty() {
let top: Vec<String> = shared.iter().take(3).map(|s| format!("'{}'", s)).collect();
return (
true,
format!("shares distinctive term(s): [{}]", top.join(", ")),
);
}
// No distinctive shared term: a NOVEL axis is eligible only if it is
// GROUNDED in the absorbed corpus via a distinctive term (a real
// principle names something real), and the proposal is not trivial
// framing.
let terms = content_words(Some(proposal));
if terms.is_empty() {
return (false, "no lexical signal".to_string());
}
if grounded(proposal, &corpus_owned, 1.0) {
return (
true,
format!("novel axis grounded in corpus ({} terms)", terms.len()),
);
}
(
false,
"novel axis not grounded in absorbed material — parked".to_string(),
)
}
// ── 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())
}
/// JSON string-or-null → Option<&str> (the oracle's `text or ""` sites).
fn as_text(v: Option<&Value>) -> Option<&str> {
v.and_then(Value::as_str)
}
/// JSON array of strings-or-null → Vec<String> (nulls become "").
fn str_list(a: &Value, key: &str) -> Vec<String> {
a.get(key)
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.map(|v| v.as_str().unwrap_or("").to_string())
.collect()
})
.unwrap_or_default()
}
/// le_content_words({text}) → {words}
#[no_mangle]
pub extern "C" fn le_content_words(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let words = content_words(as_text(a.get("text")));
json!({"words": words})
}))
}
/// le_distinctive_terms({fact, corpus_texts}) → {terms}
#[no_mangle]
pub extern "C" fn le_distinctive_terms(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let fact = as_text(a.get("fact")).unwrap_or("");
let corpus = str_list(&a, "corpus_texts");
let terms: Vec<String> = distinctive_terms(fact, &corpus).into_iter().collect();
json!({"terms": terms})
}))
}
/// le_shared_distinctive({fact, existing_values, corpus_texts}) → {shared}
#[no_mangle]
pub extern "C" fn le_shared_distinctive(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let fact = as_text(a.get("fact")).unwrap_or("");
let existing = str_list(&a, "existing_values");
let corpus = str_list(&a, "corpus_texts");
let shared: Vec<String> =
shared_distinctive(fact, &existing, &corpus).into_iter().collect();
json!({"shared": shared})
}))
}
/// le_coherence({fact, existing_values, corpus_texts}) → {verdict}
#[no_mangle]
pub extern "C" fn le_coherence(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let fact = as_text(a.get("fact")).unwrap_or("");
let existing = str_list(&a, "existing_values");
let corpus = str_list(&a, "corpus_texts");
json!({"verdict": coherence(fact, &existing, &corpus)})
}))
}
/// le_grounded({fact, source_texts, min_distinct}) → {grounded}
#[no_mangle]
pub extern "C" fn le_grounded(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let fact = as_text(a.get("fact")).unwrap_or("");
let sources = str_list(&a, "source_texts");
let min_distinct = a.get("min_distinct").and_then(Value::as_f64).unwrap_or(1.0);
json!({"grounded": grounded(fact, &sources, min_distinct)})
}))
}
/// le_stem({word}) → {stem}
#[no_mangle]
pub extern "C" fn le_stem(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let word = as_text(a.get("word")).unwrap_or("");
json!({"stem": stem(word)})
}))
}
/// le_promote_eligible({proposal, existing, evidence_strength,
/// min_strength, corpus_texts}) → {eligible, reason}
#[no_mangle]
pub extern "C" fn le_promote_eligible(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let proposal = as_text(a.get("proposal")).unwrap_or("");
let existing = str_list(&a, "existing");
let corpus = str_list(&a, "corpus_texts");
let strength = a.get("evidence_strength").cloned().unwrap_or(json!(0));
let min = a.get("min_strength").cloned().unwrap_or(json!(3));
let (eligible, reason) =
promote_eligible(proposal, &existing, &strength, &min, &corpus);
json!({"eligible": eligible, "reason": reason})
}))
}
#[cfg(test)]
mod branch_battery;