@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/README.md +71 -0
- package/dist/{chunk-G5OR2VWD.mjs → chunk-YBWE26XR.mjs} +363 -48
- package/dist/chunk-YBWE26XR.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +364 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -5
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-Dmh1kfkd.d.mts → testing-d1SrpDvg.d.mts} +117 -3
- package/dist/{testing-Dmh1kfkd.d.ts → testing-d1SrpDvg.d.ts} +117 -3
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +360 -45
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-G5OR2VWD.mjs.map +0 -1
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
|
|
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
|
-
|
|
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
|
|
4451
|
-
if (
|
|
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:
|
|
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(
|
|
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
|
|
5609
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
|
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,
|