@equationalapplications/core-llm-wiki 5.1.1 → 5.2.0

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.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-DCRNie7k.mjs';
2
- export { E as EntityStatus, d as ExtractedFact, e as ExtractedFactEdge, f as ExtractedFactWithOntology, g as ExtractedTask, h as GraphTraversalOptions, H as HEAL_BATCH_SIZE, i as HEAL_RECHECK_MS, j as HOOK_TIMEOUT_MARKER, k as HealResult, L as LLMProvider, l as ONTOLOGY_BACKFILL_BATCH_SIZE, m as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, n as ONTOLOGY_BACKFILL_RECHECK_MS, o as OntologyBackfillResult, p as OntologyConfig, q as OntologyEdgeType, r as OntologyMode, s as OntologyNodeType, t as OntologyPromptContext, u as OntologyUpdates, P as PromptOverrides, v as PromptService, w as PrunePartialFailureError, V as VectorRanker, x as VectorRankerFallback, y as VectorRankerRankArgs, z as VectorRankerSemanticResult, A as WikiBusyError, B as WikiBusyOperation, C as WikiCheckpoint, D as WikiConfig, I as WikiEdge, J as WikiEvent, K as WikiFact, N as WikiMemoryTestAccess, Q as WikiOutboxEvent, T as WikiTask, U as WikiTransactionError } from './testing-DCRNie7k.mjs';
1
+ import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-DyXISRwS.mjs';
2
+ export { E as EntityStatus, d as ExtractedFact, e as ExtractedFactEdge, f as ExtractedFactWithOntology, g as ExtractedTask, h as GraphTraversalOptions, H as HEAL_BATCH_SIZE, i as HEAL_RECHECK_MS, j as HOOK_TIMEOUT_MARKER, k as HealResult, L as LLMProvider, l as ONTOLOGY_BACKFILL_BATCH_SIZE, m as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, n as ONTOLOGY_BACKFILL_RECHECK_MS, o as OntologyBackfillResult, p as OntologyConfig, q as OntologyEdgeType, r as OntologyMode, s as OntologyNodeType, t as OntologyPromptContext, u as OntologyUpdates, P as PromptOverrides, v as PromptService, w as PrunePartialFailureError, V as VectorRanker, x as VectorRankerFallback, y as VectorRankerRankArgs, z as VectorRankerSemanticResult, A as WikiBusyError, B as WikiBusyOperation, C as WikiCheckpoint, D as WikiConfig, I as WikiDuplicateHashError, J as WikiEdge, K as WikiEvent, N as WikiFact, Q as WikiMemoryTestAccess, T as WikiOutboxEvent, U as WikiTask, X as WikiTransactionError } from './testing-DyXISRwS.mjs';
3
3
  import { OkfFile } from '@equationalapplications/core-okf';
4
4
  import 'minisearch';
5
5
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-DCRNie7k.js';
2
- export { E as EntityStatus, d as ExtractedFact, e as ExtractedFactEdge, f as ExtractedFactWithOntology, g as ExtractedTask, h as GraphTraversalOptions, H as HEAL_BATCH_SIZE, i as HEAL_RECHECK_MS, j as HOOK_TIMEOUT_MARKER, k as HealResult, L as LLMProvider, l as ONTOLOGY_BACKFILL_BATCH_SIZE, m as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, n as ONTOLOGY_BACKFILL_RECHECK_MS, o as OntologyBackfillResult, p as OntologyConfig, q as OntologyEdgeType, r as OntologyMode, s as OntologyNodeType, t as OntologyPromptContext, u as OntologyUpdates, P as PromptOverrides, v as PromptService, w as PrunePartialFailureError, V as VectorRanker, x as VectorRankerFallback, y as VectorRankerRankArgs, z as VectorRankerSemanticResult, A as WikiBusyError, B as WikiBusyOperation, C as WikiCheckpoint, D as WikiConfig, I as WikiEdge, J as WikiEvent, K as WikiFact, N as WikiMemoryTestAccess, Q as WikiOutboxEvent, T as WikiTask, U as WikiTransactionError } from './testing-DCRNie7k.js';
1
+ import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-DyXISRwS.js';
2
+ export { E as EntityStatus, d as ExtractedFact, e as ExtractedFactEdge, f as ExtractedFactWithOntology, g as ExtractedTask, h as GraphTraversalOptions, H as HEAL_BATCH_SIZE, i as HEAL_RECHECK_MS, j as HOOK_TIMEOUT_MARKER, k as HealResult, L as LLMProvider, l as ONTOLOGY_BACKFILL_BATCH_SIZE, m as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, n as ONTOLOGY_BACKFILL_RECHECK_MS, o as OntologyBackfillResult, p as OntologyConfig, q as OntologyEdgeType, r as OntologyMode, s as OntologyNodeType, t as OntologyPromptContext, u as OntologyUpdates, P as PromptOverrides, v as PromptService, w as PrunePartialFailureError, V as VectorRanker, x as VectorRankerFallback, y as VectorRankerRankArgs, z as VectorRankerSemanticResult, A as WikiBusyError, B as WikiBusyOperation, C as WikiCheckpoint, D as WikiConfig, I as WikiDuplicateHashError, J as WikiEdge, K as WikiEvent, N as WikiFact, Q as WikiMemoryTestAccess, T as WikiOutboxEvent, U as WikiTask, X as WikiTransactionError } from './testing-DyXISRwS.js';
3
3
  import { OkfFile } from '@equationalapplications/core-okf';
4
4
  import 'minisearch';
5
5
 
package/dist/index.js CHANGED
@@ -175,6 +175,15 @@ var WikiBusyError = class extends Error {
175
175
  this.entityId = entityId;
176
176
  }
177
177
  };
178
+ var WikiDuplicateHashError = class extends Error {
179
+ constructor(params) {
180
+ super(`Duplicate source hash for entity ${params.entityId}; another ref already holds this content`);
181
+ this.name = "WikiDuplicateHashError";
182
+ this.canonical = params.canonical;
183
+ this.sourceHash = params.sourceHash;
184
+ this.entityId = params.entityId;
185
+ }
186
+ };
178
187
  var WikiTransactionError = class extends Error {
179
188
  constructor(message, options) {
180
189
  super(message, options);
@@ -1080,12 +1089,154 @@ var EntryRepository = class extends BaseRepository {
1080
1089
  const row = await executor.getFirstAsync(
1081
1090
  `SELECT source_hash FROM ${this.prefix}entries
1082
1091
  WHERE entity_id = ? AND source_ref = ? AND deleted_at IS NULL
1083
- ORDER BY updated_at DESC
1092
+ ORDER BY updated_at DESC, id ASC
1084
1093
  LIMIT 1`,
1085
1094
  [entityId, sourceRef]
1086
1095
  );
1087
1096
  return row?.source_hash ?? null;
1088
1097
  }
1098
+ /**
1099
+ * Batch version of {@link findLatestSourceHash}. Returns a Map covering every
1100
+ * requested ref, where the value is the source_hash from the row with the
1101
+ * most-recently-updated live fact for that ref, or null when no live row exists.
1102
+ *
1103
+ * The SQL uses ROW_NUMBER() OVER (PARTITION BY source_ref ORDER BY updated_at
1104
+ * DESC, id ASC) — NOT MAX(source_hash). Aggregation with MAX(source_hash) is
1105
+ * wrong because MAX computes independently across grouped rows; the hash must
1106
+ * come from the exact row that wins MAX(updated_at). The `id ASC` tie-break
1107
+ * keeps selection deterministic when two live rows share `updated_at`.
1108
+ *
1109
+ * Source refs are de-duplicated and processed in chunks so the per-query bind
1110
+ * parameter count stays under SQLite's `SQLITE_MAX_VARIABLE_NUMBER` (default
1111
+ * 999, leaving one slot for `entity_id`).
1112
+ *
1113
+ * Empty input is a synchronous early return with zero SQL calls.
1114
+ */
1115
+ async findLatestSourceHashes(entityId, sourceRefs, tx) {
1116
+ const out = /* @__PURE__ */ new Map();
1117
+ if (sourceRefs.length === 0) return out;
1118
+ const dedupedRefs = Array.from(new Set(sourceRefs));
1119
+ for (const ref of dedupedRefs) {
1120
+ out.set(ref, null);
1121
+ }
1122
+ const executor = this.getExecutor(tx);
1123
+ const chunkLimit = Math.max(1, this.chunkSize - 1);
1124
+ for (let i = 0; i < dedupedRefs.length; i += chunkLimit) {
1125
+ const chunk = dedupedRefs.slice(i, i + chunkLimit);
1126
+ const placeholders = chunk.map(() => "?").join(",");
1127
+ const rows = await executor.getAllAsync(
1128
+ `WITH ranked AS (
1129
+ SELECT source_ref, source_hash,
1130
+ ROW_NUMBER() OVER (
1131
+ PARTITION BY source_ref
1132
+ ORDER BY updated_at DESC, id ASC
1133
+ ) as rn
1134
+ FROM ${this.prefix}entries
1135
+ WHERE entity_id = ? AND source_ref IN (${placeholders}) AND deleted_at IS NULL
1136
+ )
1137
+ SELECT source_ref, source_hash
1138
+ FROM ranked
1139
+ WHERE rn = 1`,
1140
+ [entityId, ...chunk]
1141
+ );
1142
+ for (const r of rows) {
1143
+ out.set(r.source_ref, r.source_hash);
1144
+ }
1145
+ }
1146
+ return out;
1147
+ }
1148
+ /**
1149
+ * Return the live source_refs for an entity that hold the given source_hash.
1150
+ * Used by the ingestDocument duplicate-hash guard and by hosts auditing
1151
+ * duplicate-content collisions. Sorted `COLLATE BINARY` so the canonical
1152
+ * ref (the code-unit-minimum of the set) is stable across deploys.
1153
+ */
1154
+ async findSourceRefsByHash(entityId, sourceHash, tx) {
1155
+ const executor = this.getExecutor(tx);
1156
+ const rows = await executor.getAllAsync(
1157
+ `SELECT source_ref FROM ${this.prefix}entries
1158
+ WHERE entity_id = ? AND source_hash = ? AND deleted_at IS NULL
1159
+ AND source_ref IS NOT NULL
1160
+ GROUP BY source_ref
1161
+ ORDER BY source_ref COLLATE BINARY`,
1162
+ [entityId, sourceHash]
1163
+ );
1164
+ return rows.map((r) => r.source_ref);
1165
+ }
1166
+ /**
1167
+ * Per-sourceRef rollup for an entity: one row per live source_ref, with the
1168
+ * most-recently-updated live hash (NOT the lexically-max hash) and a live
1169
+ * fact count.
1170
+ *
1171
+ * The hash comes from the same row that wins ROW_NUMBER() over updated_at
1172
+ * DESC, so a ref with multiple live hashes (e.g. from import) reports the
1173
+ * latest one — matching single-doc `findLatestSourceHash` semantics.
1174
+ *
1175
+ * Sort is COLLATE BINARY: locale-dependent ordering re-mints identity on
1176
+ * every deploy, which is the bug this whole section exists to prevent.
1177
+ */
1178
+ async listSourceRefs(entityId, tx) {
1179
+ const executor = this.getExecutor(tx);
1180
+ const rows = await executor.getAllAsync(
1181
+ `WITH ranked AS (
1182
+ SELECT source_ref, source_hash, updated_at,
1183
+ ROW_NUMBER() OVER (
1184
+ PARTITION BY source_ref
1185
+ ORDER BY updated_at DESC, id ASC
1186
+ ) AS rn,
1187
+ COUNT(*) OVER (PARTITION BY source_ref) AS fact_count
1188
+ FROM ${this.prefix}entries
1189
+ WHERE entity_id = ? AND deleted_at IS NULL AND source_ref IS NOT NULL
1190
+ )
1191
+ SELECT source_ref,
1192
+ source_hash AS source_hash,
1193
+ fact_count AS fact_count,
1194
+ updated_at AS last_ingested_at
1195
+ FROM ranked
1196
+ WHERE rn = 1
1197
+ ORDER BY source_ref COLLATE BINARY`,
1198
+ [entityId]
1199
+ );
1200
+ return rows.map((r) => ({
1201
+ sourceRef: r.source_ref,
1202
+ sourceHash: r.source_hash,
1203
+ factCount: Number(r.fact_count),
1204
+ lastIngestedAt: Number(r.last_ingested_at)
1205
+ }));
1206
+ }
1207
+ /**
1208
+ * Count live entries for an entity across all source_refs/source_hashes.
1209
+ * Used by forget({ dryRun, clearAll: true }).
1210
+ */
1211
+ async countLiveByEntityId(entityId, tx) {
1212
+ const executor = this.getExecutor(tx);
1213
+ const row = await executor.getFirstAsync(
1214
+ `SELECT COUNT(*) AS cnt FROM ${this.prefix}entries
1215
+ WHERE entity_id = ? AND deleted_at IS NULL`,
1216
+ [entityId]
1217
+ );
1218
+ return row?.cnt ?? 0;
1219
+ }
1220
+ /**
1221
+ * Count live entries matching a source filter (either or both may be null).
1222
+ * Used by forget({ dryRun: true }) with sourceRef/sourceHash.
1223
+ */
1224
+ async countLiveBySource(entityId, sourceRef, sourceHash, tx) {
1225
+ const executor = this.getExecutor(tx);
1226
+ let q = `SELECT COUNT(*) AS cnt FROM ${this.prefix}entries
1227
+ WHERE entity_id = ? AND deleted_at IS NULL`;
1228
+ const args = [entityId];
1229
+ if (sourceRef !== null) {
1230
+ q += ` AND source_ref = ?`;
1231
+ args.push(sourceRef);
1232
+ }
1233
+ if (sourceHash !== null) {
1234
+ q += ` AND source_hash = ?`;
1235
+ args.push(sourceHash);
1236
+ }
1237
+ const row = await executor.getFirstAsync(q, args);
1238
+ return row?.cnt ?? 0;
1239
+ }
1089
1240
  async findMetadataByIds(ids, tx) {
1090
1241
  if (ids.length === 0) return [];
1091
1242
  const executor = this.getExecutor(tx);
@@ -1637,6 +1788,18 @@ var TaskRepository = class extends BaseRepository {
1637
1788
  }
1638
1789
  return result.changes;
1639
1790
  }
1791
+ /**
1792
+ * Count live tasks for an entity. Used by forget({ dryRun, clearAll: true }).
1793
+ */
1794
+ async countLiveByEntityId(entityId, tx) {
1795
+ const executor = this.getExecutor(tx);
1796
+ const row = await executor.getFirstAsync(
1797
+ `SELECT COUNT(*) AS cnt FROM ${this.prefix}tasks
1798
+ WHERE entity_id = ? AND deleted_at IS NULL`,
1799
+ [entityId]
1800
+ );
1801
+ return row?.cnt ?? 0;
1802
+ }
1640
1803
  };
1641
1804
 
1642
1805
  // src/repositories/EventRepository.ts
@@ -2614,6 +2777,19 @@ var JobManager = class {
2614
2777
  return this.activeMaintenanceJobs.has(this.lockKeyFns[operation](entityId)) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
2615
2778
  case "prune":
2616
2779
  return this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._healKey(entityId)) || this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId)) || this._isReembedActive(entityId) || this._isIngestActiveFor(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
2780
+ case "reembed":
2781
+ return this.activeMaintenanceJobs.has(this._reembedKey(entityId)) || this.activeMaintenanceJobs.has(this._globalReembedKey()) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._healKey(entityId)) || this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId)) || this._isIngestActiveFor(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
2782
+ case "global_reembed":
2783
+ return this.activeMaintenanceJobs.has(this._globalReembedKey()) || this._isAnyMaintenanceActiveWithSuffix(":reembed") || this._isAnyMaintenanceActiveWithSuffix(":prune") || this._isAnyMaintenanceActiveWithSuffix(":librarian") || this._isAnyMaintenanceActiveWithSuffix(":heal") || this._isAnyMaintenanceActiveWithSuffix(":ontologyBackfill") || this.activeIngestJobs.size > 0 || this._isAnyMaintenanceActiveWithSuffix(":import") || this._isAnyMaintenanceActiveWithSuffix(":forget");
2784
+ case "import":
2785
+ case "forget": {
2786
+ const selfKey = operation === "import" ? this._importKey(entityId) : this._forgetKey(entityId);
2787
+ return this.activeMaintenanceJobs.has(selfKey) || this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._healKey(entityId)) || this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId)) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isIngestActiveFor(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
2788
+ }
2789
+ case "global_import":
2790
+ return this.activeMaintenanceJobs.has(this._globalImportKey());
2791
+ case "ingest":
2792
+ return this._hasIngestJob(entityId) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
2617
2793
  default:
2618
2794
  return false;
2619
2795
  }
@@ -3080,11 +3256,23 @@ var IngestionService = class {
3080
3256
  this.ontologyService = ontologyService;
3081
3257
  this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
3082
3258
  }
3083
- async ingestDocument(entityId, params) {
3259
+ async ingestDocument(entityId, params, opts) {
3084
3260
  const sourceRef = normalizeSourceRef(params.sourceRef);
3085
3261
  if (!sourceRef) throw new Error("Invalid sourceRef");
3086
3262
  const sourceHash = normalizeSourceHash(params.sourceHash);
3087
3263
  if (!sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
3264
+ const onDuplicateHash = opts?.onDuplicateHash ?? "ingest";
3265
+ if (onDuplicateHash !== "ingest") {
3266
+ const refs = await this.entryRepo.findSourceRefsByHash(entityId, sourceHash);
3267
+ const others = refs.filter((r) => r !== sourceRef);
3268
+ if (others.length > 0) {
3269
+ const canonical = others[0];
3270
+ if (onDuplicateHash === "throw") {
3271
+ throw new WikiDuplicateHashError({ canonical, sourceHash, entityId });
3272
+ }
3273
+ return { truncated: false, chunks: 0, duplicateOf: canonical };
3274
+ }
3275
+ }
3088
3276
  const maxChunkLength = params.maxChunkLength ?? this.options.config?.maxChunkLength ?? DEFAULT_MAX_CHUNK_LENGTH;
3089
3277
  const rawOverlap = params.chunkOverlap ?? this.options.config?.chunkOverlap ?? DEFAULT_CHUNK_OVERLAP;
3090
3278
  const chunkOverlap = Math.min(
@@ -3497,10 +3685,13 @@ var MaintenanceService = class {
3497
3685
  this.jobManager.releaseLock(op, entityId ?? "*");
3498
3686
  }
3499
3687
  }
3500
- async forget(entityId, params) {
3688
+ async forget(entityId, params, opts) {
3501
3689
  if (params.clearAll && (params.entryId !== void 0 || params.taskId !== void 0 || params.sourceRef !== void 0 || params.sourceHash !== void 0)) {
3502
3690
  throw new Error("forget() clearAll is mutually exclusive with entryId, taskId, sourceRef, and sourceHash");
3503
3691
  }
3692
+ if (opts?.dryRun === true) {
3693
+ return this.forgetDryRun(entityId, params);
3694
+ }
3504
3695
  this.jobManager.acquireLock("forget", entityId);
3505
3696
  try {
3506
3697
  const now = Date.now();
@@ -3561,11 +3752,42 @@ var MaintenanceService = class {
3561
3752
  throw new Error(`forget(${entityId}/${factId}) failed: ANN cleanup hook rejected`, { cause: this._sanitizeRankerError(hookErr) });
3562
3753
  }
3563
3754
  }
3564
- return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
3755
+ return params.clearAll ? { deleted: { entries: deletedEntries, tasks: deletedTasks }, metadataReset: true } : { deleted: { entries: deletedEntries, tasks: deletedTasks } };
3565
3756
  } finally {
3566
3757
  this.jobManager.releaseLock("forget", entityId);
3567
3758
  }
3568
3759
  }
3760
+ /**
3761
+ * Read-only dry-run preview of {@link forget}. Returns the same shape as a
3762
+ * real call would, without acquiring the lock, opening a transaction, staging
3763
+ * outbox events, or firing embedding hooks. Counts may be off-by-N during a
3764
+ * concurrent real forget — this is accepted, not considered a bug.
3765
+ *
3766
+ * `metadataReset` mirrors the real call: only `clearAll: true` returns true,
3767
+ * because only `clearAll` actually resets the metadata checkpoint.
3768
+ * `standard` (sourceRef/sourceHash) returns no metadataReset field, just like
3769
+ * the real call. `entryId`/`taskId` selectors are rejected with a clear error
3770
+ * (the spec keeps dry-run narrow).
3771
+ */
3772
+ async forgetDryRun(entityId, params) {
3773
+ if (params.entryId !== void 0 || params.taskId !== void 0) {
3774
+ throw new Error("forget({ dryRun: true }) does not support entryId/taskId selectors; use sourceRef/sourceHash or clearAll");
3775
+ }
3776
+ if (params.clearAll) {
3777
+ const entries2 = await this.entryRepo.countLiveByEntityId(entityId);
3778
+ const tasks = await this.taskRepo.countLiveByEntityId(entityId);
3779
+ return { deleted: { entries: entries2, tasks }, metadataReset: true };
3780
+ }
3781
+ const sourceRef = params.sourceRef !== void 0 ? normalizeSourceRef(params.sourceRef) : null;
3782
+ if (params.sourceRef !== void 0 && !sourceRef) throw new Error("Invalid sourceRef");
3783
+ const sourceHash = params.sourceHash !== void 0 ? normalizeSourceHash(params.sourceHash) : null;
3784
+ if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
3785
+ if (sourceRef === null && sourceHash === null) {
3786
+ return { deleted: { entries: 0, tasks: 0 } };
3787
+ }
3788
+ const entries = await this.entryRepo.countLiveBySource(entityId, sourceRef, sourceHash);
3789
+ return { deleted: { entries, tasks: 0 } };
3790
+ }
3569
3791
  /** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
3570
3792
  async doRunLibrarian(entityId, promptOverride) {
3571
3793
  const events = await this.eventRepo.getRecent(entityId, 50);
@@ -4289,6 +4511,15 @@ var ImportExportService = class {
4289
4511
  const safeTitle = clip(String(fact.title ?? ""), IMPORT_TITLE_MAX);
4290
4512
  const safeBody = clip(String(fact.body ?? ""), IMPORT_BODY_MAX);
4291
4513
  clippedTextByFactId.set(fact.id, { title: safeTitle, body: safeBody });
4514
+ let normalizedSourceRef = null;
4515
+ if (fact.source_ref !== null && fact.source_ref !== void 0) {
4516
+ normalizedSourceRef = normalizeSourceRef(fact.source_ref);
4517
+ if (normalizedSourceRef === null) {
4518
+ throw new Error(
4519
+ `importDump: invalid source_ref ${JSON.stringify(fact.source_ref)} for entity "${entityId}" fact "${fact.id}" (must normalize to a non-empty string; see ingestDocument's sourceRef validation)`
4520
+ );
4521
+ }
4522
+ }
4292
4523
  const factObj = {
4293
4524
  id: fact.id,
4294
4525
  entity_id: entityId,
@@ -4298,7 +4529,7 @@ var ImportExportService = class {
4298
4529
  confidence: fact.confidence,
4299
4530
  source_type: sourceType,
4300
4531
  source_hash: fact.source_hash,
4301
- source_ref: fact.source_ref,
4532
+ source_ref: normalizedSourceRef,
4302
4533
  created_at: fact.created_at,
4303
4534
  updated_at: safeUpdatedAt,
4304
4535
  last_accessed_at: fact.last_accessed_at,
@@ -5616,19 +5847,69 @@ var WikiMemory = class {
5616
5847
  });
5617
5848
  await this.searchService.sync();
5618
5849
  }
5619
- async hasChanged(entityId, sourceRef, sourceHash) {
5620
- const normalizedRef = normalizeSourceRef(sourceRef);
5621
- if (!normalizedRef) {
5622
- throw new Error(`Invalid sourceRef: ${JSON.stringify(sourceRef)}`);
5623
- }
5624
- const normalizedHash = normalizeSourceHash(sourceHash);
5625
- if (!normalizedHash) {
5626
- throw new Error(`Invalid sourceHash: must be a 64-character hex string (normalized to lowercase)`);
5627
- }
5628
- const storedHash = await this.entryRepo.findLatestSourceHash(entityId, normalizedRef);
5629
- if (storedHash === null) return true;
5630
- const normalizedStoredHash = normalizeSourceHash(storedHash);
5631
- return normalizedStoredHash !== normalizedHash;
5850
+ async hasChanged(entityId, sourceRefOrEntries, sourceHashArg) {
5851
+ if (typeof sourceRefOrEntries === "string") {
5852
+ const sourceRef = normalizeSourceRef(sourceRefOrEntries);
5853
+ if (!sourceRef) {
5854
+ throw new Error(`Invalid sourceRef: ${JSON.stringify(sourceRefOrEntries)}`);
5855
+ }
5856
+ const sourceHash = normalizeSourceHash(sourceHashArg);
5857
+ if (!sourceHash) {
5858
+ throw new Error("Invalid sourceHash: must be a 64-character hex string (normalized to lowercase)");
5859
+ }
5860
+ const storedHash = await this.entryRepo.findLatestSourceHash(entityId, sourceRef);
5861
+ if (storedHash === null) return true;
5862
+ const normalizedStoredHash = normalizeSourceHash(storedHash);
5863
+ return normalizedStoredHash !== sourceHash;
5864
+ }
5865
+ const entries = sourceRefOrEntries;
5866
+ if (entries.length === 0) return [];
5867
+ const normalized = entries.map((e) => {
5868
+ const r = normalizeSourceRef(e.sourceRef);
5869
+ if (!r) throw new Error(`Invalid sourceRef: ${JSON.stringify(e.sourceRef)}`);
5870
+ const h = normalizeSourceHash(e.sourceHash);
5871
+ if (!h) throw new Error("Invalid sourceHash: must be a 64-character hex string (normalized to lowercase)");
5872
+ return { rawSourceRef: e.sourceRef, sourceRef: r, sourceHash: h };
5873
+ });
5874
+ const latestHashes = await this.entryRepo.findLatestSourceHashes(entityId, normalized.map((e) => e.sourceRef));
5875
+ const distinctHashes = Array.from(new Set(normalized.map((e) => e.sourceHash)));
5876
+ const dupRefs = await Promise.all(
5877
+ distinctHashes.map((h) => this.entryRepo.findSourceRefsByHash(entityId, h))
5878
+ );
5879
+ const dupMap = /* @__PURE__ */ new Map();
5880
+ for (let i = 0; i < distinctHashes.length; i++) {
5881
+ dupMap.set(distinctHashes[i], dupRefs[i]);
5882
+ }
5883
+ return normalized.map((e) => {
5884
+ const stored = latestHashes.get(e.sourceRef);
5885
+ const changed = stored === void 0 || stored === null || normalizeSourceHash(stored) !== e.sourceHash;
5886
+ const allRefs = dupMap.get(e.sourceHash) ?? [];
5887
+ const others = allRefs.filter((r) => r !== e.sourceRef);
5888
+ if (others.length === 0) {
5889
+ return { sourceRef: e.rawSourceRef, changed };
5890
+ }
5891
+ const canonical = others[0];
5892
+ return { sourceRef: e.rawSourceRef, changed, duplicateOf: canonical };
5893
+ });
5894
+ }
5895
+ /**
5896
+ * Returns the live source_refs for an entity, one row per ref, with the most
5897
+ * recently-updated live `source_hash` and a live fact count. Refs are sorted
5898
+ * `COLLATE BINARY` (no locale dependency). Used by hosts to reconcile stored
5899
+ * state against a live source, or audit duplicate-hash collisions via
5900
+ * `findSourceRefsByHash`.
5901
+ */
5902
+ async listSourceRefs(entityId) {
5903
+ return this.entryRepo.listSourceRefs(entityId);
5904
+ }
5905
+ /**
5906
+ * Returns the live source_refs for an entity that hold the given source_hash,
5907
+ * sorted `COLLATE BINARY` ascending. The first element is the canonical ref
5908
+ * under the code-unit-minimum rule (no locale dependency). Used by the
5909
+ * ingestDocument guard and by hosts auditing duplicate-content collisions.
5910
+ */
5911
+ async findSourceRefsByHash(entityId, sourceHash) {
5912
+ return this.entryRepo.findSourceRefsByHash(entityId, sourceHash);
5632
5913
  }
5633
5914
  async runPrune(entityId, options) {
5634
5915
  return this.maintenanceService.runPrune(entityId, options);
@@ -5715,16 +5996,16 @@ var WikiMemory = class {
5715
5996
  async importDump(dump, opts) {
5716
5997
  return this.importExportService.importDump(dump, opts);
5717
5998
  }
5718
- async forget(entityId, params) {
5719
- return this.maintenanceService.forget(entityId, params);
5999
+ async forget(entityId, params, opts) {
6000
+ return this.maintenanceService.forget(entityId, params, opts);
5720
6001
  }
5721
6002
  /**
5722
6003
  * @param params.promptOverride - Overrides the system prompt for this ingest call only.
5723
6004
  * For persistent customization, set `options.config.prompts.ingestSystemPrompt` at
5724
6005
  * WikiMemory construction time.
5725
6006
  */
5726
- async ingestDocument(entityId, params) {
5727
- return this.ingestionService.ingestDocument(entityId, params);
6007
+ async ingestDocument(entityId, params, opts) {
6008
+ return this.ingestionService.ingestDocument(entityId, params, opts);
5728
6009
  }
5729
6010
  /**
5730
6011
  * Returns up to `limit` unprocessed outbox events, oldest first.
@@ -6543,6 +6824,7 @@ exports.ONTOLOGY_BACKFILL_SYSTEM_PROMPT = ONTOLOGY_BACKFILL_SYSTEM_PROMPT;
6543
6824
  exports.PromptService = PromptService;
6544
6825
  exports.PrunePartialFailureError = PrunePartialFailureError;
6545
6826
  exports.WikiBusyError = WikiBusyError;
6827
+ exports.WikiDuplicateHashError = WikiDuplicateHashError;
6546
6828
  exports.WikiMemory = WikiMemory;
6547
6829
  exports.WikiTransactionError = WikiTransactionError;
6548
6830
  exports.chunkText = chunkText;