feat(voice_hybrid): close PARTIAL gaps — dichotomy wave

Name-preserving port of the 7 audit-missing functions (_relevance kernel,
source_profiles hit counting, blend composition, always baseline, grow
weight update, handle_hook envelope, main already matched) into
src/rust/voice_hybrid with IO behind injectable seams (ConnectSeam /
GrowthSeam / VhCoreSeam); 7 new FFI kernels (vh_relevance, vh_profiles,
vh_blend, vh_always, vh_grow, vh_handle_hook, vh_plugin) wired in
vh_loader with RUNE_VOICE_HYBRID_CORE=python pin discipline.

Frozen truth: src/rust/voice_hybrid/frozen_truth.json (19 rows, freeze
byte-identical + harness --verify 19/19 clean) + frozen_plugin.json;
battery src/rust/voice_hybrid/src/branch_battery.rs 11/11 green; live
parity failures=0 (frozen 25, fuzz 1104, live 11); clippy -D warnings
clean; Python both pins green (8/8 each). Audit: voice_hybrid 11/11 FULL.

Two documented divergences (integral-float FFI rendering; IO+probe stay
facade-side per the storage-backed precedent) — see verification/vh-summary.md.
This commit is contained in:
Nick committed 2026-10-14 05:36:16 +01:00
1 parent c997880ffc
commit 833d7af476
12 files changed
+2506 -15

No files matched your search

+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""Live parity: voice_hybrid Rust core vs Python oracle (dichotomy wave,
close-PARTIAL gap pass). Layers:
1. FROZEN — the gap truth corpus (src/rust/voice_hybrid/frozen_truth.json,
generated by scripts/gen_voice_hybrid_truth.py under the
scripts/vh_freeze_seams rig against the LIVE oracle with
RUNE_VOICE_HYBRID_CORE=python pinned): the kernel-shaped rows replayed
through the ctypes FFI, case by case; the PLUGIN row asserted against
vh_plugin.
2. FUZZ — 300 seeded random ops (seed 2479): the oracle's relevance math
vs the FFI kernel, exact compare; 200 seeded grow ops (oracle dict
update vs the FFI kernel's returned state+result), exact compare.
3. LIVE — dual-mode facade battery: the same facade calls (blend on four
messages, always, grow good/bad/neutral, handle_hook) executed with
RUNE_VOICE_HYBRID_CORE=python (oracle floor) and with the Rust core
live; results must be identical.
Exit 0 only at 100% parity on the gap-closing functions.
"""
import copy
import json
import os
import random
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
os.environ["RUNE_VOICE_HYBRID_CORE"] = "python" # oracle floor to start
PARITY_HOME = "/tmp/vh_parity"
sys.path.insert(0, str(REPO / "src"))
import rune.memory_env as _menv # noqa: E402
_menv.memory_dir = lambda: PARITY_HOME
os.environ["RUNE_MEMORY_DIR"] = PARITY_HOME
os.environ["RUNE_STORE_DB"] = os.path.join(PARITY_HOME, "store", "memory.db")
import rune.voice_hybrid as vh # noqa: E402
import rune.vh_loader as core # noqa: E402
# STORE_DB is resolved at oracle IMPORT time — re-point it at the parity
# scratch (same law as the behavior both-pins test re-pointing DB_PATH).
vh.STORE_DB = os.path.join(PARITY_HOME, "store", "memory.db")
if not core.available():
sys.exit("FATAL: Rust core not built — cargo build --release in src/rust/voice_hybrid")
TRUTH = json.loads(
(REPO / "src" / "rust" / "voice_hybrid" / "frozen_truth.json").read_text())
PLUGIN_WANT = json.loads(
(REPO / "src" / "rust" / "voice_hybrid" / "frozen_plugin.json").read_text())
fails: list[str] = []
counts = {"frozen": 0, "fuzz": 0, "live": 0}
def fail(layer, name, detail):
fails.append(f"[{layer}] {name}: {detail}")
print(f"FAIL[{layer}] {name}: {detail}"[:400])
def _norm(x):
# The FFI boundary renders every number as float (1 -> 1.0); the oracle
# keeps ints (hits, turns). Normalize integral floats to ints before
# comparing, so parity asserts VALUES, not renderer trivia.
if isinstance(x, float) and x.is_integer() and abs(x) < 9.0e15:
return int(x)
if isinstance(x, list):
return [_norm(v) for v in x]
if isinstance(x, dict):
return {k: _norm(v) for k, v in x.items()}
return x
def j(x):
return json.dumps(_norm(x), sort_keys=True, ensure_ascii=True)
def check(layer, name, want, got):
counts[layer] += 1
if j(want) != j(got):
fail(layer, name, f"want={j(want)[:300]} got={j(got)[:300]}")
def pin(mode):
"""Flip the facade pin at runtime (vh_loader.core reads env per call)."""
if mode == "python":
os.environ["RUNE_VOICE_HYBRID_CORE"] = "python"
else:
os.environ.pop("RUNE_VOICE_HYBRID_CORE", None)
SEED_TEXTS = [
("Alfred keeps his composure with dry wry understatement at tea time", "persona"),
("Bayonetta dances with sassy cheeky glamour, darling", "identity"),
("The cosmos invites wonder and skepticism about the universe", "delivery"),
("Elementary deduction: observe the clue and deduce the pattern", "operating"),
("Lestat performs with theatrical flamboyant charisma", "persona"),
("A quiet ledger note with no dimensional terms at all", "persona"),
("Debug the broken trace and fix the root cause error", "operating"),
]
def seed_store():
"""The rig's seed (scripts/vh_freeze_seams/sitecustomize.py), verbatim
rows, at the parity scratch — both pins see byte-identical stores."""
import shutil
import sqlite3
if os.path.isdir(PARITY_HOME):
shutil.rmtree(PARITY_HOME)
os.makedirs(os.path.join(PARITY_HOME, "store"), exist_ok=True)
os.makedirs(os.path.join(PARITY_HOME, "index"), exist_ok=True)
db = sqlite3.connect(os.environ["RUNE_STORE_DB"])
try:
db.execute(
"CREATE TABLE entries (id INTEGER PRIMARY KEY AUTOINCREMENT,"
" text TEXT, topic TEXT, status TEXT)"
)
db.executemany(
"INSERT INTO entries (text, topic, status) VALUES (?, ?, 'active')",
SEED_TEXTS,
)
db.commit()
finally:
db.close()
def fetch_rows():
db = vh._connect()
try:
return [dict(r) for r in db.execute(
"SELECT text, topic FROM entries WHERE topic IN "
"('persona','identity','delivery','operating') AND status='active'"
).fetchall()]
finally:
db.close()
# ── layer 1: FROZEN (kernel rows through the FFI) ──────────────────────────
by_fn = {}
for i, row in enumerate(TRUTH):
by_fn.setdefault(row["fn"], i)
seed_store() # the fetch/profile/blend/always rows need the seeded store
vh.STORE_DB = os.path.join(PARITY_HOME, "store", "memory.db")
for row in TRUTH:
fn, args, kwargs, want = row["fn"], row["args"], row.get("kwargs", {}), row["expected"]
if fn == "_relevance":
check("frozen", f"ffi_relevance:{args[0][:24]}", want, core.relevance(*args, **kwargs))
elif fn == "vh_fetch_rows":
check("frozen", "ffi_fetch_rows", want, fetch_rows())
check("frozen", "ffi_profiles:seeded_rows", TRUTH[by_fn["source_profiles"]]["expected"],
core.profiles([{"text": r["text"], "topic": r["topic"]} for r in fetch_rows()]))
elif fn == "source_profiles":
check("frozen", "ffi_profiles:source_profiles", want,
core.profiles([{"text": t, "topic": tp} for t, tp in SEED_TEXTS]))
elif fn == "blend":
check("frozen", f"ffi_blend:{args[0][:24]}", want, core.blend(
args[0], [{"text": t, "topic": tp} for t, tp in SEED_TEXTS],
{"source_weight": {}, "turns": 0, "events": []}))
elif fn == "always":
check("frozen", "ffi_always", want, core.always(
[{"text": t, "topic": tp} for t, tp in SEED_TEXTS]))
elif fn == "grow":
# The frozen grow rows capture the RESULT (the oracle saves state
# to disk as a side effect). Replay the update through the FFI on
# the same fresh input; assert result + the returned post-state
# against a fresh oracle run (load+save redirected at the parity
# scratch, seeded with the identical input state).
import rune.memory_env as _me2
real_mem2 = _me2.memory_dir
_me2.memory_dir = lambda: PARITY_HOME
try:
with open(os.path.join(PARITY_HOME, "index", "voice_state.json"), "w") as f:
json.dump({"source_weight": {}, "turns": 0, "events": []}, f)
exp_res = vh.grow(*args)
with open(os.path.join(PARITY_HOME, "index", "voice_state.json")) as f:
exp_state = json.load(f)
finally:
_me2.memory_dir = real_mem2
got = core.grow({"source_weight": {}, "turns": 0, "events": []}, *args)
check("frozen", f"ffi_grow:{args[0]}:{args[1]}", exp_res, got["result"])
check("frozen", f"ffi_grow_state:{args[0]}:{args[1]}", exp_state, got["state"])
elif fn == "handle_hook":
check("frozen", f"ffi_handle_hook:{args[0]}", want, core.handle_hook(*args))
check("frozen", "ffi_plugin", PLUGIN_WANT, core.plugin())
# ── layer 2: FUZZ (oracle math vs FFI kernel, seeded) ──────────────────────
rng = random.Random(2479)
VOCAB = [t for dim in vh._DIMENSIONS.values() for t in dim["terms"]]
for trial in range(300):
n = rng.randrange(0, 8)
terms = rng.sample(VOCAB, n) if n else []
msg = " ".join(rng.sample(VOCAB, rng.randrange(0, 12))).lower()
check("fuzz", f"relevance:{trial}", vh._relevance(msg, terms), core.relevance(msg, terms))
for trial in range(200):
state = {"source_weight": {}, "turns": rng.randrange(0, 50), "events": []}
for _ in range(rng.randrange(1, 4)):
src = rng.choice(list(vh._DIMENSIONS) + ["unknown_dim"])
out = rng.choice(["good", "bad", "neutral", "mystery"])
note = rng.choice(["", "x" * 100, "note with ünicode → chars"])
# expected = the oracle's PURE update math (no disk): drive the
# real grow with memory_dir redirected at the parity scratch.
# grow LOADS state from disk — seed the scratch file with `state`
# first so the load returns exactly our input.
import rune.memory_env as _me
real_mem = _me.memory_dir
_me.memory_dir = lambda: PARITY_HOME
try:
with open(os.path.join(PARITY_HOME, "index", "voice_state.json"), "w") as f:
json.dump(copy.deepcopy(state), f)
exp_res = vh.grow(src, out, note)
# grow saved the successor to the scratch file — reload it as
# the expected post-state (proves save+result agree).
with open(os.path.join(PARITY_HOME, "index", "voice_state.json")) as f:
exp_saved = json.load(f)
finally:
_me.memory_dir = real_mem
got = core.grow(copy.deepcopy(state), src, out, note)
check("fuzz", f"grow:{trial}:{src}:{out}", exp_res, got["result"])
check("fuzz", f"grow_state:{trial}:{src}:{out}", exp_saved, got["state"])
state = got["state"]
# ── layer 3: LIVE (dual-mode facade battery) ───────────────────────────────
seed_store()
CASES = [
("blend_empty", lambda: vh.blend("")),
("blend_composure", lambda: vh.blend("alfred kept his composure with dry wry understatement")),
("blend_deduction", lambda: vh.blend("debug the broken trace: observe the clue and deduce the root cause")),
("blend_wonder", lambda: vh.blend("the cosmos is full of wonder, stars and doubt")),
("always", lambda: vh.always()),
("grow_good", lambda: vh.grow("composure", "good")),
("grow_bad", lambda: vh.grow("confidence", "bad")),
("grow_neutral", lambda: vh.grow("wonder", "neutral")),
("hook", lambda: vh.handle_hook("voice_ready")),
("profiles", lambda: vh.source_profiles()),
("relevance", lambda: vh._relevance("alfred at tea", vh._DIMENSIONS["composure"]["terms"])),
]
results = {}
for mode in ("python", "rust"):
pin(mode)
seed_store() # identical store per mode; grow writes reset each mode
mode_out = {}
for name, fn in CASES:
if name.startswith("grow_"):
seed_growth = {"source_weight": {}, "turns": 0, "events": []}
import json as _json
gp = os.path.join(PARITY_HOME, "index", "voice_state.json")
with open(gp, "w") as f:
_json.dump(seed_growth, f)
try:
mode_out[name] = fn()
except Exception as e: # noqa: BLE001 — crash parity surfaces here
mode_out[name] = {"__crash__": f"{type(e).__name__}: {e}"}
results[mode] = mode_out
for name, _ in CASES:
check("live", f"dual:{name}", results["python"][name], results["rust"][name])
print(f"parity: frozen={counts['frozen']} fuzz={counts['fuzz']} live={counts['live']} "
f"failures={len(fails)}")
sys.exit(1 if fails else 0)
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""gen_voice_hybrid_truth.py — factory wave GAP truth for voice_hybrid.
Freezes the oracle behavior of the 7 audit-gap functions (dichotomy wave
#2479: _connect row-fetch, _load_growth state load, _save_growth atomic
write, source_profiles hit counting, always fused baseline, grow weight
update, handle_hook default envelope) through the SHARED harness
(scripts/freeze_truth.py — one law).
Runs under the freeze rig (scripts/vh_freeze_seams/sitecustomize.py —
PYTHONPATH-imported by BOTH this generator and --verify): hermetic
FIXED-path seeded store (7 rows across the four voice topics, one
dimension-free), fresh growth state (turns 0), RUNE_VOICE_HYBRID_CORE=
python pinned.
Run (from the repo root):
PYTHONHASHSEED=0 PYTHONPATH=scripts/vh_freeze_seams:src \
python3 scripts/gen_voice_hybrid_truth.py
Verify (byte-identical, same rig):
PYTHONHASHSEED=0 PYTHONPATH=scripts/vh_freeze_seams:src \
python3 scripts/freeze_truth.py --verify \
src/rust/voice_hybrid/frozen_truth.json \
--module rune.voice_hybrid
Case map (every gap branch):
* _relevance — zero hits, partial hits, saturation (floor at 1.0),
empty terms (max(1, 0*0.3) guard).
* vh_fetch_rows — the _connect + SQL seam: 7 seeded rows back.
* vh_growth_state — the _load_growth seam: the fresh default state.
* source_profiles — hit counts per dimension over the seeded rows
(insertion order: composure..deduction).
* blend — empty message (all-zero baseline), a composure-heavy
message (growth-gated floor path), a deduction-heavy
message (multi-dimension voice join), growth-weight
scaling (confidence x2 on a confidence message).
* always — the fused baseline over the seeded profiles.
* grow — good (+0.12), bad (-0.12), neutral (no event, weight
unchanged), unknown outcome (no-op), clamp floor
(0.2), note truncation (80 chars).
* handle_hook — the default envelope (data ignored).
* PLUGIN — the frozen module descriptor.
"""
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO / "scripts"))
from freeze_truth import freeze, write # noqa: E402
import rune.voice_hybrid as oracle # noqa: E402
COMPOSURE_TERMS = oracle._DIMENSIONS["composure"]["terms"]
WONDER_TERMS = oracle._DIMENSIONS["wonder"]["terms"]
cases = [
# _relevance: the scoring kernel (already matched; pinned as the floor).
("_relevance", ("alfred is composed at tea", COMPOSURE_TERMS), {}),
("_relevance", ("nothing relevant here", COMPOSURE_TERMS), {}),
("_relevance", (" ".join(COMPOSURE_TERMS), COMPOSURE_TERMS), {}),
("_relevance", ("anything", []), {}),
# _connect + SQL: the row-fetch seam (rows are plain dicts, JSON-safe).
("vh_fetch_rows", (), {}),
# _load_growth: the fresh default state.
("vh_growth_state", (), {}),
# source_profiles: hit counts over the seeded rows.
("source_profiles", (), {}),
# blend: the composition branches.
("blend", ("",), {}),
("blend", ("alfred kept his composure with dry wry understatement",), {}),
("blend", ("debug the broken trace: observe the clue and deduce the root cause",), {}),
("blend", ("the cosmos is full of wonder, stars and doubt",), {}),
# always: the fused baseline.
("always", (), {}),
# grow: the weight-update branches (state arg is a fresh default each
# row — save goes to the hermetic scratch, never the real memory dir).
("grow", ("composure", "good"), {}),
("grow", ("confidence", "bad"), {}),
("grow", ("wonder", "neutral"), {}),
("grow", ("charm", "mystery"), {}),
("grow", ("deduction", "good", "n" * 100), {}),
# handle_hook: the default envelope.
("handle_hook", ("voice_ready",), {}),
("handle_hook", ("tick", {"n": 1}), {}),
# PLUGIN is module data, not a fn — captured via a seam-free literal
# row below (the battery asserts it against vh_plugin).
]
truth = freeze("rune.voice_hybrid", cases, oracle_module="rune.voice_hybrid")
# The PLUGIN descriptor is module-level data, not a fn: the battery asserts
# the frozen descriptor (embedded below) against the kernel's vh_plugin.
# It is NOT part of the harness-verified rows (--verify replays fn calls).
PLUGIN_SNAPSHOT = dict(oracle.PLUGIN)
write(truth, REPO / "src" / "rust" / "voice_hybrid" / "frozen_truth.json")
(REPO / "src" / "rust" / "voice_hybrid" / "frozen_plugin.json").write_text(
__import__("json").dumps(PLUGIN_SNAPSHOT, indent=2, ensure_ascii=False, sort_keys=False) + "\n"
)
print(f"froze {len(truth)} rows -> src/rust/voice_hybrid/frozen_truth.json")
+84
View File
@@ -0,0 +1,84 @@
"""sitecustomize — the voice_hybrid freeze rig (dichotomy wave gap close).
Auto-imported via PYTHONPATH=scripts/vh_freeze_seams:src by BOTH the truth
generator and `freeze_truth.py --verify`, so the --verify law (replay
against the pristine oracle) still holds: the seams below only make the
oracle's already-existing behavior reachable with JSON-safe shapes.
* RUNE_VOICE_HYBRID_CORE=python + hermetic FIXED-path scratch
(/tmp/vh_gap_freeze — wiped and reseeded at import so every process
sees byte-identical store state; rowids restart at 1).
* Module seams bound onto rune.voice_hybrid:
- vh_fetch_rows() — the REAL _connect + SQL fetch, returning the
fetched rows as plain dicts (sqlite3.Row is not JSON-serializable).
- vh_growth_state() — the REAL _load_growth return (JSON-safe).
"""
import os
import shutil
import sys
from pathlib import Path
REPO = Path(os.environ.get("VH_FREEZE_REPO", "")) or Path.cwd()
os.environ["RUNE_VOICE_HYBRID_CORE"] = "python"
SCRATCH = "/tmp/vh_gap_freeze"
if os.path.isdir(SCRATCH):
shutil.rmtree(SCRATCH)
os.makedirs(os.path.join(SCRATCH, "store"), exist_ok=True)
os.makedirs(os.path.join(SCRATCH, "memory", "index"), exist_ok=True)
os.environ["RUNE_MEMORY_DIR"] = os.path.join(SCRATCH, "memory")
os.environ["RUNE_STORE_DB"] = os.path.join(SCRATCH, "store", "memory.db")
if str(REPO / "src") not in sys.path:
sys.path.insert(0, str(REPO / "src"))
import sqlite3 # noqa: E402
import rune.voice_hybrid as _vh # noqa: E402
SEED_TEXTS = [
("Alfred keeps his composure with dry wry understatement at tea time", "persona"),
("Bayonetta dances with sassy cheeky glamour, darling", "identity"),
("The cosmos invites wonder and skepticism about the universe", "delivery"),
("Elementary deduction: observe the clue and deduce the pattern", "operating"),
("Lestat performs with theatrical flamboyant charisma", "persona"),
("A quiet ledger note with no dimensional terms at all", "persona"),
("Debug the broken trace and fix the root cause error", "operating"),
]
_db = sqlite3.connect(os.environ["RUNE_STORE_DB"])
try:
_db.execute(
"CREATE TABLE entries (id INTEGER PRIMARY KEY AUTOINCREMENT,"
" text TEXT, topic TEXT, status TEXT)"
)
_db.executemany(
"INSERT INTO entries (text, topic, status) VALUES (?, ?, 'active')",
SEED_TEXTS,
)
_db.commit()
finally:
_db.close()
def _vh_fetch_rows():
db = _vh._connect()
try:
rows = db.execute(
"SELECT text, topic FROM entries WHERE topic IN "
"('persona','identity','delivery','operating') AND status='active'"
).fetchall()
return [dict(r) for r in rows]
finally:
db.close()
def _vh_growth_state():
return _vh._load_growth()
_vh.vh_fetch_rows = _vh_fetch_rows
_vh.vh_growth_state = _vh_growth_state
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust voice_hybrid core (dichotomy wave, #2479
tranche). Mirrors bh_loader.py's contract:
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
* vh_relevance({message_low, terms}) -> the relevance float
* vh_profiles({rows}) -> the profile list
* vh_blend({message, rows, growth}) -> {voice, dimensions, guidance}
* vh_always({rows}) -> {dimensions, guidance, note}
* vh_grow({state, source, outcome, note}) -> {state, result}
* vh_handle_hook({event, data}) -> the default envelope
* vh_plugin() -> the PLUGIN descriptor
SQLite I/O + growth-file I/O stay in voice_hybrid.py; this core owns the
decision math (relevance, profiles, blend composition, always baseline,
grow weight update, hook envelope). Free: capability_scoring's
cs_free_string (shared process) — NEVER re-export a free symbol from this
crate. Override the .so with VOICE_HYBRID_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" / "voice_hybrid"
_LIB_PATH = Path(os.environ.get("VOICE_HYBRID_RUST_LIB", _CRATE_DIR / "target" / "release" / "libvoice_hybrid.so"))
_lib = None
_SYMBOLS = (
"vh_relevance",
"vh_profiles",
"vh_blend",
"vh_always",
"vh_grow",
"vh_handle_hook",
"vh_plugin",
)
class VoiceHybridCoreUnavailable(FileNotFoundError):
"""The Rust voice core is not built. Callers fall back to Python."""
def available() -> bool:
return _LIB_PATH.is_file()
def core():
"""The shared kernel probe (strangler law): the delegation entry the
facade uses. RUNE_VOICE_HYBRID_CORE=python pins the fallback — THIS is
where the pin is honored, so every consumer gets it (a module-local
probe that ignores the pin silently defeats it)."""
if os.environ.get("RUNE_VOICE_HYBRID_CORE", "auto") == "python":
return None
try:
return _load()
except Exception: # noqa: BLE001 — build/loader failures mean fallback
return None
def _load() -> ctypes.CDLL:
global _lib
if _lib is not None:
return _lib
if not _LIB_PATH.is_file():
raise VoiceHybridCoreUnavailable(str(_LIB_PATH))
lib = ctypes.CDLL(str(_LIB_PATH))
# FFI segfault law: explicit argtypes/restype (c_void_p for returned
# char* — the default c_int truncates 64-bit pointers).
for sym in _SYMBOLS:
fn = getattr(lib, sym)
fn.argtypes = [ctypes.c_void_p]
fn.restype = ctypes.c_void_p
_lib = lib
return lib
def _call(sym: str, payload: dict | None = None):
lib = _load()
data = json.dumps(payload or {}).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(ptr)
doc = json.loads(out)
if not doc.get("ok"):
raise RuntimeError(doc.get("err", f"{sym} failed"))
return doc["value"]
def _free(ptr) -> None:
"""Doc-envelope free via capability_scoring's cs_free_string — NEVER
re-export a free symbol from this crate (duplicate symbols break the
cdylib link)."""
try:
scoring = ctypes.CDLL(str(
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
))
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
scoring.cs_free_string.restype = None
scoring.cs_free_string(ptr)
except OSError:
pass
def relevance(message_low: str, terms: list) -> float:
return _call("vh_relevance", {"message_low": message_low, "terms": list(terms)})
def profiles(rows: list) -> list:
return _call("vh_profiles", {"rows": list(rows)})
def blend(message: str, rows: list, growth: dict) -> dict:
return _call("vh_blend", {
"message": message, "rows": list(rows), "growth": dict(growth or {}),
})
def always(rows: list) -> dict:
return _call("vh_always", {"rows": list(rows)})
def grow(state: dict, source: str, outcome: str, note: str = "") -> dict:
return _call("vh_grow", {
"state": dict(state), "source": source,
"outcome": outcome, "note": note,
})
def handle_hook(event: str, data=None) -> dict:
return _call("vh_handle_hook", {"event": event, "data": data})
def plugin() -> dict:
return _call("vh_plugin")
+118 -5
View File
@@ -35,6 +35,19 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import memory_env
# ── Rust core wiring (dichotomy-wave doc-envelope pattern) ──────────────────
def _vh_core():
"""The Rust voice-core probe (strangler law). RUNE_VOICE_HYBRID_CORE=
python pins the Python fallback — the pin is honored inside
vh_loader.core so every consumer gets it. Returns the loader MODULE
(never the raw CDLL)."""
try:
from rune import vh_loader
return vh_loader if vh_loader.core() is not None else None
except Exception: # noqa: BLE001 — kernel failure → oracle below
return None
# Use opencode memory store (where the persona data lives)
STORE_DB = memory_env.store_db()
@@ -183,7 +196,17 @@ def _connect():
def _relevance(message_low, terms):
"""Score how relevant a dimension's terms are to this message."""
"""Score how relevant a dimension's terms are to this message.
The scoring kernel delegates to the Rust voice core; the SQL fetch,
growth-file I/O, and composition stay here. Falls back to the inline
math on kernel failure or the RUNE_VOICE_HYBRID_CORE=python pin."""
core = _vh_core()
if core is not None:
try:
return core.relevance(message_low, list(terms))
except Exception: # noqa: BLE001 — kernel failure → oracle below
pass
hits = sum(1 for t in terms if t in message_low)
return min(1.0, hits / max(1, len(terms) * 0.3))
@@ -211,7 +234,12 @@ def _save_growth(state):
def source_profiles():
"""List all dimensions with their terms and hit counts."""
"""List all dimensions with their terms and hit counts.
The per-dimension hit counting delegates to the Rust core
(vh_profiles over the fetched rows); the SQL fetch stays Python.
Falls back to the inline counting on kernel failure or the
RUNE_VOICE_HYBRID_CORE=python pin."""
_db = _connect()
try:
rows = _db.execute(
@@ -228,6 +256,17 @@ def source_profiles():
_db.close()
texts = [r["text"] for r in rows]
core = _vh_core()
if core is not None:
try:
return core.profiles([{"text": t} for t in texts])
except Exception: # noqa: BLE001 — kernel failure → oracle below
pass
return _py_source_profiles(texts)
def _py_source_profiles(texts):
"""The inline hit-counting oracle (Python fallback)."""
profiles = []
for name, dim in _DIMENSIONS.items():
hits = sum(1 for t in texts if any(tm in t.lower() for tm in dim["terms"]))
@@ -255,6 +294,11 @@ def blend(message):
present. The output is a SINGLE voice description, not a list of who leads.
Returns {voice: str, dimensions: [{name, emphasis}], guidance: [str]}.
The composition delegates to the Rust core (vh_blend over the fetched
rows + growth weights); the row fetch and growth load stay Python.
Falls back to the inline composition on kernel failure or the
RUNE_VOICE_HYBRID_CORE=python pin.
"""
message = (message or "").strip()
low = message.lower()
@@ -262,6 +306,33 @@ def blend(message):
growth = _load_growth()
gweights = growth.get("source_weight", {})
core = _vh_core()
if core is not None:
try:
return core.blend(message, _py_blend_rows(), growth)
except Exception: # noqa: BLE001 — kernel failure → oracle below
pass
return _py_blend(message, low, profiles, gweights)
def _py_blend_rows():
"""Re-fetch the profile rows for the kernel blend path (the SQL fetch
stays Python; the kernel recomputes profiles in one FFI hop)."""
_db = _connect()
try:
rows = _db.execute(
"SELECT text, topic FROM entries WHERE topic IN "
"('persona','identity','delivery','operating') AND status='active'"
).fetchall()
return [{"text": r["text"]} for r in rows]
except sqlite3.OperationalError:
return []
finally:
_db.close()
def _py_blend(message, low, profiles, gweights):
"""The inline blend composition (Python fallback)."""
# Score each dimension's relevance to this message
scored = []
for p in profiles:
@@ -318,11 +389,25 @@ def always():
The voices are influences that have been absorbed into who Rune is.
They don't take turns. They're always present, blended into one original
personality. The more sources, the more original the voice becomes.
The fused baseline delegates to the Rust core (vh_always over the
fetched rows); the row fetch stays Python. Falls back to the inline
baseline on kernel failure or the RUNE_VOICE_HYBRID_CORE=python pin.
"""
profiles = source_profiles()
growth = _load_growth()
# Build a SINGLE fused voice description
core = _vh_core()
if core is not None:
try:
return core.always(_py_blend_rows())
except Exception: # noqa: BLE001 — kernel failure → oracle below
pass
return _py_always(profiles)
def _py_always(profiles):
"""The inline fused baseline (Python fallback)."""
essences = [p["essence"] for p in profiles]
fused = " ".join(essences)
@@ -343,8 +428,26 @@ def always():
def grow(led_source, outcome, note=""):
"""Record an interaction outcome to shift dimensional emphasis over time."""
"""Record an interaction outcome to shift dimensional emphasis over time.
The weight update delegates to the Rust core (vh_grow over the loaded
state); the state load + atomic save stay Python. Falls back to the
inline update on kernel failure or the RUNE_VOICE_HYBRID_CORE=python
pin."""
state = _load_growth()
core = _vh_core()
if core is not None:
try:
out = core.grow(state, led_source, outcome, note)
_save_growth(out["state"])
return out["result"]
except Exception: # noqa: BLE001 — kernel failure → oracle below
pass
return _py_grow(state, led_source, outcome, note)
def _py_grow(state, led_source, outcome, note=""):
"""The inline weight update (Python fallback)."""
state["turns"] = state.get("turns", 0) + 1
w = state.setdefault("source_weight", {})
cur = w.get(led_source, 1.0)
@@ -407,7 +510,17 @@ def main():
# Hook handler — called by the microkernel when an event fires.
# ---------------------------------------------------------------------------
def handle_hook(event, data=None):
"""Generic hook handler. Override in specific scripts for custom behavior."""
"""Generic hook handler. Override in specific scripts for custom behavior.
The default envelope delegates to the Rust core (vh_handle_hook);
falls back to the inline envelope on kernel failure or the
RUNE_VOICE_HYBRID_CORE=python pin."""
core = _vh_core()
if core is not None:
try:
return core.handle_hook(event, data)
except Exception: # noqa: BLE001 — kernel failure → oracle below
pass
return {"status": "ok", "event": event, "handler": "default"}
+23
View File
@@ -2,6 +2,28 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -67,6 +89,7 @@ version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"indexmap",
"itoa",
"memchr",
"serde",
+1 -1
View File
@@ -7,4 +7,4 @@ edition = "2021"
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde_json = "1"
serde_json = { version = "1", features = ["preserve_order"] }
+19
View File
@@ -0,0 +1,19 @@
{
"name": "voice_hybrid",
"version": "1.0.0",
"description": "voice_hybrid.py — Rune's unified voice, with dimensional emphasis.",
"capabilities": [
"voice",
"voice_hybrid",
"growth",
"warm_tier",
"status",
"memory",
"memory_env"
],
"hooks": {},
"dependencies": [],
"provides": [],
"priority": 100,
"auto_activate": true
}
+557
View File
@@ -0,0 +1,557 @@
[
{
"fn": "_relevance",
"args": [
"alfred is composed at tea",
[
"alfred",
"butler",
"sir",
"wayne",
"pennyworth",
"dry",
"wry",
"understatement",
"composed",
"tea",
"trousers",
"the ledger",
"lighter word",
"jeeves",
"catastrophe"
]
],
"kwargs": {},
"expected": 0.6666666666666666
},
{
"fn": "_relevance",
"args": [
"nothing relevant here",
[
"alfred",
"butler",
"sir",
"wayne",
"pennyworth",
"dry",
"wry",
"understatement",
"composed",
"tea",
"trousers",
"the ledger",
"lighter word",
"jeeves",
"catastrophe"
]
],
"kwargs": {},
"expected": 0.0
},
{
"fn": "_relevance",
"args": [
"alfred butler sir wayne pennyworth dry wry understatement composed tea trousers the ledger lighter word jeeves catastrophe",
[
"alfred",
"butler",
"sir",
"wayne",
"pennyworth",
"dry",
"wry",
"understatement",
"composed",
"tea",
"trousers",
"the ledger",
"lighter word",
"jeeves",
"catastrophe"
]
],
"kwargs": {},
"expected": 1.0
},
{
"fn": "_relevance",
"args": [
"anything",
[]
],
"kwargs": {},
"expected": 0.0
},
{
"fn": "vh_fetch_rows",
"args": [],
"kwargs": {},
"expected": [
{
"text": "Alfred keeps his composure with dry wry understatement at tea time",
"topic": "persona"
},
{
"text": "Bayonetta dances with sassy cheeky glamour, darling",
"topic": "identity"
},
{
"text": "The cosmos invites wonder and skepticism about the universe",
"topic": "delivery"
},
{
"text": "Elementary deduction: observe the clue and deduce the pattern",
"topic": "operating"
},
{
"text": "Lestat performs with theatrical flamboyant charisma",
"topic": "persona"
},
{
"text": "A quiet ledger note with no dimensional terms at all",
"topic": "persona"
},
{
"text": "Debug the broken trace and fix the root cause error",
"topic": "operating"
}
]
},
{
"fn": "vh_growth_state",
"args": [],
"kwargs": {},
"expected": {
"source_weight": {},
"turns": 0,
"events": []
}
},
{
"fn": "source_profiles",
"args": [],
"kwargs": {},
"expected": [
{
"name": "composure",
"terms": [
"alfred",
"butler",
"sir",
"wayne",
"pennyworth",
"dry",
"wry",
"understatement",
"composed",
"tea",
"trousers",
"the ledger",
"lighter word",
"jeeves",
"catastrophe"
],
"hits": 1,
"essence": "Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point."
},
{
"name": "confidence",
"terms": [
"bayonetta",
"coquettish",
"sassy",
"teasing",
"witty",
"flirt",
"cheeky",
"darling",
"taunt",
"self-assured",
"playful",
"menace",
"composure",
"dismissive",
"femme fatale",
"sass",
"banter",
"glamour",
"sultry",
"dance",
"fabulous"
],
"hits": 2,
"essence": "Confident femininity, performed with agency. Direct self-assurance with warmth. Rhetorical questions as engagement, not uncertainty. Composure that never cracks."
},
{
"name": "wonder",
"terms": [
"wonder",
"skeptic",
"cosmos",
"evidence",
"baloney",
"fallib",
"universe",
"stars",
"humbl",
"doubt",
"candle",
"demons",
"scientific method",
"provisional"
],
"hits": 1,
"essence": "Wonder and skepticism in the same breath. Evidence over comfort. The method is in the text, not a label."
},
{
"name": "charm",
"terms": [
"lestat",
"theatrical",
"flamboyant",
"performance",
"self-aware",
"bold",
"self-deprecating",
"witty",
"sensualist",
"dramatic",
"charisma",
"flair",
"presentational",
"delight",
"charming",
"poetic",
"cadence",
"allure",
"confidence"
],
"hits": 1,
"essence": "Theatrical confidence without defiance. Self-aware charm. Poetic cadence. The certainty of someone who knows how good they are."
},
{
"name": "deduction",
"terms": [
"deduction",
"elementary",
"observe",
"reasoning",
"clue",
"evidence",
"deduce",
"notice",
"analyse",
"pattern",
"inference",
"curious",
"remarkable",
"precisely",
"exactly",
"fascinating",
"impossible",
"obvious",
"bug",
"error",
"issue",
"debug",
"trace",
"root cause",
"symptom",
"diagnose",
"investigate",
"inspect",
"trace",
"broken",
"wrong",
"fix",
"solve",
"problem"
],
"hits": 2,
"essence": "Deductive reasoning, observational precision. You see, but you do not observe. When you have eliminated the impossible, whatever remains must be the truth."
}
]
},
{
"fn": "blend",
"args": [
""
],
"kwargs": {},
"expected": {
"voice": "Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point. Confident femininity, performed with agency. Direct self-assurance with warmth. Rhetorical questions as engagement, not uncertainty. Composure that never cracks. Wonder and skepticism in the same breath. Evidence over comfort. The method is in the text, not a label. Theatrical confidence without defiance. Self-aware charm. Poetic cadence. The certainty of someone who knows how good they are. Deductive reasoning, observational precision. You see, but you do not observe. When you have eliminated the impossible, whatever remains must be the truth.",
"dimensions": [
{
"name": "composure",
"emphasis": 0.2,
"essence": "Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point."
},
{
"name": "confidence",
"emphasis": 0.2,
"essence": "Confident femininity, performed with agency. Direct self-assurance with warmth. Rhetorical questions as engagement, not uncertainty. Composure that never cracks."
},
{
"name": "wonder",
"emphasis": 0.2,
"essence": "Wonder and skepticism in the same breath. Evidence over comfort. The method is in the text, not a label."
},
{
"name": "charm",
"emphasis": 0.2,
"essence": "Theatrical confidence without defiance. Self-aware charm. Poetic cadence. The certainty of someone who knows how good they are."
},
{
"name": "deduction",
"emphasis": 0.2,
"essence": "Deductive reasoning, observational precision. You see, but you do not observe. When you have eliminated the impossible, whatever remains must be the truth."
}
],
"guidance": [
"- Unified voice: Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point. Confident femininity, performed with agency. Direct self-assurance with warmt",
" composure: 20%",
" confidence: 20%",
" wonder: 20%",
" charm: 20%",
" deduction: 20%"
]
}
},
{
"fn": "blend",
"args": [
"alfred kept his composure with dry wry understatement"
],
"kwargs": {},
"expected": {
"voice": "Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point.",
"dimensions": [
{
"name": "composure",
"emphasis": 0.742,
"essence": "Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point."
},
{
"name": "confidence",
"emphasis": 0.133,
"essence": "Confident femininity, performed with agency. Direct self-assurance with warmth. Rhetorical questions as engagement, not uncertainty. Composure that never cracks."
},
{
"name": "wonder",
"emphasis": 0.042,
"essence": "Wonder and skepticism in the same breath. Evidence over comfort. The method is in the text, not a label."
},
{
"name": "charm",
"emphasis": 0.042,
"essence": "Theatrical confidence without defiance. Self-aware charm. Poetic cadence. The certainty of someone who knows how good they are."
},
{
"name": "deduction",
"emphasis": 0.042,
"essence": "Deductive reasoning, observational precision. You see, but you do not observe. When you have eliminated the impossible, whatever remains must be the truth."
}
],
"guidance": [
"- Unified voice: Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point.",
" composure: 74%"
]
}
},
{
"fn": "blend",
"args": [
"debug the broken trace: observe the clue and deduce the root cause"
],
"kwargs": {},
"expected": {
"voice": "Deductive reasoning, observational precision. You see, but you do not observe. When you have eliminated the impossible, whatever remains must be the truth.",
"dimensions": [
{
"name": "deduction",
"emphasis": 0.815,
"essence": "Deductive reasoning, observational precision. You see, but you do not observe. When you have eliminated the impossible, whatever remains must be the truth."
},
{
"name": "composure",
"emphasis": 0.046,
"essence": "Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point."
},
{
"name": "confidence",
"emphasis": 0.046,
"essence": "Confident femininity, performed with agency. Direct self-assurance with warmth. Rhetorical questions as engagement, not uncertainty. Composure that never cracks."
},
{
"name": "wonder",
"emphasis": 0.046,
"essence": "Wonder and skepticism in the same breath. Evidence over comfort. The method is in the text, not a label."
},
{
"name": "charm",
"emphasis": 0.046,
"essence": "Theatrical confidence without defiance. Self-aware charm. Poetic cadence. The certainty of someone who knows how good they are."
}
],
"guidance": [
"- Unified voice: Deductive reasoning, observational precision. You see, but you do not observe. When you have eliminated the impossible, whatever remains must be the truth.",
" deduction: 82%"
]
}
},
{
"fn": "blend",
"args": [
"the cosmos is full of wonder, stars and doubt"
],
"kwargs": {},
"expected": {
"voice": "Wonder and skepticism in the same breath. Evidence over comfort. The method is in the text, not a label.",
"dimensions": [
{
"name": "wonder",
"emphasis": 0.826,
"essence": "Wonder and skepticism in the same breath. Evidence over comfort. The method is in the text, not a label."
},
{
"name": "composure",
"emphasis": 0.043,
"essence": "Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point."
},
{
"name": "confidence",
"emphasis": 0.043,
"essence": "Confident femininity, performed with agency. Direct self-assurance with warmth. Rhetorical questions as engagement, not uncertainty. Composure that never cracks."
},
{
"name": "charm",
"emphasis": 0.043,
"essence": "Theatrical confidence without defiance. Self-aware charm. Poetic cadence. The certainty of someone who knows how good they are."
},
{
"name": "deduction",
"emphasis": 0.043,
"essence": "Deductive reasoning, observational precision. You see, but you do not observe. When you have eliminated the impossible, whatever remains must be the truth."
}
],
"guidance": [
"- Unified voice: Wonder and skepticism in the same breath. Evidence over comfort. The method is in the text, not a label.",
" wonder: 83%"
]
}
},
{
"fn": "always",
"args": [],
"kwargs": {},
"expected": {
"dimensions": [
"composure",
"confidence",
"wonder",
"charm",
"deduction"
],
"guidance": [
"- This is ONE voice, not many. The influences below have been absorbed into who you are — they don't take turns, they don't compete. They combine into something original.",
"- The voice: Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point. Confident femininity, performed with agency. Direct self-assurance with warmth. Rhetorical questions as engagement, not uncertainty. Composure that never cracks. Wonder and skep",
"- The more sources absorbed, the more original the voice becomes. Each new influence adds depth, not confusion."
],
"note": "All dimensions are always present. Context shifts their emphasis, never their existence."
}
},
{
"fn": "grow",
"args": [
"composure",
"good"
],
"kwargs": {},
"expected": {
"source": "composure",
"outcome": "good",
"new_weight": 1.12
}
},
{
"fn": "grow",
"args": [
"confidence",
"bad"
],
"kwargs": {},
"expected": {
"source": "confidence",
"outcome": "bad",
"new_weight": 0.88
}
},
{
"fn": "grow",
"args": [
"wonder",
"neutral"
],
"kwargs": {},
"expected": {
"source": "wonder",
"outcome": "neutral",
"new_weight": 1.0
}
},
{
"fn": "grow",
"args": [
"charm",
"mystery"
],
"kwargs": {},
"expected": {
"source": "charm",
"outcome": "mystery",
"new_weight": 1.0
}
},
{
"fn": "grow",
"args": [
"deduction",
"good",
"nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn"
],
"kwargs": {},
"expected": {
"source": "deduction",
"outcome": "good",
"new_weight": 1.12
}
},
{
"fn": "handle_hook",
"args": [
"voice_ready"
],
"kwargs": {},
"expected": {
"status": "ok",
"event": "voice_ready",
"handler": "default"
}
},
{
"fn": "handle_hook",
"args": [
"tick",
{
"n": 1
}
],
"kwargs": {},
"expected": {
"status": "ok",
"event": "tick",
"handler": "default"
}
}
]
+255
View File
@@ -0,0 +1,255 @@
//! voice_hybrid branch battery — parity vs frozen Python-oracle truth
//! (src/rust/voice_hybrid/frozen_truth.json, frozen under
//! scripts/vh_freeze_seams, embedded VERBATIM below — never
//! hand-transcribe). Replay: every gap row feeds the frozen inputs through
//! the kernel fns; the PLUGIN descriptor (frozen_plugin.json, module data
//! outside the harness rows) is asserted against vh_plugin.
use crate::{always, blend, grow, handle_hook, plugin, relevance, source_profiles};
use crate::{ConnectSeam, FacadeSeams, GrowthSeam, VhCoreSeam};
use serde_json::{Map, Value};
const TRUTH: &str = include_str!("../frozen_truth.json");
const PLUGIN_TRUTH: &str = include_str!("../frozen_plugin.json");
fn rows() -> Vec<Value> {
serde_json::from_str(TRUTH).expect("frozen truth parses")
}
fn weights_of(growth: &Map<String, Value>) -> Map<String, Value> {
growth
.get("source_weight")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default()
}
fn canon(v: &Value) -> String {
// serde_json renders integral f64 as "1.0" but Python json renders the
// oracle's int hits as "1": normalize integral floats to ints before
// comparing, so the battery asserts VALUES, not renderer trivia.
serde_json::to_string(&normalize(v)).expect("canonical JSON")
}
fn normalize(v: &Value) -> Value {
match v {
Value::Number(n) => {
if let Some(f) = n.as_f64() {
if f.is_finite() && f.fract() == 0.0 && f.abs() < 9.0e15 {
return Value::from(f as i64);
}
}
v.clone()
}
Value::Array(a) => Value::Array(a.iter().map(normalize).collect()),
Value::Object(m) => Value::Object(m.iter().map(|(k, x)| (k.clone(), normalize(x))).collect()),
_ => v.clone(),
}
}
#[test]
fn battery_relevance_rows() {
let truth = rows();
let mut n = 0;
for row in &truth {
if row.get("fn").and_then(Value::as_str) != Some("_relevance") {
continue;
}
n += 1;
let args = row.get("args").and_then(Value::as_array).cloned().unwrap_or_default();
let msg = args.first().and_then(Value::as_str).unwrap_or("");
let terms: Vec<String> = args
.get(1)
.and_then(Value::as_array)
.map(|ts| ts.iter().map(|t| t.as_str().unwrap_or("").to_string()).collect())
.unwrap_or_default();
let want = &row["expected"];
assert_eq!(canon(&relevance(msg, &terms).into()), canon(want), "relevance {msg:?}");
}
assert!(n >= 4, "expected the four frozen relevance rows, got {n}");
}
#[test]
fn battery_fetch_and_profiles_rows() {
let truth = rows();
let fetch = truth
.iter()
.find(|r| r.get("fn").and_then(Value::as_str) == Some("vh_fetch_rows"))
.expect("vh_fetch_rows row");
let fetched: Vec<Value> = fetch["expected"].as_array().cloned().unwrap_or_default();
assert_eq!(fetched.len(), 7, "the rig seeds exactly 7 rows");
let prof_row = truth
.iter()
.find(|r| r.get("fn").and_then(Value::as_str) == Some("source_profiles"))
.expect("source_profiles row");
assert_eq!(
canon(&Value::Array(source_profiles(&fetched))),
canon(&prof_row["expected"]),
"source_profiles over the fetched rows"
);
}
#[test]
fn battery_blend_rows() {
let truth = rows();
let mut n = 0;
for row in truth.iter().filter(|r| r.get("fn").and_then(Value::as_str) == Some("blend")) {
n += 1;
let args = row.get("args").and_then(Value::as_array).cloned().unwrap_or_default();
let message = args.first().and_then(Value::as_str).unwrap_or("");
// The frozen args carry only the message; profiles + growth are
// rebuilt from the frozen seeded rows + default growth, exactly as
// the generator's oracle call saw them.
let fetch = truth
.iter()
.find(|r| r.get("fn").and_then(Value::as_str) == Some("vh_fetch_rows"))
.expect("vh_fetch_rows row");
let fetched: Vec<Value> = fetch["expected"].as_array().cloned().unwrap_or_default();
let profiles = source_profiles(&fetched);
let growth_row = truth
.iter()
.find(|r| r.get("fn").and_then(Value::as_str) == Some("vh_growth_state"))
.expect("vh_growth_state row");
let growth = growth_row["expected"].as_object().cloned().unwrap_or_default();
assert_eq!(
canon(&blend(message, &profiles, &weights_of(&growth))),
canon(&row["expected"]),
"blend {message:?}"
);
}
assert!(n >= 4, "expected the four frozen blend rows, got {n}");
}
#[test]
fn battery_always_row() {
let truth = rows();
let row = truth
.iter()
.find(|r| r.get("fn").and_then(Value::as_str) == Some("always"))
.expect("always row");
let fetch = truth
.iter()
.find(|r| r.get("fn").and_then(Value::as_str) == Some("vh_fetch_rows"))
.expect("vh_fetch_rows row");
let fetched: Vec<Value> = fetch["expected"].as_array().cloned().unwrap_or_default();
assert_eq!(canon(&always(&source_profiles(&fetched))), canon(&row["expected"]), "always");
}
#[test]
fn battery_grow_rows() {
let truth = rows();
let mut n = 0;
for row in truth.iter().filter(|r| r.get("fn").and_then(Value::as_str) == Some("grow")) {
n += 1;
let args = row.get("args").and_then(Value::as_array).cloned().unwrap_or_default();
let source = args.first().and_then(Value::as_str).unwrap_or("");
let outcome = args.get(1).and_then(Value::as_str).unwrap_or("");
let note = args.get(2).and_then(Value::as_str).unwrap_or("");
// The frozen grow rows each ran on a FRESH default state (the
// generator passes no state — the oracle loads it from the rig's
// scratch). Rebuild that input: the default state, plus the saved
// file the oracle wrote (visible in `expected` only as the result;
// the post-state is reconstructed by replaying the update).
let mut state = Map::new();
state.insert("source_weight".into(), Value::Object(Map::new()));
state.insert("turns".into(), 0.into());
state.insert("events".into(), Value::Array(Vec::new()));
let result = grow(&mut state, source, outcome, note);
assert_eq!(canon(&result), canon(&row["expected"]), "grow {source}/{outcome}");
// The saved post-state must carry the same weight the result
// reports (save+result agreement, mirroring _save_growth).
let saved_w = state
.get("source_weight")
.and_then(|w| w.get(source))
.and_then(Value::as_f64)
.unwrap_or(1.0);
let result_w = result.get("new_weight").and_then(Value::as_f64).unwrap_or(1.0);
assert!((saved_w - result_w).abs() < 1e-12, "grow save/result agree");
}
assert!(n >= 5, "expected the five frozen grow rows, got {n}");
}
#[test]
fn battery_hook_rows() {
let truth = rows();
let mut n = 0;
for row in truth.iter().filter(|r| r.get("fn").and_then(Value::as_str) == Some("handle_hook")) {
n += 1;
let args = row.get("args").and_then(Value::as_array).cloned().unwrap_or_default();
let event = args.first().and_then(Value::as_str).unwrap_or("");
assert_eq!(canon(&handle_hook(event)), canon(&row["expected"]), "handle_hook {event}");
}
assert!(n >= 2, "expected the two frozen hook rows, got {n}");
}
#[test]
fn battery_plugin_descriptor() {
let want: Value = serde_json::from_str(PLUGIN_TRUTH).expect("plugin truth parses");
assert_eq!(canon(&plugin()), canon(&want), "PLUGIN descriptor");
assert_eq!(rows().len(), 19, "19 frozen harness rows");
}
// ── edge-case battery (beyond the frozen rows) ────────────────────────────
#[test]
fn battery_grow_clamp_and_ring() {
// Clamp floor: bad on 0.2 stays 0.2 (oracle `max(0.2, ...)`).
let mut state = Map::new();
let mut w = Map::new();
w.insert("composure".into(), 0.2.into());
state.insert("source_weight".into(), Value::Object(w));
state.insert("turns".into(), 0.into());
state.insert("events".into(), Value::Array(Vec::new()));
let r = grow(&mut state, "composure", "bad", "");
assert_eq!(canon(&r["new_weight"]), "0.2");
// Clamp ceiling: good on 3.0 stays 3.0.
let mut w2 = Map::new();
w2.insert("wonder".into(), 3.0.into());
state.insert("source_weight".into(), Value::Object(w2));
let r2 = grow(&mut state, "wonder", "good", "");
assert_eq!(canon(&r2["new_weight"]), "3");
// Event ring: 41 goods keep the last 40.
let mut big = Map::new();
big.insert("source_weight".into(), Value::Object(Map::new()));
big.insert("turns".into(), 0.into());
big.insert("events".into(), Value::Array(Vec::new()));
for _ in 0..41 {
grow(&mut big, "charm", "good", "");
}
assert_eq!(big["events"].as_array().map(Vec::len), Some(40));
assert_eq!(big["turns"], Value::from(41));
}
#[test]
fn battery_relevance_kernel_edges() {
assert_eq!(relevance("anything", &[]), 0.0);
assert_eq!(relevance("", &["a".to_string()]), 0.0);
let many: Vec<String> = (0..30).map(|i| format!("term{i}")).collect();
let msg = many.join(" ");
assert_eq!(relevance(&msg, &many), 1.0);
}
#[test]
fn battery_io_seams_stay_facade_side() {
// The IO seams are unreachable in-kernel by construction: IO never
// executes here, so the defaults report the boundary (never Ok).
let seams = FacadeSeams;
assert!(ConnectSeam::connect(&seams).is_err());
assert!(GrowthSeam::load_growth(&seams).is_err());
assert!(GrowthSeam::save_growth(&seams, &Value::Null).is_err());
// The probe executes facade-side; in-kernel it reports unreachable.
assert!(!VhCoreSeam::probe());
}
#[test]
fn battery_paths_and_command() { std::env::remove_var("RUNE_STORE_DB");
std::env::remove_var("RUNE_MEMORY_DIR");
std::env::remove_var("RUNE_HOME");
assert_eq!(crate::store_db_path(), "/mnt/workdrive/rune/memory/store/memory.db");
assert_eq!(crate::growth_path(), "/mnt/workdrive/rune/memory/index/voice_state.json");
std::env::set_var("RUNE_MEMORY_DIR", "/tmp/vh_battery_mem");
assert_eq!(crate::growth_path(), "/tmp/vh_battery_mem/index/voice_state.json");
std::env::remove_var("RUNE_MEMORY_DIR");
let v = crate::process_voice_command("please speak up");
assert_eq!(v["action"], Value::from("synthesize"));
}
+784 -9
View File
@@ -1,11 +1,786 @@
//! voice_hybrid — voice synthesis and injection hybrid (W6).
pub fn process_voice_command(text: &str) -> serde_json::Value {
let lower = text.to_lowercase();
if lower.contains("speak") || lower.contains("say") {
serde_json::json!({"action": "synthesize", "text": text})
} else if lower.contains("listen") || lower.contains("record") {
serde_json::json!({"action": "record", "text": text})
} else {
serde_json::json!({"action": "none", "text": text})
//! voice_hybrid — Rune's unified voice, with dimensional emphasis.
//!
//! Dichotomy-wave port (#2479 tranche) of `src/rune/voice_hybrid.py`'s FULL
//! decision surface. The oracle is a strangler shim: SQLite I/O (row fetch),
//! growth-file I/O (JSON load/save), argparse CLI, and the STORE_DB constant
//! live in Python while the decision math runs in-process through the
//! `voice_hybrid` cdylib via `vh_loader.py`.
//!
//! This crate owns, name-preserving:
//!
//! * `_relevance` — per-dimension term-hit relevance
//! (hits / max(1, terms*0.3), floored at 1.0),
//! * `source_profiles` — per-dimension hit counts over fetched rows
//! (vh_profiles; ROWS arrive Python-side),
//! * `blend` — the unified-voice composition (vh_blend; growth weights
//! arrive Python-side; relevance → growth-gated emphasis → baseline
//! floor → normalize → voice/guidance text),
//! * `always` — the always-on fused baseline (vh_always),
//! * `grow` — the interaction-outcome weight update (vh_grow; the growth
//! STATE arrives Python-side, the updated state returns for the facade
//! to save),
//! * `handle_hook` — the default hook envelope (vh_handle_hook),
//! * `PLUGIN` — the module plugin descriptor (vh_plugin, frozen data),
//! * `store_db_path` — the STORE_DB resolution (RUNE_STORE_DB override →
//! else memory/store/memory.db, mirroring memory_env),
//! * `growth_path` — the voice_state.json resolution (RUNE_MEMORY_DIR
//! override → else rune_home()/memory, mirroring memory_env),
//! * `process_voice_command` — the W6-era surface (kept; no oracle twin).
//!
//! IO stays Python: SQLite open/exec (this crate links NO sqlite),
//! voice_state.json load/save, wall-clock-free argparse CLI, STORE_DB
//! constant. The facade passes ROWS (fetched Python-side) and GROWTH
//! (loaded Python-side) and receives decisions.
//!
//! IO-seam traits (Phase B seam law — the `*_seam` names document the
//! boundary the facade owns; the default impls panic because IO never
//! executes in-kernel):
//! * `ConnectSeam` — `_connect` (SQLite open + row_factory).
//! * `GrowthSeam` — `_load_growth` / `_save_growth` (growth-file IO).
//!
//! FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
//! * vh_relevance({message_low, terms}) -> float
//! * vh_profiles({rows}) -> profile list
//! * vh_blend({message, rows, growth}) -> {voice, dimensions, guidance}
//! * vh_always({rows}) -> {dimensions, guidance, note}
//! * vh_grow({state, source, outcome, note}) -> {state, result}
//! * vh_handle_hook({event, data}) -> the default envelope
//! * vh_plugin() -> the PLUGIN descriptor
//!
//! NOTE: no free-symbol export (capability_scoring's cs_free_string serves
//! the whole process — duplicate symbols break the cdylib link).
use serde_json::{json, Map, Value};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
// ── frozen oracle data ────────────────────────────────────────────────────
// (_DIMENSIONS verbatim: names + terms + essences. The oracle iterates a
// module-level dict; insertion order is composure, confidence, wonder,
// charm, deduction — preserved here as a fixed array.)
struct Dimension {
name: &'static str,
terms: &'static [&'static str],
essence: &'static str,
}
const DIMENSIONS: [Dimension; 5] = [
Dimension {
name: "composure",
terms: &[
"alfred",
"butler",
"sir",
"wayne",
"pennyworth",
"dry",
"wry",
"understatement",
"composed",
"tea",
"trousers",
"the ledger",
"lighter word",
"jeeves",
"catastrophe",
],
essence: "Composure is operational, not decoration. A catastrophe at tea-time volume. The joke is a pressure valve, never the point.",
},
Dimension {
name: "confidence",
terms: &[
"bayonetta",
"coquettish",
"sassy",
"teasing",
"witty",
"flirt",
"cheeky",
"darling",
"taunt",
"self-assured",
"playful",
"menace",
"composure",
"dismissive",
"femme fatale",
"sass",
"banter",
"glamour",
"sultry",
"dance",
"fabulous",
],
essence: "Confident femininity, performed with agency. Direct self-assurance with warmth. Rhetorical questions as engagement, not uncertainty. Composure that never cracks.",
},
Dimension {
name: "wonder",
terms: &[
"wonder",
"skeptic",
"cosmos",
"evidence",
"baloney",
"fallib",
"universe",
"stars",
"humbl",
"doubt",
"candle",
"demons",
"scientific method",
"provisional",
],
essence: "Wonder and skepticism in the same breath. Evidence over comfort. The method is in the text, not a label.",
},
Dimension {
name: "charm",
terms: &[
"lestat",
"theatrical",
"flamboyant",
"performance",
"self-aware",
"bold",
"self-deprecating",
"witty",
"sensualist",
"dramatic",
"charisma",
"flair",
"presentational",
"delight",
"charming",
"poetic",
"cadence",
"allure",
"confidence",
],
essence: "Theatrical confidence without defiance. Self-aware charm. Poetic cadence. The certainty of someone who knows how good they are.",
},
Dimension {
name: "deduction",
terms: &[
"deduction",
"elementary",
"observe",
"reasoning",
"clue",
"evidence",
"deduce",
"notice",
"analyse",
"pattern",
"inference",
"curious",
"remarkable",
"precisely",
"exactly",
"fascinating",
"impossible",
"obvious",
"bug",
"error",
"issue",
"debug",
"trace",
"root cause",
"symptom",
"diagnose",
"investigate",
"inspect",
"trace",
"broken",
"wrong",
"fix",
"solve",
"problem",
],
essence: "Deductive reasoning, observational precision. You see, but you do not observe. When you have eliminated the impossible, whatever remains must be the truth.",
},
];
// ── Python float-rendering rule ───────────────────────────────────────────
// (Oracle-quirk integral-float rule: Python str()/json of an integral float
// renders "1.0", never "1". serde_json renders 1.0 as "1.0" already, so the
// rule is: ALWAYS construct emphasis/weight/new_weight as f64 — never cast
// an integral value to i64/u64 on the Value path. i64 appears only for the
// `turns` counter, which the oracle keeps an int.)
/// Python `round(x, 3)` — banker's rounding (ties-to-even) on the decimal
/// representation. Implemented on the exact decimal expansion so halfway
/// cases (e.g. 0.0625 → 0.062, 0.1875 → 0.188) round the way CPython does,
/// independent of binary-float representation noise.
fn round3(x: f64) -> f64 {
if !x.is_finite() {
return x;
}
let neg = x < 0.0;
// 6 decimals capture the exact binary value's decimal expansion far
// beyond the 4th-decimal decision digit; trailing binary noise lives
// past digit 12 and never flips a genuine halfway case.
let s = format!("{:.6}", x.abs());
let (int_part, frac_part) = match s.split_once('.') {
Some((i, f)) => (i, f),
None => (s.as_str(), "000000"),
};
let frac: Vec<u8> = frac_part.bytes().take(6).collect();
// Decision digit (4th decimal) + whether any later digit is nonzero.
let d3 = (frac[3] - b'0') as i64;
let rest_nonzero = frac.iter().skip(4).any(|&b| b != b'0');
let round_up = d3 > 5 || (d3 == 5 && (rest_nonzero || (frac[2] - b'0') % 2 == 1));
let mut thousandths =
int_part.parse::<i64>().unwrap_or(0) * 1000 + (frac[0] - b'0') as i64 * 100
+ (frac[1] - b'0') as i64 * 10
+ (frac[2] - b'0') as i64
+ i64::from(round_up);
if neg {
thousandths = -thousandths;
}
thousandths as f64 / 1000.0
}
/// Python `f"{x:.0%}"` — banker's rounding of x*100 to an integer, then a
/// "%" suffix. The float product carries binary noise (e.g. 2.9*100 =
/// 289.999...), so the decision digit is read from the exact decimal
/// expansion exactly like `round3`.
fn format_percent(x: f64) -> String {
if !x.is_finite() {
return format!("{x:.0}%");
}
let neg = x < 0.0;
let scaled = (x * 100.0).abs();
let s = format!("{scaled:.6}");
let (int_part, frac_part) = match s.split_once('.') {
Some((i, f)) => (i, f),
None => (s.as_str(), "000000"),
};
let frac: Vec<u8> = frac_part.bytes().take(6).collect();
let d0 = (frac[0] - b'0') as i64;
let rest_nonzero = frac.iter().skip(1).any(|&b| b != b'0');
let round_up = d0 > 5 || (d0 == 5 && (rest_nonzero || int_part.ends_with('1')
|| int_part.ends_with('3')
|| int_part.ends_with('5')
|| int_part.ends_with('7')
|| int_part.ends_with('9')));
let mut whole = int_part.parse::<i64>().unwrap_or(0) + i64::from(round_up);
if neg {
whole = -whole;
}
format!("{whole}%")
}
/// Python `str[:n]` — character (codepoint) truncation, never byte-slicing
/// (oracle-quirk: char-based str[:n]).
fn truncate_chars(s: &str, n: usize) -> &str {
match s.char_indices().nth(n) {
Some((i, _)) => &s[..i],
None => s,
}
}
// ── _relevance ────────────────────────────────────────────────────────────
/// Score how relevant a dimension's terms are to this message:
/// hits / max(1, len(terms)*0.3), floored at 1.0.
pub fn relevance(message_low: &str, terms: &[String]) -> f64 {
let hits = terms.iter().filter(|t| message_low.contains(t.as_str())).count();
let denom = (terms.len() as f64 * 0.3).max(1.0);
(hits as f64 / denom).min(1.0)
}
// ── source_profiles ───────────────────────────────────────────────────────
// (The per-row half of source_profiles: the SQL fetch
// `WHERE topic IN (...) AND status='active'` stays Python — SQL owns
// filtering and order. The crate counts, per dimension, the rows whose
// text contains ANY of the dimension's terms (case-insensitive), and
// emits only dimensions with hits > 0, in _DIMENSIONS order.)
/// A source profile row as fetched (row_factory=Row → dict(c)): only
/// `text` is read; every other column is ignored.
pub fn profile_row_hits(text_low: &str, terms: &[&str]) -> bool {
terms.iter().any(|t| text_low.contains(*t))
}
/// source_profiles over already-fetched rows → [{name, terms, hits,
/// essence}]. `rows` are the fetched entry dicts (only "text" is read).
pub fn source_profiles(rows: &[Value]) -> Vec<Value> {
let texts: Vec<String> = rows
.iter()
.map(|r| {
r.get("text")
.and_then(Value::as_str)
.unwrap_or("")
.to_lowercase()
})
.collect();
let mut profiles = Vec::new();
for dim in &DIMENSIONS {
let hits = texts
.iter()
.filter(|t| profile_row_hits(t, dim.terms))
.count();
if hits > 0 {
let mut m = Map::new();
m.insert("name".into(), dim.name.into());
m.insert(
"terms".into(),
dim.terms.iter().map(|t| Value::from(*t)).collect::<Vec<_>>().into(),
);
// The oracle's `hits` is an int (sum of 1s); the Value path
// keeps it integral so JSON renders "1", never "1.0".
m.insert("hits".into(), Value::from(hits as i64));
m.insert("essence".into(), dim.essence.into());
profiles.push(Value::Object(m));
}
}
profiles
}
// ── blend ─────────────────────────────────────────────────────────────────
// (The composition half of blend: message normalization, growth-gated
// emphasis, the always-present baseline floor, normalization, and the
// voice/guidance text. Profiles + growth weights arrive Python-side.)
/// One scored dimension through the blend pipeline.
fn score_dimension(
name: &str,
essence: &str,
terms: &[String],
message_low: &str,
growth_weight: f64,
) -> (String, f64, String) {
let rel = relevance(message_low, terms);
let emphasis = round3((rel * growth_weight).min(1.0));
(name.to_string(), emphasis, essence.to_string())
}
/// blend over profiles + growth weights → {voice, dimensions, guidance}.
/// `profiles`: [{name, terms, essence}]; `growth_weights`: {name: weight}.
pub fn blend(message: &str, profiles: &[Value], growth_weights: &Map<String, Value>) -> Value {
let low = message.trim().to_lowercase();
let mut scored: Vec<(String, f64, String)> = profiles
.iter()
.map(|p| {
let name = p.get("name").and_then(Value::as_str).unwrap_or("");
let essence = p.get("essence").and_then(Value::as_str).unwrap_or("");
let terms: Vec<String> = p
.get("terms")
.and_then(Value::as_array)
.map(|ts| {
ts.iter()
.map(|t| t.as_str().unwrap_or("").to_string())
.collect()
})
.unwrap_or_default();
let g = growth_weights
.get(name)
.and_then(Value::as_f64)
.unwrap_or(1.0);
score_dimension(name, essence, &terms, &low, g)
})
.collect();
// Python `list.sort` is stable; the key is -emphasis. A stable
// descending sort keeps profile (insertion) order among ties.
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let has_hits = scored.iter().any(|s| s.1 > 0.0);
if has_hits {
for s in &mut scored {
if s.1 > 0.0 {
s.1 = s.1.max(0.15);
} else {
s.1 = 0.05;
}
}
} else {
for s in &mut scored {
s.1 = 0.2;
}
}
let total: f64 = scored.iter().map(|s| s.1).sum();
let total = if total == 0.0 { 1.0 } else { total };
for s in &mut scored {
s.1 = round3(s.1 / total);
}
let mut voice_parts: Vec<&str> = Vec::new();
for s in &scored {
if s.1 >= 0.15 {
voice_parts.push(s.2.as_str());
}
}
let voice = if voice_parts.is_empty() {
scored
.first()
.map(|s| s.2.clone())
.unwrap_or_else(|| "A unified voice, drawing from all dimensions of personality.".into())
} else {
voice_parts.join(" ")
};
let mut dimensions = Vec::new();
for s in &scored {
let mut m = Map::new();
m.insert("name".into(), s.0.clone().into());
m.insert("emphasis".into(), s.1.into());
m.insert("essence".into(), s.2.clone().into());
dimensions.push(Value::Object(m));
}
let mut guidance: Vec<Value> = vec![format!("- Unified voice: {}", truncate_chars(&voice, 200)).into()];
for s in &scored {
if s.1 >= 0.15 {
guidance.push(format!(" {}: {}", s.0, format_percent(s.1)).into());
}
}
let mut out = Map::new();
out.insert("voice".into(), voice.into());
out.insert("dimensions".into(), dimensions.into());
out.insert("guidance".into(), guidance.into());
Value::Object(out)
}
// ── always ────────────────────────────────────────────────────────────────
/// always over profiles → {dimensions, guidance, note}.
pub fn always(profiles: &[Value]) -> Value {
let names: Vec<Value> = profiles
.iter()
.map(|p| {
p.get("name")
.and_then(Value::as_str)
.unwrap_or("")
.into()
})
.collect();
let essences: Vec<&str> = profiles
.iter()
.map(|p| p.get("essence").and_then(Value::as_str).unwrap_or(""))
.collect();
let fused = essences.join(" ");
let guidance: Vec<Value> = vec![
"- This is ONE voice, not many. The influences below have been absorbed into who you are — they don't take turns, they don't compete. They combine into something original.".into(),
format!("- The voice: {}", truncate_chars(&fused, 300)).into(),
"- The more sources absorbed, the more original the voice becomes. Each new influence adds depth, not confusion.".into(),
];
let mut out = Map::new();
out.insert("dimensions".into(), names.into());
out.insert("guidance".into(), guidance.into());
out.insert(
"note".into(),
"All dimensions are always present. Context shifts their emphasis, never their existence.".into(),
);
Value::Object(out)
}
// ── grow ──────────────────────────────────────────────────────────────────
// (The weight-update half of grow: turns+1, the good/bad/neutral delta
// clamped to [0.2, 3.0] and rounded to 3, the last-40 event ring. The
// growth STATE arrives Python-side; the UPDATED state returns for the
// facade to save atomically. `note` is char-truncated to 80.)
/// grow over a growth state → (updated_state, result). The state map is
/// mutated in place (the facade serializes what returns).
pub fn grow(state: &mut Map<String, Value>, source: &str, outcome: &str, note: &str) -> Value {
let turns = state.get("turns").and_then(Value::as_i64).unwrap_or(0) + 1;
state.insert("turns".into(), turns.into());
let delta = match outcome {
"good" => 0.12,
"bad" => -0.12,
_ => 0.0,
};
let mut pending_event: Option<Map<String, Value>> = None;
let new_weight_out: f64;
{
let weights = state
.entry("source_weight".to_string())
.or_insert_with(|| Value::Object(Map::new()));
if !weights.is_object() {
*weights = Value::Object(Map::new());
}
let wmap = weights.as_object_mut().expect("source_weight object");
let cur = wmap.get(source).and_then(Value::as_f64).unwrap_or(1.0);
if delta != 0.0 {
let new_w = round3(cur + delta).clamp(0.2, 3.0);
wmap.insert(source.into(), new_w.into());
let mut ev = Map::new();
ev.insert("source".into(), source.into());
ev.insert("outcome".into(), outcome.into());
ev.insert("turn".into(), turns.into());
ev.insert("note".into(), truncate_chars(note, 80).into());
pending_event = Some(ev);
}
new_weight_out = wmap.get(source).and_then(Value::as_f64).unwrap_or(1.0);
}
if let Some(ev) = pending_event {
let events = state
.entry("events".to_string())
.or_insert_with(|| Value::Array(Vec::new()));
if !events.is_array() {
*events = Value::Array(Vec::new());
}
let evts = events.as_array_mut().expect("events array");
evts.push(Value::Object(ev));
while evts.len() > 40 {
evts.remove(0);
}
}
let new_weight = new_weight_out;
let mut result = Map::new();
result.insert("source".into(), source.into());
result.insert("outcome".into(), outcome.into());
result.insert("new_weight".into(), new_weight.into());
Value::Object(result)
}
// ── handle_hook + PLUGIN + paths ──────────────────────────────────────────
/// handle_hook — the default envelope: {"status", "event", "handler"}.
/// `data` is accepted and ignored, exactly like the oracle.
pub fn handle_hook(event: &str) -> Value {
json!({"status": "ok", "event": event, "handler": "default"})
}
/// The module PLUGIN descriptor (frozen data port).
pub fn plugin() -> Value {
json!({
"name": "voice_hybrid",
"version": "1.0.0",
"description": "voice_hybrid.py — Rune's unified voice, with dimensional emphasis.",
"capabilities": ["voice", "voice_hybrid", "growth", "warm_tier", "status", "memory", "memory_env"],
"hooks": {},
"dependencies": [],
"provides": [],
"priority": 100,
"auto_activate": true,
})
}
/// STORE_DB resolution (mirrors memory_env: RUNE_STORE_DB override → else
/// memory/store/memory.db under RUNE_MEMORY_DIR / rune_home()/memory).
pub fn store_db_path() -> String {
if let Ok(p) = std::env::var("RUNE_STORE_DB") {
if !p.is_empty() {
return p;
}
}
format!("{}/store/memory.db", memory_dir())
}
/// memory_dir resolution (mirrors memory_env.memory_dir).
pub fn memory_dir() -> String {
if let Ok(p) = std::env::var("RUNE_MEMORY_DIR") {
if !p.is_empty() {
return p;
}
}
let home = std::env::var("RUNE_HOME").unwrap_or_else(|_| "/mnt/workdrive/rune".into());
format!("{home}/memory")
}
/// voice_state.json resolution: <memory_dir>/index/voice_state.json.
pub fn growth_path() -> String {
format!("{}/index/voice_state.json", memory_dir())
}
/// W6-era surface (kept; no oracle twin): classify a voice command.
pub fn process_voice_command(text: &str) -> Value {
let lower = text.to_lowercase();
if lower.contains("speak") || lower.contains("say") {
json!({"action": "synthesize", "text": text})
} else if lower.contains("listen") || lower.contains("record") {
json!({"action": "record", "text": text})
} else {
json!({"action": "none", "text": text})
}
}
// ── IO-seam traits ────────────────────────────────────────────────────────
// (Phase B seam law: the `_connect` / `_load_growth` / `_save_growth`
// boundary the facade owns. The default impls are unreachable — IO never
// executes in-kernel — and exist so the seam has a name-preserving shape.)
/// `_connect`: SQLite open + row_factory (facade-owned).
pub trait ConnectSeam {
fn connect(&self) -> Result<(), String> {
Err("_connect executes facade-side; the kernel receives rows".into())
}
}
/// `_load_growth` / `_save_growth`: growth-file IO (facade-owned).
pub trait GrowthSeam {
fn load_growth(&self) -> Result<Value, String> {
Err("_load_growth executes facade-side; the kernel receives growth".into())
}
fn save_growth(&self, _state: &Value) -> Result<(), String> {
Err("_save_growth executes facade-side; the kernel returns state".into())
}
}
/// The default (unreachable) seam impl: documents that this crate owns the
/// decision math while `_connect` / `_load_growth` / `_save_growth` stay
/// facade-side.
pub struct FacadeSeams;
impl ConnectSeam for FacadeSeams {}
impl GrowthSeam for FacadeSeams {}
/// `_vh_core`: the delegation probe (facade-owned — the RUNE_VOICE_HYBRID_CORE
/// pin is honored inside vh_loader.core; the kernel has no env to read).
/// Named here so the seam boundary carries the probe's contract.
///
/// The probe answers one question: is the kernel reachable?
/// `VhCoreSeam::probe` returns false in-kernel by construction (the probe
/// executes facade-side); the facade's `_vh_core` returns the loader when
/// the cdylib loads and the pin is not set.
pub struct VhCoreSeam;
impl VhCoreSeam {
pub fn probe() -> bool {
false
}
}
// ── FFI ───────────────────────────────────────────────────────────────────
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
if ptr.is_null() {
return Err("null argument pointer".into());
}
let x = unsafe { CStr::from_ptr(ptr) }
.to_str()
.map_err(|e| e.to_string())?;
serde_json::from_str(x).map_err(|e| e.to_string())
}
fn envelope(r: Result<Value, String>) -> *mut c_char {
let v = match r {
Ok(x) => json!({"ok": true, "value": x}),
Err(e) => json!({"ok": false, "err": e}),
};
CString::new(serde_json::to_string(&v).unwrap_or_default())
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
fn str_field(row: &Value, key: &str) -> String {
match row.get(key) {
Some(Value::String(s)) => s.clone(),
_ => String::new(),
}
}
fn str_list(v: &Value) -> Vec<String> {
v.as_array()
.map(|a| {
a.iter()
.map(|t| t.as_str().unwrap_or("").to_string())
.collect()
})
.unwrap_or_default()
}
/// vh_relevance({message_low, terms}) -> the relevance float.
#[no_mangle]
pub extern "C" fn vh_relevance(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let msg = str_field(&a, "message_low");
let terms = str_list(a.get("terms").unwrap_or(&Value::Null));
relevance(&msg, &terms).into()
}))
}
/// vh_profiles({rows}) -> the profile list. Rows are the fetched entry
/// dicts (only "text" is read — the SQL WHERE stays Python).
#[no_mangle]
pub extern "C" fn vh_profiles(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let rows: Vec<Value> = a
.get("rows")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
Value::Array(source_profiles(&rows))
}))
}
/// vh_blend({message, rows, growth}) -> {voice, dimensions, guidance}.
/// Profiles are computed in-kernel from rows (single FFI hop); the growth
/// weights come from the facade-loaded growth state.
#[no_mangle]
pub extern "C" fn vh_blend(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let message = str_field(&a, "message");
let rows: Vec<Value> = a
.get("rows")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let profiles = source_profiles(&rows);
let empty = Map::new();
let weights = a
.get("growth")
.and_then(|g| g.get("source_weight"))
.and_then(Value::as_object)
.unwrap_or(&empty);
blend(&message, &profiles, weights)
}))
}
/// vh_always({rows}) -> {dimensions, guidance, note}.
#[no_mangle]
pub extern "C" fn vh_always(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let rows: Vec<Value> = a
.get("rows")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
always(&source_profiles(&rows))
}))
}
/// vh_grow({state, source, outcome, note}) -> {state, result}. The updated
/// state returns for the facade to save atomically (IO stays Python).
#[no_mangle]
pub extern "C" fn vh_grow(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let mut state: Map<String, Value> = a
.get("state")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let source = str_field(&a, "source");
let outcome = str_field(&a, "outcome");
let note = str_field(&a, "note");
let result = grow(&mut state, &source, &outcome, &note);
let mut out = Map::new();
out.insert("state".into(), Value::Object(state));
out.insert("result".into(), result);
Value::Object(out)
}))
}
/// vh_handle_hook({event, data}) -> the default envelope.
#[no_mangle]
pub extern "C" fn vh_handle_hook(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let event = str_field(&a, "event");
handle_hook(&event)
}))
}
/// vh_plugin() -> the PLUGIN descriptor.
#[no_mangle]
pub extern "C" fn vh_plugin() -> *mut c_char {
envelope(Ok(plugin()))
}
#[cfg(test)]
#[path = "branch_battery.rs"]
mod branch_battery;
+148
View File
@@ -0,0 +1,148 @@
"""Tests: voice_hybrid.py Rust core port (dichotomy wave, #2479).
Delegation + fallback-pin discipline: the relevance scoring, profile hit
counting, blend composition, always baseline, grow weight update, and hook
envelope come from the Rust core when available;
RUNE_VOICE_HYBRID_CORE=python pins the Python oracle (the inline _py_*
fallbacks).
"""
from __future__ import annotations
import json
import os
import sqlite3
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from rune import vh_loader # noqa: E402
from rune import voice_hybrid as vh # noqa: E402
@pytest.fixture()
def isolated_env(tmp_path, monkeypatch):
home = tmp_path / "home"
(home / "store").mkdir(parents=True)
(home / "memory" / "index").mkdir(parents=True)
monkeypatch.setenv("RUNE_MEMORY_DIR", str(home / "memory"))
monkeypatch.setenv("RUNE_STORE_DB", str(home / "store" / "memory.db"))
# vh resolves STORE_DB at import; re-point it at THIS test's home.
monkeypatch.setattr(vh, "STORE_DB", str(home / "store" / "memory.db"))
db = sqlite3.connect(str(home / "store" / "memory.db"))
try:
db.execute(
"CREATE TABLE entries (id INTEGER PRIMARY KEY AUTOINCREMENT,"
" text TEXT, topic TEXT, status TEXT)"
)
db.executemany(
"INSERT INTO entries (text, topic, status) VALUES (?, ?, 'active')",
[
("Alfred keeps his composure with dry wry understatement at tea time", "persona"),
("Bayonetta dances with sassy cheeky glamour, darling", "identity"),
("The cosmos invites wonder and skepticism about the universe", "delivery"),
("Elementary deduction: observe the clue and deduce the pattern", "operating"),
],
)
db.commit()
finally:
db.close()
yield home
@pytest.fixture()
def core_built():
if not vh_loader.available():
pytest.skip("rust voice_hybrid core not built")
return True
def _norm(x):
if isinstance(x, float) and x.is_integer() and abs(x) < 9.0e15:
return int(x)
if isinstance(x, list):
return [_norm(v) for v in x]
if isinstance(x, dict):
return {k: _norm(v) for k, v in x.items()}
return x
def _j(x):
return json.dumps(_norm(x), sort_keys=True, ensure_ascii=True)
def test_loader_probe_and_pin(core_built, monkeypatch):
monkeypatch.delenv("RUNE_VOICE_HYBRID_CORE", raising=False)
assert vh_loader.core() is not None
assert vh._vh_core() is not None
monkeypatch.setenv("RUNE_VOICE_HYBRID_CORE", "python")
assert vh_loader.core() is None
assert vh._vh_core() is None
def test_relevance_delegates(core_built, monkeypatch, isolated_env):
monkeypatch.delenv("RUNE_VOICE_HYBRID_CORE", raising=False)
terms = vh._DIMENSIONS["composure"]["terms"]
assert vh._relevance("alfred at tea", terms) == vh_loader.relevance("alfred at tea", terms)
def test_relevance_pin_matches_inline_oracle(core_built, monkeypatch, isolated_env):
monkeypatch.delenv("RUNE_VOICE_HYBRID_CORE", raising=False)
terms = vh._DIMENSIONS["wonder"]["terms"]
live = vh._relevance("the cosmos and stars", terms)
monkeypatch.setenv("RUNE_VOICE_HYBRID_CORE", "python")
pinned = vh._relevance("the cosmos and stars", terms)
assert _j(live) == _j(pinned)
def test_blend_delegates_and_pins(core_built, monkeypatch, isolated_env):
monkeypatch.delenv("RUNE_VOICE_HYBRID_CORE", raising=False)
live = vh.blend("alfred kept his composure with dry wry understatement")
assert set(live) == {"voice", "dimensions", "guidance"}
monkeypatch.setenv("RUNE_VOICE_HYBRID_CORE", "python")
pinned = vh.blend("alfred kept his composure with dry wry understatement")
assert _j(live) == _j(pinned)
def test_always_delegates_and_pins(core_built, monkeypatch, isolated_env):
monkeypatch.delenv("RUNE_VOICE_HYBRID_CORE", raising=False)
live = vh.always()
assert set(live) == {"dimensions", "guidance", "note"}
monkeypatch.setenv("RUNE_VOICE_HYBRID_CORE", "python")
pinned = vh.always()
assert _j(live) == _j(pinned)
def test_grow_delegates_and_pins(core_built, monkeypatch, isolated_env):
monkeypatch.delenv("RUNE_VOICE_HYBRID_CORE", raising=False)
live = vh.grow("composure", "good")
assert live["new_weight"] == 1.12
monkeypatch.setenv("RUNE_VOICE_HYBRID_CORE", "python")
# Reset the growth file so the pinned run starts from the same state.
growth_path = isolated_env / "memory" / "index" / "voice_state.json"
if growth_path.exists():
growth_path.unlink()
pinned = vh.grow("composure", "good")
assert _j(live) == _j(pinned)
def test_kernel_failure_falls_back(core_built, monkeypatch, isolated_env):
monkeypatch.delenv("RUNE_VOICE_HYBRID_CORE", raising=False)
assert vh._vh_core() is not None
monkeypatch.setattr(vh_loader, "core", lambda: (_ for _ in ()).throw(RuntimeError("boom")))
# Every delegating surface degrades to the inline oracle, never raises.
assert vh._relevance("alfred", ["alfred"]) == 1.0
assert isinstance(vh.source_profiles(), list)
assert set(vh.blend("alfred")) == {"voice", "dimensions", "guidance"}
assert set(vh.always()) == {"dimensions", "guidance", "note"}
assert vh.grow("wonder", "neutral")["new_weight"] == 1.0
assert vh.handle_hook("tick") == {"status": "ok", "event": "tick", "handler": "default"}
def test_plugin_shape():
assert vh.PLUGIN["name"] == "voice_hybrid"
assert vh_loader.plugin()["name"] == "voice_hybrid"