feat(memory_store): close stats-family audit gaps — dichotomy wave
WORKING-SET/STATS/AUDIT lane (builder factory-heavy-ms-stats): the 5 currently-unmatched slice fns ported name-preserving into the memory-store-deep Go corpus in a NEW family file gaps_stats.go (siblings untouched) — reindex_entries (gap census + dry-run + full rebuild with contentless-FTS5 DROP/recreate + vec_missing/fts_missing counting), wake_up (CHAR-budgeted '- text' lines — rune count, never bytes), storage_backend (R2 W2.0 read-backend seam: sqlite default, RUNE_MEMORY_STORAGE=rust opt-in via injectable probe; loader is FFI machinery), stats_for_path (stats through the seam; by_topic EMISSION order preserved via parallel pair list — Go maps sort it away), handle_hook (default envelope). The other 12 slice fns were already landed by earlier waves (main.go/gaps_retrieval.go) — audit re-run first per law. Truth: 24-row truth_stats_gaps.json from the live oracle under pinned rigs (frozen clock, _embed off/scripted, fake mems_loader sys.modules injection, plain-table entries_vec mirror — vec0 observed only through COUNT/JOIN/DELETE/INSERT). Battery gaps_stats_test.go replays canonically (GAPS_TRUTH_PATH override); live parity 24/24 (fresh capture → diff vs frozen → Go battery); gofmt/vet/build/full-test clean; Python module suite failure sets byte-identical across both pins (RUNE_RETRIEVAL_CORE live + python, 9 pre-existing, none this port — no tracked Python/tests files touched); ruff clean. Documented divergences in verification/msstats-summary.md: contentless-FTS5 NULL text readback (both engines), json.dumps float-list format (pyJSONFloatList mirror, integral floats keep .0), by_topic tie order engine-shaped (distinct-count fixtures). Audit: memory_store 44/68 → 51/68; the read_query ×2 counters (dm lane's slice) also flipped via this port's ReadQuery interface method name — name-collision flip, NOT a dm port.
This commit is contained in:
1 parent
2092134c2a
commit
5591f3a921
10 files changed
+2305
No files matched your search
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""check_memory_store_stats_parity.py — live parity for the memory_store
|
||||
STATS family gap port (builder factory-heavy-ms-stats).
|
||||
|
||||
Regenerates the truth corpus from the LIVE oracle (all rigs active) into a
|
||||
temp file, byte-compares it against the committed frozen truth (drift gate),
|
||||
then replays the FRESH corpus through the Go battery via GAPS_TRUTH_PATH.
|
||||
Green = the Go kernels in src/go/memory-store-deep/gaps_stats.go agree with
|
||||
the live Python oracle on every row, in both directions.
|
||||
|
||||
Usage: python3 scripts/check_memory_store_stats_parity.py
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
GEN = REPO / "scripts/gen_memory_store_stats_truth.py"
|
||||
TRUTH = REPO / "src/go/memory-store-deep/truth_stats_gaps.json"
|
||||
GO_DIR = REPO / "src/go/memory-store-deep"
|
||||
|
||||
|
||||
def run(cmd, **kw):
|
||||
print("+", " ".join(str(c) for c in cmd))
|
||||
return subprocess.run(cmd, capture_output=True, text=True, **kw)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tmp = Path(tempfile.mkdtemp(prefix="ms_stats_parity_")) / "truth_fresh.json"
|
||||
|
||||
r = run([sys.executable, str(GEN), "--out", str(tmp)])
|
||||
print(r.stdout.strip() or r.stderr.strip())
|
||||
if r.returncode != 0:
|
||||
print("FAIL: generator crashed")
|
||||
return 1
|
||||
|
||||
# drift gate: fresh capture must equal the committed frozen truth
|
||||
r = run(["diff", str(TRUTH), str(tmp)])
|
||||
if r.returncode != 0:
|
||||
print("FAIL: live oracle drifted from frozen truth:")
|
||||
print(r.stdout[:2000])
|
||||
return 1
|
||||
print("frozen truth == fresh live capture (byte-identical)")
|
||||
|
||||
# battery replay against the FRESH corpus
|
||||
import os
|
||||
env = dict(os.environ, GAPS_TRUTH_PATH=str(tmp))
|
||||
r = run(["go", "test", "-run", "TestStatsGapsParity", "-v", "./..."],
|
||||
cwd=GO_DIR, env=env)
|
||||
out = r.stdout + r.stderr
|
||||
print(out[-1500:])
|
||||
if r.returncode != 0:
|
||||
print("FAIL: Go battery diverges from live-oracle truth")
|
||||
return 1
|
||||
|
||||
n = TRUTH.read_text().count('"fn"')
|
||||
print(f"PARITY OK: {n} frozen rows byte-stable + Go battery green on fresh truth")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,390 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_memory_store_stats_truth.py — memory_store WORKING-SET/STATS/AUDIT
|
||||
family truth battery (dichotomy wave #2479, builder factory-heavy-ms-stats).
|
||||
|
||||
Freezes the currently-audit-missing stats-family fns against the LIVE oracle
|
||||
(src/rune/memory_store.py): reindex_entries, wake_up, storage_backend,
|
||||
stats_for_path, handle_hook. Every case runs the live oracle under pinned
|
||||
rigs and captures both the fn inputs (so the Go battery in
|
||||
src/go/memory-store-deep/gaps_stats_test.go replays standalone) and the
|
||||
oracle output. Seams per the landed crate's law: the SQLite row-fetch for
|
||||
wake_up stays facade-side (rows ride the case input); _embed is pinned OFF
|
||||
or scripted deterministic vectors (no network); the rust read backend is a
|
||||
sys.modules-injected fake mems_loader (the loader is Python FFI machinery —
|
||||
its availability + traffic is the contract, not the .so); the vec side of
|
||||
reindex runs on a plain entries_vec mirror table (production vec0 is only
|
||||
observed through COUNT/LEFT JOIN/DELETE/INSERT, which a plain table serves
|
||||
identically — Go has no vec0). No wall-clock, no HOME-derived or absolute
|
||||
paths in any truth row.
|
||||
|
||||
Usage:
|
||||
python3 scripts/gen_memory_store_stats_truth.py # write truth
|
||||
python3 scripts/gen_memory_store_stats_truth.py --check # byte-compare
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
RIG_HOME = tempfile.mkdtemp(prefix="ms_stats_rig_")
|
||||
os.environ["HOME"] = RIG_HOME # import-time path derivation pin
|
||||
|
||||
from freeze_truth import _frozen_clock # noqa: E402 — shared harness rig helper
|
||||
|
||||
import memory_store as ms # noqa: E402
|
||||
|
||||
FROZEN_NOW = "2026-10-15T00:00:00"
|
||||
ms.datetime = _frozen_clock(FROZEN_NOW)
|
||||
|
||||
# ── fixture DDLs (rig mirrors; the Go battery builds the identical shape) ────
|
||||
|
||||
WAKE_DDL = """
|
||||
CREATE TABLE entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
importance REAL DEFAULT 0.5,
|
||||
created_at TEXT,
|
||||
last_accessed TEXT,
|
||||
topic TEXT DEFAULT '',
|
||||
source TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'active'
|
||||
)
|
||||
"""
|
||||
|
||||
REINDEX_DDL = """
|
||||
CREATE TABLE entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'active'
|
||||
);
|
||||
CREATE TABLE entries_vec (rowid INTEGER PRIMARY KEY, embedding TEXT);
|
||||
CREATE VIRTUAL TABLE entries_fts USING fts5(text, content='', tokenize='porter unicode61');
|
||||
"""
|
||||
|
||||
STATS_DDL = """
|
||||
CREATE TABLE entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
topic TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'active'
|
||||
);
|
||||
CREATE TABLE working (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
prev_hash TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
claim TEXT,
|
||||
verdict TEXT,
|
||||
evidence TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def mem_db(ddl: str) -> sqlite3.Connection:
|
||||
db = sqlite3.connect(":memory:")
|
||||
db.row_factory = sqlite3.Row
|
||||
db.executescript(ddl)
|
||||
return db
|
||||
|
||||
|
||||
def reindex_fixture(entries, vec_rows, fts_rows):
|
||||
"""entries: [(id, text, status)]; vec: [(rowid, emb_text)]; fts: [(rowid, text)]."""
|
||||
db = mem_db(REINDEX_DDL)
|
||||
for (eid, text, status) in entries:
|
||||
db.execute("INSERT INTO entries (id, text, status) VALUES (?,?,?)", (eid, text, status))
|
||||
for (rid, emb) in vec_rows:
|
||||
db.execute("INSERT INTO entries_vec (rowid, embedding) VALUES (?,?)", (rid, emb))
|
||||
for (rid, text) in fts_rows:
|
||||
db.execute("INSERT INTO entries_fts (rowid, text) VALUES (?,?)", (rid, text))
|
||||
db.commit()
|
||||
return db
|
||||
|
||||
|
||||
def reindex_post(db):
|
||||
fts = [[r[0], r[1]] for r in db.execute("SELECT rowid, text FROM entries_fts ORDER BY rowid").fetchall()]
|
||||
vec = [[r[0], r[1]] for r in db.execute("SELECT rowid, embedding FROM entries_vec ORDER BY rowid").fetchall()]
|
||||
return {"fts": fts, "vec": vec}
|
||||
|
||||
|
||||
def wake_fixture(entries):
|
||||
"""entries: [(id, text, importance, topic, source, status)] — importances distinct (SQL tie order is engine-shaped, not contract)."""
|
||||
db = mem_db(WAKE_DDL)
|
||||
for (eid, text, imp, topic, source, status) in entries:
|
||||
db.execute(
|
||||
"INSERT INTO entries (id, text, importance, topic, source, status) VALUES (?,?,?,?,?,?)",
|
||||
(eid, text, imp, topic, source, status))
|
||||
db.commit()
|
||||
return db
|
||||
|
||||
|
||||
def wake_rows(db):
|
||||
return [
|
||||
{"text": r["text"], "topic": r["topic"], "importance": r["importance"], "source": r["source"]}
|
||||
for r in db.execute(
|
||||
"SELECT text, topic, importance, source FROM entries"
|
||||
" WHERE status='active' ORDER BY importance DESC LIMIT 10"
|
||||
).fetchall()
|
||||
]
|
||||
|
||||
|
||||
def stats_fixture_file(entries, working, audit):
|
||||
"""Build the stats_for_path store on a real FILE (the backend opens per read)."""
|
||||
d = tempfile.mkdtemp(prefix="ms_stats_store_")
|
||||
path = os.path.join(d, "store.db")
|
||||
db = sqlite3.connect(path)
|
||||
db.executescript(STATS_DDL)
|
||||
for (eid, text, topic, status) in entries:
|
||||
db.execute("INSERT INTO entries (id, text, topic, status) VALUES (?,?,?,?)", (eid, text, topic, status))
|
||||
for (i, tid) in enumerate(working, start=1):
|
||||
db.execute("INSERT INTO working (id, task_id, text, created_at) VALUES (?,?,?,?)",
|
||||
(i, tid, f"working note {i}", "2026-01-01T00:00:00+00:00"))
|
||||
for i in range(1, len(audit) + 1):
|
||||
db.execute("INSERT INTO audit_log (id, prev_hash, hash, claim, verdict, evidence, created_at)"
|
||||
" VALUES (?,?,?,?,?,?,?)", (i, "GENESIS", "h" * 24, f"claim {i}", "pass", "", "2026-01-01T00:00:00+00:00"))
|
||||
db.commit()
|
||||
db.close()
|
||||
return path
|
||||
|
||||
|
||||
def set_env(key, value):
|
||||
"""Pin/unpin an env var; returns the restore fn."""
|
||||
saved = os.environ.get(key)
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
|
||||
def restore():
|
||||
if saved is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = saved
|
||||
|
||||
return restore
|
||||
|
||||
|
||||
def main():
|
||||
cases = []
|
||||
|
||||
def add(fn, name, inp, expected):
|
||||
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
|
||||
|
||||
# ── handle_hook (default envelope — no rigs needed) ──────────────────────
|
||||
for name, event, data in [
|
||||
("hh_basic", "session_start", None),
|
||||
("hh_data_dict", "memory_write", {"id": 7, "topic": "coffee"}),
|
||||
("hh_data_list_unicode", "grüße-Ω-event", ["alpha", "βeta"]),
|
||||
("hh_empty_event", "", None),
|
||||
]:
|
||||
add("handle_hook", name, {"event": event, "data": data}, ms.handle_hook(event, data))
|
||||
|
||||
# ── wake_up (rows pre-fetched facade-side; budget in CHARACTERS) ─────────
|
||||
W = [
|
||||
# id, text, importance, topic, source, status — importances distinct
|
||||
(1, "nick prefers black coffee in the morning", 0.95, "person", "curator", "active"),
|
||||
(2, "the spine rebuilds at three", 0.85, "ops", "curator", "active"),
|
||||
(3, "αβγδε ζηθικ λμνξο — the greek row", 0.75, "unicode", "curator", "active"),
|
||||
(4, "budget pressure trims the tail", 0.65, "ops", "curator", "active"),
|
||||
(5, "inactive rows never surface", 0.99, "person", "curator", "inactive"),
|
||||
(6, "tail line one", 0.55, "", "curator", "active"),
|
||||
(7, "tail line two", 0.45, "", "curator", "active"),
|
||||
]
|
||||
wdb = wake_fixture(W)
|
||||
wrows = wake_rows(wdb)
|
||||
for name, budget in [
|
||||
("wu_default_800", 800),
|
||||
("wu_tight_95", 95),
|
||||
("wu_budget_zero", 0),
|
||||
]:
|
||||
add("wake_up", name, {"rows": wrows, "budget_chars": budget}, ms.wake_up(wdb, budget_chars=budget))
|
||||
# char-vs-byte discriminator: greek row FIRST, budget 20 — the line is 17
|
||||
# CHARACTERS (fits) but 32 UTF-8 BYTES (byte-len would break the budget)
|
||||
CU = [(1, "αβγδεζηθικλμνξο", 0.9, "", "", "active"), (2, "beta row", 0.5, "", "", "active")]
|
||||
cdb = wake_fixture(CU)
|
||||
add("wake_up", "wu_chars_not_bytes", {"rows": wake_rows(cdb), "budget_chars": 20},
|
||||
ms.wake_up(cdb, budget_chars=20))
|
||||
wdb_empty = wake_fixture([(1, "only an inactive row", 0.9, "", "", "inactive")])
|
||||
add("wake_up", "wu_no_active", {"rows": wake_rows(wdb_empty), "budget_chars": 800}, ms.wake_up(wdb_empty, budget_chars=800))
|
||||
many = [(i, f"line number {i:02d} of many", 0.9 - i * 0.01, "", "", "active") for i in range(1, 13)]
|
||||
wdb12 = wake_fixture(many)
|
||||
add("wake_up", "wu_ten_cap", {"rows": wake_rows(wdb12), "budget_chars": 800}, ms.wake_up(wdb12, budget_chars=800))
|
||||
|
||||
# ── storage_backend (env branches; loader = injected fake) ───────────────
|
||||
restore_storage = set_env("RUNE_MEMORY_STORAGE", None)
|
||||
add("storage_backend", "sb_default_sqlite", {"env": None, "loader_available": None},
|
||||
{"name": ms.storage_backend().name})
|
||||
fake = types.ModuleType("mems_loader")
|
||||
fake.available = lambda: False
|
||||
sys.modules["rune.mems_loader"] = fake
|
||||
try:
|
||||
restore_storage = set_env("RUNE_MEMORY_STORAGE", "rust")
|
||||
add("storage_backend", "sb_rust_unavailable", {"env": "rust", "loader_available": False},
|
||||
{"name": ms.storage_backend().name})
|
||||
fake.available = lambda: True
|
||||
add("storage_backend", "sb_rust_available", {"env": "rust", "loader_available": True},
|
||||
{"name": ms.storage_backend().name})
|
||||
finally:
|
||||
restore_storage()
|
||||
del sys.modules["rune.mems_loader"]
|
||||
|
||||
# ── stats_for_path (real file through the sqlite backend) ────────────────
|
||||
SFULL = [
|
||||
# id, text, topic, status — counts distinct (GROUP BY tie order is engine-shaped)
|
||||
(1, "espresso beans rest four days", "coffee", "active"),
|
||||
(2, "the grinder burrs matter", "coffee", "active"),
|
||||
(3, "water at ninety three degrees", "coffee", "active"),
|
||||
(4, "first flush darjeeling", "tea", "active"),
|
||||
(5, "oolong rolled not cut", "tea", "active"),
|
||||
(6, "an empty topic row", "", "active"),
|
||||
(7, "a null topic row", None, "active"),
|
||||
(8, "retired row never counted", "coffee", "inactive"),
|
||||
]
|
||||
path = stats_fixture_file(SFULL, working=["t1", "t2"], audit=[(1,)])
|
||||
got = ms.stats_for_path(path)
|
||||
order = [[k, v] for k, v in got["top_topics"].items()]
|
||||
add("stats_for_path", "sp_sqlite_full",
|
||||
{"backend": "sqlite", "entries": SFULL, "working": 2, "audit": 1},
|
||||
{"stats": got, "top_topics_order": order})
|
||||
path_sparse = stats_fixture_file([(1, "lonely entry", "misc", "active")], working=[], audit=[])
|
||||
got = ms.stats_for_path(path_sparse)
|
||||
add("stats_for_path", "sp_sqlite_sparse",
|
||||
{"backend": "sqlite", "entries": [(1, "lonely entry", "misc", "active")], "working": 0, "audit": 0},
|
||||
{"stats": got, "top_topics_order": [[k, v] for k, v in got["top_topics"].items()]})
|
||||
path_none = stats_fixture_file([(1, "empty topic one", "", "active"), (2, "empty topic two", None, "active")],
|
||||
working=[], audit=[])
|
||||
got = ms.stats_for_path(path_none)
|
||||
add("stats_for_path", "sp_sqlite_no_topics",
|
||||
{"backend": "sqlite", "entries": [(1, "empty topic one", "", "active"), (2, "empty topic two", None, "active")],
|
||||
"working": 0, "audit": 0},
|
||||
{"stats": got, "top_topics_order": [[k, v] for k, v in got["top_topics"].items()]})
|
||||
|
||||
# rust backend branch: scripted read_query rows + traffic recorder (the
|
||||
# query sequence through the seam is the contract any backend implements)
|
||||
scripted = [
|
||||
[(5,)], # active entries count
|
||||
[(2,)], # working count
|
||||
[(1,)], # audit count
|
||||
[("coffee", 3), ("tea", 2), ("", 1), ("misc", 1)], # by_topic rows ('' filtered)
|
||||
]
|
||||
fake_rust = types.ModuleType("mems_loader")
|
||||
fake_rust.available = lambda: True
|
||||
traffic = []
|
||||
|
||||
def fake_read_query(db_path, sql, params):
|
||||
traffic.append({"sql": sql, "params": list(params)})
|
||||
return scripted[len(traffic) - 1]
|
||||
|
||||
fake_rust.read_query = fake_read_query
|
||||
sys.modules["rune.mems_loader"] = fake_rust
|
||||
try:
|
||||
restore_storage = set_env("RUNE_MEMORY_STORAGE", "rust")
|
||||
backend = ms.storage_backend()
|
||||
assert backend.name == "rust-memory-storage"
|
||||
got = ms.stats_for_path("whatever.db")
|
||||
finally:
|
||||
restore_storage()
|
||||
del sys.modules["rune.mems_loader"]
|
||||
add("stats_for_path", "sp_rust_scripted",
|
||||
{"backend": "rust", "scripted": scripted},
|
||||
{"stats": got, "top_topics_order": [[k, v] for k, v in got["top_topics"].items()],
|
||||
"queries": traffic})
|
||||
|
||||
# ── reindex_entries (fixture stores; _embed pinned) ──────────────────────
|
||||
E3 = [(1, "alpha fact about espresso", "active"), (2, "beta fact about grinders", "active"),
|
||||
(3, "gamma fact about water", "active")]
|
||||
|
||||
def reindex_case(name, entries, vec_rows, fts_rows, embed_map, dry_run):
|
||||
if embed_map is None:
|
||||
ms._embed = lambda texts: {}
|
||||
else:
|
||||
ms._embed = lambda texts: {t: embed_map.get(t) for t in texts}
|
||||
db = reindex_fixture(entries, vec_rows, fts_rows)
|
||||
got = ms.reindex_entries(db, dry_run=dry_run)
|
||||
post = reindex_post(db)
|
||||
db.close()
|
||||
add("reindex_entries", name,
|
||||
{"entries": [list(e) for e in entries], "vec_rows": [list(v) for v in vec_rows],
|
||||
"fts_rows": [list(f) for f in fts_rows], "embed": embed_map, "dry_run": dry_run},
|
||||
{"result": got, "post": post})
|
||||
|
||||
# dry-run over a gapped store: reports, writes nothing (post == pre state)
|
||||
reindex_case("ri_dry_run_gap", E3,
|
||||
[(1, "[0.5, 0.25]")], [(1, "alpha fact about espresso"), (98, "dangling fts row")],
|
||||
None, True)
|
||||
# missing vec rows -> full rebuild; embed off -> every active row counts vec_missing
|
||||
reindex_case("ri_missing_rebuild", E3,
|
||||
[(1, "[0.5, 0.25]")], [(1, "alpha fact about espresso"), (2, "beta fact about grinders"),
|
||||
(3, "gamma fact about water")],
|
||||
None, False)
|
||||
# complete counts but dangling rows -> rebuild
|
||||
reindex_case("ri_dangling_rebuild", E3,
|
||||
[(1, "a"), (2, "b"), (3, "c"), (99, "dangling vec")],
|
||||
[(1, "alpha fact about espresso"), (2, "beta fact about grinders"),
|
||||
(3, "gamma fact about water"), (98, "dangling fts row")],
|
||||
None, False)
|
||||
# vec complete, fts EMPTY (recreated table): zero dangling but stale -> rebuild
|
||||
reindex_case("ri_fts_stale_rebuild", E3[:2],
|
||||
[(1, "a"), (2, "b")], [],
|
||||
None, False)
|
||||
# no gap at all -> rebuilt False, store untouched
|
||||
reindex_case("ri_no_gap", E3[:2],
|
||||
[(1, "a"), (2, "b")],
|
||||
[(1, "alpha fact about espresso"), (2, "beta fact about grinders")],
|
||||
None, False)
|
||||
# scripted embeddings ride the vec side (json.dumps separators + the
|
||||
# integral-float '1.0' shape pinned by the post state)
|
||||
reindex_case("ri_embed_on", E3[:2],
|
||||
[], [],
|
||||
{"alpha fact about espresso": [0.5, 0.25], "beta fact about grinders": [1.0, 0.5]},
|
||||
False)
|
||||
# inactive rows never re-enter the indexes (dangling-by-staleness rebuild)
|
||||
E5 = E3 + [(4, "retired fact one", "inactive"), (5, "retired fact two", "inactive")]
|
||||
reindex_case("ri_inactive_excluded", E5,
|
||||
[(1, "a"), (2, "b"), (3, "c"), (4, "d")],
|
||||
[(1, "alpha fact about espresso"), (2, "beta fact about grinders"),
|
||||
(3, "gamma fact about water"), (5, "retired fts row")],
|
||||
None, False)
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/memory_store.py (stats/working-set family; rigs: _embed pinned,"
|
||||
" fake mems_loader via sys.modules, vec side on plain-table mirror)",
|
||||
"family": "working-set/stats/audit — builder factory-heavy-ms-stats",
|
||||
"frozen_now": FROZEN_NOW,
|
||||
"generated": "2026-10-15",
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/memory-store-deep" / "truth_stats_gaps.json"
|
||||
if "--out" in sys.argv:
|
||||
target = Path(sys.argv[sys.argv.index("--out") + 1])
|
||||
body = json.dumps(out, indent=1, ensure_ascii=False) + "\n"
|
||||
|
||||
if "--check" in sys.argv:
|
||||
current = target.read_text()
|
||||
if current == body:
|
||||
print(f"OK: byte-identical ({len(cases)} cases) — {target}")
|
||||
return 0
|
||||
print(f"MISMATCH: regenerated truth differs from {target}")
|
||||
return 1
|
||||
|
||||
target.write_text(body)
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,305 @@
|
||||
package main
|
||||
|
||||
// gaps_stats.go — memory_store WORKING-SET/STATS/AUDIT family gap-close
|
||||
// (dichotomy wave #2479): the audit-missing stats-family fns ported
|
||||
// name-preserving into the memory-store-deep crate. Oracle:
|
||||
// src/rune/memory_store.py; truth: truth_stats_gaps.json (battery
|
||||
// gaps_stats_test.go, GAPS_TRUTH_PATH override powers live parity via
|
||||
// scripts/check_memory_store_stats_parity.py).
|
||||
//
|
||||
// Seams per the landed crate's law: wake_up's row-fetch stays facade-side
|
||||
// (rows ride the truth input); the rust read backend is an injectable probe
|
||||
// (mems_loader is Python FFI machinery — its availability + query traffic is
|
||||
// the contract, not an in-process .so); reindex's vec side runs on the
|
||||
// plain-table entries_vec mirror (production vec0 is observed only through
|
||||
// COUNT/LEFT JOIN/DELETE/INSERT, which a plain table serves identically).
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ── wake_up ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// WakeRow — one pre-fetched wake-up row (the facade applies
|
||||
// ORDER BY importance DESC LIMIT 10; topic/importance/source ride the row
|
||||
// because the oracle's SELECT carries them even though only text is used).
|
||||
type WakeRow struct {
|
||||
Text string `json:"text"`
|
||||
Topic string `json:"topic"`
|
||||
Importance float64 `json:"importance"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// WakeUp — session-start context (L0 core + L1 top memories): full-text
|
||||
// lines under a budget. Python len() counts CHARACTERS — the budget check
|
||||
// is rune-count, never byte-count (quirk law; pinned by wu_chars_not_bytes).
|
||||
func WakeUp(rows []WakeRow, budgetChars int) string {
|
||||
parts := []string{}
|
||||
used := 0
|
||||
for _, r := range rows {
|
||||
line := "- " + r.Text
|
||||
n := utf8.RuneCountInString(line)
|
||||
if used+n > budgetChars {
|
||||
break
|
||||
}
|
||||
parts = append(parts, line)
|
||||
used += n
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
return "# What matters (wake-up context)\n\n" + strings.Join(parts, "\n")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── handle_hook ──────────────────────────────────────────────────────────────
|
||||
|
||||
// HandleHook — the default hook envelope (specific scripts override).
|
||||
func HandleHook(event string, data interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{"status": "ok", "event": event, "handler": "default"}
|
||||
}
|
||||
|
||||
// ── storage backend seam (R2 W2.0) ──────────────────────────────────────────
|
||||
|
||||
// ReadBackend — the storage-backend seam: query logic separated from
|
||||
// storage mechanics. READS only; writes keep connection-side semantics.
|
||||
type ReadBackend interface {
|
||||
Name() string
|
||||
ReadQuery(dbPath, query string, params ...interface{}) [][]interface{}
|
||||
}
|
||||
|
||||
// SQLiteBackend — the default backend: one pure-Go sqlite connection per
|
||||
// read, rows returned as tuples (the oracle's [tuple(r) for r in ...]).
|
||||
type SQLiteBackend struct{}
|
||||
|
||||
func (SQLiteBackend) Name() string { return "sqlite" }
|
||||
|
||||
func (SQLiteBackend) ReadQuery(dbPath, query string, params ...interface{}) [][]interface{} {
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query(query, params...)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
cols, _ := rows.Columns()
|
||||
out := [][]interface{}{}
|
||||
vals := make([]interface{}, len(cols))
|
||||
ptrs := make([]interface{}, len(cols))
|
||||
for i := range vals {
|
||||
ptrs[i] = &vals[i]
|
||||
}
|
||||
for rows.Next() {
|
||||
newVals := make([]interface{}, len(cols))
|
||||
for i := range vals {
|
||||
ptrs[i] = &newVals[i]
|
||||
}
|
||||
rows.Scan(ptrs...)
|
||||
for i, v := range newVals {
|
||||
if b, ok := v.([]byte); ok {
|
||||
newVals[i] = string(b)
|
||||
}
|
||||
}
|
||||
out = append(out, newVals)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// RustReadBackend — the Rust read path through the memory_storage crate
|
||||
// (read-only by construction). The Go crate reaches libmemory_storage only
|
||||
// through the facade's FFI, so the reader is injectable; the battery (and
|
||||
// any future cgo membrane) supplies it.
|
||||
type RustReadBackend struct {
|
||||
Read func(dbPath, query string, params []interface{}) [][]interface{}
|
||||
}
|
||||
|
||||
func (RustReadBackend) Name() string { return "rust-memory-storage" }
|
||||
|
||||
func (b RustReadBackend) ReadQuery(dbPath, query string, params ...interface{}) [][]interface{} {
|
||||
return b.Read(dbPath, query, params)
|
||||
}
|
||||
|
||||
// The mems_loader.available() probe + backend factory seams (battery
|
||||
// injects both; nil = the loader is absent, which is the Go crate's
|
||||
// truthful default — the oracle's fallback-not-silence law).
|
||||
var (
|
||||
rustStorageProbe func() bool
|
||||
rustBackendFactory func() ReadBackend
|
||||
)
|
||||
|
||||
// StorageBackend — the active read backend: default sqlite;
|
||||
// RUNE_MEMORY_STORAGE=rust opts in when the probe admits it, falling back
|
||||
// loudly-free to sqlite otherwise.
|
||||
func StorageBackend() ReadBackend {
|
||||
if os.Getenv("RUNE_MEMORY_STORAGE") == "rust" &&
|
||||
rustStorageProbe != nil && rustStorageProbe() &&
|
||||
rustBackendFactory != nil {
|
||||
return rustBackendFactory()
|
||||
}
|
||||
return SQLiteBackend{}
|
||||
}
|
||||
|
||||
// TopicCount — one by_topic GROUP BY row; insertion order is part of the
|
||||
// contract (Python dict emission order — Go maps would sort it away).
|
||||
type TopicCount struct {
|
||||
Topic string
|
||||
N int64
|
||||
}
|
||||
|
||||
// StatsForPath — stats() through the storage backend seam: the opt-in
|
||||
// path-based consumer. Returns the result dict plus the top_topics rows in
|
||||
// emission order.
|
||||
func StatsForPath(backend ReadBackend, dbPath string) (map[string]interface{}, []TopicCount) {
|
||||
e := backend.ReadQuery(dbPath, "SELECT COUNT(*) FROM entries WHERE status='active'")[0][0]
|
||||
w := backend.ReadQuery(dbPath, "SELECT COUNT(*) FROM working")[0][0]
|
||||
a := backend.ReadQuery(dbPath, "SELECT COUNT(*) FROM audit_log")[0][0]
|
||||
rows := backend.ReadQuery(
|
||||
dbPath,
|
||||
"SELECT topic, COUNT(*) AS n FROM entries WHERE status='active' GROUP BY topic ORDER BY n DESC LIMIT 6",
|
||||
)
|
||||
topics := map[string]interface{}{}
|
||||
var order []TopicCount
|
||||
for _, r := range rows {
|
||||
topic, _ := r[0].(string)
|
||||
if topic == "" { // the oracle's `if r[0]` filters '' and NULL alike
|
||||
continue
|
||||
}
|
||||
var n int64
|
||||
switch v := r[1].(type) { // native sqlite scans int64; JSON-membrane rows float64
|
||||
case int64:
|
||||
n = v
|
||||
case float64:
|
||||
n = int64(v)
|
||||
}
|
||||
topics[topic] = n
|
||||
order = append(order, TopicCount{Topic: topic, N: n})
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"active_entries": e, "working": w, "audit": a, "top_topics": topics,
|
||||
}, order
|
||||
}
|
||||
|
||||
// ── reindex_entries ──────────────────────────────────────────────────────────
|
||||
|
||||
// EmbedFn — the _embed seam (facade-side network call). nil or a nil
|
||||
// return leaves the dense side off for that text.
|
||||
type EmbedFn func(text string) []float64
|
||||
|
||||
// pyJSONFloatList — Python json.dumps over a float slice: ", " separators
|
||||
// and Python float repr (integral floats keep the trailing .0 — the quirk
|
||||
// law; Go json.Marshal would emit "1" and ",").
|
||||
func pyJSONFloatList(xs []float64) string {
|
||||
parts := make([]string, len(xs))
|
||||
for i, x := range xs {
|
||||
if x == float64(int64(x)) && x > -1e15 && x < 1e15 {
|
||||
parts[i] = fmt.Sprintf("%.1f", x)
|
||||
} else {
|
||||
parts[i] = fmt.Sprintf("%g", x)
|
||||
}
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
}
|
||||
|
||||
func countQuery(db *sql.DB, query string) int {
|
||||
var n int
|
||||
if err := db.QueryRow(query).Scan(&n); err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// ReindexEntries — rebuild the vector + FTS indexes for all active entries:
|
||||
// dry-run reports the gap without writing; a missing/dangling/stale FTS
|
||||
// state triggers the full rebuild (vec clear, contentless FTS5 DROP +
|
||||
// recreate — DELETE cannot touch contentless tables), then reinsert of every
|
||||
// active entry. Mirrors the oracle's per-statement try/except tolerance:
|
||||
// every index-side statement failure counts and continues.
|
||||
func ReindexEntries(db *sql.DB, embed EmbedFn, dryRun bool) map[string]interface{} {
|
||||
total := countQuery(db, "SELECT COUNT(*) FROM entries WHERE status='active'")
|
||||
indexed := countQuery(db, "SELECT COUNT(*) FROM entries_vec")
|
||||
missing := total - indexed
|
||||
if missing < 0 {
|
||||
missing = 0
|
||||
}
|
||||
dangling := 0
|
||||
if err := db.QueryRow(
|
||||
"SELECT COUNT(*) AS c FROM entries_vec v LEFT JOIN entries e ON e.id = v.rowid WHERE e.id IS NULL",
|
||||
).Scan(&dangling); err != nil {
|
||||
dangling = 0
|
||||
}
|
||||
ftsDangling := 0
|
||||
if err := db.QueryRow(
|
||||
"SELECT COUNT(*) AS c FROM entries_fts f LEFT JOIN entries e ON e.id = f.rowid WHERE e.id IS NULL",
|
||||
).Scan(&ftsDangling); err != nil {
|
||||
ftsDangling = 0
|
||||
}
|
||||
ftsTotal := -1
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM entries_fts").Scan(&ftsTotal); err != nil {
|
||||
ftsTotal = -1
|
||||
}
|
||||
if dryRun {
|
||||
return map[string]interface{}{
|
||||
"indexed": indexed, "total": total, "missing": missing,
|
||||
"dangling": dangling, "fts_dangling": ftsDangling, "fts_total": ftsTotal,
|
||||
"dry_run": true,
|
||||
}
|
||||
}
|
||||
ftsStale := ftsTotal >= 0 && ftsTotal != total
|
||||
if missing > 0 || dangling > 0 || ftsDangling > 0 || ftsStale {
|
||||
db.Exec("DELETE FROM entries_vec")
|
||||
db.Exec("DELETE FROM entries_fts") // contentless: raises, swallowed (oracle try/except)
|
||||
if _, err := db.Exec("DROP TABLE IF EXISTS entries_fts"); err == nil {
|
||||
db.Exec("CREATE VIRTUAL TABLE entries_fts USING fts5(text, content='', tokenize='porter unicode61')")
|
||||
}
|
||||
rows, err := db.Query("SELECT id, text FROM entries WHERE status='active'")
|
||||
type idText struct {
|
||||
id int64
|
||||
text string
|
||||
}
|
||||
var list []idText
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var r idText
|
||||
rows.Scan(&r.id, &r.text)
|
||||
list = append(list, r)
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
vecMissing, ftsMissing := 0, 0
|
||||
for _, r := range list {
|
||||
var vec []float64
|
||||
if embed != nil {
|
||||
vec = embed(r.text)
|
||||
}
|
||||
if vec != nil {
|
||||
if _, err := db.Exec("INSERT INTO entries_vec (rowid, embedding) VALUES (?,?)",
|
||||
r.id, pyJSONFloatList(vec)); err != nil {
|
||||
vecMissing++
|
||||
}
|
||||
} else {
|
||||
vecMissing++
|
||||
}
|
||||
if _, err := db.Exec("INSERT INTO entries_fts (rowid, text) VALUES (?,?)", r.id, r.text); err != nil {
|
||||
ftsMissing++
|
||||
}
|
||||
}
|
||||
db.Exec("COMMIT")
|
||||
return map[string]interface{}{
|
||||
"indexed": len(list), "total": total, "missing": missing,
|
||||
"dangling": dangling, "fts_dangling": ftsDangling,
|
||||
"vec_missing": vecMissing, "fts_missing": ftsMissing,
|
||||
"dry_run": false, "rebuilt": true,
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"indexed": indexed, "total": total, "missing": 0,
|
||||
"dangling": 0, "fts_dangling": 0,
|
||||
"dry_run": false, "rebuilt": false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package main
|
||||
|
||||
// gaps_stats_test.go — memory_store STATS family parity gate: replays
|
||||
// truth_stats_gaps.json (live oracle capture under the generator's rigs —
|
||||
// builder factory-heavy-ms-stats). Canonical-JSON comparison (canonEq);
|
||||
// GAPS_TRUTH_PATH overrides the truth file for live-parity runs against
|
||||
// freshly regenerated truth. Fixture builders mirror the generator's DDLs
|
||||
// exactly (same shape both sides of the membrane).
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func execDDL(t *testing.T, db *sql.DB, stmts ...string) {
|
||||
t.Helper()
|
||||
for _, s := range stmts {
|
||||
if _, err := db.Exec(s); err != nil {
|
||||
t.Fatalf("ddl: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const statsDDL = `
|
||||
CREATE TABLE entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
topic TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'active'
|
||||
)`
|
||||
const statsWorkingDDL = `
|
||||
CREATE TABLE working (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
)`
|
||||
const statsAuditDDL = `
|
||||
CREATE TABLE audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
prev_hash TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
claim TEXT,
|
||||
verdict TEXT,
|
||||
evidence TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)`
|
||||
|
||||
const reindexEntriesDDL = `
|
||||
CREATE TABLE entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'active'
|
||||
)`
|
||||
const reindexVecDDL = `CREATE TABLE entries_vec (rowid INTEGER PRIMARY KEY, embedding TEXT)`
|
||||
const reindexFtsDDL = `CREATE VIRTUAL TABLE entries_fts USING fts5(text, content='', tokenize='porter unicode61')`
|
||||
|
||||
// buildStatsStore — the stats_for_path fixture on a real FILE (the backend
|
||||
// opens one connection per read, mirroring the oracle's sqlite3.connect).
|
||||
func buildStatsStore(t *testing.T, entries [][]interface{}, working, audit int) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "store.db")
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
t.Fatalf("open store: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
execDDL(t, db, statsDDL, statsWorkingDDL, statsAuditDDL)
|
||||
for _, e := range entries {
|
||||
db.Exec("INSERT INTO entries (id, text, topic, status) VALUES (?,?,?,?)", e[0], e[1], e[2], e[3])
|
||||
}
|
||||
for i := 1; i <= working; i++ {
|
||||
db.Exec("INSERT INTO working (id, task_id, text, created_at) VALUES (?,?,?,?)",
|
||||
i, fmt.Sprintf("t%d", i), fmt.Sprintf("working note %d", i), "2026-01-01T00:00:00+00:00")
|
||||
}
|
||||
for i := 1; i <= audit; i++ {
|
||||
db.Exec("INSERT INTO audit_log (id, prev_hash, hash, claim, verdict, evidence, created_at) VALUES (?,?,?,?,?,?,?)",
|
||||
i, "GENESIS", "hhhhhhhhhhhhhhhhhhhhhhhh", fmt.Sprintf("claim %d", i), "pass", "", "2026-01-01T00:00:00+00:00")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// buildReindexStore — the reindex fixture in-memory (same DDL as the rig).
|
||||
func buildReindexStore(t *testing.T, entries, vecRows, ftsRows [][]interface{}) *sql.DB {
|
||||
t.Helper()
|
||||
db, err := sql.Open("sqlite", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("open reindex store: %v", err)
|
||||
}
|
||||
execDDL(t, db, reindexEntriesDDL, reindexVecDDL, reindexFtsDDL)
|
||||
for _, e := range entries {
|
||||
db.Exec("INSERT INTO entries (id, text, status) VALUES (?,?,?)", e[0], e[1], e[2])
|
||||
}
|
||||
for _, v := range vecRows {
|
||||
db.Exec("INSERT INTO entries_vec (rowid, embedding) VALUES (?,?)", v[0], v[1])
|
||||
}
|
||||
for _, f := range ftsRows {
|
||||
db.Exec("INSERT INTO entries_fts (rowid, text) VALUES (?,?)", f[0], f[1])
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func reindexPostState(t *testing.T, db *sql.DB) map[string]interface{} {
|
||||
t.Helper()
|
||||
post := map[string]interface{}{}
|
||||
fts := []interface{}{}
|
||||
rows, err := db.Query("SELECT rowid, text FROM entries_fts ORDER BY rowid")
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var rid int64
|
||||
var text sql.NullString
|
||||
rows.Scan(&rid, &text)
|
||||
var tv interface{}
|
||||
if text.Valid {
|
||||
tv = text.String
|
||||
}
|
||||
fts = append(fts, []interface{}{rid, tv})
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
vec := []interface{}{}
|
||||
rows, err = db.Query("SELECT rowid, embedding FROM entries_vec ORDER BY rowid")
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var rid int64
|
||||
var emb sql.NullString
|
||||
rows.Scan(&rid, &emb)
|
||||
var ev interface{}
|
||||
if emb.Valid {
|
||||
ev = emb.String
|
||||
}
|
||||
vec = append(vec, []interface{}{rid, ev})
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
post["fts"] = fts
|
||||
post["vec"] = vec
|
||||
return post
|
||||
}
|
||||
|
||||
func TestStatsGapsParity(t *testing.T) {
|
||||
path := "truth_stats_gaps.json"
|
||||
if p := os.Getenv("GAPS_TRUTH_PATH"); p != "" {
|
||||
path = p
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("truth file missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
ran := 0
|
||||
// restore the backend seams after the battery (each case re-injects before use)
|
||||
defer func() { rustStorageProbe = nil; rustBackendFactory = nil }()
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Fn {
|
||||
case "handle_hook":
|
||||
var data interface{}
|
||||
if v, ok := c.Input["data"]; ok {
|
||||
data = v
|
||||
}
|
||||
got := HandleHook(str(c.Input["event"]), data)
|
||||
canonEq(t, c.Name, "envelope", got, c.Expected)
|
||||
case "wake_up":
|
||||
var rows []WakeRow
|
||||
for _, r := range objList(c.Input["rows"]) {
|
||||
rows = append(rows, WakeRow{
|
||||
Text: str(r["text"]),
|
||||
Topic: str(r["topic"]),
|
||||
Importance: num(r["importance"]),
|
||||
Source: str(r["source"]),
|
||||
})
|
||||
}
|
||||
got := WakeUp(rows, int(num(c.Input["budget_chars"])))
|
||||
canonEq(t, c.Name, "context", got, c.Expected)
|
||||
case "storage_backend":
|
||||
envVal := ""
|
||||
if v, ok := c.Input["env"]; ok && v != nil {
|
||||
envVal = str(v)
|
||||
}
|
||||
t.Setenv("RUNE_MEMORY_STORAGE", envVal)
|
||||
rustStorageProbe = func() bool {
|
||||
if v, ok := c.Input["loader_available"]; ok {
|
||||
return boolOf(v)
|
||||
}
|
||||
return false
|
||||
}
|
||||
rustBackendFactory = func() ReadBackend {
|
||||
return RustReadBackend{Read: func(dbPath, query string, params []interface{}) [][]interface{} {
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
got := StorageBackend().Name()
|
||||
canonEq(t, c.Name, "name", map[string]interface{}{"name": got}, c.Expected)
|
||||
case "stats_for_path":
|
||||
exp, _ := c.Expected.(map[string]interface{})
|
||||
backendKind := str(c.Input["backend"])
|
||||
var backend ReadBackend
|
||||
var traffic []map[string]interface{}
|
||||
if backendKind == "rust" {
|
||||
scripted := listy(c.Input["scripted"])
|
||||
t.Setenv("RUNE_MEMORY_STORAGE", "rust")
|
||||
rustStorageProbe = func() bool { return true }
|
||||
rustBackendFactory = func() ReadBackend {
|
||||
return RustReadBackend{Read: func(dbPath, query string, params []interface{}) [][]interface{} {
|
||||
call := map[string]interface{}{"sql": query}
|
||||
pl := []interface{}{}
|
||||
for _, p := range params {
|
||||
pl = append(pl, p)
|
||||
}
|
||||
call["params"] = pl
|
||||
traffic = append(traffic, call)
|
||||
var rows [][]interface{}
|
||||
if len(traffic) <= len(scripted) {
|
||||
for _, r := range listy(scripted[len(traffic)-1]) {
|
||||
l := listy(r)
|
||||
row := make([]interface{}, len(l))
|
||||
for i, v := range l {
|
||||
row[i] = v
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}}
|
||||
}
|
||||
backend = StorageBackend()
|
||||
} else {
|
||||
entries := listy(c.Input["entries"])
|
||||
ent := make([][]interface{}, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
l := listy(e)
|
||||
row := make([]interface{}, len(l))
|
||||
for i, v := range l {
|
||||
row[i] = v
|
||||
}
|
||||
ent = append(ent, row)
|
||||
}
|
||||
store := buildStatsStore(t, ent, int(num(c.Input["working"])), int(num(c.Input["audit"])))
|
||||
got, order := StatsForPath(SQLiteBackend{}, store)
|
||||
canonEq(t, c.Name, "stats", got, exp["stats"])
|
||||
checkTopicOrder(t, c.Name, order, exp["top_topics_order"])
|
||||
ran++
|
||||
continue
|
||||
}
|
||||
got, order := StatsForPath(backend, "scripted.db")
|
||||
canonEq(t, c.Name, "stats", got, exp["stats"])
|
||||
checkTopicOrder(t, c.Name, order, exp["top_topics_order"])
|
||||
canonEq(t, c.Name, "queries", traffic, listy(exp["queries"]))
|
||||
case "reindex_entries":
|
||||
toRows := func(v interface{}) [][]interface{} {
|
||||
var out [][]interface{}
|
||||
for _, r := range listy(v) {
|
||||
l := listy(r)
|
||||
row := make([]interface{}, len(l))
|
||||
for i, x := range l {
|
||||
row[i] = x
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
return out
|
||||
}
|
||||
db := buildReindexStore(t, toRows(c.Input["entries"]), toRows(c.Input["vec_rows"]), toRows(c.Input["fts_rows"]))
|
||||
var embedFn EmbedFn
|
||||
if m, ok := c.Input["embed"].(map[string]interface{}); ok && m != nil {
|
||||
embedFn = func(text string) []float64 {
|
||||
raw, ok := m[text]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var vec []float64
|
||||
for _, x := range listy(raw) {
|
||||
vec = append(vec, num(x))
|
||||
}
|
||||
return vec
|
||||
}
|
||||
}
|
||||
got := ReindexEntries(db, embedFn, boolOf(c.Input["dry_run"]))
|
||||
exp, _ := c.Expected.(map[string]interface{})
|
||||
canonEq(t, c.Name, "result", got, exp["result"])
|
||||
canonEq(t, c.Name, "post", reindexPostState(t, db), exp["post"])
|
||||
db.Close()
|
||||
default:
|
||||
t.Errorf("unknown fn kind: %s", c.Fn)
|
||||
}
|
||||
ran++
|
||||
}
|
||||
if ran == 0 {
|
||||
t.Fatalf("no cases ran")
|
||||
}
|
||||
t.Logf("%d cases replayed", ran)
|
||||
}
|
||||
|
||||
func checkTopicOrder(t *testing.T, name string, got []TopicCount, want interface{}) {
|
||||
t.Helper()
|
||||
gotPairs := []interface{}{}
|
||||
for _, tc := range got {
|
||||
gotPairs = append(gotPairs, []interface{}{tc.Topic, tc.N})
|
||||
}
|
||||
canonEq(t, name, "top_topics_order", gotPairs, listy(want))
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,9 @@
|
||||
FAILED tests/test_rust_core_shims.py::test_capability_shims_delegation_agrees_with_fallback
|
||||
FAILED tests/test_rust_core_shims.py::test_construction_decision_placement_delegation
|
||||
FAILED tests/test_rust_core_shims.py::test_drift_shims_delegation_agrees_with_fallback
|
||||
FAILED tests/test_rust_core_shims.py::test_memory_store_helpers_delegate_to_kernel
|
||||
FAILED tests/test_rust_core_shims.py::test_mr_loader_chunk_sql_gather_through_seam
|
||||
FAILED tests/test_rust_core_shims.py::test_spine_delegation_agrees_with_fallback
|
||||
FAILED tests/test_rust_core_shims.py::test_token_optimizer_class_surface - Ru...
|
||||
FAILED tests/test_rust_core_shims.py::test_token_optimizer_delegation_agrees_with_fallback
|
||||
FAILED tests/test_rust_core_shims.py::test_warm_base_level_math_and_gather - ...
|
||||
@@ -0,0 +1 @@
|
||||
exit=0 (22 passed, 9 failed, 25 skipped — failure set byte-identical across both pins and to HEAD by construction: no tracked Python/tests files touched)
|
||||
@@ -0,0 +1,9 @@
|
||||
FAILED tests/test_rust_core_shims.py::test_capability_shims_delegation_agrees_with_fallback
|
||||
FAILED tests/test_rust_core_shims.py::test_construction_decision_placement_delegation
|
||||
FAILED tests/test_rust_core_shims.py::test_drift_shims_delegation_agrees_with_fallback
|
||||
FAILED tests/test_rust_core_shims.py::test_memory_store_helpers_delegate_to_kernel
|
||||
FAILED tests/test_rust_core_shims.py::test_mr_loader_chunk_sql_gather_through_seam
|
||||
FAILED tests/test_rust_core_shims.py::test_spine_delegation_agrees_with_fallback
|
||||
FAILED tests/test_rust_core_shims.py::test_token_optimizer_class_surface - Ru...
|
||||
FAILED tests/test_rust_core_shims.py::test_token_optimizer_delegation_agrees_with_fallback
|
||||
FAILED tests/test_rust_core_shims.py::test_warm_base_level_math_and_gather - ...
|
||||
@@ -0,0 +1 @@
|
||||
exit=0 (RUNE_RETRIEVAL_CORE=python pin: 22 passed, 9 failed, 25 skipped — set identical to live pin, see msstats-py-*-baseline.txt)
|
||||
@@ -0,0 +1,108 @@
|
||||
# memory_store STATS family gap-close — verification summary
|
||||
|
||||
- **Module**: memory_store (audit alias: memory-ops + memory-store-deep GO corpus)
|
||||
- **Family**: working-set/stats/audit — builder `factory-heavy-ms-stats`
|
||||
- **Worktree**: /mnt/workdrive/rune-wave-en (isolated, tracks gitea/master)
|
||||
- **Date**: 2026-10-15
|
||||
|
||||
## What was ported (name-preserving, src/go/memory-store-deep/gaps_stats.go — NEW file, siblings untouched)
|
||||
|
||||
The 5 CURRENTLY-UNMATCHED fns of this lane's slice (audit re-run first, per
|
||||
law; the other 12 slice fns — working_set/get/clear, audit_append/verify,
|
||||
stats, touch, memory_health_check, tier_demotion_sweep, _map_reduce, main —
|
||||
were already landed by earlier waves in main.go/gaps_retrieval.go):
|
||||
|
||||
- `reindex_entries` → `ReindexEntries`: gap census (total/indexed/missing,
|
||||
vec+fts dangling via LEFT JOIN, fts staleness), dry-run report, full
|
||||
rebuild (vec clear; contentless-FTS5 DROP+recreate — DELETE cannot touch
|
||||
contentless tables, error swallowed mirror), per-row reinsert with
|
||||
vec_missing/fts_missing counting, exact result-dict shapes for all three
|
||||
branches.
|
||||
- `wake_up` → `WakeUp`: pre-fetched rows → "- text" lines under a
|
||||
CHAR-budget (rune count, never bytes; pinned by `wu_chars_not_bytes` —
|
||||
17-char greek line fits budget 20 where 32 UTF-8 bytes would not).
|
||||
- `storage_backend` → `StorageBackend` + `SQLiteBackend`/`RustReadBackend`:
|
||||
the R2 W2.0 read-backend seam. Default sqlite; RUNE_MEMORY_STORAGE=rust
|
||||
opts in when the probe seam admits it (fallback loudly-free to sqlite).
|
||||
The Go crate reaches libmemory_storage only via the facade FFI, so the
|
||||
probe + reader are injectable seams (mems_loader is Python FFI machinery).
|
||||
- `stats_for_path` → `StatsForPath`: stats() through the backend seam; real
|
||||
file reads through SQLiteBackend (one connection per read, mirroring the
|
||||
oracle), by_topic rows in EMISSION order (`top_topics` insertion order is
|
||||
contract — Go maps would sort it away; order rides a parallel
|
||||
TopicCount list), '' and NULL topics filtered (the `if r[0]` law).
|
||||
- `handle_hook` → `HandleHook`: the default hook envelope.
|
||||
|
||||
Seam decisions (documented, not bugs): the vec side of reindex runs on the
|
||||
plain-table entries_vec mirror — production vec0 is observed only through
|
||||
COUNT/LEFT JOIN/DELETE/INSERT, which a plain table serves identically (Go
|
||||
has no vec0; the rig uses the same mirror, parity by construction).
|
||||
|
||||
## Gates (all exit 0; logs in this directory)
|
||||
|
||||
| Gate | Evidence |
|
||||
|---|---|
|
||||
| gofmt -l (clean) | msstats-gofmt.log |
|
||||
| go vet ./... | msstats-vet.log |
|
||||
| go build ./... | msstats-build.log |
|
||||
| go test ./... (whole crate, incl. prior batteries) | msstats-gotest.log |
|
||||
| frozen truth byte-identical, run 1 | msstats-freeze-verify-1.log |
|
||||
| frozen truth byte-identical, run 2 | msstats-freeze-verify-2.log |
|
||||
| live parity (fresh oracle capture → Go battery) | msstats-parity.log |
|
||||
| ruff (both scripts) | msstats-ruff.log |
|
||||
| Python module suite, live pin | msstats-py-live.exit |
|
||||
| Python module suite, RUNE_RETRIEVAL_CORE=python | msstats-py-pin.exit |
|
||||
| failure-set identity, both pins | msstats-py-{live,pin}-baseline.txt (9=9, byte-identical) |
|
||||
|
||||
Truth: `src/go/memory-store-deep/truth_stats_gaps.json` — 24 cases.
|
||||
All cases run the live oracle under pinned rigs (HOME pinned before import,
|
||||
frozen clock on memory_store.datetime, _embed pinned off or scripted
|
||||
deterministic vectors, fake mems_loader via sys.modules injection,
|
||||
RUNE_MEMORY_STORAGE juggling with restore). No wall-clock, no absolute
|
||||
paths, no HOME-derived values in any truth row. Generator:
|
||||
scripts/gen_memory_store_stats_truth.py (--check / --out) — byte-identical
|
||||
across regenerations. Battery gaps_stats_test.go replays canonically
|
||||
(canonEq) with GAPS_TRUTH_PATH override; parity:
|
||||
scripts/check_memory_store_stats_parity.py (fresh live-oracle capture →
|
||||
diff vs frozen → Go battery on fresh truth).
|
||||
|
||||
Python both-pins: tests/test_memory_port.py + test_memory_types_port.py +
|
||||
test_rust_core_shims.py — 22 passed / 9 failed / 25 skipped in BOTH pins,
|
||||
failure sets byte-identical (see baseline files). The 9 are pre-existing
|
||||
delegation/seam tests (mr_loader/capability/drift/spine/token_optimizer
|
||||
.sho dependencies), untouched by this port — the worktree modifies no
|
||||
tracked Python or tests files (git status: docs/ audit doc + new
|
||||
scripts/go files only), so HEAD-equivalence is by construction.
|
||||
|
||||
## Parity findings (documented divergences, not bugs)
|
||||
|
||||
1. **Contentless FTS5 reads back NULL text**: `SELECT rowid, text FROM
|
||||
entries_fts` returns NULL for text (content='' stores nothing — the
|
||||
oracle inserts text on rebuild, reads still see NULL). Both engines
|
||||
agree (cpython + modernc); the post-state truth rows pin `[[id, null]]`.
|
||||
2. **Python json.dumps float-list formatting in the vec corpus**:
|
||||
"[0.5, 0.25]" (", " separators) and integral floats keep ".0"
|
||||
("[1.0, 0.5]") — Go json.Marshal would emit "," and "1". Mirrored by
|
||||
pyJSONFloatList (quirk law), pinned by `ri_embed_on` post state.
|
||||
3. **stats_for_path by_topic tie order is engine-shaped**: GROUP BY topic
|
||||
ORDER BY n DESC leaves count-ties to the engine's sorter. Fixtures use
|
||||
distinct counts only (tie behavior is not contract; both engines run
|
||||
real SQLite here anyway).
|
||||
4. **read_query ×2 counters flipped by this port's interface**: the audit
|
||||
now matches Python `read_query` (both backend-class methods — the dm
|
||||
lane's slice) against Go `ReadQuery`, the seam interface method name
|
||||
this port introduces. NOT a dm-lane port; dm still owns Python
|
||||
read_query's real parity surface. Reported as a name-collision flip.
|
||||
|
||||
## Audit delta
|
||||
|
||||
memory_store: 44/68 → **51/68** (75%; ≥80% FULL needs 55). This lane's
|
||||
slice (working_set/get/clear, audit_append/verify, stats, storage_backend,
|
||||
stats_for_path, touch, reindex_entries, memory_health_check,
|
||||
tier_demotion_sweep, _map_reduce, wake_up, handle_hook, main) is now fully
|
||||
matched. Remaining missing fns belong to the IO/schema (dm) and mining
|
||||
(em) lanes: _ctx_budget, now_iso, ensure_memory_type_column, _temp_json,
|
||||
_entities_to_json, _aaak_summary, _auto_associate, associate,
|
||||
_run_lexicon_reconcile, _extract_epub, add_chunk, hierarchy, list_scenes,
|
||||
make_scene, mine_directory, mine_file, scene_entries. Module NOT FULL →
|
||||
worktree KEPT per spawn rule.
|
||||
Reference in new issue
Block a user