@equationalapplications/core-llm-wiki 7.9.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.
@@ -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
  }
@@ -602,39 +591,28 @@ var _SearchService = class _SearchService {
602
591
  /**
603
592
  * Fully resets the search service.
604
593
  */
605
- clearAll() {
594
+ async clearAll() {
606
595
  this.vectorCache.clear();
607
- this.miniSearch = this.createMiniSearch();
608
- this.miniSearchEntryIdsByEntity.clear();
596
+ await this.indexStrategy.replaceAll([]);
609
597
  this.staleEntities.clear();
610
598
  this.staleEpochs.clear();
611
599
  }
612
600
  /**
613
- * Executes a keyword search against the active MiniSearch index.
601
+ * Executes a keyword search against the active index strategy.
614
602
  */
615
- searchKeyword(query, entityIds, limit) {
616
- const entityIdSet = new Set(entityIds);
617
- const results = this.miniSearch.search(query, {
618
- filter: (r) => entityIdSet.has(r.entity_id),
619
- combineWith: "OR"
620
- });
603
+ async searchKeyword(query, entityIds, limit) {
604
+ const results = finiteResults(await this.indexStrategy.search(query, { entityIds, limit }));
621
605
  return results.sort((a, b) => this._compareSearchResults(a, b)).slice(0, limit);
622
606
  }
623
607
  /**
624
- * Pre-fetches MiniSearch scores for candidate hydration, used during hybrid weighting.
608
+ * Pre-fetches keyword scores for candidate hydration, used during hybrid weighting.
625
609
  */
626
- getMiniSearchScores(query, entityIds, preFilterLimit) {
627
- const entityIdSet = new Set(entityIds);
628
- let results = this.miniSearch.search(query, {
629
- filter: (r) => entityIdSet.has(r.entity_id),
630
- combineWith: "OR"
631
- }).sort((a, b) => this._compareSearchResults(a, b));
632
- if (preFilterLimit !== void 0) {
633
- results = results.slice(0, preFilterLimit);
634
- }
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));
635
613
  if (results.length === 0) return /* @__PURE__ */ new Map();
636
- const maxMsScore = Math.max(1, results[0]?.score ?? 1);
637
- 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]));
638
616
  }
639
617
  /**
640
618
  * Score candidate rows using in-process JS cosine similarity.
@@ -696,53 +674,16 @@ var _SearchService = class _SearchService {
696
674
  return scored.slice(0, limit);
697
675
  }
698
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
+ */
699
684
  async rebuildIndex(entityId) {
700
- if (entityId) {
701
- const rows2 = await this.entryRepo.findMiniSearchRows(entityId);
702
- const previousIds = this.miniSearchEntryIdsByEntity.get(entityId);
703
- if (previousIds) {
704
- for (const id of previousIds) {
705
- this.miniSearch.discard(id);
706
- }
707
- }
708
- const documents2 = rows2.map((row) => this.normalizeMiniSearchRow(row));
709
- if (documents2.length > 0) {
710
- this.miniSearch.addAll(documents2);
711
- }
712
- this.miniSearchEntryIdsByEntity.set(
713
- entityId,
714
- new Set(documents2.map((document) => document.id))
715
- );
716
- return;
717
- }
718
- const rows = await this.entryRepo.findMiniSearchRows();
719
- this.miniSearch.removeAll();
720
- this.miniSearchEntryIdsByEntity.clear();
721
- const documents = rows.map((row) => this.normalizeMiniSearchRow(row));
722
- if (documents.length > 0) {
723
- this.miniSearch.addAll(documents);
724
- }
725
- for (const document of documents) {
726
- const ids = this.miniSearchEntryIdsByEntity.get(document.entity_id) ?? /* @__PURE__ */ new Set();
727
- ids.add(document.id);
728
- this.miniSearchEntryIdsByEntity.set(document.entity_id, ids);
729
- }
730
- }
731
- normalizeMiniSearchRow(row) {
732
- return {
733
- id: row.id,
734
- entity_id: row.entity_id,
735
- title: row.title,
736
- body: row.body,
737
- tags: (() => {
738
- try {
739
- const parsed = JSON.parse(row.tags);
740
- return Array.isArray(parsed) ? parsed.join(" ") : row.tags;
741
- } catch {
742
- return row.tags;
743
- }
744
- })()
745
- };
685
+ const rows = await this.entryRepo.findMiniSearchRows(entityId);
686
+ await this.indexStrategy.replaceEntity(entityId, rows.map(toIndexDoc));
746
687
  }
747
688
  _tieBreakSort(items) {
748
689
  items.sort((a, b) => this._compareScoredRows(a, b));
@@ -757,11 +698,11 @@ var _SearchService = class _SearchService {
757
698
  return a.id.localeCompare(b.id);
758
699
  }
759
700
  /**
760
- * MiniSearch breaks equal-score ties by internal insertion order, which
761
- * syncEntries changes: a discarded-and-re-added id moves to the end. Every
762
- * caller truncates these results to a limit, so which ids survive a tie at
763
- * the boundary must not depend on write history. Re-sort exact score ties
764
- * 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.
765
706
  */
766
707
  _compareSearchResults(a, b) {
767
708
  const scoreDiff = b.score - a.score;
@@ -782,6 +723,9 @@ _SearchService.MAX_VECTOR_CACHE_ENTITIES = 16;
782
723
  */
783
724
  _SearchService.MAX_VECTOR_CACHE_FACTS_PER_ENTITY = 500;
784
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
+ }
785
729
 
786
730
  // src/services/JobManager.ts
787
731
  var JobManager = class {
@@ -3662,7 +3606,7 @@ async function gateCandidates(deps, ctx, candidates, cfg) {
3662
3606
  }
3663
3607
  }
3664
3608
  if (!ranked) {
3665
- 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 }));
3666
3610
  }
3667
3611
  const facts = ranked.length > 0 ? await deps.entryRepo.findByIds(ranked.map((r) => r.id), [entityId]) : [];
3668
3612
  const byId = new Map(facts.map((f) => [f.id, f]));
@@ -5123,7 +5067,7 @@ var MaintenanceService = class {
5123
5067
  if (!query) return [];
5124
5068
  const cached = cache?.get(query);
5125
5069
  if (cached) return cached;
5126
- const hits = this.searchService.searchKeyword(
5070
+ const hits = await this.searchService.searchKeyword(
5127
5071
  query,
5128
5072
  [entityId],
5129
5073
  cap * HEAL_ANCHOR_SEARCH_OVERFETCH
@@ -6115,7 +6059,7 @@ var RetrievalService = class {
6115
6059
  let miniSearchScores;
6116
6060
  if (effectivePreFilterLimit !== void 0) {
6117
6061
  populateCache = false;
6118
- 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));
6119
6063
  if (preResults.length === 0) {
6120
6064
  candidateRows = null;
6121
6065
  } else {
@@ -6142,7 +6086,7 @@ var RetrievalService = class {
6142
6086
  candidateRows = this._withoutIds(await this.entryRepo.findWithEmbeddingsByEntityIds(scoredEntityIds), excludedIds);
6143
6087
  }
6144
6088
  if (weight !== void 0 && weight < 1) {
6145
- miniSearchScores = this.searchService.getMiniSearchScores(trimmedQuery, scoredEntityIds);
6089
+ miniSearchScores = await this.searchService.getKeywordScores(trimmedQuery, scoredEntityIds);
6146
6090
  }
6147
6091
  }
6148
6092
  if (candidateRows === null) {
@@ -6385,7 +6329,7 @@ var RetrievalService = class {
6385
6329
  const hasActiveFloorsKeywordFallback = sanitizedTierFloors !== void 0 && Object.values(sanitizedTierFloors).some((f) => f > 0);
6386
6330
  const keywordOversampledLimit = hasActiveFloorsKeywordFallback ? Number.MAX_SAFE_INTEGER : padLimit(Math.max(maxResults * 2, maxResults + 50));
6387
6331
  const preFilteredIds = effectivePreFilterLimit !== void 0 ? new Set(candidateRows.map((r) => r.id)) : void 0;
6388
- const keywordResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, keywordOversampledLimit);
6332
+ const keywordResults = await this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, keywordOversampledLimit);
6389
6333
  const topResults = preFilteredIds === void 0 ? keywordResults : keywordResults.filter((r) => preFilteredIds.has(r.id));
6390
6334
  const topResultIds = new Set(topResults.map((r) => r.id));
6391
6335
  const candidateMap = new Map(candidateRows.filter((r) => topResultIds.has(r.id)).map((row) => [row.id, row]));
@@ -6488,7 +6432,7 @@ var RetrievalService = class {
6488
6432
  if (!usedEmbed && scoredEntityIds.length > 0) {
6489
6433
  const hasActiveFloors = sanitizedTierFloors !== void 0 && Object.values(sanitizedTierFloors).some((f) => f > 0);
6490
6434
  const fallbackOversampledLimit = hasActiveFloors ? Number.MAX_SAFE_INTEGER : padLimit(Math.max(maxResults * 2, maxResults + 50));
6491
- 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));
6492
6436
  const candidates = results.map((r) => ({
6493
6437
  id: r.id,
6494
6438
  entity_id: r.entity_id,
@@ -6796,5 +6740,5 @@ var WriteService = class {
6796
6740
  };
6797
6741
 
6798
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 };
6799
- //# sourceMappingURL=chunk-J2TWKOEE.mjs.map
6800
- //# sourceMappingURL=chunk-J2TWKOEE.mjs.map
6743
+ //# sourceMappingURL=chunk-7FRLQ7ZO.mjs.map
6744
+ //# sourceMappingURL=chunk-7FRLQ7ZO.mjs.map