import { Database } from "bun:sqlite"; import { existsSync, mkdirSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { inflateSync } from "node:zlib"; import type { EventRecord, ReleaseRecord, SaveRecord } from "../bucket/repo.ts"; import { fileFacts } from "../describe/describe.ts"; import { isTestPath } from "../describe/links.ts"; import { declaredVersion } from "../describe/version.ts"; // Koh 3's schema (§5.1, `src/storage/storage.v`) minus `file_cache`, which only a working tree needs, // plus `releases`: each version's release.json (§11.2) as read from the bucket, since that is bucket // data too and pages and feeds need it on every request, `hidden_files`, and what the Atlas reads. const SCHEMA_VERSION = 4; // The operator's hidden rows, moved out of `save_files` rather than deleted, so a changed hidden // list is applied in place, with nothing fetched again (2026-10-02). const HIDDEN_FILES = ` CREATE TABLE hidden_files ( save_id TEXT NOT NULL REFERENCES saves(id), path TEXT NOT NULL, hash TEXT NOT NULL REFERENCES objects(hash), executable INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (save_id, path) ); `; // What pages read, worked out as each save lands rather than on every view (2026-10-02): which // real saves touched each path (added, changed or removed against the real save before; `n` is the // touching save's rowid), what each file says about itself, and the objects real saves refer to. const DERIVED = ` CREATE TABLE known_objects (hash TEXT PRIMARY KEY) WITHOUT ROWID; CREATE TABLE touches ( path TEXT NOT NULL, n INTEGER NOT NULL ); CREATE INDEX touches_n ON touches(n); CREATE TABLE facts ( hash TEXT NOT NULL, path TEXT NOT NULL, lines INTEGER, description TEXT, outline TEXT, PRIMARY KEY (hash, path) ); `; const SCHEMA = ` CREATE TABLE objects ( hash TEXT PRIMARY KEY, size INTEGER NOT NULL, data BLOB NOT NULL ); CREATE TABLE saves ( id TEXT PRIMARY KEY, parent TEXT REFERENCES saves(id), message TEXT NOT NULL, created_at INTEGER NOT NULL, machine TEXT NOT NULL, is_checkpoint INTEGER NOT NULL DEFAULT 0, size_bytes INTEGER NOT NULL DEFAULT 0, agent TEXT, session TEXT, restores TEXT, replayed_from TEXT ); CREATE TABLE save_files ( save_id TEXT NOT NULL REFERENCES saves(id), path TEXT NOT NULL, hash TEXT NOT NULL REFERENCES objects(hash), executable INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (save_id, path) ); CREATE TABLE events ( id TEXT PRIMARY KEY, kind TEXT NOT NULL, save_id TEXT, path TEXT, reason TEXT, created_at INTEGER NOT NULL, machine TEXT NOT NULL ); CREATE TABLE state ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); CREATE TABLE releases ( version TEXT PRIMARY KEY, record TEXT NOT NULL ); CREATE INDEX saves_parent ON saves(parent); CREATE INDEX save_files_hash ON save_files(hash); ${HIDDEN_FILES} ${DERIVED}`; /** An object as the bucket stores it: zlib bytes, already checked against its hash. */ export type StoredObject = { compressed: Uint8Array; size: number }; /** One batch of a sync, landed in one transaction. Saves are oldest first. */ export type Landing = { head: string; releasesLatest: string; saves: SaveRecord[]; objects: Map; events: EventRecord[]; releases: ReleaseRecord[]; /** Escaped names of the paths these saves arrived without (§5.5, A3). */ quarantined: string[]; }; /** Marks a hidden entry as a name matched at any depth rather than a path from the root. */ export const ANY_DEPTH = "**/"; // No project declares its version in a file larger than this. const MAX_MANIFEST = 1024 * 1024; // A recount inflates every file at the newest save; a file this large is data, not lines of code. const MAX_COUNTED = 8 * 1024 * 1024; // Display names for the header's language, by extension; data and docs formats never count. const LANGUAGES: Record = { ts: "TypeScript", tsx: "TypeScript", js: "JavaScript", mjs: "JavaScript", zig: "Zig", v: "V", rs: "Rust", go: "Go", py: "Python", c: "C", h: "C", cpp: "C++", cc: "C++", hpp: "C++", swift: "Swift", kt: "Kotlin", java: "Java", rb: "Ruby", lua: "Lua", sh: "Shell", nix: "Nix", css: "CSS", html: "HTML", }; // Vendored code is someone else's: counting Koh 3's bundled SQLite made it read as 290k lines of C. const VENDORED = /(^|\/)(vendor|third_party|node_modules)\//; // Bumped when what the recount records changes, so every mirror recounts once when it opens. const STATS_RULE = "source-only, known objects, declared version (ASCII whitespace, BOM)"; const open = new Map(); /** Closes a repo's mirror, so a rename can move its file. */ export function closeMirror(dir: string, repo: string): void { const path = join(dir, `${repo}.db`); open.get(path)?.close(); open.delete(path); } /** * Opens every mirror in `dir`, bringing each to this Kepr's schema: run at start, before Kepr * listens, so no request waits on a migration. */ export function openMirrors(dir: string): void { if (!existsSync(dir)) return; for (const name of readdirSync(dir)) if (name.endsWith(".db")) openMirror(dir, name.slice(0, -3)); } /** Closes every mirror this thread holds open; the next use of each opens it again. */ export function closeMirrors(): void { for (const db of open.values()) db.close(); open.clear(); } /** The mirror of one repo at `/.db`, created on first use and kept open. */ export function openMirror(dir: string, repo: string): Database { const path = join(dir, `${repo}.db`); const cached = open.get(path); if (cached) return cached; mkdirSync(dir, { recursive: true }); const db = new Database(path, { strict: true }); db.run("PRAGMA journal_mode = WAL"); db.run("PRAGMA foreign_keys = ON"); db.run("PRAGMA busy_timeout = 5000"); // A current mirror needs no lock; only creating or migrating one takes the write lock. const found = schemaVersion(db) === SCHEMA_VERSION ? SCHEMA_VERSION : upgrade(db); if (found !== 0 && found !== 2 && found !== 3 && found !== SCHEMA_VERSION) { db.close(); throw new Error( `${path} has mirror schema version ${found}; this Kepr reads version ${SCHEMA_VERSION}. ` + "The mirror is disposable: delete it and the next sync rebuilds it from the bucket.", ); } // Mirrors counted under an older rule are recounted now, not at the next save. if (mirrorState(db, "head") && mirrorState(db, "stats_rule") !== STATS_RULE) refreshStats(db); open.set(path, db); return db; } function schemaVersion(db: Database): number { return db.query<{ user_version: number }, []>("PRAGMA user_version").get()?.user_version ?? 0; } /** * Creates or migrates a mirror and returns the version it found, read again under the write lock: * the request thread and the sync's both open mirrors, and must not both create or migrate one. */ function upgrade(db: Database): number { return db .transaction(() => { const found = schemaVersion(db); if (found === SCHEMA_VERSION || ![0, 2, 3].includes(found)) return found; if (found === 0) db.run(SCHEMA); if (found === 2) db.run(HIDDEN_FILES); if (found === 2 || found === 3) db.run(DERIVED); // Version 2 deleted hidden rows, so only the bucket can bring them back on unhide. if (found === 2 && mirrorState(db, "hidden")) clearMirror(db); else if (found !== 0) { recordTouches(db, 0); recordFacts(db, null); } db.run(`PRAGMA user_version = ${SCHEMA_VERSION}`); return found; }) .immediate(); } export function mirrorState(db: Database, key: string): string | null { const row = db .query<{ value: string }, [string]>("SELECT value FROM state WHERE key = ?") .get(key); return row?.value ?? null; } export function knownSave(db: Database, id: string): boolean { return db.query("SELECT 1 FROM saves WHERE id = ?").get(id) !== null; } export function knownObject(db: Database, hash: string): boolean { return db.query("SELECT 1 FROM objects WHERE hash = ?").get(hash) !== null; } export function knownEventIds(db: Database): Set { const rows = db.query<{ id: string }, []>("SELECT id FROM events").all(); return new Set(rows.map((row) => row.id)); } /** Drops a version deleted from the bucket; a sync only ever adds releases. */ export function forgetRelease(db: Database, version: string): void { db.query("DELETE FROM releases WHERE version = ?").run(version); } /** Replaces a version's record after Kepr rewrote it in the bucket. */ export function replaceRelease(db: Database, record: ReleaseRecord): void { db.query("UPDATE releases SET record = ? WHERE version = ?").run( JSON.stringify(record), record.version, ); } export function knownReleases(db: Database): Set { const rows = db.query<{ version: string }, []>("SELECT version FROM releases").all(); return new Set(rows.map((row) => row.version)); } /** Stores objects on their own, ahead of the save that refers to them. */ export function storeObjects(db: Database, objects: Map): void { const put = db.query("INSERT OR IGNORE INTO objects (hash, size, data) VALUES (?, ?, ?)"); db.transaction(() => { for (const [hash, object] of objects) put.run(hash, object.size, object.compressed); })(); } /** * Lands a sync like Koh's steal: objects, events, saves, then every strip over every save. The new * saves are hidden by the list the mirror holds; `applyHidden` changes that list. */ export function land(db: Database, landing: Landing): void { const putEvent = db.query( "INSERT OR IGNORE INTO events (id, kind, save_id, path, reason, created_at, machine) VALUES " + "(?, ?, ?, ?, ?, ?, ?)", ); const putSave = db.query( `INSERT OR IGNORE INTO saves (id, parent, message, created_at, machine, is_checkpoint, size_bytes, agent, session, restores, replayed_from) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ); const putFile = db.query( "INSERT OR IGNORE INTO save_files (save_id, path, hash, executable) VALUES (?, ?, ?, ?)", ); const putRelease = db.query("INSERT OR IGNORE INTO releases (version, record) VALUES (?, ?)"); const setState = db.query("INSERT OR REPLACE INTO state (key, value) VALUES (?, ?)"); // Immediate: a landing with nothing new reads before it writes, and the request thread writes // mirrors too (a deleted release). db.transaction(() => { storeObjects(db, landing.objects); for (const e of landing.events) { putEvent.run(e.id, e.kind, e.save_id, e.path, e.reason, e.created_at, e.machine); } for (const s of landing.saves) { putSave.run( s.id, s.parent === "" ? null : s.parent, s.message, s.created_at, s.machine, s.is_checkpoint ? 1 : 0, s.size_bytes, s.agent, s.session, s.restores, s.replayed_from, ); for (const f of s.files) putFile.run(s.id, f.path, f.hash, f.executable ? 1 : 0); } for (const r of landing.releases) putRelease.run(r.version, JSON.stringify(r)); const stripped = applyStrips(db); const landed = landing.saves.map((save) => save.id); hideRows(db, hiddenList(db), landed); // A strip can reach any save, so every save's touches are worked out again after one. recordTouches(db, stripped ? 0 : Number(mirrorState(db, "touches_to") ?? 0)); recordFacts(db, landed); setState.run("head", landing.head); setState.run("releases_latest", landing.releasesLatest); const quarantined = new Set([...quarantinedPaths(db), ...landing.quarantined]); setState.run("quarantined", JSON.stringify([...quarantined].sort())); staleStats(db); }).immediate(); // Outside the transaction: the recount can inflate megabytes and needs no lock to be correct. refreshStats(db); } /** * Makes `hidden` the mirror's hidden list: the operator's hidden paths (June, 2026-09-30) and the * names ignored in every repo (June, 2026-10-01), each as `ANY_DEPTH` + name. Rows move between * `save_files` and `hidden_files`, so it takes milliseconds and fetches nothing. */ export function applyHidden(db: Database, hidden: string[]): void { if (hiddenList(db).join("\n") === hidden.join("\n")) return; db.transaction(() => { db.run( `INSERT OR IGNORE INTO save_files (save_id, path, hash, executable) SELECT save_id, path, hash, executable FROM hidden_files`, ); db.run("DELETE FROM hidden_files"); hideRows(db, hidden, null); recordTouches(db, 0); recordFacts(db, null); staleStats(db); })(); refreshStats(db); } // Marks the counts out of date in the change's own transaction: if Kepr stops before the recount // that follows, the mirror's next open does it. function staleStats(db: Database): void { db.run("DELETE FROM state WHERE key = 'stats_rule'"); } /** * Records which real saves touched each path, for the saves after rowid `from`; 0 starts again. * A save touched a path when it was added, changed or removed against the real save before it; * checkpoints are skipped, so a change first recorded by a checkpoint counts at the next real save. */ function recordTouches(db: Database, from: number): void { if (from === 0) db.run("DELETE FROM touches"); db.query( `WITH line AS ( SELECT id, rowid AS n, LAG(id) OVER (ORDER BY rowid) AS prev FROM saves WHERE is_checkpoint = 0 ) INSERT INTO touches (path, n) SELECT f.path, line.n FROM line JOIN save_files f ON f.save_id = line.id LEFT JOIN save_files p ON p.save_id = line.prev AND p.path = f.path WHERE line.n > $from AND (p.hash IS NOT f.hash OR p.executable IS NOT f.executable) UNION ALL SELECT p.path, line.n FROM line JOIN save_files p ON p.save_id = line.prev WHERE line.n > $from AND NOT EXISTS (SELECT 1 FROM save_files f WHERE f.save_id = line.id AND f.path = p.path)`, ).run({ from }); const last = db.query<{ n: number | null }, []>("SELECT MAX(rowid) AS n FROM saves").get()?.n; db.query("INSERT OR REPLACE INTO state (key, value) VALUES ('touches_to', ?)").run( String(last ?? 0), ); } /** Records what each file of the given saves, or (null) every save, says about itself, once per version and path. */ function recordFacts(db: Database, saves: string[] | null): void { const wanted = db .query<{ hash: string; path: string }, { all: number; saves: string }>( `SELECT DISTINCT f.hash, f.path FROM save_files f WHERE ($all = 1 OR f.save_id IN (SELECT value FROM json_each($saves))) AND NOT EXISTS (SELECT 1 FROM facts x WHERE x.hash = f.hash AND x.path = f.path)`, ) .all({ all: saves === null ? 1 : 0, saves: JSON.stringify(saves ?? []) }); const sizeOf = db.query<{ size: number }, [string]>("SELECT size FROM objects WHERE hash = ?"); const dataOf = db.query<{ data: Uint8Array }, [string]>( "SELECT data FROM objects WHERE hash = ?", ); const put = db.query( "INSERT OR IGNORE INTO facts (hash, path, lines, description, outline) VALUES (?, ?, ?, ?, ?)", ); for (const { hash, path } of wanted) { const size = sizeOf.get(hash)?.size; if (size === undefined) continue; // The stored bytes are read only for a file small enough to describe. const read = () => inflateSync((dataOf.get(hash) as { data: Uint8Array }).data); const facts = fileFacts(path, size, read); put.run( hash, path, facts.lines, facts.description, facts.outline && JSON.stringify(facts.outline), ); } } function hiddenList(db: Database): string[] { return mirrorState(db, "hidden")?.split("\n").filter(Boolean) ?? []; } // Moves the rows `hidden` matches, in the `saves` given or (null) every save, into `hidden_files`. // The list is recorded in the same transaction, so the tarball key never names a list the rows // don't reflect yet. function hideRows(db: Database, hidden: string[], saves: string[] | null): void { for (const entry of hidden) { const { where, params } = pathMatch(entry); const scope = { ...params, all: saves === null ? 1 : 0, saves: JSON.stringify(saves ?? []) }; const chosen = `${where} AND ($all = 1 OR save_id IN (SELECT value FROM json_each($saves)))`; db.query( `INSERT OR IGNORE INTO hidden_files (save_id, path, hash, executable) SELECT save_id, path, hash, executable FROM save_files WHERE ${chosen}`, ).run(scope); db.query(`DELETE FROM save_files WHERE ${chosen}`).run(scope); } db.query("INSERT OR REPLACE INTO state (key, value) VALUES ('hidden', ?)").run(hidden.join("\n")); } /** Escaped names of every path this mirror's saves arrived without. */ export function quarantinedPaths(db: Database): string[] { return JSON.parse(mirrorState(db, "quarantined") ?? "[]"); } // §10.2: a strip of one save reaches that save; a strip of every save bans its path from all of // history, whenever a save was made or arrives. Re-applying every strip is harmless, which is how a // save that arrives after its strip event is covered too. // Returns whether any save lost a file. function applyStrips(db: Database): boolean { const strips = db .query<{ save_id: string; path: string }, []>( "SELECT save_id, path FROM events WHERE kind = 'strip'", ) .all(); if (strips.length === 0) return false; let removed = 0; for (const strip of strips) { const { where, params } = pathMatch(strip.path); // Hidden rows too, so unhiding never brings back what a strip removed. for (const table of ["save_files", "hidden_files"]) { removed += db .query(`DELETE FROM ${table} WHERE ${where} AND ($save = '' OR save_id = $save)`) .run({ ...params, save: strip.save_id }).changes; } } db.run( `DELETE FROM objects WHERE hash NOT IN (SELECT hash FROM save_files) AND hash NOT IN (SELECT hash FROM hidden_files)`, ); db.run("DELETE FROM facts WHERE hash NOT IN (SELECT hash FROM objects)"); return removed > 0; } /** The SQL condition matching a path, file or folder, or an `ANY_DEPTH` name at any depth. */ function pathMatch(path: string): { where: string; params: Record } { if (path.startsWith(ANY_DEPTH)) { return { where: "instr('/' || path || '/', $segment) > 0", params: { segment: `/${path.slice(ANY_DEPTH.length)}/` }, }; } // Koh compares the prefix exactly, never with LIKE, so `_` and `%` in a path match only themselves. return { where: "(path = $path OR substr(path, 1, length($dir)) = $dir)", params: { path, dir: `${path.replace(/\/+$/, "")}/` }, }; } /** * Empties a mirror in place so the next sync rebuilds it from the bucket, as the move to schema * version 3 needs when hidden rows were deleted. The file and its open handles stay, so a request holding one reads an empty * repo for a moment rather than failing. A new epoch keeps tarballs kept before it from matching. */ export function clearMirror(db: Database): void { db.transaction(() => { // Children before parents, for the foreign keys; every name is a table of SCHEMA's. const tables = [ "save_files", "hidden_files", "touches", "facts", "known_objects", "events", "saves", "objects", "releases", "state", ]; for (const table of tables) { db.run(`DELETE FROM ${table}`); } db.query("INSERT INTO state (key, value) VALUES ('epoch', ?)").run(crypto.randomUUID()); })(); } // A file at a save as text, if it is there and small enough to be a manifest. function smallFileText(db: Database, save: string, path: string): string | null { const row = db .query<{ size: number; data: Uint8Array }, [string, string]>( `SELECT o.size, o.data FROM save_files f JOIN objects o ON o.hash = f.hash WHERE f.save_id = ? AND f.path = ?`, ) .get(save, path); return row && row.size <= MAX_MANIFEST ? new TextDecoder("utf-8", { ignoreBOM: true }).decode(inflateSync(row.data)) : null; } // Lines and languages never change between syncs, so they are counted once here, not per view. They // come from the newest real save, not `head`: a checkpoint's files are not public history (§21.4). // Lines are source lines: files in a known language, tests left out; docs and data never count. // Read and written under one write lock: both threads recount, and a recount must never publish // what it read before another thread's newer one. function refreshStats(db: Database): void { db.transaction(() => recount(db)).immediate(); } function recount(db: Database): void { const save = db .query<{ id: string }, []>( "SELECT id FROM saves WHERE is_checkpoint = 0 ORDER BY rowid DESC LIMIT 1", ) .get()?.id ?? ""; const files = db .query<{ path: string; size: number; data: Uint8Array }, [string]>( "SELECT f.path, o.size, o.data FROM save_files f JOIN objects o ON o.hash = f.hash WHERE f.save_id = ?", ) .all(save); let lines = 0; const byLanguage = new Map(); for (const file of files) { const language = LANGUAGES[file.path.split(".").pop() ?? ""]; // Tests are not the program: counted, Koh 3's read as 18k lines of V where it has 11k. if (!language || VENDORED.test(file.path) || isTestPath(file.path)) continue; if (file.size > MAX_COUNTED) continue; const bytes = inflateSync(file.data); if (bytes.includes(0)) continue; let count = 0; for (const byte of bytes) if (byte === 10) count++; if (bytes.length > 0 && bytes[bytes.length - 1] !== 10) count++; lines += count; byLanguage.set(language, (byLanguage.get(language) ?? 0) + count); } const languages = [...byLanguage].sort((a, b) => b[1] - a[1]); const setState = db.query("INSERT OR REPLACE INTO state (key, value) VALUES (?, ?)"); setState.run("lines", String(lines)); setState.run("language", languages[0]?.[0] ?? ""); setState.run("languages", JSON.stringify(languages)); setState.run("stats_save", save); setState.run("declared_version", declaredVersion((path) => smallFileText(db, save, path)) ?? ""); // §21.1's "the library knows N things": the file contents any real save refers to, which the // front page unites across public repos. From `objects`, probing save_files_hash: a DISTINCT // over save_files reads saves × files rows. db.run("DELETE FROM known_objects"); db.run( `INSERT INTO known_objects (hash) SELECT o.hash FROM objects o WHERE EXISTS ( SELECT 1 FROM save_files f JOIN saves s ON s.id = f.save_id WHERE f.hash = o.hash AND s.is_checkpoint = 0 )`, ); // Last, with the recount it names, so one cut short is done again at the next open. setState.run("stats_rule", STATS_RULE); }