@equationalapplications/core-llm-wiki 5.2.1 → 5.3.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,5 +1,5 @@
1
- import { __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, generateId, BaseRepository, emptyManifest, resolveNodeType, validateInlineEdges, resolveEdgeDefinitions, normalizeTitleKey, WikiTransactionError, extractSqliteCode } from './chunk-6HNIOKT4.mjs';
2
- export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, HEAL_BATCH_SIZE, 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, WikiDuplicateHashError, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-6HNIOKT4.mjs';
1
+ import { __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, generateId, BaseRepository, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, normalizeTitleKey, WikiTransactionError, extractSqliteCode } from './chunk-KIJINNYU.mjs';
2
+ export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, HEAL_BATCH_SIZE, 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, WikiDuplicateHashError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-KIJINNYU.mjs';
3
3
  import { appendRelatedSection, buildConceptDocument, buildLogMd, buildEntityIndexMd, buildRootIndexMd, isAllowedOkfPath, parseRootIndexMd, parseEntityIndexMd, parseConcept, splitRelatedSection, extractMarkdownLinks, parseLogMd, parseEventIdComment, appendEventIdComment } from '@equationalapplications/core-okf';
4
4
 
5
5
  // src/db/schema.ts
@@ -736,12 +736,22 @@ var EntryRepository = class extends BaseRepository {
736
736
  /**
737
737
  * Fetch recent non-deleted entries for an entity (limited), ordered by updated_at DESC.
738
738
  * Used by MaintenanceService.doRunLibrarian().
739
- */
740
- async findRecentByEntityId(entityId, limit, tx) {
741
- const executor = this.getExecutor(tx);
739
+ *
740
+ * When `excludeSourceRef` is supplied, the WHERE clause filters out that
741
+ * sourceRef BEFORE applying LIMIT. The caller in IngestionService is
742
+ * `findRecentByEntityId(entityId, 500, tx)` — it discards same-sourceRef
743
+ * rows after the read to build a cross-sourceRef title index. If those
744
+ * rows are filtered out in SQL, the limit applies to usable rows only
745
+ * and a re-ingest where the caller's sourceRef owns most recent facts
746
+ * still leaves cross-sourceRef entries in the index.
747
+ */
748
+ async findRecentByEntityId(entityId, limit, tx, excludeSourceRef) {
749
+ const executor = this.getExecutor(tx);
750
+ const excludeClause = excludeSourceRef ? ` AND (source_ref IS NULL OR source_ref != ?)` : "";
751
+ const args = excludeSourceRef ? [entityId, excludeSourceRef, limit] : [entityId, limit];
742
752
  const rows = await executor.getAllAsync(
743
- `SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT ?`,
744
- [entityId, limit]
753
+ `SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL${excludeClause} ORDER BY updated_at DESC LIMIT ?`,
754
+ args
745
755
  );
746
756
  return rows.map(mapRowToFact);
747
757
  }
@@ -1932,6 +1942,34 @@ var EdgeRepository = class extends BaseRepository {
1932
1942
  const executor = this.getExecutor(tx);
1933
1943
  await executor.runAsync(`DELETE FROM ${this.prefix}edges WHERE entity_id = ?`, [entityId]);
1934
1944
  }
1945
+ /**
1946
+ * Hard-delete every edge whose `source_id` is in `sourceFactIds`, scoped to
1947
+ * `entityId`. Returns the number of rows deleted. Edges have no soft-delete
1948
+ * concept; this method is named `softDeleteBySourceFactIds` for symmetry with
1949
+ * `EntryRepository.softDeleteBySource` (which IS a soft delete), but its
1950
+ * semantics are immediate presence/absence removal. Use ONLY inside an open
1951
+ * transaction — the method requires `tx` and performs no autocommit.
1952
+ *
1953
+ * Used by `IngestionService.upsertGraphCore` to retire the prior parse's
1954
+ * edges whose source facts are being superseded by `softDeleteBySource`.
1955
+ */
1956
+ async softDeleteBySourceFactIds(entityId, sourceFactIds, tx) {
1957
+ if (sourceFactIds.length === 0) return 0;
1958
+ const executor = this.getExecutor(tx);
1959
+ const chunkSize = 500;
1960
+ let totalChanges = 0;
1961
+ for (let i = 0; i < sourceFactIds.length; i += chunkSize) {
1962
+ const chunk = sourceFactIds.slice(i, i + chunkSize);
1963
+ const placeholders = chunk.map(() => "?").join(",");
1964
+ const result = await executor.runAsync(
1965
+ `DELETE FROM ${this.prefix}edges
1966
+ WHERE entity_id = ? AND source_id IN (${placeholders})`,
1967
+ [entityId, ...chunk]
1968
+ );
1969
+ totalChanges += result.changes;
1970
+ }
1971
+ return totalChanges;
1972
+ }
1935
1973
  /**
1936
1974
  * Multi-hop traversal from `sourceId` via SQLite `WITH RECURSIVE`. All filtering,
1937
1975
  * dead-ending, cycle-guarding, capping, and ordering happens in this one query.
@@ -2110,16 +2148,27 @@ var OntologyService = class {
2110
2148
  this.invalidateCache(entityId);
2111
2149
  return merged;
2112
2150
  }
2113
- validateAndNormalizeFact(fact, manifest) {
2151
+ validateAndNormalizeFact(fact, manifest, opts) {
2114
2152
  const rawType = typeof fact.okf_type === "string" ? fact.okf_type : "";
2153
+ const strict = opts?.strict === true;
2115
2154
  const canonical = resolveNodeType(rawType, manifest);
2116
- if (!canonical) return { okf_type: null, edges: [] };
2117
- const edges = validateInlineEdges(canonical, null, fact.edges ?? [], manifest);
2155
+ if (!canonical) {
2156
+ if (strict) throw new WikiStrictOntologyViolation(opts?.entityId ?? "", "node", rawType);
2157
+ return { okf_type: null, edges: [] };
2158
+ }
2159
+ const edges = validateInlineEdges(canonical, null, fact.edges ?? [], manifest, opts);
2118
2160
  return { okf_type: canonical, edges };
2119
2161
  }
2120
- async resolveAndPersistEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, tx, now) {
2121
- if (!sourceType || edges.length === 0) return 0;
2122
- let persisted = 0;
2162
+ /**
2163
+ * Pure resolver: given a source type, LLM-supplied edges, the manifest,
2164
+ * and a title index, return a list of concrete `WikiEdge` objects ready
2165
+ * for persistence. Performs no DB writes. Used by callers that batch edge
2166
+ * writes inside their own transaction (e.g. `IngestionService.ingestDocument`
2167
+ * when delegating to `upsertGraphCore`).
2168
+ */
2169
+ resolveEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, now) {
2170
+ if (!sourceType || edges.length === 0) return [];
2171
+ const out = [];
2123
2172
  for (const edge of edges) {
2124
2173
  const candidates = resolveEdgeDefinitions(edge.edge_type, manifest).filter((d) => d.source_type.toLowerCase() === sourceType.toLowerCase());
2125
2174
  if (candidates.length === 0) continue;
@@ -2130,15 +2179,29 @@ var OntologyService = class {
2130
2179
  (d) => d.target_type.toLowerCase() === (target.okf_type ?? "").toLowerCase()
2131
2180
  );
2132
2181
  if (!def) continue;
2133
- const wikiEdge = {
2182
+ out.push({
2134
2183
  id: generateId(),
2135
2184
  entity_id: entityId,
2136
2185
  source_id: sourceId,
2137
2186
  target_id: target.id,
2138
2187
  edge_type: def.type,
2139
2188
  created_at: now
2140
- };
2141
- const inserted = await this.edgeRepo.addIgnoreDuplicate(wikiEdge, tx);
2189
+ });
2190
+ }
2191
+ return out;
2192
+ }
2193
+ /**
2194
+ * Backwards-compatible wrapper: resolves edges via {@link resolveEdges},
2195
+ * then persists each via `edgeRepo.addIgnoreDuplicate` in the supplied
2196
+ * transaction. Returns the number of edges persisted. Used by paths that
2197
+ * want self-contained edge persistence (e.g. MaintenanceService heal /
2198
+ * backfill callers that do not have a separate edge-write step).
2199
+ */
2200
+ async resolveAndPersistEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, tx, now) {
2201
+ const resolved = this.resolveEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, now);
2202
+ let persisted = 0;
2203
+ for (const edge of resolved) {
2204
+ const inserted = await this.edgeRepo.addIgnoreDuplicate(edge, tx);
2142
2205
  if (inserted) persisted++;
2143
2206
  }
2144
2207
  return persisted;
@@ -2214,6 +2277,8 @@ var WikiMemory = class {
2214
2277
  this.options,
2215
2278
  this.entryRepo,
2216
2279
  this.sourceRefIndexRepo,
2280
+ this.metadataRepo,
2281
+ this.edgeRepo,
2217
2282
  this.searchService,
2218
2283
  this.jobManager,
2219
2284
  this.embeddingService,
@@ -2406,6 +2471,36 @@ var WikiMemory = class {
2406
2471
  const canonical = await this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, sourceHash);
2407
2472
  return canonical === null ? [] : [canonical];
2408
2473
  }
2474
+ /**
2475
+ * Returns the set of `entity_id` values with at least one row in this
2476
+ * database, INCLUDING entities whose only remaining rows are soft-deleted
2477
+ * — sorted ascending `entity_id COLLATE BINARY`. Empty array when the
2478
+ * database has no entities.
2479
+ *
2480
+ * The deliberate inclusion of soft-deleted-only entities closes the
2481
+ * decommissioned-scope leak: a scoped namespace whose documents have all
2482
+ * been forgotten or superseded must still appear so host maintenance
2483
+ * sweeps (`runLibrarian`, `runHeal`, `runOntologyBackfill`, `runPrune`)
2484
+ * can visit it and reap the soft-deleted rows.
2485
+ *
2486
+ * Read-only. Never acquires a lock. Never opens a transaction. Propagates
2487
+ * underlying read errors.
2488
+ *
2489
+ * @param options.prefix - Optional string filter applied as
2490
+ * `id.startsWith(prefix)`. O(n) over distinct ids because the
2491
+ * `(entity_id, source_ref)` index is not seekable on `entity_id`-only
2492
+ * prefix. Empty-string prefix matches every id.
2493
+ *
2494
+ * Note: this widens the coverage of `exportDump` (which delegates to
2495
+ * `MetadataRepository.getDistinctEntityIds`); exported dumps now include
2496
+ * orphaned entities. This is a deliberate widening for backup/migration
2497
+ * coverage, not a breaking change to `exportDump`'s return shape.
2498
+ */
2499
+ async listEntityIds(options) {
2500
+ const ids = await this.metadataRepo.getDistinctEntityIds();
2501
+ if (options?.prefix === void 0) return ids;
2502
+ return ids.filter((id) => id.startsWith(options.prefix));
2503
+ }
2409
2504
  async runPrune(entityId, options) {
2410
2505
  return this.maintenanceService.runPrune(entityId, options);
2411
2506
  }
@@ -2502,6 +2597,57 @@ var WikiMemory = class {
2502
2597
  async ingestDocument(entityId, params, opts) {
2503
2598
  return this.ingestionService.ingestDocument(entityId, params, opts);
2504
2599
  }
2600
+ /**
2601
+ * Deterministic write path into the graph. Accepts caller-supplied
2602
+ * `(nodes, edges)` and writes them under `(sourceRef, sourceHash)`
2603
+ * semantics identical to `ingestDocument`, minus the LLM extraction step.
2604
+ *
2605
+ * MUST be called from inside an open `db.withTransactionAsync` callback —
2606
+ * the third argument is the caller's `tx`. The method does not open a
2607
+ * nested transaction, does not acquire any lock, and does not perform
2608
+ * post-commit work (search sync, embedding, cache eviction). Hosts that
2609
+ * want embeddings synced should drive the existing maintenance sweep
2610
+ * (`runLibrarian` / `runHeal` / `runOntologyBackfill` / `runPrune`,
2611
+ * scoped via `listEntityIds`).
2612
+ *
2613
+ * Contract (full pre-flight validation lives inside
2614
+ * `IngestionService.upsertGraphCore`, which this method delegates to):
2615
+ * - C2: no-op when `(entityId, sourceHash)` is already mapped to
2616
+ * `params.sourceRef`; throws `WikiSourceRefHashCollision` if mapped to
2617
+ * a different `sourceRef`.
2618
+ * - C3: edges with dangling `targetId` are stored verbatim (no FK, no
2619
+ * resolution).
2620
+ * - C4: under persisted ontology mode `'strict'`, an out-of-manifest node
2621
+ * or edge `type` throws `WikiStrictOntologyViolation` (pre-flight,
2622
+ * all-or-nothing — NONE written on failure).
2623
+ *
2624
+ * @returns Counts: nodesWritten (validated nodes persisted), edgesWritten
2625
+ * (manifest-valid edges persisted), superseded (prior facts soft-deleted
2626
+ * plus prior source-ref edges hard-deleted).
2627
+ */
2628
+ async upsertGraph(entityId, params, adapter) {
2629
+ const sourceRef = normalizeSourceRef(params.sourceRef);
2630
+ if (!sourceRef) throw new Error("Invalid sourceRef");
2631
+ const sourceHash = normalizeSourceHash(params.sourceHash);
2632
+ if (!sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
2633
+ const canonical = await this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, sourceHash, adapter);
2634
+ if (canonical !== null && canonical === sourceRef) {
2635
+ return { nodesWritten: 0, edgesWritten: 0, superseded: 0 };
2636
+ }
2637
+ if (canonical !== null && canonical !== sourceRef) {
2638
+ throw new WikiSourceRefHashCollision({
2639
+ entityId,
2640
+ sourceHash,
2641
+ existingSourceRef: canonical,
2642
+ attemptedSourceRef: sourceRef
2643
+ });
2644
+ }
2645
+ return this.ingestionService.upsertGraphCore(
2646
+ entityId,
2647
+ { sourceRef, sourceHash, nodes: params.nodes, edges: params.edges },
2648
+ adapter
2649
+ );
2650
+ }
2505
2651
  /**
2506
2652
  * Returns up to `limit` unprocessed outbox events, oldest first.
2507
2653
  * Works regardless of enableOutbox value — allows draining after disabling.