@equationalapplications/core-llm-wiki 5.1.0 → 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.
@@ -544,6 +544,22 @@ declare class WikiBusyError extends Error {
544
544
  readonly entityId: string;
545
545
  constructor(operation: WikiBusyOperation, entityId: string);
546
546
  }
547
+ /**
548
+ * Thrown by `ingestDocument({ onDuplicateHash: 'throw' })` when a different
549
+ * source_ref in the same entity already holds the supplied source_hash against
550
+ * a live row. Mirrors {@link WikiBusyError}'s anchoring of canonical metadata
551
+ * on the error instance for stable observability.
552
+ */
553
+ declare class WikiDuplicateHashError extends Error {
554
+ readonly canonical: string;
555
+ readonly sourceHash: string;
556
+ readonly entityId: string;
557
+ constructor(params: {
558
+ canonical: string;
559
+ sourceHash: string;
560
+ entityId: string;
561
+ });
562
+ }
547
563
  /**
548
564
  * Thrown by the serialized transaction wrapper when a SQLite driver error
549
565
  * escapes a transaction callback (nested BEGIN, SQLITE_BUSY, constraint
@@ -796,6 +812,59 @@ declare class EntryRepository extends BaseRepository {
796
812
  }>>;
797
813
  updateSourceRefByRowid(rowid: number, sourceRef: string | null, tx: SQLiteAdapter): Promise<void>;
798
814
  findLatestSourceHash(entityId: string, sourceRef: string, tx?: SQLiteAdapter): Promise<string | null>;
815
+ /**
816
+ * Batch version of {@link findLatestSourceHash}. Returns a Map covering every
817
+ * requested ref, where the value is the source_hash from the row with the
818
+ * most-recently-updated live fact for that ref, or null when no live row exists.
819
+ *
820
+ * The SQL uses ROW_NUMBER() OVER (PARTITION BY source_ref ORDER BY updated_at
821
+ * DESC, id ASC) — NOT MAX(source_hash). Aggregation with MAX(source_hash) is
822
+ * wrong because MAX computes independently across grouped rows; the hash must
823
+ * come from the exact row that wins MAX(updated_at). The `id ASC` tie-break
824
+ * keeps selection deterministic when two live rows share `updated_at`.
825
+ *
826
+ * Source refs are de-duplicated and processed in chunks so the per-query bind
827
+ * parameter count stays under SQLite's `SQLITE_MAX_VARIABLE_NUMBER` (default
828
+ * 999, leaving one slot for `entity_id`).
829
+ *
830
+ * Empty input is a synchronous early return with zero SQL calls.
831
+ */
832
+ findLatestSourceHashes(entityId: string, sourceRefs: readonly string[], tx?: SQLiteAdapter): Promise<Map<string, string | null>>;
833
+ /**
834
+ * Return the live source_refs for an entity that hold the given source_hash.
835
+ * Used by the ingestDocument duplicate-hash guard and by hosts auditing
836
+ * duplicate-content collisions. Sorted `COLLATE BINARY` so the canonical
837
+ * ref (the code-unit-minimum of the set) is stable across deploys.
838
+ */
839
+ findSourceRefsByHash(entityId: string, sourceHash: string, tx?: SQLiteAdapter): Promise<string[]>;
840
+ /**
841
+ * Per-sourceRef rollup for an entity: one row per live source_ref, with the
842
+ * most-recently-updated live hash (NOT the lexically-max hash) and a live
843
+ * fact count.
844
+ *
845
+ * The hash comes from the same row that wins ROW_NUMBER() over updated_at
846
+ * DESC, so a ref with multiple live hashes (e.g. from import) reports the
847
+ * latest one — matching single-doc `findLatestSourceHash` semantics.
848
+ *
849
+ * Sort is COLLATE BINARY: locale-dependent ordering re-mints identity on
850
+ * every deploy, which is the bug this whole section exists to prevent.
851
+ */
852
+ listSourceRefs(entityId: string, tx?: SQLiteAdapter): Promise<Array<{
853
+ sourceRef: string;
854
+ sourceHash: string | null;
855
+ factCount: number;
856
+ lastIngestedAt: number;
857
+ }>>;
858
+ /**
859
+ * Count live entries for an entity across all source_refs/source_hashes.
860
+ * Used by forget({ dryRun, clearAll: true }).
861
+ */
862
+ countLiveByEntityId(entityId: string, tx?: SQLiteAdapter): Promise<number>;
863
+ /**
864
+ * Count live entries matching a source filter (either or both may be null).
865
+ * Used by forget({ dryRun: true }) with sourceRef/sourceHash.
866
+ */
867
+ countLiveBySource(entityId: string, sourceRef: string | null, sourceHash: string | null, tx?: SQLiteAdapter): Promise<number>;
799
868
  findMetadataByIds(ids: readonly string[], tx?: SQLiteAdapter): Promise<EntryRowMetadata[]>;
800
869
  findWithEmbeddingsByIds(ids: readonly string[], tx?: SQLiteAdapter): Promise<EntryRowWithEmbeddings[]>;
801
870
  findMetadataByEntityIds(entityIds: readonly string[], tx?: SQLiteAdapter): Promise<EntryRowMetadata[]>;
@@ -1169,9 +1238,12 @@ declare class IngestionService {
1169
1238
  chunkOverlap?: number;
1170
1239
  chunkConcurrency?: number;
1171
1240
  promptOverride?: string;
1241
+ }, opts?: {
1242
+ onDuplicateHash?: 'ingest' | 'skip' | 'throw';
1172
1243
  }): Promise<{
1173
1244
  truncated: boolean;
1174
1245
  chunks: number;
1246
+ duplicateOf?: string;
1175
1247
  }>;
1176
1248
  }
1177
1249
 
@@ -1230,6 +1302,10 @@ declare class TaskRepository extends BaseRepository {
1230
1302
  * `tx` is REQUIRED.
1231
1303
  */
1232
1304
  bulkSoftDeleteByEntityId(entityId: string, tx: SQLiteAdapter): Promise<number>;
1305
+ /**
1306
+ * Count live tasks for an entity. Used by forget({ dryRun, clearAll: true }).
1307
+ */
1308
+ countLiveByEntityId(entityId: string, tx?: SQLiteAdapter): Promise<number>;
1233
1309
  }
1234
1310
 
1235
1311
  declare class EventRepository extends BaseRepository {
@@ -1328,12 +1404,28 @@ declare class MaintenanceService {
1328
1404
  sourceRef?: string;
1329
1405
  sourceHash?: string;
1330
1406
  clearAll?: boolean;
1407
+ }, opts?: {
1408
+ dryRun?: boolean;
1331
1409
  }): Promise<{
1332
1410
  deleted: {
1333
1411
  entries: number;
1334
1412
  tasks: number;
1335
1413
  };
1414
+ metadataReset?: boolean;
1336
1415
  }>;
1416
+ /**
1417
+ * Read-only dry-run preview of {@link forget}. Returns the same shape as a
1418
+ * real call would, without acquiring the lock, opening a transaction, staging
1419
+ * outbox events, or firing embedding hooks. Counts may be off-by-N during a
1420
+ * concurrent real forget — this is accepted, not considered a bug.
1421
+ *
1422
+ * `metadataReset` mirrors the real call: only `clearAll: true` returns true,
1423
+ * because only `clearAll` actually resets the metadata checkpoint.
1424
+ * `standard` (sourceRef/sourceHash) returns no metadataReset field, just like
1425
+ * the real call. `entryId`/`taskId` selectors are rejected with a clear error
1426
+ * (the spec keeps dry-run narrow).
1427
+ */
1428
+ private forgetDryRun;
1337
1429
  /** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
1338
1430
  doRunLibrarian(entityId: string, promptOverride?: string): Promise<void>;
1339
1431
  /**
@@ -1523,6 +1615,44 @@ declare class WikiMemory {
1523
1615
  get __testAccess(): WikiMemoryTestAccess;
1524
1616
  setup(): Promise<void>;
1525
1617
  hasChanged(entityId: string, sourceRef: string, sourceHash: string): Promise<boolean>;
1618
+ /**
1619
+ * Batched overload.
1620
+ *
1621
+ * `duplicateOf`, when present, is the canonical stored DIFFERENT `source_ref`
1622
+ * holding the same hash — never the incoming ref itself (the incoming ref is
1623
+ * excluded from the canonical-sort set so a self-reference is impossible).
1624
+ * `duplicateOf` reflects the *DB-side* normalized spelling; `sourceRef` in
1625
+ * each result echoes the raw caller value. Hosts should not compare these
1626
+ * two fields directly to infer canonical status.
1627
+ */
1628
+ hasChanged(entityId: string, entries: Array<{
1629
+ sourceRef: string;
1630
+ sourceHash: string;
1631
+ }>): Promise<Array<{
1632
+ sourceRef: string;
1633
+ changed: boolean;
1634
+ duplicateOf?: string;
1635
+ }>>;
1636
+ /**
1637
+ * Returns the live source_refs for an entity, one row per ref, with the most
1638
+ * recently-updated live `source_hash` and a live fact count. Refs are sorted
1639
+ * `COLLATE BINARY` (no locale dependency). Used by hosts to reconcile stored
1640
+ * state against a live source, or audit duplicate-hash collisions via
1641
+ * `findSourceRefsByHash`.
1642
+ */
1643
+ listSourceRefs(entityId: string): Promise<Array<{
1644
+ sourceRef: string;
1645
+ sourceHash: string | null;
1646
+ factCount: number;
1647
+ lastIngestedAt: number;
1648
+ }>>;
1649
+ /**
1650
+ * Returns the live source_refs for an entity that hold the given source_hash,
1651
+ * sorted `COLLATE BINARY` ascending. The first element is the canonical ref
1652
+ * under the code-unit-minimum rule (no locale dependency). Used by the
1653
+ * ingestDocument guard and by hosts auditing duplicate-content collisions.
1654
+ */
1655
+ findSourceRefsByHash(entityId: string, sourceHash: string): Promise<string[]>;
1526
1656
  runPrune(entityId: string, options?: {
1527
1657
  retainSoftDeletedFor?: number | null;
1528
1658
  retainEventsFor?: number | null;
@@ -1609,11 +1739,14 @@ declare class WikiMemory {
1609
1739
  sourceRef?: string;
1610
1740
  sourceHash?: string;
1611
1741
  clearAll?: boolean;
1742
+ }, opts?: {
1743
+ dryRun?: boolean;
1612
1744
  }): Promise<{
1613
1745
  deleted: {
1614
1746
  entries: number;
1615
1747
  tasks: number;
1616
1748
  };
1749
+ metadataReset?: boolean;
1617
1750
  }>;
1618
1751
  /**
1619
1752
  * @param params.promptOverride - Overrides the system prompt for this ingest call only.
@@ -1628,9 +1761,12 @@ declare class WikiMemory {
1628
1761
  chunkOverlap?: number;
1629
1762
  chunkConcurrency?: number;
1630
1763
  promptOverride?: string;
1764
+ }, opts?: {
1765
+ onDuplicateHash?: 'ingest' | 'skip' | 'throw';
1631
1766
  }): Promise<{
1632
1767
  truncated: boolean;
1633
1768
  chunks: number;
1769
+ duplicateOf?: string;
1634
1770
  }>;
1635
1771
  /**
1636
1772
  * Returns up to `limit` unprocessed outbox events, oldest first.
@@ -1660,4 +1796,4 @@ declare class WikiMemory {
1660
1796
  }): Promise<void>;
1661
1797
  }
1662
1798
 
1663
- export { MaintenanceService as $, WikiBusyError as A, type WikiBusyOperation as B, type WikiCheckpoint as C, type WikiConfig as D, type EntityStatus as E, type FormatContextOptions as F, type GraphNeighborhood as G, HEAL_BATCH_SIZE as H, type WikiEdge as I, type WikiEvent as J, type WikiFact as K, type LLMProvider as L, type MemoryBundle as M, type WikiMemoryTestAccess as N, type OntologyManifest as O, type PromptOverrides as P, type WikiOutboxEvent as Q, type ReadOptions as R, type SQLiteAdapter as S, type WikiTask as T, WikiTransactionError as U, type VectorRanker as V, type WikiOptions as W, EmbeddingService as X, ImportExportService as Y, IngestionService as Z, JobManager as _, type MemoryDump as a, RetrievalService as a0, SearchService as a1, WriteService as a2, type FormattedMemoryDump as b, WikiMemory as c, type ExtractedFact as d, type ExtractedFactEdge as e, type ExtractedFactWithOntology as f, type ExtractedTask as g, type GraphTraversalOptions as h, HEAL_RECHECK_MS as i, HOOK_TIMEOUT_MARKER as j, type HealResult as k, ONTOLOGY_BACKFILL_BATCH_SIZE as l, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS as m, ONTOLOGY_BACKFILL_RECHECK_MS as n, type OntologyBackfillResult as o, type OntologyConfig as p, type OntologyEdgeType as q, type OntologyMode as r, type OntologyNodeType as s, type OntologyPromptContext as t, type OntologyUpdates as u, PromptService as v, PrunePartialFailureError as w, type VectorRankerFallback as x, type VectorRankerRankArgs as y, type VectorRankerSemanticResult as z };
1799
+ export { JobManager as $, WikiBusyError as A, type WikiBusyOperation as B, type WikiCheckpoint as C, type WikiConfig as D, type EntityStatus as E, type FormatContextOptions as F, type GraphNeighborhood as G, HEAL_BATCH_SIZE as H, WikiDuplicateHashError as I, type WikiEdge as J, type WikiEvent as K, type LLMProvider as L, type MemoryBundle as M, type WikiFact as N, type OntologyManifest as O, type PromptOverrides as P, type WikiMemoryTestAccess as Q, type ReadOptions as R, type SQLiteAdapter as S, type WikiOutboxEvent as T, type WikiTask as U, type VectorRanker as V, type WikiOptions as W, WikiTransactionError as X, EmbeddingService as Y, ImportExportService as Z, IngestionService as _, type MemoryDump as a, MaintenanceService as a0, RetrievalService as a1, SearchService as a2, WriteService as a3, type FormattedMemoryDump as b, WikiMemory as c, type ExtractedFact as d, type ExtractedFactEdge as e, type ExtractedFactWithOntology as f, type ExtractedTask as g, type GraphTraversalOptions as h, HEAL_RECHECK_MS as i, HOOK_TIMEOUT_MARKER as j, type HealResult as k, ONTOLOGY_BACKFILL_BATCH_SIZE as l, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS as m, ONTOLOGY_BACKFILL_RECHECK_MS as n, type OntologyBackfillResult as o, type OntologyConfig as p, type OntologyEdgeType as q, type OntologyMode as r, type OntologyNodeType as s, type OntologyPromptContext as t, type OntologyUpdates as u, PromptService as v, PrunePartialFailureError as w, type VectorRankerFallback as x, type VectorRankerRankArgs as y, type VectorRankerSemanticResult as z };
@@ -544,6 +544,22 @@ declare class WikiBusyError extends Error {
544
544
  readonly entityId: string;
545
545
  constructor(operation: WikiBusyOperation, entityId: string);
546
546
  }
547
+ /**
548
+ * Thrown by `ingestDocument({ onDuplicateHash: 'throw' })` when a different
549
+ * source_ref in the same entity already holds the supplied source_hash against
550
+ * a live row. Mirrors {@link WikiBusyError}'s anchoring of canonical metadata
551
+ * on the error instance for stable observability.
552
+ */
553
+ declare class WikiDuplicateHashError extends Error {
554
+ readonly canonical: string;
555
+ readonly sourceHash: string;
556
+ readonly entityId: string;
557
+ constructor(params: {
558
+ canonical: string;
559
+ sourceHash: string;
560
+ entityId: string;
561
+ });
562
+ }
547
563
  /**
548
564
  * Thrown by the serialized transaction wrapper when a SQLite driver error
549
565
  * escapes a transaction callback (nested BEGIN, SQLITE_BUSY, constraint
@@ -796,6 +812,59 @@ declare class EntryRepository extends BaseRepository {
796
812
  }>>;
797
813
  updateSourceRefByRowid(rowid: number, sourceRef: string | null, tx: SQLiteAdapter): Promise<void>;
798
814
  findLatestSourceHash(entityId: string, sourceRef: string, tx?: SQLiteAdapter): Promise<string | null>;
815
+ /**
816
+ * Batch version of {@link findLatestSourceHash}. Returns a Map covering every
817
+ * requested ref, where the value is the source_hash from the row with the
818
+ * most-recently-updated live fact for that ref, or null when no live row exists.
819
+ *
820
+ * The SQL uses ROW_NUMBER() OVER (PARTITION BY source_ref ORDER BY updated_at
821
+ * DESC, id ASC) — NOT MAX(source_hash). Aggregation with MAX(source_hash) is
822
+ * wrong because MAX computes independently across grouped rows; the hash must
823
+ * come from the exact row that wins MAX(updated_at). The `id ASC` tie-break
824
+ * keeps selection deterministic when two live rows share `updated_at`.
825
+ *
826
+ * Source refs are de-duplicated and processed in chunks so the per-query bind
827
+ * parameter count stays under SQLite's `SQLITE_MAX_VARIABLE_NUMBER` (default
828
+ * 999, leaving one slot for `entity_id`).
829
+ *
830
+ * Empty input is a synchronous early return with zero SQL calls.
831
+ */
832
+ findLatestSourceHashes(entityId: string, sourceRefs: readonly string[], tx?: SQLiteAdapter): Promise<Map<string, string | null>>;
833
+ /**
834
+ * Return the live source_refs for an entity that hold the given source_hash.
835
+ * Used by the ingestDocument duplicate-hash guard and by hosts auditing
836
+ * duplicate-content collisions. Sorted `COLLATE BINARY` so the canonical
837
+ * ref (the code-unit-minimum of the set) is stable across deploys.
838
+ */
839
+ findSourceRefsByHash(entityId: string, sourceHash: string, tx?: SQLiteAdapter): Promise<string[]>;
840
+ /**
841
+ * Per-sourceRef rollup for an entity: one row per live source_ref, with the
842
+ * most-recently-updated live hash (NOT the lexically-max hash) and a live
843
+ * fact count.
844
+ *
845
+ * The hash comes from the same row that wins ROW_NUMBER() over updated_at
846
+ * DESC, so a ref with multiple live hashes (e.g. from import) reports the
847
+ * latest one — matching single-doc `findLatestSourceHash` semantics.
848
+ *
849
+ * Sort is COLLATE BINARY: locale-dependent ordering re-mints identity on
850
+ * every deploy, which is the bug this whole section exists to prevent.
851
+ */
852
+ listSourceRefs(entityId: string, tx?: SQLiteAdapter): Promise<Array<{
853
+ sourceRef: string;
854
+ sourceHash: string | null;
855
+ factCount: number;
856
+ lastIngestedAt: number;
857
+ }>>;
858
+ /**
859
+ * Count live entries for an entity across all source_refs/source_hashes.
860
+ * Used by forget({ dryRun, clearAll: true }).
861
+ */
862
+ countLiveByEntityId(entityId: string, tx?: SQLiteAdapter): Promise<number>;
863
+ /**
864
+ * Count live entries matching a source filter (either or both may be null).
865
+ * Used by forget({ dryRun: true }) with sourceRef/sourceHash.
866
+ */
867
+ countLiveBySource(entityId: string, sourceRef: string | null, sourceHash: string | null, tx?: SQLiteAdapter): Promise<number>;
799
868
  findMetadataByIds(ids: readonly string[], tx?: SQLiteAdapter): Promise<EntryRowMetadata[]>;
800
869
  findWithEmbeddingsByIds(ids: readonly string[], tx?: SQLiteAdapter): Promise<EntryRowWithEmbeddings[]>;
801
870
  findMetadataByEntityIds(entityIds: readonly string[], tx?: SQLiteAdapter): Promise<EntryRowMetadata[]>;
@@ -1169,9 +1238,12 @@ declare class IngestionService {
1169
1238
  chunkOverlap?: number;
1170
1239
  chunkConcurrency?: number;
1171
1240
  promptOverride?: string;
1241
+ }, opts?: {
1242
+ onDuplicateHash?: 'ingest' | 'skip' | 'throw';
1172
1243
  }): Promise<{
1173
1244
  truncated: boolean;
1174
1245
  chunks: number;
1246
+ duplicateOf?: string;
1175
1247
  }>;
1176
1248
  }
1177
1249
 
@@ -1230,6 +1302,10 @@ declare class TaskRepository extends BaseRepository {
1230
1302
  * `tx` is REQUIRED.
1231
1303
  */
1232
1304
  bulkSoftDeleteByEntityId(entityId: string, tx: SQLiteAdapter): Promise<number>;
1305
+ /**
1306
+ * Count live tasks for an entity. Used by forget({ dryRun, clearAll: true }).
1307
+ */
1308
+ countLiveByEntityId(entityId: string, tx?: SQLiteAdapter): Promise<number>;
1233
1309
  }
1234
1310
 
1235
1311
  declare class EventRepository extends BaseRepository {
@@ -1328,12 +1404,28 @@ declare class MaintenanceService {
1328
1404
  sourceRef?: string;
1329
1405
  sourceHash?: string;
1330
1406
  clearAll?: boolean;
1407
+ }, opts?: {
1408
+ dryRun?: boolean;
1331
1409
  }): Promise<{
1332
1410
  deleted: {
1333
1411
  entries: number;
1334
1412
  tasks: number;
1335
1413
  };
1414
+ metadataReset?: boolean;
1336
1415
  }>;
1416
+ /**
1417
+ * Read-only dry-run preview of {@link forget}. Returns the same shape as a
1418
+ * real call would, without acquiring the lock, opening a transaction, staging
1419
+ * outbox events, or firing embedding hooks. Counts may be off-by-N during a
1420
+ * concurrent real forget — this is accepted, not considered a bug.
1421
+ *
1422
+ * `metadataReset` mirrors the real call: only `clearAll: true` returns true,
1423
+ * because only `clearAll` actually resets the metadata checkpoint.
1424
+ * `standard` (sourceRef/sourceHash) returns no metadataReset field, just like
1425
+ * the real call. `entryId`/`taskId` selectors are rejected with a clear error
1426
+ * (the spec keeps dry-run narrow).
1427
+ */
1428
+ private forgetDryRun;
1337
1429
  /** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
1338
1430
  doRunLibrarian(entityId: string, promptOverride?: string): Promise<void>;
1339
1431
  /**
@@ -1523,6 +1615,44 @@ declare class WikiMemory {
1523
1615
  get __testAccess(): WikiMemoryTestAccess;
1524
1616
  setup(): Promise<void>;
1525
1617
  hasChanged(entityId: string, sourceRef: string, sourceHash: string): Promise<boolean>;
1618
+ /**
1619
+ * Batched overload.
1620
+ *
1621
+ * `duplicateOf`, when present, is the canonical stored DIFFERENT `source_ref`
1622
+ * holding the same hash — never the incoming ref itself (the incoming ref is
1623
+ * excluded from the canonical-sort set so a self-reference is impossible).
1624
+ * `duplicateOf` reflects the *DB-side* normalized spelling; `sourceRef` in
1625
+ * each result echoes the raw caller value. Hosts should not compare these
1626
+ * two fields directly to infer canonical status.
1627
+ */
1628
+ hasChanged(entityId: string, entries: Array<{
1629
+ sourceRef: string;
1630
+ sourceHash: string;
1631
+ }>): Promise<Array<{
1632
+ sourceRef: string;
1633
+ changed: boolean;
1634
+ duplicateOf?: string;
1635
+ }>>;
1636
+ /**
1637
+ * Returns the live source_refs for an entity, one row per ref, with the most
1638
+ * recently-updated live `source_hash` and a live fact count. Refs are sorted
1639
+ * `COLLATE BINARY` (no locale dependency). Used by hosts to reconcile stored
1640
+ * state against a live source, or audit duplicate-hash collisions via
1641
+ * `findSourceRefsByHash`.
1642
+ */
1643
+ listSourceRefs(entityId: string): Promise<Array<{
1644
+ sourceRef: string;
1645
+ sourceHash: string | null;
1646
+ factCount: number;
1647
+ lastIngestedAt: number;
1648
+ }>>;
1649
+ /**
1650
+ * Returns the live source_refs for an entity that hold the given source_hash,
1651
+ * sorted `COLLATE BINARY` ascending. The first element is the canonical ref
1652
+ * under the code-unit-minimum rule (no locale dependency). Used by the
1653
+ * ingestDocument guard and by hosts auditing duplicate-content collisions.
1654
+ */
1655
+ findSourceRefsByHash(entityId: string, sourceHash: string): Promise<string[]>;
1526
1656
  runPrune(entityId: string, options?: {
1527
1657
  retainSoftDeletedFor?: number | null;
1528
1658
  retainEventsFor?: number | null;
@@ -1609,11 +1739,14 @@ declare class WikiMemory {
1609
1739
  sourceRef?: string;
1610
1740
  sourceHash?: string;
1611
1741
  clearAll?: boolean;
1742
+ }, opts?: {
1743
+ dryRun?: boolean;
1612
1744
  }): Promise<{
1613
1745
  deleted: {
1614
1746
  entries: number;
1615
1747
  tasks: number;
1616
1748
  };
1749
+ metadataReset?: boolean;
1617
1750
  }>;
1618
1751
  /**
1619
1752
  * @param params.promptOverride - Overrides the system prompt for this ingest call only.
@@ -1628,9 +1761,12 @@ declare class WikiMemory {
1628
1761
  chunkOverlap?: number;
1629
1762
  chunkConcurrency?: number;
1630
1763
  promptOverride?: string;
1764
+ }, opts?: {
1765
+ onDuplicateHash?: 'ingest' | 'skip' | 'throw';
1631
1766
  }): Promise<{
1632
1767
  truncated: boolean;
1633
1768
  chunks: number;
1769
+ duplicateOf?: string;
1634
1770
  }>;
1635
1771
  /**
1636
1772
  * Returns up to `limit` unprocessed outbox events, oldest first.
@@ -1660,4 +1796,4 @@ declare class WikiMemory {
1660
1796
  }): Promise<void>;
1661
1797
  }
1662
1798
 
1663
- export { MaintenanceService as $, WikiBusyError as A, type WikiBusyOperation as B, type WikiCheckpoint as C, type WikiConfig as D, type EntityStatus as E, type FormatContextOptions as F, type GraphNeighborhood as G, HEAL_BATCH_SIZE as H, type WikiEdge as I, type WikiEvent as J, type WikiFact as K, type LLMProvider as L, type MemoryBundle as M, type WikiMemoryTestAccess as N, type OntologyManifest as O, type PromptOverrides as P, type WikiOutboxEvent as Q, type ReadOptions as R, type SQLiteAdapter as S, type WikiTask as T, WikiTransactionError as U, type VectorRanker as V, type WikiOptions as W, EmbeddingService as X, ImportExportService as Y, IngestionService as Z, JobManager as _, type MemoryDump as a, RetrievalService as a0, SearchService as a1, WriteService as a2, type FormattedMemoryDump as b, WikiMemory as c, type ExtractedFact as d, type ExtractedFactEdge as e, type ExtractedFactWithOntology as f, type ExtractedTask as g, type GraphTraversalOptions as h, HEAL_RECHECK_MS as i, HOOK_TIMEOUT_MARKER as j, type HealResult as k, ONTOLOGY_BACKFILL_BATCH_SIZE as l, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS as m, ONTOLOGY_BACKFILL_RECHECK_MS as n, type OntologyBackfillResult as o, type OntologyConfig as p, type OntologyEdgeType as q, type OntologyMode as r, type OntologyNodeType as s, type OntologyPromptContext as t, type OntologyUpdates as u, PromptService as v, PrunePartialFailureError as w, type VectorRankerFallback as x, type VectorRankerRankArgs as y, type VectorRankerSemanticResult as z };
1799
+ export { JobManager as $, WikiBusyError as A, type WikiBusyOperation as B, type WikiCheckpoint as C, type WikiConfig as D, type EntityStatus as E, type FormatContextOptions as F, type GraphNeighborhood as G, HEAL_BATCH_SIZE as H, WikiDuplicateHashError as I, type WikiEdge as J, type WikiEvent as K, type LLMProvider as L, type MemoryBundle as M, type WikiFact as N, type OntologyManifest as O, type PromptOverrides as P, type WikiMemoryTestAccess as Q, type ReadOptions as R, type SQLiteAdapter as S, type WikiOutboxEvent as T, type WikiTask as U, type VectorRanker as V, type WikiOptions as W, WikiTransactionError as X, EmbeddingService as Y, ImportExportService as Z, IngestionService as _, type MemoryDump as a, MaintenanceService as a0, RetrievalService as a1, SearchService as a2, WriteService as a3, type FormattedMemoryDump as b, WikiMemory as c, type ExtractedFact as d, type ExtractedFactEdge as e, type ExtractedFactWithOntology as f, type ExtractedTask as g, type GraphTraversalOptions as h, HEAL_RECHECK_MS as i, HOOK_TIMEOUT_MARKER as j, type HealResult as k, ONTOLOGY_BACKFILL_BATCH_SIZE as l, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS as m, ONTOLOGY_BACKFILL_RECHECK_MS as n, type OntologyBackfillResult as o, type OntologyConfig as p, type OntologyEdgeType as q, type OntologyMode as r, type OntologyNodeType as s, type OntologyPromptContext as t, type OntologyUpdates as u, PromptService as v, PrunePartialFailureError as w, type VectorRankerFallback as x, type VectorRankerRankArgs as y, type VectorRankerSemanticResult as z };
@@ -1,2 +1,2 @@
1
- export { X as EmbeddingService, Y as ImportExportService, Z as IngestionService, _ as JobManager, _ as JobManagerType, $ as MaintenanceService, a0 as RetrievalService, a1 as SearchService, a1 as SearchServiceType, N as WikiMemoryTestAccess, a2 as WriteService } from './testing-DCRNie7k.mjs';
1
+ export { Y as EmbeddingService, Z as ImportExportService, _ as IngestionService, $ as JobManager, $ as JobManagerType, a0 as MaintenanceService, a1 as RetrievalService, a2 as SearchService, a2 as SearchServiceType, Q as WikiMemoryTestAccess, a3 as WriteService } from './testing-DyXISRwS.mjs';
2
2
  import 'minisearch';
package/dist/testing.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { X as EmbeddingService, Y as ImportExportService, Z as IngestionService, _ as JobManager, _ as JobManagerType, $ as MaintenanceService, a0 as RetrievalService, a1 as SearchService, a1 as SearchServiceType, N as WikiMemoryTestAccess, a2 as WriteService } from './testing-DCRNie7k.js';
1
+ export { Y as EmbeddingService, Z as ImportExportService, _ as IngestionService, $ as JobManager, $ as JobManagerType, a0 as MaintenanceService, a1 as RetrievalService, a2 as SearchService, a2 as SearchServiceType, Q as WikiMemoryTestAccess, a3 as WriteService } from './testing-DyXISRwS.js';
2
2
  import 'minisearch';
package/dist/testing.js CHANGED
@@ -15,6 +15,15 @@ var WikiBusyError = class extends Error {
15
15
  this.entityId = entityId;
16
16
  }
17
17
  };
18
+ var WikiDuplicateHashError = class extends Error {
19
+ constructor(params) {
20
+ super(`Duplicate source hash for entity ${params.entityId}; another ref already holds this content`);
21
+ this.name = "WikiDuplicateHashError";
22
+ this.canonical = params.canonical;
23
+ this.sourceHash = params.sourceHash;
24
+ this.entityId = params.entityId;
25
+ }
26
+ };
18
27
  var PrunePartialFailureError = class extends Error {
19
28
  constructor(deleted, failedAt, remaining, cause, deletedTasks = 0, deletedEvents = 0) {
20
29
  super(`Prune partially failed: deleted ${deleted}, failed at ${failedAt}, ${remaining} remaining`);
@@ -574,6 +583,15 @@ var ImportExportService = class {
574
583
  const safeTitle = clip(String(fact.title ?? ""), IMPORT_TITLE_MAX);
575
584
  const safeBody = clip(String(fact.body ?? ""), IMPORT_BODY_MAX);
576
585
  clippedTextByFactId.set(fact.id, { title: safeTitle, body: safeBody });
586
+ let normalizedSourceRef = null;
587
+ if (fact.source_ref !== null && fact.source_ref !== void 0) {
588
+ normalizedSourceRef = normalizeSourceRef(fact.source_ref);
589
+ if (normalizedSourceRef === null) {
590
+ throw new Error(
591
+ `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)`
592
+ );
593
+ }
594
+ }
577
595
  const factObj = {
578
596
  id: fact.id,
579
597
  entity_id: entityId,
@@ -583,7 +601,7 @@ var ImportExportService = class {
583
601
  confidence: fact.confidence,
584
602
  source_type: sourceType,
585
603
  source_hash: fact.source_hash,
586
- source_ref: fact.source_ref,
604
+ source_ref: normalizedSourceRef,
587
605
  created_at: fact.created_at,
588
606
  updated_at: safeUpdatedAt,
589
607
  last_accessed_at: fact.last_accessed_at,
@@ -986,11 +1004,23 @@ var IngestionService = class {
986
1004
  this.ontologyService = ontologyService;
987
1005
  this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
988
1006
  }
989
- async ingestDocument(entityId, params) {
1007
+ async ingestDocument(entityId, params, opts) {
990
1008
  const sourceRef = normalizeSourceRef(params.sourceRef);
991
1009
  if (!sourceRef) throw new Error("Invalid sourceRef");
992
1010
  const sourceHash = normalizeSourceHash(params.sourceHash);
993
1011
  if (!sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
1012
+ const onDuplicateHash = opts?.onDuplicateHash ?? "ingest";
1013
+ if (onDuplicateHash !== "ingest") {
1014
+ const refs = await this.entryRepo.findSourceRefsByHash(entityId, sourceHash);
1015
+ const others = refs.filter((r) => r !== sourceRef);
1016
+ if (others.length > 0) {
1017
+ const canonical = others[0];
1018
+ if (onDuplicateHash === "throw") {
1019
+ throw new WikiDuplicateHashError({ canonical, sourceHash, entityId });
1020
+ }
1021
+ return { truncated: false, chunks: 0, duplicateOf: canonical };
1022
+ }
1023
+ }
994
1024
  const maxChunkLength = params.maxChunkLength ?? this.options.config?.maxChunkLength ?? DEFAULT_MAX_CHUNK_LENGTH;
995
1025
  const rawOverlap = params.chunkOverlap ?? this.options.config?.chunkOverlap ?? DEFAULT_CHUNK_OVERLAP;
996
1026
  const chunkOverlap = Math.min(
@@ -1431,10 +1461,13 @@ var MaintenanceService = class {
1431
1461
  this.jobManager.releaseLock(op, entityId ?? "*");
1432
1462
  }
1433
1463
  }
1434
- async forget(entityId, params) {
1464
+ async forget(entityId, params, opts) {
1435
1465
  if (params.clearAll && (params.entryId !== void 0 || params.taskId !== void 0 || params.sourceRef !== void 0 || params.sourceHash !== void 0)) {
1436
1466
  throw new Error("forget() clearAll is mutually exclusive with entryId, taskId, sourceRef, and sourceHash");
1437
1467
  }
1468
+ if (opts?.dryRun === true) {
1469
+ return this.forgetDryRun(entityId, params);
1470
+ }
1438
1471
  this.jobManager.acquireLock("forget", entityId);
1439
1472
  try {
1440
1473
  const now = Date.now();
@@ -1495,11 +1528,42 @@ var MaintenanceService = class {
1495
1528
  throw new Error(`forget(${entityId}/${factId}) failed: ANN cleanup hook rejected`, { cause: this._sanitizeRankerError(hookErr) });
1496
1529
  }
1497
1530
  }
1498
- return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
1531
+ return params.clearAll ? { deleted: { entries: deletedEntries, tasks: deletedTasks }, metadataReset: true } : { deleted: { entries: deletedEntries, tasks: deletedTasks } };
1499
1532
  } finally {
1500
1533
  this.jobManager.releaseLock("forget", entityId);
1501
1534
  }
1502
1535
  }
1536
+ /**
1537
+ * Read-only dry-run preview of {@link forget}. Returns the same shape as a
1538
+ * real call would, without acquiring the lock, opening a transaction, staging
1539
+ * outbox events, or firing embedding hooks. Counts may be off-by-N during a
1540
+ * concurrent real forget — this is accepted, not considered a bug.
1541
+ *
1542
+ * `metadataReset` mirrors the real call: only `clearAll: true` returns true,
1543
+ * because only `clearAll` actually resets the metadata checkpoint.
1544
+ * `standard` (sourceRef/sourceHash) returns no metadataReset field, just like
1545
+ * the real call. `entryId`/`taskId` selectors are rejected with a clear error
1546
+ * (the spec keeps dry-run narrow).
1547
+ */
1548
+ async forgetDryRun(entityId, params) {
1549
+ if (params.entryId !== void 0 || params.taskId !== void 0) {
1550
+ throw new Error("forget({ dryRun: true }) does not support entryId/taskId selectors; use sourceRef/sourceHash or clearAll");
1551
+ }
1552
+ if (params.clearAll) {
1553
+ const entries2 = await this.entryRepo.countLiveByEntityId(entityId);
1554
+ const tasks = await this.taskRepo.countLiveByEntityId(entityId);
1555
+ return { deleted: { entries: entries2, tasks }, metadataReset: true };
1556
+ }
1557
+ const sourceRef = params.sourceRef !== void 0 ? normalizeSourceRef(params.sourceRef) : null;
1558
+ if (params.sourceRef !== void 0 && !sourceRef) throw new Error("Invalid sourceRef");
1559
+ const sourceHash = params.sourceHash !== void 0 ? normalizeSourceHash(params.sourceHash) : null;
1560
+ if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
1561
+ if (sourceRef === null && sourceHash === null) {
1562
+ return { deleted: { entries: 0, tasks: 0 } };
1563
+ }
1564
+ const entries = await this.entryRepo.countLiveBySource(entityId, sourceRef, sourceHash);
1565
+ return { deleted: { entries, tasks: 0 } };
1566
+ }
1503
1567
  /** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
1504
1568
  async doRunLibrarian(entityId, promptOverride) {
1505
1569
  const events = await this.eventRepo.getRecent(entityId, 50);
@@ -1729,10 +1793,10 @@ var MaintenanceService = class {
1729
1793
  const validNewFacts = newFacts.map(validateFact).filter((f) => f !== null);
1730
1794
  const insertedFacts = [];
1731
1795
  const uniqueDeletedFactIds = Array.from(new Set(safeDeleted));
1732
- const healFactsForDedupe = (await this.entryRepo.findInferredTitlesByEntityId(entityId)).filter((f) => !safeDeletedSet.has(f.id));
1733
1796
  await this.db.withTransactionAsync(async (tx) => {
1734
1797
  await this.entryRepo.downgradeByIds(safeDowngraded, entityId, tx);
1735
1798
  await this.entryRepo.softDeleteByIds(safeDeleted, entityId, tx);
1799
+ const healFactsForDedupe = await this.entryRepo.findInferredTitlesByEntityId(entityId, tx);
1736
1800
  for (const fact of validNewFacts) {
1737
1801
  const newTokens = titleTokens(fact.title);
1738
1802
  let skip = false;
@@ -3018,6 +3082,19 @@ var JobManager = class {
3018
3082
  return this.activeMaintenanceJobs.has(this.lockKeyFns[operation](entityId)) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
3019
3083
  case "prune":
3020
3084
  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);
3085
+ case "reembed":
3086
+ 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);
3087
+ case "global_reembed":
3088
+ 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");
3089
+ case "import":
3090
+ case "forget": {
3091
+ const selfKey = operation === "import" ? this._importKey(entityId) : this._forgetKey(entityId);
3092
+ 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);
3093
+ }
3094
+ case "global_import":
3095
+ return this.activeMaintenanceJobs.has(this._globalImportKey());
3096
+ case "ingest":
3097
+ return this._hasIngestJob(entityId) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
3021
3098
  default:
3022
3099
  return false;
3023
3100
  }