@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/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-M76CF2Q6.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-M76CF2Q6.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));
@@ -3902,6 +4095,32 @@ var WikiMemory = class {
3902
4095
  clearVectorCache() {
3903
4096
  this.searchService.evictCache();
3904
4097
  }
4098
+ /**
4099
+ * Brings the keyword search index up to date with SQLite after a host
4100
+ * transaction that used `upsertGraph` has committed. Core never sees that
4101
+ * commit, so without this the nodes stay out of keyword, hybrid and
4102
+ * fallback retrieval until the entity's next core write.
4103
+ *
4104
+ * With `entityId`, rebuilds that entity if it is stale or has never been
4105
+ * indexed; otherwise it is a no-op that still waits for rebuilds already
4106
+ * queued. Without an argument, rebuilds every entity currently marked
4107
+ * stale; when none is, it resolves immediately and does not wait for rebuilds
4108
+ * already queued.
4109
+ *
4110
+ * Call only after the host's transaction commits: called inside it, the
4111
+ * rebuild may index rows that later roll back, or miss them and clear the
4112
+ * stale flag anyway. Serialized with core's internal index syncs and takes
4113
+ * no maintenance lock. Never rejects for a valid `entityId` — on failure the
4114
+ * entity stays stale and the next call retries it. Does not compute
4115
+ * embeddings; that remains the maintenance sweep's job.
4116
+ *
4117
+ * @throws {TypeError} when `entityId` is given but is not a non-empty string.
4118
+ */
4119
+ async syncSearchIndex(entityId) {
4120
+ if (entityId === void 0) return this.searchService.syncStale();
4121
+ assertEntityId(entityId);
4122
+ return this.searchService.syncEntries(entityId, []);
4123
+ }
3905
4124
  async exportDump(entityIds) {
3906
4125
  return this.importExportService.exportDump(entityIds);
3907
4126
  }
@@ -3948,9 +4167,11 @@ var WikiMemory = class {
3948
4167
  * MUST be called from inside an open `db.withTransactionAsync` callback —
3949
4168
  * the third argument is the caller's `tx`. The method does not open a
3950
4169
  * nested transaction, does not acquire any lock, and does not perform
3951
- * post-commit work (search sync, embedding, cache eviction). Hosts that
3952
- * want embeddings synced should drive the existing maintenance sweep
3953
- * (`runLibrarian` / `runHeal` / `runOntologyBackfill` / `runPrune`,
4170
+ * post-commit work (search sync, embedding, cache eviction). After the
4171
+ * transaction commits, call `syncSearchIndex(entityId)` — or
4172
+ * `syncSearchIndex()` after writing several entities in one transaction —
4173
+ * to make the new nodes keyword-searchable. Embeddings still come from the maintenance
4174
+ * sweep (`runLibrarian` / `runHeal` / `runOntologyBackfill` / `runPrune`,
3954
4175
  * scoped via `listEntityIds`).
3955
4176
  *
3956
4177
  * Contract (full pre-flight validation lives inside