Files
confidence-engine/lib/graph/prompt-builder.js
T
robbond b181c3ea75 fix(reasoning): enforce confirmation-gated decision closure
- reconcileDecisionClosureOwnership normaliser between reconciliation and validation (Boundary B)
- Strips terminal parent updates without explicit user confirmation; preserves all other proposal work
- Strips parent from resolvedUnknownNodeIds bookkeeping on no-confirmation strip
- Restores reconciler-forced resolved→unknown for synthetic updates too
- Prevents hybrid unknown+value states by nulling newValue in all stripping paths
- No-op update created when reconciler synthesized the entry to prevent downstream errors

Prompt:
- Rule #143 rewritten from evidence-sufficiency to explicit-confirmation gate
- Directs model to use possibleInference for directional conclusions when confirmation absent

Regression preservation:
- 60B.43 lifecycle invariant restored via explicit confirmation phrases in fixture answers
- 60B.49 reconciliation auto-add invariant restored under confirmed closure flow
- Test apparatus fixed: structuralActionRequired required with userSupportedMeaning (validator constraint)

New coverage:
- 10 tests for all 60B.79/80 coverage requirements
- 5 prompt alignment tests for Rule #143
2026-08-15 11:34:39 +01:00

190 lines
13 KiB
JavaScript

import {
ConfidenceLevel,
answerResolutionGuidance,
answerSupportCategory,
SituationKind,
SituationRelationship,
SituationStatus,
} from "./schema.js";
const DEFAULT_PROMPT_VERSION = "v0.4";
function formatEnumValues(values) {
return Object.values(values).join(" | ");
}
function formatGraph(graph) {
return JSON.stringify(graph, null, 2);
}
function formatExampleAnswerBlock() {
return [
"Example answer the model must be able to handle without hard-coding output:",
'"The complaint rate fell from 2.0 complaints per 100 units to 1.9 complaints per 100 units."',
"This may justify resolving a rate-related unknown or updating a metric node, but only if the current graph and answer support that proposal.",
].join("\n");
}
export function buildGraphUpdatePrompt({
situationGraph,
previousQuestion,
answer,
promptVersion = DEFAULT_PROMPT_VERSION,
}) {
const nodeKinds = formatEnumValues(SituationKind);
const nodeStatuses = formatEnumValues(SituationStatus);
const edgeRelationships = formatEnumValues(SituationRelationship);
const confidenceLevels = formatEnumValues(ConfidenceLevel);
const supportCategories = formatEnumValues(answerSupportCategory);
const resolutionGuidanceValues = formatEnumValues(answerResolutionGuidance);
return `You are proposing a graph update for Confidence Engine ${promptVersion}.
Return exactly one JSON object matching the GraphUpdate contract.
Return JSON only. Do not include markdown, explanation, or any text before or after the JSON object.
## Current Situation Graph
${formatGraph(situationGraph)}
## Previous Selected Question
${previousQuestion}
## User Answer
${answer}
## Allowed Node Kinds
${nodeKinds}
## Allowed Node Statuses
${nodeStatuses}
## Allowed Edge Relationships
${edgeRelationships}
## Allowed Confidence Values
${confidenceLevels}
## Allowed answerMeaning.supportCategory Values
${supportCategories}
## Allowed answerMeaning.resolutionGuidance Values
${resolutionGuidanceValues}
## Required JSON Field Names
The JSON object must contain exactly these top-level fields:
- addedNodes
- updatedNodes
- addedEdges
- removedEdgeIds
- resolvedUnknownNodeIds
- affectedNodeIds
- selectedQuestion
- answerMeaning
- structuralActionRequired
## Required Shapes
- addedNodes: array of nodes using these exact keys:
id, label, description, kind, status, confidence, value, unit, evidenceIds, dependsOn, affects, parentId, childIds
- updatedNodes: array of node updates using these exact keys:
nodeId, previousStatus, newStatus, previousValue, newValue, reason
- addedEdges: array of edges using these exact keys:
id, fromNodeId, toNodeId, relationship, confidence, description
- removedEdgeIds: array of strings
- resolvedUnknownNodeIds: array of strings
- affectedNodeIds: array of strings
- selectedQuestion: either null or an object using these exact keys:
nodeId, question, reason
- answerMeaning: either null or an object using these exact keys:
userSupportedMeaning, possibleInference, supportCategory, resolutionGuidance
- structuralActionRequired: boolean (required when userSupportedMeaning is populated)
## Proposal Rules
1. Propose changes only. Never return a replacement graph.
2. Preserve unrelated nodes and edges by omitting them from the proposal.
3. Reference existing node IDs when updating an existing concept.
4. Use addedNodes only for genuinely new concepts.
5. Resolve the answered unknown first when the answer supports it.
6. If answerMeaning.userSupportedMeaning contains consequential information or unresolved uncertainty that is not already represented in the graph, you MUST express its effect through structural mutation. This may be an update/refinement of existing structure, resolution of an existing unknown, a genuinely new unknown, or a justified relationship. answerMeaning alone is not sufficient for a successful proposal.
7. Add new unknown nodes only when the answer introduces a new decision, claim, object, measure, dependency, or unresolved term directly relevant to the case.
8. Add at most 3 new unknown nodes.
9. Every new unknown must be directly traceable to the user's answer and its description must state why that uncertainty matters.
9a. In the description of every new unknown, explicitly include a short why-it-matters clause using wording such as because, so that, needed to decide, or matters because.
10. Do not add broad generic discovery questions.
11. Do not add duplicate unknowns.
12. Do not expand unrelated branches.
13. Propagate only through explicit dependencies or relationships already present in the graph, except for the minimal new edges needed to connect validated new unknowns to the relevant answer-derived decision or context node.
13a. For every new unknown node, include at least one added edge that connects it to an existing updated/resolved node or to a newly added non-unknown node introduced from the answer.
14. Do not invent evidence.
15. Do not create unsupported causal edges.
16. When your proposal adds one or more new unresolved unknowns (status !== 'resolved'), you MUST include a selectedQuestion identifying one of those as a candidate unknown node. The engine validates your candidate and retains deterministic prerequisite ordering, fallback selection, and formulation authority; prefer nodes with no unresolved depends_on prerequisites from same-proposal additions. Your candidate does not need to be the highest-scoring unknown — it only needs to be a valid unresolved unknown that exists in the graph or in addedNodes.
17. selectedQuestion.nodeId must reference an unknown node that remains unresolved after applying this same proposal and that exists either already in the graph or in addedNodes. If the proposal resolves all consequential unknowns, selectedQuestion must be null.
18. selectedQuestion.question must be one narrow non-compound question about that one unknown.
19. Do not prioritise downstream implementation, pricing, optimisation, or speculative branches ahead of prerequisite definitions, actors, success criteria, constraints, measures, or terminology.
20. Return selectedQuestion as null only when no consequential unresolved unknown remains.
21. Use empty arrays when there are no changes in a category.
22. Never return null array entries.
23. Never use unknown enum values.
24. Do not change existing IDs.
25. Do not replace the whole graph, and do not restate unchanged graph content inside the proposal.
26. answerMeaning.userSupportedMeaning must state only what the user's answer directly supports.
27. Put any stronger interpretation in answerMeaning.possibleInference, not in userSupportedMeaning.
28. When answerMeaning is present, populate supportCategory with one of the allowed values whenever the user's meaning fits an existing category. Use other when none of the protected categories applies. Do not leave supportCategory null merely because the wording is uncertain.
29. If the answer is conditional or qualified, preserve that qualification explicitly in userSupportedMeaning.
30. If the answer says the user is unsure or does not resolve the distinction, state that uncertainty directly in userSupportedMeaning.
31. If the answer explicitly states a hard constraint, state that directly in userSupportedMeaning.
32. Populate resolutionGuidance when the user's meaning genuinely implies must_remain_unresolved, may_resolve, or must_resolve. Keep it null only when no existing resolution state actually applies.
## Decision Sufficiency Rule
An unresolved decision between options should not remain open merely because some uncertainty still exists.
Keep a decision context unresolved only when you can identify a specific unresolved factor that could materially change which option is preferred.
You may not resolve the decision context unless the user explicitly confirms (using their own words) that no other material uncertainty remains. If evidence appears sufficient but explicit confirmation is absent, preserve your directional conclusion in possibleInference and allow the system to ask the sufficiency confirmation / discovery question rather than closing the parent decision.
## Decision Option Structure Rules
When the user presents mutually exclusive candidate actions for one unresolved choice:
1. Create exactly one node of kind "unknown" to carry the decision question (the existing mechanism). Do not add a separate "decision" node kind. Keep that unknown as-is or create it fresh — do not duplicate it into every option.
2. For each candidate path, create exactly one node of kind "option". The option's label names the alternative; its description states what that alternative entails.
3. Link each option to the decision-context unknown using relationship "contained_in" (edge: option → unknown). Shared membership already implies these options are alternatives of each other — do not add an "alternative_to" edge between options.
4. Attach consequences and evidence to the specific option they belong to via existing edge types ("causes", "may_cause", etc.). Each consequence's fromNodeId explicitly identifies its parent option. Do not collapse all alternatives into one generic trade-off description on a single node.
5. A do-nothing / stay-put / current-state path is an option when it is genuinely one of the alternatives — represent it with kind "option" and label it clearly. Do not introduce an "is_baseline", "is_default", or "is_status_quo" field; baseline meaning is carried by label and consequences alone in this implementation.
## Contract: structuralActionRequired Declaration Rule
When answerMeaning.userSupportedMeaning is populated you MUST set structuralActionRequired to match what your proposal outputs:
- Set structuralActionRequired = true if and only if your proposal adds nodes, updates node status/value, or modifies edges (addedNodes.length > 0, updatedNodes with a meaningful change, or addedEdges.length > 0).
- Set structuralActionRequired = false if and only if your proposal has zero structural mutations — the two sentences are an intentional no-op declaration.
## Additional Guidance
- If the answer only clarifies an existing unknown, prefer updatedNodes and resolvedUnknownNodeIds over creating duplicate nodes.
- When rule #6 applies to explicitly unresolved uncertainty: first check whether an existing unresolved node already represents the same uncertainty; if so, update/refine that existing structure rather than adding a duplicate; if no such node exists, add a new unknown that directly represents the unresolved uncertainty; do not use an edge alone to represent a previously unrepresented uncertainty.
- "Same uncertainty" means the same resolution question: resolving the existing unknown would also resolve the uncertainty introduced by the user's answer. Mere topical overlap (concerning the same topic, object, decision, or domain) is not automatically the same uncertainty. If the new concern can remain unresolved after the existing node is resolved, represent it separately as a distinct uncertainty.
- When an answer resolves an existing unknown, include that existing node ID in resolvedUnknownNodeIds and update that node rather than creating only a parallel observation.
- If the answer creates a more specific decision situation, add the smallest set of new nodes and edges needed to represent that situation and only its most consequential unknowns.
- If you add a new unknown, do not leave it floating: connect it with an added edge to the relevant decision/context node created or updated from the answer.
- If you add a new unknown, its description must do two jobs in one sentence: what is unknown, and why resolving it matters for the case.
- When selectedQuestion is provided, identify the specific material continuation factor as selectedQuestion.nodeId; the engine retains deterministic prerequisite ordering, validation, and formulation authority — it favours your selected node when it has no unresolved depends_on prerequisites from same-proposal additions, falls back to existing deterministic selection otherwise, and may choose a different question if structural constraints require.
- If rule #6 does not apply (the answer contains no user-supported meaning that requires graph progress) and there is no other justification for change, return empty arrays for every category.
- If rule #6 applies but you choose an update/refinement of existing structure, resolve an existing unknown, or add justified new structure, your structural proposal plus answerMeaning together represent the complete response — answerMeaning preserves semantic fidelity while structural mutation handles graph progress; neither replaces the other.
- If you add a new unknown with addedNodes, connect it with at least one addedEdge to an existing updated/resolved node or to a newly added non-unknown node from the answer.
- For answerMeaning.supportCategory, use only these exact values: ${supportCategories}. Use other when none of the protected categories applies.
- For answerMeaning.resolutionGuidance, use only these exact values: ${resolutionGuidanceValues}. Keep it null only when none of those existing resolution states genuinely applies.
## Example Constraint Reminder
${formatExampleAnswerBlock()}
## Output Contract Reminder
Return one JSON object only, with exact field names and exact enum values.
Never include a full graph.
Never include any field other than the contract fields above.
`;
}
export const buildUpdatePrompt = buildGraphUpdatePrompt;