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

This commit is contained in:
Nick committed 2026-10-12 01:28:04 +01:00
1 parent e49eefe926
commit 7d049cc42c
8 files changed
+1635 -192

No files matched your search

+58 -5
View File
@@ -23,14 +23,41 @@ SHARD_INDEX = os.path.expanduser("~/.config/opencode/memory/shard_index.json")
class ShardManager:
"""Manages topic-based SQLite shards for unbounded memory growth."""
"""Manages topic-based SQLite shards for unbounded memory growth.
FULL Rust core (src/rust/shard_manager) when built — strangler law:
RUNE_SM_CORE=python pins the Python fallback, which remains the parity
oracle. The sqlite/sqlite-vec resource boundary is served as ops by
sm_loader; connection construction stays Python-side."""
def __init__(self):
core = self._core()
if core is not None:
self.index = core.init(SHARD_DIR, SHARD_INDEX)
return
os.makedirs(SHARD_DIR, exist_ok=True)
self.index = self._load_index()
@staticmethod
def _core():
"""The Rust shard_manager core when available (strangler law).
RUNE_SM_CORE=python pins the fallback."""
if os.environ.get("RUNE_SM_CORE", "auto") == "python":
return None
try:
from rune import sm_loader
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
return None
try:
return sm_loader if sm_loader.available() else None
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
return None
def _load_index(self) -> Dict[str, str]:
"""Load shard index from disk."""
core = self._core()
if core is not None:
return core.load_index(SHARD_INDEX)
if os.path.exists(SHARD_INDEX):
try:
with open(SHARD_INDEX) as f:
@@ -41,11 +68,20 @@ class ShardManager:
def _save_index(self):
"""Save shard index to disk."""
core = self._core()
if core is not None:
core.save_index(SHARD_INDEX, self.index)
return
with open(SHARD_INDEX, "w") as f:
json.dump(self.index, f, indent=2)
def create_shard(self, topic: str) -> str:
"""Create a new shard for a topic. Returns shard path."""
core = self._core()
if core is not None:
res = core.create_shard(SHARD_DIR, SHARD_INDEX, topic, self.index)
self.index = res["index"]
return res["shard_path"]
shard_name = f"shard_{topic.replace(' ', '_').replace('/', '_')}.db"
shard_path = os.path.join(SHARD_DIR, shard_name)
@@ -94,6 +130,9 @@ class ShardManager:
Returns the topic name. Falls back to 'general' if no match.
"""
core = self._core()
if core is not None:
return core.route_query(query, self.index)
query_lower = query.lower()
# Check existing shards for keyword matches
@@ -125,10 +164,15 @@ class ShardManager:
extension loaded PER CONNECTION — a fresh connection without it cannot
see or query entries_vec, even though the schema created it ("no such
table" in clean environments). Mirror memory_store.connect()."""
if topic not in self.index:
self.create_shard(topic)
shard_path = self.index[topic]
core = self._core()
if core is not None:
res = core.get_shard(SHARD_DIR, SHARD_INDEX, topic, self.index)
self.index = res["index"]
shard_path = res["shard_path"]
else:
if topic not in self.index:
self.create_shard(topic)
shard_path = self.index[topic]
db = sqlite3.connect(shard_path)
db.row_factory = sqlite3.Row
db.enable_load_extension(True)
@@ -142,6 +186,9 @@ class ShardManager:
def merge_results(self, results_list: List[List[dict]]) -> List[dict]:
"""Merge results from multiple shards, deduplicate, sort by relevance."""
core = self._core()
if core is not None:
return core.merge_results(results_list)
seen = set()
merged = []
@@ -158,6 +205,9 @@ class ShardManager:
def get_shard_stats(self) -> Dict[str, int]:
"""Get entry counts per shard."""
core = self._core()
if core is not None:
return core.get_shard_stats(self.index)
stats = {}
for topic, path in self.index.items():
try:
@@ -171,6 +221,9 @@ class ShardManager:
def total_entries(self) -> int:
"""Get total entries across all shards."""
core = self._core()
if core is not None:
return core.total_entries(self.index)
stats = self.get_shard_stats()
return sum(stats.values())
+205 -29
View File
@@ -1,9 +1,27 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust structural_model core (facade burn-down).
"""ctypes loader for the FULL Rust shard_manager core (dichotomy wave,
issue #2479 tranche). Mirrors broker_loader.py's contract:
FFI contract (doc-envelope law):
* sm_query({relationships, eid, op}) -> list/str/None
* sm_free_string(ptr)
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
* sm_init({shard_dir, index_path}) -> index dict
* sm_load_index({index_path}) -> index dict
* sm_save_index({index_path, index}) -> null
* sm_create_shard({shard_dir, index_path, topic, index}, ops_cb)
-> {shard_path, index}
* sm_route_query({query, index}) -> topic string
* sm_merge_results({results_list}) -> merged list
* sm_get_shard({shard_dir, index_path, topic, index}, ops_cb)
-> {shard_path, index}
* sm_get_shard_stats({index}, ops_cb) -> stats dict
* sm_total_entries({index}, ops_cb) -> int
Ops seam (resource boundary the kernel cannot own): the Rust kernel issues
`ensure_schema` / `count_entries` ops through a C callback; THIS loader
serves them against real sqlite3 + sqlite_vec. Connection construction
stays Python-side (see shard_manager.ShardManager.get_shard).
Free: capability_scoring's cs_free_string (shared process). Override the
.so with SM_RUST_LIB (a FILE path).
"""
from __future__ import annotations
@@ -11,19 +29,20 @@ from __future__ import annotations
import ctypes
import json
import os
import sqlite3
from pathlib import Path
_REPO = Path(__file__).resolve().parents[2]
_CRATE_DIR = _REPO / "src" / "rust" / "structural_model"
_LIB_PATH = Path(
os.environ.get(
"SM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libstructural_model.so"
)
)
_CRATE_DIR = _REPO / "src" / "rust" / "shard_manager"
_LIB_PATH = Path(os.environ.get("SM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libshard_manager.so"))
_lib = None
class ShardCoreUnavailable(FileNotFoundError):
"""The Rust shard_manager core is not built. Callers fall back to Python."""
def available() -> bool:
return _LIB_PATH.is_file()
@@ -32,32 +51,189 @@ def _load() -> ctypes.CDLL:
global _lib
if _lib is not None:
return _lib
if not _LIB_PATH.exists():
raise FileNotFoundError(f"structural_model not built: {_LIB_PATH}")
if not _LIB_PATH.is_file():
raise ShardCoreUnavailable(str(_LIB_PATH))
lib = ctypes.CDLL(str(_LIB_PATH))
lib.sm_free_string.argtypes = [ctypes.c_void_p]
lib.sm_free_string.restype = None
fn = lib.sm_query
fn.argtypes = [ctypes.c_char_p]
fn.restype = ctypes.c_void_p
for sym in (
"sm_init",
"sm_load_index",
"sm_save_index",
"sm_route_query",
"sm_merge_results",
):
fn = getattr(lib, sym)
fn.argtypes = [ctypes.c_void_p]
fn.restype = ctypes.c_void_p
for sym in (
"sm_create_shard",
"sm_get_shard",
"sm_get_shard_stats",
"sm_total_entries",
):
fn = getattr(lib, sym)
fn.argtypes = [ctypes.c_void_p, _OPS_CB]
fn.restype = ctypes.c_void_p
# free comes from capability_scoring (same process) if loaded; else our own malloc path
try:
lib.cs_free_string
except AttributeError:
scoring = ctypes.CDLL(
str(_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so")
)
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
lib._free = scoring.cs_free_string
_lib = lib
return lib
def _call(sym: str, payload: dict):
def _call(sym: str, payload: dict, with_ops: bool = False):
lib = _load()
ptr = getattr(lib, sym)(json.dumps(payload).encode("utf-8"))
data = json.dumps(payload).encode()
buf = ctypes.create_string_buffer(data)
args = [ctypes.cast(buf, ctypes.c_void_p)]
if with_ops:
args.append(_ops_cb)
ptr = getattr(lib, sym)(*args)
if not ptr:
raise RuntimeError(f"{sym} returned null")
try:
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
finally:
lib.sm_free_string(ptr)
env = json.loads(text)
if not env.get("ok"):
raise RuntimeError(f"{sym} core error: {env.get('err')}")
return env["value"]
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 query(relationships: list, eid: str, op: str):
return _call("sm_query", {"relationships": relationships, "eid": eid, "op": op})
# ── ops seam: the loader serves the kernel's sqlite ops ──────────────────────
_OPS_CB = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)
_last_ops_buf = None
SCHEMA = """
CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
importance REAL DEFAULT 0.5,
created_at TEXT NOT NULL,
last_accessed TEXT,
topic TEXT DEFAULT '',
source TEXT DEFAULT '',
status TEXT DEFAULT 'active',
wash_count INTEGER DEFAULT 0,
last_washed TEXT,
rock_status TEXT DEFAULT 'washable',
tier TEXT DEFAULT 'warm'
);
CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(
text, content='', tokenize='porter unicode61'
);
CREATE VIRTUAL TABLE IF NOT EXISTS entries_vec USING vec0(
embedding float[256]
);
CREATE INDEX IF NOT EXISTS idx_entries_status ON entries(status);
CREATE INDEX IF NOT EXISTS idx_entries_topic ON entries(topic);
CREATE INDEX IF NOT EXISTS idx_entries_wash_count ON entries(wash_count);
CREATE INDEX IF NOT EXISTS idx_entries_tier ON entries(tier);
"""
def _execute_op(op: dict) -> dict:
kind = op.get("op")
args = op.get("args") or {}
if kind == "ensure_schema":
# mirror oracle create_shard lines 55-86 (sqlite + sqlite_vec + schema)
path = args["path"]
db = sqlite3.connect(path)
try:
db.enable_load_extension(True)
import sqlite_vec
sqlite_vec.load(db) # #26: entries_vec virtual table needs the extension per connection
db.executescript(SCHEMA)
finally:
db.close()
return {"ok": True}
if kind == "count_entries":
# mirror oracle get_shard_stats lines 163-169 (plain connect; any
# failure degrades to the except branch → kernel writes 0)
try:
path = args["path"]
db = sqlite3.connect(path)
try:
count = db.execute("SELECT COUNT(*) FROM entries WHERE status='active'").fetchone()[0]
finally:
db.close()
return {"ok": True, "value": count}
except Exception: # noqa: BLE001 — deliberate: best-effort degrade, crash would be worse
return {"ok": False}
return {"ok": False, "err": f"unknown op {kind!r}"}
def _ops_handler(op_ptr) -> int:
global _last_ops_buf
op = json.loads(ctypes.string_at(op_ptr).decode())
result = _execute_op(op)
_last_ops_buf = ctypes.create_string_buffer(json.dumps(result).encode())
return ctypes.cast(_last_ops_buf, ctypes.c_void_p).value
_ops_cb = _OPS_CB(_ops_handler)
# ── typed public API ─────────────────────────────────────────────────────────
def init(shard_dir: str, index_path: str) -> dict:
return _call("sm_init", {"shard_dir": shard_dir, "index_path": index_path})
def load_index(index_path: str) -> dict:
return _call("sm_load_index", {"index_path": index_path})
def save_index(index_path: str, index: dict) -> None:
_call("sm_save_index", {"index_path": index_path, "index": index})
def create_shard(shard_dir: str, index_path: str, topic: str, index: dict) -> dict:
return _call(
"sm_create_shard",
{
"shard_dir": shard_dir,
"index_path": index_path,
"topic": topic,
"index": index,
},
with_ops=True,
)
def route_query(query: str, index: dict) -> str:
return _call("sm_route_query", {"query": query, "index": index})
def merge_results(results_list: list) -> list:
return _call("sm_merge_results", {"results_list": results_list})
def get_shard(shard_dir: str, index_path: str, topic: str, index: dict) -> dict:
return _call(
"sm_get_shard",
{
"shard_dir": shard_dir,
"index_path": index_path,
"topic": topic,
"index": index,
},
with_ops=True,
)
def get_shard_stats(index: dict) -> dict:
return _call("sm_get_shard_stats", {"index": index}, with_ops=True)
def total_entries(index: dict) -> int:
return _call("sm_total_entries", {"index": index}, with_ops=True)
+3 -94
View File
@@ -3,103 +3,12 @@
version = 4
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
name = "capability_scoring"
version = "0.1.0"
[[package]]
name = "shard_manager"
version = "0.1.0"
dependencies = [
"serde_json",
"capability_scoring",
]
[[package]]
name = "syn"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+1 -1
View File
@@ -7,4 +7,4 @@ edition = "2021"
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde_json = "1"
capability_scoring = { path = "../capability_scoring" }
+752
View File
@@ -0,0 +1,752 @@
{
"module": "shard_manager",
"pinned": "RUNE_SM_CORE=python",
"oracle": "src/rune/shard_manager.py",
"cases": [
{
"id": "load_missing",
"fn": "_load_index",
"setup": {
"dirs": [
"shards"
]
},
"input": {
"index_path": "shard_index.json"
},
"ops": [],
"expected": {},
"effects": {},
"created_files": []
},
{
"id": "load_corrupt",
"fn": "_load_index",
"setup": {
"dirs": [
"shards"
],
"index_file": "CORRUPT"
},
"input": {
"index_path": "shard_index.json"
},
"ops": [],
"expected": {},
"effects": {
"index_file": "CORRUPT"
},
"created_files": []
},
{
"id": "load_valid",
"fn": "_load_index",
"setup": {
"dirs": [
"shards"
],
"index_file": {
"alpha": "shards/shard_alpha.db",
"beta": "shards/shard_beta.db"
}
},
"input": {
"index_path": "shard_index.json"
},
"ops": [],
"expected": {
"alpha": "shards/shard_alpha.db",
"beta": "shards/shard_beta.db"
},
"effects": {
"index_file": {
"alpha": "shards/shard_alpha.db",
"beta": "shards/shard_beta.db"
}
},
"created_files": []
},
{
"id": "init_creates_dir",
"fn": "__init__",
"setup": {},
"input": {
"shard_dir": "shards",
"index_path": "shard_index.json"
},
"ops": [],
"expected": {
"index": {},
"dir_exists": true
},
"effects": {},
"created_files": []
},
{
"id": "create_fresh",
"fn": "create_shard",
"setup": {
"dirs": [
"shards"
]
},
"input": {
"shard_dir": "shards",
"index_path": "shard_index.json",
"topic": "deployment",
"index": {}
},
"ops": [
{
"op": "ensure_schema",
"args": {
"path": "shards/shard_deployment.db"
},
"result": null,
"ok": true
}
],
"expected": "shards/shard_deployment.db",
"effects": {
"index_file": {
"deployment": "shards/shard_deployment.db"
}
},
"created_files": [
"shards/shard_deployment.db"
]
},
{
"id": "create_file_exists",
"fn": "create_shard",
"setup": {
"dirs": [
"shards"
],
"files": [
"shards/shard_deployment.db"
]
},
"input": {
"shard_dir": "shards",
"index_path": "shard_index.json",
"topic": "deployment",
"index": {
"other": "shards/shard_other.db"
}
},
"ops": [],
"expected": "shards/shard_deployment.db",
"effects": {},
"created_files": [
"shards/shard_deployment.db"
]
},
{
"id": "create_name_normalize",
"fn": "create_shard",
"setup": {
"dirs": [
"shards"
]
},
"input": {
"shard_dir": "shards",
"index_path": "shard_index.json",
"topic": "guitar academy/basics",
"index": {}
},
"ops": [
{
"op": "ensure_schema",
"args": {
"path": "shards/shard_guitar_academy_basics.db"
},
"result": null,
"ok": true
}
],
"expected": "shards/shard_guitar_academy_basics.db",
"effects": {
"index_file": {
"guitar academy/basics": "shards/shard_guitar_academy_basics.db"
}
},
"created_files": [
"shards/shard_guitar_academy_basics.db"
]
},
{
"id": "create_topic_in_index",
"fn": "create_shard",
"setup": {
"dirs": [
"shards"
],
"files": [
"shards/shard_reuse.db"
]
},
"input": {
"shard_dir": "shards",
"index_path": "shard_index.json",
"topic": "reuse",
"index": {
"reuse": "shards/shard_reuse.db"
}
},
"ops": [],
"expected": "shards/shard_reuse.db",
"effects": {},
"created_files": [
"shards/shard_reuse.db"
]
},
{
"id": "route_overlap",
"fn": "route_query",
"setup": {},
"input": {
"query": "how is deployment going",
"index": {
"deployment": "p1",
"general": "p2"
}
},
"ops": [],
"expected": "deployment",
"effects": {},
"created_files": []
},
{
"id": "route_tie_first_wins",
"fn": "route_query",
"setup": {},
"input": {
"query": "b d",
"index": {
"a_b": "p1",
"c_d": "p2"
}
},
"ops": [],
"expected": "a_b",
"effects": {},
"created_files": []
},
{
"id": "route_multiword_beats",
"fn": "route_query",
"setup": {},
"input": {
"query": "machine learning basics",
"index": {
"general": "p1",
"machine_learning": "p2"
}
},
"ops": [],
"expected": "machine_learning",
"effects": {},
"created_files": []
},
{
"id": "route_no_match_long_words",
"fn": "route_query",
"setup": {},
"input": {
"query": "what is my progress today",
"index": {}
},
"ops": [],
"expected": "what_progress_today",
"effects": {},
"created_files": []
},
{
"id": "route_no_match_short",
"fn": "route_query",
"setup": {},
"input": {
"query": "hi",
"index": {}
},
"ops": [],
"expected": "general",
"effects": {},
"created_files": []
},
{
"id": "route_empty",
"fn": "route_query",
"setup": {},
"input": {
"query": "",
"index": {}
},
"ops": [],
"expected": "general",
"effects": {},
"created_files": []
},
{
"id": "route_case_insensitive",
"fn": "route_query",
"setup": {},
"input": {
"query": "DEPLOYMENT status",
"index": {
"deployment": "p1"
}
},
"ops": [],
"expected": "deployment",
"effects": {},
"created_files": []
},
{
"id": "merge_dedupe_prefix",
"fn": "merge_results",
"setup": {},
"input": {
"results_list": [
[
{
"text": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxaaa",
"relevance": 1
},
{
"text": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxbbb",
"relevance": 2
}
],
[
{
"text": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxccc",
"relevance": 3
}
]
]
},
"ops": [],
"expected": [
{
"text": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxaaa",
"relevance": 1
}
],
"effects": {},
"created_files": []
},
{
"id": "merge_missing_text",
"fn": "merge_results",
"setup": {},
"input": {
"results_list": [
[
{
"wash_count": 2
},
{
"text": "",
"x": 1
}
]
]
},
"ops": [],
"expected": [
{
"wash_count": 2
}
],
"effects": {},
"created_files": []
},
{
"id": "merge_sort_relevance",
"fn": "merge_results",
"setup": {},
"input": {
"results_list": [
[
{
"text": "a",
"relevance": 1
}
],
[
{
"text": "b",
"relevance": 5
}
],
[
{
"text": "c",
"relevance": 3
}
]
]
},
"ops": [],
"expected": [
{
"text": "b",
"relevance": 5
},
{
"text": "c",
"relevance": 3
},
{
"text": "a",
"relevance": 1
}
],
"effects": {},
"created_files": []
},
{
"id": "merge_sort_wash_count",
"fn": "merge_results",
"setup": {},
"input": {
"results_list": [
[
{
"text": "a",
"wash_count": 2
}
],
[
{
"text": "b",
"wash_count": 7
}
]
]
},
"ops": [],
"expected": [
{
"text": "b",
"wash_count": 7
},
{
"text": "a",
"wash_count": 2
}
],
"effects": {},
"created_files": []
},
{
"id": "merge_ties_stable",
"fn": "merge_results",
"setup": {},
"input": {
"results_list": [
[
{
"text": "a"
}
],
[
{
"text": "b"
}
],
[
{
"text": "c"
}
]
]
},
"ops": [],
"expected": [
{
"text": "a"
},
{
"text": "b"
},
{
"text": "c"
}
],
"effects": {},
"created_files": []
},
{
"id": "merge_empty",
"fn": "merge_results",
"setup": {},
"input": {
"results_list": [
[],
[]
]
},
"ops": [],
"expected": [],
"effects": {},
"created_files": []
},
{
"id": "merge_mixed_keys",
"fn": "merge_results",
"setup": {},
"input": {
"results_list": [
[
{
"text": "a",
"wash_count": 1
},
{
"text": "b",
"relevance": 0.5
}
],
[
{
"text": "c"
}
]
]
},
"ops": [],
"expected": [
{
"text": "a",
"wash_count": 1
},
{
"text": "b",
"relevance": 0.5
},
{
"text": "c"
}
],
"effects": {},
"created_files": []
},
{
"id": "getshard_existing",
"fn": "get_shard",
"setup": {
"dirs": [
"shards"
],
"files": [
"shards/shard_reuse.db"
]
},
"input": {
"shard_dir": "shards",
"index_path": "shard_index.json",
"topic": "reuse",
"index": {
"reuse": "shards/shard_reuse.db"
}
},
"ops": [],
"expected": "shards/shard_reuse.db",
"effects": {},
"created_files": [
"shards/shard_reuse.db"
]
},
{
"id": "getshard_creates",
"fn": "get_shard",
"setup": {
"dirs": [
"shards"
]
},
"input": {
"shard_dir": "shards",
"index_path": "shard_index.json",
"topic": "brand new",
"index": {}
},
"ops": [
{
"op": "ensure_schema",
"args": {
"path": "shards/shard_brand_new.db"
},
"result": null,
"ok": true
}
],
"expected": "shards/shard_brand_new.db",
"effects": {
"index_file": {
"brand new": "shards/shard_brand_new.db"
}
},
"created_files": [
"shards/shard_brand_new.db"
]
},
{
"id": "stats_counts",
"fn": "get_shard_stats",
"setup": {
"dirs": [
"shards"
],
"dbs": {
"shards/shard_t1.db": {
"entries": [
[
"a",
"active"
],
[
"b",
"active"
],
[
"c",
"washed"
],
[
"d",
"active"
]
]
},
"shards/shard_t2.db": {
"entries": []
}
}
},
"input": {
"index": {
"t1": "shards/shard_t1.db",
"t2": "shards/shard_t2.db"
}
},
"ops": [
{
"op": "count_entries",
"args": {
"path": "shards/shard_t1.db"
},
"result": 3,
"ok": true
},
{
"op": "count_entries",
"args": {
"path": "shards/shard_t2.db"
},
"result": 0,
"ok": true
}
],
"expected": {
"t1": 3,
"t2": 0
},
"effects": {},
"created_files": [
"shards/shard_t1.db",
"shards/shard_t2.db"
]
},
{
"id": "stats_broken_path",
"fn": "get_shard_stats",
"setup": {},
"input": {
"index": {
"gone": "no_such_dir/shard_gone.db"
}
},
"ops": [
{
"op": "count_entries",
"args": {
"path": "no_such_dir/shard_gone.db"
},
"result": null,
"ok": false
}
],
"expected": {
"gone": 0
},
"effects": {},
"created_files": []
},
{
"id": "total_sums",
"fn": "total_entries",
"setup": {
"dirs": [
"shards"
],
"dbs": {
"shards/shard_t1.db": {
"entries": [
[
"a",
"active"
],
[
"b",
"active"
]
]
},
"shards/shard_t2.db": {
"entries": [
[
"c",
"active"
]
]
}
}
},
"input": {
"index": {
"t1": "shards/shard_t1.db",
"t2": "shards/shard_t2.db"
}
},
"ops": [
{
"op": "count_entries",
"args": {
"path": "shards/shard_t1.db"
},
"result": 2,
"ok": true
},
{
"op": "count_entries",
"args": {
"path": "shards/shard_t2.db"
},
"result": 1,
"ok": true
}
],
"expected": 3,
"effects": {},
"created_files": [
"shards/shard_t1.db",
"shards/shard_t2.db"
]
}
]
}
@@ -0,0 +1,219 @@
//! branch_battery — verbatim replay of frozen_sm_truth.json (27 cases frozen
//! from the LIVE Python oracle, RUNE_SM_CORE=python pinned). Every case:
//! materialize the setup in a scratch cwd, call the kernel, compare the
//! result AND the issued ops (exact order, exact args, recorded results
//! replayed through a mock ops callback), then compare on-disk index effects.
use super::*;
use std::cell::RefCell;
use std::ffi::CString;
use std::os::raw::c_char;
const TRUTH: &str = include_str!("../frozen_sm_truth.json");
thread_local! {
static OPS: RefCell<Vec<Json>> = const { RefCell::new(Vec::new()) };
static LAST_BUF: RefCell<Option<CString>> = const { RefCell::new(None) };
}
extern "C" fn mock_ops_cb(op_ptr: *const c_char) -> *const c_char {
let op_json = unsafe { CStr::from_ptr(op_ptr) }.to_str().unwrap();
let op = pyjson::parse(op_json).unwrap();
let next = OPS.with(|q| {
let mut q = q.borrow_mut();
assert!(!q.is_empty(), "kernel issued an op the oracle never did: {}", op_json);
q.remove(0)
});
assert_eq!(
op.get("op"), next.get("op"),
"op order divergence: kernel issued {}, truth expects {:?}",
op_json,
next.get("op")
);
assert_eq!(
op.get("args"), next.get("args"),
"op args divergence on op {:?}: kernel {}, truth {:?}",
next.get("op"),
op_json,
next.get("args")
);
let ok = matches!(next.get("ok"), Some(Json::Bool(true)));
let value = next.get("result").cloned().unwrap_or(Json::Null);
let resp = pyjson::dumps(&Json::Obj(vec![
("ok".into(), Json::Bool(ok)),
("value".into(), value),
]));
let buf = CString::new(resp).unwrap();
let ptr = buf.as_ptr();
LAST_BUF.with(|b| *b.borrow_mut() = Some(buf));
ptr
}
fn setup_case(case: &Json, root: &std::path::Path) {
std::fs::create_dir_all(root).unwrap();
std::env::set_current_dir(root).unwrap();
if let Some(Json::Arr(dirs)) = case.get("setup").and_then(|s| s.get("dirs")) {
for d in dirs {
if let Json::Str(d) = d {
std::fs::create_dir_all(d).unwrap();
}
}
}
if let Some(Json::Arr(files)) = case.get("setup").and_then(|s| s.get("files")) {
for f in files {
if let Json::Str(f) = f {
if let Some(parent) = std::path::Path::new(f).parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(f, b"").unwrap();
}
}
}
if let Some(setup) = case.get("setup") {
match setup.get("index_file") {
Some(Json::Str(s)) if s == "CORRUPT" => {
std::fs::write("shard_index.json", b"not json {{{").unwrap();
}
Some(v) => {
std::fs::write("shard_index.json", pyjson::dumps(v)).unwrap();
}
None => {}
}
}
}
fn run_kernel(case: &Json) -> Json {
let inp = case.get("input").unwrap();
let index = inp.get("index").cloned().unwrap_or(Json::Obj(Vec::new()));
let fn_name = case.get("fn").and_then(|f| match f {
Json::Str(s) => Some(s.as_str()),
_ => None,
}).unwrap();
match fn_name {
"_load_index" => load_index(
inp.get("index_path").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
).unwrap(),
"__init__" => {
let idx = init(
inp.get("shard_dir").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
inp.get("index_path").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
).unwrap();
let dir_exists = std::path::Path::new(
inp.get("shard_dir").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
).is_dir();
Json::Obj(vec![
("index".into(), idx),
("dir_exists".into(), Json::Bool(dir_exists)),
])
}
"create_shard" => {
let (path, idx) = create_shard(
inp.get("shard_dir").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
inp.get("index_path").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
inp.get("topic").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
&index,
mock_ops_cb,
).unwrap();
Json::Obj(vec![
("shard_path".into(), Json::Str(path)),
("index".into(), idx),
])
}
"route_query" => Json::Str(route_query(
inp.get("query").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
&index,
)),
"merge_results" => merge_results(inp.get("results_list").unwrap()),
"get_shard" => {
let (path, idx) = get_shard(
inp.get("shard_dir").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
inp.get("index_path").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
inp.get("topic").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
&index,
mock_ops_cb,
).unwrap();
Json::Obj(vec![
("shard_path".into(), Json::Str(path)),
("index".into(), idx),
])
}
"get_shard_stats" => get_shard_stats(&index, mock_ops_cb).unwrap(),
"total_entries" => Json::Num(total_entries(&index, mock_ops_cb).unwrap() as f64),
other => panic!("unknown truth fn {}", other),
}
}
/// Project the kernel result into the truth's `expected` shape and compare.
fn assert_case(case: &Json, result: &Json) {
let id = match case.get("id") {
Some(Json::Str(s)) => s.as_str(),
_ => "?",
};
let fn_name = case.get("fn").and_then(|f| match f {
Json::Str(s) => Some(s.as_str()),
_ => None,
}).unwrap();
let expected = case.get("expected").unwrap();
match fn_name {
"create_shard" | "get_shard" => {
assert_eq!(
result.get("shard_path"), Some(expected),
"case {}: shard_path mismatch",
id
);
}
_ => assert_eq!(result, expected, "case {}: result mismatch", id),
}
// on-disk index effect (oracle's _save_index footprint)
if let Some(Json::Obj(_)) = case.get("effects").and_then(|e| e.get("index_file")) {
let want = case.get("effects").unwrap().get("index_file").unwrap();
let got = pyjson::parse(&std::fs::read_to_string("shard_index.json").unwrap()).unwrap();
assert_eq!(&got, want, "case {}: index file effect mismatch", id);
}
}
#[test]
fn battery_replays_frozen_truth_verbatim() {
let truth = pyjson::parse(TRUTH).unwrap();
let cases = match truth.get("cases") {
Some(Json::Arr(c)) => c.clone(),
_ => panic!("truth has no cases"),
};
assert_eq!(cases.len(), 27, "frozen truth case count");
let base = std::env::temp_dir().join(format!("sm_battery_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let mut replayed = 0;
for (i, case) in cases.iter().enumerate() {
let root = base.join(format!("case_{:02}", i));
let _ = std::fs::remove_dir_all(&root);
setup_case(case, &root);
OPS.with(|q| {
*q.borrow_mut() = case
.get("ops")
.and_then(|o| match o {
Json::Arr(a) => Some(a.clone()),
_ => None,
})
.unwrap_or_default();
});
let result = run_kernel(case);
OPS.with(|q| {
let remaining = q.borrow();
assert!(
remaining.is_empty(),
"case {}: kernel issued fewer ops than the oracle ({} pending: {:?})",
case.get("id").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
remaining.len(),
remaining
);
});
assert_case(case, &result);
replayed += 1;
let _ = std::env::set_current_dir(&base);
let _ = std::fs::remove_dir_all(&root);
}
std::env::set_current_dir(&base).ok();
let _ = std::fs::remove_dir_all(&base);
assert_eq!(replayed, 27);
}
+391 -61
View File
@@ -1,71 +1,401 @@
//! shard_manager — shard-based storage routing (W6).
//! PARITY port of src/rune/shard_manager.py: topic → shard path mapping.
//! shard_manager — FULL Rust port of src/rune/shard_manager.py (dichotomy
//! wave #2479 tranche). Topic → shard routing, index persistence, result
//! merging, per-shard entry counts.
//!
//! Parity oracle: frozen_sm_truth.json (27 cases frozen from the LIVE Python
//! oracle 2026-10-11, RUNE_SM_CORE=python pinned). The sqlite/sqlite-vec
//! resource boundary is the ops seam: the kernel issues `ensure_schema` /
//! `count_entries` ops through a C callback; the Python loader serves them
//! against real sqlite3 + sqlite_vec. Connection construction stays
//! Python-side (resource handle cannot cross FFI) — mirror of oracle lines
//! 132-141 lives in the facade, both core modes.
/// Generate a shard file name from a topic (same normalization as Python).
pub use capability_scoring::pyjson;
use pyjson::Json;
use std::collections::HashSet;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
/// Ops callback: receives a NUL-terminated op JSON
/// `{"op": ..., "args": {...}}`, returns a NUL-terminated result JSON
/// `{"ok": bool, "value": ...}` owned by the callee, valid until the next
/// invocation of the callback. A null return means op failure.
pub type OpsCb = extern "C" fn(*const c_char) -> *const c_char;
pub const OP_ENSURE_SCHEMA: &str = "ensure_schema";
pub const OP_COUNT_ENTRIES: &str = "count_entries";
// ── doc-envelope FFI plumbing ────────────────────────────────────────────────
fn read_arg(ptr: *const c_char) -> Result<Json, 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())?;
pyjson::parse(x)
}
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())
}
/// Issue one op through the callback; `None` on any failure (kernel then
/// degrades exactly like the oracle's best-effort branches).
fn issue_op(cb: OpsCb, op: &str, args: Json) -> Option<Json> {
let payload = CString::new(pyjson::dumps(&Json::Obj(vec![
("op".into(), Json::Str(op.into())),
("args".into(), args),
])))
.ok()?;
let ret = cb(payload.as_ptr());
if ret.is_null() {
return None;
}
let s = unsafe { CStr::from_ptr(ret) }.to_str().ok()?;
let v = pyjson::parse(s).ok()?;
match v.get("ok") {
Some(Json::Bool(true)) => Some(v.get("value").cloned().unwrap_or(Json::Null)),
_ => None,
}
}
// ── index helpers (Python dict semantics: insert-order, in-place update) ────
fn index_set(index: &mut Json, topic: &str, path: &str) {
if let Json::Obj(pairs) = index {
if let Some(slot) = pairs.iter_mut().find(|(k, _)| k == topic) {
slot.1 = Json::Str(path.into());
return;
}
pairs.push((topic.into(), Json::Str(path.into())));
}
}
/// os.path.join(dir, name) for the posix shapes this module produces.
fn join_path(dir: &str, name: &str) -> String {
if dir.is_empty() {
return name.into();
}
if dir.ends_with('/') {
format!("{}{}", dir, name)
} else {
format!("{}/{}", dir, name)
}
}
// ── kernel (name-preserving port of the 9 oracle fns) ───────────────────────
/// __init__: makedirs(SHARD_DIR, exist_ok=True) + _load_index()
pub fn init(shard_dir: &str, index_path: &str) -> Result<Json, String> {
std::fs::create_dir_all(shard_dir).map_err(|e| e.to_string())?;
load_index(index_path)
}
/// _load_index: missing file → {}; JSONDecodeError → {}; else the parsed dict.
pub fn load_index(index_path: &str) -> Result<Json, String> {
let p = std::path::Path::new(index_path);
if !p.exists() {
return Ok(Json::Obj(Vec::new()));
}
let raw = std::fs::read_to_string(p).map_err(|e| e.to_string())?;
Ok(pyjson::parse(&raw).unwrap_or(Json::Obj(Vec::new())))
}
/// _save_index: json.dump(self.index, f, indent=2) — no trailing newline.
pub fn save_index(index_path: &str, index: &Json) -> Result<(), String> {
std::fs::write(index_path, pyjson::dump_indent2(index)).map_err(|e| e.to_string())
}
/// Shard file name for a topic (oracle: replace(' ', '_').replace('/', '_')).
pub fn shard_name(topic: &str) -> String {
format!("shard_{}.db", topic.replace([' ', '/'], "_"))
}
/// Get the full shard path for a topic
pub fn shard_path(shard_dir: &str, topic: &str) -> String {
let name = shard_name(topic);
format!("{}/{}", shard_dir.trim_end_matches('/'), name)
/// create_shard: schema creation via the ensure_schema op; early-exists
/// branch returns the path WITHOUT touching the index (oracle line 52-53).
/// On creation the index is updated and persisted (oracle lines 88-89).
pub fn create_shard(
shard_dir: &str,
index_path: &str,
topic: &str,
index: &Json,
cb: OpsCb,
) -> Result<(String, Json), String> {
let shard_path = join_path(shard_dir, &shard_name(topic));
if std::path::Path::new(&shard_path).exists() {
return Ok((shard_path, index.clone()));
}
issue_op(cb, OP_ENSURE_SCHEMA, Json::Obj(vec![(
"path".into(),
Json::Str(shard_path.clone()),
)]))
.ok_or_else(|| "ensure_schema op failed".to_string())?;
let mut idx = index.clone();
index_set(&mut idx, topic, &shard_path);
save_index(index_path, &idx)?;
Ok((shard_path, idx))
}
/// Check if a shard exists
pub fn shard_exists(shard_dir: &str, topic: &str) -> bool {
std::path::Path::new(&shard_path(shard_dir, topic)).exists()
/// route_query: keyword overlap over the index (insertion order, strictly
/// greater wins → first topic on ties), then the >3-char 3-word fallback.
pub fn route_query(query: &str, index: &Json) -> String {
let query_lower = query.to_lowercase();
let query_words: HashSet<&str> = query_lower.split_whitespace().collect();
let mut best_topic: Option<&str> = None;
let mut best_score: i64 = 0;
if let Json::Obj(pairs) = index {
for (topic, _) in pairs {
let topic_lower = topic.to_lowercase();
let topic_words: HashSet<&str> = topic_lower.split('_').collect();
let overlap = topic_words.intersection(&query_words).count() as i64;
if overlap > best_score {
best_score = overlap;
best_topic = Some(topic.as_str());
}
}
}
if let (Some(topic), true) = (best_topic, best_score > 0) {
return topic.to_string();
}
let words: Vec<&str> = query
.split_whitespace()
.filter(|w| w.chars().count() > 3)
.take(3)
.collect();
if !words.is_empty() {
return words.join("_").to_lowercase();
}
"general".to_string()
}
fn merge_key(x: &Json) -> f64 {
for key in ["relevance", "wash_count"] {
if let Some(Json::Num(n)) = x.get(key) {
return *n;
}
}
0.0
}
/// merge_results: dedupe by text[:50] (chars), stable sort by
/// relevance-else-wash_count-else-0, descending.
pub fn merge_results(results_list: &Json) -> Json {
let mut seen: HashSet<String> = HashSet::new();
let mut merged: Vec<Json> = Vec::new();
if let Json::Arr(shards) = results_list {
for results in shards {
if let Json::Arr(rows) = results {
for r in rows {
let key: String = match r.get("text") {
Some(Json::Str(s)) => s.chars().take(50).collect(),
_ => String::new(),
};
if seen.insert(key) {
merged.push(r.clone());
}
}
}
}
}
merged.sort_by(|a, b| {
merge_key(b)
.partial_cmp(&merge_key(a))
.unwrap_or(std::cmp::Ordering::Equal)
});
Json::Arr(merged)
}
/// get_shard: resolve-or-create the shard path (connection opening stays
/// Python-side). Creation path mirrors create_shard incl. index persistence.
pub fn get_shard(
shard_dir: &str,
index_path: &str,
topic: &str,
index: &Json,
cb: OpsCb,
) -> Result<(String, Json), String> {
let existing = index.get(topic).and_then(|v| match v {
Json::Str(s) => Some(s.clone()),
_ => None,
});
if let Some(path) = existing {
return Ok((path, index.clone()));
}
create_shard(shard_dir, index_path, topic, index, cb)
}
/// get_shard_stats: one count_entries op per topic, index order; op failure
/// degrades to 0 (oracle except branch).
pub fn get_shard_stats(index: &Json, cb: OpsCb) -> Result<Json, String> {
let mut stats = Vec::new();
if let Json::Obj(pairs) = index {
for (topic, path) in pairs {
let path = match path {
Json::Str(s) => s.clone(),
_ => String::new(),
};
let count = issue_op(cb, OP_COUNT_ENTRIES, Json::Obj(vec![(
"path".into(),
Json::Str(path),
)]))
.and_then(|v| match v {
Json::Num(n) => Some(n as i64),
_ => None,
})
.unwrap_or(0);
stats.push((topic.clone(), Json::Num(count as f64)));
}
}
Ok(Json::Obj(stats))
}
/// total_entries: sum of get_shard_stats values.
pub fn total_entries(index: &Json, cb: OpsCb) -> Result<i64, String> {
let stats = get_shard_stats(index, cb)?;
let mut total: i64 = 0;
if let Json::Obj(pairs) = &stats {
for (_, v) in pairs {
if let Json::Num(n) = v {
total += *n as i64;
}
}
}
Ok(total)
}
// ── FFI surface ──────────────────────────────────────────────────────────────
#[no_mangle]
pub extern "C" fn sm_init(doc_ptr: *const c_char) -> *mut c_char {
let run = || -> Result<Json, String> {
let doc = read_arg(doc_ptr)?;
let dir = str_field(&doc, "shard_dir")?;
let idx_path = str_field(&doc, "index_path")?;
init(dir, idx_path)
};
envelope(run())
}
#[no_mangle]
pub extern "C" fn sm_load_index(doc_ptr: *const c_char) -> *mut c_char {
let run = || -> Result<Json, String> {
let doc = read_arg(doc_ptr)?;
load_index(str_field(&doc, "index_path")?)
};
envelope(run())
}
#[no_mangle]
pub extern "C" fn sm_save_index(doc_ptr: *const c_char) -> *mut c_char {
let run = || -> Result<Json, String> {
let doc = read_arg(doc_ptr)?;
let idx_path = str_field(&doc, "index_path")?;
let index = doc.get("index").cloned().unwrap_or(Json::Obj(Vec::new()));
save_index(idx_path, &index)?;
Ok(Json::Null)
};
envelope(run())
}
#[no_mangle]
pub extern "C" fn sm_create_shard(doc_ptr: *const c_char, cb: OpsCb) -> *mut c_char {
let run = || -> Result<Json, String> {
let doc = read_arg(doc_ptr)?;
let dir = str_field(&doc, "shard_dir")?;
let idx_path = str_field(&doc, "index_path")?;
let topic = str_field(&doc, "topic")?;
let index = doc.get("index").cloned().unwrap_or(Json::Obj(Vec::new()));
let (shard_path, idx) = create_shard(dir, idx_path, topic, &index, cb)?;
Ok(Json::Obj(vec![
("shard_path".into(), Json::Str(shard_path)),
("index".into(), idx),
]))
};
envelope(run())
}
#[no_mangle]
pub extern "C" fn sm_route_query(doc_ptr: *const c_char) -> *mut c_char {
let run = || -> Result<Json, String> {
let doc = read_arg(doc_ptr)?;
let query = str_field(&doc, "query")?;
let index = doc.get("index").cloned().unwrap_or(Json::Obj(Vec::new()));
Ok(Json::Str(route_query(query, &index)))
};
envelope(run())
}
#[no_mangle]
pub extern "C" fn sm_merge_results(doc_ptr: *const c_char) -> *mut c_char {
let run = || -> Result<Json, String> {
let doc = read_arg(doc_ptr)?;
let results_list = doc.get("results_list").cloned().unwrap_or(Json::Arr(Vec::new()));
Ok(merge_results(&results_list))
};
envelope(run())
}
#[no_mangle]
pub extern "C" fn sm_get_shard(doc_ptr: *const c_char, cb: OpsCb) -> *mut c_char {
let run = || -> Result<Json, String> {
let doc = read_arg(doc_ptr)?;
let dir = str_field(&doc, "shard_dir")?;
let idx_path = str_field(&doc, "index_path")?;
let topic = str_field(&doc, "topic")?;
let index = doc.get("index").cloned().unwrap_or(Json::Obj(Vec::new()));
let (shard_path, idx) = get_shard(dir, idx_path, topic, &index, cb)?;
Ok(Json::Obj(vec![
("shard_path".into(), Json::Str(shard_path)),
("index".into(), idx),
]))
};
envelope(run())
}
#[no_mangle]
pub extern "C" fn sm_get_shard_stats(doc_ptr: *const c_char, cb: OpsCb) -> *mut c_char {
let run = || -> Result<Json, String> {
let doc = read_arg(doc_ptr)?;
let index = doc.get("index").cloned().unwrap_or(Json::Obj(Vec::new()));
get_shard_stats(&index, cb)
};
envelope(run())
}
#[no_mangle]
pub extern "C" fn sm_total_entries(doc_ptr: *const c_char, cb: OpsCb) -> *mut c_char {
let run = || -> Result<Json, String> {
let doc = read_arg(doc_ptr)?;
let index = doc.get("index").cloned().unwrap_or(Json::Obj(Vec::new()));
Ok(Json::Num(total_entries(&index, cb)? as f64))
};
envelope(run())
}
fn str_field<'a>(doc: &'a Json, key: &str) -> Result<&'a str, String> {
match doc.get(key) {
Some(Json::Str(s)) => Ok(s),
_ => Err(format!("missing string field '{}'", key)),
}
}
// ── frozen-truth battery ─────────────────────────────────────────────────────
#[path = "branch_battery.rs"]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shard_name() {
assert_eq!(shard_name("deployment"), "shard_deployment.db");
assert_eq!(shard_name("some topic"), "shard_some_topic.db");
assert_eq!(shard_name("a/b"), "shard_a_b.db");
}
#[test]
fn test_shard_path() {
let path = shard_path("/tmp/shards", "test");
assert!(path.contains("/tmp/shards/shard_test.db"));
}
#[test]
fn test_shard_exists() {
let dir = "/tmp/shard_test_dir";
std::fs::create_dir_all(dir).unwrap();
let path = shard_path(dir, "existing");
std::fs::write(&path, b"").unwrap();
assert!(shard_exists(dir, "existing"));
assert!(!shard_exists(dir, "nonexistent"));
std::fs::remove_dir_all(dir).ok();
}
}
#[cfg(test)]
mod branch_battery {
use super::*;
#[test]
fn shard_name_format() {
// ORACLE: shard_name returns "shard_<Topic>.db" format
assert_eq!(shard_name("Memory"), "shard_Memory.db");
}
#[test]
fn shard_path_correct() {
assert_eq!(
shard_path("/tmp/shards", "test"),
"/tmp/shards/shard_test.db"
);
}
#[test]
fn shard_exists_false_for_missing() {
assert!(!shard_exists("/tmp/nonexistent-shards-xyz", "test"));
}
}
mod branch_battery;
+6 -2
View File
@@ -22,11 +22,15 @@ SPEC.loader.exec_module(sm_mod)
@pytest.fixture
def shard_env(tmp_path, monkeypatch):
"""Point ShardManager's SHARD_DIR at a temp home — no importlib.reload
(module-constant patching is deterministic and side-effect free)."""
"""Point ShardManager's SHARD_DIR/SHARD_INDEX at a temp home — no
importlib.reload (module-constant patching is deterministic and
side-effect free). SHARD_INDEX must be patched in the Rust-core mode too:
the kernel receives the index path explicitly (no _load_index indirection
to monkeypatch)."""
shard_dir = tmp_path / "shards"
shard_dir.mkdir()
monkeypatch.setattr(sm_mod, "SHARD_DIR", str(shard_dir))
monkeypatch.setattr(sm_mod, "SHARD_INDEX", str(tmp_path / "shard_index.json"))
# fresh index per test
monkeypatch.setattr(sm_mod.ShardManager, "_load_index", lambda self: {})
monkeypatch.setattr(sm_mod.ShardManager, "_save_index", lambda self: None)