@titan-design/store-sqlite 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Henry Jewkes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @titan-design/store-sqlite
2
+
3
+ A kit of composable SQLite table factories, not a shared database. Brain, codewatch, and
4
+ the session miner each keep their own physical database and their own domain tables; what
5
+ they share is the boilerplate they had each hand-rolled and one grammar for linking across
6
+ domains.
7
+
8
+ Tier 0 of the titan-platform DAG. Depends on `better-sqlite3` (TP-4). Design in the
9
+ initiative's `sources/research-store-synthesis.md`.
10
+
11
+ ## Open and migrate
12
+
13
+ ```ts
14
+ import { openDatabase, runMigrations, kitMigration } from "@titan-design/store-sqlite";
15
+
16
+ const db = openDatabase("~/.local/state/thing/index.sqlite3"); // WAL + foreign keys on
17
+ runMigrations(db, [
18
+ kitMigration(1, { edge: true, watermark: true, spanFts: "notes", cacheBlob: true }),
19
+ { version: 2, up: (db) => db.exec("CREATE TABLE my_domain_table (...)") },
20
+ ]);
21
+ ```
22
+
23
+ Migrations are `{ version, up(db) }`, applied in order, each in its own transaction, and
24
+ recorded in a `_migration` table. `hasTable` and `hasColumn` make `ALTER` migrations
25
+ idempotent.
26
+
27
+ ## The primitives
28
+
29
+ Every factory takes an optional `name` so a store can host several instances or keep a
30
+ legacy table name. All DDL is `IF NOT EXISTS`.
31
+
32
+ | Factory | Table | Helper class | Use when |
33
+ |---|---|---|---|
34
+ | `edgeTableDdl` | interval bi-temporal edges: `source_ref`, `relation`, `target_ref`, `t_valid/t_invalid`, `t_created/t_expired`, `fact_id`, `confidence`, `attrs`; partial indexes on live rows | `EdgeTable`: `assert`, `expire`, `supersede`, `current`, `from`, `to` | the cross-domain graph; relation names are free strings, corrections expire rather than delete |
35
+ | `entityTableDdl` | interval entity keyed by ref, with `kind`, `name`, `parent_ref`, `attrs` | `EntityTable`: `upsert`, `get`, `expire`, `listByKind` | facts that change one at a time (memory, sessions) |
36
+ | `snapshotTableDdl` + `entitySnapTableDdl` | snapshot registry and snapshot-keyed entities (`PRIMARY KEY (snapshot_id, id)`) | none yet | a whole population re-indexed together (a code graph) |
37
+ | `cacheBlobTableDdl` | `(namespace, model, content_hash)` to a blob plus JSON meta | `CacheBlobTable`: `get`, `put`, `getOrCompute`, `count` | embeddings and summaries: pure functions of text, stored once |
38
+ | `spanFtsTablesDdl` | `<name>_span` locators plus a contentless FTS5 `<name>_fts` | `SpanFtsTables`: `index`, `search`, `purgeOwner`, `orphanRatio`, `clearIndex` | full-text search where the text lives elsewhere |
39
+ | `watermarkTableDdl` | per-source offset, prefix hash, size/mtime/content hash, status | `WatermarkTable`: `ensure`, `advance`, `rewind`, `markStatus`, `list` | incremental ingest of append-mostly sources |
40
+
41
+ The two time models are deliberately both here. Snapshot scoping is right when everything
42
+ is re-indexed at once; forcing it on live single-event ingestion breaks it. Interval rows
43
+ are right when facts change independently; forcing them on a snapshot graph writes an
44
+ unchanged row per node per snapshot. One physical table cannot serve both.
45
+
46
+ ## Contentless FTS rule
47
+
48
+ `SpanFtsTables.search` always joins the FTS table through the span table. A contentless
49
+ FTS5 table cannot delete a row without its original text, so purges strand FTS rows; the
50
+ join hides them, `orphanRatio` tells you when to `clearIndex` and re-stream.
51
+
52
+ ## Refs
53
+
54
+ `ref("session", "abc")` gives `session:abc`; `parseRef` splits on the first colon so ids
55
+ may contain colons and hashes (`codewatch:repo#src/a.ts::Foo`). `refKind("task")` returns
56
+ a typed minter. Each domain mints refs for its own entities; the edge table links them.
@@ -0,0 +1,303 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ type Db = Database.Database;
4
+ interface OpenOptions {
5
+ readonly?: boolean;
6
+ /** WAL keeps readers from blocking a writer. On by default for file databases. */
7
+ wal?: boolean;
8
+ /** SQLite leaves foreign keys off per connection; the kit turns them on. */
9
+ foreignKeys?: boolean;
10
+ }
11
+ /** Open (creating if absent) a database with the kit's pragmas. `:memory:` works for tests. */
12
+ declare function openDatabase(dbPath: string, options?: OpenOptions): Db;
13
+ /** ISO-8601 with milliseconds, the timestamp shape every kit table stores. */
14
+ declare function nowIso(): string;
15
+ /** SQL expression producing the same shape as `nowIso()` inside the database. */
16
+ declare const SQL_NOW = "strftime('%Y-%m-%dT%H:%M:%fZ','now')";
17
+ declare function hasTable(db: Db, table: string): boolean;
18
+ declare function hasColumn(db: Db, table: string, column: string): boolean;
19
+ /** Double-quote an identifier so table factories can take caller-chosen names safely. */
20
+ declare function quoteIdent(name: string): string;
21
+
22
+ interface Migration {
23
+ /** Positive, unique, applied in ascending order. */
24
+ version: number;
25
+ name?: string;
26
+ up: (db: Db) => void;
27
+ }
28
+ declare function appliedVersions(db: Db): number[];
29
+ /**
30
+ * Apply every migration not yet recorded in `_migration`, each in its own
31
+ * transaction so a failure leaves the database at the last good version.
32
+ * Returns the versions applied this call.
33
+ */
34
+ declare function runMigrations(db: Db, migrations: readonly Migration[]): number[];
35
+
36
+ /**
37
+ * The cross-domain identity grammar: `<kind>:<id>`, where `kind` is a short
38
+ * lowercase token and `id` is anything the domain needs (paths, `repo#node`,
39
+ * numbers). Every domain mints refs for its own entities and the shared edge
40
+ * table links them, so `session:abc` can point at `codewatch:repo#src/a.ts::Foo`
41
+ * without either side knowing the other's schema.
42
+ */
43
+ interface ParsedRef {
44
+ kind: string;
45
+ id: string;
46
+ }
47
+ declare function ref(kind: string, id: string): string;
48
+ declare function parseRef(value: string): ParsedRef;
49
+ declare function isRef(value: unknown): value is string;
50
+ /** A typed minter for one kind: `const session = refKind("session"); session("abc")`. */
51
+ declare function refKind(kind: string): (id: string) => string;
52
+
53
+ /** Which kit tables a store wants, by name override or `true` for the default name. */
54
+ interface KitSelection {
55
+ snapshot?: string | true;
56
+ entitySnap?: string | true;
57
+ entity?: string | true;
58
+ edge?: string | true;
59
+ cacheBlob?: string | true;
60
+ spanFts?: string | true;
61
+ watermark?: string | true;
62
+ }
63
+ /** The DDL for a selection of kit tables, in dependency-safe order. Every statement is `IF NOT EXISTS`. */
64
+ declare function kitDdl(selection: KitSelection): string;
65
+ /** A migration that installs the selected kit tables, for use as a store's version 1. */
66
+ declare function kitMigration(version: number, selection: KitSelection, name?: string): Migration;
67
+
68
+ /**
69
+ * Interval bi-temporal edge table: `t_valid`/`t_invalid` is when the relation
70
+ * held in the world, `t_created`/`t_expired` is when this row was believed.
71
+ * Corrections expire a row and insert a replacement, never delete, so history
72
+ * stays auditable while partial indexes keep live lookups cheap. This is the
73
+ * cross-domain outer graph; relation names are free strings by design.
74
+ */
75
+ interface EdgeTableOptions {
76
+ name?: string;
77
+ }
78
+ declare function edgeTableDdl({ name }?: EdgeTableOptions): string;
79
+ interface EdgeInput {
80
+ sourceRef: string;
81
+ relation: string;
82
+ targetRef: string;
83
+ tValid?: string;
84
+ tInvalid?: string | null;
85
+ factId?: number | null;
86
+ confidence?: number;
87
+ attrs?: Record<string, unknown> | null;
88
+ }
89
+ interface EdgeRow {
90
+ edgeId: number;
91
+ sourceRef: string;
92
+ relation: string;
93
+ targetRef: string;
94
+ tValid: string;
95
+ tInvalid: string | null;
96
+ tCreated: string;
97
+ tExpired: string | null;
98
+ factId: number | null;
99
+ confidence: number;
100
+ attrs: Record<string, unknown> | null;
101
+ }
102
+ /** Prepared helpers over one edge table. Construct once per connection. */
103
+ declare class EdgeTable {
104
+ private readonly insertStmt;
105
+ private readonly expireStmt;
106
+ private readonly currentStmt;
107
+ private readonly fromStmt;
108
+ private readonly toStmt;
109
+ constructor(db: Db, { name }?: EdgeTableOptions);
110
+ /** Assert an edge. A live identical edge is left alone (idempotent re-ingest). Returns true when inserted. */
111
+ assert(edge: EdgeInput): boolean;
112
+ /** Retract the current belief in an edge (invalidate, never delete). Returns true when one was live. */
113
+ expire(sourceRef: string, relation: string, targetRef: string, at?: string): boolean;
114
+ /** Replace the live edge with a corrected one in a single step. */
115
+ supersede(edge: EdgeInput, at?: string): void;
116
+ current(sourceRef: string, relation: string, targetRef: string): EdgeRow | undefined;
117
+ from(sourceRef: string): EdgeRow[];
118
+ to(targetRef: string): EdgeRow[];
119
+ }
120
+
121
+ /**
122
+ * Two entity shapes, because the projects that converged on "kind + name +
123
+ * parent + JSON attrs" genuinely diverge on time. Snapshot-scoped rows are
124
+ * right when a whole population is re-indexed together (a code graph);
125
+ * interval rows are right when facts change one at a time (memory, sessions).
126
+ */
127
+ interface TableName {
128
+ name?: string;
129
+ }
130
+ /** Snapshot-scoped entity: every row belongs to one snapshot. */
131
+ declare function entitySnapTableDdl({ name }?: TableName): string;
132
+ /** The snapshot registry the snapshot-scoped tables key on. */
133
+ declare function snapshotTableDdl({ name }?: TableName): string;
134
+ /** Interval bi-temporal entity keyed by its cross-domain ref. */
135
+ declare function entityTableDdl({ name }?: TableName): string;
136
+ interface EntityInput {
137
+ ref: string;
138
+ kind: string;
139
+ name?: string | null;
140
+ parentRef?: string | null;
141
+ attrs?: Record<string, unknown> | null;
142
+ tValid?: string;
143
+ }
144
+ interface EntityRow {
145
+ ref: string;
146
+ kind: string;
147
+ name: string | null;
148
+ parentRef: string | null;
149
+ attrs: Record<string, unknown> | null;
150
+ tValid: string;
151
+ tInvalid: string | null;
152
+ tCreated: string;
153
+ tExpired: string | null;
154
+ }
155
+ /** Prepared helpers over one interval entity table. */
156
+ declare class EntityTable {
157
+ private readonly upsertStmt;
158
+ private readonly getStmt;
159
+ private readonly expireStmt;
160
+ private readonly byKindStmt;
161
+ constructor(db: Db, { name }?: TableName);
162
+ /** Insert or refresh the entity; a previously expired ref comes back live. */
163
+ upsert(entity: EntityInput): void;
164
+ get(ref: string): EntityRow | undefined;
165
+ expire(ref: string, at?: string): boolean;
166
+ listByKind(kind: string): EntityRow[];
167
+ }
168
+
169
+ /**
170
+ * Content-addressed cache for anything that is a pure function of some text
171
+ * and a model: embeddings, summaries, extracted structure. Keyed by
172
+ * `(namespace, model, content_hash)`, so unchanged inputs across re-indexes
173
+ * are cache hits rather than re-computation, and identical text is stored once.
174
+ */
175
+ interface CacheBlobOptions {
176
+ name?: string;
177
+ }
178
+ declare function cacheBlobTableDdl({ name }?: CacheBlobOptions): string;
179
+ declare function contentHashOf(text: string): string;
180
+ interface CacheKey {
181
+ namespace: string;
182
+ model: string;
183
+ /** Either the raw text (hashed for you) or a precomputed `contentHash`. */
184
+ text?: string;
185
+ contentHash?: string;
186
+ }
187
+ interface CacheHit {
188
+ value: Buffer;
189
+ meta: Record<string, unknown> | null;
190
+ }
191
+ declare class CacheBlobTable {
192
+ private readonly getStmt;
193
+ private readonly putStmt;
194
+ private readonly countStmt;
195
+ constructor(db: Db, { name }?: CacheBlobOptions);
196
+ get(key: CacheKey): CacheHit | undefined;
197
+ /** Store once; a second put for the same key is a no-op. Returns true when written. */
198
+ put(key: CacheKey, value: Buffer | string, meta?: Record<string, unknown>): boolean;
199
+ /** Get, or compute and store. The compute runs only on a miss. */
200
+ getOrCompute(key: CacheKey, compute: () => Buffer | string, meta?: Record<string, unknown>): Buffer;
201
+ count(namespace: string, model: string): number;
202
+ }
203
+
204
+ /**
205
+ * Full-text search without storing the text. `searchable_span` holds only a
206
+ * locator into the source (a byte range in a transcript, a file, a blob); the
207
+ * FTS5 table is genuinely contentless (`content=''`), so text is streamed in
208
+ * at index time and discarded. Reads MUST join through the span table: a
209
+ * contentless FTS5 table cannot delete rows without their original text, so
210
+ * purges strand FTS rows, and the join is what makes those orphans invisible.
211
+ */
212
+ interface SpanFtsOptions {
213
+ /** Base name; tables are `<name>_span` and `<name>_fts`. */
214
+ name?: string;
215
+ }
216
+ declare function spanFtsTablesDdl({ name }?: SpanFtsOptions): string;
217
+ interface SpanInput {
218
+ /** The entity this text belongs to, e.g. `session:abc`. */
219
+ ownerRef: string;
220
+ /** Which part of the owner, e.g. `prompt` or `tool_result`. */
221
+ field: string;
222
+ /** Transcript or file id the byte range is relative to. */
223
+ sourceId: number;
224
+ byteOffset: number;
225
+ byteLength: number;
226
+ }
227
+ interface SpanHit extends SpanInput {
228
+ spanId: number;
229
+ /** FTS5 bm25 rank; lower is better. */
230
+ rank: number;
231
+ }
232
+ declare class SpanFtsTables {
233
+ private readonly insertSpan;
234
+ private readonly findSpan;
235
+ private readonly insertFts;
236
+ private readonly searchStmt;
237
+ private readonly deleteAllFts;
238
+ private readonly deleteSpansStmt;
239
+ private readonly countSpans;
240
+ private readonly countFts;
241
+ constructor(db: Db, { name }?: SpanFtsOptions);
242
+ /** Index `text` for a span, storing only the locator. Re-indexing the same span is a no-op. Returns the span id. */
243
+ index(span: SpanInput, text: string): number;
244
+ /** FTS5 MATCH, joined through the span table so stranded FTS rows never surface. */
245
+ search(query: string, limit?: number): SpanHit[];
246
+ /** Drop an owner's spans. Their FTS rows become orphans until `rebuild`; the join hides them. */
247
+ purgeOwner(ownerRef: string): number;
248
+ /** Fraction of FTS rows with no span behind them; a high value means it is time to rebuild. */
249
+ orphanRatio(): number;
250
+ /** Empty the FTS index. The caller re-streams text through `index` for every surviving span. */
251
+ clearIndex(): void;
252
+ }
253
+
254
+ /**
255
+ * Incremental-ingest bookkeeping for any append-mostly source: where the reader
256
+ * stopped, a hash of what it already consumed (so a rewrite is detected rather
257
+ * than silently re-read from the wrong offset), and the size/mtime/content-hash
258
+ * triple that makes "source gone or altered" a reportable state.
259
+ */
260
+ type WatermarkStatus = "ok" | "missing" | "hash_mismatch" | "quarantined";
261
+ interface WatermarkOptions {
262
+ name?: string;
263
+ }
264
+ declare function watermarkTableDdl({ name }?: WatermarkOptions): string;
265
+ interface WatermarkRow {
266
+ sourceId: number;
267
+ sourceKey: string;
268
+ lastOffset: number;
269
+ prefixHash: string | null;
270
+ lastIndexedAt: string | null;
271
+ fileSize: number | null;
272
+ fileMtime: string | null;
273
+ contentHash: string | null;
274
+ status: WatermarkStatus;
275
+ statusReason: string | null;
276
+ }
277
+ interface WatermarkAdvance {
278
+ lastOffset: number;
279
+ prefixHash?: string | null;
280
+ fileSize?: number | null;
281
+ fileMtime?: string | null;
282
+ /** Null keeps any previously stored hash; computing it is O(file) and optional. */
283
+ contentHash?: string | null;
284
+ }
285
+ declare class WatermarkTable {
286
+ private readonly ensureStmt;
287
+ private readonly getStmt;
288
+ private readonly advanceStmt;
289
+ private readonly statusStmt;
290
+ private readonly rewindStmt;
291
+ private readonly listStmt;
292
+ constructor(db: Db, { name }?: WatermarkOptions);
293
+ /** The row for a source, created at offset 0 on first sight. The id is stable for the table's life. */
294
+ ensure(sourceKey: string): WatermarkRow;
295
+ get(sourceKey: string): WatermarkRow | undefined;
296
+ advance(sourceKey: string, update: WatermarkAdvance): void;
297
+ /** Back to byte 0, for a source that was rewritten rather than appended to. */
298
+ rewind(sourceKey: string): void;
299
+ markStatus(sourceKey: string, status: WatermarkStatus, reason?: string | null): void;
300
+ list(): WatermarkRow[];
301
+ }
302
+
303
+ export { type CacheBlobOptions, CacheBlobTable, type CacheHit, type CacheKey, type Db, type EdgeInput, type EdgeRow, EdgeTable, type EdgeTableOptions, type EntityInput, type EntityRow, EntityTable, type KitSelection, type Migration, type OpenOptions, type ParsedRef, SQL_NOW, type SpanFtsOptions, SpanFtsTables, type SpanHit, type SpanInput, type TableName, type WatermarkAdvance, type WatermarkOptions, type WatermarkRow, type WatermarkStatus, WatermarkTable, appliedVersions, cacheBlobTableDdl, contentHashOf, edgeTableDdl, entitySnapTableDdl, entityTableDdl, hasColumn, hasTable, isRef, kitDdl, kitMigration, nowIso, openDatabase, parseRef, quoteIdent, ref, refKind, runMigrations, snapshotTableDdl, spanFtsTablesDdl, watermarkTableDdl };
package/dist/index.js ADDED
@@ -0,0 +1,566 @@
1
+ // src/open.ts
2
+ import Database from "better-sqlite3";
3
+ import { mkdirSync } from "fs";
4
+ import path from "path";
5
+ function openDatabase(dbPath, options = {}) {
6
+ const inMemory = dbPath === ":memory:";
7
+ if (!inMemory && !options.readonly) mkdirSync(path.dirname(dbPath), { recursive: true });
8
+ const db = new Database(dbPath, { readonly: options.readonly ?? false });
9
+ if (!inMemory && !options.readonly && (options.wal ?? true)) db.pragma("journal_mode = WAL");
10
+ if (options.foreignKeys ?? true) db.pragma("foreign_keys = ON");
11
+ return db;
12
+ }
13
+ function nowIso() {
14
+ return (/* @__PURE__ */ new Date()).toISOString();
15
+ }
16
+ var SQL_NOW = "strftime('%Y-%m-%dT%H:%M:%fZ','now')";
17
+ function hasTable(db, table) {
18
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type IN ('table','view') AND name = ?").get(table);
19
+ return row !== void 0;
20
+ }
21
+ function hasColumn(db, table, column) {
22
+ const cols = db.prepare(`PRAGMA table_info(${quoteIdent(table)})`).all();
23
+ return cols.some((c) => c.name === column);
24
+ }
25
+ function quoteIdent(name) {
26
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`invalid SQL identifier: ${name}`);
27
+ return `"${name}"`;
28
+ }
29
+
30
+ // src/migrations.ts
31
+ var MIGRATION_TABLE = `
32
+ CREATE TABLE IF NOT EXISTS _migration (
33
+ version INTEGER PRIMARY KEY,
34
+ name TEXT,
35
+ applied_at TEXT NOT NULL
36
+ )`;
37
+ function appliedVersions(db) {
38
+ db.exec(MIGRATION_TABLE);
39
+ const rows = db.prepare("SELECT version FROM _migration ORDER BY version").all();
40
+ return rows.map((r) => r.version);
41
+ }
42
+ function runMigrations(db, migrations) {
43
+ assertWellFormed(migrations);
44
+ const applied = new Set(appliedVersions(db));
45
+ const record = db.prepare("INSERT INTO _migration (version, name, applied_at) VALUES (?, ?, ?)");
46
+ const applyOne = db.transaction((m) => {
47
+ m.up(db);
48
+ record.run(m.version, m.name ?? null, nowIso());
49
+ });
50
+ const done = [];
51
+ for (const m of [...migrations].sort((a, b) => a.version - b.version)) {
52
+ if (applied.has(m.version)) continue;
53
+ applyOne(m);
54
+ done.push(m.version);
55
+ }
56
+ return done;
57
+ }
58
+ function assertWellFormed(migrations) {
59
+ const seen = /* @__PURE__ */ new Set();
60
+ for (const m of migrations) {
61
+ if (!Number.isInteger(m.version) || m.version <= 0) throw new Error(`migration version must be a positive integer: ${m.version}`);
62
+ if (seen.has(m.version)) throw new Error(`duplicate migration version: ${m.version}`);
63
+ seen.add(m.version);
64
+ }
65
+ }
66
+
67
+ // src/ref.ts
68
+ var KIND = /^[a-z][a-z0-9_-]*$/;
69
+ function ref(kind, id) {
70
+ if (!KIND.test(kind)) throw new Error(`invalid ref kind: ${JSON.stringify(kind)}`);
71
+ if (id.length === 0) throw new Error("ref id must not be empty");
72
+ return `${kind}:${id}`;
73
+ }
74
+ function parseRef(value) {
75
+ const at = value.indexOf(":");
76
+ if (at <= 0 || at === value.length - 1 || !KIND.test(value.slice(0, at))) {
77
+ throw new Error(`invalid ref: ${JSON.stringify(value)}`);
78
+ }
79
+ return { kind: value.slice(0, at), id: value.slice(at + 1) };
80
+ }
81
+ function isRef(value) {
82
+ if (typeof value !== "string") return false;
83
+ try {
84
+ parseRef(value);
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+ function refKind(kind) {
91
+ ref(kind, "probe");
92
+ return (id) => ref(kind, id);
93
+ }
94
+
95
+ // src/tables/cache-blob.ts
96
+ import { createHash } from "crypto";
97
+ function cacheBlobTableDdl({ name = "cache_blob" } = {}) {
98
+ const t = quoteIdent(name);
99
+ return `
100
+ CREATE TABLE IF NOT EXISTS ${t} (
101
+ namespace TEXT NOT NULL,
102
+ model TEXT NOT NULL,
103
+ content_hash TEXT NOT NULL,
104
+ value BLOB NOT NULL,
105
+ meta TEXT,
106
+ created_at TEXT NOT NULL DEFAULT (${SQL_NOW}),
107
+ PRIMARY KEY (namespace, model, content_hash)
108
+ );
109
+ `;
110
+ }
111
+ function contentHashOf(text) {
112
+ return createHash("sha256").update(text, "utf8").digest("hex");
113
+ }
114
+ function hashFor(key) {
115
+ if (key.contentHash !== void 0) return key.contentHash;
116
+ if (key.text !== void 0) return contentHashOf(key.text);
117
+ throw new Error("cache key needs text or contentHash");
118
+ }
119
+ var CacheBlobTable = class {
120
+ getStmt;
121
+ putStmt;
122
+ countStmt;
123
+ constructor(db, { name = "cache_blob" } = {}) {
124
+ const t = quoteIdent(name);
125
+ this.getStmt = db.prepare(`SELECT value, meta FROM ${t} WHERE namespace = ? AND model = ? AND content_hash = ?`);
126
+ this.putStmt = db.prepare(
127
+ `INSERT INTO ${t} (namespace, model, content_hash, value, meta) VALUES (?, ?, ?, ?, ?)
128
+ ON CONFLICT (namespace, model, content_hash) DO NOTHING`
129
+ );
130
+ this.countStmt = db.prepare(`SELECT count(*) AS n FROM ${t} WHERE namespace = ? AND model = ?`);
131
+ }
132
+ get(key) {
133
+ const raw = this.getStmt.get(key.namespace, key.model, hashFor(key));
134
+ if (raw === void 0) return void 0;
135
+ return { value: raw.value, meta: raw.meta === null ? null : JSON.parse(raw.meta) };
136
+ }
137
+ /** Store once; a second put for the same key is a no-op. Returns true when written. */
138
+ put(key, value, meta) {
139
+ const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : value;
140
+ const info = this.putStmt.run(key.namespace, key.model, hashFor(key), bytes, meta === void 0 ? null : JSON.stringify(meta));
141
+ return info.changes === 1;
142
+ }
143
+ /** Get, or compute and store. The compute runs only on a miss. */
144
+ getOrCompute(key, compute, meta) {
145
+ const hit = this.get(key);
146
+ if (hit) return hit.value;
147
+ const value = compute();
148
+ this.put(key, value, meta);
149
+ return typeof value === "string" ? Buffer.from(value, "utf8") : value;
150
+ }
151
+ count(namespace, model) {
152
+ return this.countStmt.get(namespace, model).n;
153
+ }
154
+ };
155
+
156
+ // src/tables/edge.ts
157
+ function edgeTableDdl({ name = "edge" } = {}) {
158
+ const t = quoteIdent(name);
159
+ return `
160
+ CREATE TABLE IF NOT EXISTS ${t} (
161
+ edge_id INTEGER PRIMARY KEY,
162
+ source_ref TEXT NOT NULL,
163
+ relation TEXT NOT NULL,
164
+ target_ref TEXT NOT NULL,
165
+ t_valid TEXT NOT NULL,
166
+ t_invalid TEXT,
167
+ t_created TEXT NOT NULL DEFAULT (${SQL_NOW}),
168
+ t_expired TEXT,
169
+ fact_id INTEGER,
170
+ confidence REAL NOT NULL DEFAULT 1.0,
171
+ attrs TEXT
172
+ );
173
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_source`)} ON ${t}(source_ref, relation) WHERE t_expired IS NULL;
174
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_target`)} ON ${t}(target_ref, relation) WHERE t_expired IS NULL;
175
+ CREATE UNIQUE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_current`)} ON ${t}(source_ref, relation, target_ref) WHERE t_expired IS NULL;
176
+ `;
177
+ }
178
+ function toEdgeRow(raw) {
179
+ return {
180
+ edgeId: raw.edge_id,
181
+ sourceRef: raw.source_ref,
182
+ relation: raw.relation,
183
+ targetRef: raw.target_ref,
184
+ tValid: raw.t_valid,
185
+ tInvalid: raw.t_invalid,
186
+ tCreated: raw.t_created,
187
+ tExpired: raw.t_expired,
188
+ factId: raw.fact_id,
189
+ confidence: raw.confidence,
190
+ attrs: raw.attrs === null ? null : JSON.parse(raw.attrs)
191
+ };
192
+ }
193
+ var EdgeTable = class {
194
+ insertStmt;
195
+ expireStmt;
196
+ currentStmt;
197
+ fromStmt;
198
+ toStmt;
199
+ constructor(db, { name = "edge" } = {}) {
200
+ const t = quoteIdent(name);
201
+ this.insertStmt = db.prepare(
202
+ `INSERT INTO ${t} (source_ref, relation, target_ref, t_valid, t_invalid, fact_id, confidence, attrs)
203
+ VALUES (@sourceRef, @relation, @targetRef, @tValid, @tInvalid, @factId, @confidence, @attrs)
204
+ ON CONFLICT (source_ref, relation, target_ref) WHERE t_expired IS NULL DO NOTHING`
205
+ );
206
+ this.expireStmt = db.prepare(
207
+ `UPDATE ${t} SET t_expired = ? WHERE source_ref = ? AND relation = ? AND target_ref = ? AND t_expired IS NULL`
208
+ );
209
+ this.currentStmt = db.prepare(
210
+ `SELECT * FROM ${t} WHERE source_ref = ? AND relation = ? AND target_ref = ? AND t_expired IS NULL`
211
+ );
212
+ this.fromStmt = db.prepare(`SELECT * FROM ${t} WHERE source_ref = ? AND t_expired IS NULL ORDER BY edge_id`);
213
+ this.toStmt = db.prepare(`SELECT * FROM ${t} WHERE target_ref = ? AND t_expired IS NULL ORDER BY edge_id`);
214
+ }
215
+ /** Assert an edge. A live identical edge is left alone (idempotent re-ingest). Returns true when inserted. */
216
+ assert(edge) {
217
+ const info = this.insertStmt.run({
218
+ sourceRef: edge.sourceRef,
219
+ relation: edge.relation,
220
+ targetRef: edge.targetRef,
221
+ tValid: edge.tValid ?? nowIso(),
222
+ tInvalid: edge.tInvalid ?? null,
223
+ factId: edge.factId ?? null,
224
+ confidence: edge.confidence ?? 1,
225
+ attrs: edge.attrs == null ? null : JSON.stringify(edge.attrs)
226
+ });
227
+ return info.changes === 1;
228
+ }
229
+ /** Retract the current belief in an edge (invalidate, never delete). Returns true when one was live. */
230
+ expire(sourceRef, relation, targetRef, at = nowIso()) {
231
+ return this.expireStmt.run(at, sourceRef, relation, targetRef).changes === 1;
232
+ }
233
+ /** Replace the live edge with a corrected one in a single step. */
234
+ supersede(edge, at = nowIso()) {
235
+ this.expire(edge.sourceRef, edge.relation, edge.targetRef, at);
236
+ this.assert(edge);
237
+ }
238
+ current(sourceRef, relation, targetRef) {
239
+ const raw = this.currentStmt.get(sourceRef, relation, targetRef);
240
+ return raw === void 0 ? void 0 : toEdgeRow(raw);
241
+ }
242
+ from(sourceRef) {
243
+ return this.fromStmt.all(sourceRef).map(toEdgeRow);
244
+ }
245
+ to(targetRef) {
246
+ return this.toStmt.all(targetRef).map(toEdgeRow);
247
+ }
248
+ };
249
+
250
+ // src/tables/entity.ts
251
+ function entitySnapTableDdl({ name = "entity_snap" } = {}) {
252
+ const t = quoteIdent(name);
253
+ return `
254
+ CREATE TABLE IF NOT EXISTS ${t} (
255
+ snapshot_id INTEGER NOT NULL,
256
+ id TEXT NOT NULL,
257
+ kind TEXT NOT NULL,
258
+ name TEXT,
259
+ parent_id TEXT,
260
+ attrs TEXT,
261
+ PRIMARY KEY (snapshot_id, id)
262
+ );
263
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_kind`)} ON ${t}(snapshot_id, kind);
264
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_parent`)} ON ${t}(snapshot_id, parent_id);
265
+ `;
266
+ }
267
+ function snapshotTableDdl({ name = "snapshot" } = {}) {
268
+ const t = quoteIdent(name);
269
+ return `
270
+ CREATE TABLE IF NOT EXISTS ${t} (
271
+ id INTEGER PRIMARY KEY,
272
+ ref TEXT NOT NULL,
273
+ taken_at TEXT NOT NULL DEFAULT (${SQL_NOW}),
274
+ attrs TEXT
275
+ );
276
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_ref`)} ON ${t}(ref, taken_at);
277
+ `;
278
+ }
279
+ function entityTableDdl({ name = "entity" } = {}) {
280
+ const t = quoteIdent(name);
281
+ return `
282
+ CREATE TABLE IF NOT EXISTS ${t} (
283
+ ref TEXT PRIMARY KEY,
284
+ kind TEXT NOT NULL,
285
+ name TEXT,
286
+ parent_ref TEXT,
287
+ attrs TEXT,
288
+ t_valid TEXT NOT NULL,
289
+ t_invalid TEXT,
290
+ t_created TEXT NOT NULL DEFAULT (${SQL_NOW}),
291
+ t_expired TEXT
292
+ );
293
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_kind`)} ON ${t}(kind) WHERE t_expired IS NULL;
294
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_parent`)} ON ${t}(parent_ref) WHERE t_expired IS NULL;
295
+ `;
296
+ }
297
+ function toEntityRow(raw) {
298
+ return {
299
+ ref: raw.ref,
300
+ kind: raw.kind,
301
+ name: raw.name,
302
+ parentRef: raw.parent_ref,
303
+ attrs: raw.attrs === null ? null : JSON.parse(raw.attrs),
304
+ tValid: raw.t_valid,
305
+ tInvalid: raw.t_invalid,
306
+ tCreated: raw.t_created,
307
+ tExpired: raw.t_expired
308
+ };
309
+ }
310
+ var EntityTable = class {
311
+ upsertStmt;
312
+ getStmt;
313
+ expireStmt;
314
+ byKindStmt;
315
+ constructor(db, { name = "entity" } = {}) {
316
+ const t = quoteIdent(name);
317
+ this.upsertStmt = db.prepare(
318
+ `INSERT INTO ${t} (ref, kind, name, parent_ref, attrs, t_valid)
319
+ VALUES (@ref, @kind, @name, @parentRef, @attrs, @tValid)
320
+ ON CONFLICT (ref) DO UPDATE SET
321
+ kind = excluded.kind, name = excluded.name, parent_ref = excluded.parent_ref,
322
+ attrs = excluded.attrs, t_expired = NULL, t_invalid = NULL`
323
+ );
324
+ this.getStmt = db.prepare(`SELECT * FROM ${t} WHERE ref = ?`);
325
+ this.expireStmt = db.prepare(`UPDATE ${t} SET t_expired = ? WHERE ref = ? AND t_expired IS NULL`);
326
+ this.byKindStmt = db.prepare(`SELECT * FROM ${t} WHERE kind = ? AND t_expired IS NULL ORDER BY ref`);
327
+ }
328
+ /** Insert or refresh the entity; a previously expired ref comes back live. */
329
+ upsert(entity) {
330
+ this.upsertStmt.run({
331
+ ref: entity.ref,
332
+ kind: entity.kind,
333
+ name: entity.name ?? null,
334
+ parentRef: entity.parentRef ?? null,
335
+ attrs: entity.attrs == null ? null : JSON.stringify(entity.attrs),
336
+ tValid: entity.tValid ?? nowIso()
337
+ });
338
+ }
339
+ get(ref2) {
340
+ const raw = this.getStmt.get(ref2);
341
+ return raw === void 0 ? void 0 : toEntityRow(raw);
342
+ }
343
+ expire(ref2, at = nowIso()) {
344
+ return this.expireStmt.run(at, ref2).changes === 1;
345
+ }
346
+ listByKind(kind) {
347
+ return this.byKindStmt.all(kind).map(toEntityRow);
348
+ }
349
+ };
350
+
351
+ // src/tables/span-fts.ts
352
+ function names(name) {
353
+ return { span: quoteIdent(`${name}_span`), fts: quoteIdent(`${name}_fts`) };
354
+ }
355
+ function spanFtsTablesDdl({ name = "search" } = {}) {
356
+ const { span, fts } = names(name);
357
+ return `
358
+ CREATE TABLE IF NOT EXISTS ${span} (
359
+ span_id INTEGER PRIMARY KEY,
360
+ owner_ref TEXT NOT NULL,
361
+ field TEXT NOT NULL,
362
+ source_id INTEGER NOT NULL,
363
+ byte_offset INTEGER NOT NULL,
364
+ byte_length INTEGER NOT NULL,
365
+ UNIQUE (owner_ref, field, source_id, byte_offset)
366
+ );
367
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_span_owner`)} ON ${span}(owner_ref);
368
+ CREATE VIRTUAL TABLE IF NOT EXISTS ${fts} USING fts5(text, content='');
369
+ `;
370
+ }
371
+ var SpanFtsTables = class {
372
+ insertSpan;
373
+ findSpan;
374
+ insertFts;
375
+ searchStmt;
376
+ deleteAllFts;
377
+ deleteSpansStmt;
378
+ countSpans;
379
+ countFts;
380
+ constructor(db, { name = "search" } = {}) {
381
+ const { span, fts } = names(name);
382
+ this.insertSpan = db.prepare(
383
+ `INSERT INTO ${span} (owner_ref, field, source_id, byte_offset, byte_length) VALUES (?, ?, ?, ?, ?)
384
+ ON CONFLICT (owner_ref, field, source_id, byte_offset) DO NOTHING`
385
+ );
386
+ this.findSpan = db.prepare(
387
+ `SELECT span_id FROM ${span} WHERE owner_ref = ? AND field = ? AND source_id = ? AND byte_offset = ?`
388
+ );
389
+ this.insertFts = db.prepare(`INSERT INTO ${fts} (rowid, text) VALUES (?, ?)`);
390
+ this.searchStmt = db.prepare(
391
+ `SELECT s.span_id, s.owner_ref, s.field, s.source_id, s.byte_offset, s.byte_length, f.rank
392
+ FROM ${fts} f JOIN ${span} s ON s.span_id = f.rowid
393
+ WHERE ${fts} MATCH ? ORDER BY f.rank LIMIT ?`
394
+ );
395
+ this.deleteAllFts = db.prepare(`INSERT INTO ${fts}(${fts}) VALUES ('delete-all')`);
396
+ this.deleteSpansStmt = db.prepare(`DELETE FROM ${span} WHERE owner_ref = ?`);
397
+ this.countSpans = db.prepare(`SELECT count(*) AS n FROM ${span}`);
398
+ this.countFts = db.prepare(`SELECT count(*) AS n FROM ${fts}`);
399
+ }
400
+ /** Index `text` for a span, storing only the locator. Re-indexing the same span is a no-op. Returns the span id. */
401
+ index(span, text) {
402
+ const inserted = this.insertSpan.run(span.ownerRef, span.field, span.sourceId, span.byteOffset, span.byteLength);
403
+ const spanId = this.findSpan.get(span.ownerRef, span.field, span.sourceId, span.byteOffset).span_id;
404
+ if (inserted.changes === 1) this.insertFts.run(spanId, text);
405
+ return spanId;
406
+ }
407
+ /** FTS5 MATCH, joined through the span table so stranded FTS rows never surface. */
408
+ search(query, limit = 50) {
409
+ return this.searchStmt.all(query, limit).map((r) => ({
410
+ spanId: r.span_id,
411
+ ownerRef: r.owner_ref,
412
+ field: r.field,
413
+ sourceId: r.source_id,
414
+ byteOffset: r.byte_offset,
415
+ byteLength: r.byte_length,
416
+ rank: r.rank
417
+ }));
418
+ }
419
+ /** Drop an owner's spans. Their FTS rows become orphans until `rebuild`; the join hides them. */
420
+ purgeOwner(ownerRef) {
421
+ return this.deleteSpansStmt.run(ownerRef).changes;
422
+ }
423
+ /** Fraction of FTS rows with no span behind them; a high value means it is time to rebuild. */
424
+ orphanRatio() {
425
+ const fts = this.countFts.get().n;
426
+ if (fts === 0) return 0;
427
+ const spans = this.countSpans.get().n;
428
+ return Math.max(0, fts - spans) / fts;
429
+ }
430
+ /** Empty the FTS index. The caller re-streams text through `index` for every surviving span. */
431
+ clearIndex() {
432
+ this.deleteAllFts.run();
433
+ }
434
+ };
435
+
436
+ // src/tables/watermark.ts
437
+ function watermarkTableDdl({ name = "watermark" } = {}) {
438
+ const t = quoteIdent(name);
439
+ return `
440
+ CREATE TABLE IF NOT EXISTS ${t} (
441
+ source_id INTEGER PRIMARY KEY,
442
+ source_key TEXT NOT NULL UNIQUE,
443
+ last_offset INTEGER NOT NULL DEFAULT 0,
444
+ prefix_hash TEXT,
445
+ last_indexed_at TEXT,
446
+ file_size INTEGER,
447
+ file_mtime TEXT,
448
+ content_hash TEXT,
449
+ status TEXT NOT NULL DEFAULT 'ok',
450
+ status_reason TEXT,
451
+ created_at TEXT NOT NULL DEFAULT (${SQL_NOW})
452
+ );
453
+ `;
454
+ }
455
+ function toRow(raw) {
456
+ return {
457
+ sourceId: raw.source_id,
458
+ sourceKey: raw.source_key,
459
+ lastOffset: raw.last_offset,
460
+ prefixHash: raw.prefix_hash,
461
+ lastIndexedAt: raw.last_indexed_at,
462
+ fileSize: raw.file_size,
463
+ fileMtime: raw.file_mtime,
464
+ contentHash: raw.content_hash,
465
+ status: raw.status,
466
+ statusReason: raw.status_reason
467
+ };
468
+ }
469
+ var WatermarkTable = class {
470
+ ensureStmt;
471
+ getStmt;
472
+ advanceStmt;
473
+ statusStmt;
474
+ rewindStmt;
475
+ listStmt;
476
+ constructor(db, { name = "watermark" } = {}) {
477
+ const t = quoteIdent(name);
478
+ this.ensureStmt = db.prepare(`INSERT INTO ${t} (source_key) VALUES (?) ON CONFLICT (source_key) DO NOTHING`);
479
+ this.getStmt = db.prepare(`SELECT * FROM ${t} WHERE source_key = ?`);
480
+ this.advanceStmt = db.prepare(
481
+ `UPDATE ${t} SET last_offset = @lastOffset, prefix_hash = @prefixHash, last_indexed_at = ${SQL_NOW},
482
+ file_size = @fileSize, file_mtime = @fileMtime, content_hash = COALESCE(@contentHash, content_hash),
483
+ status = 'ok', status_reason = NULL
484
+ WHERE source_key = @sourceKey`
485
+ );
486
+ this.statusStmt = db.prepare(`UPDATE ${t} SET status = ?, status_reason = ? WHERE source_key = ?`);
487
+ this.rewindStmt = db.prepare(`UPDATE ${t} SET last_offset = 0, prefix_hash = NULL WHERE source_key = ?`);
488
+ this.listStmt = db.prepare(`SELECT * FROM ${t} ORDER BY source_id`);
489
+ }
490
+ /** The row for a source, created at offset 0 on first sight. The id is stable for the table's life. */
491
+ ensure(sourceKey) {
492
+ this.ensureStmt.run(sourceKey);
493
+ return toRow(this.getStmt.get(sourceKey));
494
+ }
495
+ get(sourceKey) {
496
+ const raw = this.getStmt.get(sourceKey);
497
+ return raw === void 0 ? void 0 : toRow(raw);
498
+ }
499
+ advance(sourceKey, update) {
500
+ this.advanceStmt.run({
501
+ sourceKey,
502
+ lastOffset: update.lastOffset,
503
+ prefixHash: update.prefixHash ?? null,
504
+ fileSize: update.fileSize ?? null,
505
+ fileMtime: update.fileMtime ?? null,
506
+ contentHash: update.contentHash ?? null
507
+ });
508
+ }
509
+ /** Back to byte 0, for a source that was rewritten rather than appended to. */
510
+ rewind(sourceKey) {
511
+ this.rewindStmt.run(sourceKey);
512
+ }
513
+ markStatus(sourceKey, status, reason = null) {
514
+ this.statusStmt.run(status, reason, sourceKey);
515
+ }
516
+ list() {
517
+ return this.listStmt.all().map(toRow);
518
+ }
519
+ };
520
+
521
+ // src/kit.ts
522
+ var FACTORIES = [
523
+ ["snapshot", snapshotTableDdl],
524
+ ["entitySnap", entitySnapTableDdl],
525
+ ["entity", entityTableDdl],
526
+ ["edge", edgeTableDdl],
527
+ ["cacheBlob", cacheBlobTableDdl],
528
+ ["spanFts", spanFtsTablesDdl],
529
+ ["watermark", watermarkTableDdl]
530
+ ];
531
+ function kitDdl(selection) {
532
+ return FACTORIES.filter(([key]) => selection[key] !== void 0).map(([key, factory]) => factory(selection[key] === true ? {} : { name: selection[key] })).join("\n");
533
+ }
534
+ function kitMigration(version, selection, name = "kit tables") {
535
+ return { version, name, up: (db) => db.exec(kitDdl(selection)) };
536
+ }
537
+ export {
538
+ CacheBlobTable,
539
+ EdgeTable,
540
+ EntityTable,
541
+ SQL_NOW,
542
+ SpanFtsTables,
543
+ WatermarkTable,
544
+ appliedVersions,
545
+ cacheBlobTableDdl,
546
+ contentHashOf,
547
+ edgeTableDdl,
548
+ entitySnapTableDdl,
549
+ entityTableDdl,
550
+ hasColumn,
551
+ hasTable,
552
+ isRef,
553
+ kitDdl,
554
+ kitMigration,
555
+ nowIso,
556
+ openDatabase,
557
+ parseRef,
558
+ quoteIdent,
559
+ ref,
560
+ refKind,
561
+ runMigrations,
562
+ snapshotTableDdl,
563
+ spanFtsTablesDdl,
564
+ watermarkTableDdl
565
+ };
566
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/open.ts","../src/migrations.ts","../src/ref.ts","../src/tables/cache-blob.ts","../src/tables/edge.ts","../src/tables/entity.ts","../src/tables/span-fts.ts","../src/tables/watermark.ts","../src/kit.ts"],"sourcesContent":["import Database from \"better-sqlite3\";\nimport { mkdirSync } from \"node:fs\";\nimport path from \"node:path\";\n\nexport type Db = Database.Database;\n\nexport interface OpenOptions {\n readonly?: boolean;\n /** WAL keeps readers from blocking a writer. On by default for file databases. */\n wal?: boolean;\n /** SQLite leaves foreign keys off per connection; the kit turns them on. */\n foreignKeys?: boolean;\n}\n\n/** Open (creating if absent) a database with the kit's pragmas. `:memory:` works for tests. */\nexport function openDatabase(dbPath: string, options: OpenOptions = {}): Db {\n const inMemory = dbPath === \":memory:\";\n if (!inMemory && !options.readonly) mkdirSync(path.dirname(dbPath), { recursive: true });\n const db = new Database(dbPath, { readonly: options.readonly ?? false });\n if (!inMemory && !options.readonly && (options.wal ?? true)) db.pragma(\"journal_mode = WAL\");\n if (options.foreignKeys ?? true) db.pragma(\"foreign_keys = ON\");\n return db;\n}\n\n/** ISO-8601 with milliseconds, the timestamp shape every kit table stores. */\nexport function nowIso(): string {\n return new Date().toISOString();\n}\n\n/** SQL expression producing the same shape as `nowIso()` inside the database. */\nexport const SQL_NOW = \"strftime('%Y-%m-%dT%H:%M:%fZ','now')\";\n\nexport function hasTable(db: Db, table: string): boolean {\n const row = db.prepare(\"SELECT 1 FROM sqlite_master WHERE type IN ('table','view') AND name = ?\").get(table);\n return row !== undefined;\n}\n\nexport function hasColumn(db: Db, table: string, column: string): boolean {\n const cols = db.prepare(`PRAGMA table_info(${quoteIdent(table)})`).all() as { name: string }[];\n return cols.some((c) => c.name === column);\n}\n\n/** Double-quote an identifier so table factories can take caller-chosen names safely. */\nexport function quoteIdent(name: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`invalid SQL identifier: ${name}`);\n return `\"${name}\"`;\n}\n","import type { Db } from \"./open.js\";\nimport { nowIso } from \"./open.js\";\n\nexport interface Migration {\n /** Positive, unique, applied in ascending order. */\n version: number;\n name?: string;\n up: (db: Db) => void;\n}\n\nconst MIGRATION_TABLE = `\n CREATE TABLE IF NOT EXISTS _migration (\n version INTEGER PRIMARY KEY,\n name TEXT,\n applied_at TEXT NOT NULL\n )`;\n\nexport function appliedVersions(db: Db): number[] {\n db.exec(MIGRATION_TABLE);\n const rows = db.prepare(\"SELECT version FROM _migration ORDER BY version\").all() as { version: number }[];\n return rows.map((r) => r.version);\n}\n\n/**\n * Apply every migration not yet recorded in `_migration`, each in its own\n * transaction so a failure leaves the database at the last good version.\n * Returns the versions applied this call.\n */\nexport function runMigrations(db: Db, migrations: readonly Migration[]): number[] {\n assertWellFormed(migrations);\n const applied = new Set(appliedVersions(db));\n const record = db.prepare(\"INSERT INTO _migration (version, name, applied_at) VALUES (?, ?, ?)\");\n const applyOne = db.transaction((m: Migration) => {\n m.up(db);\n record.run(m.version, m.name ?? null, nowIso());\n });\n const done: number[] = [];\n for (const m of [...migrations].sort((a, b) => a.version - b.version)) {\n if (applied.has(m.version)) continue;\n applyOne(m);\n done.push(m.version);\n }\n return done;\n}\n\nfunction assertWellFormed(migrations: readonly Migration[]): void {\n const seen = new Set<number>();\n for (const m of migrations) {\n if (!Number.isInteger(m.version) || m.version <= 0) throw new Error(`migration version must be a positive integer: ${m.version}`);\n if (seen.has(m.version)) throw new Error(`duplicate migration version: ${m.version}`);\n seen.add(m.version);\n }\n}\n","/**\n * The cross-domain identity grammar: `<kind>:<id>`, where `kind` is a short\n * lowercase token and `id` is anything the domain needs (paths, `repo#node`,\n * numbers). Every domain mints refs for its own entities and the shared edge\n * table links them, so `session:abc` can point at `codewatch:repo#src/a.ts::Foo`\n * without either side knowing the other's schema.\n */\nexport interface ParsedRef {\n kind: string;\n id: string;\n}\n\nconst KIND = /^[a-z][a-z0-9_-]*$/;\n\nexport function ref(kind: string, id: string): string {\n if (!KIND.test(kind)) throw new Error(`invalid ref kind: ${JSON.stringify(kind)}`);\n if (id.length === 0) throw new Error(\"ref id must not be empty\");\n return `${kind}:${id}`;\n}\n\nexport function parseRef(value: string): ParsedRef {\n const at = value.indexOf(\":\");\n if (at <= 0 || at === value.length - 1 || !KIND.test(value.slice(0, at))) {\n throw new Error(`invalid ref: ${JSON.stringify(value)}`);\n }\n return { kind: value.slice(0, at), id: value.slice(at + 1) };\n}\n\nexport function isRef(value: unknown): value is string {\n if (typeof value !== \"string\") return false;\n try {\n parseRef(value);\n return true;\n } catch {\n return false;\n }\n}\n\n/** A typed minter for one kind: `const session = refKind(\"session\"); session(\"abc\")`. */\nexport function refKind(kind: string): (id: string) => string {\n ref(kind, \"probe\");\n return (id) => ref(kind, id);\n}\n","import { createHash } from \"node:crypto\";\nimport { SQL_NOW, quoteIdent, type Db } from \"../open.js\";\n\n/**\n * Content-addressed cache for anything that is a pure function of some text\n * and a model: embeddings, summaries, extracted structure. Keyed by\n * `(namespace, model, content_hash)`, so unchanged inputs across re-indexes\n * are cache hits rather than re-computation, and identical text is stored once.\n */\nexport interface CacheBlobOptions {\n name?: string;\n}\n\nexport function cacheBlobTableDdl({ name = \"cache_blob\" }: CacheBlobOptions = {}): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n namespace TEXT NOT NULL,\n model TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n value BLOB NOT NULL,\n meta TEXT,\n created_at TEXT NOT NULL DEFAULT (${SQL_NOW}),\n PRIMARY KEY (namespace, model, content_hash)\n );\n `;\n}\n\nexport function contentHashOf(text: string): string {\n return createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\");\n}\n\nexport interface CacheKey {\n namespace: string;\n model: string;\n /** Either the raw text (hashed for you) or a precomputed `contentHash`. */\n text?: string;\n contentHash?: string;\n}\n\nexport interface CacheHit {\n value: Buffer;\n meta: Record<string, unknown> | null;\n}\n\nfunction hashFor(key: CacheKey): string {\n if (key.contentHash !== undefined) return key.contentHash;\n if (key.text !== undefined) return contentHashOf(key.text);\n throw new Error(\"cache key needs text or contentHash\");\n}\n\nexport class CacheBlobTable {\n private readonly getStmt;\n private readonly putStmt;\n private readonly countStmt;\n\n constructor(db: Db, { name = \"cache_blob\" }: CacheBlobOptions = {}) {\n const t = quoteIdent(name);\n this.getStmt = db.prepare(`SELECT value, meta FROM ${t} WHERE namespace = ? AND model = ? AND content_hash = ?`);\n this.putStmt = db.prepare(\n `INSERT INTO ${t} (namespace, model, content_hash, value, meta) VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (namespace, model, content_hash) DO NOTHING`,\n );\n this.countStmt = db.prepare(`SELECT count(*) AS n FROM ${t} WHERE namespace = ? AND model = ?`);\n }\n\n get(key: CacheKey): CacheHit | undefined {\n const raw = this.getStmt.get(key.namespace, key.model, hashFor(key)) as { value: Buffer; meta: string | null } | undefined;\n if (raw === undefined) return undefined;\n return { value: raw.value, meta: raw.meta === null ? null : (JSON.parse(raw.meta) as Record<string, unknown>) };\n }\n\n /** Store once; a second put for the same key is a no-op. Returns true when written. */\n put(key: CacheKey, value: Buffer | string, meta?: Record<string, unknown>): boolean {\n const bytes = typeof value === \"string\" ? Buffer.from(value, \"utf8\") : value;\n const info = this.putStmt.run(key.namespace, key.model, hashFor(key), bytes, meta === undefined ? null : JSON.stringify(meta));\n return info.changes === 1;\n }\n\n /** Get, or compute and store. The compute runs only on a miss. */\n getOrCompute(key: CacheKey, compute: () => Buffer | string, meta?: Record<string, unknown>): Buffer {\n const hit = this.get(key);\n if (hit) return hit.value;\n const value = compute();\n this.put(key, value, meta);\n return typeof value === \"string\" ? Buffer.from(value, \"utf8\") : value;\n }\n\n count(namespace: string, model: string): number {\n return (this.countStmt.get(namespace, model) as { n: number }).n;\n }\n}\n","import { SQL_NOW, nowIso, quoteIdent, type Db } from \"../open.js\";\n\n/**\n * Interval bi-temporal edge table: `t_valid`/`t_invalid` is when the relation\n * held in the world, `t_created`/`t_expired` is when this row was believed.\n * Corrections expire a row and insert a replacement, never delete, so history\n * stays auditable while partial indexes keep live lookups cheap. This is the\n * cross-domain outer graph; relation names are free strings by design.\n */\nexport interface EdgeTableOptions {\n name?: string;\n}\n\nexport function edgeTableDdl({ name = \"edge\" }: EdgeTableOptions = {}): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n edge_id INTEGER PRIMARY KEY,\n source_ref TEXT NOT NULL,\n relation TEXT NOT NULL,\n target_ref TEXT NOT NULL,\n t_valid TEXT NOT NULL,\n t_invalid TEXT,\n t_created TEXT NOT NULL DEFAULT (${SQL_NOW}),\n t_expired TEXT,\n fact_id INTEGER,\n confidence REAL NOT NULL DEFAULT 1.0,\n attrs TEXT\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_source`)} ON ${t}(source_ref, relation) WHERE t_expired IS NULL;\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_target`)} ON ${t}(target_ref, relation) WHERE t_expired IS NULL;\n CREATE UNIQUE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_current`)} ON ${t}(source_ref, relation, target_ref) WHERE t_expired IS NULL;\n `;\n}\n\nexport interface EdgeInput {\n sourceRef: string;\n relation: string;\n targetRef: string;\n tValid?: string;\n tInvalid?: string | null;\n factId?: number | null;\n confidence?: number;\n attrs?: Record<string, unknown> | null;\n}\n\nexport interface EdgeRow {\n edgeId: number;\n sourceRef: string;\n relation: string;\n targetRef: string;\n tValid: string;\n tInvalid: string | null;\n tCreated: string;\n tExpired: string | null;\n factId: number | null;\n confidence: number;\n attrs: Record<string, unknown> | null;\n}\n\ninterface RawEdgeRow {\n edge_id: number;\n source_ref: string;\n relation: string;\n target_ref: string;\n t_valid: string;\n t_invalid: string | null;\n t_created: string;\n t_expired: string | null;\n fact_id: number | null;\n confidence: number;\n attrs: string | null;\n}\n\nfunction toEdgeRow(raw: RawEdgeRow): EdgeRow {\n return {\n edgeId: raw.edge_id,\n sourceRef: raw.source_ref,\n relation: raw.relation,\n targetRef: raw.target_ref,\n tValid: raw.t_valid,\n tInvalid: raw.t_invalid,\n tCreated: raw.t_created,\n tExpired: raw.t_expired,\n factId: raw.fact_id,\n confidence: raw.confidence,\n attrs: raw.attrs === null ? null : (JSON.parse(raw.attrs) as Record<string, unknown>),\n };\n}\n\n/** Prepared helpers over one edge table. Construct once per connection. */\nexport class EdgeTable {\n private readonly insertStmt;\n private readonly expireStmt;\n private readonly currentStmt;\n private readonly fromStmt;\n private readonly toStmt;\n\n constructor(db: Db, { name = \"edge\" }: EdgeTableOptions = {}) {\n const t = quoteIdent(name);\n this.insertStmt = db.prepare(\n `INSERT INTO ${t} (source_ref, relation, target_ref, t_valid, t_invalid, fact_id, confidence, attrs)\n VALUES (@sourceRef, @relation, @targetRef, @tValid, @tInvalid, @factId, @confidence, @attrs)\n ON CONFLICT (source_ref, relation, target_ref) WHERE t_expired IS NULL DO NOTHING`,\n );\n this.expireStmt = db.prepare(\n `UPDATE ${t} SET t_expired = ? WHERE source_ref = ? AND relation = ? AND target_ref = ? AND t_expired IS NULL`,\n );\n this.currentStmt = db.prepare(\n `SELECT * FROM ${t} WHERE source_ref = ? AND relation = ? AND target_ref = ? AND t_expired IS NULL`,\n );\n this.fromStmt = db.prepare(`SELECT * FROM ${t} WHERE source_ref = ? AND t_expired IS NULL ORDER BY edge_id`);\n this.toStmt = db.prepare(`SELECT * FROM ${t} WHERE target_ref = ? AND t_expired IS NULL ORDER BY edge_id`);\n }\n\n /** Assert an edge. A live identical edge is left alone (idempotent re-ingest). Returns true when inserted. */\n assert(edge: EdgeInput): boolean {\n const info = this.insertStmt.run({\n sourceRef: edge.sourceRef,\n relation: edge.relation,\n targetRef: edge.targetRef,\n tValid: edge.tValid ?? nowIso(),\n tInvalid: edge.tInvalid ?? null,\n factId: edge.factId ?? null,\n confidence: edge.confidence ?? 1,\n attrs: edge.attrs == null ? null : JSON.stringify(edge.attrs),\n });\n return info.changes === 1;\n }\n\n /** Retract the current belief in an edge (invalidate, never delete). Returns true when one was live. */\n expire(sourceRef: string, relation: string, targetRef: string, at: string = nowIso()): boolean {\n return this.expireStmt.run(at, sourceRef, relation, targetRef).changes === 1;\n }\n\n /** Replace the live edge with a corrected one in a single step. */\n supersede(edge: EdgeInput, at: string = nowIso()): void {\n this.expire(edge.sourceRef, edge.relation, edge.targetRef, at);\n this.assert(edge);\n }\n\n current(sourceRef: string, relation: string, targetRef: string): EdgeRow | undefined {\n const raw = this.currentStmt.get(sourceRef, relation, targetRef) as RawEdgeRow | undefined;\n return raw === undefined ? undefined : toEdgeRow(raw);\n }\n\n from(sourceRef: string): EdgeRow[] {\n return (this.fromStmt.all(sourceRef) as RawEdgeRow[]).map(toEdgeRow);\n }\n\n to(targetRef: string): EdgeRow[] {\n return (this.toStmt.all(targetRef) as RawEdgeRow[]).map(toEdgeRow);\n }\n}\n","import { SQL_NOW, nowIso, quoteIdent, type Db } from \"../open.js\";\n\n/**\n * Two entity shapes, because the projects that converged on \"kind + name +\n * parent + JSON attrs\" genuinely diverge on time. Snapshot-scoped rows are\n * right when a whole population is re-indexed together (a code graph);\n * interval rows are right when facts change one at a time (memory, sessions).\n */\nexport interface TableName {\n name?: string;\n}\n\n/** Snapshot-scoped entity: every row belongs to one snapshot. */\nexport function entitySnapTableDdl({ name = \"entity_snap\" }: TableName = {}): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n snapshot_id INTEGER NOT NULL,\n id TEXT NOT NULL,\n kind TEXT NOT NULL,\n name TEXT,\n parent_id TEXT,\n attrs TEXT,\n PRIMARY KEY (snapshot_id, id)\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_kind`)} ON ${t}(snapshot_id, kind);\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_parent`)} ON ${t}(snapshot_id, parent_id);\n `;\n}\n\n/** The snapshot registry the snapshot-scoped tables key on. */\nexport function snapshotTableDdl({ name = \"snapshot\" }: TableName = {}): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n id INTEGER PRIMARY KEY,\n ref TEXT NOT NULL,\n taken_at TEXT NOT NULL DEFAULT (${SQL_NOW}),\n attrs TEXT\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_ref`)} ON ${t}(ref, taken_at);\n `;\n}\n\n/** Interval bi-temporal entity keyed by its cross-domain ref. */\nexport function entityTableDdl({ name = \"entity\" }: TableName = {}): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n ref TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n name TEXT,\n parent_ref TEXT,\n attrs TEXT,\n t_valid TEXT NOT NULL,\n t_invalid TEXT,\n t_created TEXT NOT NULL DEFAULT (${SQL_NOW}),\n t_expired TEXT\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_kind`)} ON ${t}(kind) WHERE t_expired IS NULL;\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_parent`)} ON ${t}(parent_ref) WHERE t_expired IS NULL;\n `;\n}\n\nexport interface EntityInput {\n ref: string;\n kind: string;\n name?: string | null;\n parentRef?: string | null;\n attrs?: Record<string, unknown> | null;\n tValid?: string;\n}\n\nexport interface EntityRow {\n ref: string;\n kind: string;\n name: string | null;\n parentRef: string | null;\n attrs: Record<string, unknown> | null;\n tValid: string;\n tInvalid: string | null;\n tCreated: string;\n tExpired: string | null;\n}\n\ninterface RawEntityRow {\n ref: string;\n kind: string;\n name: string | null;\n parent_ref: string | null;\n attrs: string | null;\n t_valid: string;\n t_invalid: string | null;\n t_created: string;\n t_expired: string | null;\n}\n\nfunction toEntityRow(raw: RawEntityRow): EntityRow {\n return {\n ref: raw.ref,\n kind: raw.kind,\n name: raw.name,\n parentRef: raw.parent_ref,\n attrs: raw.attrs === null ? null : (JSON.parse(raw.attrs) as Record<string, unknown>),\n tValid: raw.t_valid,\n tInvalid: raw.t_invalid,\n tCreated: raw.t_created,\n tExpired: raw.t_expired,\n };\n}\n\n/** Prepared helpers over one interval entity table. */\nexport class EntityTable {\n private readonly upsertStmt;\n private readonly getStmt;\n private readonly expireStmt;\n private readonly byKindStmt;\n\n constructor(db: Db, { name = \"entity\" }: TableName = {}) {\n const t = quoteIdent(name);\n this.upsertStmt = db.prepare(\n `INSERT INTO ${t} (ref, kind, name, parent_ref, attrs, t_valid)\n VALUES (@ref, @kind, @name, @parentRef, @attrs, @tValid)\n ON CONFLICT (ref) DO UPDATE SET\n kind = excluded.kind, name = excluded.name, parent_ref = excluded.parent_ref,\n attrs = excluded.attrs, t_expired = NULL, t_invalid = NULL`,\n );\n this.getStmt = db.prepare(`SELECT * FROM ${t} WHERE ref = ?`);\n this.expireStmt = db.prepare(`UPDATE ${t} SET t_expired = ? WHERE ref = ? AND t_expired IS NULL`);\n this.byKindStmt = db.prepare(`SELECT * FROM ${t} WHERE kind = ? AND t_expired IS NULL ORDER BY ref`);\n }\n\n /** Insert or refresh the entity; a previously expired ref comes back live. */\n upsert(entity: EntityInput): void {\n this.upsertStmt.run({\n ref: entity.ref,\n kind: entity.kind,\n name: entity.name ?? null,\n parentRef: entity.parentRef ?? null,\n attrs: entity.attrs == null ? null : JSON.stringify(entity.attrs),\n tValid: entity.tValid ?? nowIso(),\n });\n }\n\n get(ref: string): EntityRow | undefined {\n const raw = this.getStmt.get(ref) as RawEntityRow | undefined;\n return raw === undefined ? undefined : toEntityRow(raw);\n }\n\n expire(ref: string, at: string = nowIso()): boolean {\n return this.expireStmt.run(at, ref).changes === 1;\n }\n\n listByKind(kind: string): EntityRow[] {\n return (this.byKindStmt.all(kind) as RawEntityRow[]).map(toEntityRow);\n }\n}\n","import { quoteIdent, type Db } from \"../open.js\";\n\n/**\n * Full-text search without storing the text. `searchable_span` holds only a\n * locator into the source (a byte range in a transcript, a file, a blob); the\n * FTS5 table is genuinely contentless (`content=''`), so text is streamed in\n * at index time and discarded. Reads MUST join through the span table: a\n * contentless FTS5 table cannot delete rows without their original text, so\n * purges strand FTS rows, and the join is what makes those orphans invisible.\n */\nexport interface SpanFtsOptions {\n /** Base name; tables are `<name>_span` and `<name>_fts`. */\n name?: string;\n}\n\nfunction names(name: string): { span: string; fts: string } {\n return { span: quoteIdent(`${name}_span`), fts: quoteIdent(`${name}_fts`) };\n}\n\nexport function spanFtsTablesDdl({ name = \"search\" }: SpanFtsOptions = {}): string {\n const { span, fts } = names(name);\n return `\n CREATE TABLE IF NOT EXISTS ${span} (\n span_id INTEGER PRIMARY KEY,\n owner_ref TEXT NOT NULL,\n field TEXT NOT NULL,\n source_id INTEGER NOT NULL,\n byte_offset INTEGER NOT NULL,\n byte_length INTEGER NOT NULL,\n UNIQUE (owner_ref, field, source_id, byte_offset)\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`idx_${name}_span_owner`)} ON ${span}(owner_ref);\n CREATE VIRTUAL TABLE IF NOT EXISTS ${fts} USING fts5(text, content='');\n `;\n}\n\nexport interface SpanInput {\n /** The entity this text belongs to, e.g. `session:abc`. */\n ownerRef: string;\n /** Which part of the owner, e.g. `prompt` or `tool_result`. */\n field: string;\n /** Transcript or file id the byte range is relative to. */\n sourceId: number;\n byteOffset: number;\n byteLength: number;\n}\n\nexport interface SpanHit extends SpanInput {\n spanId: number;\n /** FTS5 bm25 rank; lower is better. */\n rank: number;\n}\n\ninterface RawSpanHit {\n span_id: number;\n owner_ref: string;\n field: string;\n source_id: number;\n byte_offset: number;\n byte_length: number;\n rank: number;\n}\n\nexport class SpanFtsTables {\n private readonly insertSpan;\n private readonly findSpan;\n private readonly insertFts;\n private readonly searchStmt;\n private readonly deleteAllFts;\n private readonly deleteSpansStmt;\n private readonly countSpans;\n private readonly countFts;\n\n constructor(db: Db, { name = \"search\" }: SpanFtsOptions = {}) {\n const { span, fts } = names(name);\n this.insertSpan = db.prepare(\n `INSERT INTO ${span} (owner_ref, field, source_id, byte_offset, byte_length) VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (owner_ref, field, source_id, byte_offset) DO NOTHING`,\n );\n this.findSpan = db.prepare(\n `SELECT span_id FROM ${span} WHERE owner_ref = ? AND field = ? AND source_id = ? AND byte_offset = ?`,\n );\n this.insertFts = db.prepare(`INSERT INTO ${fts} (rowid, text) VALUES (?, ?)`);\n this.searchStmt = db.prepare(\n `SELECT s.span_id, s.owner_ref, s.field, s.source_id, s.byte_offset, s.byte_length, f.rank\n FROM ${fts} f JOIN ${span} s ON s.span_id = f.rowid\n WHERE ${fts} MATCH ? ORDER BY f.rank LIMIT ?`,\n );\n this.deleteAllFts = db.prepare(`INSERT INTO ${fts}(${fts}) VALUES ('delete-all')`);\n this.deleteSpansStmt = db.prepare(`DELETE FROM ${span} WHERE owner_ref = ?`);\n this.countSpans = db.prepare(`SELECT count(*) AS n FROM ${span}`);\n this.countFts = db.prepare(`SELECT count(*) AS n FROM ${fts}`);\n }\n\n /** Index `text` for a span, storing only the locator. Re-indexing the same span is a no-op. Returns the span id. */\n index(span: SpanInput, text: string): number {\n const inserted = this.insertSpan.run(span.ownerRef, span.field, span.sourceId, span.byteOffset, span.byteLength);\n const spanId = (this.findSpan.get(span.ownerRef, span.field, span.sourceId, span.byteOffset) as { span_id: number }).span_id;\n if (inserted.changes === 1) this.insertFts.run(spanId, text);\n return spanId;\n }\n\n /** FTS5 MATCH, joined through the span table so stranded FTS rows never surface. */\n search(query: string, limit = 50): SpanHit[] {\n return (this.searchStmt.all(query, limit) as RawSpanHit[]).map((r) => ({\n spanId: r.span_id,\n ownerRef: r.owner_ref,\n field: r.field,\n sourceId: r.source_id,\n byteOffset: r.byte_offset,\n byteLength: r.byte_length,\n rank: r.rank,\n }));\n }\n\n /** Drop an owner's spans. Their FTS rows become orphans until `rebuild`; the join hides them. */\n purgeOwner(ownerRef: string): number {\n return this.deleteSpansStmt.run(ownerRef).changes;\n }\n\n /** Fraction of FTS rows with no span behind them; a high value means it is time to rebuild. */\n orphanRatio(): number {\n const fts = (this.countFts.get() as { n: number }).n;\n if (fts === 0) return 0;\n const spans = (this.countSpans.get() as { n: number }).n;\n return Math.max(0, fts - spans) / fts;\n }\n\n /** Empty the FTS index. The caller re-streams text through `index` for every surviving span. */\n clearIndex(): void {\n this.deleteAllFts.run();\n }\n}\n","import { SQL_NOW, quoteIdent, type Db } from \"../open.js\";\n\n/**\n * Incremental-ingest bookkeeping for any append-mostly source: where the reader\n * stopped, a hash of what it already consumed (so a rewrite is detected rather\n * than silently re-read from the wrong offset), and the size/mtime/content-hash\n * triple that makes \"source gone or altered\" a reportable state.\n */\nexport type WatermarkStatus = \"ok\" | \"missing\" | \"hash_mismatch\" | \"quarantined\";\n\nexport interface WatermarkOptions {\n name?: string;\n}\n\nexport function watermarkTableDdl({ name = \"watermark\" }: WatermarkOptions = {}): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n source_id INTEGER PRIMARY KEY,\n source_key TEXT NOT NULL UNIQUE,\n last_offset INTEGER NOT NULL DEFAULT 0,\n prefix_hash TEXT,\n last_indexed_at TEXT,\n file_size INTEGER,\n file_mtime TEXT,\n content_hash TEXT,\n status TEXT NOT NULL DEFAULT 'ok',\n status_reason TEXT,\n created_at TEXT NOT NULL DEFAULT (${SQL_NOW})\n );\n `;\n}\n\nexport interface WatermarkRow {\n sourceId: number;\n sourceKey: string;\n lastOffset: number;\n prefixHash: string | null;\n lastIndexedAt: string | null;\n fileSize: number | null;\n fileMtime: string | null;\n contentHash: string | null;\n status: WatermarkStatus;\n statusReason: string | null;\n}\n\ninterface RawWatermarkRow {\n source_id: number;\n source_key: string;\n last_offset: number;\n prefix_hash: string | null;\n last_indexed_at: string | null;\n file_size: number | null;\n file_mtime: string | null;\n content_hash: string | null;\n status: WatermarkStatus;\n status_reason: string | null;\n}\n\nfunction toRow(raw: RawWatermarkRow): WatermarkRow {\n return {\n sourceId: raw.source_id,\n sourceKey: raw.source_key,\n lastOffset: raw.last_offset,\n prefixHash: raw.prefix_hash,\n lastIndexedAt: raw.last_indexed_at,\n fileSize: raw.file_size,\n fileMtime: raw.file_mtime,\n contentHash: raw.content_hash,\n status: raw.status,\n statusReason: raw.status_reason,\n };\n}\n\nexport interface WatermarkAdvance {\n lastOffset: number;\n prefixHash?: string | null;\n fileSize?: number | null;\n fileMtime?: string | null;\n /** Null keeps any previously stored hash; computing it is O(file) and optional. */\n contentHash?: string | null;\n}\n\nexport class WatermarkTable {\n private readonly ensureStmt;\n private readonly getStmt;\n private readonly advanceStmt;\n private readonly statusStmt;\n private readonly rewindStmt;\n private readonly listStmt;\n\n constructor(db: Db, { name = \"watermark\" }: WatermarkOptions = {}) {\n const t = quoteIdent(name);\n this.ensureStmt = db.prepare(`INSERT INTO ${t} (source_key) VALUES (?) ON CONFLICT (source_key) DO NOTHING`);\n this.getStmt = db.prepare(`SELECT * FROM ${t} WHERE source_key = ?`);\n this.advanceStmt = db.prepare(\n `UPDATE ${t} SET last_offset = @lastOffset, prefix_hash = @prefixHash, last_indexed_at = ${SQL_NOW},\n file_size = @fileSize, file_mtime = @fileMtime, content_hash = COALESCE(@contentHash, content_hash),\n status = 'ok', status_reason = NULL\n WHERE source_key = @sourceKey`,\n );\n this.statusStmt = db.prepare(`UPDATE ${t} SET status = ?, status_reason = ? WHERE source_key = ?`);\n this.rewindStmt = db.prepare(`UPDATE ${t} SET last_offset = 0, prefix_hash = NULL WHERE source_key = ?`);\n this.listStmt = db.prepare(`SELECT * FROM ${t} ORDER BY source_id`);\n }\n\n /** The row for a source, created at offset 0 on first sight. The id is stable for the table's life. */\n ensure(sourceKey: string): WatermarkRow {\n this.ensureStmt.run(sourceKey);\n return toRow(this.getStmt.get(sourceKey) as RawWatermarkRow);\n }\n\n get(sourceKey: string): WatermarkRow | undefined {\n const raw = this.getStmt.get(sourceKey) as RawWatermarkRow | undefined;\n return raw === undefined ? undefined : toRow(raw);\n }\n\n advance(sourceKey: string, update: WatermarkAdvance): void {\n this.advanceStmt.run({\n sourceKey,\n lastOffset: update.lastOffset,\n prefixHash: update.prefixHash ?? null,\n fileSize: update.fileSize ?? null,\n fileMtime: update.fileMtime ?? null,\n contentHash: update.contentHash ?? null,\n });\n }\n\n /** Back to byte 0, for a source that was rewritten rather than appended to. */\n rewind(sourceKey: string): void {\n this.rewindStmt.run(sourceKey);\n }\n\n markStatus(sourceKey: string, status: WatermarkStatus, reason: string | null = null): void {\n this.statusStmt.run(status, reason, sourceKey);\n }\n\n list(): WatermarkRow[] {\n return (this.listStmt.all() as RawWatermarkRow[]).map(toRow);\n }\n}\n","import type { Migration } from \"./migrations.js\";\nimport { cacheBlobTableDdl } from \"./tables/cache-blob.js\";\nimport { edgeTableDdl } from \"./tables/edge.js\";\nimport { entitySnapTableDdl, entityTableDdl, snapshotTableDdl } from \"./tables/entity.js\";\nimport { spanFtsTablesDdl } from \"./tables/span-fts.js\";\nimport { watermarkTableDdl } from \"./tables/watermark.js\";\n\n/** Which kit tables a store wants, by name override or `true` for the default name. */\nexport interface KitSelection {\n snapshot?: string | true;\n entitySnap?: string | true;\n entity?: string | true;\n edge?: string | true;\n cacheBlob?: string | true;\n spanFts?: string | true;\n watermark?: string | true;\n}\n\ntype Factory = (options: { name?: string }) => string;\n\nconst FACTORIES: [keyof KitSelection, Factory][] = [\n [\"snapshot\", snapshotTableDdl],\n [\"entitySnap\", entitySnapTableDdl],\n [\"entity\", entityTableDdl],\n [\"edge\", edgeTableDdl],\n [\"cacheBlob\", cacheBlobTableDdl],\n [\"spanFts\", spanFtsTablesDdl],\n [\"watermark\", watermarkTableDdl],\n];\n\n/** The DDL for a selection of kit tables, in dependency-safe order. Every statement is `IF NOT EXISTS`. */\nexport function kitDdl(selection: KitSelection): string {\n return FACTORIES.filter(([key]) => selection[key] !== undefined)\n .map(([key, factory]) => factory(selection[key] === true ? {} : { name: selection[key] as string }))\n .join(\"\\n\");\n}\n\n/** A migration that installs the selected kit tables, for use as a store's version 1. */\nexport function kitMigration(version: number, selection: KitSelection, name = \"kit tables\"): Migration {\n return { version, name, up: (db) => db.exec(kitDdl(selection)) };\n}\n"],"mappings":"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","names":["ref"]}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@titan-design/store-sqlite",
3
+ "version": "0.1.0",
4
+ "description": "SQLite table-factory kit: entity/edge (bi-temporal), content-addressed cache, contentless FTS5, watermark, migrations",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "author": "Henry Jewkes",
19
+ "license": "MIT",
20
+ "homepage": "https://github.com/HJewkes/titan-platform#readme",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/HJewkes/titan-platform.git",
24
+ "directory": "packages/store-sqlite"
25
+ },
26
+ "bugs": "https://github.com/HJewkes/titan-platform/issues",
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "better-sqlite3": "^13.0.2"
35
+ },
36
+ "devDependencies": {
37
+ "@types/better-sqlite3": "^7.6.13"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "typecheck": "tsc --noEmit",
42
+ "lint": "eslint src"
43
+ }
44
+ }