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

This commit is contained in:
Nick committed 2026-10-13 07:57:34 +01:00
1 parent cf7a10f6d6
commit dac4889c0b
9 files changed
+1215

No files matched your search

+127
View File
@@ -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}")
+39
View File
@@ -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")
+32
View File
@@ -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():
+79
View File
@@ -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})
+14
View File
@@ -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",
]
+12
View File
@@ -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" }
+626
View File
@@ -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"
}
]
}
]
}
+83
View File
@@ -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}");
}
}
+203
View File
@@ -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.