@equationalapplications/core-llm-wiki 7.9.1 → 7.10.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,5 +1,5 @@
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-7FRLQ7ZO.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-7FRLQ7ZO.mjs';
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-LOVBNRCV.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-LOVBNRCV.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
 
@@ -2994,6 +2994,280 @@ function assertOwnedBy(entityId, documents) {
2994
2994
  }
2995
2995
  }
2996
2996
 
2997
+ // src/services/search/fts5Sql.ts
2998
+ var FTS5_STATE_KEY = "fts5_index_state";
2999
+ var FTS5_DRAIN_CHUNK = 500;
3000
+ function fts5TriggerNames(prefix) {
3001
+ return [`${prefix}entries_fts_ai`, `${prefix}entries_fts_au`, `${prefix}entries_fts_ad`];
3002
+ }
3003
+ function tagsExpr(col) {
3004
+ return `CASE WHEN json_valid(${col}) THEN
3005
+ CASE WHEN json_type(${col}) = 'array' THEN
3006
+ (SELECT group_concat(CASE type WHEN 'true' THEN 'true' WHEN 'false' THEN 'false'
3007
+ WHEN 'null' THEN '' ELSE value END, ' ')
3008
+ FROM json_each(${col}))
3009
+ ELSE ${col} END
3010
+ ELSE ${col} END`;
3011
+ }
3012
+ function fts5TablesDdl(p) {
3013
+ return `
3014
+ CREATE VIRTUAL TABLE IF NOT EXISTS ${p}entries_fts USING fts5(
3015
+ id UNINDEXED, entity_id UNINDEXED, title, body, tags,
3016
+ tokenize = 'porter unicode61'
3017
+ );
3018
+ CREATE TABLE IF NOT EXISTS ${p}fts_map (
3019
+ fts_rowid INTEGER PRIMARY KEY,
3020
+ id TEXT NOT NULL UNIQUE,
3021
+ entity_id TEXT NOT NULL
3022
+ );
3023
+ CREATE INDEX IF NOT EXISTS ${p}fts_map_entity ON ${p}fts_map(entity_id);
3024
+ CREATE TABLE IF NOT EXISTS ${p}fts_pending (
3025
+ seq INTEGER PRIMARY KEY,
3026
+ entity_id TEXT NOT NULL,
3027
+ id TEXT NOT NULL
3028
+ );`;
3029
+ }
3030
+ function fts5TriggersDdl(p) {
3031
+ const [ai, au, ad] = fts5TriggerNames(p);
3032
+ return `
3033
+ CREATE TRIGGER IF NOT EXISTS ${ai} AFTER INSERT ON ${p}entries BEGIN
3034
+ INSERT INTO ${p}fts_pending (entity_id, id) VALUES (NEW.entity_id, NEW.id);
3035
+ END;
3036
+ CREATE TRIGGER IF NOT EXISTS ${au}
3037
+ AFTER UPDATE OF id, entity_id, title, body, tags, deleted_at ON ${p}entries BEGIN
3038
+ INSERT INTO ${p}fts_pending (entity_id, id) VALUES (OLD.entity_id, OLD.id);
3039
+ INSERT INTO ${p}fts_pending (entity_id, id)
3040
+ SELECT NEW.entity_id, NEW.id WHERE NEW.id <> OLD.id OR NEW.entity_id <> OLD.entity_id;
3041
+ END;
3042
+ CREATE TRIGGER IF NOT EXISTS ${ad} AFTER DELETE ON ${p}entries BEGIN
3043
+ INSERT INTO ${p}fts_pending (entity_id, id) VALUES (OLD.entity_id, OLD.id);
3044
+ END;`;
3045
+ }
3046
+ function drainHiSql(p) {
3047
+ return `SELECT max(seq) AS hi FROM (SELECT seq FROM ${p}fts_pending ORDER BY seq LIMIT ${FTS5_DRAIN_CHUNK})`;
3048
+ }
3049
+ function drainChunkSql(p) {
3050
+ const chunkIds = `SELECT id FROM ${p}fts_pending WHERE seq <= ?`;
3051
+ return [
3052
+ `DELETE FROM ${p}entries_fts WHERE rowid IN (SELECT m.fts_rowid FROM ${p}fts_map m WHERE m.id IN (${chunkIds}))`,
3053
+ `DELETE FROM ${p}fts_map WHERE id IN (${chunkIds})`,
3054
+ `INSERT INTO ${p}fts_map (id, entity_id)
3055
+ SELECT e.id, e.entity_id FROM ${p}entries e WHERE e.id IN (${chunkIds}) AND e.deleted_at IS NULL`,
3056
+ `INSERT INTO ${p}entries_fts (rowid, id, entity_id, title, body, tags)
3057
+ SELECT m.fts_rowid, e.id, e.entity_id, e.title, e.body, ${tagsExpr("e.tags")}
3058
+ FROM ${p}fts_map m JOIN ${p}entries e ON e.id = m.id
3059
+ WHERE m.id IN (${chunkIds})`,
3060
+ `DELETE FROM ${p}fts_pending WHERE seq <= ?`
3061
+ ];
3062
+ }
3063
+ function rebuildSql(p) {
3064
+ return [
3065
+ `INSERT INTO ${p}fts_map (id, entity_id) SELECT id, entity_id FROM ${p}entries WHERE deleted_at IS NULL`,
3066
+ `INSERT INTO ${p}entries_fts (rowid, id, entity_id, title, body, tags)
3067
+ SELECT m.fts_rowid, e.id, e.entity_id, e.title, e.body, ${tagsExpr("e.tags")}
3068
+ FROM ${p}fts_map m JOIN ${p}entries e ON e.id = m.id`
3069
+ ];
3070
+ }
3071
+ function fts5LedgerDropSql(p) {
3072
+ return [
3073
+ ...fts5TriggerNames(p).map((n) => `DROP TRIGGER IF EXISTS ${n};`),
3074
+ `DROP TABLE IF EXISTS ${p}fts_pending;`,
3075
+ `DROP TABLE IF EXISTS ${p}fts_map;`
3076
+ ].join("\n");
3077
+ }
3078
+
3079
+ // src/services/search/ftsQuery.ts
3080
+ var TOKEN = /[\p{L}\p{N}]+/gu;
3081
+ function buildFtsMatchQuery(query) {
3082
+ const seen = /* @__PURE__ */ new Set();
3083
+ for (const m of query.toLowerCase().matchAll(TOKEN)) seen.add(m[0]);
3084
+ if (seen.size === 0) return null;
3085
+ return [...seen].map((t) => `"${t}"*`).join(" OR ");
3086
+ }
3087
+
3088
+ // src/services/search/Fts5IndexStrategy.ts
3089
+ var Fts5IndexStrategy = class {
3090
+ constructor(db, prefix, metadataRepo) {
3091
+ this.db = db;
3092
+ this.prefix = prefix;
3093
+ this.metadataRepo = metadataRepo;
3094
+ }
3095
+ async init() {
3096
+ const state = await this.metadataRepo.getMeta(FTS5_STATE_KEY);
3097
+ if (state === "live" && await this.triggersPresent()) {
3098
+ await this.db.execAsync(fts5TablesDdl(this.prefix));
3099
+ return;
3100
+ }
3101
+ await this.db.withTransactionAsync(async (tx) => {
3102
+ await tx.execAsync(`DROP TABLE IF EXISTS ${this.prefix}entries_fts;`);
3103
+ await tx.execAsync(fts5TablesDdl(this.prefix));
3104
+ await this.rebuildInTx(tx);
3105
+ await tx.execAsync(fts5TriggersDdl(this.prefix));
3106
+ await this.metadataRepo.setMeta(FTS5_STATE_KEY, "live", tx);
3107
+ });
3108
+ }
3109
+ async rebuildFromSource() {
3110
+ await this.db.withTransactionAsync((tx) => this.rebuildInTx(tx));
3111
+ }
3112
+ async drain() {
3113
+ const hiSql = drainHiSql(this.prefix);
3114
+ const chunk = drainChunkSql(this.prefix);
3115
+ for (; ; ) {
3116
+ const done = await this.db.withTransactionAsync(async (tx) => {
3117
+ const row = await tx.getFirstAsync(hiSql);
3118
+ if (row?.hi == null) return true;
3119
+ for (const sql of chunk) await tx.runAsync(sql, [row.hi]);
3120
+ return false;
3121
+ });
3122
+ if (done) return;
3123
+ }
3124
+ }
3125
+ async replace(entityId, ids, documents) {
3126
+ assertEntity(entityId, documents);
3127
+ const docIds = documents.map((d) => d.id);
3128
+ await this.db.withTransactionAsync(async (tx) => {
3129
+ await this.deleteByIds(tx, entityId, ids);
3130
+ await this.deleteByIds(tx, null, docIds);
3131
+ await this.insertDocs(tx, documents);
3132
+ });
3133
+ }
3134
+ async replaceEntity(entityId, documents) {
3135
+ assertEntity(entityId, documents);
3136
+ const p = this.prefix;
3137
+ await this.db.withTransactionAsync(async (tx) => {
3138
+ await tx.runAsync(
3139
+ `DELETE FROM ${p}entries_fts WHERE rowid IN (SELECT fts_rowid FROM ${p}fts_map WHERE entity_id = ?)`,
3140
+ [entityId]
3141
+ );
3142
+ await tx.runAsync(`DELETE FROM ${p}fts_map WHERE entity_id = ?`, [entityId]);
3143
+ await this.insertDocs(tx, documents);
3144
+ });
3145
+ }
3146
+ async replaceAll(documents) {
3147
+ const p = this.prefix;
3148
+ await this.db.withTransactionAsync(async (tx) => {
3149
+ await tx.execAsync(`DELETE FROM ${p}entries_fts; DELETE FROM ${p}fts_map; DELETE FROM ${p}fts_pending;`);
3150
+ await this.insertDocs(tx, documents);
3151
+ });
3152
+ }
3153
+ async search(query, options) {
3154
+ if (options.entityIds.length === 0) return [];
3155
+ const match = buildFtsMatchQuery(query);
3156
+ if (match === null) return [];
3157
+ const cap = options.preFilterLimit ?? options.limit;
3158
+ const limit = cap >= Number.MAX_SAFE_INTEGER ? -1 : cap;
3159
+ const t = `${this.prefix}entries_fts`;
3160
+ const bm25 = `bm25(${t}, 0.0, 0.0, 2.0, 1.0, 1.0)`;
3161
+ return this.db.getAllAsync(
3162
+ `SELECT id, entity_id, max(0.0, -${bm25}) AS score
3163
+ FROM ${t}
3164
+ WHERE ${t} MATCH ? AND entity_id IN (SELECT value FROM json_each(?))
3165
+ ORDER BY ${bm25}, id
3166
+ LIMIT ?`,
3167
+ [match, JSON.stringify(options.entityIds), limit]
3168
+ );
3169
+ }
3170
+ async deleteByIds(tx, entityId, ids) {
3171
+ if (ids.length === 0) return;
3172
+ const p = this.prefix;
3173
+ const json = JSON.stringify(ids);
3174
+ const whereEntity = entityId === null ? "" : " AND entity_id = ?";
3175
+ const args = [json];
3176
+ if (entityId !== null) args.push(entityId);
3177
+ await tx.runAsync(
3178
+ `DELETE FROM ${p}entries_fts WHERE rowid IN (
3179
+ SELECT fts_rowid FROM ${p}fts_map WHERE id IN (SELECT value FROM json_each(?))${whereEntity})`,
3180
+ args
3181
+ );
3182
+ await tx.runAsync(
3183
+ `DELETE FROM ${p}fts_map WHERE id IN (SELECT value FROM json_each(?))${whereEntity}`,
3184
+ args
3185
+ );
3186
+ }
3187
+ /** One INSERT pair per document: a throw part-way rolls back the whole transaction. */
3188
+ async insertDocs(tx, documents) {
3189
+ const p = this.prefix;
3190
+ for (const d of documents) {
3191
+ const { lastInsertRowId } = await tx.runAsync(
3192
+ `INSERT INTO ${p}fts_map (id, entity_id) VALUES (?, ?)`,
3193
+ [d.id, d.entity_id]
3194
+ );
3195
+ await tx.runAsync(
3196
+ `INSERT INTO ${p}entries_fts (rowid, id, entity_id, title, body, tags) VALUES (?, ?, ?, ?, ?, ?)`,
3197
+ [lastInsertRowId, d.id, d.entity_id, d.title, d.body, d.tags]
3198
+ );
3199
+ }
3200
+ }
3201
+ async rebuildInTx(tx) {
3202
+ const p = this.prefix;
3203
+ await tx.execAsync(`DELETE FROM ${p}entries_fts; DELETE FROM ${p}fts_map; DELETE FROM ${p}fts_pending;`);
3204
+ for (const sql of rebuildSql(p)) await tx.runAsync(sql);
3205
+ }
3206
+ async triggersPresent() {
3207
+ const names = fts5TriggerNames(this.prefix);
3208
+ const rows = await this.db.getAllAsync(
3209
+ `SELECT name FROM sqlite_master WHERE type = 'trigger' AND name IN (?, ?, ?)`,
3210
+ [...names]
3211
+ );
3212
+ return rows.length === names.length;
3213
+ }
3214
+ };
3215
+ function assertEntity(entityId, documents) {
3216
+ for (const d of documents) {
3217
+ if (d.entity_id !== entityId) {
3218
+ throw new TypeError(`IndexDocument ${d.id} has entity_id ${d.entity_id}, expected ${entityId}`);
3219
+ }
3220
+ }
3221
+ }
3222
+
3223
+ // src/services/search/createIndexStrategy.ts
3224
+ async function probeFts5(db, prefix) {
3225
+ const name = `temp.${prefix}fts5_probe`;
3226
+ try {
3227
+ await db.execAsync(`CREATE VIRTUAL TABLE IF NOT EXISTS ${name} USING fts5(x)`);
3228
+ const row = await db.getFirstAsync(`SELECT json_valid('[]') AS ok`);
3229
+ return row?.ok === 1;
3230
+ } catch {
3231
+ return false;
3232
+ } finally {
3233
+ try {
3234
+ await db.execAsync(`DROP TABLE IF EXISTS ${name}`);
3235
+ } catch {
3236
+ }
3237
+ }
3238
+ }
3239
+ async function detachFts5(db, prefix, metadataRepo) {
3240
+ const hasLedger = await metadataRepo.tableExists(`${prefix}fts_pending`);
3241
+ const state = await metadataRepo.getMeta(FTS5_STATE_KEY);
3242
+ if (!hasLedger && state !== "live") return;
3243
+ await db.withTransactionAsync(async (tx) => {
3244
+ await tx.execAsync(fts5LedgerDropSql(prefix));
3245
+ await metadataRepo.setMeta(FTS5_STATE_KEY, "detached", tx);
3246
+ });
3247
+ try {
3248
+ await db.execAsync(`DROP TABLE IF EXISTS ${prefix}entries_fts`);
3249
+ } catch {
3250
+ }
3251
+ }
3252
+ async function createIndexStrategy(db, prefix, metadataRepo, preferred) {
3253
+ if (preferred !== "minisearch") {
3254
+ if (await probeFts5(db, prefix)) {
3255
+ const strategy = new Fts5IndexStrategy(db, prefix, metadataRepo);
3256
+ try {
3257
+ await strategy.init();
3258
+ return strategy;
3259
+ } catch (err) {
3260
+ if (preferred === "fts5") throw err;
3261
+ console.warn("[WikiMemory] FTS5 init failed; falling back to MiniSearch:", err);
3262
+ }
3263
+ } else if (preferred === "fts5") {
3264
+ throw new Error(`indexStrategy 'fts5' was requested, but this SQLite build lacks FTS5 or json1.`);
3265
+ }
3266
+ }
3267
+ await detachFts5(db, prefix, metadataRepo);
3268
+ return new MiniSearchIndexStrategy();
3269
+ }
3270
+
2997
3271
  // src/services/MaintenanceScheduler.ts
2998
3272
  var MaintenanceScheduler = class {
2999
3273
  constructor(db, options, maintenanceService, metadataRepo, eventRepo, entryRepo, jobManager) {
@@ -3850,6 +4124,14 @@ var WikiMemory = class {
3850
4124
  }
3851
4125
  }
3852
4126
  });
4127
+ this.searchService.setIndexStrategy(
4128
+ await createIndexStrategy(
4129
+ this.db,
4130
+ this.prefix,
4131
+ this.metadataRepo,
4132
+ this.options.config?.indexStrategy ?? "minisearch"
4133
+ )
4134
+ );
3853
4135
  await this.searchService.sync();
3854
4136
  }
3855
4137
  async hasChanged(entityId, sourceRefOrEntries, sourceHashArg) {
@@ -4114,6 +4396,11 @@ var WikiMemory = class {
4114
4396
  * entity stays stale and the next call retries it. Does not compute
4115
4397
  * embeddings; that remains the maintenance sweep's job.
4116
4398
  *
4399
+ * With the FTS5 index strategy this drains the index ledger in its own
4400
+ * transactions, so it MUST be called after the host transaction commits,
4401
+ * never from inside a `withTransactionAsync` callback. Calling it inside
4402
+ * one waits on the transaction mutex that the callback holds.
4403
+ *
4117
4404
  * @throws {TypeError} when `entityId` is given but is not a non-empty string.
4118
4405
  */
4119
4406
  async syncSearchIndex(entityId) {