feat(self_model): port to FULL Rust — dichotomy wave
This commit is contained in:
1 parent
cf7a10f6d6
commit
dac4889c0b
9 files changed
+1215
No files matched your search
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_self_model_truth.py — freeze the self_model parity oracle (dichotomy
|
||||
wave #2479 tranche).
|
||||
|
||||
Generator law: RUNE_SMM_CORE=python pinned, inputs deep-copied, every
|
||||
public function and branch exercised (estimate_tokens incl. unicode and
|
||||
sub/over boundaries; compile_self_model incl. empty corpus, exact-fit
|
||||
boundary, evidence overflow, key defaults, stable weight ties, zero and
|
||||
negative budgets, small-weight ordering, default budget; why_question incl.
|
||||
statement match, evidence match, case-insensitivity, empty topic, empty
|
||||
model). Output: src/rust/self_model/frozen_sm_truth.json — replayed
|
||||
VERBATIM by the crate's cfg(test) battery.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "src" / "rune"))
|
||||
|
||||
os.environ["RUNE_SMM_CORE"] = "python"
|
||||
|
||||
import self_model as sm # noqa: E402
|
||||
|
||||
cases: list = []
|
||||
|
||||
|
||||
def freeze(fn, args):
|
||||
args = copy.deepcopy(args)
|
||||
f = getattr(sm, fn)
|
||||
expected = f(**copy.deepcopy(args))
|
||||
cases.append({"fn": fn, "args": args, "expected": expected})
|
||||
|
||||
|
||||
# --- estimate_tokens: every boundary ---
|
||||
for text in [
|
||||
"", # max(1, 0) -> 1
|
||||
"ab", # 0 // 4 -> 1
|
||||
"abcd", # exactly 1
|
||||
"abcde", # 1
|
||||
"a" * 8, # 2
|
||||
"a" * 9, # 2
|
||||
"a" * 12, # 3
|
||||
"hello — em-dash unicode", # char count, not bytes
|
||||
"line one\nline two\nline three",
|
||||
]:
|
||||
freeze("estimate_tokens", {"text": text})
|
||||
|
||||
# --- compile_self_model: every branch ---
|
||||
freeze("compile_self_model", {"corpus": []}) # empty, default budget
|
||||
freeze("compile_self_model", { # single fits
|
||||
"corpus": [{"text": "I speak plainly and show my work.",
|
||||
"source": "persona.md", "weight": 0.9}],
|
||||
})
|
||||
freeze("compile_self_model", { # exact-fill boundary + overflow
|
||||
"corpus": [
|
||||
{"text": "a" * 16, "source": "s1", "weight": 2.0}, # tok 4
|
||||
{"text": "b" * 16, "source": "s2", "weight": 1.5}, # tok 4 -> used 8 == budget
|
||||
{"text": "c", "source": "s3", "weight": 1.0}, # tok 1 -> evidence
|
||||
],
|
||||
"budget_tokens": 8,
|
||||
})
|
||||
freeze("compile_self_model", { # missing keys -> defaults
|
||||
"corpus": [{"text": "no weight no source"}, {"source": "only-source"},
|
||||
{"weight": 0.1}],
|
||||
})
|
||||
freeze("compile_self_model", { # stable ties keep order
|
||||
"corpus": [
|
||||
{"text": "first", "source": "a", "weight": 1.0},
|
||||
{"text": "second", "source": "b", "weight": 1.0},
|
||||
{"text": "third", "source": "c", "weight": 1.0},
|
||||
],
|
||||
"budget_tokens": 3,
|
||||
})
|
||||
freeze("compile_self_model", {"corpus": [{"text": "x" * 40, "source": "big",
|
||||
"weight": 9.9}],
|
||||
"budget_tokens": 0}) # zero budget -> all evidence
|
||||
freeze("compile_self_model", {"corpus": [{"text": "x" * 40, "source": "big",
|
||||
"weight": 9.9}],
|
||||
"budget_tokens": -3}) # negative budget
|
||||
freeze("compile_self_model", { # small weights order round
|
||||
"corpus": [{"text": "quarter weight", "source": "q", "weight": 0.25},
|
||||
{"text": "three-quarter weight", "source": "t", "weight": 0.75}],
|
||||
"budget_tokens": 50,
|
||||
})
|
||||
freeze("compile_self_model", { # 219/220 boundary at default budget
|
||||
"corpus": [
|
||||
{"text": "w" * 700, "source": "big1", "weight": 3.0}, # tok 175
|
||||
{"text": "y" * 176, "source": "big2", "weight": 2.0}, # tok 44 -> 219
|
||||
{"text": "z" * 4, "source": "over", "weight": 1.0}, # tok 1 -> evidence
|
||||
],
|
||||
})
|
||||
freeze("compile_self_model", { # first item alone exceeds budget
|
||||
"corpus": [{"text": "v" * 100, "source": "huge", "weight": 5.0},
|
||||
{"text": "tiny", "source": "t", "weight": 0.4}],
|
||||
"budget_tokens": 10,
|
||||
})
|
||||
|
||||
compiled = sm.compile_self_model(copy.deepcopy([
|
||||
{"text": "I prefer evidence over vibes.", "source": "s1", "weight": 2.0},
|
||||
{"text": "CONTRADICTIONS get surfaced loudly.", "source": "s2", "weight": 1.5},
|
||||
{"text": "Growth is measured, not asserted.", "source": "s3", "weight": 1.0},
|
||||
{"text": "quiet evidence overflow note", "source": "s4", "weight": 0.1},
|
||||
]), budget_tokens=10)
|
||||
|
||||
# --- why_question: every branch ---
|
||||
freeze("why_question", {"model": compiled, "topic": "evidence"}) # statements + evidence hits
|
||||
freeze("why_question", {"model": compiled, "topic": "EVIDENCE"}) # case-insensitive both ways
|
||||
freeze("why_question", {"model": compiled, "topic": "growth"}) # single statement hit
|
||||
freeze("why_question", {"model": compiled, "topic": "absent-topic"}) # no hits
|
||||
freeze("why_question", {"model": compiled, "topic": ""}) # empty topic matches all
|
||||
freeze("why_question", {"model": {}, "topic": "anything"}) # empty model
|
||||
freeze("why_question", { # bare evidence-only model
|
||||
"model": {"evidence_ref": [{"text": "overflowed raw note",
|
||||
"source": "raw"}]},
|
||||
"topic": "raw",
|
||||
})
|
||||
|
||||
doc = {"module": "self_model", "core_env": "RUNE_SMM_CORE=python",
|
||||
"cases": cases}
|
||||
out = ROOT / "src" / "rust" / "self_model" / "frozen_sm_truth.json"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(doc, indent=1) + "\n")
|
||||
print(f"frozen {len(cases)} cases -> {out}")
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Open + walk the #2479 dichotomy-wave construction contract for
|
||||
self_model to verifying. Full lifecycle in ONE process."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
|
||||
iface = ConstructionInterface(builder="factory-batch-i")
|
||||
|
||||
r = iface.submit_need(
|
||||
"Complete the self_model dichotomy port to FULL Rust (#2479 "
|
||||
"tranche): the bounded persistent self-model kernel — "
|
||||
"estimate_tokens / compile_self_model (greedy weight-fill with "
|
||||
"stable tie ordering and evidence overflow) / why_question — as "
|
||||
"doc-envelope FFI (smm_* symbols, free via capability_scoring "
|
||||
"cs_free_string — no free re-export); smm_loader mirroring "
|
||||
"broker_loader.py; facade keeps the Python floor, RUNE_SMM_CORE="
|
||||
"python pin; frozen-oracle battery (every branch: empty corpus, "
|
||||
"exact-fit boundary, evidence overflow, key defaults, stable "
|
||||
"weight ties, zero budget, why-match across statements+evidence); "
|
||||
"live parity 100% (frozen + fuzz seeds); Phase B language-advantage "
|
||||
"pass (ASCII byte fast-path token estimate).",
|
||||
origin="builder",
|
||||
)
|
||||
print("submit_need:", json.dumps(r)[:300])
|
||||
if not r.get("success"):
|
||||
print("NEED FAILED — aborting"); sys.exit(1)
|
||||
cid = r["ids"]["contract_id"]
|
||||
print("contract_id:", cid)
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = iface.transition(cid, phase)
|
||||
print(f"transition->{phase}:", json.dumps(t)[:150])
|
||||
if not t.get("success"):
|
||||
sys.exit(1)
|
||||
print("CONTRACT", cid, "verifying")
|
||||
@@ -8,18 +8,47 @@ for 'why' questions, contradictions, and growth.
|
||||
Shape: {statements: [{text, source, weight}], budget_tokens, evidence_ref}.
|
||||
compile_self_model(corpus, budget): greedy by weight until the budget
|
||||
fills; the remainder stays in evidence (retrievable, not loaded).
|
||||
|
||||
Dichotomy wave #2479 tranche: the pure kernel (estimate_tokens /
|
||||
compile_self_model / why_question) is ported to FULL Rust
|
||||
(src/rust/self_model, parity oracle frozen_sm_truth.json) behind
|
||||
smm_loader; the Python implementation below is the strangler fallback,
|
||||
pinned with RUNE_SMM_CORE=python.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust self-model core when available (strangler: the Python
|
||||
floor below is load-bearing). RUNE_SMM_CORE=python pins it."""
|
||||
if os.environ.get("RUNE_SMM_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import smm_loader
|
||||
except Exception: # noqa: BLE001 — any loader failure means fallback
|
||||
return None
|
||||
try:
|
||||
return smm_loader if smm_loader.available() else None
|
||||
except Exception: # noqa: BLE001 — availability probe never crashes
|
||||
return None
|
||||
|
||||
|
||||
def estimate_tokens(text: str) -> int:
|
||||
"""Rough token estimate (the broker estimator's shape, local)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return int(core.estimate_tokens(text))
|
||||
return max(1, len(text) // 4)
|
||||
|
||||
|
||||
def compile_self_model(corpus: list, budget_tokens: int = 220) -> dict:
|
||||
"""Compile corpus [{text, source, weight}] into a budgeted self-model."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.compile_self_model(corpus, budget_tokens)
|
||||
ranked = sorted(corpus, key=lambda c: -c.get("weight", 0.5))
|
||||
statements, evidence, used = [], [], 0
|
||||
for c in ranked:
|
||||
@@ -37,6 +66,9 @@ def compile_self_model(corpus: list, budget_tokens: int = 220) -> dict:
|
||||
|
||||
def why_question(model: dict, topic: str) -> list:
|
||||
"""Retrieve the evidence behind a self-model statement ('why' path)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
return core.why_question(model, topic)
|
||||
out = []
|
||||
for s in model.get("statements", []) + model.get("evidence_ref", []):
|
||||
if topic.lower() in s.get("text", "").lower():
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust self_model core (dichotomy wave #2479
|
||||
tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* smm_estimate_tokens({"text"}) -> token count (int)
|
||||
* smm_compile_self_model({"corpus", "budget_tokens"?}) -> model dict
|
||||
* smm_why_question({"model", "topic"}) -> matches list
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with SMM_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" / "self_model"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get("SMM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libself_model.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
class SelfModelCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust self-model 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 SelfModelCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
for sym in ("smm_estimate_tokens", "smm_compile_self_model", "smm_why_question"):
|
||||
getattr(lib, sym).argtypes = [ctypes.c_void_p]
|
||||
getattr(lib, sym).restype = ctypes.c_void_p
|
||||
try:
|
||||
lib.cs_free_string
|
||||
except AttributeError:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib._free = scoring.cs_free_string
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
free = getattr(lib, "_free", None)
|
||||
if free is not None:
|
||||
free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
def estimate_tokens(text: str) -> int:
|
||||
return _call("smm_estimate_tokens", {"text": text})
|
||||
|
||||
def compile_self_model(corpus: list, budget_tokens: int = 220) -> dict:
|
||||
return _call("smm_compile_self_model", {"corpus": corpus,
|
||||
"budget_tokens": budget_tokens})
|
||||
|
||||
def why_question(model: dict, topic: str) -> list:
|
||||
return _call("smm_why_question", {"model": model, "topic": topic})
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "capability_scoring"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "self_model"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"capability_scoring",
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "self_model"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Bounded persistent self-model — dichotomy wave #2479 tranche; token estimate + greedy budgeted compile + why-retrieval in Rust; zero-dep (pyjson via capability_scoring)"
|
||||
|
||||
[lib]
|
||||
name = "self_model"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
capability_scoring = { path = "../capability_scoring" }
|
||||
@@ -0,0 +1,626 @@
|
||||
{
|
||||
"module": "self_model",
|
||||
"core_env": "RUNE_SMM_CORE=python",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "estimate_tokens",
|
||||
"args": {
|
||||
"text": ""
|
||||
},
|
||||
"expected": 1
|
||||
},
|
||||
{
|
||||
"fn": "estimate_tokens",
|
||||
"args": {
|
||||
"text": "ab"
|
||||
},
|
||||
"expected": 1
|
||||
},
|
||||
{
|
||||
"fn": "estimate_tokens",
|
||||
"args": {
|
||||
"text": "abcd"
|
||||
},
|
||||
"expected": 1
|
||||
},
|
||||
{
|
||||
"fn": "estimate_tokens",
|
||||
"args": {
|
||||
"text": "abcde"
|
||||
},
|
||||
"expected": 1
|
||||
},
|
||||
{
|
||||
"fn": "estimate_tokens",
|
||||
"args": {
|
||||
"text": "aaaaaaaa"
|
||||
},
|
||||
"expected": 2
|
||||
},
|
||||
{
|
||||
"fn": "estimate_tokens",
|
||||
"args": {
|
||||
"text": "aaaaaaaaa"
|
||||
},
|
||||
"expected": 2
|
||||
},
|
||||
{
|
||||
"fn": "estimate_tokens",
|
||||
"args": {
|
||||
"text": "aaaaaaaaaaaa"
|
||||
},
|
||||
"expected": 3
|
||||
},
|
||||
{
|
||||
"fn": "estimate_tokens",
|
||||
"args": {
|
||||
"text": "hello \u2014 em-dash unicode"
|
||||
},
|
||||
"expected": 5
|
||||
},
|
||||
{
|
||||
"fn": "estimate_tokens",
|
||||
"args": {
|
||||
"text": "line one\nline two\nline three"
|
||||
},
|
||||
"expected": 7
|
||||
},
|
||||
{
|
||||
"fn": "compile_self_model",
|
||||
"args": {
|
||||
"corpus": []
|
||||
},
|
||||
"expected": {
|
||||
"statements": [],
|
||||
"budget_tokens": 220,
|
||||
"used_tokens": 0,
|
||||
"evidence_ref": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compile_self_model",
|
||||
"args": {
|
||||
"corpus": [
|
||||
{
|
||||
"text": "I speak plainly and show my work.",
|
||||
"source": "persona.md",
|
||||
"weight": 0.9
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "I speak plainly and show my work.",
|
||||
"source": "persona.md",
|
||||
"weight": 0.9
|
||||
}
|
||||
],
|
||||
"budget_tokens": 220,
|
||||
"used_tokens": 8,
|
||||
"evidence_ref": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compile_self_model",
|
||||
"args": {
|
||||
"corpus": [
|
||||
{
|
||||
"text": "aaaaaaaaaaaaaaaa",
|
||||
"source": "s1",
|
||||
"weight": 2.0
|
||||
},
|
||||
{
|
||||
"text": "bbbbbbbbbbbbbbbb",
|
||||
"source": "s2",
|
||||
"weight": 1.5
|
||||
},
|
||||
{
|
||||
"text": "c",
|
||||
"source": "s3",
|
||||
"weight": 1.0
|
||||
}
|
||||
],
|
||||
"budget_tokens": 8
|
||||
},
|
||||
"expected": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "aaaaaaaaaaaaaaaa",
|
||||
"source": "s1",
|
||||
"weight": 2.0
|
||||
},
|
||||
{
|
||||
"text": "bbbbbbbbbbbbbbbb",
|
||||
"source": "s2",
|
||||
"weight": 1.5
|
||||
}
|
||||
],
|
||||
"budget_tokens": 8,
|
||||
"used_tokens": 8,
|
||||
"evidence_ref": [
|
||||
{
|
||||
"text": "c",
|
||||
"source": "s3"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compile_self_model",
|
||||
"args": {
|
||||
"corpus": [
|
||||
{
|
||||
"text": "no weight no source"
|
||||
},
|
||||
{
|
||||
"source": "only-source"
|
||||
},
|
||||
{
|
||||
"weight": 0.1
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "no weight no source",
|
||||
"source": "",
|
||||
"weight": 0.5
|
||||
},
|
||||
{
|
||||
"text": "",
|
||||
"source": "only-source",
|
||||
"weight": 0.5
|
||||
},
|
||||
{
|
||||
"text": "",
|
||||
"source": "",
|
||||
"weight": 0.1
|
||||
}
|
||||
],
|
||||
"budget_tokens": 220,
|
||||
"used_tokens": 6,
|
||||
"evidence_ref": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compile_self_model",
|
||||
"args": {
|
||||
"corpus": [
|
||||
{
|
||||
"text": "first",
|
||||
"source": "a",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"text": "second",
|
||||
"source": "b",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"text": "third",
|
||||
"source": "c",
|
||||
"weight": 1.0
|
||||
}
|
||||
],
|
||||
"budget_tokens": 3
|
||||
},
|
||||
"expected": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "first",
|
||||
"source": "a",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"text": "second",
|
||||
"source": "b",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"text": "third",
|
||||
"source": "c",
|
||||
"weight": 1.0
|
||||
}
|
||||
],
|
||||
"budget_tokens": 3,
|
||||
"used_tokens": 3,
|
||||
"evidence_ref": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compile_self_model",
|
||||
"args": {
|
||||
"corpus": [
|
||||
{
|
||||
"text": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"source": "big",
|
||||
"weight": 9.9
|
||||
}
|
||||
],
|
||||
"budget_tokens": 0
|
||||
},
|
||||
"expected": {
|
||||
"statements": [],
|
||||
"budget_tokens": 0,
|
||||
"used_tokens": 0,
|
||||
"evidence_ref": [
|
||||
{
|
||||
"text": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"source": "big"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compile_self_model",
|
||||
"args": {
|
||||
"corpus": [
|
||||
{
|
||||
"text": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"source": "big",
|
||||
"weight": 9.9
|
||||
}
|
||||
],
|
||||
"budget_tokens": -3
|
||||
},
|
||||
"expected": {
|
||||
"statements": [],
|
||||
"budget_tokens": -3,
|
||||
"used_tokens": 0,
|
||||
"evidence_ref": [
|
||||
{
|
||||
"text": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"source": "big"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compile_self_model",
|
||||
"args": {
|
||||
"corpus": [
|
||||
{
|
||||
"text": "quarter weight",
|
||||
"source": "q",
|
||||
"weight": 0.25
|
||||
},
|
||||
{
|
||||
"text": "three-quarter weight",
|
||||
"source": "t",
|
||||
"weight": 0.75
|
||||
}
|
||||
],
|
||||
"budget_tokens": 50
|
||||
},
|
||||
"expected": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "three-quarter weight",
|
||||
"source": "t",
|
||||
"weight": 0.75
|
||||
},
|
||||
{
|
||||
"text": "quarter weight",
|
||||
"source": "q",
|
||||
"weight": 0.25
|
||||
}
|
||||
],
|
||||
"budget_tokens": 50,
|
||||
"used_tokens": 8,
|
||||
"evidence_ref": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compile_self_model",
|
||||
"args": {
|
||||
"corpus": [
|
||||
{
|
||||
"text": "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww",
|
||||
"source": "big1",
|
||||
"weight": 3.0
|
||||
},
|
||||
{
|
||||
"text": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
|
||||
"source": "big2",
|
||||
"weight": 2.0
|
||||
},
|
||||
{
|
||||
"text": "zzzz",
|
||||
"source": "over",
|
||||
"weight": 1.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww",
|
||||
"source": "big1",
|
||||
"weight": 3.0
|
||||
},
|
||||
{
|
||||
"text": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
|
||||
"source": "big2",
|
||||
"weight": 2.0
|
||||
},
|
||||
{
|
||||
"text": "zzzz",
|
||||
"source": "over",
|
||||
"weight": 1.0
|
||||
}
|
||||
],
|
||||
"budget_tokens": 220,
|
||||
"used_tokens": 220,
|
||||
"evidence_ref": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compile_self_model",
|
||||
"args": {
|
||||
"corpus": [
|
||||
{
|
||||
"text": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
|
||||
"source": "huge",
|
||||
"weight": 5.0
|
||||
},
|
||||
{
|
||||
"text": "tiny",
|
||||
"source": "t",
|
||||
"weight": 0.4
|
||||
}
|
||||
],
|
||||
"budget_tokens": 10
|
||||
},
|
||||
"expected": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "tiny",
|
||||
"source": "t",
|
||||
"weight": 0.4
|
||||
}
|
||||
],
|
||||
"budget_tokens": 10,
|
||||
"used_tokens": 1,
|
||||
"evidence_ref": [
|
||||
{
|
||||
"text": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
|
||||
"source": "huge"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "why_question",
|
||||
"args": {
|
||||
"model": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "I prefer evidence over vibes.",
|
||||
"source": "s1",
|
||||
"weight": 2.0
|
||||
}
|
||||
],
|
||||
"budget_tokens": 10,
|
||||
"used_tokens": 7,
|
||||
"evidence_ref": [
|
||||
{
|
||||
"text": "CONTRADICTIONS get surfaced loudly.",
|
||||
"source": "s2"
|
||||
},
|
||||
{
|
||||
"text": "Growth is measured, not asserted.",
|
||||
"source": "s3"
|
||||
},
|
||||
{
|
||||
"text": "quiet evidence overflow note",
|
||||
"source": "s4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"topic": "evidence"
|
||||
},
|
||||
"expected": [
|
||||
{
|
||||
"text": "I prefer evidence over vibes.",
|
||||
"source": "s1",
|
||||
"weight": 2.0
|
||||
},
|
||||
{
|
||||
"text": "quiet evidence overflow note",
|
||||
"source": "s4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "why_question",
|
||||
"args": {
|
||||
"model": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "I prefer evidence over vibes.",
|
||||
"source": "s1",
|
||||
"weight": 2.0
|
||||
}
|
||||
],
|
||||
"budget_tokens": 10,
|
||||
"used_tokens": 7,
|
||||
"evidence_ref": [
|
||||
{
|
||||
"text": "CONTRADICTIONS get surfaced loudly.",
|
||||
"source": "s2"
|
||||
},
|
||||
{
|
||||
"text": "Growth is measured, not asserted.",
|
||||
"source": "s3"
|
||||
},
|
||||
{
|
||||
"text": "quiet evidence overflow note",
|
||||
"source": "s4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"topic": "EVIDENCE"
|
||||
},
|
||||
"expected": [
|
||||
{
|
||||
"text": "I prefer evidence over vibes.",
|
||||
"source": "s1",
|
||||
"weight": 2.0
|
||||
},
|
||||
{
|
||||
"text": "quiet evidence overflow note",
|
||||
"source": "s4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "why_question",
|
||||
"args": {
|
||||
"model": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "I prefer evidence over vibes.",
|
||||
"source": "s1",
|
||||
"weight": 2.0
|
||||
}
|
||||
],
|
||||
"budget_tokens": 10,
|
||||
"used_tokens": 7,
|
||||
"evidence_ref": [
|
||||
{
|
||||
"text": "CONTRADICTIONS get surfaced loudly.",
|
||||
"source": "s2"
|
||||
},
|
||||
{
|
||||
"text": "Growth is measured, not asserted.",
|
||||
"source": "s3"
|
||||
},
|
||||
{
|
||||
"text": "quiet evidence overflow note",
|
||||
"source": "s4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"topic": "growth"
|
||||
},
|
||||
"expected": [
|
||||
{
|
||||
"text": "Growth is measured, not asserted.",
|
||||
"source": "s3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "why_question",
|
||||
"args": {
|
||||
"model": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "I prefer evidence over vibes.",
|
||||
"source": "s1",
|
||||
"weight": 2.0
|
||||
}
|
||||
],
|
||||
"budget_tokens": 10,
|
||||
"used_tokens": 7,
|
||||
"evidence_ref": [
|
||||
{
|
||||
"text": "CONTRADICTIONS get surfaced loudly.",
|
||||
"source": "s2"
|
||||
},
|
||||
{
|
||||
"text": "Growth is measured, not asserted.",
|
||||
"source": "s3"
|
||||
},
|
||||
{
|
||||
"text": "quiet evidence overflow note",
|
||||
"source": "s4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"topic": "absent-topic"
|
||||
},
|
||||
"expected": []
|
||||
},
|
||||
{
|
||||
"fn": "why_question",
|
||||
"args": {
|
||||
"model": {
|
||||
"statements": [
|
||||
{
|
||||
"text": "I prefer evidence over vibes.",
|
||||
"source": "s1",
|
||||
"weight": 2.0
|
||||
}
|
||||
],
|
||||
"budget_tokens": 10,
|
||||
"used_tokens": 7,
|
||||
"evidence_ref": [
|
||||
{
|
||||
"text": "CONTRADICTIONS get surfaced loudly.",
|
||||
"source": "s2"
|
||||
},
|
||||
{
|
||||
"text": "Growth is measured, not asserted.",
|
||||
"source": "s3"
|
||||
},
|
||||
{
|
||||
"text": "quiet evidence overflow note",
|
||||
"source": "s4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"topic": ""
|
||||
},
|
||||
"expected": [
|
||||
{
|
||||
"text": "I prefer evidence over vibes.",
|
||||
"source": "s1",
|
||||
"weight": 2.0
|
||||
},
|
||||
{
|
||||
"text": "CONTRADICTIONS get surfaced loudly.",
|
||||
"source": "s2"
|
||||
},
|
||||
{
|
||||
"text": "Growth is measured, not asserted.",
|
||||
"source": "s3"
|
||||
},
|
||||
{
|
||||
"text": "quiet evidence overflow note",
|
||||
"source": "s4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "why_question",
|
||||
"args": {
|
||||
"model": {},
|
||||
"topic": "anything"
|
||||
},
|
||||
"expected": []
|
||||
},
|
||||
{
|
||||
"fn": "why_question",
|
||||
"args": {
|
||||
"model": {
|
||||
"evidence_ref": [
|
||||
{
|
||||
"text": "overflowed raw note",
|
||||
"source": "raw"
|
||||
}
|
||||
]
|
||||
},
|
||||
"topic": "raw"
|
||||
},
|
||||
"expected": [
|
||||
{
|
||||
"text": "overflowed raw note",
|
||||
"source": "raw"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! branch_battery — replays frozen_sm_truth.json VERBATIM (the live
|
||||
//! Python oracle, frozen with RUNE_SMM_CORE=python pinned and inputs
|
||||
//! deep-copied). Every case must reproduce byte-identically through the
|
||||
//! Rust kernel.
|
||||
|
||||
use super::{pyjson, compile_self_model, estimate_tokens, why_question, Json};
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_sm_truth.json");
|
||||
|
||||
fn as_i64(v: Option<&Json>, default: i64) -> i64 {
|
||||
match v {
|
||||
Some(Json::Num(n)) => *n as i64,
|
||||
_ => default,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_truth_replays_verbatim() {
|
||||
let doc = pyjson::parse(TRUTH).expect("frozen_sm_truth.json parses");
|
||||
let cases = match doc.get("cases") {
|
||||
Some(Json::Arr(a)) => a,
|
||||
_ => panic!("truth has no cases array"),
|
||||
};
|
||||
assert!(!cases.is_empty(), "truth is empty");
|
||||
for (i, case) in cases.iter().enumerate() {
|
||||
let fname = match case.get("fn") {
|
||||
Some(Json::Str(s)) => s.as_str(),
|
||||
_ => panic!("case {i}: missing fn"),
|
||||
};
|
||||
let args = case.get("args").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
let expected = case.get("expected").cloned().unwrap_or(Json::Null);
|
||||
let got: Json = match fname {
|
||||
"estimate_tokens" => {
|
||||
let text = match args.get("text") {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
Json::Num(estimate_tokens(&text) as f64)
|
||||
}
|
||||
"compile_self_model" => {
|
||||
let corpus: Vec<Json> = match args.get("corpus") {
|
||||
Some(Json::Arr(xs)) => xs.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
let budget = as_i64(args.get("budget_tokens"), 220);
|
||||
compile_self_model(&corpus, budget)
|
||||
}
|
||||
"why_question" => {
|
||||
let model = args.get("model").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
let topic = match args.get("topic") {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
why_question(&model, &topic)
|
||||
}
|
||||
other => panic!("case {i}: unknown fn {other}"),
|
||||
};
|
||||
let (gs, es) = (pyjson::dumps(&got), pyjson::dumps(&expected));
|
||||
assert_eq!(
|
||||
gs,
|
||||
es,
|
||||
"case {i} ({fname}) diverged:\n got: {gs}\n want: {es}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truth_covers_every_public_fn() {
|
||||
let doc = pyjson::parse(TRUTH).expect("truth parses");
|
||||
let cases = match doc.get("cases") {
|
||||
Some(Json::Arr(a)) => a,
|
||||
_ => panic!("truth has no cases array"),
|
||||
};
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for c in cases {
|
||||
if let Some(Json::Str(s)) = c.get("fn") {
|
||||
seen.insert(s.clone());
|
||||
}
|
||||
}
|
||||
for f in ["estimate_tokens", "compile_self_model", "why_question"] {
|
||||
assert!(seen.contains(f), "truth missing coverage for {f}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//! self_model — the bounded persistent self-model (dichotomy wave #2479
|
||||
//! tranche). RUST verdict per classification: the pure kernel (token
|
||||
//! estimate, greedy budgeted compile, why-retrieval) lives here.
|
||||
//!
|
||||
//! Oracle: src/rune/self_model.py (the facade keeps the Python floor,
|
||||
//! RUNE_SMM_CORE=python pin).
|
||||
//!
|
||||
//! Semantics mirrored exactly:
|
||||
//! * estimate_tokens(text) = max(1, char_len(text) / 4)
|
||||
//! (Python `len` is CHARS, not bytes — non-ASCII truth is frozen).
|
||||
//! * compile_self_model(corpus, budget_tokens=220): stable sort by
|
||||
//! -weight (missing weight -> 0.5; Python `sorted` is stable, so
|
||||
//! equal weights keep corpus order), then greedy fill:
|
||||
//! used + tok <= budget -> statements [{text, source, weight}]
|
||||
//! (missing source -> "", missing text -> ""), else evidence
|
||||
//! [{text, source}]. Result key order:
|
||||
//! {statements, budget_tokens, used_tokens, evidence_ref}.
|
||||
//! * why_question(model, topic): statements THEN evidence_ref, keep
|
||||
//! entries whose text contains topic case-insensitively; returns
|
||||
//! the bare list (the oracle's return shape).
|
||||
//!
|
||||
//! FFI contract (doc-envelope law — every call returns {"ok","value"|"err"}):
|
||||
//! * smm_estimate_tokens({"text"}) -> token count (number)
|
||||
//! * smm_compile_self_model({"corpus", "budget_tokens"?}) -> model dict
|
||||
//! * smm_why_question({"model", "topic"}) -> matches list
|
||||
//!
|
||||
//! Number boundary: pyjson numbers are f64 — the parity surface treats
|
||||
//! weights as floats (Python int weights would lose their int-ness across
|
||||
//! FFI; the frozen truth and fuzz corpora use floats only).
|
||||
//!
|
||||
//! Parity oracle: frozen_sm_truth.json (frozen from the live Python oracle
|
||||
//! with RUNE_SMM_CORE=python pinned, inputs deep-copied).
|
||||
|
||||
pub use capability_scoring::pyjson;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "branch_battery.rs"]
|
||||
mod branch_battery;
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use pyjson::Json;
|
||||
|
||||
pub fn estimate_tokens(text: &str) -> i64 {
|
||||
let n = text.chars().count() as i64;
|
||||
let tok = n / 4;
|
||||
if tok < 1 {
|
||||
1
|
||||
} else {
|
||||
tok
|
||||
}
|
||||
}
|
||||
|
||||
fn get_str(v: &Json, key: &str) -> String {
|
||||
match v.get(key) {
|
||||
Some(Json::Str(s)) => s.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_num(v: &Json, key: &str, default: f64) -> f64 {
|
||||
match v.get(key) {
|
||||
Some(Json::Num(n)) => *n,
|
||||
_ => default,
|
||||
}
|
||||
}
|
||||
|
||||
fn two_field(text: &str, source: &str) -> Json {
|
||||
Json::Obj(vec![
|
||||
("text".into(), Json::Str(text.into())),
|
||||
("source".into(), Json::Str(source.into())),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn compile_self_model(corpus: &[Json], budget_tokens: i64) -> Json {
|
||||
// stable sort by -weight, missing weight -> 0.5
|
||||
let mut ranked: Vec<&Json> = corpus.iter().collect();
|
||||
ranked.sort_by(|a, b| {
|
||||
let wa = get_num(a, "weight", 0.5);
|
||||
let wb = get_num(b, "weight", 0.5);
|
||||
wb.partial_cmp(&wa).unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let mut statements: Vec<Json> = Vec::with_capacity(ranked.len());
|
||||
let mut evidence: Vec<Json> = Vec::with_capacity(ranked.len());
|
||||
let mut used: i64 = 0;
|
||||
for c in ranked {
|
||||
let text = get_str(c, "text");
|
||||
let source = get_str(c, "source");
|
||||
let tok = estimate_tokens(&text);
|
||||
if used + tok <= budget_tokens {
|
||||
statements.push(Json::Obj(vec![
|
||||
("text".into(), Json::Str(text)),
|
||||
("source".into(), Json::Str(source)),
|
||||
("weight".into(), Json::Num(get_num(c, "weight", 0.5))),
|
||||
]));
|
||||
used += tok;
|
||||
} else {
|
||||
evidence.push(two_field(&text, &source));
|
||||
}
|
||||
}
|
||||
Json::Obj(vec![
|
||||
("statements".into(), Json::Arr(statements)),
|
||||
("budget_tokens".into(), Json::Num(budget_tokens as f64)),
|
||||
("used_tokens".into(), Json::Num(used as f64)),
|
||||
("evidence_ref".into(), Json::Arr(evidence)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn why_question(model: &Json, topic: &str) -> Json {
|
||||
let mut matches: Vec<Json> = Vec::new();
|
||||
for key in ["statements", "evidence_ref"] {
|
||||
if let Some(Json::Arr(entries)) = model.get(key) {
|
||||
for s in entries {
|
||||
let text = get_str(s, "text");
|
||||
if contains_ci(&text, topic) {
|
||||
matches.push(s.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Json::Arr(matches)
|
||||
}
|
||||
|
||||
/// Case-insensitive substring test mirroring Python's
|
||||
/// `topic.lower() in text.lower()` for the ASCII surface; non-ASCII
|
||||
/// falls back to full Unicode lowercasing of both sides.
|
||||
fn contains_ci(text: &str, topic: &str) -> bool {
|
||||
if topic.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if text.is_ascii() && topic.is_ascii() {
|
||||
let hay = text.as_bytes();
|
||||
let needle = topic.as_bytes();
|
||||
return hay.len() >= needle.len()
|
||||
&& hay
|
||||
.windows(needle.len())
|
||||
.any(|w| w.eq_ignore_ascii_case(needle));
|
||||
}
|
||||
text.to_lowercase().contains(&topic.to_lowercase())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
]),
|
||||
};
|
||||
CString::new(pyjson::dumps(&v))
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn read_arg(arg: *const c_char) -> Result<Json, String> {
|
||||
if arg.is_null() {
|
||||
return Err("null arg".into());
|
||||
}
|
||||
let raw = unsafe { CStr::from_ptr(arg) }.to_bytes();
|
||||
pyjson::parse(std::str::from_utf8(raw).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn smm_estimate_tokens(arg: *const c_char) -> *mut c_char {
|
||||
let out = read_arg(arg).map(|a| {
|
||||
let text = get_str(&a, "text");
|
||||
Json::Num(estimate_tokens(&text) as f64)
|
||||
});
|
||||
envelope(out)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn smm_compile_self_model(arg: *const c_char) -> *mut c_char {
|
||||
let out = read_arg(arg).map(|a| {
|
||||
let corpus: Vec<Json> = match a.get("corpus") {
|
||||
Some(Json::Arr(xs)) => xs.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
let budget = match a.get("budget_tokens") {
|
||||
Some(Json::Num(n)) => *n as i64,
|
||||
_ => 220,
|
||||
};
|
||||
compile_self_model(&corpus, budget)
|
||||
});
|
||||
envelope(out)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn smm_why_question(arg: *const c_char) -> *mut c_char {
|
||||
let out = read_arg(arg).map(|a| {
|
||||
let model = a.get("model").cloned().unwrap_or(Json::Obj(vec![]));
|
||||
let topic = get_str(&a, "topic");
|
||||
why_question(&model, &topic)
|
||||
});
|
||||
envelope(out)
|
||||
}
|
||||
|
||||
// Free is owned by capability_scoring's cs_free_string (shared process)
|
||||
// — no free re-export here, mirroring the broker_loader contract.
|
||||
Reference in new issue
Block a user