fix(confidence-engine): constrain understanding to supported evidence

This commit is contained in:
2026-09-01 18:17:26 +01:00
parent 161527f66c
commit b647236d44
2 changed files with 472 additions and 255 deletions
+84 -36
View File
@@ -52,31 +52,66 @@ export function filterEligibleFindings(findings) {
* The prompt instructs the model to produce one coherent Current Understanding narrative
* from the provided inputs, without append semantics.
*/
export function buildSynthesisPrompt(situationGraph, findings) {
// Build structured graph representation for the prompt
const graphInfo = {
centralStatement: situationGraph.centralStatement ?? "",
nodes: (situationGraph.nodes ?? []).map((n) => ({
id: n.id,
proposition: n.proposition ?? "",
description: n.description ?? "",
status: n.status ?? null,
confidence: n.confidence ?? null,
})),
edges: (situationGraph.edges ?? []).map((e) => ({
from: e.from ?? null,
to: e.to ?? null,
type: e.type ?? "",
context: e.context ?? "",
})),
};
// Build a structured representation of eligible Findings for the prompt
const KNOWN_SUPPORTED_STATUSES = new Set(["known", "supported"]);
function safeDesc(value) {
return (value && typeof value === "string") ? value : null;
}
/**
* Evidence-authority projection for CU synthesis.
*
* Includes only:
* - centralStatement (framing context — not independent evidence)
* - known nodes (provider-active graph evidence)
* - supported nodes (provider-active graph evidence)
* - eligible Findings (from the findings parameter)
*
* Explicitly excludes from provider-active synthesis input:
* provisional nodes, unknown nodes, resolved nodes, edges,
* activeUnknownNodeId, resolvedNodeIds, currentSummary,
* reasoningState, confidence/control metadata, dependency/control fields.
*/
function buildGraphEvidenceProjection(graphInfo) {
const knownNodes = (graphInfo.nodes ?? [])
.filter((n) => KNOWN_SUPPORTED_STATUSES.has(n.status))
.map((n) => ({
kind: n.kind ?? null,
label: safeDesc(n.label),
description: safeDesc(n.description),
value: n.value ?? null,
unit: n.unit ?? null,
status: n.status ?? null,
}));
const supportedNodes = (graphInfo.nodes ?? [])
.filter((n) => KNOWN_SUPPORTED_STATUSES.has(n.status))
.filter((n) => n.status !== "known")
.map((n) => ({
kind: n.kind ?? null,
label: safeDesc(n.label),
description: safeDesc(n.description),
value: n.value ?? null,
unit: n.unit ?? null,
status: n.status ?? null,
}));
const centralStatement = safeDesc(graphInfo.centralStatement) || "";
return { centralStatement, knownNodes, supportedNodes };
}
export function buildSynthesisPrompt(situationGraph, findings) {
// Structured evidence projection for the model
const evidence = buildGraphEvidenceProjection(situationGraph);
// Eligible Finding sections (preserve existing agree vs null distinction)
const findingsSections = [];
if (findings.length > 0) {
const agreed = findings.filter((f) => f.userDisposition === "agree");
const provisional = findings.filter(
const working = findings.filter(
(f) => f.userDisposition === null
);
@@ -90,10 +125,10 @@ export function buildSynthesisPrompt(situationGraph, findings) {
});
}
if (provisional.length > 0) {
if (working.length > 0) {
findingsSections.push({
label: "Provisional Findings (working interpretation)",
items: provisional.map((f) => ({
label: "Working Premises",
items: working.map((f) => ({
proposition: f.proposition,
id: f.id ?? null,
})),
@@ -101,14 +136,26 @@ export function buildSynthesisPrompt(situationGraph, findings) {
}
}
// Human-readable node and edge representations for the prompt
const nodesSection = graphInfo.nodes.length > 0 ? `\nNodes:\n${graphInfo.nodes.map((n) => ` Node(${n.id}): ${n.proposition}${n.description ? `${n.description}` : ""}${n.status ? ` [${n.status}]` : ""}`).join("\n")}` : "";
const edgesSection = graphInfo.edges.length > 0 ? `\nEdges:\n${graphInfo.edges.map((e) => ` Edge(${e.from}${e.to}, type=${e.type}): ${e.context || "(no context)"}`).join("\n")}` : "";
// Human-readable node display for the prompt
function formatNodeSection(title, nodes) {
if (!nodes || nodes.length === 0) return "";
const items = nodes.map(
(n) => ` ${title}: kind=${n.kind}, label="${n.label}", value=${n.value ? n.value + (n.unit ? " (" + n.unit + ")" : "") : null}${n.description ?? "(no description)"} [${n.status}]`
);
return "\n" + items.join("\n");
}
const knownSection = formatNodeSection("Known", evidence.knownNodes);
const supportedItem = formatNodeSection("Supported", evidence.supportedNodes);
const prompt = `You are producing a Current Understanding narrative from investigation evidence.
Canonical Situation Graph:
${JSON.stringify(graphInfo, null, 2)}${nodesSection}${edgesSection}
Situation Framing:
${evidence.centralStatement ? " Central Statement: " + evidence.centralStatement : "(none)"}
Provider-Active Evidence:
Known Facts:${knownSection}
Supported Inferences:${supportedItem}
Eligible Findings:
${findingsSections.length > 0
@@ -116,14 +163,15 @@ ${findingsSections.length > 0
: "(none)"}
Rules for this synthesis:
1. Produce exactly ONE coherent narrative paragraph (or short multi-sentence paragraph) that represents the Current Understanding of the situation.
2. Synthesize all provided evidence into a unified understanding — do not list or append findings. The result should read as a natural summary, not a bullet list.
3. This is a FRESH synthesis from the complete set of inputs above. Do NOT treat any previous Current Understanding as input or authority. Do NOT append to prior summaries.
4. Use only information present in the Situation context and Eligible Findings above.
5. If no eligible Findings are provided, synthesize from the Situation context alone.
6. Return ONLY a JSON object with this exact structure:
1. Current Understanding describes only established or supported understanding from the evidence supplied here.
2. Do not introduce or describe open questions, unresolved uncertainties, assumptions, provisional hypotheses, speculative explanations, or future investigation needs.
3. Produce exactly ONE coherent narrative paragraph (or short multi-sentence paragraph) that represents the Current Understanding of the situation.
4. Synthesize all provided evidence into a unified understanding — do not list or append findings. The result should read as a natural summary, not a bullet list.
5. This is a FRESH synthesis from the complete set of inputs above. Do NOT treat any previous Current Understanding as input or authority. Do NOT append to prior summaries.
6. Use only information present in the evidence above. The centralStatement is framing context, not independent evidence.
7. Return ONLY a JSON object with this exact structure:
{"currentUnderstanding": "your narrative here"}
7. The currentUnderstanding value must be a non-empty string.
8. The currentUnderstanding value must be a non-empty string.
Return ONLY the JSON object. No markdown, no explanation, no preamble.`;
@@ -204,7 +252,7 @@ export async function synthesizeCurrentUnderstanding(
// 2. Eligibility normalization (domain seam responsibility)
const eligibleFindings = filterEligibleFindings(allFindings);
// 3. Build synthesis prompt (uses full canonical graph, not just centralStatement)
// 3. Build synthesis prompt (uses evidence-authority projection, not raw graph)
const prompt = buildSynthesisPrompt(situationGraph, eligibleFindings);
// 4. Resolve provider — DI fallback to configured default