@equationalapplications/core-llm-wiki 7.4.0 → 7.6.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/index.js CHANGED
@@ -1144,15 +1144,16 @@ var EntryRepository = class extends BaseRepository {
1144
1144
  * fact, not only immutable_document rows, so the source-type restriction has
1145
1145
  * to be applied here, after retrieval.
1146
1146
  */
1147
- async findAnchorRowsByIds(entityId, ids, tx) {
1147
+ async findAnchorRowsByIds(entityId, ids, tx, opts) {
1148
1148
  if (ids.length === 0) return [];
1149
1149
  const executor = this.getExecutor(tx);
1150
+ const columns = opts?.withBody ? "id, title, source_ref, body, lifecycle_status" : "id, title, source_ref";
1150
1151
  const rows = [];
1151
1152
  for (let i = 0; i < ids.length; i += this.chunkSize) {
1152
1153
  const chunk = ids.slice(i, i + this.chunkSize);
1153
1154
  const placeholders = chunk.map(() => "?").join(", ");
1154
1155
  const chunkRows = await executor.getAllAsync(
1155
- `SELECT id, title, source_ref FROM ${this.prefix}entries
1156
+ `SELECT ${columns} FROM ${this.prefix}entries
1156
1157
  WHERE entity_id = ? AND deleted_at IS NULL
1157
1158
  AND source_type = 'immutable_document'
1158
1159
  AND id IN (${placeholders})`,
@@ -4154,6 +4155,19 @@ function validateTags(tags) {
4154
4155
  if (!Array.isArray(tags)) return [];
4155
4156
  return tags.filter((t) => typeof t === "string").map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0 && t.length <= 40).slice(0, 6);
4156
4157
  }
4158
+ var MAX_EVIDENCE_QUOTES = 10;
4159
+ function normalizeEvidence(raw) {
4160
+ if (!Array.isArray(raw)) return void 0;
4161
+ const out = [];
4162
+ for (const entry of raw) {
4163
+ if (typeof entry !== "string") continue;
4164
+ const trimmed = entry.trim();
4165
+ if (!trimmed) continue;
4166
+ out.push(trimmed);
4167
+ if (out.length > MAX_EVIDENCE_QUOTES) break;
4168
+ }
4169
+ return out;
4170
+ }
4157
4171
  function validateFact(fact) {
4158
4172
  if (typeof fact?.title !== "string" || typeof fact?.body !== "string") return null;
4159
4173
  const title = clip(fact.title, 80);
@@ -4161,13 +4175,17 @@ function validateFact(fact) {
4161
4175
  if (!title || !body) return null;
4162
4176
  let confidence = fact.confidence;
4163
4177
  if (confidence !== "certain" && confidence !== "tentative") confidence = "inferred";
4164
- return {
4178
+ const valid = {
4165
4179
  ...fact,
4166
4180
  title,
4167
4181
  body,
4168
4182
  confidence,
4169
4183
  tags: validateTags(fact.tags)
4170
4184
  };
4185
+ const evidence = normalizeEvidence(fact.evidence);
4186
+ if (evidence) valid.evidence = evidence;
4187
+ else delete valid.evidence;
4188
+ return valid;
4171
4189
  }
4172
4190
  function validateTask(task) {
4173
4191
  if (typeof task?.description !== "string") return null;
@@ -4212,6 +4230,60 @@ function jaccardScore(a, b) {
4212
4230
  return intersection.size / union.size;
4213
4231
  }
4214
4232
 
4233
+ // src/utils/grounding.ts
4234
+ var GROUNDING_VERIFIER = "process:grounding-check";
4235
+ var WRITERS = ["ingest", "librarian", "heal"];
4236
+ function positiveInt(value, fallback) {
4237
+ return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.floor(value) : fallback;
4238
+ }
4239
+ function resolveGrounding(config) {
4240
+ if (!config || config.mode !== "draft") return null;
4241
+ const writers = Array.isArray(config.writers) ? config.writers.filter((w) => WRITERS.includes(w)) : ["ingest"];
4242
+ return {
4243
+ writers: new Set(writers),
4244
+ minEvidenceChars: positiveInt(config.minEvidenceChars, 20),
4245
+ // Never ask for more quotes than checkGrounding accepts (MAX_EVIDENCE_QUOTES).
4246
+ maxEvidence: Math.min(MAX_EVIDENCE_QUOTES, positiveInt(config.maxEvidence, 3)),
4247
+ maxEvidenceChars: positiveInt(config.maxEvidenceChars, 300)
4248
+ };
4249
+ }
4250
+ function normalizeForGrounding(text) {
4251
+ return text.normalize("NFKC").replace(/\s+/g, " ").trim();
4252
+ }
4253
+ function buildGroundingCorpus(parts) {
4254
+ return parts.filter((p) => typeof p === "string").map(normalizeForGrounding);
4255
+ }
4256
+ function checkGrounding(evidence, normalizedCorpus, cfg) {
4257
+ const quotes = evidence ?? [];
4258
+ if (quotes.length > MAX_EVIDENCE_QUOTES) return { status: "failed", reason: "too_many_quotes" };
4259
+ if (quotes.length === 0) return { status: "missing", reason: "no_evidence" };
4260
+ const qualifying = quotes.map(normalizeForGrounding).filter((q) => q.length >= cfg.minEvidenceChars);
4261
+ if (qualifying.length === 0) return { status: "missing", reason: "evidence_too_short" };
4262
+ if (qualifying.some((q) => !normalizedCorpus.some((part) => part.includes(q)))) {
4263
+ return { status: "failed", reason: "quote_not_found" };
4264
+ }
4265
+ return {
4266
+ status: "grounded",
4267
+ retained: qualifying.slice(0, cfg.maxEvidence).map((q) => safeSlice(q, 0, cfg.maxEvidenceChars))
4268
+ };
4269
+ }
4270
+ function groundingOutcome(verdict, now) {
4271
+ if (verdict.status === "grounded") {
4272
+ return {
4273
+ trust: {
4274
+ lifecycle_status: "stable",
4275
+ okf_verified: [{ by: GROUNDING_VERIFIER, at: new Date(now).toISOString() }],
4276
+ last_verified_at: now,
4277
+ last_verified_by: GROUNDING_VERIFIER
4278
+ }
4279
+ };
4280
+ }
4281
+ return {
4282
+ trust: { lifecycle_status: "draft" },
4283
+ diagnostic: { code: verdict.status === "missing" ? "grounding_missing" : "grounding_failed", reason: verdict.reason }
4284
+ };
4285
+ }
4286
+
4215
4287
  // src/utils/diagnostics.ts
4216
4288
  var SEVERITY = {
4217
4289
  ingest_chunk_failed: "warn",
@@ -4341,17 +4413,40 @@ Return ONLY a valid JSON object matching this schema:
4341
4413
  }
4342
4414
  If no manifest type fits a fact, omit that fact from "classifications" entirely \u2014 do not guess.
4343
4415
  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.`;
4416
+ var GROUNDING_SOURCE = {
4417
+ ingest: { key: "facts", source: "the document chunk" },
4418
+ librarian: { key: "facts", source: 'the "summary" text of the events' },
4419
+ heal: { key: "newFacts", source: 'the "summary" text of the recent events or the "body" text of the document anchors' }
4420
+ };
4421
+ function groundingEvidenceBlock(writer, cfg) {
4422
+ const { key, source } = GROUNDING_SOURCE[writer];
4423
+ 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.
4424
+ "evidence": ["exact substring copied from the SOURCE section"]`;
4425
+ }
4344
4426
 
4345
4427
  // src/utils/healConstants.ts
4346
4428
  var HEAL_MAX_ANCHORS = 50;
4347
4429
  var HEAL_ANCHORS_PER_CANDIDATE = 4;
4348
4430
  var HEAL_MAX_FACT_BODY_CHARS_L3 = 4e3;
4349
4431
  var HEAL_MAX_TASKS = 50;
4432
+ var HEAL_ANCHOR_BODY_CHARS = 800;
4350
4433
 
4351
4434
  // src/services/PromptService.ts
4352
4435
  var PromptService = class {
4353
- constructor(globalOverrides) {
4436
+ constructor(globalOverrides, grounding = null) {
4354
4437
  this.globalOverrides = globalOverrides;
4438
+ this.grounding = grounding;
4439
+ }
4440
+ /** The resolved grounding config when `writer` is in `grounding.writers`; otherwise null (spec §6.2). */
4441
+ groundingFor(writer) {
4442
+ return this.grounding?.writers.has(writer) ? this.grounding : null;
4443
+ }
4444
+ /** Appended after any override and after ontology context, so it is always last. */
4445
+ appendGrounding(systemPrompt, writer) {
4446
+ const cfg = this.groundingFor(writer);
4447
+ return cfg ? `${systemPrompt}
4448
+
4449
+ ${groundingEvidenceBlock(writer, cfg)}` : systemPrompt;
4355
4450
  }
4356
4451
  hydrate(template, variables) {
4357
4452
  return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => {
@@ -4381,13 +4476,13 @@ ${ctx.ontologyModeInstructions}`;
4381
4476
  const hasDocumentChunk = /\{\{\s*documentChunk\s*\}\}/.test(template);
4382
4477
  if (hasDocumentChunk || this.hasOntologyPlaceholders(template)) {
4383
4478
  return {
4384
- systemPrompt: this.buildSystemPrompt(template, { documentChunk }, ontologyContext),
4479
+ systemPrompt: this.appendGrounding(this.buildSystemPrompt(template, { documentChunk }, ontologyContext), "ingest"),
4385
4480
  userPrompt: hasDocumentChunk ? "Please extract the facts." : `Document Chunk:
4386
4481
  ${documentChunk}`
4387
4482
  };
4388
4483
  }
4389
4484
  return {
4390
- systemPrompt: this.appendOntology(template, ontologyContext),
4485
+ systemPrompt: this.appendGrounding(this.appendOntology(template, ontologyContext), "ingest"),
4391
4486
  userPrompt: `Document Chunk:
4392
4487
  ${documentChunk}`
4393
4488
  };
@@ -4396,23 +4491,27 @@ ${documentChunk}`
4396
4491
  const template = runtimeOverride ?? this.globalOverrides?.librarianSystemPrompt ?? LIBRARIAN_SYSTEM_PROMPT;
4397
4492
  const hasEvents = /\{\{\s*events\s*\}\}/.test(template);
4398
4493
  const hasCurrentFacts = /\{\{\s*currentFacts\s*\}\}/.test(template);
4494
+ const eventsShown = hasEvents || !hasCurrentFacts;
4495
+ const corpusField = this.groundingFor("librarian") ? { groundingCorpus: buildGroundingCorpus(eventsShown ? events.map(summaryOf) : []) } : {};
4399
4496
  if (hasEvents || hasCurrentFacts || this.hasOntologyPlaceholders(template)) {
4400
4497
  return {
4401
- systemPrompt: this.buildSystemPrompt(template, { events, currentFacts }, ontologyContext),
4498
+ systemPrompt: this.appendGrounding(this.buildSystemPrompt(template, { events, currentFacts }, ontologyContext), "librarian"),
4402
4499
  userPrompt: hasEvents || hasCurrentFacts ? "Please synthesize the context." : `Events:
4403
4500
  ${JSON.stringify(events, null, 2)}
4404
4501
 
4405
4502
  Current Facts:
4406
- ${JSON.stringify(currentFacts, null, 2)}`
4503
+ ${JSON.stringify(currentFacts, null, 2)}`,
4504
+ ...corpusField
4407
4505
  };
4408
4506
  }
4409
4507
  return {
4410
- systemPrompt: this.appendOntology(template, ontologyContext),
4508
+ systemPrompt: this.appendGrounding(this.appendOntology(template, ontologyContext), "librarian"),
4411
4509
  userPrompt: `Events:
4412
4510
  ${JSON.stringify(events, null, 2)}
4413
4511
 
4414
4512
  Current Facts:
4415
- ${JSON.stringify(currentFacts, null, 2)}`
4513
+ ${JSON.stringify(currentFacts, null, 2)}`,
4514
+ ...corpusField
4416
4515
  };
4417
4516
  }
4418
4517
  /**
@@ -4442,40 +4541,54 @@ ${JSON.stringify(currentFacts, null, 2)}`
4442
4541
  const effectiveEvents = attemptLevel >= 2 ? [] : recentEvents;
4443
4542
  const maxAnchors = Math.max(1, Math.min(HEAL_MAX_ANCHORS, healCandidates.length * HEAL_ANCHORS_PER_CANDIDATE));
4444
4543
  const effectiveAnchors = documentAnchors.slice(0, maxAnchors);
4544
+ const template = runtimeOverride ?? this.globalOverrides?.healSystemPrompt ?? HEAL_SYSTEM_PROMPT;
4545
+ const hasRecentEvents = /\{\{\s*recentEvents\s*\}\}/.test(template);
4546
+ const hasDocumentAnchors = /\{\{\s*documentAnchors\s*\}\}/.test(template);
4547
+ const usesPlaceholders = /\{\{\s*healCandidates\s*\}\}/.test(template) || hasDocumentAnchors || /\{\{\s*allTasks\s*\}\}/.test(template) || hasRecentEvents;
4548
+ const healGrounding = this.groundingFor("heal");
4549
+ const promptAnchors = healGrounding ? effectiveAnchors.map(toGroundingAnchor) : effectiveAnchors;
4550
+ const eventsShown = !usesPlaceholders || hasRecentEvents;
4551
+ const anchorsShown = !usesPlaceholders || hasDocumentAnchors;
4552
+ const groundingCorpus = healGrounding ? buildGroundingCorpus([
4553
+ ...eventsShown ? effectiveEvents.map(summaryOf) : [],
4554
+ ...anchorsShown ? effectiveAnchors.map((a, i) => ({ status: a?.lifecycle_status, shown: promptAnchors[i] })).filter((x) => x.status !== "draft").map((x) => x.shown?.body) : []
4555
+ ]) : null;
4445
4556
  const { shapedCandidates, degraded } = applyBodyTruncation(
4446
4557
  healCandidates,
4447
4558
  attemptLevel,
4448
4559
  bodyTruncationChars
4449
4560
  );
4450
- const template = runtimeOverride ?? this.globalOverrides?.healSystemPrompt ?? HEAL_SYSTEM_PROMPT;
4451
- if (/\{\{\s*healCandidates\s*\}\}/.test(template) || /\{\{\s*documentAnchors\s*\}\}/.test(template) || /\{\{\s*allTasks\s*\}\}/.test(template) || /\{\{\s*recentEvents\s*\}\}/.test(template)) {
4561
+ const corpusField = groundingCorpus !== null ? { groundingCorpus } : {};
4562
+ if (usesPlaceholders) {
4452
4563
  return {
4453
4564
  prompts: {
4454
- systemPrompt: this.hydrate(template, {
4565
+ systemPrompt: this.appendGrounding(this.hydrate(template, {
4455
4566
  healCandidates: shapedCandidates,
4456
- documentAnchors: effectiveAnchors,
4567
+ documentAnchors: promptAnchors,
4457
4568
  allTasks: effectiveTasks,
4458
4569
  recentEvents: effectiveEvents
4459
- }),
4570
+ }), "heal"),
4460
4571
  userPrompt: "Please heal the memory graph."
4461
4572
  },
4462
- degraded
4573
+ degraded,
4574
+ ...corpusField
4463
4575
  };
4464
4576
  }
4465
4577
  return {
4466
4578
  prompts: {
4467
- systemPrompt: template,
4579
+ systemPrompt: this.appendGrounding(template, "heal"),
4468
4580
  userPrompt: `Heal Candidates:
4469
4581
  ${JSON.stringify(shapedCandidates, null, 2)}
4470
4582
  Document Anchors (DO NOT MODIFY OR DELETE):
4471
- ${JSON.stringify(effectiveAnchors, null, 2)}
4583
+ ${JSON.stringify(promptAnchors, null, 2)}
4472
4584
  All Tasks:
4473
4585
  ${JSON.stringify(effectiveTasks, null, 2)}
4474
4586
  Recent Events:
4475
4587
  ${JSON.stringify(effectiveEvents, null, 2)}
4476
4588
  The following document anchors are provided for contradiction detection only. Do not include them in \`downgraded\`, \`deleted\`, or \`newFacts\`.`
4477
4589
  },
4478
- degraded
4590
+ degraded,
4591
+ ...corpusField
4479
4592
  };
4480
4593
  }
4481
4594
  buildOntologyBackfillPrompt(facts, runtimeOverride, ontologyContext) {
@@ -4525,6 +4638,19 @@ function applyBodyTruncation(candidates, attemptLevel, bodyTruncationChars) {
4525
4638
  }
4526
4639
  return { shapedCandidates, degraded };
4527
4640
  }
4641
+ function summaryOf(event) {
4642
+ return event?.summary;
4643
+ }
4644
+ function toGroundingAnchor(anchor) {
4645
+ if (typeof anchor !== "object" || anchor === null) return anchor;
4646
+ const a = anchor;
4647
+ return {
4648
+ id: a.id,
4649
+ title: a.title,
4650
+ source_ref: a.source_ref,
4651
+ body: typeof a.body === "string" ? safeSlice(a.body, 0, HEAL_ANCHOR_BODY_CHARS) : ""
4652
+ };
4653
+ }
4528
4654
 
4529
4655
  // src/utils/chunkingDefaults.ts
4530
4656
  var DEFAULT_MAX_CHUNK_LENGTH = 12e3;
@@ -4554,7 +4680,7 @@ var IngestionService = class {
4554
4680
  this.jobManager = jobManager;
4555
4681
  this.embeddingService = embeddingService;
4556
4682
  this.ontologyService = ontologyService;
4557
- this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
4683
+ this.promptService = promptService ?? new PromptService(this.options.config?.prompts, resolveGrounding(this.options.config?.grounding));
4558
4684
  }
4559
4685
  async ingestDocument(entityId, params, opts) {
4560
4686
  const sourceRef = normalizeSourceRef(params.sourceRef);
@@ -4587,6 +4713,7 @@ var IngestionService = class {
4587
4713
  const { chunks, truncated } = chunkText(params.documentChunk, maxChunkLength, chunkOverlap);
4588
4714
  if (chunks.length === 0) return zeroChunkResult();
4589
4715
  const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
4716
+ const ingestGrounding = this.promptService.groundingFor("ingest");
4590
4717
  const chunkResults = await withConcurrency(
4591
4718
  chunks.map((chunk, chunkIndex) => async () => {
4592
4719
  const { systemPrompt, userPrompt } = this.promptService.buildIngestPrompt(
@@ -4610,11 +4737,16 @@ var IngestionService = class {
4610
4737
  rejected.push({ itemIndex, reason: factRejectionReason(raw) });
4611
4738
  }
4612
4739
  });
4740
+ const verdicts = ingestGrounding ? (() => {
4741
+ const corpus = buildGroundingCorpus([chunk]);
4742
+ return facts.map((f) => checkGrounding(f.evidence, corpus, ingestGrounding));
4743
+ })() : [];
4613
4744
  return {
4614
4745
  status: "ok",
4615
4746
  facts,
4616
4747
  itemIndexes,
4617
4748
  rejected,
4749
+ verdicts,
4618
4750
  ontology_updates: result2.ontology_updates
4619
4751
  };
4620
4752
  } catch (e) {
@@ -4659,6 +4791,7 @@ var IngestionService = class {
4659
4791
  const failures = [];
4660
4792
  const seen = /* @__PURE__ */ new Set();
4661
4793
  const orderedChunkFacts = [];
4794
+ const groundingLedger = /* @__PURE__ */ new Map();
4662
4795
  const diagBuffer = new DiagnosticBuffer();
4663
4796
  const diagBase = { entityId, operation: "ingest", trigger: "call" };
4664
4797
  for (const [chunkIndex, slot] of chunkResults.entries()) {
@@ -4677,6 +4810,7 @@ var IngestionService = class {
4677
4810
  if (!seen.has(normalizedTitle)) {
4678
4811
  seen.add(normalizedTitle);
4679
4812
  dedupedFacts.push(fact);
4813
+ if (ingestGrounding) groundingLedger.set(fact, { verdict: slot.verdicts[k], chunkIndex, itemIndex: slot.itemIndexes[k] });
4680
4814
  } else {
4681
4815
  diagBuffer.push({
4682
4816
  ...diagBase,
@@ -4713,7 +4847,7 @@ var IngestionService = class {
4713
4847
  try {
4714
4848
  if (failedChunks === 0) {
4715
4849
  const fullResult = await this.db.withTransactionAsync(async (tx) => {
4716
- return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer);
4850
+ return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer, groundingLedger);
4717
4851
  });
4718
4852
  deletedSourceFactIds.push(...fullResult.deletedSourceFactIds);
4719
4853
  insertedFacts.push(...fullResult.insertedFacts);
@@ -4721,7 +4855,7 @@ var IngestionService = class {
4721
4855
  const partialResult = await this.db.withTransactionAsync(async (tx) => {
4722
4856
  const flat = [];
4723
4857
  for (const slot of orderedChunkFacts) flat.push(...slot.facts);
4724
- return await this.appendPartialFacts(entityId, sourceRef, flat, tx, diagBuffer);
4858
+ return await this.appendPartialFacts(entityId, sourceRef, flat, tx, diagBuffer, groundingLedger);
4725
4859
  });
4726
4860
  insertedFacts.push(...partialResult.insertedDescriptors);
4727
4861
  }
@@ -4863,7 +4997,8 @@ var IngestionService = class {
4863
4997
  last_accessed_at: null,
4864
4998
  access_count: 0,
4865
4999
  deleted_at: null,
4866
- okf_type: normalized.okf_type
5000
+ okf_type: normalized.okf_type,
5001
+ ...opts?.nodeTrust?.get(node.id)
4867
5002
  };
4868
5003
  wikiFacts.push(wikiFact);
4869
5004
  }
@@ -4954,9 +5089,10 @@ var IngestionService = class {
4954
5089
  * On the partial path the caller never invokes this method; the empty
4955
5090
  * arrays stay empty.
4956
5091
  */
4957
- async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer) {
5092
+ async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer, groundingLedger = /* @__PURE__ */ new Map()) {
4958
5093
  const deletedSourceFactIds = [];
4959
5094
  const insertedFacts = [];
5095
+ const nodeTrust = /* @__PURE__ */ new Map();
4960
5096
  deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
4961
5097
  const titleIndex = /* @__PURE__ */ new Map();
4962
5098
  const existingFacts = await this.entryRepo.findRecentByEntityId(entityId, 500, tx, sourceRef);
@@ -5000,6 +5136,20 @@ var IngestionService = class {
5000
5136
  tags: fact.tags,
5001
5137
  confidence: fact.confidence
5002
5138
  });
5139
+ const grounding = groundingLedger.get(fact);
5140
+ if (grounding) {
5141
+ const outcome = groundingOutcome(grounding.verdict, now);
5142
+ nodeTrust.set(id, outcome.trust);
5143
+ if (outcome.diagnostic) {
5144
+ diagBuffer.push({
5145
+ entityId,
5146
+ operation: "ingest",
5147
+ trigger: "call",
5148
+ code: outcome.diagnostic.code,
5149
+ detail: { factId: id, sourceRef, chunkIndex: grounding.chunkIndex, itemIndex: grounding.itemIndex, reason: outcome.diagnostic.reason }
5150
+ });
5151
+ }
5152
+ }
5003
5153
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
5004
5154
  titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
5005
5155
  if (normalized.edges.length > 0) {
@@ -5031,7 +5181,7 @@ var IngestionService = class {
5031
5181
  entityId,
5032
5182
  { sourceRef, sourceHash, nodes: hostNodes, edges: hostEdges },
5033
5183
  tx,
5034
- { strict: false, diag: { buffer: diagBuffer, operation: "ingest" } }
5184
+ { strict: false, diag: { buffer: diagBuffer, operation: "ingest" }, ...nodeTrust.size > 0 ? { nodeTrust } : {} }
5035
5185
  );
5036
5186
  return { deletedSourceFactIds, insertedFacts };
5037
5187
  }
@@ -5055,7 +5205,7 @@ var IngestionService = class {
5055
5205
  * Runs INSIDE the caller's `tx`. Does not open a nested transaction.
5056
5206
  * Returns `{ inserted, skippedDuplicate }` for observability.
5057
5207
  */
5058
- async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx, diagBuffer) {
5208
+ async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx, diagBuffer, groundingLedger = /* @__PURE__ */ new Map()) {
5059
5209
  const liveIds = await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false);
5060
5210
  const liveFacts = liveIds.length === 0 ? [] : await this.entryRepo.findByIds(liveIds, void 0, tx);
5061
5211
  const liveTitles = new Set(liveFacts.map((f) => normalizeTitleKey(f.title)));
@@ -5078,6 +5228,17 @@ var IngestionService = class {
5078
5228
  }
5079
5229
  liveTitles.add(normalizedTitle);
5080
5230
  const id = generateId("fact_");
5231
+ const grounding = groundingLedger.get(fact);
5232
+ const outcome = grounding ? groundingOutcome(grounding.verdict, now) : null;
5233
+ if (grounding && outcome?.diagnostic) {
5234
+ diagBuffer.push({
5235
+ entityId,
5236
+ operation: "ingest",
5237
+ trigger: "call",
5238
+ code: outcome.diagnostic.code,
5239
+ detail: { factId: id, sourceRef, chunkIndex: grounding.chunkIndex, itemIndex: grounding.itemIndex, reason: outcome.diagnostic.reason }
5240
+ });
5241
+ }
5081
5242
  const wikiFact = {
5082
5243
  id,
5083
5244
  entity_id: entityId,
@@ -5096,7 +5257,8 @@ var IngestionService = class {
5096
5257
  last_accessed_at: null,
5097
5258
  access_count: 0,
5098
5259
  deleted_at: null,
5099
- okf_type: null
5260
+ okf_type: null,
5261
+ ...outcome?.trust
5100
5262
  };
5101
5263
  await this.entryRepo.upsert(wikiFact, tx);
5102
5264
  insertedDescriptors.push({
@@ -5112,6 +5274,51 @@ var IngestionService = class {
5112
5274
  }
5113
5275
  };
5114
5276
 
5277
+ // src/utils/classifier.ts
5278
+ var isUnit = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1;
5279
+ function validateClassifierAnswer(response, key, question) {
5280
+ try {
5281
+ if (response === null || typeof response !== "object") return { ok: false, reason: "malformed" };
5282
+ const answers = response.answers;
5283
+ if (answers === null || typeof answers !== "object") return { ok: false, reason: "missing_answer" };
5284
+ if (!Object.prototype.hasOwnProperty.call(answers, key)) return { ok: false, reason: "missing_answer" };
5285
+ const raw = answers[key];
5286
+ if (raw === null || typeof raw !== "object") return { ok: false, reason: "missing_answer" };
5287
+ const a = raw;
5288
+ if (a.kind !== question.kind) return { ok: false, reason: "kind_mismatch" };
5289
+ if (question.kind === "choice") {
5290
+ if (typeof a.choice !== "string" || !question.options.includes(a.choice)) return { ok: false, reason: "choice_not_offered" };
5291
+ if (!isUnit(a.confidence)) return { ok: false, reason: "invalid_probability" };
5292
+ const probs = a.probabilities;
5293
+ if (probs === null || typeof probs !== "object" || Array.isArray(probs)) return { ok: false, reason: "invalid_probability" };
5294
+ const probabilities = {};
5295
+ for (const [k, v] of Object.entries(probs)) {
5296
+ if (!isUnit(v)) return { ok: false, reason: "invalid_probability" };
5297
+ probabilities[k] = v;
5298
+ }
5299
+ return { ok: true, answer: { kind: "choice", choice: a.choice, confidence: a.confidence, probabilities } };
5300
+ }
5301
+ if (question.kind === "binary") {
5302
+ if (!isUnit(a.probability)) return { ok: false, reason: "invalid_probability" };
5303
+ return { ok: true, answer: { kind: "binary", probability: a.probability } };
5304
+ }
5305
+ const maxScore = question.levels.length - 1;
5306
+ if (typeof a.score !== "number" || !Number.isFinite(a.score) || a.score < 0 || a.score > maxScore) {
5307
+ return { ok: false, reason: "score_out_of_range" };
5308
+ }
5309
+ if (!isUnit(a.confidence)) return { ok: false, reason: "invalid_probability" };
5310
+ if (!Array.isArray(a.probabilities) || !a.probabilities.every(isUnit)) return { ok: false, reason: "invalid_probability" };
5311
+ return { ok: true, answer: { kind: "score", score: a.score, confidence: a.confidence, probabilities: a.probabilities.slice() } };
5312
+ } catch {
5313
+ return { ok: false, reason: "malformed" };
5314
+ }
5315
+ }
5316
+ function classifierStateForFact(fact) {
5317
+ const parts = [fact.title, fact.body];
5318
+ if (Array.isArray(fact.tags) && fact.tags.length > 0) parts.push(`Tags: ${fact.tags.join(", ")}`);
5319
+ return parts.join("\n\n");
5320
+ }
5321
+
5115
5322
  // src/services/BoundedLlmCall.ts
5116
5323
  var DEFAULT_BATCH_SIZE = 10;
5117
5324
  var ESTIMATED_OUTPUT_TOKENS_PER_ITEM = 150;
@@ -5219,7 +5426,7 @@ async function runBatched(args) {
5219
5426
  }
5220
5427
  let result;
5221
5428
  try {
5222
- result = parse(responseText, batch);
5429
+ result = parse(responseText, batch, prompts);
5223
5430
  } catch (err) {
5224
5431
  await onFailure(batch, err, attemptLevel, false);
5225
5432
  return;
@@ -5300,7 +5507,7 @@ var MaintenanceService = class {
5300
5507
  this.jobManager = jobManager;
5301
5508
  this.embeddingService = embeddingService;
5302
5509
  this.ontologyService = ontologyService;
5303
- this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
5510
+ this.promptService = promptService ?? new PromptService(this.options.config?.prompts, resolveGrounding(this.options.config?.grounding));
5304
5511
  }
5305
5512
  async runPrune(entityId, options) {
5306
5513
  this.jobManager.acquireLock("prune", entityId);
@@ -5605,8 +5812,10 @@ var MaintenanceService = class {
5605
5812
  };
5606
5813
  });
5607
5814
  const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
5608
- const { systemPrompt, userPrompt } = this.promptService.buildLibrarianPrompt(
5609
- events.reverse(),
5815
+ const promptEvents = events.reverse();
5816
+ const librarianGrounding = this.promptService.groundingFor("librarian");
5817
+ const { systemPrompt, userPrompt, groundingCorpus: librarianCorpus = [] } = this.promptService.buildLibrarianPrompt(
5818
+ promptEvents,
5610
5819
  currentFacts,
5611
5820
  promptOverride,
5612
5821
  ontologyContext
@@ -5675,6 +5884,10 @@ var MaintenanceService = class {
5675
5884
  const validationDrops = [];
5676
5885
  const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
5677
5886
  for (const drop of validationDrops) diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: id }));
5887
+ const grounding = librarianGrounding ? groundingOutcome(checkGrounding(fact.evidence, librarianCorpus, librarianGrounding), now) : null;
5888
+ if (grounding?.diagnostic) {
5889
+ diagBuffer.push({ ...diagBase, code: grounding.diagnostic.code, detail: { factId: id, itemIndex: validFactItemIndexes[k], reason: grounding.diagnostic.reason } });
5890
+ }
5678
5891
  const factObj = {
5679
5892
  id,
5680
5893
  entity_id: entityId,
@@ -5690,7 +5903,8 @@ var MaintenanceService = class {
5690
5903
  last_accessed_at: null,
5691
5904
  access_count: 0,
5692
5905
  deleted_at: null,
5693
- okf_type: normalized.okf_type
5906
+ okf_type: normalized.okf_type,
5907
+ ...grounding?.trust
5694
5908
  };
5695
5909
  await this.entryRepo.upsert(factObj, tx);
5696
5910
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
@@ -5806,11 +6020,14 @@ var MaintenanceService = class {
5806
6020
  }
5807
6021
  const allTasks = await this.taskRepo.findAllPending([entityId], HEAL_MAX_TASKS);
5808
6022
  const recentEvents = await this.eventRepo.getRecent(entityId, 20);
6023
+ const healGrounding = this.promptService.groundingFor("heal");
5809
6024
  const toPromptShape = (f) => {
5810
6025
  const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
5811
- return { ...rest, tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags };
6026
+ const shown = healGrounding ? (({ lifecycle_status: _l, okf_verified: _v, last_verified_at: _a, last_verified_by: _b, ...r }) => r)(rest) : rest;
6027
+ return { ...shown, tags: typeof shown.tags === "string" ? JSON.parse(shown.tags) : shown.tags };
5812
6028
  };
5813
6029
  const anchorCache = /* @__PURE__ */ new Map();
6030
+ const corpusByPrompt = /* @__PURE__ */ new WeakMap();
5814
6031
  const degraded = [];
5815
6032
  const outcome = await runBatched({
5816
6033
  items: healCandidates,
@@ -5823,9 +6040,10 @@ var MaintenanceService = class {
5823
6040
  // not overfetch the same 200 keyword hits a 25-fact batch once did.
5824
6041
  // buildHealPrompt applies the matching cap on its side — values must match.
5825
6042
  Math.min(HEAL_MAX_ANCHORS, batch.length * HEAL_ANCHORS_PER_CANDIDATE),
5826
- anchorCache
6043
+ anchorCache,
6044
+ healGrounding !== null
5827
6045
  );
5828
- const { prompts, degraded: batchDegraded } = await this.promptService.buildHealPrompt(
6046
+ const { prompts, degraded: batchDegraded, groundingCorpus } = await this.promptService.buildHealPrompt(
5829
6047
  batch.map(toPromptShape),
5830
6048
  documentAnchors,
5831
6049
  allTasks,
@@ -5835,16 +6053,18 @@ var MaintenanceService = class {
5835
6053
  bodyTruncationChars
5836
6054
  );
5837
6055
  degraded.push(...batchDegraded);
6056
+ if (groundingCorpus !== void 0) corpusByPrompt.set(prompts, groundingCorpus);
5838
6057
  return prompts;
5839
6058
  },
5840
6059
  call: (prompts) => this.options.llmProvider.generateText(prompts),
5841
- parse: (responseText, batch) => {
6060
+ parse: (responseText, batch, prompts) => {
5842
6061
  const result = parseJsonResponse(responseText);
5843
6062
  return {
5844
6063
  batch,
5845
6064
  downgraded: Array.isArray(result.downgraded) ? result.downgraded : [],
5846
6065
  deleted: Array.isArray(result.deleted) ? result.deleted : [],
5847
- newFacts: Array.isArray(result.newFacts) ? result.newFacts : []
6066
+ newFacts: Array.isArray(result.newFacts) ? result.newFacts : [],
6067
+ corpus: corpusByPrompt.get(prompts) ?? null
5848
6068
  };
5849
6069
  },
5850
6070
  maxOutputTokens: this.options.llmProvider.maxOutputTokens,
@@ -5859,6 +6079,7 @@ var MaintenanceService = class {
5859
6079
  const safeDeletedSet = /* @__PURE__ */ new Set();
5860
6080
  const newFacts = [];
5861
6081
  const newFactItemIndexes = [];
6082
+ const newFactCorpora = [];
5862
6083
  for (const batchResult of outcome.results) {
5863
6084
  const mutableIds = new Set(batchResult.batch.map((f) => f.id));
5864
6085
  for (const id of batchResult.downgraded) if (mutableIds.has(id)) safeDowngradedSet.add(id);
@@ -5866,6 +6087,7 @@ var MaintenanceService = class {
5866
6087
  batchResult.newFacts.forEach((raw, itemIndex) => {
5867
6088
  newFacts.push(raw);
5868
6089
  newFactItemIndexes.push(itemIndex);
6090
+ newFactCorpora.push(batchResult.corpus);
5869
6091
  });
5870
6092
  }
5871
6093
  const safeDowngraded = Array.from(safeDowngradedSet);
@@ -5873,12 +6095,14 @@ var MaintenanceService = class {
5873
6095
  const diagBuffer = new DiagnosticBuffer();
5874
6096
  const validNewFacts = [];
5875
6097
  const validNewFactItemIndexes = [];
6098
+ const validNewFactCorpora = [];
5876
6099
  newFacts.forEach((raw, k) => {
5877
6100
  const itemIndex = newFactItemIndexes[k];
5878
6101
  const valid = validateFact(raw);
5879
6102
  if (valid) {
5880
6103
  validNewFacts.push(valid);
5881
6104
  validNewFactItemIndexes.push(itemIndex);
6105
+ validNewFactCorpora.push(newFactCorpora[k]);
5882
6106
  } else {
5883
6107
  diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
5884
6108
  }
@@ -5908,6 +6132,10 @@ var MaintenanceService = class {
5908
6132
  continue;
5909
6133
  }
5910
6134
  const id = generateId("fact_");
6135
+ const grounding = healGrounding ? groundingOutcome(checkGrounding(fact.evidence, validNewFactCorpora[k] ?? [], healGrounding), now) : null;
6136
+ if (grounding?.diagnostic) {
6137
+ diagBuffer.push({ ...diagBase, code: grounding.diagnostic.code, detail: { factId: id, itemIndex: validNewFactItemIndexes[k], reason: grounding.diagnostic.reason } });
6138
+ }
5911
6139
  const factObj = {
5912
6140
  id,
5913
6141
  entity_id: entityId,
@@ -5922,7 +6150,8 @@ var MaintenanceService = class {
5922
6150
  updated_at: now,
5923
6151
  last_accessed_at: null,
5924
6152
  access_count: 0,
5925
- deleted_at: null
6153
+ deleted_at: null,
6154
+ ...grounding?.trust
5926
6155
  };
5927
6156
  await this.entryRepo.upsert(factObj, tx);
5928
6157
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
@@ -5994,7 +6223,7 @@ var MaintenanceService = class {
5994
6223
  if (!ontologyService) {
5995
6224
  return { ...zeroed, remaining: 0, deferred: 0 };
5996
6225
  }
5997
- const { mode } = await ontologyService.getEffectiveState(entityId);
6226
+ const { mode, manifest: effectiveManifest } = await ontologyService.getEffectiveState(entityId);
5998
6227
  if (mode === "off") {
5999
6228
  const counts2 = await this.entryRepo.countUntypedByEntityId(entityId, recheckCutoff);
6000
6229
  return { ...zeroed, remaining: 0, deferred: counts2.deferred };
@@ -6004,6 +6233,14 @@ var MaintenanceService = class {
6004
6233
  const counts2 = await this.entryRepo.countUntypedByEntityId(entityId, recheckCutoff);
6005
6234
  return { ...zeroed, remaining: counts2.eligible, deferred: counts2.deferred };
6006
6235
  }
6236
+ const classifierMode = options?.classifier ?? this.options.config?.ontology?.backfillClassifier ?? "llm";
6237
+ const classify = this.options.llmProvider.classify;
6238
+ if (classifierMode === "auto" && typeof classify === "function") {
6239
+ const slugs = effectiveManifest.node_types.map((n) => n.type);
6240
+ if (slugs.length > 0 && slugs.length <= 255) {
6241
+ return this._runClassifierBackfill(entityId, candidates, effectiveManifest, now, recheckCutoff);
6242
+ }
6243
+ }
6007
6244
  const ontologyContext = await ontologyService.buildPromptContext(entityId);
6008
6245
  const toPromptShape = (f) => ({ id: f.id, title: f.title, body: f.body, tags: f.tags });
6009
6246
  const buildPrompt = (facts) => this.promptService.buildOntologyBackfillPrompt(
@@ -6082,6 +6319,87 @@ var MaintenanceService = class {
6082
6319
  deferred: counts.deferred
6083
6320
  };
6084
6321
  }
6322
+ /**
6323
+ * Classifier-mode backfill (spec §7.3): one `choice` question per untyped
6324
+ * fact over the effective manifest's node-type slugs. Accepted answers go
6325
+ * through `_applyOntologyBackfillBatch` exactly like LLM classifications.
6326
+ * No edges are proposed.
6327
+ */
6328
+ async _runClassifierBackfill(entityId, candidates, manifest, now, recheckCutoff) {
6329
+ const provider = this.options.llmProvider;
6330
+ const rawMin = this.options.config?.ontology?.classifyMinConfidence;
6331
+ const minConfidence = typeof rawMin === "number" && Number.isFinite(rawMin) && rawMin >= 0 && rawMin <= 1 ? rawMin : 0.5;
6332
+ const rawConcurrency = this.options.config?.chunkConcurrency ?? 1;
6333
+ const concurrency = Number.isFinite(rawConcurrency) && rawConcurrency >= 1 ? Math.floor(rawConcurrency) : 1;
6334
+ const question = {
6335
+ kind: "choice",
6336
+ options: manifest.node_types.map((n) => n.type),
6337
+ instructions: "Choose the ontology node type that best describes this fact.\n" + manifest.node_types.map((n) => `- ${n.type}: ${n.description}`).join("\n")
6338
+ };
6339
+ const outcomes = await withConcurrency(
6340
+ candidates.map((fact) => async () => {
6341
+ let response;
6342
+ try {
6343
+ response = await provider.classify({ state: classifierStateForFact(fact), questions: { okf_type: question } });
6344
+ } catch {
6345
+ return { fact, kind: "threw" };
6346
+ }
6347
+ const checked = validateClassifierAnswer(response, "okf_type", question);
6348
+ if (!checked.ok) return { fact, kind: "invalid", reason: checked.reason };
6349
+ if (checked.answer.kind !== "choice") return { fact, kind: "invalid", reason: "kind_mismatch" };
6350
+ if (checked.answer.confidence < minConfidence) return { fact, kind: "low_confidence" };
6351
+ return { fact, kind: "accepted", okfType: checked.answer.choice };
6352
+ }),
6353
+ concurrency
6354
+ );
6355
+ const attempted = outcomes.filter((o) => o.kind !== "threw").map((o) => o.fact);
6356
+ const skipped = outcomes.length - attempted.length;
6357
+ const classifications = outcomes.filter((o) => o.kind === "accepted").map((o) => ({ id: o.fact.id, okf_type: o.okfType }));
6358
+ const invalidCount = outcomes.filter((o) => o.kind === "invalid").length;
6359
+ let typed = 0;
6360
+ let failedValidation = 0;
6361
+ let edgesAdded = 0;
6362
+ let aborted = false;
6363
+ if (attempted.length > 0) {
6364
+ const applied = await this._applyOntologyBackfillBatch(
6365
+ entityId,
6366
+ { batch: attempted, classifications, ontologyUpdates: void 0 },
6367
+ now
6368
+ );
6369
+ aborted = applied.abortedOntologyOff;
6370
+ if (!aborted) {
6371
+ typed = applied.typed;
6372
+ failedValidation = invalidCount + applied.failedValidation;
6373
+ edgesAdded = applied.edgesAdded;
6374
+ }
6375
+ }
6376
+ const diagBuffer = new DiagnosticBuffer();
6377
+ const diagBase = { entityId, operation: "ontologyBackfill", trigger: "call" };
6378
+ for (const o of outcomes) {
6379
+ if (o.kind === "low_confidence") {
6380
+ diagBuffer.push({ ...diagBase, code: "classification_low_confidence", detail: { factId: o.fact.id, reason: "below_threshold" } });
6381
+ } else if (o.kind === "invalid") {
6382
+ diagBuffer.push({ ...diagBase, code: "classification_invalid", detail: { factId: o.fact.id, reason: o.reason } });
6383
+ } else if (o.kind === "threw") {
6384
+ diagBuffer.push({ ...diagBase, code: "classification_invalid", detail: { factId: o.fact.id, reason: "classify_threw" } });
6385
+ }
6386
+ }
6387
+ diagBuffer.flush(this.options);
6388
+ const counts = await this.entryRepo.countUntypedByEntityId(entityId, recheckCutoff);
6389
+ if (aborted) {
6390
+ return { scanned: 0, typed: 0, failedValidation: 0, edgesAdded: 0, skipped, remaining: 0, deferred: counts.deferred };
6391
+ }
6392
+ this.searchService.evictCache(entityId);
6393
+ return {
6394
+ scanned: candidates.length,
6395
+ typed,
6396
+ failedValidation,
6397
+ edgesAdded,
6398
+ skipped,
6399
+ remaining: counts.eligible,
6400
+ deferred: counts.deferred
6401
+ };
6402
+ }
6085
6403
  /**
6086
6404
  * Applies one parsed backfill batch in its own transaction. Per-batch rather
6087
6405
  * than one transaction for the pass, so mergeEmergentUpdates semantics and
@@ -6197,7 +6515,7 @@ var MaintenanceService = class {
6197
6515
  * batches that reduce to the same query share one lookup. Caller-owned and
6198
6516
  * per-pass — see the call site in doRunHeal.
6199
6517
  */
6200
- async _selectHealAnchors(entityId, batch, cap = HEAL_MAX_ANCHORS, cache) {
6518
+ async _selectHealAnchors(entityId, batch, cap = HEAL_MAX_ANCHORS, cache, withBodies = false) {
6201
6519
  const query = batch.map((f) => f.title).join(" ").trim();
6202
6520
  if (!query) return [];
6203
6521
  const cached = cache?.get(query);
@@ -6210,7 +6528,7 @@ var MaintenanceService = class {
6210
6528
  const hitIds = hits.map((h) => h.id);
6211
6529
  const anchors = [];
6212
6530
  if (hitIds.length > 0) {
6213
- const rows = await this.entryRepo.findAnchorRowsByIds(entityId, hitIds);
6531
+ const rows = withBodies ? await this.entryRepo.findAnchorRowsByIds(entityId, hitIds, void 0, { withBody: true }) : await this.entryRepo.findAnchorRowsByIds(entityId, hitIds);
6214
6532
  const byId = new Map(rows.map((r) => [r.id, r]));
6215
6533
  for (const id of hitIds) {
6216
6534
  const row = byId.get(id);
@@ -6785,8 +7103,7 @@ var EmbeddingService = class {
6785
7103
  }
6786
7104
  }
6787
7105
  async tryEmbedFact(fact, ctx) {
6788
- const embedFn = this.options.llmProvider.embed;
6789
- if (typeof embedFn !== "function") return { ok: false, kind: "no_provider" };
7106
+ if (typeof this.options.llmProvider.embed !== "function") return { ok: false, kind: "no_provider" };
6790
7107
  let tagsStr;
6791
7108
  if (Array.isArray(fact.tags)) {
6792
7109
  tagsStr = fact.tags.join(" ");
@@ -6803,7 +7120,7 @@ var EmbeddingService = class {
6803
7120
  const text = clip(`${fact.title} ${fact.body} ${tagsStr}`.trim(), maxEmbedChars);
6804
7121
  let float32Vector;
6805
7122
  try {
6806
- const vector = await embedFn(text);
7123
+ const vector = await this.options.llmProvider.embed(text);
6807
7124
  if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
6808
7125
  console.warn(`[WikiMemory] embedFact: embed() returned an invalid vector for ${fact.id}; skipping.`);
6809
7126
  this.reportEmbed(ctx, fact, "embedding_failed", "invalid_vector");
@@ -7062,7 +7379,6 @@ var RetrievalService = class {
7062
7379
  const hybridWeight = options?.hybridWeight ?? config?.hybridWeight;
7063
7380
  const weight = hybridWeight !== void 0 && !Number.isNaN(hybridWeight) ? Math.max(0, Math.min(1, hybridWeight)) : void 0;
7064
7381
  const skipEmbed = weight === 0;
7065
- const embedFn = this.options.llmProvider.embed;
7066
7382
  let facts = [];
7067
7383
  let scoreByFactId;
7068
7384
  if (maxResults === 0) ; else if (trimmedQuery) {
@@ -7073,11 +7389,11 @@ var RetrievalService = class {
7073
7389
  const padLimit = (n) => n >= Number.MAX_SAFE_INTEGER ? n : n + draftPad;
7074
7390
  if (scoredEntityIds.length === 0) {
7075
7391
  usedEmbed = true;
7076
- } else if (!skipEmbed && embedFn) {
7392
+ } else if (!skipEmbed && typeof this.options.llmProvider.embed === "function") {
7077
7393
  let rankerShouldRethrow = false;
7078
7394
  let pendingRankerFallbackError;
7079
7395
  try {
7080
- const queryVec = await embedFn(trimmedQuery);
7396
+ const queryVec = await this.options.llmProvider.embed(trimmedQuery);
7081
7397
  if (queryVec.length === 0 || !queryVec.every((v) => typeof v === "number" && isFinite(v))) {
7082
7398
  throw new Error(
7083
7399
  "embed() returned an empty or non-finite vector. Falling back to keyword search."
@@ -8147,7 +8463,7 @@ var WikiMemory = class {
8147
8463
  this.embeddingService = new EmbeddingService(this.db, this.options, this.entryRepo, this.metadataRepo);
8148
8464
  this.searchService = new SearchService(this.entryRepo);
8149
8465
  this.jobManager = new JobManager(this.prefix);
8150
- this.promptService = new PromptService(options.config?.prompts);
8466
+ this.promptService = new PromptService(options.config?.prompts, resolveGrounding(options.config?.grounding));
8151
8467
  this.ingestionService = new IngestionService(
8152
8468
  this.db,
8153
8469
  this.prefix,