@equationalapplications/core-llm-wiki 7.11.2 → 7.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,187 +1,25 @@
1
- import { BaseRepository, WikiGraphNodeOwnershipConflict, liveAtSql, __privateAdd, MetadataRepository, EmbeddingService, SearchService, TemporalService, JobManager, PromptService, resolveGrounding, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, DiagnosticBuffer, validateManifest, WikiDraftNotFound, generateId, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, typeSatisfies, normalizeTitleKey, edgeTripleAllowed, MAX_EMBED_ATTEMPTS, UsageMeter, WikiBusyError, WikiInvalidReadOptions, isLiveAt, packFactsByBudget, factTokenCost, WikiTransactionError, extractSqliteCode } from './chunk-522SCN2E.mjs';
2
- export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, EMBED_CHARS_CEILING, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiSupersedeError, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-522SCN2E.mjs';
1
+ import { BaseRepository, WikiGraphNodeOwnershipConflict, parseJsonObject, parseJsonArray, liveAtSql, __privateAdd, MetadataRepository, EmbeddingService, SearchService, TemporalService, JobManager, PromptService, resolveGrounding, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, createLegacySourceTypeIndex, writeNormalizeStamp, CURRENT_SCHEMA_VERSION, detectSchemaVersion, MIGRATIONS, runSourceRefNormalize, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, DiagnosticBuffer, validateManifest, WikiDraftNotFound, generateId, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, typeSatisfies, normalizeTitleKey, edgeTripleAllowed, MAX_EMBED_ATTEMPTS, UsageMeter, isWikiSearchUnavailableError, WikiBusyError, WikiInvalidReadOptions, isLiveAt, packFactsByBudget, factTokenCost, probePendingWork, WikiMigrationRequiredError, setupDatabase, UnavailableIndexStrategy, WikiTransactionError, extractSqliteCode } from './chunk-KN4XSLLB.mjs';
2
+ export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, EMBED_CHARS_CEILING, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiMigrationRequiredError, WikiParseError, WikiSearchUnavailableError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiSupersedeError, WikiTransactionError, chunkText, configureRandomSource, isWikiSearchUnavailableError, parseEmbedding, safeSlice, validateManifest } from './chunk-KN4XSLLB.mjs';
3
3
  import { deriveTrustTier, isStaleAfter, appendRelatedSection, buildConceptDocument, buildLogMd, buildEntityIndexMd, buildRootIndexMd, isAllowedOkfPath, parseRootIndexMd, parseEntityIndexMd, parseConcept, splitRelatedSection, extractMarkdownLinks, parseLogMd, parseEventIdComment, latestVerified, formatSourcesJson, appendEventIdComment, parseVerifiedFlexible, parseCitationsList } from '@equationalapplications/core-okf';
4
4
  import MiniSearch from 'minisearch';
5
5
 
6
- // src/db/schema.ts
7
- async function setupDatabase(db, prefix) {
8
- await db.execAsync(`
9
- CREATE TABLE IF NOT EXISTS ${prefix}entries (
10
- id TEXT PRIMARY KEY,
11
- entity_id TEXT NOT NULL,
12
- title TEXT NOT NULL,
13
- body TEXT NOT NULL,
14
- tags TEXT NOT NULL DEFAULT '[]',
15
- confidence TEXT NOT NULL DEFAULT 'inferred',
16
- source_type TEXT NOT NULL DEFAULT 'librarian_inferred',
17
- source_hash TEXT,
18
- source_ref TEXT,
19
- created_at INTEGER NOT NULL,
20
- updated_at INTEGER NOT NULL,
21
- last_accessed_at INTEGER,
22
- access_count INTEGER NOT NULL DEFAULT 0,
23
- deleted_at INTEGER,
24
- embedding TEXT,
25
- embedding_blob BLOB,
26
- okf_type TEXT,
27
- ontology_checked_at INTEGER,
28
- heal_checked_at INTEGER,
29
- lifecycle_status TEXT NOT NULL DEFAULT 'stable',
30
- stale_after INTEGER,
31
- generated_by TEXT,
32
- last_verified_at INTEGER,
33
- last_verified_by TEXT,
34
- okf_sources TEXT,
35
- okf_verified TEXT,
36
- okf_usage_window TEXT,
37
- embedding_failed_at INTEGER,
38
- embedding_failure_kind TEXT,
39
- embedding_attempts INTEGER NOT NULL DEFAULT 0,
40
- valid_from INTEGER,
41
- valid_to INTEGER,
42
- superseded_by TEXT,
43
- superseded_at INTEGER
44
- );
45
-
46
- CREATE INDEX IF NOT EXISTS ${prefix}entries_entity_idx ON ${prefix}entries(entity_id);
47
- CREATE INDEX IF NOT EXISTS ${prefix}entries_source_ref_idx ON ${prefix}entries(entity_id, source_ref);
48
- CREATE INDEX IF NOT EXISTS ${prefix}entries_source_hash_idx ON ${prefix}entries(entity_id, source_hash) WHERE source_hash IS NOT NULL;
49
- CREATE INDEX IF NOT EXISTS ${prefix}entries_updated_idx ON ${prefix}entries(updated_at DESC);
50
-
51
- -- source_ref_index: per-(entity, source_hash) record of the canonical sourceRef
52
- -- currently holding that hash. The partial UNIQUE index on (entity_id, source_hash)
53
- -- WHERE deleted_at IS NULL enforces the sourceRef-level TOCTOU-race invariant;
54
- -- entries-level uniqueness cannot express it because a single ingestDocument call
55
- -- writes N facts that all share (entity_id, source_ref, source_hash). See
56
- -- docs/superpowers/specs/2026-08-07-dependabot-concurrency-release-hygiene-design.md \xA7B1.
57
- CREATE TABLE IF NOT EXISTS ${prefix}source_ref_index (
58
- id TEXT PRIMARY KEY,
59
- entity_id TEXT NOT NULL,
60
- source_hash TEXT NOT NULL,
61
- source_ref TEXT NOT NULL,
62
- created_at INTEGER NOT NULL,
63
- deleted_at INTEGER
64
- );
65
- CREATE UNIQUE INDEX IF NOT EXISTS ${prefix}idx_source_ref_hash
66
- ON ${prefix}source_ref_index (entity_id, source_hash)
67
- WHERE deleted_at IS NULL;
68
-
69
- CREATE TABLE IF NOT EXISTS ${prefix}tasks (
70
- id TEXT PRIMARY KEY,
71
- entity_id TEXT NOT NULL,
72
- description TEXT NOT NULL,
73
- status TEXT NOT NULL DEFAULT 'pending',
74
- priority INTEGER NOT NULL DEFAULT 0,
75
- created_at INTEGER NOT NULL,
76
- updated_at INTEGER NOT NULL,
77
- resolved_at INTEGER,
78
- deleted_at INTEGER,
79
- okf_type TEXT,
80
- lifecycle_status TEXT NOT NULL DEFAULT 'stable',
81
- stale_after INTEGER,
82
- generated_by TEXT,
83
- last_verified_at INTEGER,
84
- last_verified_by TEXT,
85
- okf_sources TEXT,
86
- okf_verified TEXT,
87
- okf_usage_window TEXT
88
- );
89
-
90
- CREATE INDEX IF NOT EXISTS ${prefix}tasks_entity_idx ON ${prefix}tasks(entity_id, status);
91
-
92
- CREATE TABLE IF NOT EXISTS ${prefix}edges (
93
- id TEXT PRIMARY KEY,
94
- entity_id TEXT NOT NULL,
95
- source_id TEXT NOT NULL,
96
- target_id TEXT NOT NULL,
97
- edge_type TEXT NOT NULL,
98
- created_at INTEGER NOT NULL,
99
- UNIQUE(entity_id, source_id, target_id, edge_type)
100
- );
101
-
102
- CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_id_idx ON ${prefix}edges(entity_id, id);
103
-
104
- CREATE TABLE IF NOT EXISTS ${prefix}events (
105
- id TEXT PRIMARY KEY,
106
- entity_id TEXT NOT NULL,
107
- event_type TEXT NOT NULL,
108
- summary TEXT NOT NULL,
109
- related_entry_id TEXT,
110
- created_at INTEGER NOT NULL,
111
- occurred_at INTEGER
112
- );
113
-
114
- CREATE INDEX IF NOT EXISTS ${prefix}events_entity_idx ON ${prefix}events(entity_id, created_at DESC);
115
-
116
- CREATE TABLE IF NOT EXISTS ${prefix}checkpoints (
117
- entity_id TEXT PRIMARY KEY,
118
- heal_checkpoint INTEGER NOT NULL DEFAULT 0,
119
- memory_checkpoint INTEGER NOT NULL DEFAULT 0,
120
- librarian_watermark_at INTEGER,
121
- librarian_watermark_id TEXT
122
- );
123
-
124
- CREATE TABLE IF NOT EXISTS ${prefix}entity_manifests (
125
- entity_id TEXT PRIMARY KEY,
126
- mode TEXT NOT NULL DEFAULT 'off',
127
- manifest_json TEXT NOT NULL DEFAULT '{"node_types":[],"edge_types":[]}',
128
- updated_at INTEGER NOT NULL
129
- );
130
-
131
- CREATE TABLE IF NOT EXISTS ${prefix}meta (
132
- key TEXT PRIMARY KEY,
133
- value TEXT NOT NULL
134
- );
135
-
136
- CREATE TABLE IF NOT EXISTS ${prefix}outbox (
137
- id TEXT PRIMARY KEY,
138
- entity_id TEXT NOT NULL,
139
- table_name TEXT NOT NULL,
140
- record_id TEXT NOT NULL,
141
- operation TEXT NOT NULL,
142
- payload TEXT NOT NULL,
143
- created_at INTEGER NOT NULL
144
- );
145
-
146
- CREATE INDEX IF NOT EXISTS ${prefix}outbox_entity_id_created_at
147
- ON ${prefix}outbox (entity_id, created_at);
148
-
149
- CREATE INDEX IF NOT EXISTS ${prefix}outbox_created_at
150
- ON ${prefix}outbox (created_at);
151
- `);
152
- await createOkfV02IndexesIfColumnsExist(db, prefix);
153
- await createTemporalIndexesIfColumnsExist(db, prefix);
154
- }
155
- async function createOkfV02IndexesIfColumnsExist(db, prefix) {
156
- for (const table of ["entries", "tasks"]) {
157
- const cols = await db.getAllAsync(
158
- `PRAGMA table_info(${prefix}${table})`
159
- );
160
- const colNames = new Set(cols.map((c) => c.name));
161
- if (colNames.has("lifecycle_status")) {
162
- await db.execAsync(`CREATE INDEX IF NOT EXISTS ${prefix}${table}_lifecycle_status_idx ON ${prefix}${table}(lifecycle_status);`);
163
- }
164
- if (colNames.has("stale_after")) {
165
- await db.execAsync(`CREATE INDEX IF NOT EXISTS ${prefix}${table}_stale_after_idx ON ${prefix}${table}(stale_after);`);
166
- }
167
- if (colNames.has("last_verified_at")) {
168
- await db.execAsync(`CREATE INDEX IF NOT EXISTS ${prefix}${table}_last_verified_at_idx ON ${prefix}${table}(last_verified_at);`);
6
+ // src/db/migrationGate.ts
7
+ var MIGRATION_POLICIES = ["allow", "refuse", "auto"];
8
+ async function setupDatabaseWithGate(db, prefix, meta, migrations = "allow") {
9
+ if (!MIGRATION_POLICIES.includes(migrations)) {
10
+ throw new TypeError(`Invalid migrations policy ${JSON.stringify(migrations)}; expected 'allow', 'refuse' or 'auto'.`);
11
+ }
12
+ if (migrations !== "allow") {
13
+ const pending = await probePendingWork(db, meta, prefix);
14
+ if (pending && (migrations === "refuse" || pending.type === "schema_migration")) {
15
+ throw new WikiMigrationRequiredError(describePending(pending, migrations), pending);
169
16
  }
170
17
  }
18
+ return setupDatabase(db, prefix);
171
19
  }
172
- async function createTemporalIndexesIfColumnsExist(db, prefix) {
173
- const cols = await db.getAllAsync(`PRAGMA table_info(${prefix}entries)`);
174
- const names = new Set(cols.map((c) => c.name));
175
- if (names.has("superseded_by")) {
176
- await db.execAsync(
177
- `CREATE INDEX IF NOT EXISTS ${prefix}entries_superseded_idx ON ${prefix}entries(entity_id, superseded_by) WHERE superseded_by IS NOT NULL;`
178
- );
179
- }
180
- if (names.has("valid_from") && names.has("valid_to")) {
181
- await db.execAsync(
182
- `CREATE INDEX IF NOT EXISTS ${prefix}entries_temporal_idx ON ${prefix}entries(entity_id) WHERE valid_from IS NOT NULL OR valid_to IS NOT NULL;`
183
- );
184
- }
20
+ function describePending(pending, policy) {
21
+ const what = pending.type === "schema_migration" ? `schema migration v${pending.fromVersion} -> v${pending.toVersion} is pending` : pending.entriesWatermark === 0 && pending.sourceRefIndexWatermark === 0 ? "source_ref normalize has not run on this store (normalize stamp missing or outdated; may complete with zero rewrites)" : "source_ref normalize was interrupted and will resume from its watermark";
22
+ return `Migration refused (migrations: '${policy}'): ${what}. Open once with setup({ migrations: 'allow' }) outside any time-critical path to complete it.`;
185
23
  }
186
24
 
187
25
  // src/db/serializedAdapter.ts
@@ -227,314 +65,6 @@ function withSerializedTransactions(db) {
227
65
  }
228
66
  });
229
67
  }
230
-
231
- // src/db/migrations.ts
232
- var MIGRATIONS = [
233
- {
234
- version: 1,
235
- description: "Rebuild FTS5 with porter unicode61 tokenizer (superseded by v2)",
236
- run: async (_db, _prefix) => {
237
- }
238
- },
239
- {
240
- version: 2,
241
- description: "Remove FTS5; add embedding column for semantic retrieval",
242
- run: async (db, prefix) => {
243
- await db.withTransactionAsync(async (tx) => {
244
- await tx.execAsync(`
245
- DROP TRIGGER IF EXISTS ${prefix}entries_ai;
246
- DROP TRIGGER IF EXISTS ${prefix}entries_ad;
247
- DROP TRIGGER IF EXISTS ${prefix}entries_au;
248
- DROP TABLE IF EXISTS ${prefix}entries_fts;
249
- `);
250
- });
251
- const cols = await db.getAllAsync(
252
- `PRAGMA table_info(${prefix}entries)`
253
- );
254
- if (!cols.some((c) => c.name === "embedding")) {
255
- await db.execAsync(`ALTER TABLE ${prefix}entries ADD COLUMN embedding TEXT`);
256
- }
257
- }
258
- },
259
- {
260
- version: 3,
261
- description: "Add embedding_blob BLOB column for Float32Array vector storage",
262
- run: async (db, prefix) => {
263
- const cols = await db.getAllAsync(
264
- `PRAGMA table_info(${prefix}entries)`
265
- );
266
- if (!cols.some((c) => c.name === "embedding_blob")) {
267
- await db.execAsync(
268
- `ALTER TABLE ${prefix}entries ADD COLUMN embedding_blob BLOB`
269
- );
270
- }
271
- }
272
- },
273
- {
274
- version: 4,
275
- description: "Create outbox table for change data capture",
276
- run: async (db, prefix) => {
277
- await db.execAsync(`
278
- CREATE TABLE IF NOT EXISTS ${prefix}outbox (
279
- id TEXT PRIMARY KEY,
280
- entity_id TEXT NOT NULL,
281
- table_name TEXT NOT NULL,
282
- record_id TEXT NOT NULL,
283
- operation TEXT NOT NULL,
284
- payload TEXT NOT NULL,
285
- created_at INTEGER NOT NULL
286
- );
287
- CREATE INDEX IF NOT EXISTS ${prefix}outbox_entity_id_created_at
288
- ON ${prefix}outbox (entity_id, created_at);
289
- `);
290
- }
291
- },
292
- {
293
- version: 5,
294
- description: "Add okf_type to entries/tasks for OKF type fidelity; create edges table for OKF graph import",
295
- run: async (db, prefix) => {
296
- for (const table of ["entries", "tasks"]) {
297
- const cols = await db.getAllAsync(
298
- `PRAGMA table_info(${prefix}${table})`
299
- );
300
- if (!cols.some((c) => c.name === "okf_type")) {
301
- await db.execAsync(`ALTER TABLE ${prefix}${table} ADD COLUMN okf_type TEXT`);
302
- }
303
- }
304
- await db.execAsync(`
305
- CREATE TABLE IF NOT EXISTS ${prefix}edges (
306
- id TEXT PRIMARY KEY,
307
- entity_id TEXT NOT NULL,
308
- source_id TEXT NOT NULL,
309
- target_id TEXT NOT NULL,
310
- edge_type TEXT NOT NULL,
311
- created_at INTEGER NOT NULL,
312
- UNIQUE(entity_id, source_id, target_id, edge_type)
313
- );
314
- CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_idx ON ${prefix}edges (entity_id);
315
- `);
316
- }
317
- },
318
- {
319
- version: 6,
320
- description: "Add entity_manifests table for per-entity ontology state",
321
- run: async (db, prefix) => {
322
- await db.execAsync(`
323
- CREATE TABLE IF NOT EXISTS ${prefix}entity_manifests (
324
- entity_id TEXT PRIMARY KEY,
325
- mode TEXT NOT NULL DEFAULT 'off',
326
- manifest_json TEXT NOT NULL DEFAULT '{"node_types":[],"edge_types":[]}',
327
- updated_at INTEGER NOT NULL
328
- );
329
- `);
330
- }
331
- },
332
- {
333
- version: 7,
334
- description: "Add ontology_checked_at to entries for ontology backfill recheck cooldown",
335
- run: async (db, prefix) => {
336
- const cols = await db.getAllAsync(
337
- `PRAGMA table_info(${prefix}entries)`
338
- );
339
- if (!cols.some((c) => c.name === "ontology_checked_at")) {
340
- await db.execAsync(
341
- `ALTER TABLE ${prefix}entries ADD COLUMN ontology_checked_at INTEGER`
342
- );
343
- }
344
- }
345
- },
346
- {
347
- version: 8,
348
- description: "Add heal_checked_at to entries for heal recheck cooldown",
349
- run: async (db, prefix) => {
350
- const cols = await db.getAllAsync(
351
- `PRAGMA table_info(${prefix}entries)`
352
- );
353
- if (!cols.some((c) => c.name === "heal_checked_at")) {
354
- await db.execAsync(
355
- `ALTER TABLE ${prefix}entries ADD COLUMN heal_checked_at INTEGER`
356
- );
357
- }
358
- }
359
- },
360
- {
361
- version: 9,
362
- description: "add_source_ref_index",
363
- run: async (db, prefix) => {
364
- const duplicates = await db.getAllAsync(
365
- `SELECT entity_id, source_hash, COUNT(DISTINCT source_ref) AS n_refs
366
- FROM ${prefix}entries
367
- WHERE deleted_at IS NULL AND source_hash IS NOT NULL
368
- GROUP BY entity_id, source_hash
369
- HAVING COUNT(DISTINCT source_ref) > 1`
370
- );
371
- if (duplicates.length > 0) {
372
- const sample = duplicates.slice(0, 5).map((d) => `(entity_id=${d.entity_id}, source_hash=${d.source_hash.slice(0, 12)}\u2026, n_refs=${d.n_refs})`).join(", ");
373
- throw new Error(
374
- `Migration v9 (add_source_ref_index) failed: existing live rows have multiple sourceRefs sharing a hash. Found ${duplicates.length} duplicate (entity_id, source_hash) groups. First ${Math.min(5, duplicates.length)}: ${sample}. Resolve each by calling forget({ sourceRef: <loser> }) for the offending sourceRef, then re-run setup.`
375
- );
376
- }
377
- await db.execAsync(
378
- `CREATE TABLE IF NOT EXISTS ${prefix}source_ref_index (
379
- id TEXT PRIMARY KEY,
380
- entity_id TEXT NOT NULL,
381
- source_hash TEXT NOT NULL,
382
- source_ref TEXT NOT NULL,
383
- created_at INTEGER NOT NULL,
384
- deleted_at INTEGER
385
- );
386
- CREATE UNIQUE INDEX IF NOT EXISTS ${prefix}idx_source_ref_hash
387
- ON ${prefix}source_ref_index (entity_id, source_hash)
388
- WHERE deleted_at IS NULL;`
389
- );
390
- await db.execAsync(
391
- `INSERT OR IGNORE INTO ${prefix}source_ref_index (id, entity_id, source_hash, source_ref, created_at, deleted_at)
392
- SELECT
393
- 'sri:' || entity_id || ':' || source_hash,
394
- entity_id,
395
- source_hash,
396
- source_ref,
397
- updated_at,
398
- NULL
399
- FROM (
400
- SELECT
401
- entity_id, source_hash, source_ref, updated_at,
402
- ROW_NUMBER() OVER (
403
- PARTITION BY entity_id, source_hash
404
- ORDER BY updated_at ASC, id ASC
405
- ) AS rn
406
- FROM ${prefix}entries
407
- WHERE deleted_at IS NULL
408
- AND source_hash IS NOT NULL
409
- AND source_ref IS NOT NULL
410
- ) ranked
411
- WHERE rn = 1;`
412
- );
413
- }
414
- },
415
- {
416
- version: 10,
417
- description: "OKF v0.2: add lifecycle_status, stale_after, generated_by, last_verified_*, okf_sources, okf_verified, okf_usage_window to entries and tasks",
418
- run: async (db, prefix) => {
419
- for (const table of ["entries", "tasks"]) {
420
- const cols = await db.getAllAsync(
421
- `PRAGMA table_info(${prefix}${table})`
422
- );
423
- const existing = new Set(cols.map((c) => c.name));
424
- const adds = [
425
- ["lifecycle_status", `ALTER TABLE ${prefix}${table} ADD COLUMN lifecycle_status TEXT NOT NULL DEFAULT 'stable'`],
426
- ["stale_after", `ALTER TABLE ${prefix}${table} ADD COLUMN stale_after INTEGER`],
427
- ["generated_by", `ALTER TABLE ${prefix}${table} ADD COLUMN generated_by TEXT`],
428
- ["last_verified_at", `ALTER TABLE ${prefix}${table} ADD COLUMN last_verified_at INTEGER`],
429
- ["last_verified_by", `ALTER TABLE ${prefix}${table} ADD COLUMN last_verified_by TEXT`],
430
- ["okf_sources", `ALTER TABLE ${prefix}${table} ADD COLUMN okf_sources TEXT`],
431
- ["okf_verified", `ALTER TABLE ${prefix}${table} ADD COLUMN okf_verified TEXT`],
432
- ["okf_usage_window", `ALTER TABLE ${prefix}${table} ADD COLUMN okf_usage_window TEXT`]
433
- ];
434
- for (const [name, sql] of adds) {
435
- if (!existing.has(name)) await db.execAsync(sql);
436
- }
437
- }
438
- await db.execAsync(`
439
- CREATE INDEX IF NOT EXISTS ${prefix}entries_lifecycle_status_idx ON ${prefix}entries(lifecycle_status);
440
- CREATE INDEX IF NOT EXISTS ${prefix}entries_stale_after_idx ON ${prefix}entries(stale_after);
441
- CREATE INDEX IF NOT EXISTS ${prefix}entries_last_verified_at_idx ON ${prefix}entries(last_verified_at);
442
- CREATE INDEX IF NOT EXISTS ${prefix}tasks_lifecycle_status_idx ON ${prefix}tasks(lifecycle_status);
443
- CREATE INDEX IF NOT EXISTS ${prefix}tasks_stale_after_idx ON ${prefix}tasks(stale_after);
444
- CREATE INDEX IF NOT EXISTS ${prefix}tasks_last_verified_at_idx ON ${prefix}tasks(last_verified_at);
445
- `);
446
- }
447
- },
448
- {
449
- version: 11,
450
- description: "Add embedding_failed_at, embedding_failure_kind, embedding_attempts to entries for embed retry state",
451
- run: async (db, prefix) => {
452
- const cols = await db.getAllAsync(
453
- `PRAGMA table_info(${prefix}entries)`
454
- );
455
- const existing = new Set(cols.map((c) => c.name));
456
- const adds = [
457
- ["embedding_failed_at", `ALTER TABLE ${prefix}entries ADD COLUMN embedding_failed_at INTEGER`],
458
- ["embedding_failure_kind", `ALTER TABLE ${prefix}entries ADD COLUMN embedding_failure_kind TEXT`],
459
- ["embedding_attempts", `ALTER TABLE ${prefix}entries ADD COLUMN embedding_attempts INTEGER NOT NULL DEFAULT 0`]
460
- ];
461
- for (const [name, sql] of adds) {
462
- if (!existing.has(name)) await db.execAsync(sql);
463
- }
464
- }
465
- },
466
- {
467
- version: 12,
468
- description: "Replace edges_entity_idx with composite edges(entity_id, id) for keyset paging",
469
- run: async (db, prefix) => {
470
- await db.execAsync(`
471
- CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_id_idx ON ${prefix}edges(entity_id, id);
472
- DROP INDEX IF EXISTS ${prefix}edges_entity_idx;
473
- `);
474
- }
475
- },
476
- {
477
- version: 13,
478
- description: "Temporal facts: valid_from/valid_to/superseded_by/superseded_at on entries, occurred_at on events, librarian watermark on checkpoints",
479
- run: async (db, prefix) => {
480
- const adds = [
481
- ["entries", "valid_from", "INTEGER"],
482
- ["entries", "valid_to", "INTEGER"],
483
- ["entries", "superseded_by", "TEXT"],
484
- ["entries", "superseded_at", "INTEGER"],
485
- ["events", "occurred_at", "INTEGER"],
486
- ["checkpoints", "librarian_watermark_at", "INTEGER"],
487
- ["checkpoints", "librarian_watermark_id", "TEXT"]
488
- ];
489
- const existingByTable = /* @__PURE__ */ new Map();
490
- for (const [table, column, type] of adds) {
491
- let existing = existingByTable.get(table);
492
- if (!existing) {
493
- const cols = await db.getAllAsync(`PRAGMA table_info(${prefix}${table})`);
494
- existing = new Set(cols.map((c) => c.name));
495
- existingByTable.set(table, existing);
496
- }
497
- if (!existing.has(column)) {
498
- await db.execAsync(`ALTER TABLE ${prefix}${table} ADD COLUMN ${column} ${type}`);
499
- existing.add(column);
500
- }
501
- }
502
- await createTemporalIndexesIfColumnsExist(db, prefix);
503
- }
504
- }
505
- ];
506
- for (let i = 1; i < MIGRATIONS.length; i++) {
507
- if (MIGRATIONS[i].version <= MIGRATIONS[i - 1].version) {
508
- throw new Error(
509
- `migrations.ts: MIGRATIONS must be in strictly ascending version order. Found version ${MIGRATIONS[i].version} after ${MIGRATIONS[i - 1].version} at index ${i}.`
510
- );
511
- }
512
- }
513
- var CURRENT_SCHEMA_VERSION = MIGRATIONS.length > 0 ? MIGRATIONS[MIGRATIONS.length - 1].version : 0;
514
-
515
- // src/repositories/rowMappers.ts
516
- function parseJsonArray(s, fallback) {
517
- if (Array.isArray(s)) return s;
518
- if (typeof s === "string" && s.length > 0) {
519
- try {
520
- const p = JSON.parse(s);
521
- if (Array.isArray(p)) return p;
522
- } catch {
523
- }
524
- }
525
- return fallback;
526
- }
527
- function parseJsonObject(s, fallback = null) {
528
- if (s && typeof s === "object" && !Array.isArray(s)) return s;
529
- if (typeof s === "string" && s.length > 0) {
530
- try {
531
- const p = JSON.parse(s);
532
- if (p && typeof p === "object" && !Array.isArray(p)) return p;
533
- } catch {
534
- }
535
- }
536
- return fallback;
537
- }
538
68
  function mapRowToFact(row) {
539
69
  const tags = (() => {
540
70
  if (Array.isArray(row.tags)) return row.tags;
@@ -612,6 +142,15 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
612
142
  this.outbox = outbox;
613
143
  this.chunkSize = 500;
614
144
  }
145
+ /** Whether the entity has any non-deleted row (librarian gate: keyword ranking over zero facts is trivially empty). */
146
+ async hasLiveEntry(entityId, tx) {
147
+ const executor = this.getExecutor(tx);
148
+ const row = await executor.getFirstAsync(
149
+ `SELECT 1 AS one FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL LIMIT 1`,
150
+ [entityId]
151
+ );
152
+ return row != null;
153
+ }
615
154
  /**
616
155
  * Fetch facts by IDs, optionally scoped to entity IDs.
617
156
  * Returns facts in the order of the input IDs (first match wins).
@@ -1498,27 +1037,6 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
1498
1037
  `SELECT * FROM ${this.prefix}entries WHERE deleted_at IS NULL`
1499
1038
  );
1500
1039
  }
1501
- async findRowsForSourceRefMigration(tx) {
1502
- const executor = this.getExecutor(tx);
1503
- return executor.getAllAsync(
1504
- `SELECT rowid, source_ref FROM ${this.prefix}entries
1505
- WHERE source_ref IS NOT NULL
1506
- AND (
1507
- TRIM(source_ref) != source_ref
1508
- OR INSTR(source_ref, '/') > 0
1509
- OR INSTR(source_ref, '\\') > 0
1510
- OR INSTR(source_ref, CHAR(0)) > 0
1511
- OR source_ref GLOB '*[^-A-Za-z0-9._ ]*'
1512
- )`
1513
- );
1514
- }
1515
- async updateSourceRefByRowid(rowid, sourceRef, tx) {
1516
- const executor = this.getExecutor(tx);
1517
- await executor.runAsync(
1518
- `UPDATE ${this.prefix}entries SET source_ref = ? WHERE rowid = ?`,
1519
- [sourceRef, rowid]
1520
- );
1521
- }
1522
1040
  async findLatestSourceHash(entityId, sourceRef, tx) {
1523
1041
  const executor = this.getExecutor(tx);
1524
1042
  const row = await executor.getFirstAsync(
@@ -3222,14 +2740,34 @@ function assertEntity(entityId, documents) {
3222
2740
  }
3223
2741
 
3224
2742
  // src/services/search/createIndexStrategy.ts
2743
+ var FTS5_INIT_RETRY_MS = 250;
2744
+ function isModuleAbsentError(err) {
2745
+ let cur = err;
2746
+ for (let depth = 0; depth < 3 && cur != null; depth++) {
2747
+ let msg = "";
2748
+ try {
2749
+ msg = String(cur.message ?? cur);
2750
+ } catch {
2751
+ return false;
2752
+ }
2753
+ if (/no such module: fts5/i.test(msg) || /no such function: json_valid/i.test(msg)) return true;
2754
+ try {
2755
+ cur = cur.cause;
2756
+ } catch {
2757
+ return false;
2758
+ }
2759
+ }
2760
+ return false;
2761
+ }
3225
2762
  async function probeFts5(db, prefix) {
3226
2763
  const name = `temp.${prefix}fts5_probe`;
3227
2764
  try {
3228
2765
  await db.execAsync(`CREATE VIRTUAL TABLE IF NOT EXISTS ${name} USING fts5(x)`);
3229
2766
  const row = await db.getFirstAsync(`SELECT json_valid('[]') AS ok`);
3230
2767
  return row?.ok === 1;
3231
- } catch {
3232
- return false;
2768
+ } catch (err) {
2769
+ if (isModuleAbsentError(err)) return false;
2770
+ throw err;
3233
2771
  } finally {
3234
2772
  try {
3235
2773
  await db.execAsync(`DROP TABLE IF EXISTS ${name}`);
@@ -3251,22 +2789,36 @@ async function detachFts5(db, prefix, metadataRepo) {
3251
2789
  }
3252
2790
  }
3253
2791
  async function createIndexStrategy(db, prefix, metadataRepo, preferred) {
3254
- if (preferred !== "minisearch") {
3255
- if (await probeFts5(db, prefix)) {
3256
- const strategy = new Fts5IndexStrategy(db, prefix, metadataRepo);
3257
- try {
3258
- await strategy.init();
3259
- return strategy;
3260
- } catch (err) {
3261
- if (preferred === "fts5") throw err;
3262
- console.warn("[WikiMemory] FTS5 init failed; falling back to MiniSearch:", err);
3263
- }
3264
- } else if (preferred === "fts5") {
2792
+ if (preferred === "minisearch") {
2793
+ await detachFts5(db, prefix, metadataRepo);
2794
+ return new MiniSearchIndexStrategy();
2795
+ }
2796
+ const attempt = async () => {
2797
+ if (!await probeFts5(db, prefix)) return "absent";
2798
+ const strategy = new Fts5IndexStrategy(db, prefix, metadataRepo);
2799
+ await strategy.init();
2800
+ return strategy;
2801
+ };
2802
+ const absent = async () => {
2803
+ if (preferred === "fts5") {
3265
2804
  throw new Error(`indexStrategy 'fts5' was requested, but this SQLite build lacks FTS5 or json1.`);
3266
2805
  }
2806
+ await detachFts5(db, prefix, metadataRepo);
2807
+ return new MiniSearchIndexStrategy();
2808
+ };
2809
+ for (let tries = 1; ; tries++) {
2810
+ try {
2811
+ const s = await attempt();
2812
+ return s === "absent" ? await absent() : s;
2813
+ } catch (err) {
2814
+ if (preferred === "fts5") throw err;
2815
+ if (tries === 2) {
2816
+ console.warn("[WikiMemory] FTS5 init failed after retry; keyword search unavailable until a later open or syncSearchIndex():", err);
2817
+ return new UnavailableIndexStrategy(err);
2818
+ }
2819
+ }
2820
+ await new Promise((resolve) => setTimeout(resolve, FTS5_INIT_RETRY_MS));
3267
2821
  }
3268
- await detachFts5(db, prefix, metadataRepo);
3269
- return new MiniSearchIndexStrategy();
3270
2822
  }
3271
2823
 
3272
2824
  // src/services/MaintenanceScheduler.ts
@@ -3350,8 +2902,14 @@ var MaintenanceScheduler = class {
3350
2902
  var _a;
3351
2903
  ((_a = st.report).busy ?? (_a.busy = [])).push(job);
3352
2904
  };
2905
+ const markSearchUnavailable = (st, job) => {
2906
+ var _a;
2907
+ ((_a = st.report).searchUnavailable ?? (_a.searchUnavailable = [])).push(job);
2908
+ };
2909
+ const openJobs = () => states.reduce((n, s) => n + +s.librarian + +s.heal + +s.reembed, 0);
3353
2910
  rounds: for (; ; ) {
3354
2911
  let progressed = false;
2912
+ const openAtStart = openJobs();
3355
2913
  for (const st of states) {
3356
2914
  if (!st.librarian && !st.heal && !st.reembed) continue;
3357
2915
  if (Date.now() >= deadline) {
@@ -3369,6 +2927,11 @@ var MaintenanceScheduler = class {
3369
2927
  let r;
3370
2928
  try {
3371
2929
  r = await this.maintenanceService.runLibrarianPass({ entityId, trigger: "call", meter });
2930
+ } catch (err) {
2931
+ if (!isWikiSearchUnavailableError(err)) throw err;
2932
+ st.librarian = false;
2933
+ markSearchUnavailable(st, "librarian");
2934
+ continue;
3372
2935
  } finally {
3373
2936
  this.jobManager.releaseLock("librarian", entityId);
3374
2937
  }
@@ -3412,6 +2975,11 @@ var MaintenanceScheduler = class {
3412
2975
  st.heal = false;
3413
2976
  continue;
3414
2977
  }
2978
+ if (h.scanned === 0 && h.remaining > 0) {
2979
+ st.heal = false;
2980
+ if (h.searchUnavailable) markSearchUnavailable(st, "heal");
2981
+ continue;
2982
+ }
3415
2983
  st.report.healPasses++;
3416
2984
  progressed = true;
3417
2985
  if (h.remaining === 0) {
@@ -3436,7 +3004,7 @@ var MaintenanceScheduler = class {
3436
3004
  }
3437
3005
  }
3438
3006
  }
3439
- if (!progressed) break;
3007
+ if (!progressed && openJobs() === openAtStart) break;
3440
3008
  }
3441
3009
  if (stopped.reason === "complete" && tooSmall) stopped = tooSmall;
3442
3010
  return {
@@ -3937,6 +3505,10 @@ var OkfTrustWritesRepository = class {
3937
3505
 
3938
3506
  // src/WikiMemory.ts
3939
3507
  var TABLE_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,30}_$/;
3508
+ var SEARCH_REINIT_COOLDOWN_MS = 5e3;
3509
+ var SEARCH_REINIT_COOLDOWN_MAX_MS = 5 * 6e4;
3510
+ var SEARCH_REINIT_COST_FACTOR = 10;
3511
+ var monotonicNow = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
3940
3512
  function decodeDraftCursor(cursor) {
3941
3513
  const sep = typeof cursor === "string" ? cursor.indexOf(":") : -1;
3942
3514
  const createdAt = sep > 0 ? Number(cursor.slice(0, sep)) : Number.NaN;
@@ -3951,6 +3523,11 @@ var WikiMemory = class {
3951
3523
  constructor(db, options) {
3952
3524
  /** Emits `__testAccess` console warning at most once per instance when NODE_ENV ≠ "test". */
3953
3525
  __privateAdd(this, _testAccessNonTestEnvWarned, false);
3526
+ this.reinitSearch = null;
3527
+ /** Monotonic time before which a no-arg syncSearchIndex() skips the re-init (0 = no cooldown). */
3528
+ this.reinitRetryAt = 0;
3529
+ /** Consecutive failed re-inits; drives the exponential backoff. */
3530
+ this.reinitFailures = 0;
3954
3531
  this.db = withSerializedTransactions(db);
3955
3532
  this.options = options;
3956
3533
  this.prefix = options.config?.tablePrefix ?? "llm_wiki_";
@@ -4082,23 +3659,31 @@ var WikiMemory = class {
4082
3659
  jobManager: this.jobManager
4083
3660
  };
4084
3661
  }
4085
- async setup() {
3662
+ /**
3663
+ * Opens the store: migration gate, DDL, schema migrations, then the stamped
3664
+ * source_ref normalize. Await it before any other call on this instance — the
3665
+ * normalize commits in chunks, so a sourceRef-keyed call (forget, upsertGraph,
3666
+ * hasChanged, ingest) issued before setup() resolves can see partially
3667
+ * normalized refs.
3668
+ *
3669
+ * Pass `{ migrations: 'refuse' }` from inside a writer lease: pending one-time
3670
+ * work then throws WikiMigrationRequiredError before any mutation, and the
3671
+ * host runs a plain `setup()` outside the lease.
3672
+ */
3673
+ async setup(opts) {
4086
3674
  const entriesExistedBeforeSetup = await this.metadataRepo.tableExists(`${this.prefix}entries`);
4087
- await setupDatabase(this.db, this.prefix);
3675
+ const migrationPolicy = opts?.migrations ?? this.options.config?.migrations ?? "allow";
3676
+ await setupDatabaseWithGate(this.db, this.prefix, this.metadataRepo, migrationPolicy);
4088
3677
  let currentVersion;
4089
3678
  if (!entriesExistedBeforeSetup) {
4090
- await this.metadataRepo.setMeta("schema_version", String(CURRENT_SCHEMA_VERSION), this.db);
3679
+ await this.db.withTransactionAsync(async (tx) => {
3680
+ await createLegacySourceTypeIndex(tx, this.prefix);
3681
+ await writeNormalizeStamp(this.metadataRepo, tx);
3682
+ await this.metadataRepo.setMeta("schema_version", String(CURRENT_SCHEMA_VERSION), tx);
3683
+ });
4091
3684
  currentVersion = CURRENT_SCHEMA_VERSION;
4092
3685
  } else {
4093
- const schemaVersionValue = await this.metadataRepo.getMeta("schema_version");
4094
- if (schemaVersionValue) {
4095
- currentVersion = parseInt(schemaVersionValue, 10);
4096
- if (!Number.isFinite(currentVersion)) currentVersion = 0;
4097
- } else {
4098
- const ftsDdl = await this.metadataRepo.getTableDdl(`${this.prefix}entries_fts`);
4099
- const hasPorter = /tokenize\s*=\s*['"]porter\s+unicode61['"]/i.test(ftsDdl ?? "");
4100
- currentVersion = hasPorter ? 1 : 0;
4101
- }
3686
+ currentVersion = await detectSchemaVersion(this.metadataRepo, this.prefix);
4102
3687
  }
4103
3688
  for (const migration of MIGRATIONS) {
4104
3689
  if (migration.version > currentVersion) {
@@ -4112,19 +3697,9 @@ var WikiMemory = class {
4112
3697
  if (!schemaVersionCheck) {
4113
3698
  await this.metadataRepo.setMeta("schema_version", String(currentVersion), this.db);
4114
3699
  }
4115
- }
4116
- if (entriesExistedBeforeSetup) {
4117
3700
  await this.importExportService.assertNoLegacySourceTypes();
3701
+ await runSourceRefNormalize(this.db, this.prefix, this.metadataRepo);
4118
3702
  }
4119
- const rows = await this.entryRepo.findRowsForSourceRefMigration();
4120
- await this.db.withTransactionAsync(async (tx) => {
4121
- for (const row of rows) {
4122
- const normalized = normalizeSourceRef(row.source_ref);
4123
- if (normalized !== row.source_ref) {
4124
- await this.entryRepo.updateSourceRefByRowid(row.rowid, normalized, tx);
4125
- }
4126
- }
4127
- });
4128
3703
  this.searchService.setIndexStrategy(
4129
3704
  await createIndexStrategy(
4130
3705
  this.db,
@@ -4388,7 +3963,10 @@ var WikiMemory = class {
4388
3963
  * indexed; otherwise it is a no-op that still waits for rebuilds already
4389
3964
  * queued. Without an argument, rebuilds every entity currently marked
4390
3965
  * stale; when none is, it resolves immediately and does not wait for rebuilds
4391
- * already queued.
3966
+ * already queued. While keyword search is unavailable it first retries the
3967
+ * index init; after a failed attempt it waits at least 5 s, doubling per
3968
+ * consecutive failure up to 5 min (longer when an attempt is itself slow),
3969
+ * before retrying again.
4392
3970
  *
4393
3971
  * Call only after the host's transaction commits: called inside it, the
4394
3972
  * rebuild may index rows that later roll back, or miss them and clear the
@@ -4405,7 +3983,36 @@ var WikiMemory = class {
4405
3983
  * @throws {TypeError} when `entityId` is given but is not a non-empty string.
4406
3984
  */
4407
3985
  async syncSearchIndex(entityId) {
4408
- if (entityId === void 0) return this.searchService.syncStale();
3986
+ if (entityId === void 0) {
3987
+ if (!this.searchService.isKeywordAvailable() && monotonicNow() >= this.reinitRetryAt) {
3988
+ this.reinitSearch ?? (this.reinitSearch = (async () => {
3989
+ const started = monotonicNow();
3990
+ try {
3991
+ const strategy = await createIndexStrategy(
3992
+ this.db,
3993
+ this.prefix,
3994
+ this.metadataRepo,
3995
+ this.options.config?.indexStrategy ?? "auto"
3996
+ );
3997
+ this.searchService.setIndexStrategy(strategy);
3998
+ if (this.searchService.isKeywordAvailable()) await this.searchService.sync();
3999
+ } finally {
4000
+ if (this.searchService.isKeywordAvailable()) {
4001
+ this.reinitFailures = 0;
4002
+ this.reinitRetryAt = 0;
4003
+ } else {
4004
+ const now = monotonicNow();
4005
+ const backoff = Math.min(SEARCH_REINIT_COOLDOWN_MS * 2 ** this.reinitFailures, SEARCH_REINIT_COOLDOWN_MAX_MS);
4006
+ this.reinitFailures++;
4007
+ this.reinitRetryAt = now + Math.max(backoff, SEARCH_REINIT_COST_FACTOR * (now - started));
4008
+ }
4009
+ this.reinitSearch = null;
4010
+ }
4011
+ })());
4012
+ await this.reinitSearch;
4013
+ }
4014
+ return this.searchService.syncStale();
4015
+ }
4409
4016
  assertEntityId(entityId);
4410
4017
  return this.searchService.syncEntries(entityId, []);
4411
4018
  }