@equationalapplications/core-llm-wiki 5.2.1 → 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.
@@ -1003,6 +1047,15 @@ declare class MetadataRepository extends BaseRepository {
1003
1047
  tableExists(tableName: string, tx?: SQLiteAdapter): Promise<boolean>;
1004
1048
  getTableDdl(tableName: string, tx?: SQLiteAdapter): Promise<string | null>;
1005
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
+ */
1006
1059
  getDistinctEntityIds(tx?: SQLiteAdapter): Promise<string[]>;
1007
1060
  getManifest(entityId: string, tx?: SQLiteAdapter): Promise<{
1008
1061
  mode: OntologyMode;
@@ -1211,26 +1264,6 @@ declare class JobManager {
1211
1264
  private _notifyStatusSubscribers;
1212
1265
  }
1213
1266
 
1214
- declare class EmbeddingService {
1215
- private db;
1216
- private options;
1217
- private entryRepo;
1218
- private metadataRepo;
1219
- constructor(db: SQLiteAdapter, options: WikiOptions, entryRepo: EntryRepository, metadataRepo: MetadataRepository);
1220
- storeEmbeddingDimension(dim: number): Promise<void>;
1221
- /** Promotes embedding_dimension_mismatch to canonical embedding_dimension when safe. */
1222
- reconcileEmbeddingDimension(): Promise<void>;
1223
- embedFact(fact: {
1224
- id: string;
1225
- entity_id: string;
1226
- title: string;
1227
- body: string;
1228
- tags: string | string[];
1229
- }): Promise<boolean>;
1230
- notifyEmbeddingPersisted(entityId: string, factId: string, vector: Float32Array | null): Promise<void>;
1231
- notifyEmbeddingPersistedOrThrow(entityId: string, factId: string, vector: Float32Array | null): Promise<void>;
1232
- }
1233
-
1234
1267
  interface NeighborhoodQueryOptions {
1235
1268
  maxDepth: number;
1236
1269
  direction: 'inbound' | 'outbound' | 'both';
@@ -1249,6 +1282,18 @@ declare class EdgeRepository extends BaseRepository {
1249
1282
  getByEntityId(entityId: string, tx?: SQLiteAdapter): Promise<WikiEdge[]>;
1250
1283
  /** Hard delete — edges have no soft-delete concept, only presence/absence. `tx` is REQUIRED. */
1251
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>;
1252
1297
  /**
1253
1298
  * Multi-hop traversal from `sourceId` via SQLite `WITH RECURSIVE`. All filtering,
1254
1299
  * dead-ending, cycle-guarding, capping, and ordering happens in this one query.
@@ -1261,6 +1306,26 @@ declare class EdgeRepository extends BaseRepository {
1261
1306
  }>;
1262
1307
  }
1263
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
+
1264
1329
  type TitleIndexEntry = {
1265
1330
  id: string;
1266
1331
  okf_type: string | null;
@@ -1283,10 +1348,28 @@ declare class OntologyService {
1283
1348
  }>;
1284
1349
  buildPromptContext(entityId: string): Promise<OntologyPromptContext | null>;
1285
1350
  mergeEmergentUpdates(entityId: string, updates: OntologyUpdates, tx: SQLiteAdapter): Promise<OntologyManifest>;
1286
- validateAndNormalizeFact(fact: ExtractedFactWithOntology, manifest: OntologyManifest): {
1351
+ validateAndNormalizeFact(fact: ExtractedFactWithOntology, manifest: OntologyManifest, opts?: {
1352
+ strict?: boolean;
1353
+ entityId?: string;
1354
+ }): {
1287
1355
  okf_type: string | null;
1288
1356
  edges: ExtractedFactEdge[];
1289
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
+ */
1290
1373
  resolveAndPersistEdges(entityId: string, sourceId: string, sourceType: string | null, edges: ExtractedFactEdge[], manifest: OntologyManifest, titleIndex: Map<string, TitleIndexEntry>, tx: SQLiteAdapter, now: number): Promise<number>;
1291
1374
  }
1292
1375
 
@@ -1321,12 +1404,14 @@ declare class IngestionService {
1321
1404
  private options;
1322
1405
  private entryRepo;
1323
1406
  private sourceRefIndexRepo;
1407
+ private metadataRepo;
1408
+ private edgeRepo;
1324
1409
  private searchService;
1325
1410
  private jobManager;
1326
1411
  private embeddingService;
1327
1412
  private ontologyService?;
1328
1413
  private promptService;
1329
- constructor(db: SQLiteAdapter, prefix: string, options: WikiOptions, entryRepo: EntryRepository, sourceRefIndexRepo: SourceRefIndexRepository, 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);
1330
1415
  ingestDocument(entityId: string, params: {
1331
1416
  sourceRef: string;
1332
1417
  sourceHash: string;
@@ -1342,6 +1427,49 @@ declare class IngestionService {
1342
1427
  chunks: number;
1343
1428
  duplicateOf?: string;
1344
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
+ }>;
1345
1473
  }
1346
1474
 
1347
1475
  declare class TaskRepository extends BaseRepository {
@@ -1756,6 +1884,34 @@ declare class WikiMemory {
1756
1884
  * auditing duplicate-content collisions.
1757
1885
  */
1758
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[]>;
1759
1915
  runPrune(entityId: string, options?: {
1760
1916
  retainSoftDeletedFor?: number | null;
1761
1917
  retainEventsFor?: number | null;
@@ -1871,6 +2027,54 @@ declare class WikiMemory {
1871
2027
  chunks: number;
1872
2028
  duplicateOf?: string;
1873
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
+ }>;
1874
2078
  /**
1875
2079
  * Returns up to `limit` unprocessed outbox events, oldest first.
1876
2080
  * Works regardless of enableOutbox value — allows draining after disabling.
@@ -1899,4 +2103,4 @@ declare class WikiMemory {
1899
2103
  }): Promise<void>;
1900
2104
  }
1901
2105
 
1902
- 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 };
@@ -1,2 +1,2 @@
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-CAk9oFvw.mjs';
1
+ export { _ as EmbeddingService, $ as ImportExportService, a0 as IngestionService, a1 as JobManager, a1 as JobManagerType, a2 as MaintenanceService, a3 as RetrievalService, a4 as SearchService, a4 as SearchServiceType, Q as WikiMemoryTestAccess, a5 as WriteService } from './testing-zf3Jp_RL.mjs';
2
2
  import 'minisearch';
package/dist/testing.d.ts CHANGED
@@ -1,2 +1,2 @@
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-CAk9oFvw.js';
1
+ export { _ as EmbeddingService, $ as ImportExportService, a0 as IngestionService, a1 as JobManager, a1 as JobManagerType, a2 as MaintenanceService, a3 as RetrievalService, a4 as SearchService, a4 as SearchServiceType, Q as WikiMemoryTestAccess, a5 as WriteService } from './testing-zf3Jp_RL.js';
2
2
  import 'minisearch';
package/dist/testing.js CHANGED
@@ -77,6 +77,18 @@ var WikiDuplicateHashError = class extends Error {
77
77
  this.entityId = params.entityId;
78
78
  }
79
79
  };
80
+ var WikiStrictOntologyViolation = class extends Error {
81
+ constructor(entityId, kind, type) {
82
+ super(
83
+ `Out-of-manifest ${kind} type "${type}" for entity "${entityId}" under strict mode.`
84
+ );
85
+ this.code = "WIKI_STRICT_ONTOLOGY_VIOLATION";
86
+ this.entityId = entityId;
87
+ this.kind = kind;
88
+ this.type = type;
89
+ this.name = "WikiStrictOntologyViolation";
90
+ }
91
+ };
80
92
  var WikiTransactionError = class extends Error {
81
93
  constructor(message, options) {
82
94
  super(message, options);
@@ -1053,12 +1065,14 @@ var DEFAULT_CHUNK_OVERLAP = 400;
1053
1065
 
1054
1066
  // src/services/IngestionService.ts
1055
1067
  var IngestionService = class {
1056
- constructor(db, prefix, options, entryRepo, sourceRefIndexRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
1068
+ constructor(db, prefix, options, entryRepo, sourceRefIndexRepo, metadataRepo, edgeRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
1057
1069
  this.db = db;
1058
1070
  this.prefix = prefix;
1059
1071
  this.options = options;
1060
1072
  this.entryRepo = entryRepo;
1061
1073
  this.sourceRefIndexRepo = sourceRefIndexRepo;
1074
+ this.metadataRepo = metadataRepo;
1075
+ this.edgeRepo = edgeRepo;
1062
1076
  this.searchService = searchService;
1063
1077
  this.jobManager = jobManager;
1064
1078
  this.embeddingService = embeddingService;
@@ -1131,12 +1145,8 @@ var IngestionService = class {
1131
1145
  try {
1132
1146
  await this.db.withTransactionAsync(async (tx) => {
1133
1147
  deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
1134
- await this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, null);
1135
- await this.sourceRefIndexRepo.softDeleteByEntityAndSourceRef(entityId, sourceRef, tx);
1136
- await this.sourceRefIndexRepo.upsert(entityId, sourceHash, sourceRef, tx);
1137
1148
  const titleIndex = /* @__PURE__ */ new Map();
1138
- const pendingEdges = [];
1139
- const existingFacts = await this.entryRepo.findRecentByEntityId(entityId, 500, tx);
1149
+ const existingFacts = await this.entryRepo.findRecentByEntityId(entityId, 500, tx, sourceRef);
1140
1150
  for (const existing of existingFacts) {
1141
1151
  titleIndex.set(normalizeTitleKey(existing.title), {
1142
1152
  id: existing.id,
@@ -1145,6 +1155,8 @@ var IngestionService = class {
1145
1155
  }
1146
1156
  let ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
1147
1157
  let { mode, manifest } = ontologyState;
1158
+ const hostNodes = [];
1159
+ const rawEdgeRequests = [];
1148
1160
  for (const { facts, ontology_updates } of orderedChunkFacts) {
1149
1161
  if (mode === "emergent" && ontology_updates && this.ontologyService) {
1150
1162
  manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontology_updates, tx);
@@ -1153,45 +1165,51 @@ var IngestionService = class {
1153
1165
  }
1154
1166
  for (const fact of facts) {
1155
1167
  const ontologyFact = fact;
1156
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
1168
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
1157
1169
  const id = generateId("fact_");
1158
- const wikiFact = {
1170
+ hostNodes.push({
1159
1171
  id,
1160
- entity_id: entityId,
1172
+ type: ontologyFact.okf_type ?? "",
1161
1173
  title: fact.title,
1162
1174
  body: fact.body,
1175
+ // Forward the LLM-extracted tags/confidence through the
1176
+ // extracted-shape upsertGraphCore path so the entries row
1177
+ // stores them (search filterability, heal-candidate
1178
+ // selection, runReembed embedding-text signal). The public
1179
+ // WikiMemory.upsertGraph API doesn't accept tags/confidence
1180
+ // — host-supplied deterministic nodes default to [] and
1181
+ // 'certain' as documented.
1163
1182
  tags: fact.tags,
1164
- confidence: fact.confidence,
1165
- source_type: "immutable_document",
1166
- source_hash: sourceHash,
1167
- source_ref: sourceRef,
1168
- created_at: now,
1169
- updated_at: now,
1170
- last_accessed_at: null,
1171
- access_count: 0,
1172
- deleted_at: null,
1173
- okf_type: normalized.okf_type
1174
- };
1175
- await this.entryRepo.upsert(wikiFact, tx);
1183
+ confidence: fact.confidence
1184
+ });
1176
1185
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1177
1186
  titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
1178
1187
  if (normalized.edges.length > 0) {
1179
- pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
1188
+ rawEdgeRequests.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
1180
1189
  }
1181
1190
  }
1182
1191
  }
1183
- for (const item of pendingEdges) {
1184
- await this.ontologyService?.resolveAndPersistEdges(
1192
+ const hostEdges = [];
1193
+ for (const req of rawEdgeRequests) {
1194
+ const resolved = this.ontologyService?.resolveEdges(
1185
1195
  entityId,
1186
- item.sourceId,
1187
- item.sourceType,
1188
- item.edges ?? [],
1196
+ req.sourceId,
1197
+ req.sourceType,
1198
+ req.edges,
1189
1199
  manifest,
1190
1200
  titleIndex,
1191
- tx,
1192
1201
  now
1193
- );
1202
+ ) ?? [];
1203
+ for (const e of resolved) {
1204
+ hostEdges.push({ type: e.edge_type, sourceId: e.source_id, targetId: e.target_id });
1205
+ }
1194
1206
  }
1207
+ await this.upsertGraphCore(
1208
+ entityId,
1209
+ { sourceRef, sourceHash, nodes: hostNodes, edges: hostEdges },
1210
+ tx,
1211
+ { strict: false }
1212
+ );
1195
1213
  });
1196
1214
  } catch (err) {
1197
1215
  const sqliteCode = err instanceof WikiTransactionError ? err.sqliteErrorCode : extractSqliteCode(err);
@@ -1221,6 +1239,126 @@ var IngestionService = class {
1221
1239
  releaseIngestLocks();
1222
1240
  }
1223
1241
  }
1242
+ /**
1243
+ * Implements spec §2 data flow steps a–j with the host-facing
1244
+ * `{sourceRef, sourceHash, nodes, edges}` parameter shape. Shared between
1245
+ * `ingestDocument` (with `{ strict: false }` to preserve current behavior)
1246
+ * and `WikiMemory.upsertGraph` (which lets the persisted ontology mode
1247
+ * decide strictness).
1248
+ *
1249
+ * Runs ENTIRELY inside the supplied `tx`. Does not acquire locks, does not
1250
+ * open a nested transaction, does not perform post-commit work (search
1251
+ * sync, embedding, cache eviction). Those concerns remain in `ingestDocument`.
1252
+ *
1253
+ * The pre-flight validation (steps a–c) lives INSIDE this method per the
1254
+ * spec, so the public `WikiMemory.upsertGraph` can be a thin wrapper that
1255
+ * performs only the C2 probe before delegating.
1256
+ *
1257
+ * @returns Counts: nodesWritten (validated nodes persisted), edgesWritten
1258
+ * (manifest-valid edges persisted), superseded (prior facts soft-deleted
1259
+ * plus prior source-ref edges hard-deleted).
1260
+ */
1261
+ async upsertGraphCore(entityId, params, tx, opts) {
1262
+ const now = Date.now();
1263
+ const ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
1264
+ let { mode, manifest } = ontologyState;
1265
+ const strictEffective = opts?.strict === false ? false : opts?.strict === true || mode === "strict";
1266
+ const wikiFacts = [];
1267
+ for (const node of params.nodes) {
1268
+ const factLike = { okf_type: node.type, edges: [] };
1269
+ const normalized = this.ontologyService?.validateAndNormalizeFact(factLike, manifest, {
1270
+ strict: strictEffective,
1271
+ entityId
1272
+ }) ?? { okf_type: null};
1273
+ const wikiFact = {
1274
+ id: node.id,
1275
+ entity_id: entityId,
1276
+ title: node.title,
1277
+ body: node.body ?? "",
1278
+ // Public upsertGraph callers don't supply tags/confidence, so they
1279
+ // default to [] / 'certain' for deterministic AST-grade facts.
1280
+ // ingestDocument routes through this method for the LLM path and
1281
+ // forwards the LLM-supplied tags + confidence, preserving the prior
1282
+ // behavior (search filterability, heal-candidate selection, runReembed
1283
+ // embedding-text signal).
1284
+ tags: node.tags ? Array.from(node.tags) : [],
1285
+ confidence: node.confidence ?? "certain",
1286
+ source_type: "immutable_document",
1287
+ source_hash: params.sourceHash,
1288
+ source_ref: params.sourceRef,
1289
+ created_at: now,
1290
+ updated_at: now,
1291
+ last_accessed_at: null,
1292
+ access_count: 0,
1293
+ deleted_at: null,
1294
+ okf_type: normalized.okf_type
1295
+ };
1296
+ wikiFacts.push(wikiFact);
1297
+ }
1298
+ const sourceIdToType = /* @__PURE__ */ new Map();
1299
+ for (const fact of wikiFacts) sourceIdToType.set(fact.id, fact.okf_type ?? null);
1300
+ const hasConstraints = (manifest.node_types?.length ?? 0) > 0 || (manifest.edge_types?.length ?? 0) > 0;
1301
+ const validEdges = [];
1302
+ for (const edge of params.edges) {
1303
+ if (!hasConstraints) {
1304
+ validEdges.push({
1305
+ id: edge.id ?? generateId(),
1306
+ entity_id: entityId,
1307
+ source_id: edge.sourceId,
1308
+ target_id: edge.targetId,
1309
+ edge_type: edge.type,
1310
+ created_at: now
1311
+ });
1312
+ continue;
1313
+ }
1314
+ if (!sourceIdToType.has(edge.sourceId)) {
1315
+ validEdges.push({
1316
+ id: edge.id ?? generateId(),
1317
+ entity_id: entityId,
1318
+ source_id: edge.sourceId,
1319
+ target_id: edge.targetId,
1320
+ edge_type: edge.type,
1321
+ created_at: now
1322
+ });
1323
+ continue;
1324
+ }
1325
+ const sourceType = sourceIdToType.get(edge.sourceId);
1326
+ const candidates = (manifest.edge_types ?? []).filter(
1327
+ (d) => d.type.toLowerCase() === edge.type.toLowerCase() && d.source_type.toLowerCase() === (sourceType ?? "").toLowerCase()
1328
+ );
1329
+ const match = candidates[0];
1330
+ if (!match) {
1331
+ if (strictEffective) throw new WikiStrictOntologyViolation(entityId, "edge", edge.type);
1332
+ continue;
1333
+ }
1334
+ validEdges.push({
1335
+ id: edge.id ?? generateId(),
1336
+ entity_id: entityId,
1337
+ source_id: edge.sourceId,
1338
+ target_id: edge.targetId,
1339
+ edge_type: match.type,
1340
+ created_at: now
1341
+ });
1342
+ }
1343
+ const deletedFactIds = await this.entryRepo.findIdsBySource(entityId, params.sourceRef, null, tx, false);
1344
+ await this.entryRepo.softDeleteBySource(entityId, tx, params.sourceRef, null);
1345
+ const deletedEdgeCount = await this.edgeRepo.softDeleteBySourceFactIds(entityId, deletedFactIds, tx);
1346
+ await this.sourceRefIndexRepo.softDeleteByEntityAndSourceRef(entityId, params.sourceRef, tx);
1347
+ await this.sourceRefIndexRepo.upsert(entityId, params.sourceHash, params.sourceRef, tx);
1348
+ for (const wikiFact of wikiFacts) {
1349
+ await this.entryRepo.upsert(wikiFact, tx);
1350
+ }
1351
+ let edgesInsertedCount = 0;
1352
+ for (const edge of validEdges) {
1353
+ const inserted = await this.edgeRepo.addIgnoreDuplicate(edge, tx);
1354
+ if (inserted) edgesInsertedCount++;
1355
+ }
1356
+ return {
1357
+ nodesWritten: wikiFacts.length,
1358
+ edgesWritten: edgesInsertedCount,
1359
+ superseded: deletedFactIds.length + deletedEdgeCount
1360
+ };
1361
+ }
1224
1362
  };
1225
1363
 
1226
1364
  // src/utils/embedding.ts
@@ -1697,7 +1835,7 @@ var MaintenanceService = class {
1697
1835
  }
1698
1836
  if (skip) continue;
1699
1837
  const ontologyFact = fact;
1700
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
1838
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
1701
1839
  const id = generateId("fact_");
1702
1840
  const factObj = {
1703
1841
  id,
@@ -2082,7 +2220,8 @@ var MaintenanceService = class {
2082
2220
  okf_type: classification.okf_type,
2083
2221
  edges: classification.edges
2084
2222
  },
2085
- manifest
2223
+ manifest,
2224
+ { strict: false }
2086
2225
  );
2087
2226
  if (!normalized.okf_type) {
2088
2227
  failedValidation++;