@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.
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
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-J2TWKOEE.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-J2TWKOEE.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-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';
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
+ import MiniSearch from 'minisearch';
4
5
 
5
6
  // src/db/schema.ts
6
7
  async function setupDatabase(db, prefix) {
@@ -2803,6 +2804,195 @@ function mapRowToEdge(row) {
2803
2804
  created_at: Number(row.created_at)
2804
2805
  };
2805
2806
  }
2807
+ var MiniSearchIndexStrategy = class {
2808
+ constructor() {
2809
+ /**
2810
+ * Per-entity id set. `replaceEntity()` and `hasIndexedEntity()` read it;
2811
+ * `addDocuments()` and `discardIds()` keep it in sync. `SearchService` does
2812
+ * not see this map; it only sees `hasIndexedEntity`. The previous
2813
+ * `miniSearchEntryIdsByEntity` lived on `SearchService`.
2814
+ */
2815
+ this.entryIdsByEntity = /* @__PURE__ */ new Map();
2816
+ /**
2817
+ * Reverse of `entryIdsByEntity`. `replace(entityId, ids, …)` uses it to
2818
+ * decide which ids are owned by `entityId` (the only safe membership test
2819
+ * — a rebuild that failed between its discard pass and the set replacement
2820
+ * can leave the forward set claiming ids the MiniSearch instance does not
2821
+ * hold). The pre-extraction `syncEntries` deleted from one set per id, not
2822
+ * from every entity's set.
2823
+ */
2824
+ this.entityIdById = /* @__PURE__ */ new Map();
2825
+ this.index = this.createIndex();
2826
+ }
2827
+ /**
2828
+ * Build a fresh MiniSearch instance with the project's index options.
2829
+ * Called from the constructor and from `replaceAll`. We do not use
2830
+ * `removeAll()` on an existing instance because that empties the index but
2831
+ * leaves `dirtCount` and the vacuum bookkeeping intact, which would trip
2832
+ * `syncEntries`' conditional vacuum early after a clear.
2833
+ */
2834
+ createIndex() {
2835
+ return new MiniSearch({
2836
+ fields: ["title", "body", "tags"],
2837
+ storeFields: ["entity_id"],
2838
+ // Vacuuming is driven explicitly at the end of each serialized rebuild
2839
+ // (see SearchService.syncEntries). Auto-vacuum fires on its own
2840
+ // schedule, asynchronously with respect to the caller, and traversing
2841
+ // the tree mid-rebuild is what threw the uncaught TypeError in
2842
+ // MiniSearch.performVacuuming (#64).
2843
+ autoVacuum: false,
2844
+ searchOptions: {
2845
+ boost: { title: 2 },
2846
+ fuzzy: 0.2,
2847
+ prefix: true
2848
+ }
2849
+ });
2850
+ }
2851
+ /**
2852
+ * Incremental sync. Removes only ids currently tracked under `entityId`
2853
+ * (other entities' docs are untouched), then upserts `documents` by id.
2854
+ * The same id appearing in both `ids` and `documents` is discarded first
2855
+ * so the upsert does not throw on a duplicate id.
2856
+ *
2857
+ * Every document must carry `entityId` as its `entity_id`; a mismatch is
2858
+ * rejected before the write mutates anything. A tracking-only call
2859
+ * (`replace(entityId, [], [])`) registers the entity as indexed, matching
2860
+ * the interface contract.
2861
+ *
2862
+ * Atomicity: never awaits between the discard pass and `addDocuments`, so
2863
+ * a concurrent `search()` sees the index wholly before or wholly after,
2864
+ * never half-updated. This is the precondition the pre-extraction
2865
+ * `syncEntries` kept with its "no await from here to `addAll`" comment.
2866
+ */
2867
+ async replace(entityId, ids, documents) {
2868
+ assertOwnedBy(entityId, documents);
2869
+ this.discardIds(ids, entityId);
2870
+ if (!this.entryIdsByEntity.has(entityId)) this.entryIdsByEntity.set(entityId, /* @__PURE__ */ new Set());
2871
+ this.addDocuments(documents);
2872
+ }
2873
+ /**
2874
+ * Per-entity rebuild. Removes every id tracked under `entityId` (with the
2875
+ * `index.has(id)` guard so a stale tracking set cannot make `discard()`
2876
+ * throw), then writes `documents`. The entity stays registered as
2877
+ * indexed — even when `documents` is empty — so the next `syncEntries`
2878
+ * call can take the fast path. Every document must belong to `entityId`.
2879
+ */
2880
+ async replaceEntity(entityId, documents) {
2881
+ assertOwnedBy(entityId, documents);
2882
+ const previous = this.entryIdsByEntity.get(entityId);
2883
+ if (previous) this.discardIds([...previous]);
2884
+ this.entryIdsByEntity.set(entityId, /* @__PURE__ */ new Set());
2885
+ this.addDocuments(documents);
2886
+ }
2887
+ /**
2888
+ * Global rebuild / clear. Replaces the MiniSearch instance and clears both
2889
+ * id maps, so `hasIndexedEntity` returns `false` for every entity afterwards.
2890
+ * `SearchService.clearAll()` calls `replaceAll([])`.
2891
+ */
2892
+ async replaceAll(documents) {
2893
+ this.index = this.createIndex();
2894
+ this.entryIdsByEntity.clear();
2895
+ this.entityIdById.clear();
2896
+ this.addDocuments(documents);
2897
+ }
2898
+ /**
2899
+ * Best-match-first search over the documents whose `entity_id` is in
2900
+ * `options.entityIds`. `preFilterLimit` overrides `limit` (hybrid retrieval
2901
+ * uses this to over-fetch candidates before the `limit` cap). Scores are
2902
+ * raw BM25+ — `SearchService.getKeywordScores` and `searchKeyword` apply
2903
+ * any scaling, not the strategy.
2904
+ */
2905
+ async search(query, options) {
2906
+ if (options.entityIds.length === 0) return [];
2907
+ const entityIdSet = new Set(options.entityIds);
2908
+ const limit = options.preFilterLimit ?? options.limit;
2909
+ const raw = this.index.search(query, {
2910
+ filter: (r) => entityIdSet.has(r.entity_id),
2911
+ combineWith: "OR"
2912
+ });
2913
+ raw.sort((a, b) => {
2914
+ const scoreDiff = b.score - a.score;
2915
+ if (!Number.isNaN(scoreDiff) && scoreDiff !== 0) return scoreDiff;
2916
+ return a.id.localeCompare(b.id);
2917
+ });
2918
+ return raw.slice(0, limit).map((r) => ({
2919
+ id: r.id,
2920
+ entity_id: r.entity_id,
2921
+ score: r.score
2922
+ }));
2923
+ }
2924
+ /**
2925
+ * Explicit vacuum. MiniSearch 7.2.0's `dirtCount` rule: search after an
2926
+ * un-vacuumed discard inverts the relative scores of term-sharing
2927
+ * documents at any index size. `SearchService.syncEntries` calls this
2928
+ * after a discard-bearing turn. No-op when `dirtCount === 0`.
2929
+ */
2930
+ async vacuum() {
2931
+ if (this.index.dirtCount > 0) {
2932
+ await this.index.vacuum();
2933
+ }
2934
+ }
2935
+ /**
2936
+ * `true` when `entityId` has been indexed at least once under the current
2937
+ * strategy state. Backed by `entryIdsByEntity.has(entityId)`, which is
2938
+ * kept in sync by `addDocuments`, `replaceEntity`, and `replaceAll`.
2939
+ */
2940
+ hasIndexedEntity(entityId) {
2941
+ return this.entryIdsByEntity.has(entityId);
2942
+ }
2943
+ /**
2944
+ * Add `documents` to the MiniSearch instance and register them in
2945
+ * `entryIdsByEntity` and `entityIdById`. MiniSearch mutates documents
2946
+ * during `add`, so each doc is copied first. `add` throws on an id the
2947
+ * index already holds — `replace` and `replaceEntity` discard first for
2948
+ * that reason.
2949
+ *
2950
+ * Adds and registers one document at a time: if a duplicate id throws
2951
+ * part-way through, every document already in the index is tracked, so
2952
+ * the next rebuild can discard it instead of failing on it again. This is
2953
+ * the precondition the per-turn recovery path relies on (#258 review).
2954
+ */
2955
+ addDocuments(documents) {
2956
+ for (const d of documents) {
2957
+ const doc = { ...d };
2958
+ this.index.add(doc);
2959
+ const set = this.entryIdsByEntity.get(doc.entity_id) ?? /* @__PURE__ */ new Set();
2960
+ set.add(doc.id);
2961
+ this.entryIdsByEntity.set(doc.entity_id, set);
2962
+ this.entityIdById.set(doc.id, doc.entity_id);
2963
+ }
2964
+ }
2965
+ /**
2966
+ * Discard `ids` from the MiniSearch instance and from both id maps, but
2967
+ * only for ids currently owned by `entityId` (the second argument). When
2968
+ * `entityId` is omitted (used by `replaceEntity` for the bulk rebuild
2969
+ * path), every id is discarded regardless of owning entity.
2970
+ *
2971
+ * `index.has(id)` is the membership test of record: a rebuild that failed
2972
+ * between its own discard pass and the set replacement can leave the
2973
+ * tracking set claiming ids the index no longer holds — `discard()` throws
2974
+ * on those, so we guard.
2975
+ */
2976
+ discardIds(ids, entityId) {
2977
+ for (const id of ids) {
2978
+ const owning = this.entityIdById.get(id);
2979
+ if (owning === void 0) continue;
2980
+ if (entityId !== void 0 && owning !== entityId) continue;
2981
+ if (this.index.has(id)) this.index.discard(id);
2982
+ this.entryIdsByEntity.get(owning)?.delete(id);
2983
+ this.entityIdById.delete(id);
2984
+ }
2985
+ }
2986
+ };
2987
+ function assertOwnedBy(entityId, documents) {
2988
+ for (const doc of documents) {
2989
+ if (doc.entity_id !== entityId) {
2990
+ throw new Error(
2991
+ `IndexStrategy write for entity ${entityId} got document ${doc.id} under entity ${doc.entity_id}`
2992
+ );
2993
+ }
2994
+ }
2995
+ }
2806
2996
 
2807
2997
  // src/services/MaintenanceScheduler.ts
2808
2998
  var MaintenanceScheduler = class {
@@ -3509,7 +3699,10 @@ var WikiMemory = class {
3509
3699
  );
3510
3700
  this.lintService = new LintService(this.lintRepo, this.ontologyService);
3511
3701
  this.embeddingService = new EmbeddingService(this.db, this.options, this.entryRepo, this.metadataRepo);
3512
- this.searchService = new SearchService(this.entryRepo);
3702
+ this.searchService = new SearchService(
3703
+ this.entryRepo,
3704
+ new MiniSearchIndexStrategy()
3705
+ );
3513
3706
  this.temporalService = new TemporalService(this.db, this.options, this.entryRepo, this.searchService, this.embeddingService);
3514
3707
  this.jobManager = new JobManager(this.prefix);
3515
3708
  this.promptService = new PromptService(options.config?.prompts, resolveGrounding(options.config?.grounding));