@equationalapplications/core-llm-wiki 7.5.0 → 7.7.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.
@@ -233,6 +233,10 @@ function typeSatisfies(declaredType, concreteType, manifest) {
233
233
  const parent = typeof def?.parent_type === "string" ? def.parent_type.trim().toLowerCase() : "";
234
234
  return parent !== "" && parent === declared;
235
235
  }
236
+ function edgeTripleAllowed(manifest, edgeType, sourceType, targetType) {
237
+ const wanted = edgeType.trim().toLowerCase();
238
+ return (manifest.edge_types ?? []).some((d) => typeof d?.type === "string" && d.type.trim().toLowerCase() === wanted && typeof d.source_type === "string" && typeSatisfies(d.source_type, sourceType, manifest) && typeof d.target_type === "string" && typeSatisfies(d.target_type, targetType, manifest));
239
+ }
236
240
  function validateManifest(manifest) {
237
241
  const nodeSlugs = /* @__PURE__ */ new Set();
238
242
  for (const node of manifest.node_types ?? []) {
@@ -1513,6 +1517,19 @@ function validateTags(tags) {
1513
1517
  if (!Array.isArray(tags)) return [];
1514
1518
  return tags.filter((t) => typeof t === "string").map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0 && t.length <= 40).slice(0, 6);
1515
1519
  }
1520
+ var MAX_EVIDENCE_QUOTES = 10;
1521
+ function normalizeEvidence(raw) {
1522
+ if (!Array.isArray(raw)) return void 0;
1523
+ const out = [];
1524
+ for (const entry of raw) {
1525
+ if (typeof entry !== "string") continue;
1526
+ const trimmed = entry.trim();
1527
+ if (!trimmed) continue;
1528
+ out.push(trimmed);
1529
+ if (out.length > MAX_EVIDENCE_QUOTES) break;
1530
+ }
1531
+ return out;
1532
+ }
1516
1533
  function validateFact(fact) {
1517
1534
  if (typeof fact?.title !== "string" || typeof fact?.body !== "string") return null;
1518
1535
  const title = clip(fact.title, 80);
@@ -1520,13 +1537,17 @@ function validateFact(fact) {
1520
1537
  if (!title || !body) return null;
1521
1538
  let confidence = fact.confidence;
1522
1539
  if (confidence !== "certain" && confidence !== "tentative") confidence = "inferred";
1523
- return {
1540
+ const valid = {
1524
1541
  ...fact,
1525
1542
  title,
1526
1543
  body,
1527
1544
  confidence,
1528
1545
  tags: validateTags(fact.tags)
1529
1546
  };
1547
+ const evidence = normalizeEvidence(fact.evidence);
1548
+ if (evidence) valid.evidence = evidence;
1549
+ else delete valid.evidence;
1550
+ return valid;
1530
1551
  }
1531
1552
  function validateTask(task) {
1532
1553
  if (typeof task?.description !== "string") return null;
@@ -1602,17 +1623,94 @@ Return ONLY a valid JSON object matching this schema:
1602
1623
  }
1603
1624
  If no manifest type fits a fact, omit that fact from "classifications" entirely \u2014 do not guess.
1604
1625
  When echoing an existing fact's title verbatim into "target_title", preserve every JSON escape sequence (\\", \\n, \\\\, \\/) exactly as it appeared in the input body \u2014 do not strip backslashes, do not add unescaped quotes. Do not return markdown, just raw JSON.`;
1626
+ var GROUNDING_SOURCE = {
1627
+ ingest: { key: "facts", source: "the document chunk" },
1628
+ librarian: { key: "facts", source: 'the "summary" text of the events' },
1629
+ heal: { key: "newFacts", source: 'the "summary" text of the recent events or the "body" text of the document anchors' }
1630
+ };
1631
+ function groundingEvidenceBlock(writer, cfg) {
1632
+ const { key, source } = GROUNDING_SOURCE[writer];
1633
+ return `EVIDENCE REQUIREMENT: every object in "${key}" must also carry an "evidence" array of 1 to ${cfg.maxEvidence} quotes. Each quote must be an exact substring copied character-for-character from ${source} (the SOURCE section), at least ${cfg.minEvidenceChars} characters long. Do not paraphrase. Do not quote these instructions, the ontology manifest, or any existing fact. A fact whose quotes cannot be found in the SOURCE section is stored as an unreviewed draft.
1634
+ "evidence": ["exact substring copied from the SOURCE section"]`;
1635
+ }
1605
1636
 
1606
1637
  // src/utils/healConstants.ts
1607
1638
  var HEAL_MAX_ANCHORS = 50;
1608
1639
  var HEAL_ANCHORS_PER_CANDIDATE = 4;
1609
1640
  var HEAL_MAX_FACT_BODY_CHARS_L3 = 4e3;
1610
1641
  var HEAL_MAX_TASKS = 50;
1642
+ var HEAL_ANCHOR_BODY_CHARS = 800;
1643
+
1644
+ // src/utils/grounding.ts
1645
+ var GROUNDING_VERIFIER = "process:grounding-check";
1646
+ var WRITERS = ["ingest", "librarian", "heal"];
1647
+ function positiveInt(value, fallback) {
1648
+ return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.floor(value) : fallback;
1649
+ }
1650
+ function resolveGrounding(config) {
1651
+ if (!config || config.mode !== "draft") return null;
1652
+ const writers = Array.isArray(config.writers) ? config.writers.filter((w) => WRITERS.includes(w)) : ["ingest"];
1653
+ return {
1654
+ writers: new Set(writers),
1655
+ minEvidenceChars: positiveInt(config.minEvidenceChars, 20),
1656
+ // Never ask for more quotes than checkGrounding accepts (MAX_EVIDENCE_QUOTES).
1657
+ maxEvidence: Math.min(MAX_EVIDENCE_QUOTES, positiveInt(config.maxEvidence, 3)),
1658
+ maxEvidenceChars: positiveInt(config.maxEvidenceChars, 300)
1659
+ };
1660
+ }
1661
+ function normalizeForGrounding(text) {
1662
+ return text.normalize("NFKC").replace(/\s+/g, " ").trim();
1663
+ }
1664
+ function buildGroundingCorpus(parts) {
1665
+ return parts.filter((p) => typeof p === "string").map(normalizeForGrounding);
1666
+ }
1667
+ function checkGrounding(evidence, normalizedCorpus, cfg) {
1668
+ const quotes = evidence ?? [];
1669
+ if (quotes.length > MAX_EVIDENCE_QUOTES) return { status: "failed", reason: "too_many_quotes" };
1670
+ if (quotes.length === 0) return { status: "missing", reason: "no_evidence" };
1671
+ const qualifying = quotes.map(normalizeForGrounding).filter((q) => q.length >= cfg.minEvidenceChars);
1672
+ if (qualifying.length === 0) return { status: "missing", reason: "evidence_too_short" };
1673
+ if (qualifying.some((q) => !normalizedCorpus.some((part) => part.includes(q)))) {
1674
+ return { status: "failed", reason: "quote_not_found" };
1675
+ }
1676
+ return {
1677
+ status: "grounded",
1678
+ retained: qualifying.slice(0, cfg.maxEvidence).map((q) => safeSlice(q, 0, cfg.maxEvidenceChars))
1679
+ };
1680
+ }
1681
+ function groundingOutcome(verdict, now) {
1682
+ if (verdict.status === "grounded") {
1683
+ return {
1684
+ trust: {
1685
+ lifecycle_status: "stable",
1686
+ okf_verified: [{ by: GROUNDING_VERIFIER, at: new Date(now).toISOString() }],
1687
+ last_verified_at: now,
1688
+ last_verified_by: GROUNDING_VERIFIER
1689
+ }
1690
+ };
1691
+ }
1692
+ return {
1693
+ trust: { lifecycle_status: "draft" },
1694
+ diagnostic: { code: verdict.status === "missing" ? "grounding_missing" : "grounding_failed", reason: verdict.reason }
1695
+ };
1696
+ }
1611
1697
 
1612
1698
  // src/services/PromptService.ts
1613
1699
  var PromptService = class {
1614
- constructor(globalOverrides) {
1700
+ constructor(globalOverrides, grounding = null) {
1615
1701
  this.globalOverrides = globalOverrides;
1702
+ this.grounding = grounding;
1703
+ }
1704
+ /** The resolved grounding config when `writer` is in `grounding.writers`; otherwise null (spec §6.2). */
1705
+ groundingFor(writer) {
1706
+ return this.grounding?.writers.has(writer) ? this.grounding : null;
1707
+ }
1708
+ /** Appended after any override and after ontology context, so it is always last. */
1709
+ appendGrounding(systemPrompt, writer) {
1710
+ const cfg = this.groundingFor(writer);
1711
+ return cfg ? `${systemPrompt}
1712
+
1713
+ ${groundingEvidenceBlock(writer, cfg)}` : systemPrompt;
1616
1714
  }
1617
1715
  hydrate(template, variables) {
1618
1716
  return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => {
@@ -1642,13 +1740,13 @@ ${ctx.ontologyModeInstructions}`;
1642
1740
  const hasDocumentChunk = /\{\{\s*documentChunk\s*\}\}/.test(template);
1643
1741
  if (hasDocumentChunk || this.hasOntologyPlaceholders(template)) {
1644
1742
  return {
1645
- systemPrompt: this.buildSystemPrompt(template, { documentChunk }, ontologyContext),
1743
+ systemPrompt: this.appendGrounding(this.buildSystemPrompt(template, { documentChunk }, ontologyContext), "ingest"),
1646
1744
  userPrompt: hasDocumentChunk ? "Please extract the facts." : `Document Chunk:
1647
1745
  ${documentChunk}`
1648
1746
  };
1649
1747
  }
1650
1748
  return {
1651
- systemPrompt: this.appendOntology(template, ontologyContext),
1749
+ systemPrompt: this.appendGrounding(this.appendOntology(template, ontologyContext), "ingest"),
1652
1750
  userPrompt: `Document Chunk:
1653
1751
  ${documentChunk}`
1654
1752
  };
@@ -1657,23 +1755,27 @@ ${documentChunk}`
1657
1755
  const template = runtimeOverride ?? this.globalOverrides?.librarianSystemPrompt ?? LIBRARIAN_SYSTEM_PROMPT;
1658
1756
  const hasEvents = /\{\{\s*events\s*\}\}/.test(template);
1659
1757
  const hasCurrentFacts = /\{\{\s*currentFacts\s*\}\}/.test(template);
1758
+ const eventsShown = hasEvents || !hasCurrentFacts;
1759
+ const corpusField = this.groundingFor("librarian") ? { groundingCorpus: buildGroundingCorpus(eventsShown ? events.map(summaryOf) : []) } : {};
1660
1760
  if (hasEvents || hasCurrentFacts || this.hasOntologyPlaceholders(template)) {
1661
1761
  return {
1662
- systemPrompt: this.buildSystemPrompt(template, { events, currentFacts }, ontologyContext),
1762
+ systemPrompt: this.appendGrounding(this.buildSystemPrompt(template, { events, currentFacts }, ontologyContext), "librarian"),
1663
1763
  userPrompt: hasEvents || hasCurrentFacts ? "Please synthesize the context." : `Events:
1664
1764
  ${JSON.stringify(events, null, 2)}
1665
1765
 
1666
1766
  Current Facts:
1667
- ${JSON.stringify(currentFacts, null, 2)}`
1767
+ ${JSON.stringify(currentFacts, null, 2)}`,
1768
+ ...corpusField
1668
1769
  };
1669
1770
  }
1670
1771
  return {
1671
- systemPrompt: this.appendOntology(template, ontologyContext),
1772
+ systemPrompt: this.appendGrounding(this.appendOntology(template, ontologyContext), "librarian"),
1672
1773
  userPrompt: `Events:
1673
1774
  ${JSON.stringify(events, null, 2)}
1674
1775
 
1675
1776
  Current Facts:
1676
- ${JSON.stringify(currentFacts, null, 2)}`
1777
+ ${JSON.stringify(currentFacts, null, 2)}`,
1778
+ ...corpusField
1677
1779
  };
1678
1780
  }
1679
1781
  /**
@@ -1703,40 +1805,54 @@ ${JSON.stringify(currentFacts, null, 2)}`
1703
1805
  const effectiveEvents = attemptLevel >= 2 ? [] : recentEvents;
1704
1806
  const maxAnchors = Math.max(1, Math.min(HEAL_MAX_ANCHORS, healCandidates.length * HEAL_ANCHORS_PER_CANDIDATE));
1705
1807
  const effectiveAnchors = documentAnchors.slice(0, maxAnchors);
1808
+ const template = runtimeOverride ?? this.globalOverrides?.healSystemPrompt ?? HEAL_SYSTEM_PROMPT;
1809
+ const hasRecentEvents = /\{\{\s*recentEvents\s*\}\}/.test(template);
1810
+ const hasDocumentAnchors = /\{\{\s*documentAnchors\s*\}\}/.test(template);
1811
+ const usesPlaceholders = /\{\{\s*healCandidates\s*\}\}/.test(template) || hasDocumentAnchors || /\{\{\s*allTasks\s*\}\}/.test(template) || hasRecentEvents;
1812
+ const healGrounding = this.groundingFor("heal");
1813
+ const promptAnchors = healGrounding ? effectiveAnchors.map(toGroundingAnchor) : effectiveAnchors;
1814
+ const eventsShown = !usesPlaceholders || hasRecentEvents;
1815
+ const anchorsShown = !usesPlaceholders || hasDocumentAnchors;
1816
+ const groundingCorpus = healGrounding ? buildGroundingCorpus([
1817
+ ...eventsShown ? effectiveEvents.map(summaryOf) : [],
1818
+ ...anchorsShown ? effectiveAnchors.map((a, i) => ({ status: a?.lifecycle_status, shown: promptAnchors[i] })).filter((x) => x.status !== "draft").map((x) => x.shown?.body) : []
1819
+ ]) : null;
1706
1820
  const { shapedCandidates, degraded } = applyBodyTruncation(
1707
1821
  healCandidates,
1708
1822
  attemptLevel,
1709
1823
  bodyTruncationChars
1710
1824
  );
1711
- const template = runtimeOverride ?? this.globalOverrides?.healSystemPrompt ?? HEAL_SYSTEM_PROMPT;
1712
- if (/\{\{\s*healCandidates\s*\}\}/.test(template) || /\{\{\s*documentAnchors\s*\}\}/.test(template) || /\{\{\s*allTasks\s*\}\}/.test(template) || /\{\{\s*recentEvents\s*\}\}/.test(template)) {
1825
+ const corpusField = groundingCorpus !== null ? { groundingCorpus } : {};
1826
+ if (usesPlaceholders) {
1713
1827
  return {
1714
1828
  prompts: {
1715
- systemPrompt: this.hydrate(template, {
1829
+ systemPrompt: this.appendGrounding(this.hydrate(template, {
1716
1830
  healCandidates: shapedCandidates,
1717
- documentAnchors: effectiveAnchors,
1831
+ documentAnchors: promptAnchors,
1718
1832
  allTasks: effectiveTasks,
1719
1833
  recentEvents: effectiveEvents
1720
- }),
1834
+ }), "heal"),
1721
1835
  userPrompt: "Please heal the memory graph."
1722
1836
  },
1723
- degraded
1837
+ degraded,
1838
+ ...corpusField
1724
1839
  };
1725
1840
  }
1726
1841
  return {
1727
1842
  prompts: {
1728
- systemPrompt: template,
1843
+ systemPrompt: this.appendGrounding(template, "heal"),
1729
1844
  userPrompt: `Heal Candidates:
1730
1845
  ${JSON.stringify(shapedCandidates, null, 2)}
1731
1846
  Document Anchors (DO NOT MODIFY OR DELETE):
1732
- ${JSON.stringify(effectiveAnchors, null, 2)}
1847
+ ${JSON.stringify(promptAnchors, null, 2)}
1733
1848
  All Tasks:
1734
1849
  ${JSON.stringify(effectiveTasks, null, 2)}
1735
1850
  Recent Events:
1736
1851
  ${JSON.stringify(effectiveEvents, null, 2)}
1737
1852
  The following document anchors are provided for contradiction detection only. Do not include them in \`downgraded\`, \`deleted\`, or \`newFacts\`.`
1738
1853
  },
1739
- degraded
1854
+ degraded,
1855
+ ...corpusField
1740
1856
  };
1741
1857
  }
1742
1858
  buildOntologyBackfillPrompt(facts, runtimeOverride, ontologyContext) {
@@ -1755,6 +1871,21 @@ ${JSON.stringify(facts, null, 2)}`
1755
1871
  ${JSON.stringify(facts, null, 2)}`
1756
1872
  };
1757
1873
  }
1874
+ /**
1875
+ * The system prompt each writer would send, without hydrating data (spec
1876
+ * §8.3). `buildSystemPrompt` with no variables hydrates only ontology
1877
+ * placeholders and leaves data placeholders verbatim; heal never receives
1878
+ * ontology context, matching `buildHealPrompt`.
1879
+ */
1880
+ buildInstructionTemplates(ontologyContext) {
1881
+ const o = this.globalOverrides;
1882
+ return {
1883
+ ingest: this.appendGrounding(this.buildSystemPrompt(o?.ingestSystemPrompt ?? INGEST_SYSTEM_PROMPT, {}, ontologyContext), "ingest"),
1884
+ librarian: this.appendGrounding(this.buildSystemPrompt(o?.librarianSystemPrompt ?? LIBRARIAN_SYSTEM_PROMPT, {}, ontologyContext), "librarian"),
1885
+ heal: this.appendGrounding(o?.healSystemPrompt ?? HEAL_SYSTEM_PROMPT, "heal"),
1886
+ ontologyBackfill: this.buildSystemPrompt(o?.ontologyBackfillSystemPrompt ?? ONTOLOGY_BACKFILL_SYSTEM_PROMPT, {}, ontologyContext)
1887
+ };
1888
+ }
1758
1889
  };
1759
1890
  function applyBodyTruncation(candidates, attemptLevel, bodyTruncationChars) {
1760
1891
  if (attemptLevel < 3) {
@@ -1786,6 +1917,19 @@ function applyBodyTruncation(candidates, attemptLevel, bodyTruncationChars) {
1786
1917
  }
1787
1918
  return { shapedCandidates, degraded };
1788
1919
  }
1920
+ function summaryOf(event) {
1921
+ return event?.summary;
1922
+ }
1923
+ function toGroundingAnchor(anchor) {
1924
+ if (typeof anchor !== "object" || anchor === null) return anchor;
1925
+ const a = anchor;
1926
+ return {
1927
+ id: a.id,
1928
+ title: a.title,
1929
+ source_ref: a.source_ref,
1930
+ body: typeof a.body === "string" ? safeSlice(a.body, 0, HEAL_ANCHOR_BODY_CHARS) : ""
1931
+ };
1932
+ }
1789
1933
 
1790
1934
  // src/utils/chunkingDefaults.ts
1791
1935
  var DEFAULT_MAX_CHUNK_LENGTH = 12e3;
@@ -1913,7 +2057,7 @@ var IngestionService = class {
1913
2057
  this.jobManager = jobManager;
1914
2058
  this.embeddingService = embeddingService;
1915
2059
  this.ontologyService = ontologyService;
1916
- this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
2060
+ this.promptService = promptService ?? new PromptService(this.options.config?.prompts, resolveGrounding(this.options.config?.grounding));
1917
2061
  }
1918
2062
  async ingestDocument(entityId, params, opts) {
1919
2063
  const sourceRef = normalizeSourceRef(params.sourceRef);
@@ -1946,6 +2090,7 @@ var IngestionService = class {
1946
2090
  const { chunks, truncated } = chunkText(params.documentChunk, maxChunkLength, chunkOverlap);
1947
2091
  if (chunks.length === 0) return zeroChunkResult();
1948
2092
  const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
2093
+ const ingestGrounding = this.promptService.groundingFor("ingest");
1949
2094
  const chunkResults = await withConcurrency(
1950
2095
  chunks.map((chunk, chunkIndex) => async () => {
1951
2096
  const { systemPrompt, userPrompt } = this.promptService.buildIngestPrompt(
@@ -1969,11 +2114,16 @@ var IngestionService = class {
1969
2114
  rejected.push({ itemIndex, reason: factRejectionReason(raw) });
1970
2115
  }
1971
2116
  });
2117
+ const verdicts = ingestGrounding ? (() => {
2118
+ const corpus = buildGroundingCorpus([chunk]);
2119
+ return facts.map((f) => checkGrounding(f.evidence, corpus, ingestGrounding));
2120
+ })() : [];
1972
2121
  return {
1973
2122
  status: "ok",
1974
2123
  facts,
1975
2124
  itemIndexes,
1976
2125
  rejected,
2126
+ verdicts,
1977
2127
  ontology_updates: result2.ontology_updates
1978
2128
  };
1979
2129
  } catch (e) {
@@ -2018,6 +2168,7 @@ var IngestionService = class {
2018
2168
  const failures = [];
2019
2169
  const seen = /* @__PURE__ */ new Set();
2020
2170
  const orderedChunkFacts = [];
2171
+ const groundingLedger = /* @__PURE__ */ new Map();
2021
2172
  const diagBuffer = new DiagnosticBuffer();
2022
2173
  const diagBase = { entityId, operation: "ingest", trigger: "call" };
2023
2174
  for (const [chunkIndex, slot] of chunkResults.entries()) {
@@ -2036,6 +2187,7 @@ var IngestionService = class {
2036
2187
  if (!seen.has(normalizedTitle)) {
2037
2188
  seen.add(normalizedTitle);
2038
2189
  dedupedFacts.push(fact);
2190
+ if (ingestGrounding) groundingLedger.set(fact, { verdict: slot.verdicts[k], chunkIndex, itemIndex: slot.itemIndexes[k] });
2039
2191
  } else {
2040
2192
  diagBuffer.push({
2041
2193
  ...diagBase,
@@ -2072,7 +2224,7 @@ var IngestionService = class {
2072
2224
  try {
2073
2225
  if (failedChunks === 0) {
2074
2226
  const fullResult = await this.db.withTransactionAsync(async (tx) => {
2075
- return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer);
2227
+ return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer, groundingLedger);
2076
2228
  });
2077
2229
  deletedSourceFactIds.push(...fullResult.deletedSourceFactIds);
2078
2230
  insertedFacts.push(...fullResult.insertedFacts);
@@ -2080,7 +2232,7 @@ var IngestionService = class {
2080
2232
  const partialResult = await this.db.withTransactionAsync(async (tx) => {
2081
2233
  const flat = [];
2082
2234
  for (const slot of orderedChunkFacts) flat.push(...slot.facts);
2083
- return await this.appendPartialFacts(entityId, sourceRef, flat, tx, diagBuffer);
2235
+ return await this.appendPartialFacts(entityId, sourceRef, flat, tx, diagBuffer, groundingLedger);
2084
2236
  });
2085
2237
  insertedFacts.push(...partialResult.insertedDescriptors);
2086
2238
  }
@@ -2222,7 +2374,8 @@ var IngestionService = class {
2222
2374
  last_accessed_at: null,
2223
2375
  access_count: 0,
2224
2376
  deleted_at: null,
2225
- okf_type: normalized.okf_type
2377
+ okf_type: normalized.okf_type,
2378
+ ...opts?.nodeTrust?.get(node.id)
2226
2379
  };
2227
2380
  wikiFacts.push(wikiFact);
2228
2381
  }
@@ -2313,9 +2466,10 @@ var IngestionService = class {
2313
2466
  * On the partial path the caller never invokes this method; the empty
2314
2467
  * arrays stay empty.
2315
2468
  */
2316
- async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer) {
2469
+ async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer, groundingLedger = /* @__PURE__ */ new Map()) {
2317
2470
  const deletedSourceFactIds = [];
2318
2471
  const insertedFacts = [];
2472
+ const nodeTrust = /* @__PURE__ */ new Map();
2319
2473
  deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
2320
2474
  const titleIndex = /* @__PURE__ */ new Map();
2321
2475
  const existingFacts = await this.entryRepo.findRecentByEntityId(entityId, 500, tx, sourceRef);
@@ -2359,6 +2513,20 @@ var IngestionService = class {
2359
2513
  tags: fact.tags,
2360
2514
  confidence: fact.confidence
2361
2515
  });
2516
+ const grounding = groundingLedger.get(fact);
2517
+ if (grounding) {
2518
+ const outcome = groundingOutcome(grounding.verdict, now);
2519
+ nodeTrust.set(id, outcome.trust);
2520
+ if (outcome.diagnostic) {
2521
+ diagBuffer.push({
2522
+ entityId,
2523
+ operation: "ingest",
2524
+ trigger: "call",
2525
+ code: outcome.diagnostic.code,
2526
+ detail: { factId: id, sourceRef, chunkIndex: grounding.chunkIndex, itemIndex: grounding.itemIndex, reason: outcome.diagnostic.reason }
2527
+ });
2528
+ }
2529
+ }
2362
2530
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
2363
2531
  titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
2364
2532
  if (normalized.edges.length > 0) {
@@ -2390,7 +2558,7 @@ var IngestionService = class {
2390
2558
  entityId,
2391
2559
  { sourceRef, sourceHash, nodes: hostNodes, edges: hostEdges },
2392
2560
  tx,
2393
- { strict: false, diag: { buffer: diagBuffer, operation: "ingest" } }
2561
+ { strict: false, diag: { buffer: diagBuffer, operation: "ingest" }, ...nodeTrust.size > 0 ? { nodeTrust } : {} }
2394
2562
  );
2395
2563
  return { deletedSourceFactIds, insertedFacts };
2396
2564
  }
@@ -2414,7 +2582,7 @@ var IngestionService = class {
2414
2582
  * Runs INSIDE the caller's `tx`. Does not open a nested transaction.
2415
2583
  * Returns `{ inserted, skippedDuplicate }` for observability.
2416
2584
  */
2417
- async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx, diagBuffer) {
2585
+ async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx, diagBuffer, groundingLedger = /* @__PURE__ */ new Map()) {
2418
2586
  const liveIds = await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false);
2419
2587
  const liveFacts = liveIds.length === 0 ? [] : await this.entryRepo.findByIds(liveIds, void 0, tx);
2420
2588
  const liveTitles = new Set(liveFacts.map((f) => normalizeTitleKey(f.title)));
@@ -2437,6 +2605,17 @@ var IngestionService = class {
2437
2605
  }
2438
2606
  liveTitles.add(normalizedTitle);
2439
2607
  const id = generateId("fact_");
2608
+ const grounding = groundingLedger.get(fact);
2609
+ const outcome = grounding ? groundingOutcome(grounding.verdict, now) : null;
2610
+ if (grounding && outcome?.diagnostic) {
2611
+ diagBuffer.push({
2612
+ entityId,
2613
+ operation: "ingest",
2614
+ trigger: "call",
2615
+ code: outcome.diagnostic.code,
2616
+ detail: { factId: id, sourceRef, chunkIndex: grounding.chunkIndex, itemIndex: grounding.itemIndex, reason: outcome.diagnostic.reason }
2617
+ });
2618
+ }
2440
2619
  const wikiFact = {
2441
2620
  id,
2442
2621
  entity_id: entityId,
@@ -2455,7 +2634,8 @@ var IngestionService = class {
2455
2634
  last_accessed_at: null,
2456
2635
  access_count: 0,
2457
2636
  deleted_at: null,
2458
- okf_type: null
2637
+ okf_type: null,
2638
+ ...outcome?.trust
2459
2639
  };
2460
2640
  await this.entryRepo.upsert(wikiFact, tx);
2461
2641
  insertedDescriptors.push({
@@ -2809,7 +2989,7 @@ async function runBatched(args) {
2809
2989
  }
2810
2990
  let result;
2811
2991
  try {
2812
- result = parse(responseText, batch);
2992
+ result = parse(responseText, batch, prompts);
2813
2993
  } catch (err) {
2814
2994
  await onFailure(batch, err, attemptLevel, false);
2815
2995
  return;
@@ -2890,7 +3070,7 @@ var MaintenanceService = class {
2890
3070
  this.jobManager = jobManager;
2891
3071
  this.embeddingService = embeddingService;
2892
3072
  this.ontologyService = ontologyService;
2893
- this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
3073
+ this.promptService = promptService ?? new PromptService(this.options.config?.prompts, resolveGrounding(this.options.config?.grounding));
2894
3074
  }
2895
3075
  async runPrune(entityId, options) {
2896
3076
  this.jobManager.acquireLock("prune", entityId);
@@ -3195,8 +3375,10 @@ var MaintenanceService = class {
3195
3375
  };
3196
3376
  });
3197
3377
  const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
3198
- const { systemPrompt, userPrompt } = this.promptService.buildLibrarianPrompt(
3199
- events.reverse(),
3378
+ const promptEvents = events.reverse();
3379
+ const librarianGrounding = this.promptService.groundingFor("librarian");
3380
+ const { systemPrompt, userPrompt, groundingCorpus: librarianCorpus = [] } = this.promptService.buildLibrarianPrompt(
3381
+ promptEvents,
3200
3382
  currentFacts,
3201
3383
  promptOverride,
3202
3384
  ontologyContext
@@ -3265,6 +3447,10 @@ var MaintenanceService = class {
3265
3447
  const validationDrops = [];
3266
3448
  const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
3267
3449
  for (const drop of validationDrops) diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: id }));
3450
+ const grounding = librarianGrounding ? groundingOutcome(checkGrounding(fact.evidence, librarianCorpus, librarianGrounding), now) : null;
3451
+ if (grounding?.diagnostic) {
3452
+ diagBuffer.push({ ...diagBase, code: grounding.diagnostic.code, detail: { factId: id, itemIndex: validFactItemIndexes[k], reason: grounding.diagnostic.reason } });
3453
+ }
3268
3454
  const factObj = {
3269
3455
  id,
3270
3456
  entity_id: entityId,
@@ -3280,7 +3466,8 @@ var MaintenanceService = class {
3280
3466
  last_accessed_at: null,
3281
3467
  access_count: 0,
3282
3468
  deleted_at: null,
3283
- okf_type: normalized.okf_type
3469
+ okf_type: normalized.okf_type,
3470
+ ...grounding?.trust
3284
3471
  };
3285
3472
  await this.entryRepo.upsert(factObj, tx);
3286
3473
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
@@ -3396,11 +3583,14 @@ var MaintenanceService = class {
3396
3583
  }
3397
3584
  const allTasks = await this.taskRepo.findAllPending([entityId], HEAL_MAX_TASKS);
3398
3585
  const recentEvents = await this.eventRepo.getRecent(entityId, 20);
3586
+ const healGrounding = this.promptService.groundingFor("heal");
3399
3587
  const toPromptShape = (f) => {
3400
3588
  const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
3401
- return { ...rest, tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags };
3589
+ const shown = healGrounding ? (({ lifecycle_status: _l, okf_verified: _v, last_verified_at: _a, last_verified_by: _b, ...r }) => r)(rest) : rest;
3590
+ return { ...shown, tags: typeof shown.tags === "string" ? JSON.parse(shown.tags) : shown.tags };
3402
3591
  };
3403
3592
  const anchorCache = /* @__PURE__ */ new Map();
3593
+ const corpusByPrompt = /* @__PURE__ */ new WeakMap();
3404
3594
  const degraded = [];
3405
3595
  const outcome = await runBatched({
3406
3596
  items: healCandidates,
@@ -3413,9 +3603,10 @@ var MaintenanceService = class {
3413
3603
  // not overfetch the same 200 keyword hits a 25-fact batch once did.
3414
3604
  // buildHealPrompt applies the matching cap on its side — values must match.
3415
3605
  Math.min(HEAL_MAX_ANCHORS, batch.length * HEAL_ANCHORS_PER_CANDIDATE),
3416
- anchorCache
3606
+ anchorCache,
3607
+ healGrounding !== null
3417
3608
  );
3418
- const { prompts, degraded: batchDegraded } = await this.promptService.buildHealPrompt(
3609
+ const { prompts, degraded: batchDegraded, groundingCorpus } = await this.promptService.buildHealPrompt(
3419
3610
  batch.map(toPromptShape),
3420
3611
  documentAnchors,
3421
3612
  allTasks,
@@ -3425,16 +3616,18 @@ var MaintenanceService = class {
3425
3616
  bodyTruncationChars
3426
3617
  );
3427
3618
  degraded.push(...batchDegraded);
3619
+ if (groundingCorpus !== void 0) corpusByPrompt.set(prompts, groundingCorpus);
3428
3620
  return prompts;
3429
3621
  },
3430
3622
  call: (prompts) => this.options.llmProvider.generateText(prompts),
3431
- parse: (responseText, batch) => {
3623
+ parse: (responseText, batch, prompts) => {
3432
3624
  const result = parseJsonResponse(responseText);
3433
3625
  return {
3434
3626
  batch,
3435
3627
  downgraded: Array.isArray(result.downgraded) ? result.downgraded : [],
3436
3628
  deleted: Array.isArray(result.deleted) ? result.deleted : [],
3437
- newFacts: Array.isArray(result.newFacts) ? result.newFacts : []
3629
+ newFacts: Array.isArray(result.newFacts) ? result.newFacts : [],
3630
+ corpus: corpusByPrompt.get(prompts) ?? null
3438
3631
  };
3439
3632
  },
3440
3633
  maxOutputTokens: this.options.llmProvider.maxOutputTokens,
@@ -3449,6 +3642,7 @@ var MaintenanceService = class {
3449
3642
  const safeDeletedSet = /* @__PURE__ */ new Set();
3450
3643
  const newFacts = [];
3451
3644
  const newFactItemIndexes = [];
3645
+ const newFactCorpora = [];
3452
3646
  for (const batchResult of outcome.results) {
3453
3647
  const mutableIds = new Set(batchResult.batch.map((f) => f.id));
3454
3648
  for (const id of batchResult.downgraded) if (mutableIds.has(id)) safeDowngradedSet.add(id);
@@ -3456,6 +3650,7 @@ var MaintenanceService = class {
3456
3650
  batchResult.newFacts.forEach((raw, itemIndex) => {
3457
3651
  newFacts.push(raw);
3458
3652
  newFactItemIndexes.push(itemIndex);
3653
+ newFactCorpora.push(batchResult.corpus);
3459
3654
  });
3460
3655
  }
3461
3656
  const safeDowngraded = Array.from(safeDowngradedSet);
@@ -3463,12 +3658,14 @@ var MaintenanceService = class {
3463
3658
  const diagBuffer = new DiagnosticBuffer();
3464
3659
  const validNewFacts = [];
3465
3660
  const validNewFactItemIndexes = [];
3661
+ const validNewFactCorpora = [];
3466
3662
  newFacts.forEach((raw, k) => {
3467
3663
  const itemIndex = newFactItemIndexes[k];
3468
3664
  const valid = validateFact(raw);
3469
3665
  if (valid) {
3470
3666
  validNewFacts.push(valid);
3471
3667
  validNewFactItemIndexes.push(itemIndex);
3668
+ validNewFactCorpora.push(newFactCorpora[k]);
3472
3669
  } else {
3473
3670
  diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
3474
3671
  }
@@ -3498,6 +3695,10 @@ var MaintenanceService = class {
3498
3695
  continue;
3499
3696
  }
3500
3697
  const id = generateId("fact_");
3698
+ const grounding = healGrounding ? groundingOutcome(checkGrounding(fact.evidence, validNewFactCorpora[k] ?? [], healGrounding), now) : null;
3699
+ if (grounding?.diagnostic) {
3700
+ diagBuffer.push({ ...diagBase, code: grounding.diagnostic.code, detail: { factId: id, itemIndex: validNewFactItemIndexes[k], reason: grounding.diagnostic.reason } });
3701
+ }
3501
3702
  const factObj = {
3502
3703
  id,
3503
3704
  entity_id: entityId,
@@ -3512,7 +3713,8 @@ var MaintenanceService = class {
3512
3713
  updated_at: now,
3513
3714
  last_accessed_at: null,
3514
3715
  access_count: 0,
3515
- deleted_at: null
3716
+ deleted_at: null,
3717
+ ...grounding?.trust
3516
3718
  };
3517
3719
  await this.entryRepo.upsert(factObj, tx);
3518
3720
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
@@ -3876,7 +4078,7 @@ var MaintenanceService = class {
3876
4078
  * batches that reduce to the same query share one lookup. Caller-owned and
3877
4079
  * per-pass — see the call site in doRunHeal.
3878
4080
  */
3879
- async _selectHealAnchors(entityId, batch, cap = HEAL_MAX_ANCHORS, cache) {
4081
+ async _selectHealAnchors(entityId, batch, cap = HEAL_MAX_ANCHORS, cache, withBodies = false) {
3880
4082
  const query = batch.map((f) => f.title).join(" ").trim();
3881
4083
  if (!query) return [];
3882
4084
  const cached = cache?.get(query);
@@ -3889,7 +4091,7 @@ var MaintenanceService = class {
3889
4091
  const hitIds = hits.map((h) => h.id);
3890
4092
  const anchors = [];
3891
4093
  if (hitIds.length > 0) {
3892
- const rows = await this.entryRepo.findAnchorRowsByIds(entityId, hitIds);
4094
+ const rows = withBodies ? await this.entryRepo.findAnchorRowsByIds(entityId, hitIds, void 0, { withBody: true }) : await this.entryRepo.findAnchorRowsByIds(entityId, hitIds);
3893
4095
  const byId = new Map(rows.map((r) => [r.id, r]));
3894
4096
  for (const id of hitIds) {
3895
4097
  const row = byId.get(id);
@@ -5423,6 +5625,6 @@ var WriteService = class {
5423
5625
  }
5424
5626
  };
5425
5627
 
5426
- export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, DiagnosticBuffer, EMBED_CHARS_CEILING, EmbeddingService, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
5427
- //# sourceMappingURL=chunk-3P7FAKJA.mjs.map
5428
- //# sourceMappingURL=chunk-3P7FAKJA.mjs.map
5628
+ export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, DiagnosticBuffer, EMBED_CHARS_CEILING, EmbeddingService, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, edgeTripleAllowed, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveGrounding, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
5629
+ //# sourceMappingURL=chunk-VOSMYISQ.mjs.map
5630
+ //# sourceMappingURL=chunk-VOSMYISQ.mjs.map