Merge pull request 'R2 W2.0: SQLite-backed zero-dep Rust read boundary' (#2457) from r2-wave2-memory-storage into master

This commit is contained in:
conductor committed 2026-10-05 01:56:51 +01:00
commit de5445357e
14 files changed
+6955 -272

No files matched your search

+5
View File
@@ -79,6 +79,7 @@ jobs:
CS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcapability_scoring.so
TC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libtoken_counting.so
SC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libscaffold_core.so
MEMS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_storage.so
steps:
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -131,6 +132,7 @@ jobs:
cargo build --release --manifest-path src/rust/capability_scoring/Cargo.toml
cargo build --release --manifest-path src/rust/token_counting/Cargo.toml
cargo build --release --manifest-path src/rust/scaffold_core/Cargo.toml
cargo build --release --manifest-path src/rust/memory_storage/Cargo.toml
- name: Test with coverage (global floor + changed-lines ratchet)
run: |
# #149 coverage ratchet — both floors live in .coveragerc; raise,
@@ -179,6 +181,7 @@ jobs:
CS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcapability_scoring.so
TC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libtoken_counting.so
SC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libscaffold_core.so
MEMS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_storage.so
steps:
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -216,6 +219,7 @@ jobs:
cargo build --release --manifest-path src/rust/capability_scoring/Cargo.toml
cargo build --release --manifest-path src/rust/token_counting/Cargo.toml
cargo build --release --manifest-path src/rust/scaffold_core/Cargo.toml
cargo build --release --manifest-path src/rust/memory_storage/Cargo.toml
- name: Run canary suite
run: |
FAILED=0
@@ -252,6 +256,7 @@ jobs:
src/rust/capability_scoring/canary_capability_scoring.py \
src/rust/token_counting/canary_token_counting.py \
src/rust/scaffold_core/canary_scaffold_core.py \
src/rust/memory_storage/canary_memory_storage.py \
src/rune/canary_unbounded.py \
src/rune/canary_full_system.py \
src/rune/canary_native.py; do
+2
View File
@@ -165,6 +165,7 @@
"memory_env",
"memory_selector",
"memory_store",
"mems_loader",
"ms_loader",
"query_memory",
"recall_router",
@@ -184,6 +185,7 @@
],
"rust_crates": [
"memory_scoring",
"memory_storage",
"query_memory"
]
},
+65
View File
@@ -1907,6 +1907,71 @@ def stats(db):
return {"active_entries": e, "working": w, "audit": a, "top_topics": by_topic}
# ── storage backend seam (R2 W2.0) ───────────────────────────────────────────
# SQLite is RETAINED (Nick's ruling) — the seam separates query logic from
# storage mechanics so the Rust core can serve proven-identical READS via
# system libsqlite3 FFI. Connection-based callers (stats above) keep their
# transactional semantics untouched; path-based consumers opt into the
# backend. NO WRITES route through the Rust backend this slice.
class _SQLiteBackend:
"""The default backend: the stdlib sqlite3 mechanics, unchanged."""
name = "sqlite"
def read_query(self, db_path: str, sql: str, params=()) -> list:
conn = sqlite3.connect(db_path)
try:
return [tuple(r) for r in conn.execute(sql, params).fetchall()]
finally:
conn.close()
class _RustReadBackend:
"""The Rust read path through the memory_storage crate (read-only by
construction — the crate exports no write-capable symbols)."""
name = "rust-memory-storage"
def read_query(self, db_path: str, sql: str, params=()) -> list:
from rune import mems_loader
return [tuple(r) for r in mems_loader.read_query(db_path, sql, list(params))]
def storage_backend():
"""The active read backend. Default sqlite; RUNE_MEMORY_STORAGE=rust
opts into the crate when built (falls back loudly-free to sqlite)."""
if os.environ.get("RUNE_MEMORY_STORAGE", "auto") == "rust":
try:
from rune import mems_loader
if mems_loader.available():
return _RustReadBackend()
except Exception: # noqa: BLE001, S110 — backend probe failure means the default serves (fallback, not silence)
pass
return _SQLiteBackend()
def stats_for_path(db_path: str) -> dict:
"""stats() through the storage backend seam — the opt-in consumer the
parity battery and future Rust callers use. Same result dict shape."""
backend = storage_backend()
e = backend.read_query(db_path, "SELECT COUNT(*) FROM entries WHERE status='active'")[0][0]
w = backend.read_query(db_path, "SELECT COUNT(*) FROM working")[0][0]
a = backend.read_query(db_path, "SELECT COUNT(*) FROM audit_log")[0][0]
by_topic = {
r[0]: r[1]
for r in backend.read_query(
db_path,
"SELECT topic, COUNT(*) AS n FROM entries WHERE status='active' GROUP BY topic ORDER BY n DESC LIMIT 6",
)
if r[0]
}
return {"active_entries": e, "working": w, "audit": a, "top_topics": by_topic}
# ------------------------------------------------------------------ associations ----
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust memory_storage core (R2 W2.0).
Contract BUILDER-2026-10-04T15:54:17Z-cli. READ-ONLY by construction:
the only operation is read_query — the crate exports no write-capable
symbols (Nick's ruling: reads proven byte-identical before any writes
exist). SQLite is retained; the crate reaches it via system libsqlite3.
FFI contract (doc-envelope law): mems_read_query({db_path, sql, params})
-> [[cell, ...], ...]; mems_free_string(ptr). FFI segfault law: argtypes +
restype pinned per symbol. Override the .so with MEMS_RUST_LIB (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" / "memory_storage"
_LIB_PATH = Path(
os.environ.get(
"MEMS_RUST_LIB", _CRATE_DIR / "target" / "release" / "libmemory_storage.so"
)
)
_lib = None
class MemoryStorageUnavailable(FileNotFoundError):
"""The Rust core is not built (or MEMS_RUST_LIB points nowhere)."""
def available() -> bool:
return _LIB_PATH.is_file()
def _load() -> ctypes.CDLL:
global _lib
if _lib is not None:
return _lib
if not _LIB_PATH.exists():
raise MemoryStorageUnavailable(
f"Rust memory_storage core not built: {_LIB_PATH} missing — run "
f"`cargo build --release` in {_CRATE_DIR}"
)
lib = ctypes.CDLL(str(_LIB_PATH))
lib.mems_free_string.argtypes = [ctypes.c_void_p]
lib.mems_free_string.restype = None
lib.mems_read_query.argtypes = [ctypes.c_char_p]
lib.mems_read_query.restype = ctypes.c_void_p # char* — never c_int
_lib = lib
return lib
def _call(payload: dict) -> dict:
"""Raw envelope: {"ok", "value"|"err"} — the error-honesty section of
the canary needs the envelope, not an exception."""
lib = _load()
raw = json.dumps(payload).encode("utf-8")
ptr = lib.mems_read_query(raw)
if not ptr:
raise RuntimeError("mems_read_query returned a null pointer")
try:
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
finally:
lib.mems_free_string(ptr)
return json.loads(text)
def read_query(db_path: str, sql: str, params: list) -> list:
"""Read-only query through the Rust core; raises on the error envelope."""
env = _call({"db_path": db_path, "sql": sql, "params": params})
if not env.get("ok"):
raise RuntimeError(f"mems_read_query core error: {env.get('err')}")
return env["value"]
def read_query_result(db_path: str, sql: str, params: list) -> dict:
"""The raw envelope (error-honesty checks consume this)."""
return _call({"db_path": db_path, "sql": sql, "params": params})
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "memory_storage"
version = "0.1.0"
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "memory_storage"
version = "0.1.0"
edition = "2021"
description = "R2 W2.0 — SQLite read path via system libsqlite3 FFI (contract BUILDER-2026-10-04T15:54:17Z-cli); zero cargo deps; NO WRITES (Nick's ruling: reads proven identical first)"
[lib]
name = "memory_storage"
crate-type = ["rlib", "cdylib"]
[[bin]]
name = "memory_storage"
path = "src/main.rs"
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""canary_memory_storage.py — read-path parity battery for the
memory_storage crate (R2 W2.0, contract BUILDER-2026-10-04T15:54:17Z-cli).
Nick's super-careful ruling is the law of this battery: SQLite is RETAINED;
the Rust core reads the SAME database through system libsqlite3 FFI;
byte-identical rows or the wave stops. NO WRITES through the Rust path —
the FFI surface carries no write symbols, and section 5 proves it.
Sections:
1. crate builds (rustup PATH-first per the SYSTEM RUSTC BROKEN law)
2. fixture store built by PYTHON (the real schema, production-shaped rows)
3. read-path parity: byte-identical row sets across typed values
(ints, floats, text, NULLs, unicode)
4. error honesty: missing table/file produce the same error class
5. no-write gate: the FFI surface exports no write-capable symbols
Run: python3 src/rust/memory_storage/canary_memory_storage.py
"""
from __future__ import annotations
import json
import os
import sqlite3
import subprocess
import sys
import tempfile
from pathlib import Path
CRATE_DIR = Path(__file__).resolve().parent
REPO = CRATE_DIR.parents[2]
sys.path.insert(0, str(REPO / "src"))
PASS = 0
FAIL = 0
def test(name: str, ok: bool, detail: str = "") -> None:
global PASS, FAIL
if ok:
PASS += 1
print(f" ✓ {name}")
else:
FAIL += 1
print(f" ✗ {name} — {detail}")
def build_crate() -> subprocess.CompletedProcess:
env = dict(os.environ)
_absolute = Path("/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin")
rustup_bin = _absolute if _absolute.is_dir() else (
Path.home() / ".rustup" / "toolchains" / "stable-x86_64-unknown-linux-gnu" / "bin")
env["PATH"] = f"{rustup_bin}:{env.get('PATH', '')}"
env["CARGO_TARGET_DIR"] = env.get("CARGO_TARGET_DIR", str(CRATE_DIR / "target"))
return subprocess.run(
["cargo", "build", "--release"],
cwd=CRATE_DIR, env=env, capture_output=True, text=True, timeout=600)
def build_fixture(db_path: str) -> None:
"""The production-shaped fixture: the real entries schema + rows that
exercise every column type (int, float, text, NULL, unicode)."""
db = sqlite3.connect(db_path)
db.execute("""
CREATE TABLE 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 '',
entities TEXT DEFAULT '[]',
source TEXT DEFAULT ''
)""")
rows = [
("plain text entry", 0.5, "2026-10-01T00:00:00", None, "general", "[]", "test"),
("unicode — héllo wörld 🎸", 0.9, "2026-10-02T00:00:00", "2026-10-03T00:00:00", "identity", '["rune"]', "test"),
("empty topic", 0.0, "2026-10-03T12:00:00", None, "", "[]", ""),
("big number", 1.0, "2026-10-04T00:00:00", None, "numbers", "[]", "test"),
]
db.executemany(
"INSERT INTO entries (text, importance, created_at, last_accessed, topic, entities, source) VALUES (?,?,?,?,?,?,?)",
rows)
# stats() consumer tables (the seam's consumer surface)
db.execute("ALTER TABLE entries ADD COLUMN status TEXT DEFAULT 'active'")
db.execute("CREATE TABLE working (id INTEGER PRIMARY KEY, note TEXT)")
db.execute("CREATE TABLE audit_log (id INTEGER PRIMARY KEY, action TEXT)")
db.execute("INSERT INTO working (note) VALUES ('held')")
db.execute("INSERT INTO working (note) VALUES ('held-2')")
db.execute("INSERT INTO audit_log (action) VALUES ('boot')")
db.commit()
db.close()
def main() -> int:
print("=" * 47)
print("canary_memory_storage — SQLite read-path parity battery")
print("=" * 47)
print("\n--- 1. Crate builds ---")
r = build_crate()
test("cargo build --release exits 0", r.returncode == 0,
f"rc={r.returncode} err={r.stderr[-300:]!r}")
if r.returncode != 0:
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
return 1
default_so = Path(os.environ.get(
"CARGO_TARGET_DIR", str(CRATE_DIR / "target"))) / "release" / "libmemory_storage.so"
so = Path(os.environ.get("MEMS_RUST_LIB", default_so))
test("libmemory_storage.so exists", so.is_file(), str(so))
if not so.is_file():
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
return 1
# ── 2. fixture store (Python-built, real schema) ──────────────────────
print("\n--- 2. Fixture store ---")
tmp = Path(tempfile.mkdtemp(prefix="mems-parity-"))
db_path = str(tmp / "store.db")
build_fixture(db_path)
import hashlib
digest_before = hashlib.sha256(Path(db_path).read_bytes()).hexdigest()
test("fixture store created with the entries schema",
Path(db_path).is_file())
from rune import mems_loader # noqa: E402
# ── 3. read-path parity ───────────────────────────────────────────────
print("\n--- 3. Read-path parity (byte-identical rows) ---")
queries = [
("full table", "SELECT id, text, importance, created_at, last_accessed, topic, entities, source FROM entries ORDER BY id", []),
("filtered by topic", "SELECT id, text, topic FROM entries WHERE topic = ? ORDER BY id", ["identity"]),
("null projection", "SELECT id, last_accessed FROM entries ORDER BY id", []),
("float filter", "SELECT id, importance FROM entries WHERE importance > ? ORDER BY id", [0.6]),
("count aggregate", "SELECT COUNT(*) FROM entries", []),
("empty result", "SELECT id FROM entries WHERE topic = ?", ["nonexistent-topic"]),
("null parameter", "SELECT id FROM entries WHERE last_accessed IS ? ORDER BY id", [None]),
("blob value", "SELECT CAST(X'00FF41' AS BLOB)", []),
]
for name, sql, params in queries:
db = sqlite3.connect(db_path)
py_rows = db.execute(sql, params).fetchall()
db.close()
rs_rows = mems_loader.read_query(db_path, sql, params)
# Python tuples -> lists for JSON comparison; bytes have no JSON
# scalar form, so the transport contract represents them as byte
# arrays on both sides. Floats compare exact.
py_norm = [
[list(v) if isinstance(v, bytes) else v for v in r]
for r in py_rows
]
test(f"parity: {name}", py_norm == rs_rows,
f"py={py_norm[:2]!r} rs={rs_rows[:2]!r}")
# ── 4. error honesty ──────────────────────────────────────────────────
print("\n--- 4. Error honesty ---")
py_err = None
try:
db = sqlite3.connect(db_path)
db.execute("SELECT id FROM missing_table").fetchall()
except sqlite3.OperationalError as e:
py_err = str(e)
finally:
db.close()
rs = mems_loader.read_query_result(db_path, "SELECT id FROM missing_table", [])
test("missing table: same error class both sides",
py_err is not None and not rs.get("ok", True)
and "no such table" in rs.get("err", ""),
f"py={py_err!r} rs={rs!r}")
rs = mems_loader.read_query_result(str(tmp / "nonexistent" / "ghost.db"),
"SELECT 1", [])
py_err = None
try:
db = sqlite3.connect(str(tmp / "nonexistent" / "ghost.db"))
db.execute("SELECT 1").fetchall()
except sqlite3.OperationalError as e:
py_err = str(e)
test("missing file: same error class both sides",
py_err is not None and not rs.get("ok", True),
f"py={py_err!r} rs={rs!r}")
# ── 4b. the seam consumer: stats through both backends ────────────────
print("\n--- 4b. Seam consumer parity (stats_for_path) ---")
from rune import memory_store # noqa: E402
py_stats = memory_store._SQLiteBackend() # direct, for row-level checks
py_rows = py_stats.read_query(db_path, "SELECT COUNT(*) FROM entries WHERE status='active'")
rs_rows = mems_loader.read_query(db_path,
"SELECT COUNT(*) FROM entries WHERE status='active'", [])
test("backend row parity on the stats read",
[list(r) for r in py_rows] == rs_rows, f"py={py_rows!r} rs={rs_rows!r}")
os.environ["RUNE_MEMORY_STORAGE"] = "auto"
stats_py = memory_store.stats_for_path(db_path)
test("stats_for_path via the default backend", stats_py["active_entries"] == 4
and stats_py["working"] == 2 and stats_py["audit"] == 1,
f"{stats_py!r}")
os.environ["RUNE_MEMORY_STORAGE"] = "rust"
stats_rs = memory_store.stats_for_path(db_path)
test("stats_for_path via the Rust backend — identical",
stats_rs == stats_py and stats_rs["top_topics"].get("general") == 1,
f"py={stats_py!r} rs={stats_rs!r}")
os.environ.pop("RUNE_MEMORY_STORAGE", None)
# ── 5. no-write gate ──────────────────────────────────────────────────
print("\n--- 5. No-write gate (Nick's super-careful ruling) ---")
nm = subprocess.run(["nm", "-D", "--defined-only", str(so)],
capture_output=True, text=True)
exported = [l.split()[-1] for l in nm.stdout.splitlines() if l.strip()]
write_syms = [s for s in exported if any(
k in s.lower() for k in ("write", "insert", "update", "delete", "exec"))]
write_syms = [s for s in write_syms if s.startswith("mems_")]
test("the FFI surface exports no write-capable mems_* symbols",
write_syms == [], f"found: {write_syms}")
# Connection-level enforcement: even explicit mutation SQL cannot write.
mutation = mems_loader.read_query_result(
db_path, "UPDATE entries SET text = 'mutated' WHERE id = 1 RETURNING id", [])
test("SQLite READONLY rejects explicit mutation SQL",
not mutation.get("ok", True) and "readonly" in mutation.get("err", "").lower(),
f"{mutation!r}")
# And the fixture is byte-identical after every read + rejected mutation.
digest_after = hashlib.sha256(Path(db_path).read_bytes()).hexdigest()
test("the fixture store is byte-identical after all reads",
digest_before == digest_after,
f"before={digest_before} after={digest_after}")
print(f"\n{'=' * 47}")
print(f"RESULTS: {PASS} pass, {FAIL} fail")
return 1 if FAIL else 0
if __name__ == "__main__":
sys.exit(main())
+85
View File
@@ -0,0 +1,85 @@
//! lib.rs — FFI surface for the memory_storage crate (R2 W2.0).
//!
//! READ-ONLY BY CONSTRUCTION (Nick's ruling): the only exported operation
//! is mems_read_query. There is no write symbol to call.
//!
//! Envelope law: {"ok": true, "value": [[row], ...]} or
//! {"ok": false, "err": "..."}. The caller frees with mems_free_string.
//! FFI segfault law: mems_loader.py pins argtypes + restype.
pub mod pyjson;
pub mod sqlite;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
fn read_arg(ptr: *const c_char) -> Result<String, String> {
if ptr.is_null() {
return Err("null argument pointer".to_string());
}
unsafe { CStr::from_ptr(ptr) }
.to_str()
.map(|s| s.to_string())
.map_err(|e| format!("argument is not valid UTF-8: {}", e))
}
fn envelope(result: Result<pyjson::Json, String>) -> *mut c_char {
let body = match result {
Ok(value) => pyjson::Json::Obj(vec![
("ok".to_string(), pyjson::Json::Bool(true)),
("value".to_string(), value),
]),
Err(err) => pyjson::Json::Obj(vec![
("ok".to_string(), pyjson::Json::Bool(false)),
("err".to_string(), pyjson::Json::Str(err)),
]),
};
let out = pyjson::dumps(&body);
CString::new(out)
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
fn get<'a>(v: &'a pyjson::Json, key: &str) -> Option<&'a pyjson::Json> {
match v {
pyjson::Json::Obj(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v),
_ => None,
}
}
/// mems_read_query({"db_path": str, "sql": str, "params": [...]})
/// -> envelope([[cell, ...], ...])
#[no_mangle]
pub extern "C" fn mems_read_query(arg: *const c_char) -> *mut c_char {
let result = read_arg(arg)
.and_then(|text| pyjson::parse(&text).map_err(|e| format!("arg parse: {}", e)))
.and_then(|a| {
let db_path = match get(&a, "db_path") {
Some(pyjson::Json::Str(s)) => s.clone(),
_ => return Err("arg.db_path required".to_string()),
};
let sql = match get(&a, "sql") {
Some(pyjson::Json::Str(s)) => s.clone(),
_ => return Err("arg.sql required".to_string()),
};
let params = match get(&a, "params") {
Some(pyjson::Json::Arr(items)) => items.clone(),
_ => vec![],
};
let rows = sqlite::read_query(&db_path, &sql, &params)?;
Ok(pyjson::Json::Arr(
rows.into_iter().map(pyjson::Json::Arr).collect(),
))
});
envelope(result)
}
/// Free a string returned by any mems_* function.
#[no_mangle]
pub extern "C" fn mems_free_string(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe {
let _ = CString::from_raw(ptr);
}
}
}
+81
View File
@@ -0,0 +1,81 @@
//! main.rs — CLI front for the memory_storage core (runner/debug use).
//! READ-ONLY: the only op is read_query.
//!
//! stdin: {"op": "read_query", "arg": {"db_path": str, "sql": str, "params": [...]}}
//! stdout: the same {ok, value|err} envelope the FFI returns.
use memory_storage::{pyjson, sqlite};
use std::io::Read;
fn get<'a>(v: &'a pyjson::Json, key: &str) -> Option<&'a pyjson::Json> {
match v {
pyjson::Json::Obj(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v),
_ => None,
}
}
fn main() {
let mut input = String::new();
if std::io::stdin().read_to_string(&mut input).is_err() {
emit(Err("failed to read stdin".to_string()));
return;
}
let parsed = match pyjson::parse(&input) {
Ok(p) => p,
Err(e) => {
emit(Err(format!("stdin JSON parse failed: {}", e)));
return;
}
};
let op = match get(&parsed, "op") {
Some(pyjson::Json::Str(s)) => s.clone(),
_ => {
emit(Err("op required".to_string()));
return;
}
};
let arg = get(&parsed, "arg").cloned().unwrap_or(pyjson::Json::Null);
let result = match op.as_str() {
"read_query" => {
let db_path = match get(&arg, "db_path") {
Some(pyjson::Json::Str(s)) => s.clone(),
_ => {
emit(Err("arg.db_path required".to_string()));
return;
}
};
let sql = match get(&arg, "sql") {
Some(pyjson::Json::Str(s)) => s.clone(),
_ => {
emit(Err("arg.sql required".to_string()));
return;
}
};
let params = match get(&arg, "params") {
Some(pyjson::Json::Arr(items)) => items.clone(),
_ => vec![],
};
sqlite::read_query(&db_path, &sql, &params).map(|rows| {
pyjson::Json::Arr(rows.into_iter().map(pyjson::Json::Arr).collect())
})
}
other => Err(format!("unknown op: {:?} (read-only build)", other)),
};
emit(result);
}
fn emit(result: Result<pyjson::Json, String>) {
let ok = result.is_ok();
let body = match result {
Ok(value) => pyjson::Json::Obj(vec![
("ok".to_string(), pyjson::Json::Bool(true)),
("value".to_string(), value),
]),
Err(err) => pyjson::Json::Obj(vec![
("ok".to_string(), pyjson::Json::Bool(false)),
("err".to_string(), pyjson::Json::Str(err)),
]),
};
println!("{}", pyjson::dumps(&body));
std::process::exit(if ok { 0 } else { 1 });
}
+444
View File
@@ -0,0 +1,444 @@
//! json_min.py — minimal JSON parse/serialize with Python-parity output.
//!
//! Exists because contract C1 demands byte-comparable state files with the
//! Python implementation (`json.dumps` / `json.dump(indent=2)`):
//! - compact separators: ", " between items, ": " between key/value
//! (Python json.dumps DEFAULTS — no separators arg means the space-y form)
//! - ensure_ascii: non-ASCII escaped as \uXXXX (with surrogate pairs)
//! - floats: shortest round-trip repr (Rust `{}` == Python `repr` for f64
//! in every value this module produces, including arithmetic artifacts
//! like 0.30000000000000004)
//! - indent=2 dump: newline+indent after every item separator, "{}" when
//! empty, NO trailing newline (json.dump never writes one)
//! Object key order is preserved (Vec of pairs) — matches Python 3.7+ dict
//! insertion order, which the classification tie-break law depends on.
use std::fmt::Write as _;
/// Preserved-order JSON value.
#[derive(Debug, Clone, PartialEq)]
pub enum Json {
Null,
Bool(bool),
Num(f64),
Str(String),
Arr(Vec<Json>),
Obj(Vec<(String, Json)>),
}
impl Json {
pub fn get(&self, key: &str) -> Option<&Json> {
match self {
Json::Obj(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v),
_ => None,
}
}
}
// ── parsing ─────────────────────────────────────────────────────────────────
pub fn parse(input: &str) -> Result<Json, String> {
let bytes = input.as_bytes();
let mut p = Parser { b: bytes, i: 0 };
p.skip_ws();
let v = p.value()?;
p.skip_ws();
if p.i != bytes.len() {
return Err(format!("trailing data at byte {}", p.i));
}
Ok(v)
}
struct Parser<'a> {
b: &'a [u8],
i: usize,
}
impl<'a> Parser<'a> {
fn skip_ws(&mut self) {
while self.i < self.b.len() && matches!(self.b[self.i], b' ' | b'\t' | b'\n' | b'\r') {
self.i += 1;
}
}
fn peek(&self) -> Option<u8> {
self.b.get(self.i).copied()
}
fn expect(&mut self, c: u8) -> Result<(), String> {
if self.peek() == Some(c) {
self.i += 1;
Ok(())
} else {
Err(format!("expected '{}' at byte {}", c as char, self.i))
}
}
fn value(&mut self) -> Result<Json, String> {
match self.peek() {
Some(b'{') => self.object(),
Some(b'[') => self.array(),
Some(b'"') => Ok(Json::Str(self.string()?)),
Some(b't') => self.literal("true", Json::Bool(true)),
Some(b'f') => self.literal("false", Json::Bool(false)),
Some(b'n') => self.literal("null", Json::Null),
Some(c) if c == b'-' || c.is_ascii_digit() => self.number(),
_ => Err(format!("unexpected byte at {}", self.i)),
}
}
fn literal(&mut self, lit: &str, v: Json) -> Result<Json, String> {
if self.b[self.i..].starts_with(lit.as_bytes()) {
self.i += lit.len();
Ok(v)
} else {
Err(format!("bad literal at byte {}", self.i))
}
}
fn object(&mut self) -> Result<Json, String> {
self.expect(b'{')?;
let mut pairs = Vec::new();
self.skip_ws();
if self.peek() == Some(b'}') {
self.i += 1;
return Ok(Json::Obj(pairs));
}
loop {
self.skip_ws();
let key = self.string()?;
self.skip_ws();
self.expect(b':')?;
self.skip_ws();
let val = self.value()?;
pairs.push((key, val));
self.skip_ws();
match self.peek() {
Some(b',') => self.i += 1,
Some(b'}') => {
self.i += 1;
return Ok(Json::Obj(pairs));
}
_ => return Err(format!("expected ',' or '}}' at byte {}", self.i)),
}
}
}
fn array(&mut self) -> Result<Json, String> {
self.expect(b'[')?;
let mut items = Vec::new();
self.skip_ws();
if self.peek() == Some(b']') {
self.i += 1;
return Ok(Json::Arr(items));
}
loop {
self.skip_ws();
items.push(self.value()?);
self.skip_ws();
match self.peek() {
Some(b',') => self.i += 1,
Some(b']') => {
self.i += 1;
return Ok(Json::Arr(items));
}
_ => return Err(format!("expected ',' or ']' at byte {}", self.i)),
}
}
}
fn string(&mut self) -> Result<String, String> {
self.expect(b'"')?;
let mut out = String::new();
loop {
match self.peek() {
None => return Err("unterminated string".into()),
Some(b'"') => {
self.i += 1;
return Ok(out);
}
Some(b'\\') => {
self.i += 1;
match self.peek() {
Some(b'"') => out.push('"'),
Some(b'\\') => out.push('\\'),
Some(b'/') => out.push('/'),
Some(b'b') => out.push('\u{0008}'),
Some(b'f') => out.push('\u{000C}'),
Some(b'n') => out.push('\n'),
Some(b'r') => out.push('\r'),
Some(b't') => out.push('\t'),
Some(b'u') => {
let cp = self.hex4()?;
// surrogate pair handling (Python accepts these)
if (0xD800..0xDC00).contains(&cp) {
if self.b[self.i + 1..].starts_with(b"\\u") {
self.i += 2;
let lo = self.hex4()?;
if (0xDC00..0xE000).contains(&lo) {
let c = 0x10000
+ ((cp - 0xD800) << 10)
+ (lo - 0xDC00);
out.push(
char::from_u32(c)
.ok_or("bad surrogate pair")?,
);
} else {
// lone high surrogate — Python keeps it as
// an escaped lone surrogate; we degrade to
// U+FFFD (not produced by this writer)
out.push('\u{FFFD}');
}
} else {
out.push('\u{FFFD}');
}
} else {
out.push(char::from_u32(cp).unwrap_or('\u{FFFD}'));
}
continue; // hex4 already advanced i
}
_ => return Err(format!("bad escape at byte {}", self.i)),
}
self.i += 1;
}
Some(_) => {
// consume one UTF-8 scalar
let s = &self.b[self.i..];
let ch_len = utf8_len(s[0]);
let chunk = std::str::from_utf8(&s[..ch_len.min(s.len())])
.map_err(|_| "bad utf8")?;
out.push_str(chunk);
self.i += ch_len;
}
}
}
}
fn hex4(&mut self) -> Result<u32, String> {
if self.i + 4 > self.b.len() {
return Err("short \\u escape".into());
}
let s = std::str::from_utf8(&self.b[self.i + 1..self.i + 5])
.map_err(|_| "bad \\u")?;
let v = u32::from_str_radix(s, 16).map_err(|_| "bad \\u digits")?;
self.i += 5; // 'u' + 4 hex digits (caller consumed the backslash)
Ok(v)
}
fn number(&mut self) -> Result<Json, String> {
let start = self.i;
if self.peek() == Some(b'-') {
self.i += 1;
}
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.i += 1;
}
if self.peek() == Some(b'.') {
self.i += 1;
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.i += 1;
}
}
if matches!(self.peek(), Some(b'e') | Some(b'E')) {
self.i += 1;
if matches!(self.peek(), Some(b'+') | Some(b'-')) {
self.i += 1;
}
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.i += 1;
}
}
let s = std::str::from_utf8(&self.b[start..self.i]).map_err(|_| "bad number")?;
s.parse::<f64>()
.map(Json::Num)
.map_err(|_| format!("bad number '{}'", s))
}
}
fn utf8_len(b: u8) -> usize {
if b < 0x80 {
1
} else if b >> 5 == 0b110 {
2
} else if b >> 4 == 0b1110 {
3
} else {
4
}
}
// ── serialization (Python json.dumps / json.dump(indent=2) parity) ─────────
/// Python `json.dumps(value, sort_keys=True)` — compact separators, object
/// keys recursively sorted. Python sorts str keys by code point; Rust's
/// byte-wise UTF-8 `String` ordering is identical for valid UTF-8, so a
/// plain `sort_by` on the key is faithful.
pub fn dumps_sorted(v: &Json) -> String {
let mut out = String::new();
write_compact_sorted(&mut out, v);
out
}
fn write_compact_sorted(out: &mut String, v: &Json) {
match v {
Json::Obj(pairs) => {
let mut sorted: Vec<&(String, Json)> = pairs.iter().collect();
sorted.sort_by(|a, b| a.0.cmp(&b.0));
out.push('{');
for (i, (k, val)) in sorted.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_escaped(out, k);
out.push_str(": ");
write_compact_sorted(out, val);
}
out.push('}');
}
Json::Arr(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_compact_sorted(out, item);
}
out.push(']');
}
other => write_compact(out, other),
}
}
/// Python `json.dumps(value)` — compact separators `", "` / `": "`,
/// ensure_ascii escaping.
pub fn dumps(v: &Json) -> String {
let mut out = String::new();
write_compact(&mut out, v);
out
}
fn write_compact(out: &mut String, v: &Json) {
match v {
Json::Null => out.push_str("null"),
Json::Bool(true) => out.push_str("true"),
Json::Bool(false) => out.push_str("false"),
Json::Num(n) => out.push_str(&fmt_num(*n)),
Json::Str(s) => write_escaped(out, s),
Json::Arr(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_compact(out, item);
}
out.push(']');
}
Json::Obj(pairs) => {
out.push('{');
for (i, (k, val)) in pairs.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_escaped(out, k);
out.push_str(": ");
write_compact(out, val);
}
out.push('}');
}
}
}
/// Python `json.dump(value, indent=2)` — item separator becomes
/// `",\n<indent>"`, key separator `": "`, empty container → `{}` / `[]`,
/// NO trailing newline.
pub fn dump_indent2(v: &Json) -> String {
let mut out = String::new();
write_indent(&mut out, v, 0);
out
}
fn write_indent(out: &mut String, v: &Json, depth: usize) {
match v {
Json::Arr(items) if !items.is_empty() => {
out.push_str("[\n");
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(",\n");
}
push_indent(out, depth + 1);
write_indent(out, item, depth + 1);
}
out.push('\n');
push_indent(out, depth);
out.push(']');
}
Json::Arr(_) => out.push_str("[]"),
Json::Obj(pairs) if !pairs.is_empty() => {
out.push_str("{\n");
for (i, (k, val)) in pairs.iter().enumerate() {
if i > 0 {
out.push_str(",\n");
}
push_indent(out, depth + 1);
write_escaped(out, k);
out.push_str(": ");
write_indent(out, val, depth + 1);
}
out.push('\n');
push_indent(out, depth);
out.push('}');
}
Json::Obj(_) => out.push_str("{}"),
other => write_compact(out, other),
}
}
fn push_indent(out: &mut String, depth: usize) {
for _ in 0..depth {
out.push_str(" ");
}
}
/// Integer-shaped f64 prints without a decimal point (matching how this
/// module's writers emit counts); all other values use shortest round-trip.
pub fn fmt_num(n: f64) -> String {
if n.is_finite() && n.fract() == 0.0 && n.abs() < 9.007_199_254_740_992e15 {
return format!("{}", n as i64);
}
format!("{}", n)
}
/// Python `json.dumps` string escaping with ensure_ascii=True.
pub fn write_escaped(out: &mut String, s: &str) {
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{0008}' => out.push_str("\\b"),
'\u{000C}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c if (c as u32) <= 0x7E => out.push(c),
// ensure_ascii: escape everything non-ASCII, surrogate pairs
// for astral planes (Python behaviour)
c => {
let cp = c as u32;
if cp > 0xFFFF {
let v = cp - 0x10000;
let hi = 0xD800 + (v >> 10);
let lo = 0xDC00 + (v & 0x3FF);
let _ = write!(out, "\\u{:04x}\\u{:04x}", hi, lo);
} else {
let _ = write!(out, "\\u{:04x}", cp);
}
}
}
}
out.push('"');
}
+214
View File
@@ -0,0 +1,214 @@
//! sqlite.rs — zero-dep read-only bindings to the SYSTEM libsqlite3
//! (R2 W2.0). Nick's ruling: SQLite is retained and wired in; a system
//! library is not a crate dependency, so the zero-dep law holds.
//!
//! READ-ONLY BY CONSTRUCTION: this module binds only open/prepare/bind/
//! step/column/finalize/close/errmsg — there is NO exec/prepare-for-write
//! path, and the crate exports no write-capable symbols (canary section 5
//! enforces it).
use crate::pyjson::Json;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_void};
const SQLITE_OK: c_int = 0;
const SQLITE_ROW: c_int = 100;
const SQLITE_DONE: c_int = 101;
const SQLITE_OPEN_READONLY: c_int = 0x00000001;
const SQLITE_INTEGER: c_int = 1;
const SQLITE_FLOAT: c_int = 2;
const SQLITE_TEXT: c_int = 3;
const SQLITE_BLOB: c_int = 4;
const SQLITE_NULL: c_int = 5;
#[link(name = "sqlite3")]
extern "C" {
fn sqlite3_open_v2(
filename: *const c_char,
db: *mut *mut c_void,
flags: c_int,
vfs: *const c_char,
) -> c_int;
fn sqlite3_prepare_v2(
db: *mut c_void,
sql: *const c_char,
nbytes: c_int,
stmt: *mut *mut c_void,
tail: *mut *const c_char,
) -> c_int;
fn sqlite3_bind_int64(stmt: *mut c_void, idx: c_int, val: i64) -> c_int;
fn sqlite3_bind_double(stmt: *mut c_void, idx: c_int, val: f64) -> c_int;
fn sqlite3_bind_text(
stmt: *mut c_void,
idx: c_int,
val: *const c_char,
nbytes: c_int,
destructor: *mut c_void,
) -> c_int;
fn sqlite3_bind_null(stmt: *mut c_void, idx: c_int) -> c_int;
fn sqlite3_step(stmt: *mut c_void) -> c_int;
fn sqlite3_column_count(stmt: *mut c_void) -> c_int;
fn sqlite3_column_type(stmt: *mut c_void, i: c_int) -> c_int;
fn sqlite3_column_int64(stmt: *mut c_void, i: c_int) -> i64;
fn sqlite3_column_double(stmt: *mut c_void, i: c_int) -> f64;
fn sqlite3_column_text(stmt: *mut c_void, i: c_int) -> *const u8;
fn sqlite3_column_blob(stmt: *mut c_void, i: c_int) -> *const c_void;
fn sqlite3_column_bytes(stmt: *mut c_void, i: c_int) -> c_int;
fn sqlite3_finalize(stmt: *mut c_void) -> c_int;
fn sqlite3_close(db: *mut c_void) -> c_int;
fn sqlite3_errmsg(db: *mut c_void) -> *const c_char;
}
// SQLITE_TRANSIENT (-1 as a function pointer) — SQLite copies the bound text.
const SQLITE_TRANSIENT: *mut c_void = !0usize as *mut c_void;
struct Db {
handle: *mut c_void,
}
impl Drop for Db {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe { sqlite3_close(self.handle) };
}
}
}
fn errmsg(db: *mut c_void) -> String {
unsafe {
let ptr = sqlite3_errmsg(db);
if ptr.is_null() {
return "unknown sqlite error".to_string();
}
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
/// read_query(db_path, sql, params) -> rows as JSON arrays.
/// Params: JSON array; numbers bind int64/double by integrality, strings
/// bind text, bools bind 0/1, and null binds SQL NULL.
pub fn read_query(db_path: &str, sql: &str, params: &[Json]) -> Result<Vec<Vec<Json>>, String> {
let c_path = CString::new(db_path).map_err(|_| "db path contains NUL")?;
let c_sql = CString::new(sql).map_err(|_| "sql contains NUL")?;
let mut handle: *mut c_void = std::ptr::null_mut();
// READ-ONLY at the SQLite connection level. A read-sounding exported
// symbol is not enough: sqlite3_open() permits writes and creates a
// missing database. open_v2(READONLY) makes mutation impossible even
// if a caller supplies INSERT/UPDATE SQL.
let rc = unsafe {
sqlite3_open_v2(
c_path.as_ptr(),
&mut handle,
SQLITE_OPEN_READONLY,
std::ptr::null(),
)
};
if rc != SQLITE_OK {
let msg = if handle.is_null() {
"cannot open database file".to_string()
} else {
errmsg(handle)
};
if !handle.is_null() {
unsafe { sqlite3_close(handle) };
}
return Err(msg);
}
let db = Db { handle };
let mut stmt: *mut c_void = std::ptr::null_mut();
let rc = unsafe {
sqlite3_prepare_v2(
db.handle,
c_sql.as_ptr(),
-1,
&mut stmt,
std::ptr::null_mut(),
)
};
if rc != SQLITE_OK {
return Err(errmsg(db.handle));
}
struct Stmt(*mut c_void);
impl Drop for Stmt {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { sqlite3_finalize(self.0) };
}
}
}
let stmt = Stmt(stmt);
for (i, p) in params.iter().enumerate() {
let idx = (i + 1) as c_int;
let rc = match p {
Json::Num(n) => {
if n.fract() == 0.0 && n.abs() < 9.0e15 {
unsafe { sqlite3_bind_int64(stmt.0, idx, *n as i64) }
} else {
unsafe { sqlite3_bind_double(stmt.0, idx, *n) }
}
}
Json::Str(s) => {
let c = CString::new(s.as_str()).map_err(|_| "param contains NUL")?;
unsafe {
sqlite3_bind_text(stmt.0, idx, c.as_ptr(), s.len() as c_int, SQLITE_TRANSIENT)
}
}
Json::Bool(b) => unsafe { sqlite3_bind_int64(stmt.0, idx, if *b { 1 } else { 0 }) },
Json::Null => unsafe { sqlite3_bind_null(stmt.0, idx) },
_ => return Err("unsupported param type".to_string()),
};
if rc != SQLITE_OK {
return Err(errmsg(db.handle));
}
}
let col_count = unsafe { sqlite3_column_count(stmt.0) };
let mut rows: Vec<Vec<Json>> = vec![];
loop {
let rc = unsafe { sqlite3_step(stmt.0) };
if rc == SQLITE_DONE {
break;
}
if rc != SQLITE_ROW {
return Err(errmsg(db.handle));
}
let mut row: Vec<Json> = vec![];
for i in 0..col_count {
let v = match unsafe { sqlite3_column_type(stmt.0, i) } {
SQLITE_INTEGER => Json::Num(unsafe { sqlite3_column_int64(stmt.0, i) } as f64),
SQLITE_FLOAT => Json::Num(unsafe { sqlite3_column_double(stmt.0, i) }),
SQLITE_TEXT => {
let ptr = unsafe { sqlite3_column_text(stmt.0, i) };
let n = unsafe { sqlite3_column_bytes(stmt.0, i) };
if ptr.is_null() || n == 0 {
Json::Str(String::new())
} else {
let slice = unsafe { std::slice::from_raw_parts(ptr, n as usize) };
Json::Str(String::from_utf8_lossy(slice).into_owned())
}
}
SQLITE_BLOB => {
let ptr = unsafe { sqlite3_column_blob(stmt.0, i) };
let n = unsafe { sqlite3_column_bytes(stmt.0, i) };
let slice = if ptr.is_null() {
&[]
} else {
unsafe { std::slice::from_raw_parts(ptr as *const u8, n as usize) }
};
// blobs render as byte arrays (the JSON transport has no
// bytes type; Python's bytes render via list(x) here)
Json::Arr(slice.iter().map(|b| Json::Num(*b as f64)).collect())
}
SQLITE_NULL => Json::Null,
_ => Json::Null,
};
row.push(v);
}
rows.push(row);
}
Ok(rows)
}
File diff suppressed because it is too large. Load diff
+401 -272
View File
File diff suppressed because it is too large. Load diff
+38
View File
@@ -389,3 +389,41 @@ def test_construction_decision_placement_delegation():
assert py["placement"] == "rust"
pub = construction_decision.language_placement("rune", ["storage"])
assert pub == py or pub["placement"] == "rust"
# ── mems_loader / SQLite read path (R2 W2.0) ────────────────────────────────
def test_mems_loader_read_path_through_cdylib(tmp_path):
import sqlite3 as _sq
from rune import mems_loader
if not mems_loader.available():
pytest.skip("memory_storage crate not built")
db_path = str(tmp_path / "t.db")
conn = _sq.connect(db_path)
conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
conn.execute("INSERT INTO t (name) VALUES ('alpha')")
conn.commit()
conn.close()
rows = mems_loader.read_query(db_path, "SELECT id, name FROM t", [])
assert rows == [[1, "alpha"]]
env = mems_loader.read_query_result(db_path, "SELECT x FROM missing", [])
assert env.get("ok") is False and "no such table" in env.get("err", "")
def test_memory_store_storage_seam(tmp_path):
import sqlite3 as _sq
from rune import memory_store
db_path = str(tmp_path / "s.db")
conn = _sq.connect(db_path)
conn.execute("CREATE TABLE entries (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'active', topic TEXT DEFAULT '')")
conn.execute("CREATE TABLE working (id INTEGER PRIMARY KEY)")
conn.execute("CREATE TABLE audit_log (id INTEGER PRIMARY KEY)")
conn.execute("INSERT INTO entries (topic) VALUES ('general')")
conn.commit()
conn.close()
out = memory_store.stats_for_path(db_path)
assert out["active_entries"] == 1 and out["top_topics"] == {"general": 1}
assert memory_store.storage_backend().name == "sqlite"