@equationalapplications/core-llm-wiki 7.8.0 → 7.9.1

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
@@ -1254,6 +1254,15 @@ const { nodesWritten, edgesWritten, superseded } = await wikiMemory.upsertGraph(
1254
1254
 
1255
1255
  `upsertGraph` is "the tail of `ingestDocument` with the middle (LLM extraction) step removed" — it accepts caller-supplied nodes (`{ id, type, title, body? }`) and edges (`{ type, sourceId, targetId, id? }`) and writes them under the same `(sourceRef, sourceHash)` semantics. If a *different* live `sourceRef` already holds the same `sourceHash`, it throws `WikiSourceRefHashCollision`; re-writing the identical `(sourceRef, sourceHash)` is a no-op returning zero counts. The adapter parameter is required so writes participate in the caller's transaction.
1256
1256
 
1257
+ Core never sees your transaction commit, so it can't index the new nodes itself. Until you tell it, they won't show up in keyword or hybrid `read()` results on the same instance. Call `syncSearchIndex` **after** the commit, not inside the transaction:
1258
+
1259
+ ```typescript
1260
+ await db.withTransactionAsync((tx) => wikiMemory.upsertGraph('entity-123', params, tx));
1261
+ await wikiMemory.syncSearchIndex('entity-123'); // or syncSearchIndex() for every entity written
1262
+ ```
1263
+
1264
+ It is cheap on an entity that is already current, runs in order with core's own index syncs, and never rejects for a valid `entityId` (an empty or non-string id throws `TypeError`). It does not compute embeddings; those still come from the maintenance sweep.
1265
+
1257
1266
  Entry IDs occupy one globally shared namespace within a database. An ID belongs
1258
1267
  to its existing entity while its row exists, including after soft deletion.
1259
1268
  Same-entity updates and resurrection retain their existing behavior; physically
@@ -1,4 +1,3 @@
1
- import MiniSearch from 'minisearch';
2
1
  import { latestVerified } from '@equationalapplications/core-okf';
3
2
 
4
3
  var __typeError = (msg) => {
@@ -426,11 +425,28 @@ function cosineSimilarity(a, b) {
426
425
  return denom === 0 ? 0 : dot / denom;
427
426
  }
428
427
 
428
+ // src/utils/indexDoc.ts
429
+ function toIndexDoc(row) {
430
+ return {
431
+ id: row.id,
432
+ entity_id: row.entity_id,
433
+ title: row.title,
434
+ body: row.body,
435
+ tags: (() => {
436
+ try {
437
+ const parsed = JSON.parse(row.tags);
438
+ return Array.isArray(parsed) ? parsed.join(" ") : row.tags;
439
+ } catch {
440
+ return row.tags;
441
+ }
442
+ })()
443
+ };
444
+ }
445
+
429
446
  // src/services/SearchService.ts
430
447
  var _SearchService = class _SearchService {
431
- constructor(entryRepo) {
448
+ constructor(entryRepo, indexStrategy) {
432
449
  this.entryRepo = entryRepo;
433
- this.miniSearchEntryIdsByEntity = /* @__PURE__ */ new Map();
434
450
  this.vectorCache = /* @__PURE__ */ new Map();
435
451
  /**
436
452
  * Serializes rebuilds. `rebuildIndex` awaits a repository read between
@@ -456,35 +472,13 @@ var _SearchService = class _SearchService {
456
472
  * its flag must outlive the turn (#233 review).
457
473
  */
458
474
  this.staleEpochs = /* @__PURE__ */ new Map();
459
- this.miniSearch = this.createMiniSearch();
460
- }
461
- /**
462
- * A fresh index with the production options. clearAll() swaps one in because
463
- * MiniSearch.removeAll() empties the index but leaves dirtCount (and its
464
- * vacuum bookkeeping) at its old value, which would trip syncEntries'
465
- * conditional vacuum early after a clear.
466
- */
467
- createMiniSearch() {
468
- return new MiniSearch({
469
- fields: ["title", "body", "tags"],
470
- storeFields: ["entity_id"],
471
- // Vacuuming is driven explicitly at the end of each serialized rebuild
472
- // (see sync). Auto-vacuum fires on its own schedule, asynchronously with
473
- // respect to the caller, and traversing the tree mid-rebuild is what
474
- // threw the uncaught TypeError in MiniSearch.performVacuuming (#64).
475
- autoVacuum: false,
476
- searchOptions: {
477
- boost: { title: 2 },
478
- fuzzy: 0.2,
479
- prefix: true
480
- }
481
- });
475
+ this.indexStrategy = indexStrategy;
482
476
  }
483
477
  /**
484
478
  * Rebuilds the search index and clears the vector cache for a given entity.
485
479
  * A direct replacement for manually syncing state after a DB transaction.
486
480
  *
487
- * Rebuilds are serialized per instance and never reject: the MiniSearch index
481
+ * Rebuilds are serialized per instance and never reject: the keyword index
488
482
  * is a rebuildable cache over SQLite, so degraded keyword search is the
489
483
  * correct failure mode and killing the host process is not.
490
484
  */
@@ -493,19 +487,21 @@ var _SearchService = class _SearchService {
493
487
  try {
494
488
  try {
495
489
  const epochsBefore = new Map(this.staleEpochs);
496
- await this.rebuildIndex(entityId);
497
- if (entityId) {
498
- if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
499
- this.staleEntities.delete(entityId);
500
- }
501
- } else {
490
+ if (entityId === void 0) {
491
+ const rows = await this.entryRepo.findMiniSearchRows();
492
+ await this.indexStrategy.replaceAll(rows.map(toIndexDoc));
502
493
  for (const id of [...this.staleEntities]) {
503
494
  if ((this.staleEpochs.get(id) ?? 0) === (epochsBefore.get(id) ?? 0)) {
504
495
  this.staleEntities.delete(id);
505
496
  }
506
497
  }
498
+ } else {
499
+ await this.rebuildIndex(entityId);
500
+ if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
501
+ this.staleEntities.delete(entityId);
502
+ }
507
503
  }
508
- await this.miniSearch.vacuum();
504
+ await this.indexStrategy.vacuum?.();
509
505
  } finally {
510
506
  this.evictCache(entityId);
511
507
  }
@@ -530,27 +526,20 @@ var _SearchService = class _SearchService {
530
526
  */
531
527
  async syncEntries(entityId, ids) {
532
528
  const uniqueIds = [...new Set(ids)];
533
- if (uniqueIds.length === 0 && !this.staleEntities.has(entityId) && this.miniSearchEntryIdsByEntity.has(entityId)) {
529
+ const indexed = this.indexStrategy.hasIndexedEntity?.(entityId);
530
+ if (uniqueIds.length === 0 && !this.staleEntities.has(entityId) && indexed === true) {
534
531
  return this.syncChain;
535
532
  }
536
533
  const work = this.syncChain.then(async () => {
537
534
  try {
538
535
  try {
539
536
  const epochsBefore = new Map(this.staleEpochs);
540
- const needsRebuild = () => this.staleEntities.has(entityId) || !this.miniSearchEntryIdsByEntity.has(entityId);
537
+ const needsRebuild = () => this.staleEntities.has(entityId) || this.indexStrategy.hasIndexedEntity?.(entityId) !== true;
541
538
  if (!needsRebuild()) {
542
539
  const rows = await this.entryRepo.findMiniSearchRowsByIds(entityId, uniqueIds);
543
- const tracked = this.miniSearchEntryIdsByEntity.get(entityId);
544
- if (tracked && !this.staleEntities.has(entityId)) {
545
- for (const id of uniqueIds) {
546
- if (tracked.delete(id) && this.miniSearch.has(id)) this.miniSearch.discard(id);
547
- }
548
- const documents = rows.map((row) => this.normalizeMiniSearchRow(row));
549
- if (documents.length > 0) this.miniSearch.addAll(documents);
550
- for (const document of documents) tracked.add(document.id);
551
- if (this.miniSearch.dirtCount > 0) {
552
- await this.miniSearch.vacuum();
553
- }
540
+ if (!this.staleEntities.has(entityId)) {
541
+ await this.indexStrategy.replace(entityId, uniqueIds, rows.map(toIndexDoc));
542
+ await this.indexStrategy.vacuum?.();
554
543
  return;
555
544
  }
556
545
  }
@@ -558,7 +547,7 @@ var _SearchService = class _SearchService {
558
547
  if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
559
548
  this.staleEntities.delete(entityId);
560
549
  }
561
- await this.miniSearch.vacuum();
550
+ await this.indexStrategy.vacuum?.();
562
551
  } finally {
563
552
  this.evictCache(entityId);
564
553
  }
@@ -578,6 +567,16 @@ var _SearchService = class _SearchService {
578
567
  this.staleEntities.add(entityId);
579
568
  this.staleEpochs.set(entityId, (this.staleEpochs.get(entityId) ?? 0) + 1);
580
569
  }
570
+ /**
571
+ * Runs syncEntries(id, []) for each entity marked stale when this is called,
572
+ * so only those entities are rebuilt. Each rebuild still takes its own turn
573
+ * on the sync chain, and like syncEntries this never rejects. With nothing
574
+ * stale it resolves at once, without waiting on work already on the chain.
575
+ */
576
+ async syncStale() {
577
+ const ids = [...this.staleEntities];
578
+ await Promise.all(ids.map((id) => this.syncEntries(id, [])));
579
+ }
581
580
  /**
582
581
  * Clears the parsed vector cache. Useful for mid-loop flush guarantees
583
582
  * or memory pressure evictions.
@@ -592,39 +591,28 @@ var _SearchService = class _SearchService {
592
591
  /**
593
592
  * Fully resets the search service.
594
593
  */
595
- clearAll() {
594
+ async clearAll() {
596
595
  this.vectorCache.clear();
597
- this.miniSearch = this.createMiniSearch();
598
- this.miniSearchEntryIdsByEntity.clear();
596
+ await this.indexStrategy.replaceAll([]);
599
597
  this.staleEntities.clear();
600
598
  this.staleEpochs.clear();
601
599
  }
602
600
  /**
603
- * Executes a keyword search against the active MiniSearch index.
601
+ * Executes a keyword search against the active index strategy.
604
602
  */
605
- searchKeyword(query, entityIds, limit) {
606
- const entityIdSet = new Set(entityIds);
607
- const results = this.miniSearch.search(query, {
608
- filter: (r) => entityIdSet.has(r.entity_id),
609
- combineWith: "OR"
610
- });
603
+ async searchKeyword(query, entityIds, limit) {
604
+ const results = finiteResults(await this.indexStrategy.search(query, { entityIds, limit }));
611
605
  return results.sort((a, b) => this._compareSearchResults(a, b)).slice(0, limit);
612
606
  }
613
607
  /**
614
- * Pre-fetches MiniSearch scores for candidate hydration, used during hybrid weighting.
608
+ * Pre-fetches keyword scores for candidate hydration, used during hybrid weighting.
615
609
  */
616
- getMiniSearchScores(query, entityIds, preFilterLimit) {
617
- const entityIdSet = new Set(entityIds);
618
- let results = this.miniSearch.search(query, {
619
- filter: (r) => entityIdSet.has(r.entity_id),
620
- combineWith: "OR"
621
- }).sort((a, b) => this._compareSearchResults(a, b));
622
- if (preFilterLimit !== void 0) {
623
- results = results.slice(0, preFilterLimit);
624
- }
610
+ async getKeywordScores(query, entityIds, preFilterLimit) {
611
+ const opts = preFilterLimit !== void 0 ? { entityIds, limit: preFilterLimit, preFilterLimit } : { entityIds, limit: Number.MAX_SAFE_INTEGER };
612
+ const results = finiteResults(await this.indexStrategy.search(query, opts));
625
613
  if (results.length === 0) return /* @__PURE__ */ new Map();
626
- const maxMsScore = Math.max(1, results[0]?.score ?? 1);
627
- return new Map(results.map((r) => [r.id, r.score / maxMsScore]));
614
+ const maxScore = Math.max(1, results[0]?.score ?? 1);
615
+ return new Map(results.map((r) => [r.id, r.score / maxScore]));
628
616
  }
629
617
  /**
630
618
  * Score candidate rows using in-process JS cosine similarity.
@@ -686,53 +674,16 @@ var _SearchService = class _SearchService {
686
674
  return scored.slice(0, limit);
687
675
  }
688
676
  // --- Internal Index Management ---
677
+ /**
678
+ * Rebuild a single entity's index from scratch. Reads every row for
679
+ * `entityId` from the repository (already filtered by `deleted_at IS NULL`)
680
+ * and hands the lot to `indexStrategy.replaceEntity`, which owns the
681
+ * drop-then-add bookkeeping so callers don't need to know the strategy's
682
+ * id-tracking shape.
683
+ */
689
684
  async rebuildIndex(entityId) {
690
- if (entityId) {
691
- const rows2 = await this.entryRepo.findMiniSearchRows(entityId);
692
- const previousIds = this.miniSearchEntryIdsByEntity.get(entityId);
693
- if (previousIds) {
694
- for (const id of previousIds) {
695
- this.miniSearch.discard(id);
696
- }
697
- }
698
- const documents2 = rows2.map((row) => this.normalizeMiniSearchRow(row));
699
- if (documents2.length > 0) {
700
- this.miniSearch.addAll(documents2);
701
- }
702
- this.miniSearchEntryIdsByEntity.set(
703
- entityId,
704
- new Set(documents2.map((document) => document.id))
705
- );
706
- return;
707
- }
708
- const rows = await this.entryRepo.findMiniSearchRows();
709
- this.miniSearch.removeAll();
710
- this.miniSearchEntryIdsByEntity.clear();
711
- const documents = rows.map((row) => this.normalizeMiniSearchRow(row));
712
- if (documents.length > 0) {
713
- this.miniSearch.addAll(documents);
714
- }
715
- for (const document of documents) {
716
- const ids = this.miniSearchEntryIdsByEntity.get(document.entity_id) ?? /* @__PURE__ */ new Set();
717
- ids.add(document.id);
718
- this.miniSearchEntryIdsByEntity.set(document.entity_id, ids);
719
- }
720
- }
721
- normalizeMiniSearchRow(row) {
722
- return {
723
- id: row.id,
724
- entity_id: row.entity_id,
725
- title: row.title,
726
- body: row.body,
727
- tags: (() => {
728
- try {
729
- const parsed = JSON.parse(row.tags);
730
- return Array.isArray(parsed) ? parsed.join(" ") : row.tags;
731
- } catch {
732
- return row.tags;
733
- }
734
- })()
735
- };
685
+ const rows = await this.entryRepo.findMiniSearchRows(entityId);
686
+ await this.indexStrategy.replaceEntity(entityId, rows.map(toIndexDoc));
736
687
  }
737
688
  _tieBreakSort(items) {
738
689
  items.sort((a, b) => this._compareScoredRows(a, b));
@@ -747,11 +698,11 @@ var _SearchService = class _SearchService {
747
698
  return a.id.localeCompare(b.id);
748
699
  }
749
700
  /**
750
- * MiniSearch breaks equal-score ties by internal insertion order, which
751
- * syncEntries changes: a discarded-and-re-added id moves to the end. Every
752
- * caller truncates these results to a limit, so which ids survive a tie at
753
- * the boundary must not depend on write history. Re-sort exact score ties
754
- * by id — the same final tie-break _compareScoredRows applies.
701
+ * The keyword index breaks equal-score ties by internal insertion order,
702
+ * which syncEntries changes: a discarded-and-re-added id moves to the end.
703
+ * Every caller truncates these results to a limit, so which ids survive a
704
+ * tie at the boundary must not depend on write history. Re-sort exact
705
+ * score ties by id — the same final tie-break _compareScoredRows applies.
755
706
  */
756
707
  _compareSearchResults(a, b) {
757
708
  const scoreDiff = b.score - a.score;
@@ -772,6 +723,9 @@ _SearchService.MAX_VECTOR_CACHE_ENTITIES = 16;
772
723
  */
773
724
  _SearchService.MAX_VECTOR_CACHE_FACTS_PER_ENTITY = 500;
774
725
  var SearchService = _SearchService;
726
+ function finiteResults(results) {
727
+ return results.every((r) => Number.isFinite(r.score)) ? results : results.filter((r) => Number.isFinite(r.score));
728
+ }
775
729
 
776
730
  // src/services/JobManager.ts
777
731
  var JobManager = class {
@@ -3652,7 +3606,7 @@ async function gateCandidates(deps, ctx, candidates, cfg) {
3652
3606
  }
3653
3607
  }
3654
3608
  if (!ranked) {
3655
- ranked = deps.searchService.searchKeyword(`${c.fact.title} ${c.fact.body}`, [entityId], cfg.k + nonLive.size).filter((h) => !nonLive.has(h.id)).slice(0, cfg.k).map((h) => ({ id: h.id, score: null }));
3609
+ ranked = (await deps.searchService.searchKeyword(`${c.fact.title} ${c.fact.body}`, [entityId], cfg.k + nonLive.size)).filter((h) => !nonLive.has(h.id)).slice(0, cfg.k).map((h) => ({ id: h.id, score: null }));
3656
3610
  }
3657
3611
  const facts = ranked.length > 0 ? await deps.entryRepo.findByIds(ranked.map((r) => r.id), [entityId]) : [];
3658
3612
  const byId = new Map(facts.map((f) => [f.id, f]));
@@ -5113,7 +5067,7 @@ var MaintenanceService = class {
5113
5067
  if (!query) return [];
5114
5068
  const cached = cache?.get(query);
5115
5069
  if (cached) return cached;
5116
- const hits = this.searchService.searchKeyword(
5070
+ const hits = await this.searchService.searchKeyword(
5117
5071
  query,
5118
5072
  [entityId],
5119
5073
  cap * HEAL_ANCHOR_SEARCH_OVERFETCH
@@ -6105,7 +6059,7 @@ var RetrievalService = class {
6105
6059
  let miniSearchScores;
6106
6060
  if (effectivePreFilterLimit !== void 0) {
6107
6061
  populateCache = false;
6108
- const preResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER).filter((r) => !excludedIds.has(r.id));
6062
+ const preResults = (await this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER)).filter((r) => !excludedIds.has(r.id));
6109
6063
  if (preResults.length === 0) {
6110
6064
  candidateRows = null;
6111
6065
  } else {
@@ -6132,7 +6086,7 @@ var RetrievalService = class {
6132
6086
  candidateRows = this._withoutIds(await this.entryRepo.findWithEmbeddingsByEntityIds(scoredEntityIds), excludedIds);
6133
6087
  }
6134
6088
  if (weight !== void 0 && weight < 1) {
6135
- miniSearchScores = this.searchService.getMiniSearchScores(trimmedQuery, scoredEntityIds);
6089
+ miniSearchScores = await this.searchService.getKeywordScores(trimmedQuery, scoredEntityIds);
6136
6090
  }
6137
6091
  }
6138
6092
  if (candidateRows === null) {
@@ -6375,7 +6329,7 @@ var RetrievalService = class {
6375
6329
  const hasActiveFloorsKeywordFallback = sanitizedTierFloors !== void 0 && Object.values(sanitizedTierFloors).some((f) => f > 0);
6376
6330
  const keywordOversampledLimit = hasActiveFloorsKeywordFallback ? Number.MAX_SAFE_INTEGER : padLimit(Math.max(maxResults * 2, maxResults + 50));
6377
6331
  const preFilteredIds = effectivePreFilterLimit !== void 0 ? new Set(candidateRows.map((r) => r.id)) : void 0;
6378
- const keywordResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, keywordOversampledLimit);
6332
+ const keywordResults = await this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, keywordOversampledLimit);
6379
6333
  const topResults = preFilteredIds === void 0 ? keywordResults : keywordResults.filter((r) => preFilteredIds.has(r.id));
6380
6334
  const topResultIds = new Set(topResults.map((r) => r.id));
6381
6335
  const candidateMap = new Map(candidateRows.filter((r) => topResultIds.has(r.id)).map((row) => [row.id, row]));
@@ -6478,7 +6432,7 @@ var RetrievalService = class {
6478
6432
  if (!usedEmbed && scoredEntityIds.length > 0) {
6479
6433
  const hasActiveFloors = sanitizedTierFloors !== void 0 && Object.values(sanitizedTierFloors).some((f) => f > 0);
6480
6434
  const fallbackOversampledLimit = hasActiveFloors ? Number.MAX_SAFE_INTEGER : padLimit(Math.max(maxResults * 2, maxResults + 50));
6481
- const results = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, fallbackOversampledLimit).filter((r) => !excludedIds.has(r.id));
6435
+ const results = (await this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, fallbackOversampledLimit)).filter((r) => !excludedIds.has(r.id));
6482
6436
  const candidates = results.map((r) => ({
6483
6437
  id: r.id,
6484
6438
  entity_id: r.entity_id,
@@ -6786,5 +6740,5 @@ var WriteService = class {
6786
6740
  };
6787
6741
 
6788
6742
  export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, DiagnosticBuffer, EMBED_CHARS_CEILING, EmbeddingService, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MAX_EMBED_ATTEMPTS, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, TemporalService, UsageMeter, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiSupersedeError, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, edgeTripleAllowed, emptyManifest, entitySummaryMetaKey, extractSqliteCode, factTokenCost, generateId, isLiveAt, liveAtSql, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, packFactsByBudget, parseEmbedding, resolveEdgeDefinitions, resolveGrounding, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
6789
- //# sourceMappingURL=chunk-M76CF2Q6.mjs.map
6790
- //# sourceMappingURL=chunk-M76CF2Q6.mjs.map
6743
+ //# sourceMappingURL=chunk-7FRLQ7ZO.mjs.map
6744
+ //# sourceMappingURL=chunk-7FRLQ7ZO.mjs.map