@equationalapplications/core-llm-wiki 7.7.5 → 7.7.6

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.
@@ -432,7 +432,31 @@ var _SearchService = class _SearchService {
432
432
  * auto-vacuum debt behind the TypeError in #64.
433
433
  */
434
434
  this.syncChain = Promise.resolve();
435
- this.miniSearch = new MiniSearch({
435
+ /**
436
+ * Entities whose index may have drifted from SQLite: an incremental update
437
+ * failed part-way, or core wrote rows it could not index (upsertGraph runs in
438
+ * the host's transaction). Their next syncEntries rebuilds the entity in full.
439
+ * See spec 2026-09-28 §5.
440
+ */
441
+ this.staleEntities = /* @__PURE__ */ new Set();
442
+ /**
443
+ * Per-entity count of markStale() calls. A rebuild clears an entity's stale
444
+ * flag only when the count it snapshotted before its read still holds
445
+ * afterwards: markStale() runs inside the host's still-open transaction, so
446
+ * a call landing mid-read belongs to rows the read cannot have seen, and
447
+ * its flag must outlive the turn (#233 review).
448
+ */
449
+ this.staleEpochs = /* @__PURE__ */ new Map();
450
+ this.miniSearch = this.createMiniSearch();
451
+ }
452
+ /**
453
+ * A fresh index with the production options. clearAll() swaps one in because
454
+ * MiniSearch.removeAll() empties the index but leaves dirtCount (and its
455
+ * vacuum bookkeeping) at its old value, which would trip syncEntries'
456
+ * conditional vacuum early after a clear.
457
+ */
458
+ createMiniSearch() {
459
+ return new MiniSearch({
436
460
  fields: ["title", "body", "tags"],
437
461
  storeFields: ["entity_id"],
438
462
  // Vacuuming is driven explicitly at the end of each serialized rebuild
@@ -459,7 +483,19 @@ var _SearchService = class _SearchService {
459
483
  const work = this.syncChain.then(async () => {
460
484
  try {
461
485
  try {
486
+ const epochsBefore = new Map(this.staleEpochs);
462
487
  await this.rebuildIndex(entityId);
488
+ if (entityId) {
489
+ if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
490
+ this.staleEntities.delete(entityId);
491
+ }
492
+ } else {
493
+ for (const id of [...this.staleEntities]) {
494
+ if ((this.staleEpochs.get(id) ?? 0) === (epochsBefore.get(id) ?? 0)) {
495
+ this.staleEntities.delete(id);
496
+ }
497
+ }
498
+ }
463
499
  await this.miniSearch.vacuum();
464
500
  } finally {
465
501
  this.evictCache(entityId);
@@ -471,6 +507,68 @@ var _SearchService = class _SearchService {
471
507
  this.syncChain = work;
472
508
  return work;
473
509
  }
510
+ /**
511
+ * Re-indexes only `ids` for `entityId`: drops each from the index, then
512
+ * re-adds the ones still live in SQLite, so soft-deleted or missing ids end
513
+ * up absent. Costs O(ids), where sync(entityId) costs O(entity) — callers that
514
+ * know what a write touched use this so chunked imports stay linear (#232).
515
+ *
516
+ * Serialized with sync() on the same chain and, like it, never rejects. An
517
+ * entity that is stale or has never been indexed gets a full rebuild instead
518
+ * — including on an empty `ids` list, which is a no-op only for an entity
519
+ * the index already tracks, and even then one that waits for rebuilds
520
+ * already queued on the chain.
521
+ */
522
+ async syncEntries(entityId, ids) {
523
+ const uniqueIds = [...new Set(ids)];
524
+ if (uniqueIds.length === 0 && !this.staleEntities.has(entityId) && this.miniSearchEntryIdsByEntity.has(entityId)) {
525
+ return this.syncChain;
526
+ }
527
+ const work = this.syncChain.then(async () => {
528
+ try {
529
+ try {
530
+ const epochsBefore = new Map(this.staleEpochs);
531
+ const needsRebuild = () => this.staleEntities.has(entityId) || !this.miniSearchEntryIdsByEntity.has(entityId);
532
+ if (!needsRebuild()) {
533
+ const rows = await this.entryRepo.findMiniSearchRowsByIds(entityId, uniqueIds);
534
+ const tracked = this.miniSearchEntryIdsByEntity.get(entityId);
535
+ if (tracked && !this.staleEntities.has(entityId)) {
536
+ for (const id of uniqueIds) {
537
+ if (tracked.delete(id) && this.miniSearch.has(id)) this.miniSearch.discard(id);
538
+ }
539
+ const documents = rows.map((row) => this.normalizeMiniSearchRow(row));
540
+ if (documents.length > 0) this.miniSearch.addAll(documents);
541
+ for (const document of documents) tracked.add(document.id);
542
+ if (this.miniSearch.dirtCount > 0) {
543
+ await this.miniSearch.vacuum();
544
+ }
545
+ return;
546
+ }
547
+ }
548
+ await this.rebuildIndex(entityId);
549
+ if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
550
+ this.staleEntities.delete(entityId);
551
+ }
552
+ await this.miniSearch.vacuum();
553
+ } finally {
554
+ this.evictCache(entityId);
555
+ }
556
+ } catch (err) {
557
+ this.staleEntities.add(entityId);
558
+ console.warn(`[WikiMemory] search index incremental sync failed for ${entityId}:`, err);
559
+ }
560
+ });
561
+ this.syncChain = work;
562
+ return work;
563
+ }
564
+ /**
565
+ * Forces the entity's next syncEntries to rebuild it in full. For writes core
566
+ * cannot index itself, such as upsertGraph inside a host transaction.
567
+ */
568
+ markStale(entityId) {
569
+ this.staleEntities.add(entityId);
570
+ this.staleEpochs.set(entityId, (this.staleEpochs.get(entityId) ?? 0) + 1);
571
+ }
474
572
  /**
475
573
  * Clears the parsed vector cache. Useful for mid-loop flush guarantees
476
574
  * or memory pressure evictions.
@@ -487,8 +585,10 @@ var _SearchService = class _SearchService {
487
585
  */
488
586
  clearAll() {
489
587
  this.vectorCache.clear();
490
- this.miniSearch.removeAll();
588
+ this.miniSearch = this.createMiniSearch();
491
589
  this.miniSearchEntryIdsByEntity.clear();
590
+ this.staleEntities.clear();
591
+ this.staleEpochs.clear();
492
592
  }
493
593
  /**
494
594
  * Executes a keyword search against the active MiniSearch index.
@@ -499,7 +599,7 @@ var _SearchService = class _SearchService {
499
599
  filter: (r) => entityIdSet.has(r.entity_id),
500
600
  combineWith: "OR"
501
601
  });
502
- return results.slice(0, limit);
602
+ return results.sort((a, b) => this._compareSearchResults(a, b)).slice(0, limit);
503
603
  }
504
604
  /**
505
605
  * Pre-fetches MiniSearch scores for candidate hydration, used during hybrid weighting.
@@ -509,7 +609,7 @@ var _SearchService = class _SearchService {
509
609
  let results = this.miniSearch.search(query, {
510
610
  filter: (r) => entityIdSet.has(r.entity_id),
511
611
  combineWith: "OR"
512
- });
612
+ }).sort((a, b) => this._compareSearchResults(a, b));
513
613
  if (preFilterLimit !== void 0) {
514
614
  results = results.slice(0, preFilterLimit);
515
615
  }
@@ -637,6 +737,18 @@ var _SearchService = class _SearchService {
637
737
  if (updatedAtDiff !== 0) return updatedAtDiff;
638
738
  return a.id.localeCompare(b.id);
639
739
  }
740
+ /**
741
+ * MiniSearch breaks equal-score ties by internal insertion order, which
742
+ * syncEntries changes: a discarded-and-re-added id moves to the end. Every
743
+ * caller truncates these results to a limit, so which ids survive a tie at
744
+ * the boundary must not depend on write history. Re-sort exact score ties
745
+ * by id — the same final tie-break _compareScoredRows applies.
746
+ */
747
+ _compareSearchResults(a, b) {
748
+ const scoreDiff = b.score - a.score;
749
+ if (!Number.isNaN(scoreDiff) && scoreDiff !== 0) return scoreDiff;
750
+ return a.id.localeCompare(b.id);
751
+ }
640
752
  };
641
753
  /**
642
754
  * Maximum number of entities whose parsed embedding vectors are held in
@@ -2290,7 +2402,10 @@ var IngestionService = class {
2290
2402
  return zeroChunkResult(canonical);
2291
2403
  }
2292
2404
  diagBuffer.flush(this.options);
2293
- await this.searchService.sync(entityId);
2405
+ await this.searchService.syncEntries(
2406
+ entityId,
2407
+ [...deletedSourceFactIds, ...insertedFacts.map((fact) => fact.id)]
2408
+ );
2294
2409
  if (failedChunks === 0) {
2295
2410
  const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
2296
2411
  for (const factId of uniqueDeletedSourceFactIds) {
@@ -4479,7 +4594,11 @@ var ImportExportService = class {
4479
4594
  );
4480
4595
  }
4481
4596
  });
4482
- await this.searchService.sync(entityId);
4597
+ if (merge) {
4598
+ await this.searchService.syncEntries(entityId, [...upsertedFactIds]);
4599
+ } else {
4600
+ await this.searchService.sync(entityId);
4601
+ }
4483
4602
  for (const fact of bundle.facts) {
4484
4603
  if (!fact.deleted_at && upsertedFactIds.has(fact.id) && !factsWithPreservedBlob.has(fact.id)) {
4485
4604
  const clipped = clippedTextByFactId.get(fact.id);
@@ -5663,5 +5782,5 @@ var WriteService = class {
5663
5782
  };
5664
5783
 
5665
5784
  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, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, edgeTripleAllowed, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveGrounding, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
5666
- //# sourceMappingURL=chunk-JV2OQ5CM.mjs.map
5667
- //# sourceMappingURL=chunk-JV2OQ5CM.mjs.map
5785
+ //# sourceMappingURL=chunk-PBUZLSOM.mjs.map
5786
+ //# sourceMappingURL=chunk-PBUZLSOM.mjs.map