@equationalapplications/core-llm-wiki 7.2.0 → 7.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.
package/dist/testing.js CHANGED
@@ -621,6 +621,17 @@ function validateTask(task) {
621
621
  priority
622
622
  };
623
623
  }
624
+ function factRejectionReason(raw) {
625
+ const fact = raw;
626
+ if (typeof fact?.title !== "string" || typeof fact?.body !== "string") return "invalid_shape";
627
+ if (!clip(fact.title, 80)) return "missing_title";
628
+ return "missing_body";
629
+ }
630
+ function taskRejectionReason(raw) {
631
+ const task = raw;
632
+ if (typeof task?.description !== "string") return "invalid_shape";
633
+ return "missing_description";
634
+ }
624
635
  function normalizeSourceRef(value) {
625
636
  if (typeof value !== "string") return null;
626
637
  const cleaned = value.replace(/[^A-Za-z0-9._\- ]/g, "").trim().slice(0, 255);
@@ -644,6 +655,104 @@ function jaccardScore(a, b) {
644
655
  var DEFAULT_MAX_EMBED_CHARS = 6e3;
645
656
  var EMBED_CHARS_CEILING = 16e3;
646
657
 
658
+ // src/utils/diagnostics.ts
659
+ var SEVERITY = {
660
+ ingest_chunk_failed: "warn",
661
+ fact_rejected: "warn",
662
+ task_rejected: "warn",
663
+ fact_deduplicated: "info",
664
+ edge_dropped: "warn",
665
+ embedding_failed: "warn",
666
+ hook_failed: "warn",
667
+ background_job_failed: "error",
668
+ heal_skipped: "warn",
669
+ grounding_missing: "warn",
670
+ grounding_failed: "warn",
671
+ classification_low_confidence: "info",
672
+ classification_invalid: "warn"
673
+ };
674
+ var MESSAGE = {
675
+ ingest_chunk_failed: "One or more document chunks failed to parse or generate.",
676
+ fact_rejected: "An extracted fact failed validation and was not written.",
677
+ task_rejected: "An extracted task failed validation and was not written.",
678
+ fact_deduplicated: "An extracted fact duplicated an existing fact and was not written.",
679
+ edge_dropped: "An extracted edge could not be resolved against the ontology and was not written.",
680
+ embedding_failed: "A fact could not be embedded.",
681
+ hook_failed: "A host hook threw or rejected.",
682
+ background_job_failed: "A background maintenance job failed.",
683
+ heal_skipped: "Heal skipped a candidate fact.",
684
+ grounding_missing: "A fact carried no qualifying evidence and was stored as a draft.",
685
+ grounding_failed: "A fact carried evidence not found in its source and was stored as a draft.",
686
+ classification_low_confidence: "A classifier answer was below the confidence threshold and was not applied.",
687
+ classification_invalid: "A classifier answer was invalid and was not applied."
688
+ };
689
+ var warnedNonFunction = /* @__PURE__ */ new WeakSet();
690
+ function copyDetail(detail) {
691
+ const copy = { ...detail };
692
+ if (detail.chunkIndexes) copy.chunkIndexes = detail.chunkIndexes.slice();
693
+ return copy;
694
+ }
695
+ function emitDiagnostic(options, input) {
696
+ const hook = options.onDiagnostic;
697
+ if (hook === void 0 || hook === null) return;
698
+ if (typeof hook !== "function") {
699
+ if (!warnedNonFunction.has(options)) {
700
+ warnedNonFunction.add(options);
701
+ console.warn("[WikiMemory] onDiagnostic is not a function; diagnostics are disabled.");
702
+ }
703
+ return;
704
+ }
705
+ const diagnostic = {
706
+ code: input.code,
707
+ severity: SEVERITY[input.code],
708
+ operation: input.operation,
709
+ trigger: input.trigger,
710
+ entityId: input.entityId,
711
+ at: Date.now(),
712
+ message: MESSAGE[input.code],
713
+ ...input.detail ? { detail: copyDetail(input.detail) } : {}
714
+ };
715
+ try {
716
+ const returned = hook(diagnostic);
717
+ if (returned !== void 0) {
718
+ Promise.resolve(returned).catch((err) => {
719
+ console.warn("[WikiMemory] onDiagnostic hook rejected:", err);
720
+ });
721
+ }
722
+ } catch (err) {
723
+ console.warn("[WikiMemory] onDiagnostic hook threw:", err);
724
+ }
725
+ }
726
+ var DiagnosticBuffer = class {
727
+ constructor() {
728
+ this.items = [];
729
+ }
730
+ push(input) {
731
+ this.items.push(input);
732
+ }
733
+ get size() {
734
+ return this.items.length;
735
+ }
736
+ flush(options) {
737
+ const pending = this.items;
738
+ this.items = [];
739
+ for (const input of pending) emitDiagnostic(options, input);
740
+ }
741
+ discard() {
742
+ this.items = [];
743
+ }
744
+ };
745
+ function edgeDropDiagnostic(drop, ctx) {
746
+ const detail = { reason: drop.reason };
747
+ const factId = ctx.factId ?? drop.sourceId;
748
+ if (factId) detail.factId = factId;
749
+ if (drop.edgeType) detail.edgeType = drop.edgeType;
750
+ if (drop.sourceNodeType) detail.sourceNodeType = drop.sourceNodeType;
751
+ if (drop.targetNodeType) detail.targetNodeType = drop.targetNodeType;
752
+ if (ctx.sourceRef) detail.sourceRef = ctx.sourceRef;
753
+ return { code: "edge_dropped", operation: ctx.operation, trigger: ctx.trigger, entityId: ctx.entityId, detail };
754
+ }
755
+
647
756
  // src/services/EmbeddingService.ts
648
757
  var EmbeddingService = class {
649
758
  constructor(db, options, entryRepo, metadataRepo) {
@@ -707,7 +816,7 @@ var EmbeddingService = class {
707
816
  });
708
817
  }
709
818
  }
710
- async tryEmbedFact(fact) {
819
+ async tryEmbedFact(fact, ctx) {
711
820
  const embedFn = this.options.llmProvider.embed;
712
821
  if (typeof embedFn !== "function") return { ok: false, kind: "no_provider" };
713
822
  let tagsStr;
@@ -729,6 +838,7 @@ var EmbeddingService = class {
729
838
  const vector = await embedFn(text);
730
839
  if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
731
840
  console.warn(`[WikiMemory] embedFact: embed() returned an invalid vector for ${fact.id}; skipping.`);
841
+ this.reportEmbed(ctx, fact, "embedding_failed", "invalid_vector");
732
842
  await this.markFailure(fact.id, "invalid_vector");
733
843
  return { ok: false, kind: "invalid_vector" };
734
844
  }
@@ -742,11 +852,13 @@ var EmbeddingService = class {
742
852
  }
743
853
  if (hasNonFinite) {
744
854
  console.warn(`[WikiMemory] embedFact: embed() returned values that overflow float32 for ${fact.id}; skipping.`);
855
+ this.reportEmbed(ctx, fact, "embedding_failed", "float32_overflow");
745
856
  await this.markFailure(fact.id, "float32_overflow");
746
857
  return { ok: false, kind: "float32_overflow" };
747
858
  }
748
859
  } catch (err) {
749
860
  console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
861
+ this.reportEmbed(ctx, fact, "embedding_failed", "embed_threw");
750
862
  await this.markFailure(fact.id, "provider_error");
751
863
  return { ok: false, kind: "provider_error" };
752
864
  }
@@ -756,15 +868,31 @@ var EmbeddingService = class {
756
868
  await this.entryRepo.updateEmbeddingBlob(fact.id, blob);
757
869
  } catch (err) {
758
870
  console.warn(`[WikiMemory] embedFact: persisting embedding failed for ${fact.id}:`, err);
871
+ this.reportEmbed(ctx, fact, "embedding_failed", "persist_failed");
759
872
  return { ok: false, kind: "storage_error" };
760
873
  }
761
874
  try {
762
875
  await this.notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
763
876
  } catch (hookErr) {
764
877
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
878
+ this.reportEmbed(ctx, fact, "hook_failed", "on_embedding_persisted");
765
879
  }
766
880
  return { ok: true, dimension: float32Vector.length };
767
881
  }
882
+ reportEmbed(ctx, fact, code, reason) {
883
+ if (!ctx) return;
884
+ emitDiagnostic(this.options, {
885
+ code,
886
+ operation: ctx.operation,
887
+ trigger: ctx.trigger,
888
+ entityId: fact.entity_id,
889
+ detail: { factId: fact.id, reason }
890
+ });
891
+ }
892
+ /** For callers that invoke `notifyEmbeddingPersisted` directly and catch its failure. */
893
+ reportHookFailed(ctx, entityId, factId) {
894
+ this.reportEmbed(ctx, { id: factId, entity_id: entityId }, "hook_failed", "on_embedding_persisted");
895
+ }
768
896
  /** Marker writes must never fail the caller. Only marker-eligible kinds reach here. */
769
897
  async markFailure(id, kind) {
770
898
  try {
@@ -778,8 +906,8 @@ var EmbeddingService = class {
778
906
  * ImportExportService branch on `if (!embedded)`; a discriminated result
779
907
  * object is always truthy and would silently disable that branch.
780
908
  */
781
- async embedFact(fact) {
782
- const result = await this.tryEmbedFact(fact);
909
+ async embedFact(fact, ctx) {
910
+ const result = await this.tryEmbedFact(fact, ctx);
783
911
  return result.ok;
784
912
  }
785
913
  async notifyEmbeddingPersisted(entityId, factId, vector) {
@@ -1192,7 +1320,7 @@ var ImportExportService = class {
1192
1320
  title: clipped?.title ?? fact.title,
1193
1321
  body: clipped?.body ?? fact.body,
1194
1322
  tags: Array.isArray(fact.tags) || typeof fact.tags === "string" ? fact.tags : []
1195
- });
1323
+ }, { operation: "importDump", trigger: "call" });
1196
1324
  if (!embedded) {
1197
1325
  await this.embeddingService.notifyEmbeddingPersisted(entityId, fact.id, null);
1198
1326
  }
@@ -1217,6 +1345,11 @@ var ImportExportService = class {
1217
1345
  `[WikiMemory] onEmbeddingPersisted hook failed for preserved-blob fact ${fact.id}:`,
1218
1346
  hookErr
1219
1347
  );
1348
+ this.embeddingService.reportHookFailed(
1349
+ { operation: "importDump", trigger: "call" },
1350
+ entityId,
1351
+ fact.id
1352
+ );
1220
1353
  }
1221
1354
  }
1222
1355
  }
@@ -1233,6 +1366,11 @@ var ImportExportService = class {
1233
1366
  `[WikiMemory] onEmbeddingPersisted(vector=null) hook failed for soft-deleted fact ${factId}:`,
1234
1367
  hookErr
1235
1368
  );
1369
+ this.embeddingService.reportHookFailed(
1370
+ { operation: "importDump", trigger: "call" },
1371
+ entityId,
1372
+ factId
1373
+ );
1236
1374
  }
1237
1375
  }
1238
1376
  }
@@ -1628,9 +1766,24 @@ var IngestionService = class {
1628
1766
  try {
1629
1767
  const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
1630
1768
  const result2 = parseJsonResponse(responseText);
1769
+ const rawFacts = Array.isArray(result2.facts) ? result2.facts : [];
1770
+ const facts = [];
1771
+ const itemIndexes = [];
1772
+ const rejected = [];
1773
+ rawFacts.forEach((raw, itemIndex) => {
1774
+ const valid = validateFact(raw);
1775
+ if (valid) {
1776
+ facts.push(valid);
1777
+ itemIndexes.push(itemIndex);
1778
+ } else {
1779
+ rejected.push({ itemIndex, reason: factRejectionReason(raw) });
1780
+ }
1781
+ });
1631
1782
  return {
1632
1783
  status: "ok",
1633
- facts: (Array.isArray(result2.facts) ? result2.facts : []).map(validateFact).filter((f) => f !== null),
1784
+ facts,
1785
+ itemIndexes,
1786
+ rejected,
1634
1787
  ontology_updates: result2.ontology_updates
1635
1788
  };
1636
1789
  } catch (e) {
@@ -1675,23 +1828,48 @@ var IngestionService = class {
1675
1828
  const failures = [];
1676
1829
  const seen = /* @__PURE__ */ new Set();
1677
1830
  const orderedChunkFacts = [];
1678
- for (const slot of chunkResults) {
1831
+ const diagBuffer = new DiagnosticBuffer();
1832
+ const diagBase = { entityId, operation: "ingest", trigger: "call" };
1833
+ for (const [chunkIndex, slot] of chunkResults.entries()) {
1679
1834
  if (slot.status === "failed") {
1680
1835
  failedChunks++;
1681
1836
  failures.push(slot.error);
1682
1837
  continue;
1683
1838
  }
1684
1839
  ingestedChunks++;
1840
+ for (const r of slot.rejected) {
1841
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { sourceRef, chunkIndex, itemIndex: r.itemIndex, reason: r.reason } });
1842
+ }
1685
1843
  const dedupedFacts = [];
1686
- for (const fact of slot.facts) {
1844
+ slot.facts.forEach((fact, k) => {
1687
1845
  const normalizedTitle = normalizeTitleKey(fact.title);
1688
1846
  if (!seen.has(normalizedTitle)) {
1689
1847
  seen.add(normalizedTitle);
1690
1848
  dedupedFacts.push(fact);
1849
+ } else {
1850
+ diagBuffer.push({
1851
+ ...diagBase,
1852
+ code: "fact_deduplicated",
1853
+ detail: { sourceRef, chunkIndex, itemIndex: slot.itemIndexes[k], reason: "exact_title" }
1854
+ });
1691
1855
  }
1692
- }
1856
+ });
1693
1857
  orderedChunkFacts.push({ facts: dedupedFacts, ontology_updates: slot.ontology_updates });
1694
1858
  }
1859
+ for (const reason of ["parse", "llm"]) {
1860
+ const ofReason = failures.filter((f) => f.source === reason);
1861
+ if (ofReason.length === 0) continue;
1862
+ diagBuffer.push({
1863
+ ...diagBase,
1864
+ code: "ingest_chunk_failed",
1865
+ detail: {
1866
+ sourceRef,
1867
+ reason,
1868
+ count: ofReason.length,
1869
+ chunkIndexes: ofReason.map((f) => f.chunkIndex).sort((a, b) => a - b).slice(0, 20)
1870
+ }
1871
+ });
1872
+ }
1695
1873
  if (failedChunks === chunks.length) {
1696
1874
  throw new WikiIngestEmptyError({
1697
1875
  parseFailures: failures,
@@ -1704,7 +1882,7 @@ var IngestionService = class {
1704
1882
  try {
1705
1883
  if (failedChunks === 0) {
1706
1884
  const fullResult = await this.db.withTransactionAsync(async (tx) => {
1707
- return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx);
1885
+ return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer);
1708
1886
  });
1709
1887
  deletedSourceFactIds.push(...fullResult.deletedSourceFactIds);
1710
1888
  insertedFacts.push(...fullResult.insertedFacts);
@@ -1712,7 +1890,7 @@ var IngestionService = class {
1712
1890
  const partialResult = await this.db.withTransactionAsync(async (tx) => {
1713
1891
  const flat = [];
1714
1892
  for (const slot of orderedChunkFacts) flat.push(...slot.facts);
1715
- return await this.appendPartialFacts(entityId, sourceRef, flat, tx);
1893
+ return await this.appendPartialFacts(entityId, sourceRef, flat, tx, diagBuffer);
1716
1894
  });
1717
1895
  insertedFacts.push(...partialResult.insertedDescriptors);
1718
1896
  }
@@ -1726,6 +1904,7 @@ var IngestionService = class {
1726
1904
  }
1727
1905
  return zeroChunkResult(canonical);
1728
1906
  }
1907
+ diagBuffer.flush(this.options);
1729
1908
  await this.searchService.sync(entityId);
1730
1909
  if (failedChunks === 0) {
1731
1910
  const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
@@ -1734,11 +1913,18 @@ var IngestionService = class {
1734
1913
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
1735
1914
  } catch (hookErr) {
1736
1915
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during ingest for ${factId}:`, hookErr);
1916
+ emitDiagnostic(this.options, {
1917
+ entityId,
1918
+ operation: "ingest",
1919
+ trigger: "call",
1920
+ code: "hook_failed",
1921
+ detail: { factId, reason: "on_embedding_persisted" }
1922
+ });
1737
1923
  }
1738
1924
  }
1739
1925
  }
1740
1926
  for (const fact of insertedFacts) {
1741
- await this.embeddingService.embedFact(fact);
1927
+ await this.embeddingService.embedFact(fact, { operation: "ingest", trigger: "call" });
1742
1928
  }
1743
1929
  this.searchService.evictCache(entityId);
1744
1930
  const result = {
@@ -1884,6 +2070,19 @@ var IngestionService = class {
1884
2070
  const match = candidates[0];
1885
2071
  if (!match) {
1886
2072
  if (strictEffective) throw new WikiStrictOntologyViolation(entityId, "edge", edge.type);
2073
+ opts?.diag?.buffer.push({
2074
+ entityId,
2075
+ operation: opts.diag.operation,
2076
+ trigger: "call",
2077
+ code: "edge_dropped",
2078
+ detail: {
2079
+ reason: "manifest_violation",
2080
+ factId: edge.sourceId,
2081
+ edgeType: edge.type,
2082
+ ...sourceType ? { sourceNodeType: sourceType } : {},
2083
+ sourceRef: params.sourceRef
2084
+ }
2085
+ });
1887
2086
  continue;
1888
2087
  }
1889
2088
  validEdges.push({
@@ -1924,7 +2123,7 @@ var IngestionService = class {
1924
2123
  * On the partial path the caller never invokes this method; the empty
1925
2124
  * arrays stay empty.
1926
2125
  */
1927
- async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx) {
2126
+ async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer) {
1928
2127
  const deletedSourceFactIds = [];
1929
2128
  const insertedFacts = [];
1930
2129
  deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
@@ -1949,8 +2148,12 @@ var IngestionService = class {
1949
2148
  }
1950
2149
  for (const fact of facts) {
1951
2150
  const ontologyFact = fact;
1952
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
1953
2151
  const id = generateId("fact_");
2152
+ const validationDrops = [];
2153
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
2154
+ for (const drop of validationDrops) {
2155
+ diagBuffer.push(edgeDropDiagnostic(drop, { entityId, operation: "ingest", trigger: "call", sourceRef, factId: id }));
2156
+ }
1954
2157
  hostNodes.push({
1955
2158
  id,
1956
2159
  type: ontologyFact.okf_type ?? "",
@@ -1975,6 +2178,7 @@ var IngestionService = class {
1975
2178
  }
1976
2179
  const hostEdges = [];
1977
2180
  for (const req of rawEdgeRequests) {
2181
+ const resolveDrops = [];
1978
2182
  const resolved = this.ontologyService?.resolveEdges(
1979
2183
  entityId,
1980
2184
  req.sourceId,
@@ -1982,8 +2186,12 @@ var IngestionService = class {
1982
2186
  req.edges,
1983
2187
  manifest,
1984
2188
  titleIndex,
1985
- now
2189
+ now,
2190
+ resolveDrops
1986
2191
  ) ?? [];
2192
+ for (const drop of resolveDrops) {
2193
+ diagBuffer.push(edgeDropDiagnostic(drop, { entityId, operation: "ingest", trigger: "call", sourceRef }));
2194
+ }
1987
2195
  for (const e of resolved) {
1988
2196
  hostEdges.push({ type: e.edge_type, sourceId: e.source_id, targetId: e.target_id });
1989
2197
  }
@@ -1992,7 +2200,7 @@ var IngestionService = class {
1992
2200
  entityId,
1993
2201
  { sourceRef, sourceHash, nodes: hostNodes, edges: hostEdges },
1994
2202
  tx,
1995
- { strict: false }
2203
+ { strict: false, diag: { buffer: diagBuffer, operation: "ingest" } }
1996
2204
  );
1997
2205
  return { deletedSourceFactIds, insertedFacts };
1998
2206
  }
@@ -2016,7 +2224,7 @@ var IngestionService = class {
2016
2224
  * Runs INSIDE the caller's `tx`. Does not open a nested transaction.
2017
2225
  * Returns `{ inserted, skippedDuplicate }` for observability.
2018
2226
  */
2019
- async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx) {
2227
+ async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx, diagBuffer) {
2020
2228
  const liveIds = await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false);
2021
2229
  const liveFacts = liveIds.length === 0 ? [] : await this.entryRepo.findByIds(liveIds, void 0, tx);
2022
2230
  const liveTitles = new Set(liveFacts.map((f) => normalizeTitleKey(f.title)));
@@ -2027,6 +2235,13 @@ var IngestionService = class {
2027
2235
  for (const fact of dedupedFacts) {
2028
2236
  const normalizedTitle = normalizeTitleKey(fact.title);
2029
2237
  if (liveTitles.has(normalizedTitle)) {
2238
+ diagBuffer.push({
2239
+ entityId,
2240
+ operation: "ingest",
2241
+ trigger: "call",
2242
+ code: "fact_deduplicated",
2243
+ detail: { sourceRef, reason: "exact_title" }
2244
+ });
2030
2245
  skippedDuplicate++;
2031
2246
  continue;
2032
2247
  }
@@ -2455,7 +2670,7 @@ var MaintenanceService = class {
2455
2670
  permanentlyFailed++;
2456
2671
  continue;
2457
2672
  }
2458
- const result = await this.embeddingService.tryEmbedFact(row);
2673
+ const result = await this.embeddingService.tryEmbedFact(row, { operation: "reembed", trigger: "call" });
2459
2674
  if (result.ok) embedded++;
2460
2675
  else failed++;
2461
2676
  }
@@ -2576,7 +2791,7 @@ var MaintenanceService = class {
2576
2791
  return { deleted: { entries, tasks: 0 } };
2577
2792
  }
2578
2793
  /** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
2579
- async doRunLibrarian(entityId, promptOverride) {
2794
+ async doRunLibrarian(entityId, promptOverride, trigger = "call") {
2580
2795
  const events = await this.eventRepo.getRecent(entityId, 50);
2581
2796
  const currentFactsRows = await this.entryRepo.findRecentByEntityId(entityId, 100);
2582
2797
  const currentFacts = currentFactsRows.map((f) => {
@@ -2598,8 +2813,25 @@ var MaintenanceService = class {
2598
2813
  const facts = Array.isArray(result.facts) ? result.facts : [];
2599
2814
  const tasks = Array.isArray(result.tasks) ? result.tasks : [];
2600
2815
  const ontologyUpdates = result.ontology_updates;
2601
- const validFacts = facts.map(validateFact).filter((f) => f !== null);
2602
- const validTasks = tasks.map(validateTask).filter((t) => t !== null);
2816
+ const diagBuffer = new DiagnosticBuffer();
2817
+ const diagBase = { entityId, operation: "librarian", trigger };
2818
+ const validFacts = [];
2819
+ const validFactItemIndexes = [];
2820
+ facts.forEach((raw, itemIndex) => {
2821
+ const valid = validateFact(raw);
2822
+ if (valid) {
2823
+ validFacts.push(valid);
2824
+ validFactItemIndexes.push(itemIndex);
2825
+ } else {
2826
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
2827
+ }
2828
+ });
2829
+ const validTasks = [];
2830
+ tasks.forEach((raw, itemIndex) => {
2831
+ const valid = validateTask(raw);
2832
+ if (valid) validTasks.push(valid);
2833
+ else diagBuffer.push({ ...diagBase, code: "task_rejected", detail: { itemIndex, reason: taskRejectionReason(raw) } });
2834
+ });
2603
2835
  const now = Date.now();
2604
2836
  const insertedFacts = [];
2605
2837
  await this.db.withTransactionAsync(async (tx) => {
@@ -2616,7 +2848,7 @@ var MaintenanceService = class {
2616
2848
  }
2617
2849
  const factsForDedupe = await this.entryRepo.findRecentByEntityId(entityId, 100, tx);
2618
2850
  const pendingEdges = [];
2619
- for (const fact of validFacts) {
2851
+ for (const [k, fact] of validFacts.entries()) {
2620
2852
  const newTokens = titleTokens(fact.title);
2621
2853
  let skip = false;
2622
2854
  if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
@@ -2631,10 +2863,15 @@ var MaintenanceService = class {
2631
2863
  }
2632
2864
  }
2633
2865
  }
2634
- if (skip) continue;
2866
+ if (skip) {
2867
+ diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validFactItemIndexes[k], reason: "fuzzy_title" } });
2868
+ continue;
2869
+ }
2635
2870
  const ontologyFact = fact;
2636
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
2637
2871
  const id = generateId("fact_");
2872
+ const validationDrops = [];
2873
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
2874
+ for (const drop of validationDrops) diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: id }));
2638
2875
  const factObj = {
2639
2876
  id,
2640
2877
  entity_id: entityId,
@@ -2661,6 +2898,7 @@ var MaintenanceService = class {
2661
2898
  }
2662
2899
  }
2663
2900
  for (const item of pendingEdges) {
2901
+ const resolveDrops = [];
2664
2902
  await this.ontologyService?.resolveAndPersistEdges(
2665
2903
  entityId,
2666
2904
  item.sourceId,
@@ -2669,8 +2907,10 @@ var MaintenanceService = class {
2669
2907
  manifest,
2670
2908
  titleIndex,
2671
2909
  tx,
2672
- now
2910
+ now,
2911
+ resolveDrops
2673
2912
  );
2913
+ for (const drop of resolveDrops) diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
2674
2914
  }
2675
2915
  for (const task of validTasks) {
2676
2916
  const id = generateId("task_");
@@ -2688,9 +2928,10 @@ var MaintenanceService = class {
2688
2928
  await this.taskRepo.upsert(taskObj, tx);
2689
2929
  }
2690
2930
  });
2931
+ diagBuffer.flush(this.options);
2691
2932
  await this.searchService.sync(entityId);
2692
2933
  for (const fact of insertedFacts) {
2693
- await this.embeddingService.embedFact(fact);
2934
+ await this.embeddingService.embedFact(fact, { operation: "librarian", trigger });
2694
2935
  }
2695
2936
  this.searchService.evictCache(entityId);
2696
2937
  }
@@ -2704,6 +2945,8 @@ var MaintenanceService = class {
2704
2945
  async doRunHeal(entityId, options) {
2705
2946
  const promptOverride = options?.promptOverride;
2706
2947
  const batchSize = options?.batchSize ?? HEAL_BATCH_SIZE;
2948
+ const trigger = options?.trigger ?? "call";
2949
+ const diagBase = { entityId, operation: "heal", trigger };
2707
2950
  if (!Number.isInteger(batchSize) || batchSize < 1) {
2708
2951
  throw new Error("Invalid batchSize: must be an integer >= 1");
2709
2952
  }
@@ -2739,6 +2982,7 @@ var MaintenanceService = class {
2739
2982
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
2740
2983
  } catch (hookErr) {
2741
2984
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal orphan pass for ${factId}:`, hookErr);
2985
+ emitDiagnostic(this.options, { ...diagBase, code: "hook_failed", detail: { factId, reason: "on_embedding_persisted" } });
2742
2986
  }
2743
2987
  }
2744
2988
  const healCandidates = await this.entryRepo.findHealCandidatesByEntityId(entityId, batchSize, recheckCutoff);
@@ -2811,22 +3055,38 @@ var MaintenanceService = class {
2811
3055
  const safeDowngradedSet = /* @__PURE__ */ new Set();
2812
3056
  const safeDeletedSet = /* @__PURE__ */ new Set();
2813
3057
  const newFacts = [];
3058
+ const newFactItemIndexes = [];
2814
3059
  for (const batchResult of outcome.results) {
2815
3060
  const mutableIds = new Set(batchResult.batch.map((f) => f.id));
2816
3061
  for (const id of batchResult.downgraded) if (mutableIds.has(id)) safeDowngradedSet.add(id);
2817
3062
  for (const id of batchResult.deleted) if (mutableIds.has(id)) safeDeletedSet.add(id);
2818
- newFacts.push(...batchResult.newFacts);
3063
+ batchResult.newFacts.forEach((raw, itemIndex) => {
3064
+ newFacts.push(raw);
3065
+ newFactItemIndexes.push(itemIndex);
3066
+ });
2819
3067
  }
2820
3068
  const safeDowngraded = Array.from(safeDowngradedSet);
2821
3069
  const safeDeleted = Array.from(safeDeletedSet);
2822
- const validNewFacts = newFacts.map(validateFact).filter((f) => f !== null);
3070
+ const diagBuffer = new DiagnosticBuffer();
3071
+ const validNewFacts = [];
3072
+ const validNewFactItemIndexes = [];
3073
+ newFacts.forEach((raw, k) => {
3074
+ const itemIndex = newFactItemIndexes[k];
3075
+ const valid = validateFact(raw);
3076
+ if (valid) {
3077
+ validNewFacts.push(valid);
3078
+ validNewFactItemIndexes.push(itemIndex);
3079
+ } else {
3080
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
3081
+ }
3082
+ });
2823
3083
  const insertedFacts = [];
2824
3084
  const uniqueDeletedFactIds = Array.from(new Set(safeDeleted));
2825
3085
  await this.db.withTransactionAsync(async (tx) => {
2826
3086
  await this.entryRepo.downgradeByIds(safeDowngraded, entityId, tx);
2827
3087
  await this.entryRepo.softDeleteByIds(safeDeleted, entityId, tx);
2828
3088
  const healFactsForDedupe = await this.entryRepo.findInferredTitlesByEntityId(entityId, tx);
2829
- for (const fact of validNewFacts) {
3089
+ for (const [k, fact] of validNewFacts.entries()) {
2830
3090
  const newTokens = titleTokens(fact.title);
2831
3091
  let skip = false;
2832
3092
  if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
@@ -2840,7 +3100,10 @@ var MaintenanceService = class {
2840
3100
  }
2841
3101
  }
2842
3102
  }
2843
- if (skip) continue;
3103
+ if (skip) {
3104
+ diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validNewFactItemIndexes[k], reason: "fuzzy_title" } });
3105
+ continue;
3106
+ }
2844
3107
  const id = generateId("fact_");
2845
3108
  const factObj = {
2846
3109
  id,
@@ -2875,16 +3138,21 @@ var MaintenanceService = class {
2875
3138
  tx
2876
3139
  );
2877
3140
  });
3141
+ for (const { item, reason } of outcome.skipped) {
3142
+ diagBuffer.push({ ...diagBase, code: "heal_skipped", detail: { factId: item.id, reason } });
3143
+ }
3144
+ diagBuffer.flush(this.options);
2878
3145
  await this.searchService.sync(entityId);
2879
3146
  for (const factId of uniqueDeletedFactIds) {
2880
3147
  try {
2881
3148
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
2882
3149
  } catch (hookErr) {
2883
3150
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal for ${factId}:`, hookErr);
3151
+ emitDiagnostic(this.options, { ...diagBase, code: "hook_failed", detail: { factId, reason: "on_embedding_persisted" } });
2884
3152
  }
2885
3153
  }
2886
3154
  for (const fact of insertedFacts) {
2887
- await this.embeddingService.embedFact(fact);
3155
+ await this.embeddingService.embedFact(fact, { operation: "heal", trigger });
2888
3156
  }
2889
3157
  this.searchService.evictCache(entityId);
2890
3158
  const skippedIds = new Set(outcome.skipped.map(({ item }) => item.id));
@@ -3024,6 +3292,8 @@ var MaintenanceService = class {
3024
3292
  let failedValidation = 0;
3025
3293
  let edgesAdded = 0;
3026
3294
  let abortedOntologyOff = false;
3295
+ const diagBuffer = new DiagnosticBuffer();
3296
+ const diagBase = { entityId, operation: "ontologyBackfill", trigger: "call" };
3027
3297
  await this.db.withTransactionAsync(async (tx) => {
3028
3298
  let { mode: txMode, manifest } = await ontologyService.getEffectiveState(entityId, tx);
3029
3299
  if (txMode === "off") {
@@ -3048,6 +3318,7 @@ var MaintenanceService = class {
3048
3318
  continue;
3049
3319
  }
3050
3320
  applied.add(fact.id);
3321
+ const validationDrops = [];
3051
3322
  const normalized = ontologyService.validateAndNormalizeFact(
3052
3323
  {
3053
3324
  title: fact.title,
@@ -3058,8 +3329,11 @@ var MaintenanceService = class {
3058
3329
  edges: classification.edges
3059
3330
  },
3060
3331
  manifest,
3061
- { strict: false }
3332
+ { strict: false, drops: validationDrops }
3062
3333
  );
3334
+ for (const drop of validationDrops) {
3335
+ diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: fact.id }));
3336
+ }
3063
3337
  if (!normalized.okf_type) {
3064
3338
  failedValidation++;
3065
3339
  continue;
@@ -3073,7 +3347,8 @@ var MaintenanceService = class {
3073
3347
  }
3074
3348
  }
3075
3349
  for (const item of pendingEdges) {
3076
- edgesAdded += await ontologyService.resolveAndPersistEdges(
3350
+ const resolveDrops = [];
3351
+ const added = await ontologyService.resolveAndPersistEdges(
3077
3352
  entityId,
3078
3353
  item.sourceId,
3079
3354
  item.sourceType,
@@ -3081,11 +3356,17 @@ var MaintenanceService = class {
3081
3356
  manifest,
3082
3357
  titleIndex,
3083
3358
  tx,
3084
- now
3359
+ now,
3360
+ resolveDrops
3085
3361
  );
3362
+ edgesAdded += added;
3363
+ for (const drop of resolveDrops) {
3364
+ diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
3365
+ }
3086
3366
  }
3087
3367
  await this.entryRepo.markOntologyChecked(batch.map((f) => f.id), entityId, now, tx);
3088
3368
  });
3369
+ diagBuffer.flush(this.options);
3089
3370
  return { typed, failedValidation, edgesAdded, abortedOntologyOff };
3090
3371
  }
3091
3372
  _validatePruneDuration(value, name) {
@@ -4569,7 +4850,11 @@ var WriteService = class {
4569
4850
  if (shouldRunLibrarian) {
4570
4851
  try {
4571
4852
  this.jobManager.acquireLock("librarian", entityId);
4572
- this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint).catch(console.error).finally(() => {
4853
+ const stage = { job: "librarian" };
4854
+ this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint, stage).catch((err) => {
4855
+ console.error(err);
4856
+ this.reportBackgroundFailure(entityId, stage.job);
4857
+ }).finally(() => {
4573
4858
  this.jobManager.releaseLock("librarian", entityId);
4574
4859
  });
4575
4860
  } catch (e) {
@@ -4577,17 +4862,21 @@ var WriteService = class {
4577
4862
  await this.metadataRepo.updateCheckpoint(entityId, { memory: prevMemoryCheckpoint }, this.db);
4578
4863
  }
4579
4864
  } else if (!this.jobManager.isBlocked("librarian", entityId)) {
4580
- this.maybeRunHeal(entityId, eventCount).catch(console.error);
4865
+ this.maybeRunHeal(entityId, eventCount).catch((err) => {
4866
+ console.error(err);
4867
+ this.reportBackgroundFailure(entityId, "heal");
4868
+ });
4581
4869
  }
4582
4870
  }
4583
- async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint) {
4871
+ async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint, stage) {
4584
4872
  try {
4585
- await this.maintenanceService.doRunLibrarian(entityId);
4873
+ await this.maintenanceService.doRunLibrarian(entityId, void 0, "auto");
4586
4874
  await this.metadataRepo.updateCheckpoint(entityId, { memory: currentEventCount }, this.db);
4587
4875
  } catch (e) {
4588
4876
  await this.metadataRepo.updateCheckpoint(entityId, { memory: prevCheckpoint }, this.db);
4589
4877
  throw e;
4590
4878
  }
4879
+ stage.job = "heal";
4591
4880
  await this.maybeRunHeal(entityId, currentEventCount);
4592
4881
  }
4593
4882
  /**
@@ -4603,7 +4892,7 @@ var WriteService = class {
4603
4892
  const shouldRunHeal = currentEventCount - healCheckpoint >= autoHealThreshold;
4604
4893
  if (shouldRunHeal && this.jobManager.tryAcquireAutoHealLock(entityId)) {
4605
4894
  try {
4606
- const result = await this.maintenanceService.doRunHeal(entityId);
4895
+ const result = await this.maintenanceService.doRunHeal(entityId, { trigger: "auto" });
4607
4896
  if (result.remaining === 0) {
4608
4897
  await this.metadataRepo.updateCheckpoint(entityId, { heal: currentEventCount }, this.db);
4609
4898
  }
@@ -4612,6 +4901,15 @@ var WriteService = class {
4612
4901
  }
4613
4902
  }
4614
4903
  }
4904
+ reportBackgroundFailure(entityId, job) {
4905
+ emitDiagnostic(this.options, {
4906
+ code: "background_job_failed",
4907
+ operation: job,
4908
+ trigger: "auto",
4909
+ entityId,
4910
+ detail: { reason: "unhandled_rejection" }
4911
+ });
4912
+ }
4615
4913
  };
4616
4914
 
4617
4915
  exports.EmbeddingService = EmbeddingService;