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:
1 parent
53b788f3c6
commit
b704f87549
8 files changed
+2944
-7
No files matched your search
@@ -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())
|
||||
@@ -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),
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
Generated
+105
@@ -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"
|
||||
@@ -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
@@ -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;
|
||||
Reference in new issue
Block a user