@titan-design/store-sqlite 0.3.1 → 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
@@ -140,12 +140,13 @@ declare class EdgeTable {
140
140
  private readonly currentStmt;
141
141
  private readonly fromStmt;
142
142
  private readonly toStmt;
143
+ private readonly supersedeTxn;
143
144
  constructor(db: Db, { name }?: EdgeTableOptions);
144
145
  /** Assert an edge. A live identical edge is left alone (idempotent re-ingest). Returns true when inserted. */
145
146
  assert(edge: EdgeInput): boolean;
146
147
  /** Retract the current belief in an edge (invalidate, never delete). Returns true when one was live. */
147
148
  expire(sourceRef: string, relation: string, targetRef: string, at?: string): boolean;
148
- /** 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. */
149
150
  supersede(edge: EdgeInput, at?: string): void;
150
151
  current(sourceRef: string, relation: string, targetRef: string): EdgeRow | undefined;
151
152
  from(sourceRef: string): EdgeRow[];
@@ -290,6 +291,7 @@ declare class SpanFtsTables {
290
291
  private readonly insertSpan;
291
292
  private readonly findSpan;
292
293
  private readonly insertFts;
294
+ private readonly hasFtsRow;
293
295
  private readonly searchStmt;
294
296
  /** One prepared statement per scope shape, since the predicate list varies. */
295
297
  private readonly scopedSearchStmts;
@@ -297,8 +299,15 @@ declare class SpanFtsTables {
297
299
  private readonly deleteSpansStmt;
298
300
  private readonly countSpans;
299
301
  private readonly countFts;
302
+ private readonly indexTxn;
300
303
  constructor(db: Db, { name }?: SpanFtsOptions);
301
- /** 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
+ */
302
311
  index(span: SpanInput, text: string): number;
303
312
  /**
304
313
  * FTS5 MATCH, joined through the span table so stranded FTS rows never surface.
@@ -317,11 +326,14 @@ declare class SpanFtsTables {
317
326
  * is everything that changes the SQL.
318
327
  */
319
328
  private scopedStatement;
320
- /** 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. */
321
330
  purgeOwner(ownerRef: string): number;
322
- /** 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. */
323
332
  orphanRatio(): number;
324
- /** 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
+ */
325
337
  clearIndex(): void;
326
338
  }
327
339
 
package/dist/index.js CHANGED
@@ -254,6 +254,7 @@ var EdgeTable = class {
254
254
  currentStmt;
255
255
  fromStmt;
256
256
  toStmt;
257
+ supersedeTxn;
257
258
  constructor(db, { name = "edge" } = {}) {
258
259
  const t = quoteIdent(name);
259
260
  this.insertStmt = db.prepare(
@@ -269,6 +270,10 @@ var EdgeTable = class {
269
270
  );
270
271
  this.fromStmt = db.prepare(`SELECT * FROM ${t} WHERE source_ref = ? AND t_expired IS NULL ORDER BY edge_id`);
271
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
+ });
272
277
  }
273
278
  /** Assert an edge. A live identical edge is left alone (idempotent re-ingest). Returns true when inserted. */
274
279
  assert(edge) {
@@ -288,10 +293,9 @@ var EdgeTable = class {
288
293
  expire(sourceRef, relation, targetRef, at = nowIso()) {
289
294
  return this.expireStmt.run(at, sourceRef, relation, targetRef).changes === 1;
290
295
  }
291
- /** 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. */
292
297
  supersede(edge, at = nowIso()) {
293
- this.expire(edge.sourceRef, edge.relation, edge.targetRef, at);
294
- this.assert(edge);
298
+ this.supersedeTxn(edge, at);
295
299
  }
296
300
  current(sourceRef, relation, targetRef) {
297
301
  const raw = this.currentStmt.get(sourceRef, relation, targetRef);
@@ -445,6 +449,7 @@ var SpanFtsTables = class {
445
449
  insertSpan;
446
450
  findSpan;
447
451
  insertFts;
452
+ hasFtsRow;
448
453
  searchStmt;
449
454
  /** One prepared statement per scope shape, since the predicate list varies. */
450
455
  scopedSearchStmts = /* @__PURE__ */ new Map();
@@ -452,6 +457,7 @@ var SpanFtsTables = class {
452
457
  deleteSpansStmt;
453
458
  countSpans;
454
459
  countFts;
460
+ indexTxn;
455
461
  constructor(db, { name = "search" } = {}) {
456
462
  const { span, fts } = names(name);
457
463
  this.db = db;
@@ -465,6 +471,7 @@ var SpanFtsTables = class {
465
471
  `SELECT span_id FROM ${span} WHERE owner_ref = ? AND field = ? AND source_id = ? AND byte_offset = ?`
466
472
  );
467
473
  this.insertFts = db.prepare(`INSERT INTO ${fts} (rowid, text) VALUES (?, ?)`);
474
+ this.hasFtsRow = db.prepare(`SELECT 1 FROM ${fts} WHERE rowid = ?`);
468
475
  this.searchStmt = db.prepare(
469
476
  `SELECT s.span_id, s.owner_ref, s.field, s.source_id, s.byte_offset, s.byte_length, f.rank
470
477
  FROM ${fts} f JOIN ${span} s ON s.span_id = f.rowid
@@ -474,13 +481,22 @@ var SpanFtsTables = class {
474
481
  this.deleteSpansStmt = db.prepare(`DELETE FROM ${span} WHERE owner_ref = ?`);
475
482
  this.countSpans = db.prepare(`SELECT count(*) AS n FROM ${span}`);
476
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
+ });
477
490
  }
478
- /** 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
+ */
479
498
  index(span, text) {
480
- const inserted = this.insertSpan.run(span.ownerRef, span.field, span.sourceId, span.byteOffset, span.byteLength);
481
- const spanId = this.findSpan.get(span.ownerRef, span.field, span.sourceId, span.byteOffset).span_id;
482
- if (inserted.changes === 1) this.insertFts.run(spanId, text);
483
- return spanId;
499
+ return this.indexTxn(span, text);
484
500
  }
485
501
  /**
486
502
  * FTS5 MATCH, joined through the span table so stranded FTS rows never surface.
@@ -528,18 +544,21 @@ var SpanFtsTables = class {
528
544
  this.scopedSearchStmts.set(key, statement);
529
545
  return statement;
530
546
  }
531
- /** 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. */
532
548
  purgeOwner(ownerRef) {
533
549
  return this.deleteSpansStmt.run(ownerRef).changes;
534
550
  }
535
- /** 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. */
536
552
  orphanRatio() {
537
553
  const fts = this.countFts.get().n;
538
554
  if (fts === 0) return 0;
539
555
  const spans = this.countSpans.get().n;
540
556
  return Math.max(0, fts - spans) / fts;
541
557
  }
542
- /** 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
+ */
543
562
  clearIndex() {
544
563
  this.deleteAllFts.run();
545
564
  }
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 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\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":"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","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.1",
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",