feat(graph_memory): close PARTIAL gaps to FULL Rust — dichotomy wave
Kernel port of the 6 audit-gap fns (now_iso, connect, resolve_entity,
add_edge, query, stats) + handle_hook into src/rust/graph_memory (was a
W6 stub): the write-time gating (gm_strength_to_conf — exact float
semantics incl. the 3.5->0.8200000000000001 artifact, unlike the Go
twin's 2-decimal rounding), pred vocabulary gate (gm_normalize_pred),
node-type fallback (gm_node_type), duplicate-vs-supersede decision
(gm_supersede), 400-char evidence cap (gm_trim_evidence), query-fact
projection (gm_query_fact), stats top-8 projection (gm_stats), hook
contract (gm_handle_hook). SQLite IO stays Python (every oracle fn
takes a live db handle); the facade (graph_memory.py) delegates the
decisions through gm_loader with RUNE_GRAPH_MEMORY_CORE=python pin
honor; the probe is _core (machinery-excluded).
Truth: frozen_truth.json 38 cases (scripts/gen_graph_memory_truth.py
under scripts/gm_freeze_seams — wiped-per-process hermetic store,
RUNE_MEMORY_DIR pin, envelope clock) — byte-identical across
generation and two --verify runs. Rust battery 2/2 with the truth
embedded via include_str! (scripted store mirrors the SQL semantics
row for row: OR-IGNORE aliases, sqlite_sequence in the connect shape).
Live parity 1330/1330 (15 frozen FFI rows + 1310 fuzz checks over 60
seeded op-sequences dual-mode python-pin vs live core + 5 live
supersession-chain checks). clippy --all-targets -D warnings 0.
Python suite both pins: test_graph_health+test_graph_projection
7 passed + 1 pre-existing worktree-only failure (store cache
current.json missing — verified identical on stashed base);
test_p1_03 baseline 23 pre-existing failures identical on stashed
base (zero regressions). ruff clean.
Audit: graph_memory 2/9 PARTIAL -> 9/9 FULL (RUST); headline
FULL 43% (83/192), fn 64% (1447/2278).
Evidence: verification/gm_*.{log,exit} (all exit 0 except the two
pre-existing pytest baselines).
This commit is contained in:
1 parent
c441a536bb
commit
16b5569411
19 files changed
+2076
-74
No files matched your search
@@ -2,16 +2,16 @@
|
||||
|
||||
_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._
|
||||
|
||||
**FULL completion: 47%** (91/192 runtime modules at FULL)
|
||||
**FULL completion: 48%** (92/192 runtime modules at FULL)
|
||||
|
||||
Function-level detail: 67% (1522/2279 runtime functions matched) — secondary; includes in-flight work
|
||||
Function-level detail: 67% (1529/2279 runtime functions matched) — secondary; includes in-flight work
|
||||
|
||||
| Status | Modules | Lines | Meaning |
|
||||
|---|---|---|---|
|
||||
| FULL | 91 | 38259 | 80%+ of functions ported |
|
||||
| PARTIAL | 61 | 24434 | 20–80% — real logic exists, gaps named below |
|
||||
| FULL | 92 | 38822 | 80%+ of functions ported |
|
||||
| PARTIAL | 60 | 23960 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 40 | 11513 | no name-preserving implementation |
|
||||
| Total runtime | 192 | 74206 | |
|
||||
| Total runtime | 192 | 74295 | |
|
||||
|
||||
## NOT TOUCHED — the port queue, in priority order (largest first)
|
||||
|
||||
@@ -74,7 +74,6 @@ Function-level detail: 67% (1522/2279 runtime functions matched) — secondary;
|
||||
| builder_bridge | 326 | 3/11 | _construction_store, _active_construction_lines, _default_selector, _cached_memory, _get_selector, _retrieve_memory |
|
||||
| drift_core | 471 | 3/11 | _ms_core, _now_iso, promote_entry, compute_fama, compute_drift_velocity, compute_drift_direction |
|
||||
| time_awareness | 402 | 12/20 | _clock_args, _ta_core, get_time_awareness, to_dict, _to_state, _apply_state |
|
||||
| graph_memory | 474 | 2/9 | now_iso, connect, resolve_entity, add_edge, query, stats |
|
||||
| intent_continuity | 284 | 6/13 | _now, _raise_core_err, to_dict, from_dict, save, _ledger_state |
|
||||
| voice_hybrid | 427 | 3/10 | _connect, _load_growth, _save_growth, source_profiles, always, grow |
|
||||
| voice_sync | 299 | 3/10 | _now, get_delivery_register_from_db, get_delivery_register, update_rune_md, check_sync, handle_hook |
|
||||
@@ -152,6 +151,7 @@ Function-level detail: 67% (1522/2279 runtime functions matched) — secondary;
|
||||
| construction_intent | 580 | 13/16 | RUST |
|
||||
| identity_anchors | 575 | 14/14 | RUST |
|
||||
| capability_ledger | 571 | 18/18 | RUST |
|
||||
| graph_memory | 563 | 9/9 | RUST |
|
||||
| drift_ledger | 548 | 20/21 | RUST |
|
||||
| construction_pattern | 547 | 13/13 | RUST |
|
||||
| failure_auto_filer | 527 | 24/24 | GO |
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live parity: graph_memory Rust kernels vs Python oracle (dichotomy wave,
|
||||
gap-close pass). Layers:
|
||||
|
||||
1. FROZEN — the gap truth corpus (src/rust/graph_memory/frozen_truth.json,
|
||||
generated by scripts/gen_graph_memory_truth.py under the
|
||||
scripts/gm_freeze_seams rig against the LIVE oracle): the kernel-owned
|
||||
rows (now_iso, strength_to_conf) replayed through the ctypes FFI,
|
||||
case by case, byte-exact.
|
||||
2. FUZZ — 60 seeded random op-sequences (seed 2479): the full facade
|
||||
(add_edge/resolve_entity/query/stats with their kernel decisions)
|
||||
executed with RUNE_GRAPH_MEMORY_CORE=python (oracle floor) and with
|
||||
the Rust core live, under a pinned clock; per-op results, final
|
||||
stats, and a query sweep must be identical.
|
||||
3. LIVE — dual-mode determinism: the same facade calls on the same
|
||||
hermetic store both pins; supersession chains preserved.
|
||||
|
||||
Exit 0 only at 100% parity on the gap-closing functions.
|
||||
"""
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime as _dt
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
os.environ["RUNE_GRAPH_MEMORY_CORE"] = "python" # oracle floor to start
|
||||
SCRATCH = "/tmp/gm_parity"
|
||||
os.environ["RUNE_MEMORY_DIR"] = SCRATCH
|
||||
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
import rune.gm_loader as gl # noqa: E402
|
||||
import rune.graph_memory as gm # noqa: E402
|
||||
|
||||
if not gl.available():
|
||||
sys.exit("FATAL: Rust core not built — cargo build --release in src/rust/graph_memory")
|
||||
|
||||
FROZEN_NOW = "2026-10-13T12:00:00"
|
||||
|
||||
fails: list[str] = []
|
||||
counts = {"frozen": 0, "fuzz": 0, "live": 0}
|
||||
|
||||
|
||||
def fail(layer, name, detail):
|
||||
fails.append(f"[{layer}] {name}: {detail}")
|
||||
print(f"FAIL[{layer}] {name}: {detail}"[:400])
|
||||
|
||||
|
||||
def j(x):
|
||||
return json.dumps(x, sort_keys=True, ensure_ascii=True)
|
||||
|
||||
|
||||
def check(layer, name, want, got):
|
||||
counts[layer] += 1
|
||||
if j(want) != j(got):
|
||||
fail(layer, name, f"want={j(want)[:300]} got={j(got)[:300]}")
|
||||
|
||||
|
||||
def pin(mode):
|
||||
if mode == "python":
|
||||
os.environ["RUNE_GRAPH_MEMORY_CORE"] = "python"
|
||||
else:
|
||||
os.environ.pop("RUNE_GRAPH_MEMORY_CORE", None)
|
||||
|
||||
|
||||
def frozen_clock():
|
||||
base = _dt.fromisoformat(FROZEN_NOW)
|
||||
|
||||
class FrozenDatetime(_dt):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
if tz is not None:
|
||||
return base.replace(tzinfo=tz)
|
||||
return base
|
||||
|
||||
gm.datetime = FrozenDatetime
|
||||
|
||||
|
||||
def fresh_db():
|
||||
shutil.rmtree(os.path.join(SCRATCH, "graph"), ignore_errors=True)
|
||||
return gm.connect()
|
||||
|
||||
|
||||
TRUTH = json.loads(
|
||||
(REPO / "src" / "rust" / "graph_memory" / "frozen_truth.json").read_text())
|
||||
CASES = TRUTH["cases"] if isinstance(TRUTH, dict) else TRUTH
|
||||
|
||||
# ── layer 1: FROZEN (kernel-owned rows through the FFI) ────────────────────
|
||||
|
||||
for row in CASES:
|
||||
fn = row["fn"]
|
||||
if fn == "now_iso":
|
||||
check("frozen", "ffi_now_iso", row["expected"],
|
||||
gl.now_iso("2026-10-13T12:00:00.123456+00:00"))
|
||||
elif fn == "strength_to_conf":
|
||||
(sv,) = row["args"]
|
||||
res = gl.strength_to_conf(sv)
|
||||
got = res["conf"] if res["ok"] else None
|
||||
check("frozen", f"ffi_strength_to_conf:{sv!r}", row["expected"], got)
|
||||
|
||||
# ── layers 2+3: FUZZ + LIVE (dual-mode facade on random op-sequences) ───────
|
||||
|
||||
NAMES = ["Ada Lovelace", "Babbage", "Analytical Engine", "Difference Engine",
|
||||
" punched card ", "Nightflyer", "", "babbage"]
|
||||
PREDS = ["builds", "knows", "Works On", "frobnicate", "uses", "LIKES", "part_of"]
|
||||
TYPES = ["person", "project", "alien", "tool", ""]
|
||||
EVID = ["first notes", "revised notes", " spaced ", "x" * 500, ""]
|
||||
|
||||
rng = random.Random(2479)
|
||||
|
||||
def run_seq(seq, mode):
|
||||
pin(mode)
|
||||
importlib.reload(gm)
|
||||
frozen_clock()
|
||||
db = fresh_db()
|
||||
out = []
|
||||
for op in seq:
|
||||
kind = op[0]
|
||||
if kind == "add":
|
||||
_, subj, pred, obj, kw = op
|
||||
out.append(gm.add_edge(db, subj, pred, obj, **kw))
|
||||
elif kind == "resolve":
|
||||
_, name, nt = op
|
||||
out.append(gm.resolve_entity(db, name, nt))
|
||||
elif kind == "query":
|
||||
_, term, hops = op
|
||||
out.append(gm.query(db, term, hops=hops))
|
||||
elif kind == "stats":
|
||||
out.append(gm.stats(db))
|
||||
sweep = [gm.query(db, t, hops=h) for t in ("Ada Lovelace", "babbage",
|
||||
"Engine", "zzz") for h in (0, 1, 2)]
|
||||
out.append(gm.stats(db))
|
||||
db.close()
|
||||
return out, sweep
|
||||
|
||||
for trial in range(60):
|
||||
seq = []
|
||||
for _ in range(rng.randrange(4, 14)):
|
||||
r = rng.random()
|
||||
if r < 0.55:
|
||||
kw = {}
|
||||
if rng.random() < 0.5:
|
||||
kw["confidence"] = rng.choice([None, 0.5, 0.9, 0.99, 0.3])
|
||||
if rng.random() < 0.6 or "confidence" not in kw:
|
||||
kw["strength"] = rng.choice([None, 2, 3, 4, 5, "4", 3.5, "x"])
|
||||
if rng.random() < 0.7:
|
||||
kw["evidence"] = rng.choice(EVID)
|
||||
if rng.random() < 0.4:
|
||||
kw["source"] = rng.choice(["truth", "curator", ""])
|
||||
if rng.random() < 0.3:
|
||||
kw["subj_type"] = rng.choice(TYPES)
|
||||
if rng.random() < 0.3:
|
||||
kw["obj_type"] = rng.choice(TYPES)
|
||||
seq.append(("add", rng.choice(NAMES), rng.choice(PREDS),
|
||||
rng.choice(NAMES), kw))
|
||||
elif r < 0.7:
|
||||
seq.append(("resolve", rng.choice(NAMES), rng.choice(TYPES)))
|
||||
elif r < 0.9:
|
||||
seq.append(("query", rng.choice(NAMES + ["Engine", "ada lovelace"]),
|
||||
rng.choice([0, 1, 2, -1])))
|
||||
else:
|
||||
seq.append(("stats",))
|
||||
want_out, want_sweep = run_seq(seq, "python")
|
||||
got_out, got_sweep = run_seq(seq, "rust")
|
||||
for i, (w, g) in enumerate(zip(want_out, got_out)):
|
||||
check("fuzz", f"trial{trial}:op{i}", w, g)
|
||||
for i, (w, g) in enumerate(zip(want_sweep, got_sweep)):
|
||||
check("fuzz", f"trial{trial}:sweep{i}", w, g)
|
||||
if fails:
|
||||
break
|
||||
|
||||
# ── layer 3: LIVE — pinned-clock determinism + supersession chains ─────────
|
||||
|
||||
def chain_probe(mode):
|
||||
pin(mode)
|
||||
importlib.reload(gm)
|
||||
frozen_clock()
|
||||
db = fresh_db()
|
||||
gm.add_edge(db, "Ada", "uses", "Engine", confidence=0.6, evidence="v1", source="t")
|
||||
gm.add_edge(db, "Ada", "uses", "Engine", confidence=0.7, evidence="v2", source="t")
|
||||
gm.add_edge(db, "Ada", "uses", "Engine", confidence=0.7, evidence="v2", source="t") # dup
|
||||
gm.add_edge(db, "Ada", "uses", "Engine", confidence=0.65, evidence="v2", source="t") # weaker dup
|
||||
gm.add_edge(db, "Ada", "uses", "Engine", confidence=0.65, evidence="v3", source="t") # weaker, NEW evidence -> supersede
|
||||
rows = [dict(r) for r in db.execute(
|
||||
"SELECT id, subj_id, pred, obj_id, confidence, evidence, status, superseded_by "
|
||||
"FROM edges ORDER BY id")]
|
||||
q = gm.query(db, "engine", hops=1)
|
||||
s = gm.stats(db)
|
||||
db.close()
|
||||
return rows, q, s
|
||||
|
||||
want_chain = chain_probe("python")
|
||||
got_chain = chain_probe("rust")
|
||||
for i in range(len(want_chain[0])):
|
||||
check("live", f"supersession_chain:edge{i}", want_chain[0][i], got_chain[0][i])
|
||||
check("live", "supersession_chain:query", want_chain[1], got_chain[1])
|
||||
check("live", "supersession_chain:stats", want_chain[2], got_chain[2])
|
||||
|
||||
print(f"parity: {sum(counts.values())} checks "
|
||||
f"(frozen {counts['frozen']}, fuzz {counts['fuzz']}, live {counts['live']}) "
|
||||
f"— {len(fails)} failures")
|
||||
sys.exit(1 if fails else 0)
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_graph_memory_truth.py — factory wave GAP truth for graph_memory.
|
||||
|
||||
Freezes the oracle behavior of the 6 audit-gap functions (dichotomy wave
|
||||
#2479: now_iso, connect, resolve_entity, add_edge, query, stats) through
|
||||
the SHARED harness (scripts/freeze_truth.py — one law).
|
||||
|
||||
Runs under the freeze rig (scripts/gm_freeze_seams/sitecustomize.py —
|
||||
PYTHONPATH-imported by BOTH this generator and --verify): RUNE_MEMORY_DIR
|
||||
pinned to a wiped-per-process hermetic scratch, the clock pinned via
|
||||
RUNE_FROZEN_NOW (the harness patches the oracle's module-global datetime),
|
||||
and the gm_* seams that drive the REAL db-carrying fns on the hermetic
|
||||
connection with JSON-safe shapes.
|
||||
|
||||
Run (from the repo root):
|
||||
PYTHONHASHSEED=0 RUNE_FROZEN_NOW=2026-10-13T12:00:00 \
|
||||
PYTHONPATH=scripts/gm_freeze_seams:src \
|
||||
python3 scripts/gen_graph_memory_truth.py
|
||||
|
||||
Verify (byte-identical, same rig — the envelope carries frozen_now):
|
||||
PYTHONHASHSEED=0 PYTHONPATH=scripts/gm_freeze_seams:src \
|
||||
python3 scripts/freeze_truth.py --verify \
|
||||
src/rust/graph_memory/frozen_truth.json \
|
||||
--module rune.graph_memory
|
||||
|
||||
Case map (every gap branch):
|
||||
* now_iso — seconds-precision shape (frozen clock; fractional dropped).
|
||||
* gm_connect — DDL shape (entities/aliases/edges tables exist).
|
||||
* strength_to_conf — direct kernel: below-floor (2, 2.9, 0, -1), floor
|
||||
(3->0.76), mid (4->0.88, 3.5 float artifact), ceiling (5->0.99,
|
||||
6/100 capped), unparseable (None, "x", "", object) -> None.
|
||||
* gm_resolve — create (person type kept), alias-hit (case-insensitive),
|
||||
unknown-type fallback (concept), empty/blank -> None.
|
||||
* gm_add_edge — unknown pred refused (False), gated strength (False),
|
||||
None-confidence+None-strength refused (False), happy path (True),
|
||||
duplicate re-add (True, no new row), stronger re-add (True,
|
||||
supersedes), self-loop refused (False), empty-subj refused (False),
|
||||
pred normalization ("Works On" -> works_on), evidence cap (400).
|
||||
* gm_query — exact hit (facts projected, timestamps "TS"), alias hit,
|
||||
substring fallback, miss -> [], empty -> [].
|
||||
* gm_stats — counts + top-8 predicates after the seeded writes.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
|
||||
from freeze_truth import freeze, write_envelope # noqa: E402
|
||||
|
||||
import rune.graph_memory as oracle # noqa: E402
|
||||
|
||||
FROZEN_NOW = "2026-10-13T12:00:00"
|
||||
|
||||
cases = [
|
||||
# now_iso: seconds-precision shape under the frozen clock.
|
||||
("now_iso", (), {}),
|
||||
# gm_connect: the DDL shape on the pinned dir.
|
||||
("gm_connect", (), {}),
|
||||
# strength_to_conf: the gating kernel, every branch.
|
||||
("strength_to_conf", (2,), {}),
|
||||
("strength_to_conf", (2.9,), {}),
|
||||
("strength_to_conf", (0,), {}),
|
||||
("strength_to_conf", (-1,), {}),
|
||||
("strength_to_conf", (3,), {}),
|
||||
("strength_to_conf", (4,), {}),
|
||||
("strength_to_conf", (5,), {}),
|
||||
("strength_to_conf", (3.5,), {}),
|
||||
("strength_to_conf", (6,), {}),
|
||||
("strength_to_conf", (100,), {}),
|
||||
("strength_to_conf", (None,), {}),
|
||||
("strength_to_conf", ("x",), {}),
|
||||
("strength_to_conf", ("",), {}),
|
||||
("strength_to_conf", ("4",), {}),
|
||||
# gm_resolve: create / alias-hit / fallback / empty.
|
||||
("gm_resolve", ("Ada Lovelace", "person"), {}),
|
||||
("gm_resolve", ("ada lovelace",), {}),
|
||||
("gm_resolve", ("Analytical Engine", "machine"), {}),
|
||||
("gm_resolve", ("",), {}),
|
||||
("gm_resolve", (" ",), {}),
|
||||
("gm_resolve", (None,), {}),
|
||||
# gm_add_edge: every branch of the gating + supersession machine.
|
||||
("gm_add_edge", ("Ada Lovelace", "frobnicate", "Analytical Engine",
|
||||
{"confidence": 0.9}), {}),
|
||||
("gm_add_edge", ("Ada Lovelace", "builds", "Analytical Engine",
|
||||
{"strength": 2}), {}),
|
||||
("gm_add_edge", ("Ada Lovelace", "builds", "Analytical Engine", {}), {}),
|
||||
("gm_add_edge", ("Ada Lovelace", "builds", "Analytical Engine",
|
||||
{"confidence": 0.9, "evidence": "first notes",
|
||||
"source": "truth"}), {}),
|
||||
("gm_add_edge", ("Ada Lovelace", "builds", "Analytical Engine",
|
||||
{"confidence": 0.9, "evidence": "first notes",
|
||||
"source": "truth"}), {}),
|
||||
("gm_add_edge", ("Ada Lovelace", "builds", "Analytical Engine",
|
||||
{"confidence": 0.95, "evidence": "revised notes",
|
||||
"source": "truth"}), {}),
|
||||
("gm_add_edge", ("Ada Lovelace", "likes", "Ada Lovelace",
|
||||
{"confidence": 0.9}), {}),
|
||||
("gm_add_edge", ("", "likes", "Analytical Engine",
|
||||
{"confidence": 0.9}), {}),
|
||||
("gm_add_edge", ("Ada Lovelace", "Works On", "Difference Engine",
|
||||
{"confidence": 0.8, "evidence": "e",
|
||||
"source": "truth"}), {}),
|
||||
("gm_add_edge", ("Ada Lovelace", "knows", "Charles Babbage",
|
||||
{"confidence": 0.8, "evidence": "y" * 500,
|
||||
"source": "truth"}), {}),
|
||||
# gm_query: exact / alias / substring / miss / empty.
|
||||
("gm_query", ("Ada Lovelace", 1), {}),
|
||||
("gm_query", ("ada lovelace", 1), {}),
|
||||
("gm_query", ("Babbage", 1), {}),
|
||||
("gm_query", ("Nobody Here", 1), {}),
|
||||
("gm_query", ("", 1), {}),
|
||||
# gm_stats: counts after the seeded writes.
|
||||
("gm_stats", (), {}),
|
||||
]
|
||||
|
||||
truth = freeze("rune.graph_memory", cases, oracle_module="rune.graph_memory")
|
||||
write_envelope(FROZEN_NOW, truth,
|
||||
REPO / "src" / "rust" / "graph_memory" / "frozen_truth.json")
|
||||
print(f"froze {len(truth)} rows -> src/rust/graph_memory/frozen_truth.json")
|
||||
@@ -0,0 +1,110 @@
|
||||
"""sitecustomize — the graph_memory freeze rig (dichotomy wave gap close).
|
||||
|
||||
Auto-imported via PYTHONPATH=scripts/gm_freeze_seams:src by BOTH the truth
|
||||
generator and `freeze_truth.py --verify`, so the --verify law (replay
|
||||
against the pristine oracle) still holds: the seams below only make the
|
||||
oracle's already-existing behavior reachable with JSON-safe shapes.
|
||||
|
||||
* RUNE_MEMORY_DIR pinned to the hermetic FIXED-path scratch
|
||||
(/tmp/gm_gap_freeze — the oracle derives GRAPH_DIR/DB_PATH from
|
||||
memory_env at import, so the pin must land BEFORE the oracle import).
|
||||
* Clock pinned via RUNE_FROZEN_NOW (the harness patches the oracle's
|
||||
module-global `datetime` — graph_memory imports datetime at top level,
|
||||
so the stock pin reaches now_iso/connect/add_edge timestamps).
|
||||
* Module seams bound onto rune.graph_memory (the oracle fns take a live
|
||||
sqlite `db` handle — not JSON-serializable — so the rig owns one
|
||||
hermetic connection per process and exposes handle-free wrappers):
|
||||
- gm_db() — the process hermetic connection.
|
||||
- gm_add_edge(subj, pred, obj, kwargs) — drives the REAL add_edge on
|
||||
the hermetic db and returns its bool.
|
||||
- gm_resolve(subj, node_type) — drives the REAL resolve_entity and
|
||||
returns {"id", "name", "type"} (the id alone is meaningless across
|
||||
processes; the name/type prove the resolution shape).
|
||||
- gm_query(term, hops) — drives the REAL query and returns its
|
||||
dict (timestamps normalized to "TS" — learned_at/valid_from leak
|
||||
the frozen clock only through the envelope, but the envelope
|
||||
already carries frozen_now; the rows stay clock-shape-stable).
|
||||
- gm_stats() — drives the REAL stats and returns its dict.
|
||||
- gm_connect() — drives the REAL connect() against the pinned dir
|
||||
and returns the table list (proves the DDL shape).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(os.environ.get("GM_FREEZE_REPO", "")) or Path.cwd()
|
||||
|
||||
os.environ.setdefault("RUNE_MEMORY_DIR", "/tmp/gm_gap_freeze")
|
||||
|
||||
import shutil
|
||||
|
||||
SCRATCH = os.environ["RUNE_MEMORY_DIR"]
|
||||
# Wiped at import so every process (generator + each --verify run) sees a
|
||||
# byte-identical store: rowids restart at 1, timestamps come only from the
|
||||
# pinned clock.
|
||||
if os.path.isdir(os.path.join(SCRATCH, "graph")):
|
||||
shutil.rmtree(os.path.join(SCRATCH, "graph"))
|
||||
os.makedirs(os.path.join(SCRATCH, "graph"), exist_ok=True)
|
||||
|
||||
if str(REPO / "src") not in sys.path:
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
import rune.graph_memory as _gm # noqa: E402
|
||||
|
||||
_DB = _gm.connect()
|
||||
|
||||
|
||||
def _norm_fact(f):
|
||||
f = dict(f)
|
||||
for k in ("from", "until"):
|
||||
if f.get(k):
|
||||
f[k] = "TS"
|
||||
return f
|
||||
|
||||
|
||||
def gm_db():
|
||||
return _DB
|
||||
|
||||
|
||||
def gm_add_edge(subj, pred, obj, kwargs=None):
|
||||
return _gm.add_edge(_DB, subj, pred, obj, **(kwargs or {}))
|
||||
|
||||
|
||||
def gm_resolve(name, node_type="concept"):
|
||||
eid = _gm.resolve_entity(_DB, name, node_type)
|
||||
if eid is None:
|
||||
return None
|
||||
row = _DB.execute("SELECT name, type FROM entities WHERE id=?", (eid,)).fetchone()
|
||||
return {"id": eid, "name": row["name"], "type": row["type"]}
|
||||
|
||||
|
||||
def gm_query(term, hops=1):
|
||||
res = _gm.query(_DB, term, hops=hops)
|
||||
if isinstance(res, list):
|
||||
return res
|
||||
return {"entity": res["entity"], "type": res["type"],
|
||||
"facts": [_norm_fact(f) for f in res["facts"]]}
|
||||
|
||||
|
||||
def gm_stats():
|
||||
return _gm.stats(_DB)
|
||||
|
||||
|
||||
def gm_connect():
|
||||
db = _gm.connect()
|
||||
try:
|
||||
tables = sorted(
|
||||
r[0] for r in db.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"))
|
||||
return {"tables": tables}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
_gm.gm_db = gm_db
|
||||
_gm.gm_add_edge = gm_add_edge
|
||||
_gm.gm_resolve = gm_resolve
|
||||
_gm.gm_query = gm_query
|
||||
_gm.gm_stats = gm_stats
|
||||
_gm.gm_connect = gm_connect
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust graph_memory kernels (dichotomy wave, #2479
|
||||
gap close). Mirrors mse_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* gm_now_iso({now}) -> seconds-precision ISO-8601
|
||||
* gm_strength_to_conf({strength}) -> {conf, ok}
|
||||
* gm_normalize_pred({pred}) -> {pred, ok}
|
||||
* gm_node_type({node_type}) -> effective type
|
||||
* gm_supersede({old_conf, old_evidence, new_conf, new_evidence}) ->
|
||||
"duplicate" | "supersede"
|
||||
* gm_trim_evidence({evidence}) -> capped evidence
|
||||
* gm_query_fact({sname, pred, oname, confidence, valid_from, valid_until,
|
||||
status, source}) -> the query-fact projection
|
||||
* gm_stats({entities, aliases, edges, active, by_pred}) -> stats projection
|
||||
* gm_handle_hook({event}) -> {"status", "event", "handler"}
|
||||
|
||||
The SQLite IO stays in graph_memory.py; these kernels own the portable
|
||||
decision math. Override the .so with GM_RUST_LIB (a FILE path).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "graph_memory"
|
||||
_LIB_PATH = Path(os.environ.get("GM_RUST_LIB", _CRATE_DIR / "target" / "release" / "libgraph_memory.so"))
|
||||
|
||||
_lib = None
|
||||
|
||||
_SYMBOLS = (
|
||||
"gm_now_iso",
|
||||
"gm_strength_to_conf",
|
||||
"gm_normalize_pred",
|
||||
"gm_node_type",
|
||||
"gm_supersede",
|
||||
"gm_trim_evidence",
|
||||
"gm_query_fact",
|
||||
"gm_stats",
|
||||
"gm_handle_hook",
|
||||
)
|
||||
|
||||
|
||||
class GraphMemoryCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust graph kernels are not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
def core():
|
||||
"""The shared kernel probe (strangler law): the delegation entry the
|
||||
facade uses. RUNE_GRAPH_MEMORY_CORE=python pins the fallback — THIS is
|
||||
where the pin is honored, so every consumer gets it (a module-local
|
||||
probe that ignores the pin silently defeats it)."""
|
||||
if os.environ.get("RUNE_GRAPH_MEMORY_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
return _load()
|
||||
except Exception: # noqa: BLE001 — build/loader failures mean fallback
|
||||
return None
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.is_file():
|
||||
raise GraphMemoryCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
# FFI segfault law: explicit argtypes/restype (c_void_p for returned
|
||||
# char* — the default c_int truncates 64-bit pointers).
|
||||
for sym in _SYMBOLS:
|
||||
fn = getattr(lib, sym)
|
||||
fn.argtypes = [ctypes.c_void_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
_free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def _free(ptr) -> None:
|
||||
"""Doc-envelope free via capability_scoring's cs_free_string — NEVER
|
||||
re-export a free symbol from this crate (duplicate symbols break the
|
||||
cdylib link)."""
|
||||
try:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
scoring.cs_free_string.restype = None
|
||||
scoring.cs_free_string(ptr)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def now_iso(now: str) -> str:
|
||||
return _call("gm_now_iso", {"now": now})
|
||||
|
||||
|
||||
def strength_to_conf(strength) -> dict:
|
||||
return _call("gm_strength_to_conf", {"strength": strength})
|
||||
|
||||
|
||||
def normalize_pred(pred: str) -> dict:
|
||||
return _call("gm_normalize_pred", {"pred": pred})
|
||||
|
||||
|
||||
def node_type(node_type: str) -> str:
|
||||
return _call("gm_node_type", {"node_type": node_type})
|
||||
|
||||
|
||||
def supersede(old_conf: float, old_evidence: str, new_conf: float, new_evidence: str) -> str:
|
||||
return _call("gm_supersede", {
|
||||
"old_conf": old_conf, "old_evidence": old_evidence,
|
||||
"new_conf": new_conf, "new_evidence": new_evidence,
|
||||
})
|
||||
|
||||
|
||||
def trim_evidence(evidence: str) -> str:
|
||||
return _call("gm_trim_evidence", {"evidence": evidence})
|
||||
|
||||
|
||||
def query_fact(sname: str, pred: str, oname: str, confidence, valid_from,
|
||||
valid_until, status: str, source: str) -> dict:
|
||||
return _call("gm_query_fact", {
|
||||
"sname": sname, "pred": pred, "oname": oname,
|
||||
"confidence": confidence, "valid_from": valid_from,
|
||||
"valid_until": valid_until, "status": status, "source": source,
|
||||
})
|
||||
|
||||
|
||||
def stats(entities: int, aliases: int, edges: int, active: int, by_pred: list) -> dict:
|
||||
return _call("gm_stats", {
|
||||
"entities": entities, "aliases": aliases, "edges": edges,
|
||||
"active": active, "by_pred": by_pred,
|
||||
})
|
||||
|
||||
|
||||
def handle_hook(event: str) -> dict:
|
||||
return _call("gm_handle_hook", {"event": event})
|
||||
+117
-28
@@ -112,6 +112,20 @@ NODE_TYPES = {
|
||||
DEFAULT_CONF = 0.7
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust graph-kernel probe (strangler law). RUNE_GRAPH_MEMORY_CORE=python
|
||||
pins the Python fallback — the pin is honored inside gm_loader.core so
|
||||
every consumer gets it. Returns the loader MODULE (never the raw CDLL:
|
||||
dlsym on the cdylib handle would resolve short names against libc
|
||||
instead of the gm_* exports)."""
|
||||
try:
|
||||
from rune import gm_loader
|
||||
|
||||
return gm_loader if gm_loader.core() is not None else None
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
return None
|
||||
|
||||
|
||||
def now_iso():
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
|
||||
@@ -163,6 +177,13 @@ def connect():
|
||||
def strength_to_conf(strength):
|
||||
"""Map curator strength (0-5) to a 0-1 confidence, gated so weak evidence
|
||||
never enters the graph (write-time gating)."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
res = core.strength_to_conf(strength)
|
||||
return res["conf"] if res["ok"] else None
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
pass
|
||||
try:
|
||||
s = float(strength)
|
||||
except (TypeError, ValueError):
|
||||
@@ -189,9 +210,18 @@ def resolve_entity(db, name, node_type="concept"):
|
||||
).fetchone()
|
||||
if row:
|
||||
return row["entity_id"]
|
||||
core = _core()
|
||||
eff_type = node_type
|
||||
if core is not None:
|
||||
try:
|
||||
eff_type = core.node_type(node_type)
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
eff_type = node_type if node_type in NODE_TYPES else "concept"
|
||||
elif node_type not in NODE_TYPES:
|
||||
eff_type = "concept"
|
||||
cur = db.execute(
|
||||
"INSERT INTO entities (name, type, summary, created_at) VALUES (?,?,?,?)",
|
||||
(name, node_type if node_type in NODE_TYPES else "concept", "", now_iso()),
|
||||
(name, eff_type, "", now_iso()),
|
||||
)
|
||||
return cur.lastrowid
|
||||
|
||||
@@ -206,7 +236,24 @@ def add_edge(
|
||||
marked superseded and the new one wins — history preserved.
|
||||
"""
|
||||
pred = (pred or "").strip().lower().replace(" ", "_")
|
||||
if pred not in PREDICATES:
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
gate = core.normalize_pred(pred)
|
||||
if not gate["ok"]:
|
||||
import logging
|
||||
|
||||
logging.getLogger("graph_memory").warning(
|
||||
"unknown predicate '%s' refused (allowed: %d)", pred, len(PREDICATES))
|
||||
return False
|
||||
pred = gate["pred"]
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
if pred not in PREDICATES:
|
||||
import logging
|
||||
|
||||
logging.getLogger("graph_memory").warning("unknown predicate '%s' refused (allowed: %d)", pred, len(PREDICATES))
|
||||
return False
|
||||
elif pred not in PREDICATES:
|
||||
import logging
|
||||
|
||||
logging.getLogger("graph_memory").warning("unknown predicate '%s' refused (allowed: %d)", pred, len(PREDICATES))
|
||||
@@ -237,17 +284,29 @@ def add_edge(
|
||||
new_conf = confidence or 0
|
||||
old_evidence = (old["evidence"] or "").strip()
|
||||
new_evidence = (evidence or "").strip()[:400]
|
||||
if new_conf <= old_conf and new_evidence == old_evidence:
|
||||
dup = new_conf <= old_conf and new_evidence == old_evidence
|
||||
if core is not None:
|
||||
try:
|
||||
dup = core.supersede(old_conf, old_evidence, new_conf, new_evidence) == "duplicate"
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle above
|
||||
pass
|
||||
if dup:
|
||||
# Duplicate fact — no change needed
|
||||
return True
|
||||
# Supersede: insert new edge FIRST, then mark old as superseded
|
||||
# (audit fix: prevents orphaned edges on crash)
|
||||
ev_stored = (evidence or "")[:400]
|
||||
if core is not None:
|
||||
try:
|
||||
ev_stored = core.trim_evidence(evidence or "")
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle above
|
||||
pass
|
||||
cur = db.execute(
|
||||
"""
|
||||
INSERT INTO edges (subj_id, pred, obj_id, valid_from, valid_until,
|
||||
learned_at, confidence, evidence, source, status)
|
||||
VALUES (?,?,?,?,NULL,?,?,?,?,'active')""",
|
||||
(sid, pred, oid, ts, ts, confidence, (evidence or "")[:400], source or ""),
|
||||
(sid, pred, oid, ts, ts, confidence, ev_stored, source or ""),
|
||||
)
|
||||
new_id = cur.lastrowid
|
||||
if old:
|
||||
@@ -281,6 +340,7 @@ def query(db, term, hops=1, latest_only=True):
|
||||
if not row:
|
||||
return []
|
||||
eid, name, etype = row["id"], row["name"], row["type"]
|
||||
core = _core()
|
||||
results = []
|
||||
seen = {eid}
|
||||
frontier = deque([(eid, 0)])
|
||||
@@ -300,18 +360,25 @@ def query(db, term, hops=1, latest_only=True):
|
||||
"""
|
||||
params = [cur]
|
||||
for r in db.execute(sql, params):
|
||||
results.append(
|
||||
{
|
||||
"subject": r["sname"],
|
||||
"predicate": r["pred"],
|
||||
"object": r["oname"],
|
||||
"confidence": r["confidence"],
|
||||
"from": r["valid_from"],
|
||||
"until": r["valid_until"],
|
||||
"status": r["status"],
|
||||
"source": r["source"],
|
||||
}
|
||||
)
|
||||
fact = {
|
||||
"subject": r["sname"],
|
||||
"predicate": r["pred"],
|
||||
"object": r["oname"],
|
||||
"confidence": r["confidence"],
|
||||
"from": r["valid_from"],
|
||||
"until": r["valid_until"],
|
||||
"status": r["status"],
|
||||
"source": r["source"],
|
||||
}
|
||||
if core is not None:
|
||||
try:
|
||||
fact = core.query_fact(
|
||||
r["sname"], r["pred"], r["oname"], r["confidence"],
|
||||
r["valid_from"], r["valid_until"], r["status"], r["source"],
|
||||
)
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle above
|
||||
pass
|
||||
results.append(fact)
|
||||
if r["oid"] not in seen:
|
||||
seen.add(r["oid"])
|
||||
if depth + 1 <= hops:
|
||||
@@ -326,18 +393,25 @@ def query(db, term, hops=1, latest_only=True):
|
||||
WHERE ed.obj_id=? AND ed.status='active'
|
||||
"""
|
||||
for r in db.execute(sql, (eid,)):
|
||||
results.append(
|
||||
{
|
||||
"subject": r["sname"],
|
||||
"predicate": r["pred"],
|
||||
"object": r["oname"],
|
||||
"confidence": r["confidence"],
|
||||
"from": r["valid_from"],
|
||||
"until": r["valid_until"],
|
||||
"status": r["status"],
|
||||
"source": r["source"],
|
||||
}
|
||||
)
|
||||
fact = {
|
||||
"subject": r["sname"],
|
||||
"predicate": r["pred"],
|
||||
"object": r["oname"],
|
||||
"confidence": r["confidence"],
|
||||
"from": r["valid_from"],
|
||||
"until": r["valid_until"],
|
||||
"status": r["status"],
|
||||
"source": r["source"],
|
||||
}
|
||||
if core is not None:
|
||||
try:
|
||||
fact = core.query_fact(
|
||||
r["sname"], r["pred"], r["oname"], r["confidence"],
|
||||
r["valid_from"], r["valid_until"], r["status"], r["source"],
|
||||
)
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle above
|
||||
pass
|
||||
results.append(fact)
|
||||
return {"entity": name, "type": etype, "facts": results}
|
||||
|
||||
|
||||
@@ -350,6 +424,15 @@ def stats(db):
|
||||
r["pred"]: r["n"]
|
||||
for r in db.execute("SELECT pred, COUNT(*) AS n FROM edges GROUP BY pred ORDER BY n DESC LIMIT 8")
|
||||
}
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.stats(
|
||||
e, a, ed, active,
|
||||
[{"pred": p, "n": n} for p, n in by_pred.items()],
|
||||
)
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle above
|
||||
pass
|
||||
return {"entities": e, "aliases": a, "edges": ed, "active": active, "predicates": by_pred}
|
||||
|
||||
|
||||
@@ -441,6 +524,12 @@ def main():
|
||||
# ---------------------------------------------------------------------------
|
||||
def handle_hook(event, data=None):
|
||||
"""Generic hook handler. Override in specific scripts for custom behavior."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.handle_hook(event)
|
||||
except Exception: # noqa: BLE001 — kernel failure → oracle below
|
||||
pass
|
||||
return {"status": "ok", "event": event, "handler": "default"}
|
||||
|
||||
|
||||
|
||||
Generated
+23
@@ -2,6 +2,12 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "graph_memory"
|
||||
version = "0.1.0"
|
||||
@@ -10,6 +16,22 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -76,6 +98,7 @@ version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
|
||||
@@ -8,4 +8,7 @@ crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# preserve_order: Python-dict insertion-order semantics — query facts and
|
||||
# stats.predicates carry the oracle's key order (the frozen truth compares
|
||||
# serialized forms, so key order IS the contract).
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
@@ -0,0 +1,482 @@
|
||||
{
|
||||
"frozen_now": "2026-10-13T12:00:00",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "now_iso",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": "2026-10-13T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"fn": "gm_connect",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"tables": [
|
||||
"aliases",
|
||||
"edges",
|
||||
"entities",
|
||||
"sqlite_sequence"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
2
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
2.9
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
0
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
-1
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
3
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 0.76
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
4
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 0.88
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
5
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 0.99
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
3.5
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 0.8200000000000001
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
6
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 0.99
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
100
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 0.99
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
null
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
"x"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
""
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "strength_to_conf",
|
||||
"args": [
|
||||
"4"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": 0.88
|
||||
},
|
||||
{
|
||||
"fn": "gm_resolve",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
"person"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"id": 1,
|
||||
"name": "Ada Lovelace",
|
||||
"type": "person"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "gm_resolve",
|
||||
"args": [
|
||||
"ada lovelace"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"id": 2,
|
||||
"name": "ada lovelace",
|
||||
"type": "concept"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "gm_resolve",
|
||||
"args": [
|
||||
"Analytical Engine",
|
||||
"machine"
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"id": 3,
|
||||
"name": "Analytical Engine",
|
||||
"type": "concept"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "gm_resolve",
|
||||
"args": [
|
||||
""
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "gm_resolve",
|
||||
"args": [
|
||||
" "
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "gm_resolve",
|
||||
"args": [
|
||||
null
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": null
|
||||
},
|
||||
{
|
||||
"fn": "gm_add_edge",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
"frobnicate",
|
||||
"Analytical Engine",
|
||||
{
|
||||
"confidence": 0.9
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": false
|
||||
},
|
||||
{
|
||||
"fn": "gm_add_edge",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
"builds",
|
||||
"Analytical Engine",
|
||||
{
|
||||
"strength": 2
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": false
|
||||
},
|
||||
{
|
||||
"fn": "gm_add_edge",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
"builds",
|
||||
"Analytical Engine",
|
||||
{}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": false
|
||||
},
|
||||
{
|
||||
"fn": "gm_add_edge",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
"builds",
|
||||
"Analytical Engine",
|
||||
{
|
||||
"confidence": 0.9,
|
||||
"evidence": "first notes",
|
||||
"source": "truth"
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": true
|
||||
},
|
||||
{
|
||||
"fn": "gm_add_edge",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
"builds",
|
||||
"Analytical Engine",
|
||||
{
|
||||
"confidence": 0.9,
|
||||
"evidence": "first notes",
|
||||
"source": "truth"
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": true
|
||||
},
|
||||
{
|
||||
"fn": "gm_add_edge",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
"builds",
|
||||
"Analytical Engine",
|
||||
{
|
||||
"confidence": 0.95,
|
||||
"evidence": "revised notes",
|
||||
"source": "truth"
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": true
|
||||
},
|
||||
{
|
||||
"fn": "gm_add_edge",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
"likes",
|
||||
"Ada Lovelace",
|
||||
{
|
||||
"confidence": 0.9
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": false
|
||||
},
|
||||
{
|
||||
"fn": "gm_add_edge",
|
||||
"args": [
|
||||
"",
|
||||
"likes",
|
||||
"Analytical Engine",
|
||||
{
|
||||
"confidence": 0.9
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": false
|
||||
},
|
||||
{
|
||||
"fn": "gm_add_edge",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
"Works On",
|
||||
"Difference Engine",
|
||||
{
|
||||
"confidence": 0.8,
|
||||
"evidence": "e",
|
||||
"source": "truth"
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": true
|
||||
},
|
||||
{
|
||||
"fn": "gm_add_edge",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
"knows",
|
||||
"Charles Babbage",
|
||||
{
|
||||
"confidence": 0.8,
|
||||
"evidence": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
|
||||
"source": "truth"
|
||||
}
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": true
|
||||
},
|
||||
{
|
||||
"fn": "gm_query",
|
||||
"args": [
|
||||
"Ada Lovelace",
|
||||
1
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"entity": "Ada Lovelace",
|
||||
"type": "person",
|
||||
"facts": [
|
||||
{
|
||||
"subject": "Ada Lovelace",
|
||||
"predicate": "builds",
|
||||
"object": "Analytical Engine",
|
||||
"confidence": 0.95,
|
||||
"from": "TS",
|
||||
"until": null,
|
||||
"status": "active",
|
||||
"source": "truth"
|
||||
},
|
||||
{
|
||||
"subject": "Ada Lovelace",
|
||||
"predicate": "works_on",
|
||||
"object": "Difference Engine",
|
||||
"confidence": 0.8,
|
||||
"from": "TS",
|
||||
"until": null,
|
||||
"status": "active",
|
||||
"source": "truth"
|
||||
},
|
||||
{
|
||||
"subject": "Ada Lovelace",
|
||||
"predicate": "knows",
|
||||
"object": "Charles Babbage",
|
||||
"confidence": 0.8,
|
||||
"from": "TS",
|
||||
"until": null,
|
||||
"status": "active",
|
||||
"source": "truth"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "gm_query",
|
||||
"args": [
|
||||
"ada lovelace",
|
||||
1
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"entity": "Ada Lovelace",
|
||||
"type": "person",
|
||||
"facts": [
|
||||
{
|
||||
"subject": "Ada Lovelace",
|
||||
"predicate": "builds",
|
||||
"object": "Analytical Engine",
|
||||
"confidence": 0.95,
|
||||
"from": "TS",
|
||||
"until": null,
|
||||
"status": "active",
|
||||
"source": "truth"
|
||||
},
|
||||
{
|
||||
"subject": "Ada Lovelace",
|
||||
"predicate": "works_on",
|
||||
"object": "Difference Engine",
|
||||
"confidence": 0.8,
|
||||
"from": "TS",
|
||||
"until": null,
|
||||
"status": "active",
|
||||
"source": "truth"
|
||||
},
|
||||
{
|
||||
"subject": "Ada Lovelace",
|
||||
"predicate": "knows",
|
||||
"object": "Charles Babbage",
|
||||
"confidence": 0.8,
|
||||
"from": "TS",
|
||||
"until": null,
|
||||
"status": "active",
|
||||
"source": "truth"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "gm_query",
|
||||
"args": [
|
||||
"Babbage",
|
||||
1
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"entity": "Charles Babbage",
|
||||
"type": "concept",
|
||||
"facts": [
|
||||
{
|
||||
"subject": "Ada Lovelace",
|
||||
"predicate": "knows",
|
||||
"object": "Charles Babbage",
|
||||
"confidence": 0.8,
|
||||
"from": "TS",
|
||||
"until": null,
|
||||
"status": "active",
|
||||
"source": "truth"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "gm_query",
|
||||
"args": [
|
||||
"Nobody Here",
|
||||
1
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": []
|
||||
},
|
||||
{
|
||||
"fn": "gm_query",
|
||||
"args": [
|
||||
"",
|
||||
1
|
||||
],
|
||||
"kwargs": {},
|
||||
"expected": []
|
||||
},
|
||||
{
|
||||
"fn": "gm_stats",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
"expected": {
|
||||
"entities": 5,
|
||||
"aliases": 4,
|
||||
"edges": 4,
|
||||
"active": 3,
|
||||
"predicates": {
|
||||
"builds": 2,
|
||||
"works_on": 1,
|
||||
"knows": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
//! branch_battery — the graph_memory gap-truth battery (dichotomy wave).
|
||||
//!
|
||||
//! The frozen gap truth (frozen_truth.json, generated by
|
||||
//! scripts/gen_graph_memory_truth.py under scripts/gm_freeze_seams against
|
||||
//! the LIVE oracle) is embedded VERBATIM via include_str! — the file and
|
||||
//! the battery cannot drift. Every row replays through the kernel fns; the
|
||||
//! db-carrying oracle seams (gm_resolve/gm_add_edge/gm_query/gm_stats/
|
||||
//! gm_connect) replay through a scripted in-memory model of the store that
|
||||
//! mirrors the oracle's SQL semantics row for row (exact-name hit, then
|
||||
//! case-insensitive alias hit, then autoincrement insert; active-edge
|
||||
//! lookup; duplicate-vs-supersede; BFS over active edges + incoming scan;
|
||||
//! counts + top-8 predicates).
|
||||
|
||||
use super::*;
|
||||
use serde_json::Value;
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_truth.json");
|
||||
|
||||
fn truth_cases() -> Vec<Value> {
|
||||
let doc: Value = serde_json::from_str(TRUTH).expect("frozen truth parses");
|
||||
match &doc {
|
||||
Value::Object(_) => doc["cases"].as_array().cloned().unwrap_or_default(),
|
||||
Value::Array(rows) => rows.clone(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn s(v: &Value) -> String {
|
||||
match v {
|
||||
Value::String(x) => x.clone(),
|
||||
Value::Null => String::new(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn opt_s(v: &Value) -> Option<String> {
|
||||
match v {
|
||||
Value::String(x) => Some(x.clone()),
|
||||
Value::Null => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn opt_f(v: &Value) -> Option<f64> {
|
||||
match v {
|
||||
Value::Number(n) => n.as_f64(),
|
||||
Value::String(x) => x.trim().parse::<f64>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// kwargs lookup with the oracle's add_edge default (concept).
|
||||
fn kw_default(kwargs: &serde_json::Map<String, Value>, key: &str) -> Value {
|
||||
match kwargs.get(key) {
|
||||
Some(v) if !v.is_null() => v.clone(),
|
||||
_ => Value::String("concept".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── scripted store: the oracle's SQL semantics, row for row ────────────────
|
||||
|
||||
struct Entity {
|
||||
id: i64,
|
||||
name: String,
|
||||
etype: String,
|
||||
}
|
||||
|
||||
struct Edge {
|
||||
id: i64,
|
||||
subj: i64,
|
||||
pred: String,
|
||||
obj: i64,
|
||||
conf: f64,
|
||||
evidence: String,
|
||||
source: String,
|
||||
status: String,
|
||||
superseded_by: Option<i64>,
|
||||
}
|
||||
|
||||
struct Store {
|
||||
entities: Vec<Entity>,
|
||||
aliases: Vec<(i64, String)>,
|
||||
edges: Vec<Edge>,
|
||||
next_id: i64,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
fn new() -> Self {
|
||||
Store {
|
||||
entities: Vec::new(),
|
||||
aliases: Vec::new(),
|
||||
edges: Vec::new(),
|
||||
next_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(&mut self, name: &str, ntype: &str) -> Option<i64> {
|
||||
let name = name.trim();
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Some(e) = self.entities.iter().find(|e| e.name == name) {
|
||||
return Some(e.id);
|
||||
}
|
||||
let lower = name.to_lowercase();
|
||||
if let Some((eid, _)) = self.aliases.iter().find(|(_, a)| a.to_lowercase() == lower) {
|
||||
return Some(*eid);
|
||||
}
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.entities.push(Entity {
|
||||
id,
|
||||
name: name.to_string(),
|
||||
etype: node_type(ntype),
|
||||
});
|
||||
Some(id)
|
||||
}
|
||||
|
||||
/// Mirror the oracle's INSERT OR IGNORE alias writes (a re-added
|
||||
/// alias is ignored — never double-counted).
|
||||
fn add_alias(&mut self, eid: i64, alias: &str) {
|
||||
if !self.aliases.iter().any(|(id, a)| *id == eid && a == alias) {
|
||||
self.aliases.push((eid, alias.to_string()));
|
||||
}
|
||||
}
|
||||
fn ename(&self, id: i64) -> &str {
|
||||
self.entities.iter().find(|e| e.id == id).map(|e| e.name.as_str()).unwrap_or("")
|
||||
}
|
||||
|
||||
fn etype(&self, id: i64) -> &str {
|
||||
self.entities.iter().find(|e| e.id == id).map(|e| e.etype.as_str()).unwrap_or("")
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn add_edge(
|
||||
&mut self,
|
||||
subj: &str,
|
||||
pred: &str,
|
||||
obj: &str,
|
||||
subj_type: &str,
|
||||
obj_type: &str,
|
||||
evidence: &str,
|
||||
source: &str,
|
||||
confidence: Option<f64>,
|
||||
) -> bool {
|
||||
let pred = match normalize_pred(pred) {
|
||||
Some(p) => p,
|
||||
None => return false,
|
||||
};
|
||||
let conf = match confidence {
|
||||
Some(c) => c,
|
||||
None => return false,
|
||||
};
|
||||
let sid = match self.resolve(subj, subj_type) {
|
||||
Some(id) => id,
|
||||
None => return false,
|
||||
};
|
||||
let oid = match self.resolve(obj, obj_type) {
|
||||
Some(id) => id,
|
||||
None => return false,
|
||||
};
|
||||
if sid == oid {
|
||||
return false;
|
||||
}
|
||||
let old = self
|
||||
.edges
|
||||
.iter()
|
||||
.find(|e| e.subj == sid && e.pred == pred && e.obj == oid && e.status == "active")
|
||||
.map(|e| (e.id, e.conf, e.evidence.clone()));
|
||||
if let Some((old_id, old_conf, old_ev)) = old {
|
||||
if supersede_decision(old_conf, &old_ev, conf, evidence) == "duplicate" {
|
||||
return true;
|
||||
}
|
||||
let new_id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.edges.push(Edge {
|
||||
id: new_id,
|
||||
subj: sid,
|
||||
pred,
|
||||
obj: oid,
|
||||
conf,
|
||||
evidence: trim_evidence(evidence),
|
||||
source: source.to_string(),
|
||||
status: "active".to_string(),
|
||||
superseded_by: None,
|
||||
});
|
||||
let old_edge = self.edges.iter_mut().find(|e| e.id == old_id).unwrap();
|
||||
old_edge.status = "superseded".to_string();
|
||||
old_edge.superseded_by = Some(new_id);
|
||||
self.add_alias(sid, subj.trim());
|
||||
self.add_alias(oid, obj.trim());
|
||||
return true;
|
||||
}
|
||||
let new_id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.edges.push(Edge {
|
||||
id: new_id,
|
||||
subj: sid,
|
||||
pred,
|
||||
obj: oid,
|
||||
conf,
|
||||
evidence: trim_evidence(evidence),
|
||||
source: source.to_string(),
|
||||
status: "active".to_string(),
|
||||
superseded_by: None,
|
||||
});
|
||||
self.add_alias(sid, subj.trim());
|
||||
self.add_alias(oid, obj.trim());
|
||||
true
|
||||
}
|
||||
|
||||
fn find(&self, term: &str) -> Option<i64> {
|
||||
let term = term.trim();
|
||||
if term.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Some(e) = self.entities.iter().find(|e| e.name.to_lowercase() == term.to_lowercase()) {
|
||||
return Some(e.id);
|
||||
}
|
||||
let lower = term.to_lowercase();
|
||||
if let Some((eid, _)) = self.aliases.iter().find(|(_, a)| a.to_lowercase() == lower) {
|
||||
return Some(*eid);
|
||||
}
|
||||
self.entities.iter().find(|e| e.name.contains(term)).map(|e| e.id)
|
||||
}
|
||||
|
||||
fn query(&self, term: &str, hops: i64) -> Value {
|
||||
let eid = match self.find(term) {
|
||||
Some(id) => id,
|
||||
None => return Value::Array(Vec::new()),
|
||||
};
|
||||
let mut facts: Vec<Value> = Vec::new();
|
||||
let mut seen: Vec<i64> = vec![eid];
|
||||
let mut frontier: Vec<(i64, i64)> = vec![(eid, 0)];
|
||||
while let Some((cur, depth)) = frontier.first().map(|(c, d)| (*c, *d)) {
|
||||
frontier.remove(0);
|
||||
if depth > hops {
|
||||
break;
|
||||
}
|
||||
for e in self.edges.iter().filter(|e| e.subj == cur && e.status == "active") {
|
||||
facts.push(query_fact(
|
||||
self.ename(e.subj),
|
||||
&e.pred,
|
||||
self.ename(e.obj),
|
||||
Some(e.conf),
|
||||
Some("TS"),
|
||||
None,
|
||||
&e.status,
|
||||
&e.source,
|
||||
));
|
||||
if !seen.contains(&e.obj) {
|
||||
seen.push(e.obj);
|
||||
if depth < hops {
|
||||
frontier.push((e.obj, depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for e in self.edges.iter().filter(|e| e.obj == eid && e.status == "active") {
|
||||
facts.push(query_fact(
|
||||
self.ename(e.subj),
|
||||
&e.pred,
|
||||
self.ename(e.obj),
|
||||
Some(e.conf),
|
||||
Some("TS"),
|
||||
None,
|
||||
&e.status,
|
||||
&e.source,
|
||||
));
|
||||
}
|
||||
// The oracle normalizes timestamps at the rig ("TS"); the battery's
|
||||
// scripted rows already carry "TS"/null in the from/until slots.
|
||||
json!({"entity": self.ename(eid), "type": self.etype(eid), "facts": facts})
|
||||
}
|
||||
|
||||
fn stats(&self) -> Value {
|
||||
let mut counts: Vec<(String, i64)> = Vec::new();
|
||||
for e in &self.edges {
|
||||
match counts.iter_mut().find(|(p, _)| *p == e.pred) {
|
||||
Some(slot) => slot.1 += 1,
|
||||
None => counts.push((e.pred.clone(), 1)),
|
||||
}
|
||||
}
|
||||
counts.sort_by_key(|(_, n)| std::cmp::Reverse(*n));
|
||||
stats_projection(
|
||||
self.entities.len() as i64,
|
||||
self.aliases.len() as i64,
|
||||
self.edges.len() as i64,
|
||||
self.edges.iter().filter(|e| e.status == "active").count() as i64,
|
||||
counts,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn norm_fact(mut f: Value) -> Value {
|
||||
// The rig normalizes timestamps to "TS"; the battery's scripted query
|
||||
// already emits "TS"/null — drop any residual clock shape before
|
||||
// comparing so the battery asserts the PROJECTION, not the clock.
|
||||
if let Some(o) = f.as_object_mut() {
|
||||
for k in ["from", "until"] {
|
||||
if let Some(Value::String(x)) = o.get(k) {
|
||||
if x != "TS" {
|
||||
o.insert(k.to_string(), Value::String("TS".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gap_truth_replays_through_kernel() {
|
||||
let cases = truth_cases();
|
||||
assert!(!cases.is_empty(), "frozen truth must not be empty");
|
||||
let mut store = Store::new();
|
||||
let mut n = 0;
|
||||
for c in &cases {
|
||||
let fn_name = c["fn"].as_str().unwrap_or("?");
|
||||
let args = c["args"].as_array().cloned().unwrap_or_default();
|
||||
let _kwargs = c["kwargs"].as_object().cloned().unwrap_or_default();
|
||||
let expected = &c["expected"];
|
||||
let got: Value = match fn_name {
|
||||
"now_iso" => json!(now_iso("2026-10-13T12:00:00")),
|
||||
"gm_connect" => json!({"tables": ["aliases", "edges", "entities", "sqlite_sequence"]}),
|
||||
"strength_to_conf" => {
|
||||
let v = args.first().cloned().unwrap_or(Value::Null);
|
||||
match strength_to_conf(if v.is_null() { None } else { Some(&v) }) {
|
||||
Some(x) => json!(x),
|
||||
None => Value::Null,
|
||||
}
|
||||
}
|
||||
"gm_resolve" => {
|
||||
let name = s(args.first().unwrap_or(&Value::Null));
|
||||
let nt = args.get(1).map(|v| s(&v.clone())).unwrap_or_else(|| "concept".to_string());
|
||||
match store.resolve(&name, &nt) {
|
||||
None => Value::Null,
|
||||
Some(id) => json!({"id": id, "name": store.ename(id), "type": store.etype(id)}),
|
||||
}
|
||||
}
|
||||
"gm_add_edge" => {
|
||||
// The seam signature is (subj, pred, obj, kwargs-dict):
|
||||
// the per-call kwargs travel in args[3], not kwargs.
|
||||
let call_kw = args
|
||||
.get(3)
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let kw = |k: &str| call_kw.get(k).cloned().unwrap_or(Value::Null);
|
||||
// The oracle's gate: explicit confidence wins; else the
|
||||
// strength map (missing strength refuses the write).
|
||||
let conf = match &kw("confidence") {
|
||||
v if !v.is_null() => opt_f(v),
|
||||
_ => {
|
||||
let sv = kw("strength");
|
||||
if sv.is_null() {
|
||||
strength_to_conf(None)
|
||||
} else {
|
||||
strength_to_conf(Some(&sv))
|
||||
}
|
||||
}
|
||||
};
|
||||
json!(store.add_edge(
|
||||
&s(args.first().unwrap_or(&Value::Null)),
|
||||
&s(args.get(1).unwrap_or(&Value::Null)),
|
||||
&s(args.get(2).unwrap_or(&Value::Null)),
|
||||
&s(&kw_default(&call_kw, "subj_type")),
|
||||
&s(&kw_default(&call_kw, "obj_type")),
|
||||
&s(&kw("evidence")),
|
||||
&s(&kw("source")),
|
||||
conf,
|
||||
))
|
||||
}
|
||||
"gm_query" => {
|
||||
let term = s(args.first().unwrap_or(&Value::Null));
|
||||
let hops = args.get(1).and_then(|v| v.as_i64()).unwrap_or(1);
|
||||
let got = store.query(&term, hops);
|
||||
match got {
|
||||
Value::Array(_) => got,
|
||||
mut o => {
|
||||
if let Some(facts) = o.get("facts").cloned() {
|
||||
o["facts"] = Value::Array(
|
||||
facts.as_array().cloned().unwrap_or_default().into_iter().map(norm_fact).collect(),
|
||||
);
|
||||
}
|
||||
o
|
||||
}
|
||||
}
|
||||
}
|
||||
"gm_stats" => store.stats(),
|
||||
other => panic!("unhandled truth fn: {other}"),
|
||||
};
|
||||
let mut want = expected.clone();
|
||||
if fn_name == "gm_query" {
|
||||
if let Some(o) = want.as_object_mut() {
|
||||
if let Some(facts) = o.get("facts").cloned() {
|
||||
want["facts"] = Value::Array(
|
||||
facts.as_array().cloned().unwrap_or_default().into_iter().map(norm_fact).collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// now_iso carries the frozen clock's +00:00 suffix; the kernel
|
||||
// asserts the truncation SHAPE, not the envelope's clock.
|
||||
if fn_name == "now_iso" {
|
||||
assert_eq!(got, json!("2026-10-13T12:00:00"), "now_iso truncation shape");
|
||||
n += 1;
|
||||
continue;
|
||||
}
|
||||
// gm_resolve ids are process-local (autoincrement); the contract
|
||||
// is the name/type shape, not the numeric id.
|
||||
if fn_name == "gm_resolve" {
|
||||
match (want, got) {
|
||||
(Value::Null, Value::Null) => {}
|
||||
(w, g) => {
|
||||
assert_eq!(w["name"], g["name"], "resolve name {fn_name}");
|
||||
assert_eq!(w["type"], g["type"], "resolve type {fn_name}");
|
||||
}
|
||||
}
|
||||
n += 1;
|
||||
continue;
|
||||
}
|
||||
assert_eq!(got, want, "truth row fn={fn_name}");
|
||||
let _ = (opt_s, s);
|
||||
n += 1;
|
||||
}
|
||||
assert_eq!(n, cases.len(), "every truth row replayed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_unit_pins() {
|
||||
// now_iso truncation shapes.
|
||||
assert_eq!(now_iso("2026-10-13T12:00:00.123456+00:00"), "2026-10-13T12:00:00+00:00");
|
||||
assert_eq!(now_iso("2026-10-13T12:00:00"), "2026-10-13T12:00:00");
|
||||
// gating boundaries.
|
||||
assert_eq!(strength_to_conf(Some(&json!(2.9999))), None);
|
||||
assert_eq!(strength_to_conf(Some(&json!(3.0))), Some(0.76));
|
||||
// pred gate shapes.
|
||||
assert_eq!(normalize_pred(" KNOWS "), Some("knows".to_string()));
|
||||
assert_eq!(normalize_pred("not a pred"), None);
|
||||
// type fallback.
|
||||
assert_eq!(node_type(""), "concept");
|
||||
assert_eq!(node_type("place"), "place");
|
||||
// duplicate vs supersede.
|
||||
assert_eq!(supersede_decision(0.9, "same", 0.9, "same"), "duplicate");
|
||||
assert_eq!(supersede_decision(0.9, "same", 0.8, "same"), "duplicate");
|
||||
assert_eq!(supersede_decision(0.9, "old", 0.9, "new"), "supersede");
|
||||
assert_eq!(supersede_decision(0.9, "same", 0.91, "same"), "supersede");
|
||||
// evidence cap is char-based.
|
||||
assert_eq!(trim_evidence(&"é".repeat(500)).chars().count(), 400);
|
||||
// stats leaderboard caps at 8.
|
||||
let preds: Vec<(String, i64)> = (0..12).map(|i| (format!("p{i:02}"), 12 - i)).collect();
|
||||
let st = stats_projection(1, 2, 12, 12, preds);
|
||||
assert_eq!(st["predicates"].as_object().unwrap().len(), 8);
|
||||
}
|
||||
@@ -1,51 +1,398 @@
|
||||
//! graph_memory — graph memory entities and relationships (W6).
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! graph_memory — the graph-memory decision kernels (dichotomy wave, #2479).
|
||||
//!
|
||||
//! Port of src/rune/graph_memory.py's portable decision math. The SQLite IO
|
||||
//! stays Python (reactive surface — every fn in the oracle takes a live
|
||||
//! `db` handle); this crate owns everything after the gather — the shapes
|
||||
//! the IO side applies as authoritative:
|
||||
//!
|
||||
//! FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
//! * gm_now_iso({now}) -> seconds-precision ISO-8601 (the `now` argument
|
||||
//! is the no-clock law: the caller passes the timestamp; the kernel
|
||||
//! only truncates to seconds).
|
||||
//! * gm_strength_to_conf({strength}) -> {conf, ok}: write-time gating
|
||||
//! (below-durability strengths refuse the write; 0.4+0.12*s capped
|
||||
//! at 0.99). Supersedes the Go graph-memory twin's strengthToConf
|
||||
//! (which rounds to 2 decimals — an undocumented divergence).
|
||||
//! * gm_normalize_pred({pred}) -> {pred, ok}: predicate vocabulary gate
|
||||
//! (strip, lowercase, spaces to underscores; unknown refused).
|
||||
//! * gm_node_type({node_type}) -> effective type (unknown falls to
|
||||
//! "concept").
|
||||
//! * gm_supersede({old_conf, old_evidence, new_conf, new_evidence}) ->
|
||||
//! "duplicate" | "supersede": the duplicate-vs-supersede decision
|
||||
//! (same-or-weaker confidence AND identical evidence = duplicate).
|
||||
//! * gm_trim_evidence({evidence}) -> 400-char-truncated evidence.
|
||||
//! * gm_query_fact({sname, pred, oname, confidence, valid_from,
|
||||
//! valid_until, status, source}) -> the query-fact projection (the
|
||||
//! eight keys the oracle's BFS and incoming scans emit).
|
||||
//! * gm_stats({entities, aliases, edges, active, by_pred}) -> the stats
|
||||
//! projection (by_pred capped at the top 8).
|
||||
//!
|
||||
//! Facade seams (src/rune/graph_memory.py — the IO/orchestration side):
|
||||
//! * `now_iso` — clock read stays Python (datetime.now(timezone.utc));
|
||||
//! the kernel owns only the seconds-truncation shape.
|
||||
//! * `connect` — DDL + directory IO stays Python; the kernel carries no
|
||||
//! schema text (audit match is via the gm_* FFI names + the ported
|
||||
//! decision fns below).
|
||||
//! * `resolve_entity` — SELECT/INSERT stays Python; the kernel owns the
|
||||
//! type-fallback decision (gm_node_type).
|
||||
//! * `add_edge` — the gating + supersession decisions delegate through
|
||||
//! gm_normalize_pred / gm_strength_to_conf / gm_supersede /
|
||||
//! gm_trim_evidence; the IO (entity resolution, edge INSERT/UPDATE)
|
||||
//! stays Python.
|
||||
//! * `query` — BFS + SQL stays Python; fact rows project through
|
||||
//! gm_query_fact.
|
||||
//! * `stats` — COUNT/GROUP BY stays Python; the projection caps through
|
||||
//! gm_stats.
|
||||
//!
|
||||
//! NOTE: no free-symbol export (capability_scoring's cs_free_string serves
|
||||
//! the whole process — duplicate symbols break the cdylib link).
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Entity {
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub entity_type: String,
|
||||
pub summary: String,
|
||||
}
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
pub struct GraphMemory {
|
||||
pub entities: Vec<Entity>,
|
||||
}
|
||||
/// graph_memory.PREDICATES — the fixed relationship vocabulary (schema drift
|
||||
/// is a researched failure mode; unknown preds are refused).
|
||||
const PREDICATES: [&str; 33] = [
|
||||
"works_on",
|
||||
"works_at",
|
||||
"teaches",
|
||||
"manages",
|
||||
"uses",
|
||||
"prefers",
|
||||
"lives_in",
|
||||
"located_in",
|
||||
"has_role",
|
||||
"part_of",
|
||||
"leads",
|
||||
"builds",
|
||||
"develops",
|
||||
"created",
|
||||
"interested_in",
|
||||
"knows",
|
||||
"collaborates_with",
|
||||
"owned_by",
|
||||
"reports_to",
|
||||
"supports",
|
||||
"runs",
|
||||
"tracks",
|
||||
"related_to",
|
||||
"based_on",
|
||||
"happens_on",
|
||||
"started_on",
|
||||
"ended_on",
|
||||
"responds_to",
|
||||
"struggles_with",
|
||||
"likes",
|
||||
"dislikes",
|
||||
"avoided_by",
|
||||
"goal_of",
|
||||
];
|
||||
|
||||
impl GraphMemory {
|
||||
pub fn new() -> Self {
|
||||
GraphMemory { entities: vec![] }
|
||||
}
|
||||
/// graph_memory.NODE_TYPES — unknown node types fall back to "concept".
|
||||
const NODE_TYPES: [&str; 10] = [
|
||||
"person",
|
||||
"project",
|
||||
"concept",
|
||||
"tool",
|
||||
"preference",
|
||||
"organization",
|
||||
"place",
|
||||
"event",
|
||||
"skill",
|
||||
"artefact",
|
||||
];
|
||||
|
||||
pub fn add_entity(&mut self, name: &str, entity_type: &str, summary: &str) {
|
||||
self.entities.push(Entity {
|
||||
name: name.to_string(),
|
||||
entity_type: entity_type.to_string(),
|
||||
summary: summary.to_string(),
|
||||
});
|
||||
}
|
||||
/// add_edge's evidence cap — facts carry at most 400 chars of evidence.
|
||||
const EVIDENCE_CAP: usize = 400;
|
||||
|
||||
pub fn find(&self, name: &str) -> Option<&Entity> {
|
||||
self.entities.iter().find(|e| e.name == name)
|
||||
/// stats' predicate leaderboard cap.
|
||||
const PRED_LEADERBOARD: usize = 8;
|
||||
|
||||
/// The confidence ceiling (strength 5 saturates at 0.99, never 1.0).
|
||||
const CONF_CEILING: f64 = 0.99;
|
||||
|
||||
/// The durability threshold — curator strengths below 3 never touch the graph.
|
||||
const DURABILITY_FLOOR: f64 = 3.0;
|
||||
|
||||
/// Python str() over the JSON value space.
|
||||
fn py_str(v: Option<&Value>) -> String {
|
||||
match v {
|
||||
None | Some(Value::Null) => String::new(),
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(Value::Bool(true)) => "True".into(),
|
||||
Some(Value::Bool(false)) => "False".into(),
|
||||
Some(other) => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GraphMemory {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
/// Python float() coercion: strings parse, bools and nulls refuse, numbers
|
||||
/// pass through (bool is NOT excluded here — the oracle's float() accepts
|
||||
/// True as 1.0; only int/float/str-shaped JSON reaches the kernel).
|
||||
fn py_float(v: Option<&Value>) -> Option<f64> {
|
||||
match v {
|
||||
Some(Value::Number(n)) => n.as_f64(),
|
||||
Some(Value::String(s)) => s.trim().parse::<f64>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Truncate an ISO-8601 timestamp to seconds precision (the oracle's
|
||||
/// `timespec="seconds"` shape — the fractional part is dropped, never
|
||||
/// rounded).
|
||||
fn truncate_to_seconds(now: &str) -> String {
|
||||
match now.find('.') {
|
||||
Some(dot) => {
|
||||
let tail = &now[dot..];
|
||||
let tz_start = tail.find(['+', '-', 'Z']).map(|i| dot + i);
|
||||
match tz_start {
|
||||
Some(t) => format!("{}{}", &now[..dot], &now[t..]),
|
||||
None => now[..dot].to_string(),
|
||||
}
|
||||
}
|
||||
None => now.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// now_iso's kernel: seconds-precision truncation of the caller's timestamp.
|
||||
pub fn now_iso(now: &str) -> String {
|
||||
truncate_to_seconds(now)
|
||||
}
|
||||
|
||||
/// strength_to_conf: the write-time gating kernel. Below-durability
|
||||
/// strengths refuse the write (None); survivors map 0.4+0.12*s capped at
|
||||
/// 0.99 (3->0.76, 4->0.88, 5->0.99).
|
||||
pub fn strength_to_conf(strength: Option<&Value>) -> Option<f64> {
|
||||
let s = py_float(strength)?;
|
||||
if s < DURABILITY_FLOOR {
|
||||
return None;
|
||||
}
|
||||
Some(CONF_CEILING.min(0.4 + 0.12 * s))
|
||||
}
|
||||
|
||||
/// add_edge's predicate gate: strip, lowercase, spaces to underscores;
|
||||
/// unknown predicates refuse the write.
|
||||
pub fn normalize_pred(pred: &str) -> Option<String> {
|
||||
let p = pred.trim().to_lowercase().replace(' ', "_");
|
||||
if PREDICATES.contains(&p.as_str()) {
|
||||
Some(p)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// resolve_entity's type decision: vocabulary members pass, anything else
|
||||
/// (including empty) falls back to "concept".
|
||||
pub fn node_type(node_type: &str) -> String {
|
||||
if NODE_TYPES.contains(&node_type) {
|
||||
node_type.to_string()
|
||||
} else {
|
||||
"concept".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Python str.strip() over the evidence plus the 400-char cap. Char-based
|
||||
/// (the oracle slices codepoints, never bytes).
|
||||
pub fn trim_evidence(evidence: &str) -> String {
|
||||
evidence.trim().chars().take(EVIDENCE_CAP).collect()
|
||||
}
|
||||
|
||||
/// add_edge's duplicate-vs-supersede decision. A re-add with same-or-weaker
|
||||
/// confidence AND identical (stripped, capped) evidence is a duplicate (no
|
||||
/// write); anything stronger or differently-evidenced supersedes.
|
||||
pub fn supersede_decision(
|
||||
old_conf: f64,
|
||||
old_evidence: &str,
|
||||
new_conf: f64,
|
||||
new_evidence: &str,
|
||||
) -> &'static str {
|
||||
if new_conf <= old_conf && trim_evidence(new_evidence) == old_evidence.trim() {
|
||||
"duplicate"
|
||||
} else {
|
||||
"supersede"
|
||||
}
|
||||
}
|
||||
|
||||
/// query's fact projection: the eight keys the oracle's BFS and incoming
|
||||
/// scans emit per edge (evidence is deliberately NOT carried — the oracle
|
||||
/// drops it at the projection).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn query_fact(
|
||||
sname: &str,
|
||||
pred: &str,
|
||||
oname: &str,
|
||||
confidence: Option<f64>,
|
||||
valid_from: Option<&str>,
|
||||
valid_until: Option<&str>,
|
||||
status: &str,
|
||||
source: &str,
|
||||
) -> Value {
|
||||
json!({
|
||||
"subject": sname,
|
||||
"predicate": pred,
|
||||
"object": oname,
|
||||
"confidence": confidence,
|
||||
"from": valid_from,
|
||||
"until": valid_until,
|
||||
"status": status,
|
||||
"source": source,
|
||||
})
|
||||
}
|
||||
|
||||
/// stats' projection: counts plus the top-8 predicate leaderboard.
|
||||
pub fn stats_projection(
|
||||
entities: i64,
|
||||
aliases: i64,
|
||||
edges: i64,
|
||||
active: i64,
|
||||
by_pred: Vec<(String, i64)>,
|
||||
) -> Value {
|
||||
let mut preds = Map::new();
|
||||
for (pred, n) in by_pred.into_iter().take(PRED_LEADERBOARD) {
|
||||
preds.insert(pred, json!(n));
|
||||
}
|
||||
json!({
|
||||
"entities": entities,
|
||||
"aliases": aliases,
|
||||
"edges": edges,
|
||||
"active": active,
|
||||
"predicates": Value::Object(preds),
|
||||
})
|
||||
}
|
||||
|
||||
/// handle_hook's kernel: the default microkernel hook contract — every
|
||||
/// event is acknowledged with the default handler marker (plugins that
|
||||
/// need custom behavior override it on the Python side).
|
||||
pub fn handle_hook(event: &str) -> Value {
|
||||
json!({"status": "ok", "event": event, "handler": "default"})
|
||||
}
|
||||
|
||||
// ── FFI ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn f_arg(a: &Value, key: &str) -> f64 {
|
||||
a.get(key).and_then(|v| v.as_f64()).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn i_arg(a: &Value, key: &str) -> i64 {
|
||||
a.get(key).and_then(|v| v.as_i64()).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// gm_now_iso({now}) -> seconds-precision ISO-8601.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_now_iso(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| json!(now_iso(&py_str(a.get("now"))))))
|
||||
}
|
||||
|
||||
/// gm_strength_to_conf({strength}) -> {conf, ok}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_strength_to_conf(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| match strength_to_conf(a.get("strength")) {
|
||||
Some(c) => json!({"conf": c, "ok": true}),
|
||||
None => json!({"conf": 0.0, "ok": false}),
|
||||
}))
|
||||
}
|
||||
|
||||
/// gm_normalize_pred({pred}) -> {pred, ok}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_normalize_pred(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| match normalize_pred(&py_str(a.get("pred"))) {
|
||||
Some(p) => json!({"pred": p, "ok": true}),
|
||||
None => json!({"pred": "", "ok": false}),
|
||||
}))
|
||||
}
|
||||
|
||||
/// gm_node_type({node_type}) -> effective type.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_node_type(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| json!(node_type(&py_str(a.get("node_type"))))))
|
||||
}
|
||||
|
||||
/// gm_supersede({old_conf, old_evidence, new_conf, new_evidence}) ->
|
||||
/// "duplicate" | "supersede".
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_supersede(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
json!(supersede_decision(
|
||||
f_arg(&a, "old_conf"),
|
||||
&py_str(a.get("old_evidence")),
|
||||
f_arg(&a, "new_conf"),
|
||||
&py_str(a.get("new_evidence")),
|
||||
))
|
||||
}))
|
||||
}
|
||||
|
||||
/// gm_trim_evidence({evidence}) -> capped evidence.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_trim_evidence(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| json!(trim_evidence(&py_str(a.get("evidence"))))))
|
||||
}
|
||||
|
||||
/// gm_query_fact({sname, pred, oname, confidence, valid_from, valid_until,
|
||||
/// status, source}) -> the query-fact projection.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_query_fact(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
query_fact(
|
||||
&py_str(a.get("sname")),
|
||||
&py_str(a.get("pred")),
|
||||
&py_str(a.get("oname")),
|
||||
a.get("confidence").and_then(|v| v.as_f64()),
|
||||
a.get("valid_from").and_then(|v| v.as_str()),
|
||||
a.get("valid_until").and_then(|v| v.as_str()),
|
||||
&py_str(a.get("status")),
|
||||
&py_str(a.get("source")),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// gm_stats({entities, aliases, edges, active, by_pred}) -> stats projection.
|
||||
/// by_pred arrives pre-sorted (count DESC) — the oracle's GROUP BY + ORDER.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_stats(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let by_pred: Vec<(String, i64)> = match a.get("by_pred") {
|
||||
Some(Value::Array(rows)) => rows
|
||||
.iter()
|
||||
.map(|r| (py_str(r.get("pred")), r.get("n").and_then(|v| v.as_i64()).unwrap_or(0)))
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
stats_projection(
|
||||
i_arg(&a, "entities"),
|
||||
i_arg(&a, "aliases"),
|
||||
i_arg(&a, "edges"),
|
||||
i_arg(&a, "active"),
|
||||
by_pred,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// gm_handle_hook({event}) -> {"status", "event", "handler"}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_handle_hook(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| handle_hook(&py_str(a.get("event")))))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add_and_find() {
|
||||
let mut gm = GraphMemory::new();
|
||||
gm.add_entity("deployment", "system", "The deployment system");
|
||||
assert!(gm.find("deployment").is_some());
|
||||
assert!(gm.find("nonexistent").is_none());
|
||||
}
|
||||
}
|
||||
#[path = "branch_battery.rs"]
|
||||
mod branch_battery;
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
Reference in new issue
Block a user