@titan-design/store-sqlite 0.3.0 → 0.3.3

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/README.md CHANGED
@@ -11,9 +11,12 @@ initiative's `sources/research-store-synthesis.md`.
11
11
  ## Open and migrate
12
12
 
13
13
  ```ts
14
+ import os from "node:os";
15
+ import path from "node:path";
14
16
  import { openDatabase, runMigrations, kitMigration } from "@titan-design/store-sqlite";
15
17
 
16
- const db = openDatabase("~/.local/state/thing/index.sqlite3"); // WAL + foreign keys on
18
+ const dbPath = path.join(os.homedir(), ".local/state/thing/index.sqlite3");
19
+ const db = openDatabase(dbPath); // WAL + foreign keys on
17
20
  runMigrations(db, [
18
21
  kitMigration(1, { edge: true, watermark: true, spanFts: "notes", cacheBlob: true }),
19
22
  { version: 2, up: (db) => db.exec("CREATE TABLE my_domain_table (...)") },
@@ -45,6 +48,15 @@ A store layering its own migrations onto a shared database sees one band of vers
45
48
  than the top of it — `products/session-miner` numbers its own from 1000 precisely so it can
46
49
  sit above `@titan-design/session-graph`'s.
47
50
 
51
+ ### Refusing a renamed migration
52
+
53
+ Migrations are `{ version, name?, up(db) }`, and the name is recorded beside the version.
54
+ When an already-applied version's recorded name differs from the declared one,
55
+ `runMigrations` throws `MigrationIdentityError` (carrying `version`, `recordedName`, and
56
+ `declaredName`) instead of skipping it. That catches two migration lists that collided on a
57
+ version number, so neither believes in a schema it never applied. A migration with no
58
+ declared name, or a row recorded before names were kept, is never compared.
59
+
48
60
  ## The primitives
49
61
 
50
62
  Every factory takes an optional `name` so a store can host several instances or keep a
@@ -68,7 +80,9 @@ unchanged row per node per snapshot. One physical table cannot serve both.
68
80
 
69
81
  `SpanFtsTables.search` always joins the FTS table through the span table. A contentless
70
82
  FTS5 table cannot delete a row without its original text, so purges strand FTS rows; the
71
- join hides them, `orphanRatio` tells you when to `clearIndex` and re-stream.
83
+ join hides them. When `orphanRatio` climbs, rebuild: call `clearIndex`, then call `index`
84
+ again for every surviving span. `index` puts back the FTS row of a span that has lost it,
85
+ so the span ids stay the same and the orphans are gone.
72
86
 
73
87
  ## Refs
74
88
 
package/dist/index.d.ts CHANGED
@@ -53,6 +53,12 @@ interface Migration {
53
53
  name?: string;
54
54
  up: (db: Db) => void;
55
55
  }
56
+ declare class MigrationIdentityError extends Error {
57
+ readonly version: number;
58
+ readonly recordedName: string;
59
+ readonly declaredName: string;
60
+ constructor(version: number, recordedName: string, declaredName: string);
61
+ }
56
62
  declare function appliedVersions(db: Db): number[];
57
63
  /**
58
64
  * Apply every migration not yet recorded in `_migration`, each in its own
@@ -134,12 +140,13 @@ declare class EdgeTable {
134
140
  private readonly currentStmt;
135
141
  private readonly fromStmt;
136
142
  private readonly toStmt;
143
+ private readonly supersedeTxn;
137
144
  constructor(db: Db, { name }?: EdgeTableOptions);
138
145
  /** Assert an edge. A live identical edge is left alone (idempotent re-ingest). Returns true when inserted. */
139
146
  assert(edge: EdgeInput): boolean;
140
147
  /** Retract the current belief in an edge (invalidate, never delete). Returns true when one was live. */
141
148
  expire(sourceRef: string, relation: string, targetRef: string, at?: string): boolean;
142
- /** Replace the live edge with a corrected one in a single step. */
149
+ /** Replace the live edge with a corrected one in a single step; a failed insert rolls the expire back. */
143
150
  supersede(edge: EdgeInput, at?: string): void;
144
151
  current(sourceRef: string, relation: string, targetRef: string): EdgeRow | undefined;
145
152
  from(sourceRef: string): EdgeRow[];
@@ -284,6 +291,7 @@ declare class SpanFtsTables {
284
291
  private readonly insertSpan;
285
292
  private readonly findSpan;
286
293
  private readonly insertFts;
294
+ private readonly hasFtsRow;
287
295
  private readonly searchStmt;
288
296
  /** One prepared statement per scope shape, since the predicate list varies. */
289
297
  private readonly scopedSearchStmts;
@@ -291,8 +299,15 @@ declare class SpanFtsTables {
291
299
  private readonly deleteSpansStmt;
292
300
  private readonly countSpans;
293
301
  private readonly countFts;
302
+ private readonly indexTxn;
294
303
  constructor(db: Db, { name }?: SpanFtsOptions);
295
- /** Index `text` for a span, storing only the locator. Re-indexing the same span is a no-op. Returns the span id. */
304
+ /**
305
+ * Index `text` for a span, storing only the locator. Returns the span id.
306
+ *
307
+ * Re-indexing a span that still has its FTS row is a no-op; re-indexing one
308
+ * whose row `clearIndex` removed puts the row back, which is what makes
309
+ * clear-then-re-stream a rebuild. Only the conflict path pays the rowid probe.
310
+ */
296
311
  index(span: SpanInput, text: string): number;
297
312
  /**
298
313
  * FTS5 MATCH, joined through the span table so stranded FTS rows never surface.
@@ -311,11 +326,14 @@ declare class SpanFtsTables {
311
326
  * is everything that changes the SQL.
312
327
  */
313
328
  private scopedStatement;
314
- /** Drop an owner's spans. Their FTS rows become orphans until `rebuild`; the join hides them. */
329
+ /** Drop an owner's spans. Their FTS rows become orphans, hidden by the join, until `clearIndex` and a re-stream. */
315
330
  purgeOwner(ownerRef: string): number;
316
- /** Fraction of FTS rows with no span behind them; a high value means it is time to rebuild. */
331
+ /** Fraction of FTS rows with no span behind them; a high value means it is time to `clearIndex` and re-stream. */
317
332
  orphanRatio(): number;
318
- /** Empty the FTS index. The caller re-streams text through `index` for every surviving span. */
333
+ /**
334
+ * Empty the FTS index, keeping every span row. Re-streaming each surviving
335
+ * span's text through `index` then rebuilds it with the same span ids.
336
+ */
319
337
  clearIndex(): void;
320
338
  }
321
339
 
@@ -368,4 +386,4 @@ declare class WatermarkTable {
368
386
  list(): WatermarkRow[];
369
387
  }
370
388
 
371
- export { type CacheBlobOptions, CacheBlobTable, type CacheHit, type CacheKey, type Db, type EdgeInput, type EdgeRow, EdgeTable, type EdgeTableOptions, type EntityInput, type EntityRow, EntityTable, type KitSelection, MIGRATION_TABLE_NAME, type Migration, type OpenOptions, type ParsedRef, SQL_NOW, SchemaTooNewError, type SpanFtsOptions, SpanFtsTables, type SpanHit, type SpanInput, type SpanScope, type TableName, type WatermarkAdvance, type WatermarkOptions, type WatermarkRow, type WatermarkStatus, WatermarkTable, appliedVersions, assertSchemaVersion, cacheBlobTableDdl, contentHashOf, edgeTableDdl, entitySnapTableDdl, entityTableDdl, hasColumn, hasTable, isRef, kitDdl, kitMigration, nowIso, openDatabase, parseRef, quoteIdent, ref, refKind, runMigrations, snapshotTableDdl, spanFtsTablesDdl, watermarkTableDdl };
389
+ export { type CacheBlobOptions, CacheBlobTable, type CacheHit, type CacheKey, type Db, type EdgeInput, type EdgeRow, EdgeTable, type EdgeTableOptions, type EntityInput, type EntityRow, EntityTable, type KitSelection, MIGRATION_TABLE_NAME, type Migration, MigrationIdentityError, type OpenOptions, type ParsedRef, SQL_NOW, SchemaTooNewError, type SpanFtsOptions, SpanFtsTables, type SpanHit, type SpanInput, type SpanScope, type TableName, type WatermarkAdvance, type WatermarkOptions, type WatermarkRow, type WatermarkStatus, WatermarkTable, appliedVersions, assertSchemaVersion, cacheBlobTableDdl, contentHashOf, edgeTableDdl, entitySnapTableDdl, entityTableDdl, hasColumn, hasTable, isRef, kitDdl, kitMigration, nowIso, openDatabase, parseRef, quoteIdent, ref, refKind, runMigrations, snapshotTableDdl, spanFtsTablesDdl, watermarkTableDdl };
package/dist/index.js CHANGED
@@ -65,6 +65,20 @@ var MIGRATION_TABLE = `
65
65
  name TEXT,
66
66
  applied_at TEXT NOT NULL
67
67
  )`;
68
+ var MigrationIdentityError = class extends Error {
69
+ constructor(version, recordedName, declaredName) {
70
+ super(
71
+ `migration version ${version} was applied as "${recordedName}", but this runtime declares it as "${declaredName}"; two migration lists have collided on that version and the database matches neither`
72
+ );
73
+ this.version = version;
74
+ this.recordedName = recordedName;
75
+ this.declaredName = declaredName;
76
+ this.name = "MigrationIdentityError";
77
+ }
78
+ version;
79
+ recordedName;
80
+ declaredName;
81
+ };
68
82
  function appliedVersions(db) {
69
83
  db.exec(MIGRATION_TABLE);
70
84
  const rows = db.prepare("SELECT version FROM _migration ORDER BY version").all();
@@ -72,7 +86,7 @@ function appliedVersions(db) {
72
86
  }
73
87
  function runMigrations(db, migrations) {
74
88
  assertWellFormed(migrations);
75
- const applied = new Set(appliedVersions(db));
89
+ const applied = appliedNames(db);
76
90
  const record = db.prepare("INSERT INTO _migration (version, name, applied_at) VALUES (?, ?, ?)");
77
91
  const applyOne = db.transaction((m) => {
78
92
  m.up(db);
@@ -80,12 +94,25 @@ function runMigrations(db, migrations) {
80
94
  });
81
95
  const done = [];
82
96
  for (const m of [...migrations].sort((a, b) => a.version - b.version)) {
83
- if (applied.has(m.version)) continue;
97
+ if (applied.has(m.version)) {
98
+ assertSameIdentity(m, applied.get(m.version) ?? null);
99
+ continue;
100
+ }
84
101
  applyOne(m);
85
102
  done.push(m.version);
86
103
  }
87
104
  return done;
88
105
  }
106
+ function assertSameIdentity(m, recordedName) {
107
+ if (m.name === void 0 || recordedName === null) return;
108
+ if (m.name === recordedName) return;
109
+ throw new MigrationIdentityError(m.version, recordedName, m.name);
110
+ }
111
+ function appliedNames(db) {
112
+ db.exec(MIGRATION_TABLE);
113
+ const rows = db.prepare("SELECT version, name FROM _migration").all();
114
+ return new Map(rows.map((r) => [r.version, r.name]));
115
+ }
89
116
  function assertWellFormed(migrations) {
90
117
  const seen = /* @__PURE__ */ new Set();
91
118
  for (const m of migrations) {
@@ -227,6 +254,7 @@ var EdgeTable = class {
227
254
  currentStmt;
228
255
  fromStmt;
229
256
  toStmt;
257
+ supersedeTxn;
230
258
  constructor(db, { name = "edge" } = {}) {
231
259
  const t = quoteIdent(name);
232
260
  this.insertStmt = db.prepare(
@@ -242,6 +270,10 @@ var EdgeTable = class {
242
270
  );
243
271
  this.fromStmt = db.prepare(`SELECT * FROM ${t} WHERE source_ref = ? AND t_expired IS NULL ORDER BY edge_id`);
244
272
  this.toStmt = db.prepare(`SELECT * FROM ${t} WHERE target_ref = ? AND t_expired IS NULL ORDER BY edge_id`);
273
+ this.supersedeTxn = db.transaction((edge, at) => {
274
+ this.expire(edge.sourceRef, edge.relation, edge.targetRef, at);
275
+ this.assert(edge);
276
+ });
245
277
  }
246
278
  /** Assert an edge. A live identical edge is left alone (idempotent re-ingest). Returns true when inserted. */
247
279
  assert(edge) {
@@ -261,10 +293,9 @@ var EdgeTable = class {
261
293
  expire(sourceRef, relation, targetRef, at = nowIso()) {
262
294
  return this.expireStmt.run(at, sourceRef, relation, targetRef).changes === 1;
263
295
  }
264
- /** Replace the live edge with a corrected one in a single step. */
296
+ /** Replace the live edge with a corrected one in a single step; a failed insert rolls the expire back. */
265
297
  supersede(edge, at = nowIso()) {
266
- this.expire(edge.sourceRef, edge.relation, edge.targetRef, at);
267
- this.assert(edge);
298
+ this.supersedeTxn(edge, at);
268
299
  }
269
300
  current(sourceRef, relation, targetRef) {
270
301
  const raw = this.currentStmt.get(sourceRef, relation, targetRef);
@@ -418,6 +449,7 @@ var SpanFtsTables = class {
418
449
  insertSpan;
419
450
  findSpan;
420
451
  insertFts;
452
+ hasFtsRow;
421
453
  searchStmt;
422
454
  /** One prepared statement per scope shape, since the predicate list varies. */
423
455
  scopedSearchStmts = /* @__PURE__ */ new Map();
@@ -425,6 +457,7 @@ var SpanFtsTables = class {
425
457
  deleteSpansStmt;
426
458
  countSpans;
427
459
  countFts;
460
+ indexTxn;
428
461
  constructor(db, { name = "search" } = {}) {
429
462
  const { span, fts } = names(name);
430
463
  this.db = db;
@@ -438,6 +471,7 @@ var SpanFtsTables = class {
438
471
  `SELECT span_id FROM ${span} WHERE owner_ref = ? AND field = ? AND source_id = ? AND byte_offset = ?`
439
472
  );
440
473
  this.insertFts = db.prepare(`INSERT INTO ${fts} (rowid, text) VALUES (?, ?)`);
474
+ this.hasFtsRow = db.prepare(`SELECT 1 FROM ${fts} WHERE rowid = ?`);
441
475
  this.searchStmt = db.prepare(
442
476
  `SELECT s.span_id, s.owner_ref, s.field, s.source_id, s.byte_offset, s.byte_length, f.rank
443
477
  FROM ${fts} f JOIN ${span} s ON s.span_id = f.rowid
@@ -447,13 +481,22 @@ var SpanFtsTables = class {
447
481
  this.deleteSpansStmt = db.prepare(`DELETE FROM ${span} WHERE owner_ref = ?`);
448
482
  this.countSpans = db.prepare(`SELECT count(*) AS n FROM ${span}`);
449
483
  this.countFts = db.prepare(`SELECT count(*) AS n FROM ${fts}`);
484
+ this.indexTxn = db.transaction((span2, text) => {
485
+ const inserted = this.insertSpan.run(span2.ownerRef, span2.field, span2.sourceId, span2.byteOffset, span2.byteLength);
486
+ const spanId = this.findSpan.get(span2.ownerRef, span2.field, span2.sourceId, span2.byteOffset).span_id;
487
+ if (inserted.changes === 1 || this.hasFtsRow.get(spanId) === void 0) this.insertFts.run(spanId, text);
488
+ return spanId;
489
+ });
450
490
  }
451
- /** Index `text` for a span, storing only the locator. Re-indexing the same span is a no-op. Returns the span id. */
491
+ /**
492
+ * Index `text` for a span, storing only the locator. Returns the span id.
493
+ *
494
+ * Re-indexing a span that still has its FTS row is a no-op; re-indexing one
495
+ * whose row `clearIndex` removed puts the row back, which is what makes
496
+ * clear-then-re-stream a rebuild. Only the conflict path pays the rowid probe.
497
+ */
452
498
  index(span, text) {
453
- const inserted = this.insertSpan.run(span.ownerRef, span.field, span.sourceId, span.byteOffset, span.byteLength);
454
- const spanId = this.findSpan.get(span.ownerRef, span.field, span.sourceId, span.byteOffset).span_id;
455
- if (inserted.changes === 1) this.insertFts.run(spanId, text);
456
- return spanId;
499
+ return this.indexTxn(span, text);
457
500
  }
458
501
  /**
459
502
  * FTS5 MATCH, joined through the span table so stranded FTS rows never surface.
@@ -501,18 +544,21 @@ var SpanFtsTables = class {
501
544
  this.scopedSearchStmts.set(key, statement);
502
545
  return statement;
503
546
  }
504
- /** Drop an owner's spans. Their FTS rows become orphans until `rebuild`; the join hides them. */
547
+ /** Drop an owner's spans. Their FTS rows become orphans, hidden by the join, until `clearIndex` and a re-stream. */
505
548
  purgeOwner(ownerRef) {
506
549
  return this.deleteSpansStmt.run(ownerRef).changes;
507
550
  }
508
- /** Fraction of FTS rows with no span behind them; a high value means it is time to rebuild. */
551
+ /** Fraction of FTS rows with no span behind them; a high value means it is time to `clearIndex` and re-stream. */
509
552
  orphanRatio() {
510
553
  const fts = this.countFts.get().n;
511
554
  if (fts === 0) return 0;
512
555
  const spans = this.countSpans.get().n;
513
556
  return Math.max(0, fts - spans) / fts;
514
557
  }
515
- /** Empty the FTS index. The caller re-streams text through `index` for every surviving span. */
558
+ /**
559
+ * Empty the FTS index, keeping every span row. Re-streaming each surviving
560
+ * span's text through `index` then rebuilds it with the same span ids.
561
+ */
516
562
  clearIndex() {
517
563
  this.deleteAllFts.run();
518
564
  }
@@ -624,6 +670,7 @@ export {
624
670
  EdgeTable,
625
671
  EntityTable,
626
672
  MIGRATION_TABLE_NAME,
673
+ MigrationIdentityError,
627
674
  SQL_NOW,
628
675
  SchemaTooNewError,
629
676
  SpanFtsTables,
package/dist/index.js.map CHANGED
@@ -1 +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 /**\n * The highest migration version this runtime knows. When set, opening a database\n * stamped past it throws `SchemaTooNewError` instead of quietly proceeding.\n */\n schemaVersion?: number;\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 if (options.schemaVersion !== undefined) assertOrClose(db, options.schemaVersion);\n return db;\n}\n\nfunction assertOrClose(db: Db, schemaVersion: number): void {\n try {\n assertSchemaVersion(db, schemaVersion);\n } catch (err) {\n db.close();\n throw err;\n }\n}\n\n/** The one table name the kit reserves: every migration a store applies is recorded here. */\nexport const MIGRATION_TABLE_NAME = \"_migration\";\n\n/**\n * Thrown when a database carries a schema version this runtime has never applied.\n *\n * Adapted from beads (MIT), `internal/storage/embeddeddolt/store.go`: a stale binary whose\n * migrations silently no-op dies later on a column the newer schema dropped, a long way\n * from the cause.\n */\nexport class SchemaTooNewError extends Error {\n constructor(\n readonly storedVersion: number,\n readonly knownVersion: number,\n ) {\n super(\n `database schema is at version ${storedVersion}, but this runtime knows only up to ${knownVersion}; ` +\n \"upgrade the package that owns these migrations, or open an older copy of the database\",\n );\n this.name = \"SchemaTooNewError\";\n }\n}\n\n/**\n * Refuse a database stamped newer than `knownVersion`. A no-op on one that has never been\n * migrated, so a fresh file opens normally.\n *\n * Only a caller that owns the whole schema can name that version: a store layering its own\n * migrations onto a shared database sees one band of versions, not the top. That is why\n * this is opt-in rather than a check inside `runMigrations`.\n */\nexport function assertSchemaVersion(db: Db, knownVersion: number): void {\n const stored = storedSchemaVersion(db);\n if (stored !== undefined && stored > knownVersion) throw new SchemaTooNewError(stored, knownVersion);\n}\n\n/** Reads without creating the table, so a read-only connection can ask. */\nfunction storedSchemaVersion(db: Db): number | undefined {\n if (!hasTable(db, MIGRATION_TABLE_NAME)) return undefined;\n const row = db.prepare(`SELECT MAX(version) AS version FROM ${MIGRATION_TABLE_NAME}`).get() as { version: number | null };\n return row.version ?? undefined;\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 { MIGRATION_TABLE_NAME, 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_TABLE_NAME} (\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\n/**\n * Narrow a search to one class of owner, so one class cannot flood a result set.\n *\n * A span table that holds several entity classes holds them in wildly different\n * volumes. Measured on active-work's graph: 92,713 transcript spans against\n * 1,569 note spans, 59 to 1. Ranking one pooled query returns transcript\n * chatter and the notes never surface, so the fix is a separate ranked list per\n * class, which needs a way to ask for one class.\n *\n * `ownerPrefix` and `fields` are both needed, not either. active-work keys both\n * a mined transcript and a workspace session record under `session:`, and they\n * are separable only by field — `body` is the record, `prompt` and the\n * `tool_*` fields are the transcript. A prefix-only filter would put 92,713\n * transcript spans into the records' list and look like it was working.\n */\nexport interface SpanScope {\n /** Match owners whose ref starts with this, e.g. `note:`. A range scan, so the owner index applies. */\n ownerPrefix?: string;\n /** Match only these fields, e.g. `[\"title\", \"body\"]`. Empty means no field matches. */\n fields?: string[];\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\n/** The string just past the prefix, so a prefix match is a half-open range the index can serve. */\nfunction prefixUpperBound(prefix: string): string {\n const last = prefix.charCodeAt(prefix.length - 1);\n return prefix.slice(0, -1) + String.fromCharCode(last + 1);\n}\n\n/** An empty prefix means \"every owner\", which is what omitting it already means. */\nfunction hasPrefix(scope: SpanScope): boolean {\n return scope.ownerPrefix !== undefined && scope.ownerPrefix.length > 0;\n}\n\n/** Bind values for `scopedStatement`'s predicates, in the order it writes them. */\nfunction scopeValues(scope: SpanScope): (string | number)[] {\n const values: (string | number)[] = [];\n if (hasPrefix(scope)) values.push(scope.ownerPrefix!, prefixUpperBound(scope.ownerPrefix!));\n if (scope.fields !== undefined) values.push(...scope.fields);\n return values;\n}\n\nexport class SpanFtsTables {\n private readonly db: Db;\n private readonly tableNames: { span: string; fts: string };\n private readonly insertSpan;\n private readonly findSpan;\n private readonly insertFts;\n private readonly searchStmt;\n /** One prepared statement per scope shape, since the predicate list varies. */\n private readonly scopedSearchStmts = new Map<string, ReturnType<Db[\"prepare\"]>>();\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.db = db;\n this.tableNames = { span, fts };\n // Contentless FTS retains orphan rowids after a purge. Never reuse one: old\n // tokens would otherwise become visible through an unrelated replacement span.\n this.insertSpan = db.prepare(\n `INSERT INTO ${span} (span_id, owner_ref, field, source_id, byte_offset, byte_length)\n VALUES ((SELECT MAX(id) + 1 FROM (SELECT COALESCE(MAX(span_id), 0) AS id FROM ${span} UNION ALL SELECT COALESCE(MAX(rowid), 0) AS id FROM ${fts})), ?, ?, ?, ?, ?)\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 /**\n * FTS5 MATCH, joined through the span table so stranded FTS rows never surface.\n *\n * `scope` narrows to one class of owner; `limit` is applied after it, so a\n * scoped search returns its own top-N rather than whatever survives the\n * filter out of a global top-N.\n */\n search(query: string, limit = 50, scope?: SpanScope): SpanHit[] {\n const rows = (\n scope ? this.scopedStatement(scope).all([query, ...scopeValues(scope), limit]) : this.searchStmt.all(query, limit)\n ) as RawSpanHit[];\n return rows.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 /**\n * Prepared statement for one scope shape, cached.\n *\n * Cached by shape rather than by values, because a product registers a fixed\n * handful of class retrievers and then queries them for the life of the\n * process. Keyed on the field *count* and whether a prefix is present, which\n * is everything that changes the SQL.\n */\n private scopedStatement(scope: SpanScope): ReturnType<Db[\"prepare\"]> {\n const fieldCount = scope.fields?.length ?? 0;\n const key = `${hasPrefix(scope) ? 1 : 0}:${scope.fields === undefined ? \"all\" : fieldCount}`;\n const cached = this.scopedSearchStmts.get(key);\n if (cached) return cached;\n\n const { span, fts } = this.tableNames;\n const predicates = [`${fts} MATCH ?`];\n // A half-open range rather than LIKE or GLOB, so the owner index applies.\n if (hasPrefix(scope)) predicates.push(\"s.owner_ref >= ? AND s.owner_ref < ?\");\n if (scope.fields !== undefined) {\n predicates.push(fieldCount === 0 ? \"0\" : `s.field IN (${new Array(fieldCount).fill(\"?\").join(\", \")})`);\n }\n const statement = this.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 ${predicates.join(\" AND \")} ORDER BY f.rank LIMIT ?`,\n );\n this.scopedSearchStmts.set(key, statement);\n return statement;\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":";AAAA,OAAO,cAAc;AACrB,SAAS,iBAAiB;AAC1B,OAAO,UAAU;AAkBV,SAAS,aAAa,QAAgB,UAAuB,CAAC,GAAO;AAC1E,QAAM,WAAW,WAAW;AAC5B,MAAI,CAAC,YAAY,CAAC,QAAQ,SAAU,WAAU,KAAK,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AACvF,QAAM,KAAK,IAAI,SAAS,QAAQ,EAAE,UAAU,QAAQ,YAAY,MAAM,CAAC;AACvE,MAAI,CAAC,YAAY,CAAC,QAAQ,aAAa,QAAQ,OAAO,MAAO,IAAG,OAAO,oBAAoB;AAC3F,MAAI,QAAQ,eAAe,KAAM,IAAG,OAAO,mBAAmB;AAC9D,MAAI,QAAQ,kBAAkB,OAAW,eAAc,IAAI,QAAQ,aAAa;AAChF,SAAO;AACT;AAEA,SAAS,cAAc,IAAQ,eAA6B;AAC1D,MAAI;AACF,wBAAoB,IAAI,aAAa;AAAA,EACvC,SAAS,KAAK;AACZ,OAAG,MAAM;AACT,UAAM;AAAA,EACR;AACF;AAGO,IAAM,uBAAuB;AAS7B,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAC3C,YACW,eACA,cACT;AACA;AAAA,MACE,iCAAiC,aAAa,uCAAuC,YAAY;AAAA,IAEnG;AANS;AACA;AAMT,SAAK,OAAO;AAAA,EACd;AAAA,EARW;AAAA,EACA;AAQb;AAUO,SAAS,oBAAoB,IAAQ,cAA4B;AACtE,QAAM,SAAS,oBAAoB,EAAE;AACrC,MAAI,WAAW,UAAa,SAAS,aAAc,OAAM,IAAI,kBAAkB,QAAQ,YAAY;AACrG;AAGA,SAAS,oBAAoB,IAA4B;AACvD,MAAI,CAAC,SAAS,IAAI,oBAAoB,EAAG,QAAO;AAChD,QAAM,MAAM,GAAG,QAAQ,uCAAuC,oBAAoB,EAAE,EAAE,IAAI;AAC1F,SAAO,IAAI,WAAW;AACxB;AAGO,SAAS,SAAiB;AAC/B,UAAO,oBAAI,KAAK,GAAE,YAAY;AAChC;AAGO,IAAM,UAAU;AAEhB,SAAS,SAAS,IAAQ,OAAwB;AACvD,QAAM,MAAM,GAAG,QAAQ,yEAAyE,EAAE,IAAI,KAAK;AAC3G,SAAO,QAAQ;AACjB;AAEO,SAAS,UAAU,IAAQ,OAAe,QAAyB;AACxE,QAAM,OAAO,GAAG,QAAQ,qBAAqB,WAAW,KAAK,CAAC,GAAG,EAAE,IAAI;AACvE,SAAO,KAAK,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM;AAC3C;AAGO,SAAS,WAAW,MAAsB;AAC/C,MAAI,CAAC,2BAA2B,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,2BAA2B,IAAI,EAAE;AAC7F,SAAO,IAAI,IAAI;AACjB;;;AC9FA,IAAM,kBAAkB;AAAA,+BACO,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAM5C,SAAS,gBAAgB,IAAkB;AAChD,KAAG,KAAK,eAAe;AACvB,QAAM,OAAO,GAAG,QAAQ,iDAAiD,EAAE,IAAI;AAC/E,SAAO,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO;AAClC;AAOO,SAAS,cAAc,IAAQ,YAA4C;AAChF,mBAAiB,UAAU;AAC3B,QAAM,UAAU,IAAI,IAAI,gBAAgB,EAAE,CAAC;AAC3C,QAAM,SAAS,GAAG,QAAQ,qEAAqE;AAC/F,QAAM,WAAW,GAAG,YAAY,CAAC,MAAiB;AAChD,MAAE,GAAG,EAAE;AACP,WAAO,IAAI,EAAE,SAAS,EAAE,QAAQ,MAAM,OAAO,CAAC;AAAA,EAChD,CAAC;AACD,QAAM,OAAiB,CAAC;AACxB,aAAW,KAAK,CAAC,GAAG,UAAU,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO,GAAG;AACrE,QAAI,QAAQ,IAAI,EAAE,OAAO,EAAG;AAC5B,aAAS,CAAC;AACV,SAAK,KAAK,EAAE,OAAO;AAAA,EACrB;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,YAAwC;AAChE,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,KAAK,YAAY;AAC1B,QAAI,CAAC,OAAO,UAAU,EAAE,OAAO,KAAK,EAAE,WAAW,EAAG,OAAM,IAAI,MAAM,iDAAiD,EAAE,OAAO,EAAE;AAChI,QAAI,KAAK,IAAI,EAAE,OAAO,EAAG,OAAM,IAAI,MAAM,gCAAgC,EAAE,OAAO,EAAE;AACpF,SAAK,IAAI,EAAE,OAAO;AAAA,EACpB;AACF;;;ACxCA,IAAM,OAAO;AAEN,SAAS,IAAI,MAAc,IAAoB;AACpD,MAAI,CAAC,KAAK,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,qBAAqB,KAAK,UAAU,IAAI,CAAC,EAAE;AACjF,MAAI,GAAG,WAAW,EAAG,OAAM,IAAI,MAAM,0BAA0B;AAC/D,SAAO,GAAG,IAAI,IAAI,EAAE;AACtB;AAEO,SAAS,SAAS,OAA0B;AACjD,QAAM,KAAK,MAAM,QAAQ,GAAG;AAC5B,MAAI,MAAM,KAAK,OAAO,MAAM,SAAS,KAAK,CAAC,KAAK,KAAK,MAAM,MAAM,GAAG,EAAE,CAAC,GAAG;AACxE,UAAM,IAAI,MAAM,gBAAgB,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,EACzD;AACA,SAAO,EAAE,MAAM,MAAM,MAAM,GAAG,EAAE,GAAG,IAAI,MAAM,MAAM,KAAK,CAAC,EAAE;AAC7D;AAEO,SAAS,MAAM,OAAiC;AACrD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI;AACF,aAAS,KAAK;AACd,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,QAAQ,MAAsC;AAC5D,MAAI,MAAM,OAAO;AACjB,SAAO,CAAC,OAAO,IAAI,MAAM,EAAE;AAC7B;;;AC1CA,SAAS,kBAAkB;AAapB,SAAS,kBAAkB,EAAE,OAAO,aAAa,IAAsB,CAAC,GAAW;AACxF,QAAM,IAAI,WAAW,IAAI;AACzB,SAAO;AAAA,iCACwB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4CAMU,OAAO;AAAA;AAAA;AAAA;AAInD;AAEO,SAAS,cAAc,MAAsB;AAClD,SAAO,WAAW,QAAQ,EAAE,OAAO,MAAM,MAAM,EAAE,OAAO,KAAK;AAC/D;AAeA,SAAS,QAAQ,KAAuB;AACtC,MAAI,IAAI,gBAAgB,OAAW,QAAO,IAAI;AAC9C,MAAI,IAAI,SAAS,OAAW,QAAO,cAAc,IAAI,IAAI;AACzD,QAAM,IAAI,MAAM,qCAAqC;AACvD;AAEO,IAAM,iBAAN,MAAqB;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,IAAQ,EAAE,OAAO,aAAa,IAAsB,CAAC,GAAG;AAClE,UAAM,IAAI,WAAW,IAAI;AACzB,SAAK,UAAU,GAAG,QAAQ,2BAA2B,CAAC,yDAAyD;AAC/G,SAAK,UAAU,GAAG;AAAA,MAChB,eAAe,CAAC;AAAA;AAAA,IAElB;AACA,SAAK,YAAY,GAAG,QAAQ,6BAA6B,CAAC,oCAAoC;AAAA,EAChG;AAAA,EAEA,IAAI,KAAqC;AACvC,UAAM,MAAM,KAAK,QAAQ,IAAI,IAAI,WAAW,IAAI,OAAO,QAAQ,GAAG,CAAC;AACnE,QAAI,QAAQ,OAAW,QAAO;AAC9B,WAAO,EAAE,OAAO,IAAI,OAAO,MAAM,IAAI,SAAS,OAAO,OAAQ,KAAK,MAAM,IAAI,IAAI,EAA8B;AAAA,EAChH;AAAA;AAAA,EAGA,IAAI,KAAe,OAAwB,MAAyC;AAClF,UAAM,QAAQ,OAAO,UAAU,WAAW,OAAO,KAAK,OAAO,MAAM,IAAI;AACvE,UAAM,OAAO,KAAK,QAAQ,IAAI,IAAI,WAAW,IAAI,OAAO,QAAQ,GAAG,GAAG,OAAO,SAAS,SAAY,OAAO,KAAK,UAAU,IAAI,CAAC;AAC7H,WAAO,KAAK,YAAY;AAAA,EAC1B;AAAA;AAAA,EAGA,aAAa,KAAe,SAAgC,MAAwC;AAClG,UAAM,MAAM,KAAK,IAAI,GAAG;AACxB,QAAI,IAAK,QAAO,IAAI;AACpB,UAAM,QAAQ,QAAQ;AACtB,SAAK,IAAI,KAAK,OAAO,IAAI;AACzB,WAAO,OAAO,UAAU,WAAW,OAAO,KAAK,OAAO,MAAM,IAAI;AAAA,EAClE;AAAA,EAEA,MAAM,WAAmB,OAAuB;AAC9C,WAAQ,KAAK,UAAU,IAAI,WAAW,KAAK,EAAoB;AAAA,EACjE;AACF;;;AC9EO,SAAS,aAAa,EAAE,OAAO,OAAO,IAAsB,CAAC,GAAW;AAC7E,QAAM,IAAI,WAAW,IAAI;AACzB,SAAO;AAAA,iCACwB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0CAOQ,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCAMhB,WAAW,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC;AAAA,iCACxC,WAAW,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC;AAAA,wCACjC,WAAW,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC;AAAA;AAEjF;AAyCA,SAAS,UAAU,KAA0B;AAC3C,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI;AAAA,IACf,UAAU,IAAI;AAAA,IACd,WAAW,IAAI;AAAA,IACf,QAAQ,IAAI;AAAA,IACZ,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA,IACd,QAAQ,IAAI;AAAA,IACZ,YAAY,IAAI;AAAA,IAChB,OAAO,IAAI,UAAU,OAAO,OAAQ,KAAK,MAAM,IAAI,KAAK;AAAA,EAC1D;AACF;AAGO,IAAM,YAAN,MAAgB;AAAA,EACJ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,IAAQ,EAAE,OAAO,OAAO,IAAsB,CAAC,GAAG;AAC5D,UAAM,IAAI,WAAW,IAAI;AACzB,SAAK,aAAa,GAAG;AAAA,MACnB,eAAe,CAAC;AAAA;AAAA;AAAA,IAGlB;AACA,SAAK,aAAa,GAAG;AAAA,MACnB,UAAU,CAAC;AAAA,IACb;AACA,SAAK,cAAc,GAAG;AAAA,MACpB,iBAAiB,CAAC;AAAA,IACpB;AACA,SAAK,WAAW,GAAG,QAAQ,iBAAiB,CAAC,8DAA8D;AAC3G,SAAK,SAAS,GAAG,QAAQ,iBAAiB,CAAC,8DAA8D;AAAA,EAC3G;AAAA;AAAA,EAGA,OAAO,MAA0B;AAC/B,UAAM,OAAO,KAAK,WAAW,IAAI;AAAA,MAC/B,WAAW,KAAK;AAAA,MAChB,UAAU,KAAK;AAAA,MACf,WAAW,KAAK;AAAA,MAChB,QAAQ,KAAK,UAAU,OAAO;AAAA,MAC9B,UAAU,KAAK,YAAY;AAAA,MAC3B,QAAQ,KAAK,UAAU;AAAA,MACvB,YAAY,KAAK,cAAc;AAAA,MAC/B,OAAO,KAAK,SAAS,OAAO,OAAO,KAAK,UAAU,KAAK,KAAK;AAAA,IAC9D,CAAC;AACD,WAAO,KAAK,YAAY;AAAA,EAC1B;AAAA;AAAA,EAGA,OAAO,WAAmB,UAAkB,WAAmB,KAAa,OAAO,GAAY;AAC7F,WAAO,KAAK,WAAW,IAAI,IAAI,WAAW,UAAU,SAAS,EAAE,YAAY;AAAA,EAC7E;AAAA;AAAA,EAGA,UAAU,MAAiB,KAAa,OAAO,GAAS;AACtD,SAAK,OAAO,KAAK,WAAW,KAAK,UAAU,KAAK,WAAW,EAAE;AAC7D,SAAK,OAAO,IAAI;AAAA,EAClB;AAAA,EAEA,QAAQ,WAAmB,UAAkB,WAAwC;AACnF,UAAM,MAAM,KAAK,YAAY,IAAI,WAAW,UAAU,SAAS;AAC/D,WAAO,QAAQ,SAAY,SAAY,UAAU,GAAG;AAAA,EACtD;AAAA,EAEA,KAAK,WAA8B;AACjC,WAAQ,KAAK,SAAS,IAAI,SAAS,EAAmB,IAAI,SAAS;AAAA,EACrE;AAAA,EAEA,GAAG,WAA8B;AAC/B,WAAQ,KAAK,OAAO,IAAI,SAAS,EAAmB,IAAI,SAAS;AAAA,EACnE;AACF;;;AC5IO,SAAS,mBAAmB,EAAE,OAAO,cAAc,IAAe,CAAC,GAAW;AACnF,QAAM,IAAI,WAAW,IAAI;AACzB,SAAO;AAAA,iCACwB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCASD,WAAW,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;AAAA,iCACtC,WAAW,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC;AAAA;AAEzE;AAGO,SAAS,iBAAiB,EAAE,OAAO,WAAW,IAAe,CAAC,GAAW;AAC9E,QAAM,IAAI,WAAW,IAAI;AACzB,SAAO;AAAA,iCACwB,CAAC;AAAA;AAAA;AAAA,wCAGM,OAAO;AAAA;AAAA;AAAA,iCAGd,WAAW,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC;AAAA;AAEtE;AAGO,SAAS,eAAe,EAAE,OAAO,SAAS,IAAe,CAAC,GAAW;AAC1E,QAAM,IAAI,WAAW,IAAI;AACzB,SAAO;AAAA,iCACwB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0CAQQ,OAAO;AAAA;AAAA;AAAA,iCAGhB,WAAW,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;AAAA,iCACtC,WAAW,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC;AAAA;AAEzE;AAmCA,SAAS,YAAY,KAA8B;AACjD,SAAO;AAAA,IACL,KAAK,IAAI;AAAA,IACT,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,WAAW,IAAI;AAAA,IACf,OAAO,IAAI,UAAU,OAAO,OAAQ,KAAK,MAAM,IAAI,KAAK;AAAA,IACxD,QAAQ,IAAI;AAAA,IACZ,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA,EAChB;AACF;AAGO,IAAM,cAAN,MAAkB;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,IAAQ,EAAE,OAAO,SAAS,IAAe,CAAC,GAAG;AACvD,UAAM,IAAI,WAAW,IAAI;AACzB,SAAK,aAAa,GAAG;AAAA,MACnB,eAAe,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAKlB;AACA,SAAK,UAAU,GAAG,QAAQ,iBAAiB,CAAC,gBAAgB;AAC5D,SAAK,aAAa,GAAG,QAAQ,UAAU,CAAC,wDAAwD;AAChG,SAAK,aAAa,GAAG,QAAQ,iBAAiB,CAAC,oDAAoD;AAAA,EACrG;AAAA;AAAA,EAGA,OAAO,QAA2B;AAChC,SAAK,WAAW,IAAI;AAAA,MAClB,KAAK,OAAO;AAAA,MACZ,MAAM,OAAO;AAAA,MACb,MAAM,OAAO,QAAQ;AAAA,MACrB,WAAW,OAAO,aAAa;AAAA,MAC/B,OAAO,OAAO,SAAS,OAAO,OAAO,KAAK,UAAU,OAAO,KAAK;AAAA,MAChE,QAAQ,OAAO,UAAU,OAAO;AAAA,IAClC,CAAC;AAAA,EACH;AAAA,EAEA,IAAIA,MAAoC;AACtC,UAAM,MAAM,KAAK,QAAQ,IAAIA,IAAG;AAChC,WAAO,QAAQ,SAAY,SAAY,YAAY,GAAG;AAAA,EACxD;AAAA,EAEA,OAAOA,MAAa,KAAa,OAAO,GAAY;AAClD,WAAO,KAAK,WAAW,IAAI,IAAIA,IAAG,EAAE,YAAY;AAAA,EAClD;AAAA,EAEA,WAAW,MAA2B;AACpC,WAAQ,KAAK,WAAW,IAAI,IAAI,EAAqB,IAAI,WAAW;AAAA,EACtE;AACF;;;AC7IA,SAAS,MAAM,MAA6C;AAC1D,SAAO,EAAE,MAAM,WAAW,GAAG,IAAI,OAAO,GAAG,KAAK,WAAW,GAAG,IAAI,MAAM,EAAE;AAC5E;AAEO,SAAS,iBAAiB,EAAE,OAAO,SAAS,IAAoB,CAAC,GAAW;AACjF,QAAM,EAAE,MAAM,IAAI,IAAI,MAAM,IAAI;AAChC,SAAO;AAAA,iCACwB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCASJ,WAAW,OAAO,IAAI,aAAa,CAAC,OAAO,IAAI;AAAA,yCACvC,GAAG;AAAA;AAE5C;AAoDA,SAAS,iBAAiB,QAAwB;AAChD,QAAM,OAAO,OAAO,WAAW,OAAO,SAAS,CAAC;AAChD,SAAO,OAAO,MAAM,GAAG,EAAE,IAAI,OAAO,aAAa,OAAO,CAAC;AAC3D;AAGA,SAAS,UAAU,OAA2B;AAC5C,SAAO,MAAM,gBAAgB,UAAa,MAAM,YAAY,SAAS;AACvE;AAGA,SAAS,YAAY,OAAuC;AAC1D,QAAM,SAA8B,CAAC;AACrC,MAAI,UAAU,KAAK,EAAG,QAAO,KAAK,MAAM,aAAc,iBAAiB,MAAM,WAAY,CAAC;AAC1F,MAAI,MAAM,WAAW,OAAW,QAAO,KAAK,GAAG,MAAM,MAAM;AAC3D,SAAO;AACT;AAEO,IAAM,gBAAN,MAAoB;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA,oBAAoB,oBAAI,IAAuC;AAAA,EAC/D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,IAAQ,EAAE,OAAO,SAAS,IAAoB,CAAC,GAAG;AAC5D,UAAM,EAAE,MAAM,IAAI,IAAI,MAAM,IAAI;AAChC,SAAK,KAAK;AACV,SAAK,aAAa,EAAE,MAAM,IAAI;AAG9B,SAAK,aAAa,GAAG;AAAA,MACnB,eAAe,IAAI;AAAA,uFAC8D,IAAI,wDAAwD,GAAG;AAAA;AAAA,IAElJ;AACA,SAAK,WAAW,GAAG;AAAA,MACjB,uBAAuB,IAAI;AAAA,IAC7B;AACA,SAAK,YAAY,GAAG,QAAQ,eAAe,GAAG,8BAA8B;AAC5E,SAAK,aAAa,GAAG;AAAA,MACnB;AAAA,cACQ,GAAG,WAAW,IAAI;AAAA,eACjB,GAAG;AAAA,IACd;AACA,SAAK,eAAe,GAAG,QAAQ,eAAe,GAAG,IAAI,GAAG,yBAAyB;AACjF,SAAK,kBAAkB,GAAG,QAAQ,eAAe,IAAI,sBAAsB;AAC3E,SAAK,aAAa,GAAG,QAAQ,6BAA6B,IAAI,EAAE;AAChE,SAAK,WAAW,GAAG,QAAQ,6BAA6B,GAAG,EAAE;AAAA,EAC/D;AAAA;AAAA,EAGA,MAAM,MAAiB,MAAsB;AAC3C,UAAM,WAAW,KAAK,WAAW,IAAI,KAAK,UAAU,KAAK,OAAO,KAAK,UAAU,KAAK,YAAY,KAAK,UAAU;AAC/G,UAAM,SAAU,KAAK,SAAS,IAAI,KAAK,UAAU,KAAK,OAAO,KAAK,UAAU,KAAK,UAAU,EAA0B;AACrH,QAAI,SAAS,YAAY,EAAG,MAAK,UAAU,IAAI,QAAQ,IAAI;AAC3D,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,OAAO,OAAe,QAAQ,IAAI,OAA8B;AAC9D,UAAM,OACJ,QAAQ,KAAK,gBAAgB,KAAK,EAAE,IAAI,CAAC,OAAO,GAAG,YAAY,KAAK,GAAG,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,OAAO,KAAK;AAEnH,WAAO,KAAK,IAAI,CAAC,OAAO;AAAA,MACtB,QAAQ,EAAE;AAAA,MACV,UAAU,EAAE;AAAA,MACZ,OAAO,EAAE;AAAA,MACT,UAAU,EAAE;AAAA,MACZ,YAAY,EAAE;AAAA,MACd,YAAY,EAAE;AAAA,MACd,MAAM,EAAE;AAAA,IACV,EAAE;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,gBAAgB,OAA6C;AACnE,UAAM,aAAa,MAAM,QAAQ,UAAU;AAC3C,UAAM,MAAM,GAAG,UAAU,KAAK,IAAI,IAAI,CAAC,IAAI,MAAM,WAAW,SAAY,QAAQ,UAAU;AAC1F,UAAM,SAAS,KAAK,kBAAkB,IAAI,GAAG;AAC7C,QAAI,OAAQ,QAAO;AAEnB,UAAM,EAAE,MAAM,IAAI,IAAI,KAAK;AAC3B,UAAM,aAAa,CAAC,GAAG,GAAG,UAAU;AAEpC,QAAI,UAAU,KAAK,EAAG,YAAW,KAAK,sCAAsC;AAC5E,QAAI,MAAM,WAAW,QAAW;AAC9B,iBAAW,KAAK,eAAe,IAAI,MAAM,eAAe,IAAI,MAAM,UAAU,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI,CAAC,GAAG;AAAA,IACvG;AACA,UAAM,YAAY,KAAK,GAAG;AAAA,MACxB;AAAA,cACQ,GAAG,WAAW,IAAI;AAAA,eACjB,WAAW,KAAK,OAAO,CAAC;AAAA,IACnC;AACA,SAAK,kBAAkB,IAAI,KAAK,SAAS;AACzC,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,WAAW,UAA0B;AACnC,WAAO,KAAK,gBAAgB,IAAI,QAAQ,EAAE;AAAA,EAC5C;AAAA;AAAA,EAGA,cAAsB;AACpB,UAAM,MAAO,KAAK,SAAS,IAAI,EAAoB;AACnD,QAAI,QAAQ,EAAG,QAAO;AACtB,UAAM,QAAS,KAAK,WAAW,IAAI,EAAoB;AACvD,WAAO,KAAK,IAAI,GAAG,MAAM,KAAK,IAAI;AAAA,EACpC;AAAA;AAAA,EAGA,aAAmB;AACjB,SAAK,aAAa,IAAI;AAAA,EACxB;AACF;;;AC/MO,SAAS,kBAAkB,EAAE,OAAO,YAAY,IAAsB,CAAC,GAAW;AACvF,QAAM,IAAI,WAAW,IAAI;AACzB,SAAO;AAAA,iCACwB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+CAWa,OAAO;AAAA;AAAA;AAGtD;AA4BA,SAAS,MAAM,KAAoC;AACjD,SAAO;AAAA,IACL,UAAU,IAAI;AAAA,IACd,WAAW,IAAI;AAAA,IACf,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,eAAe,IAAI;AAAA,IACnB,UAAU,IAAI;AAAA,IACd,WAAW,IAAI;AAAA,IACf,aAAa,IAAI;AAAA,IACjB,QAAQ,IAAI;AAAA,IACZ,cAAc,IAAI;AAAA,EACpB;AACF;AAWO,IAAM,iBAAN,MAAqB;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,IAAQ,EAAE,OAAO,YAAY,IAAsB,CAAC,GAAG;AACjE,UAAM,IAAI,WAAW,IAAI;AACzB,SAAK,aAAa,GAAG,QAAQ,eAAe,CAAC,8DAA8D;AAC3G,SAAK,UAAU,GAAG,QAAQ,iBAAiB,CAAC,uBAAuB;AACnE,SAAK,cAAc,GAAG;AAAA,MACpB,UAAU,CAAC,gFAAgF,OAAO;AAAA;AAAA;AAAA;AAAA,IAIpG;AACA,SAAK,aAAa,GAAG,QAAQ,UAAU,CAAC,yDAAyD;AACjG,SAAK,aAAa,GAAG,QAAQ,UAAU,CAAC,+DAA+D;AACvG,SAAK,WAAW,GAAG,QAAQ,iBAAiB,CAAC,qBAAqB;AAAA,EACpE;AAAA;AAAA,EAGA,OAAO,WAAiC;AACtC,SAAK,WAAW,IAAI,SAAS;AAC7B,WAAO,MAAM,KAAK,QAAQ,IAAI,SAAS,CAAoB;AAAA,EAC7D;AAAA,EAEA,IAAI,WAA6C;AAC/C,UAAM,MAAM,KAAK,QAAQ,IAAI,SAAS;AACtC,WAAO,QAAQ,SAAY,SAAY,MAAM,GAAG;AAAA,EAClD;AAAA,EAEA,QAAQ,WAAmB,QAAgC;AACzD,SAAK,YAAY,IAAI;AAAA,MACnB;AAAA,MACA,YAAY,OAAO;AAAA,MACnB,YAAY,OAAO,cAAc;AAAA,MACjC,UAAU,OAAO,YAAY;AAAA,MAC7B,WAAW,OAAO,aAAa;AAAA,MAC/B,aAAa,OAAO,eAAe;AAAA,IACrC,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,OAAO,WAAyB;AAC9B,SAAK,WAAW,IAAI,SAAS;AAAA,EAC/B;AAAA,EAEA,WAAW,WAAmB,QAAyB,SAAwB,MAAY;AACzF,SAAK,WAAW,IAAI,QAAQ,QAAQ,SAAS;AAAA,EAC/C;AAAA,EAEA,OAAuB;AACrB,WAAQ,KAAK,SAAS,IAAI,EAAwB,IAAI,KAAK;AAAA,EAC7D;AACF;;;ACxHA,IAAM,YAA6C;AAAA,EACjD,CAAC,YAAY,gBAAgB;AAAA,EAC7B,CAAC,cAAc,kBAAkB;AAAA,EACjC,CAAC,UAAU,cAAc;AAAA,EACzB,CAAC,QAAQ,YAAY;AAAA,EACrB,CAAC,aAAa,iBAAiB;AAAA,EAC/B,CAAC,WAAW,gBAAgB;AAAA,EAC5B,CAAC,aAAa,iBAAiB;AACjC;AAGO,SAAS,OAAO,WAAiC;AACtD,SAAO,UAAU,OAAO,CAAC,CAAC,GAAG,MAAM,UAAU,GAAG,MAAM,MAAS,EAC5D,IAAI,CAAC,CAAC,KAAK,OAAO,MAAM,QAAQ,UAAU,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,MAAM,UAAU,GAAG,EAAY,CAAC,CAAC,EAClG,KAAK,IAAI;AACd;AAGO,SAAS,aAAa,SAAiB,WAAyB,OAAO,cAAyB;AACrG,SAAO,EAAE,SAAS,MAAM,IAAI,CAAC,OAAO,GAAG,KAAK,OAAO,SAAS,CAAC,EAAE;AACjE;","names":["ref"]}
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 /**\n * The highest migration version this runtime knows. When set, opening a database\n * stamped past it throws `SchemaTooNewError` instead of quietly proceeding.\n */\n schemaVersion?: number;\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 if (options.schemaVersion !== undefined) assertOrClose(db, options.schemaVersion);\n return db;\n}\n\nfunction assertOrClose(db: Db, schemaVersion: number): void {\n try {\n assertSchemaVersion(db, schemaVersion);\n } catch (err) {\n db.close();\n throw err;\n }\n}\n\n/** The one table name the kit reserves: every migration a store applies is recorded here. */\nexport const MIGRATION_TABLE_NAME = \"_migration\";\n\n/**\n * Thrown when a database carries a schema version this runtime has never applied.\n *\n * Adapted from beads (MIT), `internal/storage/embeddeddolt/store.go`: a stale binary whose\n * migrations silently no-op dies later on a column the newer schema dropped, a long way\n * from the cause.\n */\nexport class SchemaTooNewError extends Error {\n constructor(\n readonly storedVersion: number,\n readonly knownVersion: number,\n ) {\n super(\n `database schema is at version ${storedVersion}, but this runtime knows only up to ${knownVersion}; ` +\n \"upgrade the package that owns these migrations, or open an older copy of the database\",\n );\n this.name = \"SchemaTooNewError\";\n }\n}\n\n/**\n * Refuse a database stamped newer than `knownVersion`. A no-op on one that has never been\n * migrated, so a fresh file opens normally.\n *\n * Only a caller that owns the whole schema can name that version: a store layering its own\n * migrations onto a shared database sees one band of versions, not the top. That is why\n * this is opt-in rather than a check inside `runMigrations`.\n */\nexport function assertSchemaVersion(db: Db, knownVersion: number): void {\n const stored = storedSchemaVersion(db);\n if (stored !== undefined && stored > knownVersion) throw new SchemaTooNewError(stored, knownVersion);\n}\n\n/** Reads without creating the table, so a read-only connection can ask. */\nfunction storedSchemaVersion(db: Db): number | undefined {\n if (!hasTable(db, MIGRATION_TABLE_NAME)) return undefined;\n const row = db.prepare(`SELECT MAX(version) AS version FROM ${MIGRATION_TABLE_NAME}`).get() as { version: number | null };\n return row.version ?? undefined;\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 { MIGRATION_TABLE_NAME, 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_TABLE_NAME} (\n version INTEGER PRIMARY KEY,\n name TEXT,\n applied_at TEXT NOT NULL\n )`;\n\nexport class MigrationIdentityError extends Error {\n constructor(\n readonly version: number,\n readonly recordedName: string,\n readonly declaredName: string,\n ) {\n super(\n `migration version ${version} was applied as \"${recordedName}\", but this runtime declares it as \"${declaredName}\"; ` +\n \"two migration lists have collided on that version and the database matches neither\",\n );\n this.name = \"MigrationIdentityError\";\n }\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 = appliedNames(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)) {\n assertSameIdentity(m, applied.get(m.version) ?? null);\n continue;\n }\n applyOne(m);\n done.push(m.version);\n }\n return done;\n}\n\n/**\n * A version already applied under a different name means two migration lists\n * have collided on that number, so the database is not the shape either expects.\n */\nfunction assertSameIdentity(m: Migration, recordedName: string | null): void {\n if (m.name === undefined || recordedName === null) return;\n if (m.name === recordedName) return;\n throw new MigrationIdentityError(m.version, recordedName, m.name);\n}\n\nfunction appliedNames(db: Db): Map<number, string | null> {\n db.exec(MIGRATION_TABLE);\n const rows = db.prepare(\"SELECT version, name FROM _migration\").all() as { version: number; name: string | null }[];\n return new Map(rows.map((r) => [r.version, r.name]));\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 private readonly supersedeTxn;\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 this.supersedeTxn = db.transaction((edge: EdgeInput, at: string) => {\n this.expire(edge.sourceRef, edge.relation, edge.targetRef, at);\n this.assert(edge);\n });\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; a failed insert rolls the expire back. */\n supersede(edge: EdgeInput, at: string = nowIso()): void {\n this.supersedeTxn(edge, at);\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\n/**\n * Narrow a search to one class of owner, so one class cannot flood a result set.\n *\n * A span table that holds several entity classes holds them in wildly different\n * volumes. Measured on active-work's graph: 92,713 transcript spans against\n * 1,569 note spans, 59 to 1. Ranking one pooled query returns transcript\n * chatter and the notes never surface, so the fix is a separate ranked list per\n * class, which needs a way to ask for one class.\n *\n * `ownerPrefix` and `fields` are both needed, not either. active-work keys both\n * a mined transcript and a workspace session record under `session:`, and they\n * are separable only by field — `body` is the record, `prompt` and the\n * `tool_*` fields are the transcript. A prefix-only filter would put 92,713\n * transcript spans into the records' list and look like it was working.\n */\nexport interface SpanScope {\n /** Match owners whose ref starts with this, e.g. `note:`. A range scan, so the owner index applies. */\n ownerPrefix?: string;\n /** Match only these fields, e.g. `[\"title\", \"body\"]`. Empty means no field matches. */\n fields?: string[];\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\n/** The string just past the prefix, so a prefix match is a half-open range the index can serve. */\nfunction prefixUpperBound(prefix: string): string {\n const last = prefix.charCodeAt(prefix.length - 1);\n return prefix.slice(0, -1) + String.fromCharCode(last + 1);\n}\n\n/** An empty prefix means \"every owner\", which is what omitting it already means. */\nfunction hasPrefix(scope: SpanScope): boolean {\n return scope.ownerPrefix !== undefined && scope.ownerPrefix.length > 0;\n}\n\n/** Bind values for `scopedStatement`'s predicates, in the order it writes them. */\nfunction scopeValues(scope: SpanScope): (string | number)[] {\n const values: (string | number)[] = [];\n if (hasPrefix(scope)) values.push(scope.ownerPrefix!, prefixUpperBound(scope.ownerPrefix!));\n if (scope.fields !== undefined) values.push(...scope.fields);\n return values;\n}\n\nexport class SpanFtsTables {\n private readonly db: Db;\n private readonly tableNames: { span: string; fts: string };\n private readonly insertSpan;\n private readonly findSpan;\n private readonly insertFts;\n private readonly hasFtsRow;\n private readonly searchStmt;\n /** One prepared statement per scope shape, since the predicate list varies. */\n private readonly scopedSearchStmts = new Map<string, ReturnType<Db[\"prepare\"]>>();\n private readonly deleteAllFts;\n private readonly deleteSpansStmt;\n private readonly countSpans;\n private readonly countFts;\n private readonly indexTxn;\n\n constructor(db: Db, { name = \"search\" }: SpanFtsOptions = {}) {\n const { span, fts } = names(name);\n this.db = db;\n this.tableNames = { span, fts };\n // Contentless FTS retains orphan rowids after a purge. Never reuse one: old\n // tokens would otherwise become visible through an unrelated replacement span.\n this.insertSpan = db.prepare(\n `INSERT INTO ${span} (span_id, owner_ref, field, source_id, byte_offset, byte_length)\n VALUES ((SELECT MAX(id) + 1 FROM (SELECT COALESCE(MAX(span_id), 0) AS id FROM ${span} UNION ALL SELECT COALESCE(MAX(rowid), 0) AS id FROM ${fts})), ?, ?, ?, ?, ?)\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.hasFtsRow = db.prepare(`SELECT 1 FROM ${fts} WHERE rowid = ?`);\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 // One transaction, so a failed FTS insert cannot strand a span row with no FTS row.\n this.indexTxn = db.transaction((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.hasFtsRow.get(spanId) === undefined) this.insertFts.run(spanId, text);\n return spanId;\n });\n }\n\n /**\n * Index `text` for a span, storing only the locator. Returns the span id.\n *\n * Re-indexing a span that still has its FTS row is a no-op; re-indexing one\n * whose row `clearIndex` removed puts the row back, which is what makes\n * clear-then-re-stream a rebuild. Only the conflict path pays the rowid probe.\n */\n index(span: SpanInput, text: string): number {\n return this.indexTxn(span, text);\n }\n\n /**\n * FTS5 MATCH, joined through the span table so stranded FTS rows never surface.\n *\n * `scope` narrows to one class of owner; `limit` is applied after it, so a\n * scoped search returns its own top-N rather than whatever survives the\n * filter out of a global top-N.\n */\n search(query: string, limit = 50, scope?: SpanScope): SpanHit[] {\n const rows = (\n scope ? this.scopedStatement(scope).all([query, ...scopeValues(scope), limit]) : this.searchStmt.all(query, limit)\n ) as RawSpanHit[];\n return rows.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 /**\n * Prepared statement for one scope shape, cached.\n *\n * Cached by shape rather than by values, because a product registers a fixed\n * handful of class retrievers and then queries them for the life of the\n * process. Keyed on the field *count* and whether a prefix is present, which\n * is everything that changes the SQL.\n */\n private scopedStatement(scope: SpanScope): ReturnType<Db[\"prepare\"]> {\n const fieldCount = scope.fields?.length ?? 0;\n const key = `${hasPrefix(scope) ? 1 : 0}:${scope.fields === undefined ? \"all\" : fieldCount}`;\n const cached = this.scopedSearchStmts.get(key);\n if (cached) return cached;\n\n const { span, fts } = this.tableNames;\n const predicates = [`${fts} MATCH ?`];\n // A half-open range rather than LIKE or GLOB, so the owner index applies.\n if (hasPrefix(scope)) predicates.push(\"s.owner_ref >= ? AND s.owner_ref < ?\");\n if (scope.fields !== undefined) {\n predicates.push(fieldCount === 0 ? \"0\" : `s.field IN (${new Array(fieldCount).fill(\"?\").join(\", \")})`);\n }\n const statement = this.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 ${predicates.join(\" AND \")} ORDER BY f.rank LIMIT ?`,\n );\n this.scopedSearchStmts.set(key, statement);\n return statement;\n }\n\n /** Drop an owner's spans. Their FTS rows become orphans, hidden by the join, until `clearIndex` and a re-stream. */\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 `clearIndex` and re-stream. */\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 /**\n * Empty the FTS index, keeping every span row. Re-streaming each surviving\n * span's text through `index` then rebuilds it with the same span ids.\n */\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","span"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@titan-design/store-sqlite",
3
- "version": "0.3.0",
3
+ "version": "0.3.3",
4
4
  "description": "SQLite table-factory kit: entity/edge (bi-temporal), content-addressed cache, contentless FTS5, watermark, migrations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",