@equationalapplications/core-llm-wiki 5.2.0 → 5.3.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.
@@ -560,6 +560,42 @@ declare class WikiDuplicateHashError extends Error {
560
560
  entityId: string;
561
561
  });
562
562
  }
563
+ /**
564
+ * Thrown by `WikiMemory.upsertGraph` when the persisted ontology mode for the
565
+ * entity is `'strict'` and a caller-supplied node or edge `type` does not
566
+ * resolve against the entity's manifest. Mirrors {@link WikiBusyError}'s
567
+ * anchoring of canonical metadata on the error instance for stable
568
+ * observability. Per C4, the check is pre-flight and all-or-nothing — the
569
+ * first invalid item throws; NONE are written.
570
+ */
571
+ declare class WikiStrictOntologyViolation extends Error {
572
+ readonly entityId: string;
573
+ readonly kind: 'node' | 'edge';
574
+ readonly type: string;
575
+ readonly code: "WIKI_STRICT_ONTOLOGY_VIOLATION";
576
+ constructor(entityId: string, kind: 'node' | 'edge', type: string);
577
+ }
578
+ /**
579
+ * Thrown by `WikiMemory.upsertGraph`'s C2 probe when the supplied
580
+ * `sourceHash` is already mapped to a *different* `sourceRef` for the same
581
+ * entity. Indicates either a caller-side id-derivation collision, a race with
582
+ * another writer, or — per the C2 known-limitation note in the spec — two
583
+ * source files with genuinely identical content (byte-identical barrel
584
+ * re-exports, blank stubs, vendored boilerplate). In all cases: fail loud.
585
+ */
586
+ declare class WikiSourceRefHashCollision extends Error {
587
+ readonly entityId: string;
588
+ readonly sourceHash: string;
589
+ readonly existingSourceRef: string;
590
+ readonly attemptedSourceRef: string;
591
+ readonly code: "WIKI_SOURCE_REF_HASH_COLLISION";
592
+ constructor(params: {
593
+ entityId: string;
594
+ sourceHash: string;
595
+ existingSourceRef: string;
596
+ attemptedSourceRef: string;
597
+ });
598
+ }
563
599
  /**
564
600
  * Thrown by the serialized transaction wrapper when a SQLite driver error
565
601
  * escapes a transaction callback (nested BEGIN, SQLITE_BUSY, constraint
@@ -734,8 +770,16 @@ declare class EntryRepository extends BaseRepository {
734
770
  /**
735
771
  * Fetch recent non-deleted entries for an entity (limited), ordered by updated_at DESC.
736
772
  * Used by MaintenanceService.doRunLibrarian().
773
+ *
774
+ * When `excludeSourceRef` is supplied, the WHERE clause filters out that
775
+ * sourceRef BEFORE applying LIMIT. The caller in IngestionService is
776
+ * `findRecentByEntityId(entityId, 500, tx)` — it discards same-sourceRef
777
+ * rows after the read to build a cross-sourceRef title index. If those
778
+ * rows are filtered out in SQL, the limit applies to usable rows only
779
+ * and a re-ingest where the caller's sourceRef owns most recent facts
780
+ * still leaves cross-sourceRef entries in the index.
737
781
  */
738
- findRecentByEntityId(entityId: string, limit: number, tx?: SQLiteAdapter): Promise<WikiFact[]>;
782
+ findRecentByEntityId(entityId: string, limit: number, tx?: SQLiteAdapter, excludeSourceRef?: string): Promise<WikiFact[]>;
739
783
  /**
740
784
  * Fetch all non-deleted entries for an entity with embedding blobs preserved.
741
785
  * Used by ImportExportService for export/import round-tripping.
@@ -943,6 +987,49 @@ declare class EntryRepository extends BaseRepository {
943
987
  }>>;
944
988
  }
945
989
 
990
+ /**
991
+ * Per-(entity_id, source_hash) record of the canonical sourceRef currently
992
+ * holding that hash. The partial UNIQUE index on (entity_id, source_hash)
993
+ * WHERE deleted_at IS NULL enforces the sourceRef-level TOCTOU-race invariant
994
+ * for the #79 fix. See
995
+ * docs/superpowers/specs/2026-08-07-dependabot-concurrency-release-hygiene-design.md §B1.
996
+ *
997
+ * The entries table cannot express this invariant directly because a single
998
+ * `ingestDocument` call writes N facts that all share
999
+ * (entity_id, source_ref, source_hash); a UNIQUE index on
1000
+ * `entries(entity_id, source_hash)` would block the 2nd–Nth fact inserts in
1001
+ * the normal multi-fact path. The per-(entity, hash) granularity of
1002
+ * source_ref_index matches the TOCTOU invariant exactly.
1003
+ *
1004
+ * No outbox events: this is an internal index table whose state is fully
1005
+ * derivable from `entries`. Hosts that need CDC continue to read `entries`
1006
+ * outbox events; the index is not part of the user-visible contract.
1007
+ */
1008
+ declare class SourceRefIndexRepository extends BaseRepository {
1009
+ /**
1010
+ * Idempotent insert: the partial UNIQUE index catches concurrent inserts for
1011
+ * the same (entity_id, source_hash). The caller (IngestionService) catches
1012
+ * the resulting SQLITE_CONSTRAINT_UNIQUE and translates it to the per-mode
1013
+ * duplicate-hash outcome. Runtime IDs use the `sri_` prefix to avoid
1014
+ * collision with the deterministic `sri:<entity>:<hash>` IDs used by the v9
1015
+ * backfill (the colon separator keeps the two ID spaces disjoint).
1016
+ */
1017
+ upsert(entityId: string, sourceHash: string, sourceRef: string, tx: SQLiteAdapter): Promise<void>;
1018
+ /**
1019
+ * Idempotent soft-delete of the live row for (entity_id, source_ref).
1020
+ * Called at the start of every ingestDocument to remove the prior run's
1021
+ * index row, so the new upsert doesn't collide with itself. No-op when the
1022
+ * row is already soft-deleted or never existed.
1023
+ */
1024
+ softDeleteByEntityAndSourceRef(entityId: string, sourceRef: string, tx: SQLiteAdapter): Promise<number>;
1025
+ /**
1026
+ * Returns the live sourceRef holding the given hash, or null when no live
1027
+ * row exists. Used by the IngestionService pre-check (line 82) and the
1028
+ * catch-and-translate canonical lookup (line 212).
1029
+ */
1030
+ findActiveByEntityAndHash(entityId: string, sourceHash: string, tx?: SQLiteAdapter): Promise<string | null>;
1031
+ }
1032
+
946
1033
  declare class MetadataRepository extends BaseRepository {
947
1034
  getCheckpoint(entityId: string, tx: SQLiteAdapter): Promise<{
948
1035
  memory?: number;
@@ -960,6 +1047,15 @@ declare class MetadataRepository extends BaseRepository {
960
1047
  tableExists(tableName: string, tx?: SQLiteAdapter): Promise<boolean>;
961
1048
  getTableDdl(tableName: string, tx?: SQLiteAdapter): Promise<string | null>;
962
1049
  vacuum(): Promise<void>;
1050
+ /**
1051
+ * Returns the set of distinct `entity_id` values across entries/tasks/events.
1052
+ *
1053
+ * Deliberately widened from a prior `WHERE deleted_at IS NULL` form: the
1054
+ * returned set intentionally includes entities whose only rows are
1055
+ * soft-deleted, so the host's maintenance sweep can visit such
1056
+ * "decommissioned-scope" namespaces and reap the soft-deleted rows. Sorting
1057
+ * is `entity_id COLLATE BINARY` for stable, locale-independent ordering.
1058
+ */
963
1059
  getDistinctEntityIds(tx?: SQLiteAdapter): Promise<string[]>;
964
1060
  getManifest(entityId: string, tx?: SQLiteAdapter): Promise<{
965
1061
  mode: OntologyMode;
@@ -1065,8 +1161,46 @@ declare class JobManager {
1065
1161
  private prefix;
1066
1162
  private activeMaintenanceJobs;
1067
1163
  private activeIngestJobs;
1164
+ /**
1165
+ * Per-(entityId, sourceHash) promise-chain lock. Serializes ingest races
1166
+ * at the application level (the v9 partial UNIQUE index enforces it at
1167
+ * the DB level as defense-in-depth). Two callers holding the same hash
1168
+ * see FIFO order; different hashes never block one another.
1169
+ *
1170
+ * Keyed `${entityId}\0${sourceHash}` so distinct (entityId, sourceHash)
1171
+ * pairs can never share a lock. A raw concatenation would let
1172
+ * ("ab", "c…") collide with ("a", "bc…"); the NUL separator makes that
1173
+ * impossible because neither component can contain NUL (entityIds are
1174
+ * user-supplied identifiers; sourceHashes are hex digests).
1175
+ */
1176
+ private hashLocks;
1068
1177
  private statusSubscribers;
1069
1178
  constructor(prefix: string);
1179
+ private _hashLockKey;
1180
+ /**
1181
+ * Acquire a promise-chain lock scoped to one `(entityId, sourceHash)` pair.
1182
+ * Returns a zero-argument release closure that removes the current tail
1183
+ * from the chain. Releasing is idempotent so a holder that double-fires
1184
+ * (e.g. on a finally that races an explicit release) cannot unblock
1185
+ * the next holder prematurely.
1186
+ *
1187
+ * Documented ordering: ingest acquires the hash lock FIRST, then the
1188
+ * synchronous sourceRef ingest lock, and the release closure unwinds in
1189
+ * the opposite order (sourceRef first, hash second). Hash-then-sourceRef
1190
+ * is required because the v9 partial UNIQUE index conflicts on the
1191
+ * (entity_id, source_hash) pair regardless of source_ref, so two callers
1192
+ * with DIFFERENT source_refs racing the same hash are the exact case the
1193
+ * TOCTOU race fix needs to serialize.
1194
+ *
1195
+ * Mechanics: each holder owns a private `released` promise that resolves
1196
+ * only when THIS holder calls release(). The map stores the holder's
1197
+ * `released` promise as the "tail" — the NEXT caller awaits that tail
1198
+ * via `previous = this.hashLocks.get(key)`. The current caller gets
1199
+ * their unique release function once `previous` (the prior holder's
1200
+ * release signal) settles, so each holder is guaranteed a UNIQUE release
1201
+ * closure bound to their own `released` promise.
1202
+ */
1203
+ acquireHashLock(entityId: string, sourceHash: string): Promise<() => void>;
1070
1204
  private _pruneKey;
1071
1205
  private _reembedKey;
1072
1206
  private _globalReembedKey;
@@ -1103,6 +1237,21 @@ declare class JobManager {
1103
1237
  * for write() auto-trigger paths while preserving stricter checks in acquireLock().
1104
1238
  */
1105
1239
  tryAcquireAutoHealLock(entityId: string): boolean;
1240
+ /**
1241
+ * Centralized ingest lock acquisition. Acquires the hash lock FIRST
1242
+ * (FIFO across callers racing the same hash), then the synchronous
1243
+ * sourceRef ingest lock (rejects cross-entity conflicts and busy
1244
+ * operations). Returns a single zero-argument release closure that
1245
+ * unwinds in the opposite order — sourceRef first, hash second — so
1246
+ * the sourceRef lock is freed the instant its work is done and only
1247
+ * the hash lock keeps serializing the duplicate-content race window.
1248
+ *
1249
+ * On a sourceRef conflict (WikiBusyError thrown synchronously by the
1250
+ * underlying acquireLock) the hash lock is released BEFORE the error
1251
+ * propagates, so the next caller in the hash chain is not held up by
1252
+ * a request that never reached the DB.
1253
+ */
1254
+ acquireIngestLocks(entityId: string, sourceRef: string, sourceHash: string): Promise<() => void>;
1106
1255
  /**
1107
1256
  * Validates then acquires global + per-entity import locks atomically.
1108
1257
  * Validates all entities before acquiring any lock (same as current importDump semantics).
@@ -1115,26 +1264,6 @@ declare class JobManager {
1115
1264
  private _notifyStatusSubscribers;
1116
1265
  }
1117
1266
 
1118
- declare class EmbeddingService {
1119
- private db;
1120
- private options;
1121
- private entryRepo;
1122
- private metadataRepo;
1123
- constructor(db: SQLiteAdapter, options: WikiOptions, entryRepo: EntryRepository, metadataRepo: MetadataRepository);
1124
- storeEmbeddingDimension(dim: number): Promise<void>;
1125
- /** Promotes embedding_dimension_mismatch to canonical embedding_dimension when safe. */
1126
- reconcileEmbeddingDimension(): Promise<void>;
1127
- embedFact(fact: {
1128
- id: string;
1129
- entity_id: string;
1130
- title: string;
1131
- body: string;
1132
- tags: string | string[];
1133
- }): Promise<boolean>;
1134
- notifyEmbeddingPersisted(entityId: string, factId: string, vector: Float32Array | null): Promise<void>;
1135
- notifyEmbeddingPersistedOrThrow(entityId: string, factId: string, vector: Float32Array | null): Promise<void>;
1136
- }
1137
-
1138
1267
  interface NeighborhoodQueryOptions {
1139
1268
  maxDepth: number;
1140
1269
  direction: 'inbound' | 'outbound' | 'both';
@@ -1153,6 +1282,18 @@ declare class EdgeRepository extends BaseRepository {
1153
1282
  getByEntityId(entityId: string, tx?: SQLiteAdapter): Promise<WikiEdge[]>;
1154
1283
  /** Hard delete — edges have no soft-delete concept, only presence/absence. `tx` is REQUIRED. */
1155
1284
  bulkDeleteByEntityId(entityId: string, tx: SQLiteAdapter): Promise<void>;
1285
+ /**
1286
+ * Hard-delete every edge whose `source_id` is in `sourceFactIds`, scoped to
1287
+ * `entityId`. Returns the number of rows deleted. Edges have no soft-delete
1288
+ * concept; this method is named `softDeleteBySourceFactIds` for symmetry with
1289
+ * `EntryRepository.softDeleteBySource` (which IS a soft delete), but its
1290
+ * semantics are immediate presence/absence removal. Use ONLY inside an open
1291
+ * transaction — the method requires `tx` and performs no autocommit.
1292
+ *
1293
+ * Used by `IngestionService.upsertGraphCore` to retire the prior parse's
1294
+ * edges whose source facts are being superseded by `softDeleteBySource`.
1295
+ */
1296
+ softDeleteBySourceFactIds(entityId: string, sourceFactIds: readonly string[], tx: SQLiteAdapter): Promise<number>;
1156
1297
  /**
1157
1298
  * Multi-hop traversal from `sourceId` via SQLite `WITH RECURSIVE`. All filtering,
1158
1299
  * dead-ending, cycle-guarding, capping, and ordering happens in this one query.
@@ -1165,6 +1306,26 @@ declare class EdgeRepository extends BaseRepository {
1165
1306
  }>;
1166
1307
  }
1167
1308
 
1309
+ declare class EmbeddingService {
1310
+ private db;
1311
+ private options;
1312
+ private entryRepo;
1313
+ private metadataRepo;
1314
+ constructor(db: SQLiteAdapter, options: WikiOptions, entryRepo: EntryRepository, metadataRepo: MetadataRepository);
1315
+ storeEmbeddingDimension(dim: number): Promise<void>;
1316
+ /** Promotes embedding_dimension_mismatch to canonical embedding_dimension when safe. */
1317
+ reconcileEmbeddingDimension(): Promise<void>;
1318
+ embedFact(fact: {
1319
+ id: string;
1320
+ entity_id: string;
1321
+ title: string;
1322
+ body: string;
1323
+ tags: string | string[];
1324
+ }): Promise<boolean>;
1325
+ notifyEmbeddingPersisted(entityId: string, factId: string, vector: Float32Array | null): Promise<void>;
1326
+ notifyEmbeddingPersistedOrThrow(entityId: string, factId: string, vector: Float32Array | null): Promise<void>;
1327
+ }
1328
+
1168
1329
  type TitleIndexEntry = {
1169
1330
  id: string;
1170
1331
  okf_type: string | null;
@@ -1187,10 +1348,28 @@ declare class OntologyService {
1187
1348
  }>;
1188
1349
  buildPromptContext(entityId: string): Promise<OntologyPromptContext | null>;
1189
1350
  mergeEmergentUpdates(entityId: string, updates: OntologyUpdates, tx: SQLiteAdapter): Promise<OntologyManifest>;
1190
- validateAndNormalizeFact(fact: ExtractedFactWithOntology, manifest: OntologyManifest): {
1351
+ validateAndNormalizeFact(fact: ExtractedFactWithOntology, manifest: OntologyManifest, opts?: {
1352
+ strict?: boolean;
1353
+ entityId?: string;
1354
+ }): {
1191
1355
  okf_type: string | null;
1192
1356
  edges: ExtractedFactEdge[];
1193
1357
  };
1358
+ /**
1359
+ * Pure resolver: given a source type, LLM-supplied edges, the manifest,
1360
+ * and a title index, return a list of concrete `WikiEdge` objects ready
1361
+ * for persistence. Performs no DB writes. Used by callers that batch edge
1362
+ * writes inside their own transaction (e.g. `IngestionService.ingestDocument`
1363
+ * when delegating to `upsertGraphCore`).
1364
+ */
1365
+ resolveEdges(entityId: string, sourceId: string, sourceType: string | null, edges: ExtractedFactEdge[], manifest: OntologyManifest, titleIndex: Map<string, TitleIndexEntry>, now: number): WikiEdge[];
1366
+ /**
1367
+ * Backwards-compatible wrapper: resolves edges via {@link resolveEdges},
1368
+ * then persists each via `edgeRepo.addIgnoreDuplicate` in the supplied
1369
+ * transaction. Returns the number of edges persisted. Used by paths that
1370
+ * want self-contained edge persistence (e.g. MaintenanceService heal /
1371
+ * backfill callers that do not have a separate edge-write step).
1372
+ */
1194
1373
  resolveAndPersistEdges(entityId: string, sourceId: string, sourceType: string | null, edges: ExtractedFactEdge[], manifest: OntologyManifest, titleIndex: Map<string, TitleIndexEntry>, tx: SQLiteAdapter, now: number): Promise<number>;
1195
1374
  }
1196
1375
 
@@ -1224,12 +1403,15 @@ declare class IngestionService {
1224
1403
  private prefix;
1225
1404
  private options;
1226
1405
  private entryRepo;
1406
+ private sourceRefIndexRepo;
1407
+ private metadataRepo;
1408
+ private edgeRepo;
1227
1409
  private searchService;
1228
1410
  private jobManager;
1229
1411
  private embeddingService;
1230
1412
  private ontologyService?;
1231
1413
  private promptService;
1232
- constructor(db: SQLiteAdapter, prefix: string, options: WikiOptions, entryRepo: EntryRepository, searchService: SearchService, jobManager: JobManager, embeddingService: EmbeddingService, promptService?: PromptService, ontologyService?: OntologyService | undefined);
1414
+ constructor(db: SQLiteAdapter, prefix: string, options: WikiOptions, entryRepo: EntryRepository, sourceRefIndexRepo: SourceRefIndexRepository, metadataRepo: MetadataRepository, edgeRepo: EdgeRepository, searchService: SearchService, jobManager: JobManager, embeddingService: EmbeddingService, promptService?: PromptService, ontologyService?: OntologyService | undefined);
1233
1415
  ingestDocument(entityId: string, params: {
1234
1416
  sourceRef: string;
1235
1417
  sourceHash: string;
@@ -1245,6 +1427,49 @@ declare class IngestionService {
1245
1427
  chunks: number;
1246
1428
  duplicateOf?: string;
1247
1429
  }>;
1430
+ /**
1431
+ * Implements spec §2 data flow steps a–j with the host-facing
1432
+ * `{sourceRef, sourceHash, nodes, edges}` parameter shape. Shared between
1433
+ * `ingestDocument` (with `{ strict: false }` to preserve current behavior)
1434
+ * and `WikiMemory.upsertGraph` (which lets the persisted ontology mode
1435
+ * decide strictness).
1436
+ *
1437
+ * Runs ENTIRELY inside the supplied `tx`. Does not acquire locks, does not
1438
+ * open a nested transaction, does not perform post-commit work (search
1439
+ * sync, embedding, cache eviction). Those concerns remain in `ingestDocument`.
1440
+ *
1441
+ * The pre-flight validation (steps a–c) lives INSIDE this method per the
1442
+ * spec, so the public `WikiMemory.upsertGraph` can be a thin wrapper that
1443
+ * performs only the C2 probe before delegating.
1444
+ *
1445
+ * @returns Counts: nodesWritten (validated nodes persisted), edgesWritten
1446
+ * (manifest-valid edges persisted), superseded (prior facts soft-deleted
1447
+ * plus prior source-ref edges hard-deleted).
1448
+ */
1449
+ upsertGraphCore(entityId: string, params: {
1450
+ sourceRef: string;
1451
+ sourceHash: string;
1452
+ nodes: readonly {
1453
+ id: string;
1454
+ type: string;
1455
+ title: string;
1456
+ body?: string;
1457
+ tags?: readonly string[];
1458
+ confidence?: 'certain' | 'inferred' | 'tentative';
1459
+ }[];
1460
+ edges: readonly {
1461
+ type: string;
1462
+ sourceId: string;
1463
+ targetId: string;
1464
+ id?: string;
1465
+ }[];
1466
+ }, tx: SQLiteAdapter, opts?: {
1467
+ strict?: boolean;
1468
+ }): Promise<{
1469
+ nodesWritten: number;
1470
+ edgesWritten: number;
1471
+ superseded: number;
1472
+ }>;
1248
1473
  }
1249
1474
 
1250
1475
  declare class TaskRepository extends BaseRepository {
@@ -1361,6 +1586,7 @@ declare class MaintenanceService {
1361
1586
  private prefix;
1362
1587
  private options;
1363
1588
  private entryRepo;
1589
+ private sourceRefIndexRepo;
1364
1590
  private taskRepo;
1365
1591
  private eventRepo;
1366
1592
  private metadataRepo;
@@ -1369,7 +1595,7 @@ declare class MaintenanceService {
1369
1595
  private embeddingService;
1370
1596
  private ontologyService?;
1371
1597
  private promptService;
1372
- constructor(db: SQLiteAdapter, prefix: string, options: WikiOptions, entryRepo: EntryRepository, taskRepo: TaskRepository, eventRepo: EventRepository, metadataRepo: MetadataRepository, searchService: SearchService, jobManager: JobManager, embeddingService: EmbeddingService, promptService?: PromptService, ontologyService?: OntologyService | undefined);
1598
+ constructor(db: SQLiteAdapter, prefix: string, options: WikiOptions, entryRepo: EntryRepository, sourceRefIndexRepo: SourceRefIndexRepository, taskRepo: TaskRepository, eventRepo: EventRepository, metadataRepo: MetadataRepository, searchService: SearchService, jobManager: JobManager, embeddingService: EmbeddingService, promptService?: PromptService, ontologyService?: OntologyService | undefined);
1373
1599
  runPrune(entityId: string, options?: {
1374
1600
  retainSoftDeletedFor?: number | null;
1375
1601
  retainEventsFor?: number | null;
@@ -1582,6 +1808,7 @@ interface WikiMemoryTestAccess {
1582
1808
  promptService: PromptService;
1583
1809
  graphTraversalService: GraphTraversalService;
1584
1810
  entryRepo: EntryRepository;
1811
+ sourceRefIndexRepo: SourceRefIndexRepository;
1585
1812
  metadataRepo: MetadataRepository;
1586
1813
  jobManager: JobManager;
1587
1814
  }
@@ -1592,6 +1819,7 @@ declare class WikiMemory {
1592
1819
  private options;
1593
1820
  private entryRepo;
1594
1821
  private outboxRepo;
1822
+ private sourceRefIndexRepo;
1595
1823
  private taskRepo;
1596
1824
  private eventRepo;
1597
1825
  private edgeRepo;
@@ -1647,12 +1875,43 @@ declare class WikiMemory {
1647
1875
  lastIngestedAt: number;
1648
1876
  }>>;
1649
1877
  /**
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.
1878
+ * Returns the live source_refs for an entity that hold the given source_hash.
1879
+ * With v9, source_ref_index is the source of truth for the sourceRef-level
1880
+ * TOCTOU-race invariant: at most one sourceRef can hold a given
1881
+ * (entity_id, source_hash). The result is either a single-element array
1882
+ * (one canonical ref) or empty (no live ref holds the hash). Returned as
1883
+ * an array to preserve the existing public-API shape used by hosts
1884
+ * auditing duplicate-content collisions.
1654
1885
  */
1655
1886
  findSourceRefsByHash(entityId: string, sourceHash: string): Promise<string[]>;
1887
+ /**
1888
+ * Returns the set of `entity_id` values with at least one row in this
1889
+ * database, INCLUDING entities whose only remaining rows are soft-deleted
1890
+ * — sorted ascending `entity_id COLLATE BINARY`. Empty array when the
1891
+ * database has no entities.
1892
+ *
1893
+ * The deliberate inclusion of soft-deleted-only entities closes the
1894
+ * decommissioned-scope leak: a scoped namespace whose documents have all
1895
+ * been forgotten or superseded must still appear so host maintenance
1896
+ * sweeps (`runLibrarian`, `runHeal`, `runOntologyBackfill`, `runPrune`)
1897
+ * can visit it and reap the soft-deleted rows.
1898
+ *
1899
+ * Read-only. Never acquires a lock. Never opens a transaction. Propagates
1900
+ * underlying read errors.
1901
+ *
1902
+ * @param options.prefix - Optional string filter applied as
1903
+ * `id.startsWith(prefix)`. O(n) over distinct ids because the
1904
+ * `(entity_id, source_ref)` index is not seekable on `entity_id`-only
1905
+ * prefix. Empty-string prefix matches every id.
1906
+ *
1907
+ * Note: this widens the coverage of `exportDump` (which delegates to
1908
+ * `MetadataRepository.getDistinctEntityIds`); exported dumps now include
1909
+ * orphaned entities. This is a deliberate widening for backup/migration
1910
+ * coverage, not a breaking change to `exportDump`'s return shape.
1911
+ */
1912
+ listEntityIds(options?: {
1913
+ prefix?: string;
1914
+ }): Promise<string[]>;
1656
1915
  runPrune(entityId: string, options?: {
1657
1916
  retainSoftDeletedFor?: number | null;
1658
1917
  retainEventsFor?: number | null;
@@ -1768,6 +2027,54 @@ declare class WikiMemory {
1768
2027
  chunks: number;
1769
2028
  duplicateOf?: string;
1770
2029
  }>;
2030
+ /**
2031
+ * Deterministic write path into the graph. Accepts caller-supplied
2032
+ * `(nodes, edges)` and writes them under `(sourceRef, sourceHash)`
2033
+ * semantics identical to `ingestDocument`, minus the LLM extraction step.
2034
+ *
2035
+ * MUST be called from inside an open `db.withTransactionAsync` callback —
2036
+ * the third argument is the caller's `tx`. The method does not open a
2037
+ * nested transaction, does not acquire any lock, and does not perform
2038
+ * post-commit work (search sync, embedding, cache eviction). Hosts that
2039
+ * want embeddings synced should drive the existing maintenance sweep
2040
+ * (`runLibrarian` / `runHeal` / `runOntologyBackfill` / `runPrune`,
2041
+ * scoped via `listEntityIds`).
2042
+ *
2043
+ * Contract (full pre-flight validation lives inside
2044
+ * `IngestionService.upsertGraphCore`, which this method delegates to):
2045
+ * - C2: no-op when `(entityId, sourceHash)` is already mapped to
2046
+ * `params.sourceRef`; throws `WikiSourceRefHashCollision` if mapped to
2047
+ * a different `sourceRef`.
2048
+ * - C3: edges with dangling `targetId` are stored verbatim (no FK, no
2049
+ * resolution).
2050
+ * - C4: under persisted ontology mode `'strict'`, an out-of-manifest node
2051
+ * or edge `type` throws `WikiStrictOntologyViolation` (pre-flight,
2052
+ * all-or-nothing — NONE written on failure).
2053
+ *
2054
+ * @returns Counts: nodesWritten (validated nodes persisted), edgesWritten
2055
+ * (manifest-valid edges persisted), superseded (prior facts soft-deleted
2056
+ * plus prior source-ref edges hard-deleted).
2057
+ */
2058
+ upsertGraph(entityId: string, params: {
2059
+ sourceRef: string;
2060
+ sourceHash: string;
2061
+ nodes: readonly {
2062
+ id: string;
2063
+ type: string;
2064
+ title: string;
2065
+ body?: string;
2066
+ }[];
2067
+ edges: readonly {
2068
+ type: string;
2069
+ sourceId: string;
2070
+ targetId: string;
2071
+ id?: string;
2072
+ }[];
2073
+ }, adapter: SQLiteAdapter): Promise<{
2074
+ nodesWritten: number;
2075
+ edgesWritten: number;
2076
+ superseded: number;
2077
+ }>;
1771
2078
  /**
1772
2079
  * Returns up to `limit` unprocessed outbox events, oldest first.
1773
2080
  * Works regardless of enableOutbox value — allows draining after disabling.
@@ -1796,4 +2103,4 @@ declare class WikiMemory {
1796
2103
  }): Promise<void>;
1797
2104
  }
1798
2105
 
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 };
2106
+ export { ImportExportService 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, WikiSourceRefHashCollision as U, type VectorRanker as V, type WikiOptions as W, WikiStrictOntologyViolation as X, type WikiTask as Y, WikiTransactionError as Z, EmbeddingService as _, type MemoryDump as a, IngestionService as a0, JobManager as a1, MaintenanceService as a2, RetrievalService as a3, SearchService as a4, WriteService as a5, 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 };