Files
self-directed-learning/test/test_access_log.js
T
Krystie dc5dc94d79 Initial commit: Adaptive Recall sidecar for UMP (Phase 5)
Multi-channel retrieval sidecar over Universal Memory Protocol:
- 3-channel RRF (UMP FTS5 + Qdrant vector + knowledge graph)
- ACT-R re-ranking (Anderson 1983) with access tracking
- Co-occurrence graph edges (Phase 6) for dense traversal
- Memory lifecycle decay (Phase 4) with per-kind confidence
- MCP shim routes recall through sidecar, falls back to canonical UMP

Architecture:
- src/server.js      HTTP sidecar on port 4380
- src/graph.js       2592-node / 111-edge graph from UMP (or +cooccur: 13k+)
- src/actr.js        A_i = -d*ln(age) + beta*log1p(freq) + epsilon*conf
- src/access_log.js  per-URN counter + last_accessed_at
- src/ump-recall-mcp.js  MCP shim (recall via sidecar, others passthrough)

Eval results (851-record UMP corpus):
- 2ch RRF over baseline: +50pp recall@10
- 3ch RRF (+graph): +60pp, 12 unique wins
- ACT-R re-rank: 4/20 #1 changes, 84% top-5 retention

Tests: 76/76 passing across graph (27), actr (27), access_log (28),
decay (20), mcp-shim (sidecar + fallback). Run with: npm test

Inspired by AIAppsAPI/adaptive-recall but built from scratch against
existing DNS2 infrastructure (UMP at :4317, Qdrant at :6333,
Ollama at :11434). No paid SaaS, MIT-licensed.
2026-07-12 19:24:47 -07:00

156 lines
4.8 KiB
JavaScript

// Phase 5 tests — access tracking.
// Run: node test/test_access_log.js
import { promises as fs } from "node:fs";
import path from "node:path";
import { AccessLog } from "../src/access_log.js";
let passed = 0;
let failed = 0;
function assert(name, ok, extra = "") {
if (ok) { passed++; console.log(`PASS ${name}`); }
else { failed++; console.log(`FAIL ${name}${extra ? ` (${extra})` : ""}`); }
}
const TMP = `/tmp/access_log_test_${process.pid}.json`;
async function fresh() {
// wipe file
try { await fs.unlink(TMP); } catch {}
// simple: pass filePath directly to constructor
return new AccessLog(TMP);
}
// ---- constructor ----
{
const al = await fresh();
assert("new AccessLog: empty map", al.byUrn.size === 0);
assert("new AccessLog: not loaded", al._loaded === false);
assert("new AccessLog: schema_version=1", al.meta.schema_version === 1);
}
// ---- bump ----
{
const al = await fresh();
const r1 = al.bump("urn:ump:a");
assert("bump new urn: returns object", r1 && r1.count === 1);
assert("bump new urn: last_accessed_at is ISO", typeof r1.last_accessed_at === "string" && r1.last_accessed_at.endsWith("Z"));
const r2 = al.bump("urn:ump:a");
assert("bump same urn: count++", r2.count === 2, `got ${r2.count}`);
const r3 = al.bump("urn:ump:b");
assert("bump different urn: separate counter", r3.count === 1 && al.byUrn.size === 2);
}
{
const al = await fresh();
const r = al.bump(null);
assert("bump null: returns null", r === null);
const r2 = al.bump(undefined);
assert("bump undefined: returns null", r2 === null);
const r3 = al.bump("");
assert("bump empty string: returns null", r3 === null);
}
// ---- bumpMany ----
{
const al = await fresh();
const results = al.bumpMany(["urn:ump:a", "urn:ump:b", "urn:ump:c", null, ""]);
assert("bumpMany: skips null/empty entries",
Object.keys(results).length === 3);
al.bumpMany(["urn:ump:a", "urn:ump:a"]);
assert("bumpMany: cumulative counts",
al.byUrn.get("urn:ump:a").count === 3, `got ${al.byUrn.get("urn:ump:a").count}`);
}
// ---- get ----
{
const al = await fresh();
al.bump("urn:ump:x");
const got = al.get("urn:ump:x");
assert("get: returns copy", got && got.count === 1);
// Mutating returned object shouldn't affect internal state
got.count = 999;
assert("get: returns shallow copy",
al.byUrn.get("urn:ump:x").count === 1,
`internal was mutated`);
assert("get: null urn → null", al.get(null) === null);
assert("get: unknown urn → null", al.get("urn:ump:never-bumped") === null);
}
// ---- snapshot ----
{
const al = await fresh();
al.bump("urn:ump:a");
al.bump("urn:ump:b");
al.bump("urn:ump:b");
const snap = al.snapshot();
assert("snapshot: includes all URNs",
Object.keys(snap).length === 2);
assert("snapshot: counts preserved",
snap["urn:ump:a"].count === 1 && snap["urn:ump:b"].count === 2);
}
// ---- load ----
{
// Write a log file manually, then load
const al1 = await fresh();
al1.bump("urn:ump:existing");
al1.bump("urn:ump:existing");
await al1.flush();
const al2 = new AccessLog(TMP);
await al2.load();
assert("load: existing data loaded",
al2.byUrn.has("urn:ump:existing") && al2.byUrn.get("urn:ump:existing").count === 2);
}
{
// Load from non-existent file → no error, empty map
const al = new AccessLog("/tmp/does-not-exist-" + Date.now() + ".json");
await al.load();
assert("load: missing file → empty (no error)", al.byUrn.size === 0);
}
// ---- flush + persistence roundtrip ----
{
const al = await fresh();
al.bump("urn:ump:flush-test");
al.bump("urn:ump:flush-test");
al.bump("urn:ump:flush-test");
await al.flush();
// File exists
const raw = await fs.readFile(TMP, "utf8");
const data = JSON.parse(raw);
assert("flush: file written",
data.by_urn && data.by_urn["urn:ump:flush-test"].count === 3);
assert("flush: schema_version present", data.schema_version === 1);
assert("flush: meta.total_writes incremented", data.meta.total_writes >= 1);
// No .tmp left behind
const tmpFiles = (await fs.readdir(path.dirname(TMP))).filter(f => f.includes("access_log_test") && f.endsWith(".tmp-" + process.pid));
assert("flush: no .tmp file left behind", tmpFiles.length === 0, `found ${tmpFiles.join(",")}`);
}
// ---- stats ----
{
const al = await fresh();
al.bump("urn:ump:s1");
al.bump("urn:ump:s1");
al.bump("urn:ump:s2");
al.bump("urn:ump:s3");
const s = al.stats();
assert("stats: unique_urns=3", s.unique_urns === 3, `got ${s.unique_urns}`);
assert("stats: total_accesses=4", s.total_accesses === 4, `got ${s.total_accesses}`);
assert("stats: accessed_within_7d=3 (all just bumped)", s.accessed_within_7d === 3);
assert("stats: file path set", s.file && s.file.length > 0);
}
console.log(`\n${passed}/${passed + failed} passed`);
process.exit(failed > 0 ? 1 : 0);