feat(runtime/knowledge): SQLite knowledge base with starter intel + lessons

Per-server state/<host>/knowledge.db. Schema:
- recipes (seeded from docs/minecraft-recipes.json, 38 rows)
- mob_intel (15 mobs incl. creeper/zombie/skeleton with verdict_no_weapon)
- block_intel (30 blocks with required_tool / drops)
- lessons (12 starter rules — "don't attack creepers with fists", etc.)
- deaths / postmortems / poi / wiki_pages / chat_log / code_changes

Public API in runtime/knowledge/index.js: initKnowledge, recall, record,
markApplied, topAdvice, lookupRecipe/Mob/Block, insertDeath, insertPostmortem,
recordPOI, poiNearby, logChat. All operations gracefully no-op when
better-sqlite3 is unavailable.

10 tests; covers init, seeding, recall filters, lesson lifecycle, death/PM
round-trip, spatial POI queries, chat log.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 13:14:59 +03:00
co-authored by Claude Opus 4.7
parent b5954f193d
commit 3da10ce58c
8 changed files with 1277 additions and 4 deletions
+134
View File
@@ -0,0 +1,134 @@
// SQLite-backed knowledge store.
//
// Lazy-loads better-sqlite3 on first use so a fresh checkout without
// `npm install` still boots — the knowledge subsystem just goes into
// disabled mode and every public API becomes a safe no-op.
//
// Public API:
// await ensureStore({ stateDir }) → opens (or reopens) the DB
// getStore() → underlying Database handle or null
// isAvailable() → boolean
// closeStore()
// runMaintenance() → idempotent vacuum/analyze
//
// All schema is in schema.sql alongside this file. Apply happens once on
// first open; subsequent opens are no-ops.
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { readFileSync, mkdirSync, existsSync } from "node:fs";
import { info, warn, error as logError } from "../log.js";
const HERE = dirname(fileURLToPath(import.meta.url));
const SCHEMA_PATH = resolve(HERE, "schema.sql");
const CURRENT_SCHEMA_VERSION = 1;
let _db = null;
let _disabled = false;
let _disabledReason = null;
let _Database = null;
let _loadAttempted = false;
async function loadDriver() {
if (_Database) return _Database;
if (_loadAttempted) return null;
_loadAttempted = true;
try {
const mod = await import("better-sqlite3");
_Database = mod.default ?? mod;
return _Database;
} catch (e) {
_disabled = true;
_disabledReason = e?.code === "ERR_MODULE_NOT_FOUND"
? "better-sqlite3 not installed (run npm install)"
: `better-sqlite3 load failed: ${e?.message ?? e}`;
warn("knowledge", `${_disabledReason}; knowledge subsystem disabled`);
return null;
}
}
export function isAvailable() {
return !_disabled && _db !== null;
}
export function disabledReason() {
return _disabledReason;
}
export function getStore() {
return _db;
}
export async function ensureStore({ stateDir } = {}) {
if (_db) return _db;
if (_disabled) return null;
if (!stateDir) {
warn("knowledge", "ensureStore called without stateDir; ignoring");
return null;
}
const Database = await loadDriver();
if (!Database) return null;
try {
mkdirSync(stateDir, { recursive: true });
const dbPath = resolve(stateDir, "knowledge.db");
const isNew = !existsSync(dbPath);
_db = new Database(dbPath);
_db.pragma("journal_mode = WAL");
_db.pragma("synchronous = NORMAL");
_db.pragma("foreign_keys = ON");
const ddl = readFileSync(SCHEMA_PATH, "utf8");
_db.exec(ddl);
applyMigrations(_db);
info("knowledge", `store opened at ${dbPath}${isNew ? " (new)" : ""}`);
return _db;
} catch (e) {
_disabled = true;
_disabledReason = `store open failed: ${e?.message ?? e}`;
logError("knowledge", _disabledReason);
try { _db?.close(); } catch {}
_db = null;
return null;
}
}
function applyMigrations(db) {
const row = db
.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1")
.get();
const current = row?.version ?? 0;
if (current >= CURRENT_SCHEMA_VERSION) return;
// Migrations stack here when we cross schema versions in the future.
// For v1 the schema.sql already defines everything; just record the version.
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (?, ?)").run(
CURRENT_SCHEMA_VERSION,
Date.now(),
);
}
export function closeStore() {
if (!_db) return;
try {
_db.close();
} catch (e) {
warn("knowledge", `closeStore: ${e?.message ?? e}`);
}
_db = null;
}
export function runMaintenance() {
if (!isAvailable()) return;
try {
_db.exec("ANALYZE");
} catch (e) {
warn("knowledge", `maintenance failed: ${e?.message ?? e}`);
}
}
// Reset for tests. Not exported for runtime callers.
export function __resetForTests() {
closeStore();
_disabled = false;
_disabledReason = null;
_loadAttempted = false;
_Database = null;
}