1232 lines
46 KiB
JavaScript
1232 lines
46 KiB
JavaScript
/**
|
|
* Situation Graph Case Orchestrator — manages the lifecycle of a case.
|
|
* startCase builds initial graph from analysis; updateCase applies answers.
|
|
*/
|
|
|
|
import { analyseScenario } from "../analysis.js";
|
|
import { assertConfig } from "../config.js";
|
|
import { getProvider } from "../llm/provider.js";
|
|
import {
|
|
makeGraph,
|
|
startCaseRequestSchema,
|
|
situationGraphSchema,
|
|
updateCaseRequestSchema,
|
|
} from "./schema.js";
|
|
import { buildInitialGraph, describeGraph } from "./builder.js";
|
|
import {
|
|
applyValidatedProposal,
|
|
determineGraphBackedQuestion,
|
|
} from "./apply-proposal.js";
|
|
import { buildGraphUpdatePrompt } from "./prompt-builder.js";
|
|
import assessInvestigationState from "../assessment/investigation-state-assessor.js";
|
|
import {
|
|
buildReasoningState,
|
|
formulateQuestion,
|
|
formulateTieResolutionQuestion,
|
|
} from "./question-formulator.js";
|
|
import { parseGraphUpdateProposal } from "./update-proposal.js";
|
|
import {
|
|
explainUnknownSelection,
|
|
selectActiveUnknownCandidate,
|
|
validateGraphReferences,
|
|
} from "./utils.js";
|
|
import { validateFindings, produceFindingInformedSummary } from "./finding-helpers.js";
|
|
import { prepareCompletedEpisode } from "./episode-preparation.js";
|
|
import { buildEpisodeAwareGraphPrompt } from "./prompt-builder-episode.js";
|
|
|
|
function toValidationErrors(error) {
|
|
return (
|
|
error?.errors?.map((issue) => ({
|
|
path: issue.path,
|
|
message: issue.message,
|
|
code: issue.code,
|
|
})) ?? [{ message: "Validation failed" }]
|
|
);
|
|
}
|
|
|
|
function buildDiagnostics({
|
|
analysis,
|
|
graph,
|
|
graphReferenceValidation,
|
|
unknownSelectionExplanation,
|
|
reconstructionQuestion,
|
|
reconstructionQuestionAccepted,
|
|
reconstructionQuestionRejectionReasons,
|
|
finalGraphBackedQuestion,
|
|
selectedUnknownNodeId,
|
|
decompositionApplied,
|
|
questionComplexityAssessment,
|
|
answerabilityAssessment,
|
|
independentlyAnswerable,
|
|
prerequisiteConceptCount,
|
|
decompositionTriggeredByAnswerability,
|
|
decompositionReason,
|
|
selectedContainerUnknown,
|
|
selectedChildUnknown,
|
|
reasoningPattern,
|
|
questionFamily,
|
|
allowedQuestionFamilies,
|
|
rejectedQuestionFamilies,
|
|
selectedQuestionTemplate,
|
|
reasoningPatternReason,
|
|
reasoningPatternValidation,
|
|
patternCompatibleNodeCount,
|
|
incompatibleNodeIds,
|
|
compatibilityFailures,
|
|
replacementActions,
|
|
graphReasoningIntegrity,
|
|
noQuestionReason,
|
|
}) {
|
|
return {
|
|
promptVersion: analysis?.promptVersion ?? null,
|
|
modelName: analysis?.modelName ?? null,
|
|
responseDurationMs: analysis?.responseDurationMs ?? null,
|
|
validationStatus: analysis?.validationStatus ?? "invalid",
|
|
nodeCount: graph?.nodes?.length ?? 0,
|
|
edgeCount: graph?.edges?.length ?? 0,
|
|
graphReferenceValidation,
|
|
compatibilityApplied: analysis?.compatibilityApplied ?? false,
|
|
compatibilityChanges: analysis?.compatibilityChanges ?? [],
|
|
compatibilityWarnings: analysis?.compatibilityWarnings ?? [],
|
|
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
|
|
reconstructionQuestion: reconstructionQuestion ?? null,
|
|
reconstructionQuestionAccepted: reconstructionQuestionAccepted ?? null,
|
|
reconstructionQuestionRejectionReasons:
|
|
reconstructionQuestionRejectionReasons ?? [],
|
|
finalGraphBackedQuestion: finalGraphBackedQuestion ?? null,
|
|
selectedUnknownNodeId: selectedUnknownNodeId ?? null,
|
|
decompositionApplied: decompositionApplied ?? false,
|
|
questionComplexityAssessment: questionComplexityAssessment ?? null,
|
|
answerabilityAssessment: answerabilityAssessment ?? null,
|
|
independentlyAnswerable: independentlyAnswerable ?? null,
|
|
prerequisiteConceptCount: prerequisiteConceptCount ?? null,
|
|
decompositionTriggeredByAnswerability:
|
|
decompositionTriggeredByAnswerability ?? false,
|
|
decompositionReason: decompositionReason ?? null,
|
|
selectedContainerUnknown: selectedContainerUnknown ?? null,
|
|
selectedChildUnknown: selectedChildUnknown ?? null,
|
|
reasoningPattern: reasoningPattern ?? null,
|
|
questionFamily: questionFamily ?? null,
|
|
allowedQuestionFamilies: allowedQuestionFamilies ?? [],
|
|
rejectedQuestionFamilies: rejectedQuestionFamilies ?? [],
|
|
selectedQuestionTemplate: selectedQuestionTemplate ?? null,
|
|
reasoningPatternReason: reasoningPatternReason ?? null,
|
|
reasoningPatternValidation: reasoningPatternValidation ?? null,
|
|
patternCompatibleNodeCount: patternCompatibleNodeCount ?? 0,
|
|
incompatibleNodeIds: incompatibleNodeIds ?? [],
|
|
compatibilityFailures: compatibilityFailures ?? [],
|
|
replacementActions: replacementActions ?? [],
|
|
graphReasoningIntegrity: graphReasoningIntegrity ?? null,
|
|
noQuestionReason: noQuestionReason ?? null,
|
|
};
|
|
}
|
|
|
|
function fallbackStartCaseReasoningPatternValidation(
|
|
selectedQuestion,
|
|
existingValidation,
|
|
) {
|
|
if (existingValidation) {
|
|
return existingValidation;
|
|
}
|
|
|
|
if (!selectedQuestion?.reasoningPattern) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
activePattern: selectedQuestion.reasoningPattern,
|
|
valid: Boolean(selectedQuestion.question),
|
|
reason: selectedQuestion.question
|
|
? "Initial graph-backed selection produced a reasoning-pattern-compatible question."
|
|
: "Initial graph-backed selection did not produce a valid question for the inferred reasoning pattern.",
|
|
};
|
|
}
|
|
|
|
function buildUnknownSelectionDiagnostics(
|
|
graph,
|
|
resolvedNodeIds = [],
|
|
selectedQuestion = null,
|
|
) {
|
|
const explanation = explainUnknownSelection(graph, resolvedNodeIds);
|
|
if (explanation.status === "ambiguous") {
|
|
return {
|
|
...explanation,
|
|
tieResolutionQuestion:
|
|
selectedQuestion?.selectionStatus === "ambiguous"
|
|
? selectedQuestion.question
|
|
: formulateTieResolutionQuestion({ graph }).question,
|
|
alphabeticalUsedAsReasoning: false,
|
|
};
|
|
}
|
|
|
|
return explanation;
|
|
}
|
|
|
|
function buildUpdateDiagnostics({
|
|
promptVersion,
|
|
modelName,
|
|
responseDurationMs,
|
|
normalisationsApplied,
|
|
graph,
|
|
graphReferenceValidation,
|
|
selectedQuestion,
|
|
unknownSelectionExplanation,
|
|
previousReasoningState,
|
|
reasoningState,
|
|
resolvedReasoningNodeIds,
|
|
emergentReasoningNodeCreated,
|
|
emergentReasoningNodeId,
|
|
emergentReasoningNodeReason,
|
|
atomicityAssessment,
|
|
atomicityDecisionReason,
|
|
decompositionDepth,
|
|
decompositionAttempted,
|
|
decompositionAccepted,
|
|
decompositionStoppedReason,
|
|
proposedChildCount,
|
|
acceptedChildCount,
|
|
rejectedChildren,
|
|
selectedChildNodeId,
|
|
childQualitySummary,
|
|
propagationPerformed,
|
|
resolvedChildNodeId,
|
|
parentNodeId,
|
|
parentStatusBefore,
|
|
parentStatusAfter,
|
|
parentConfidenceBefore,
|
|
parentConfidenceAfter,
|
|
evidenceConfidenceBefore,
|
|
evidenceConfidenceAfter,
|
|
completenessBefore,
|
|
completenessAfter,
|
|
conclusionConfidenceBefore,
|
|
conclusionConfidenceAfter,
|
|
resolvedDirectChildren,
|
|
unresolvedDirectChildren,
|
|
contradictoryDirectChildren,
|
|
corroboratingBranchCount,
|
|
conflictingBranchCount,
|
|
duplicateEvidenceCount,
|
|
independentBranchCount,
|
|
interactionSummary,
|
|
confidenceCapReason,
|
|
ancestorPropagationStoppedReason,
|
|
affectedAncestorIds,
|
|
nextSelectedSibling,
|
|
parentResolved,
|
|
decompositionPerformed,
|
|
childUnknownCount,
|
|
childNodeIds,
|
|
atomicityReason,
|
|
questionComplexityAccepted,
|
|
primaryConceptCount,
|
|
cognitiveLoad,
|
|
complexityReasons,
|
|
decompositionTriggeredByQuestionComplexity,
|
|
previousQuestion,
|
|
finalQuestion,
|
|
selectedUnknownBefore,
|
|
selectedUnknownAfter,
|
|
plainLanguageNormalisations,
|
|
reasoningPattern,
|
|
questionFamily,
|
|
allowedQuestionFamilies,
|
|
rejectedQuestionFamilies,
|
|
selectedQuestionTemplate,
|
|
reasoningPatternReason,
|
|
unresolvedCandidateCount,
|
|
eligibleCandidateCount,
|
|
candidateNodeIds,
|
|
resolvedCurrentTurnNodeIds,
|
|
noQuestionReason,
|
|
reasoningPatternValidation,
|
|
patternCompatibleNodeCount,
|
|
incompatibleNodeIds,
|
|
compatibilityFailures,
|
|
replacementActions,
|
|
graphReasoningIntegrity,
|
|
}) {
|
|
return {
|
|
promptVersion: promptVersion ?? "v0.4",
|
|
modelName: modelName ?? null,
|
|
responseDurationMs: responseDurationMs ?? null,
|
|
validationStatus: "valid",
|
|
nodeCount: graph?.nodes?.length ?? 0,
|
|
edgeCount: graph?.edges?.length ?? 0,
|
|
graphReferenceValidation: graphReferenceValidation ?? {
|
|
valid: true,
|
|
errors: [],
|
|
},
|
|
normalisationsApplied: normalisationsApplied ?? [],
|
|
investigationStrategy:
|
|
selectedQuestion?.investigationStrategy ??
|
|
selectedQuestion?.strategy ??
|
|
null,
|
|
previousComparabilityStatus:
|
|
previousReasoningState?.comparabilityStatus ?? null,
|
|
comparabilityStatus: reasoningState?.comparabilityStatus ?? null,
|
|
relationshipStatus: reasoningState?.relationshipStatus ?? null,
|
|
relationshipAssessed: reasoningState?.relationshipAssessed ?? null,
|
|
reasoningStagesBefore: previousReasoningState?.reasoningStages ?? [],
|
|
reasoningStagesAfter: reasoningState?.reasoningStages ?? [],
|
|
resolvedReasoningNodeIds: resolvedReasoningNodeIds ?? [],
|
|
emergentReasoningNodeCreated: emergentReasoningNodeCreated ?? false,
|
|
emergentReasoningNodeId: emergentReasoningNodeId ?? null,
|
|
emergentReasoningNodeReason: emergentReasoningNodeReason ?? null,
|
|
atomicityAssessment: atomicityAssessment ?? null,
|
|
atomicityDecisionReason: atomicityDecisionReason ?? null,
|
|
decompositionDepth: decompositionDepth ?? 0,
|
|
decompositionAttempted: decompositionAttempted ?? false,
|
|
decompositionAccepted: decompositionAccepted ?? false,
|
|
decompositionStoppedReason: decompositionStoppedReason ?? null,
|
|
proposedChildCount: proposedChildCount ?? 0,
|
|
acceptedChildCount: acceptedChildCount ?? 0,
|
|
rejectedChildren: rejectedChildren ?? [],
|
|
selectedChildNodeId: selectedChildNodeId ?? null,
|
|
childQualitySummary: childQualitySummary ?? [],
|
|
propagationPerformed: propagationPerformed ?? false,
|
|
resolvedChildNodeId: resolvedChildNodeId ?? null,
|
|
parentNodeId: parentNodeId ?? null,
|
|
parentStatusBefore: parentStatusBefore ?? null,
|
|
parentStatusAfter: parentStatusAfter ?? null,
|
|
parentConfidenceBefore: parentConfidenceBefore ?? null,
|
|
parentConfidenceAfter: parentConfidenceAfter ?? null,
|
|
evidenceConfidenceBefore: evidenceConfidenceBefore ?? null,
|
|
evidenceConfidenceAfter: evidenceConfidenceAfter ?? null,
|
|
completenessBefore: completenessBefore ?? null,
|
|
completenessAfter: completenessAfter ?? null,
|
|
conclusionConfidenceBefore: conclusionConfidenceBefore ?? null,
|
|
conclusionConfidenceAfter: conclusionConfidenceAfter ?? null,
|
|
resolvedDirectChildren: resolvedDirectChildren ?? 0,
|
|
unresolvedDirectChildren: unresolvedDirectChildren ?? 0,
|
|
contradictoryDirectChildren: contradictoryDirectChildren ?? 0,
|
|
corroboratingBranchCount: corroboratingBranchCount ?? 0,
|
|
conflictingBranchCount: conflictingBranchCount ?? 0,
|
|
duplicateEvidenceCount: duplicateEvidenceCount ?? 0,
|
|
independentBranchCount: independentBranchCount ?? 0,
|
|
interactionSummary: interactionSummary ?? null,
|
|
confidenceCapReason: confidenceCapReason ?? null,
|
|
ancestorPropagationStoppedReason: ancestorPropagationStoppedReason ?? null,
|
|
affectedAncestorIds: affectedAncestorIds ?? [],
|
|
nextSelectedSibling: nextSelectedSibling ?? null,
|
|
parentResolved: parentResolved ?? false,
|
|
decompositionPerformed: decompositionPerformed ?? false,
|
|
childUnknownCount: childUnknownCount ?? 0,
|
|
childNodeIds: childNodeIds ?? [],
|
|
atomicityReason: atomicityReason ?? null,
|
|
questionComplexityAccepted: questionComplexityAccepted ?? null,
|
|
primaryConceptCount: primaryConceptCount ?? null,
|
|
cognitiveLoad: cognitiveLoad ?? null,
|
|
complexityReasons: complexityReasons ?? [],
|
|
decompositionTriggeredByQuestionComplexity:
|
|
decompositionTriggeredByQuestionComplexity ?? false,
|
|
previousQuestion: previousQuestion ?? null,
|
|
finalQuestion: finalQuestion ?? null,
|
|
selectedUnknownBefore: selectedUnknownBefore ?? null,
|
|
selectedUnknownAfter: selectedUnknownAfter ?? null,
|
|
plainLanguageNormalisations: plainLanguageNormalisations ?? [],
|
|
reasoningPattern: reasoningPattern ?? null,
|
|
questionFamily: questionFamily ?? null,
|
|
allowedQuestionFamilies: allowedQuestionFamilies ?? [],
|
|
rejectedQuestionFamilies: rejectedQuestionFamilies ?? [],
|
|
selectedQuestionTemplate: selectedQuestionTemplate ?? null,
|
|
reasoningPatternReason: reasoningPatternReason ?? null,
|
|
unresolvedCandidateCount: unresolvedCandidateCount ?? 0,
|
|
eligibleCandidateCount: eligibleCandidateCount ?? 0,
|
|
candidateNodeIds: candidateNodeIds ?? [],
|
|
resolvedCurrentTurnNodeIds: resolvedCurrentTurnNodeIds ?? [],
|
|
noQuestionReason: noQuestionReason ?? null,
|
|
reasoningPatternValidation: reasoningPatternValidation ?? null,
|
|
patternCompatibleNodeCount: patternCompatibleNodeCount ?? 0,
|
|
incompatibleNodeIds: incompatibleNodeIds ?? [],
|
|
compatibilityFailures: compatibilityFailures ?? [],
|
|
replacementActions: replacementActions ?? [],
|
|
graphReasoningIntegrity: graphReasoningIntegrity ?? null,
|
|
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
|
|
};
|
|
}
|
|
|
|
export async function startCase(body, dependencies = {}) {
|
|
const parsedRequest = startCaseRequestSchema.safeParse(body);
|
|
|
|
if (!parsedRequest.success) {
|
|
return {
|
|
success: false,
|
|
error: "Invalid start-case request",
|
|
validationErrors: toValidationErrors(parsedRequest.error),
|
|
statusCode: 400,
|
|
};
|
|
}
|
|
|
|
const { scenario, promptVersion } = parsedRequest.data;
|
|
const analysis = await analyseScenario(scenario, { promptVersion });
|
|
|
|
if (!analysis.success) {
|
|
return {
|
|
success: false,
|
|
error: analysis.error ?? "Scenario analysis failed",
|
|
diagnostics: buildDiagnostics({
|
|
analysis,
|
|
graph: null,
|
|
graphReferenceValidation: null,
|
|
}),
|
|
analysisErrors: analysis.errors ?? undefined,
|
|
validationIssues: analysis.validationIssues ?? undefined,
|
|
rawResponse: analysis.rawResponse ?? undefined,
|
|
statusCode: Number(analysis.statusCode) || 502,
|
|
};
|
|
}
|
|
|
|
const initialGraph = buildInitialGraph({
|
|
reconstruction: analysis.reconstruction,
|
|
evidence: analysis.evidence,
|
|
});
|
|
|
|
const currentSummary = describeGraph(initialGraph);
|
|
const initialSituationGraph = makeGraph({
|
|
centralStatement: scenario,
|
|
nodes: initialGraph.nodes,
|
|
edges: initialGraph.edges,
|
|
activeUnknownNodeId: null,
|
|
resolvedNodeIds: [],
|
|
currentSummary,
|
|
reasoningState: buildReasoningState({
|
|
centralStatement: scenario,
|
|
nodes: initialGraph.nodes,
|
|
edges: initialGraph.edges,
|
|
resolvedNodeIds: [],
|
|
}),
|
|
});
|
|
|
|
situationGraphSchema.parse(initialSituationGraph);
|
|
|
|
const graphReferenceValidation = validateGraphReferences(
|
|
initialSituationGraph,
|
|
);
|
|
const provider = dependencies.provider ?? getProvider();
|
|
const modelName = dependencies.modelName ?? analysis?.modelName ?? null;
|
|
const initialQuestionResult = await determineGraphBackedQuestion({
|
|
situationGraph: initialSituationGraph,
|
|
provider,
|
|
modelName,
|
|
});
|
|
const situationGraph = initialQuestionResult.success
|
|
? initialQuestionResult.updatedSituationGraph
|
|
: initialSituationGraph;
|
|
const selectedQuestion = initialQuestionResult.success
|
|
? initialQuestionResult.selectedQuestion
|
|
: null;
|
|
const unknownSelectionExplanation = buildUnknownSelectionDiagnostics(
|
|
situationGraph,
|
|
[],
|
|
selectedQuestion,
|
|
);
|
|
if (!graphReferenceValidation.valid) {
|
|
return {
|
|
success: false,
|
|
error: "Situation graph reference validation failed",
|
|
diagnostics: buildDiagnostics({
|
|
analysis,
|
|
graph: situationGraph,
|
|
graphReferenceValidation,
|
|
unknownSelectionExplanation,
|
|
reconstructionQuestion: analysis.nextQuestion?.question ?? null,
|
|
reconstructionQuestionAccepted: false,
|
|
reconstructionQuestionRejectionReasons:
|
|
analysis.nextQuestion?.question != null
|
|
? [
|
|
"reconstruction_question_not_authoritative",
|
|
"graph_backed_pipeline_required",
|
|
]
|
|
: [],
|
|
finalGraphBackedQuestion: selectedQuestion?.question ?? null,
|
|
selectedUnknownNodeId:
|
|
initialQuestionResult.selectedUnknownAfter ?? null,
|
|
decompositionApplied:
|
|
initialQuestionResult.decompositionPerformed ?? false,
|
|
questionComplexityAssessment:
|
|
initialQuestionResult.questionComplexityAssessment ?? null,
|
|
answerabilityAssessment:
|
|
initialQuestionResult.answerabilityAssessment ?? null,
|
|
independentlyAnswerable:
|
|
initialQuestionResult.independentlyAnswerable ?? null,
|
|
prerequisiteConceptCount:
|
|
initialQuestionResult.prerequisiteConceptCount ?? null,
|
|
decompositionTriggeredByAnswerability:
|
|
initialQuestionResult.decompositionTriggeredByAnswerability ?? false,
|
|
decompositionReason:
|
|
initialQuestionResult.selectedQuestion?.reason ?? null,
|
|
selectedContainerUnknown:
|
|
initialQuestionResult.selectedContainerUnknown ?? null,
|
|
selectedChildUnknown:
|
|
initialQuestionResult.selectedChildUnknown ?? null,
|
|
reasoningPattern:
|
|
initialQuestionResult.selectedQuestion?.reasoningPattern ?? null,
|
|
questionFamily:
|
|
initialQuestionResult.selectedQuestion?.questionFamily ?? null,
|
|
allowedQuestionFamilies:
|
|
initialQuestionResult.selectedQuestion?.allowedQuestionFamilies ?? [],
|
|
rejectedQuestionFamilies:
|
|
initialQuestionResult.selectedQuestion?.rejectedQuestionFamilies ??
|
|
[],
|
|
selectedQuestionTemplate:
|
|
initialQuestionResult.selectedQuestion?.selectedQuestionTemplate ??
|
|
null,
|
|
reasoningPatternReason:
|
|
initialQuestionResult.selectedQuestion?.reasoningPatternReason ??
|
|
null,
|
|
reasoningPatternValidation: fallbackStartCaseReasoningPatternValidation(
|
|
initialQuestionResult.selectedQuestion,
|
|
initialQuestionResult.reasoningPatternValidation,
|
|
),
|
|
patternCompatibleNodeCount:
|
|
initialQuestionResult.patternCompatibleNodeCount ?? 0,
|
|
incompatibleNodeIds: initialQuestionResult.incompatibleNodeIds ?? [],
|
|
compatibilityFailures:
|
|
initialQuestionResult.compatibilityFailures ?? [],
|
|
replacementActions: initialQuestionResult.replacementActions ?? [],
|
|
graphReasoningIntegrity:
|
|
initialQuestionResult.graphReasoningIntegrity ?? null,
|
|
noQuestionReason: initialQuestionResult.noQuestionReason ?? null,
|
|
}),
|
|
validationErrors: graphReferenceValidation.errors,
|
|
statusCode: 500,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
summary: analysis.reconstruction?.summary ?? null,
|
|
reconstruction: analysis.reconstruction,
|
|
situationGraph,
|
|
selectedQuestion,
|
|
diagnostics: buildDiagnostics({
|
|
analysis,
|
|
graph: situationGraph,
|
|
graphReferenceValidation,
|
|
unknownSelectionExplanation,
|
|
reconstructionQuestion: analysis.nextQuestion?.question ?? null,
|
|
reconstructionQuestionAccepted: false,
|
|
reconstructionQuestionRejectionReasons:
|
|
analysis.nextQuestion?.question != null
|
|
? [
|
|
"reconstruction_question_not_authoritative",
|
|
"graph_backed_pipeline_required",
|
|
]
|
|
: [],
|
|
finalGraphBackedQuestion: selectedQuestion?.question ?? null,
|
|
selectedUnknownNodeId: initialQuestionResult.selectedUnknownAfter ?? null,
|
|
decompositionApplied:
|
|
initialQuestionResult.decompositionPerformed ?? false,
|
|
questionComplexityAssessment:
|
|
initialQuestionResult.questionComplexityAssessment ?? null,
|
|
answerabilityAssessment:
|
|
initialQuestionResult.answerabilityAssessment ?? null,
|
|
independentlyAnswerable:
|
|
initialQuestionResult.independentlyAnswerable ?? null,
|
|
prerequisiteConceptCount:
|
|
initialQuestionResult.prerequisiteConceptCount ?? null,
|
|
decompositionTriggeredByAnswerability:
|
|
initialQuestionResult.decompositionTriggeredByAnswerability ?? false,
|
|
decompositionReason:
|
|
initialQuestionResult.selectedQuestion?.reason ?? null,
|
|
selectedContainerUnknown:
|
|
initialQuestionResult.selectedContainerUnknown ?? null,
|
|
selectedChildUnknown: initialQuestionResult.selectedChildUnknown ?? null,
|
|
reasoningPattern:
|
|
initialQuestionResult.selectedQuestion?.reasoningPattern ?? null,
|
|
questionFamily:
|
|
initialQuestionResult.selectedQuestion?.questionFamily ?? null,
|
|
allowedQuestionFamilies:
|
|
initialQuestionResult.selectedQuestion?.allowedQuestionFamilies ?? [],
|
|
rejectedQuestionFamilies:
|
|
initialQuestionResult.selectedQuestion?.rejectedQuestionFamilies ?? [],
|
|
selectedQuestionTemplate:
|
|
initialQuestionResult.selectedQuestion?.selectedQuestionTemplate ??
|
|
null,
|
|
reasoningPatternReason:
|
|
initialQuestionResult.selectedQuestion?.reasoningPatternReason ?? null,
|
|
reasoningPatternValidation: fallbackStartCaseReasoningPatternValidation(
|
|
initialQuestionResult.selectedQuestion,
|
|
initialQuestionResult.reasoningPatternValidation,
|
|
),
|
|
patternCompatibleNodeCount:
|
|
initialQuestionResult.patternCompatibleNodeCount ?? 0,
|
|
incompatibleNodeIds: initialQuestionResult.incompatibleNodeIds ?? [],
|
|
compatibilityFailures: initialQuestionResult.compatibilityFailures ?? [],
|
|
replacementActions: initialQuestionResult.replacementActions ?? [],
|
|
graphReasoningIntegrity:
|
|
initialQuestionResult.graphReasoningIntegrity ?? null,
|
|
noQuestionReason: initialQuestionResult.noQuestionReason ?? null,
|
|
}),
|
|
assessment: assessInvestigationState({
|
|
situationGraph,
|
|
selectedQuestion,
|
|
noQuestionReason: initialQuestionResult.noQuestionReason ?? null,
|
|
diagnostics: {
|
|
promptVersion,
|
|
modelName: analysis?.modelName ?? null,
|
|
responseDurationMs: analysis?.responseDurationMs ?? null,
|
|
validationStatus: analysis?.validationStatus ?? "invalid",
|
|
nodeCount: situationGraph?.nodes?.length ?? 0,
|
|
edgeCount: situationGraph?.edges?.length ?? 0,
|
|
reasoningPattern: initialQuestionResult.selectedQuestion?.reasoningPattern ?? null,
|
|
},
|
|
}),
|
|
};
|
|
}
|
|
|
|
export async function updateCase() {
|
|
return updateCaseWithDependencies(...arguments);
|
|
}
|
|
|
|
function sanitiseErrorMessage(error, fallbackMessage) {
|
|
if (typeof error?.message === "string" && error.message.trim().length > 0) {
|
|
return error.message;
|
|
}
|
|
|
|
return fallbackMessage;
|
|
}
|
|
|
|
async function updateCaseWithDependencies(body, dependencies = {}) {
|
|
const parsedRequest = updateCaseRequestSchema.safeParse(body);
|
|
|
|
if (!parsedRequest.success) {
|
|
return {
|
|
success: false,
|
|
stage: "request_validation",
|
|
error: "Invalid update-case request",
|
|
validationErrors: toValidationErrors(parsedRequest.error),
|
|
statusCode: 400,
|
|
};
|
|
}
|
|
|
|
const { situationGraph, previousQuestion, answer, promptVersion, findings: incomingFindings } =
|
|
parsedRequest.data;
|
|
|
|
const graphSchemaValidation = situationGraphSchema.safeParse(situationGraph);
|
|
const graphReferenceValidation = validateGraphReferences(situationGraph);
|
|
|
|
if (!graphSchemaValidation.success || !graphReferenceValidation.valid) {
|
|
return {
|
|
success: false,
|
|
stage: "graph_validation",
|
|
error: "Invalid situation graph",
|
|
graphValidationErrors: [
|
|
...(!graphSchemaValidation.success
|
|
? toValidationErrors(graphSchemaValidation.error)
|
|
: []),
|
|
...(!graphReferenceValidation.valid
|
|
? graphReferenceValidation.errors
|
|
: []),
|
|
],
|
|
statusCode: 400,
|
|
};
|
|
}
|
|
|
|
const buildPrompt =
|
|
dependencies.buildGraphUpdatePrompt ?? buildGraphUpdatePrompt;
|
|
const parseProposal =
|
|
dependencies.parseGraphUpdateProposal ?? parseGraphUpdateProposal;
|
|
const applyProposalUpdate =
|
|
dependencies.applyValidatedProposal ?? applyValidatedProposal;
|
|
const shouldApplyProposal = dependencies.applyProposal === true;
|
|
|
|
let modelName = null;
|
|
let rawResponse;
|
|
const startedAt = Date.now();
|
|
|
|
try {
|
|
const config = dependencies.config ?? assertConfig();
|
|
modelName = config.OLLAMA_MODEL;
|
|
|
|
const prompt = buildPrompt({
|
|
situationGraph,
|
|
previousQuestion,
|
|
answer,
|
|
promptVersion,
|
|
});
|
|
|
|
const provider = dependencies.provider ?? getProvider();
|
|
rawResponse = await provider.generateReconstruction(prompt, modelName);
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
stage: "provider",
|
|
error: "Graph update proposal generation failed",
|
|
providerErrors: [
|
|
sanitiseErrorMessage(
|
|
error,
|
|
"Provider failed to generate graph update proposal",
|
|
),
|
|
],
|
|
diagnostics: {
|
|
promptVersion: promptVersion ?? null,
|
|
modelName,
|
|
responseDurationMs: Date.now() - startedAt,
|
|
normalisationsApplied: [],
|
|
},
|
|
statusCode: 502,
|
|
};
|
|
}
|
|
|
|
const parsedProposal = parseProposal(rawResponse);
|
|
const responseDurationMs = Date.now() - startedAt;
|
|
|
|
if (!parsedProposal.success) {
|
|
return {
|
|
success: false,
|
|
stage: "proposal_validation",
|
|
error: "Invalid graph update proposal",
|
|
proposalErrors: parsedProposal.errors,
|
|
diagnostics: {
|
|
promptVersion: promptVersion ?? null,
|
|
modelName,
|
|
responseDurationMs,
|
|
normalisationsApplied: parsedProposal.normalisationsApplied,
|
|
},
|
|
statusCode: 502,
|
|
};
|
|
}
|
|
|
|
if (shouldApplyProposal) {
|
|
const applicationResult = applyProposalUpdate({
|
|
situationGraph,
|
|
proposal: parsedProposal.proposal,
|
|
previousQuestion,
|
|
answer,
|
|
});
|
|
|
|
if (!applicationResult.success) {
|
|
// Compact rejected-proposal snapshot for proposal_compatibility diagnostics.
|
|
// Diagnostic evidence only — does not alter validation or mutation.
|
|
const rejectedProposalSnapshot =
|
|
applicationResult.stage === "proposal_compatibility" && parsedProposal.proposal
|
|
? {
|
|
answerMeaning: parsedProposal.proposal.answerMeaning
|
|
? {
|
|
userSupportedMeaning:
|
|
parsedProposal.proposal.answerMeaning.userSupportedMeaning ?? null,
|
|
possibleInference:
|
|
parsedProposal.proposal.answerMeaning.possibleInference ?? null,
|
|
}
|
|
: null,
|
|
updatedNodes: (parsedProposal.proposal.updatedNodes ?? []).map((n) => ({
|
|
nodeId: n.nodeId,
|
|
newValue: n.newValue,
|
|
})),
|
|
resolvedUnknownNodeIds: parsedProposal.proposal.resolvedUnknownNodeIds ?? [],
|
|
addedNodes: (parsedProposal.proposal.addedNodes ?? []).map((n) => ({
|
|
id: n.id,
|
|
kind: n.kind,
|
|
label: n.label,
|
|
description: n.description,
|
|
parentId: n.parentId ?? null,
|
|
dependsOn: n.dependsOn ?? [],
|
|
affects: n.affects ?? [],
|
|
childIds: n.childIds ?? [],
|
|
})),
|
|
addedEdges: (parsedProposal.proposal.addedEdges ?? []).map((e) => ({
|
|
fromNodeId: e.fromNodeId,
|
|
toNodeId: e.toNodeId,
|
|
relationship: e.relationship,
|
|
})),
|
|
}
|
|
: null;
|
|
|
|
return {
|
|
success: false,
|
|
stage: applicationResult.stage,
|
|
errors: applicationResult.errors,
|
|
diagnostics: {
|
|
...buildUpdateDiagnostics({
|
|
promptVersion,
|
|
modelName,
|
|
responseDurationMs,
|
|
normalisationsApplied: parsedProposal.normalisationsApplied,
|
|
graph: situationGraph,
|
|
graphReferenceValidation: graphReferenceValidation,
|
|
selectedQuestion: null,
|
|
previousReasoningState: buildReasoningState(situationGraph),
|
|
reasoningState: buildReasoningState(situationGraph),
|
|
resolvedReasoningNodeIds: [],
|
|
emergentReasoningNodeCreated: false,
|
|
emergentReasoningNodeId: null,
|
|
emergentReasoningNodeReason: null,
|
|
atomicityAssessment: null,
|
|
atomicityDecisionReason: null,
|
|
decompositionDepth: 0,
|
|
decompositionAttempted: false,
|
|
decompositionAccepted: false,
|
|
decompositionStoppedReason: null,
|
|
proposedChildCount: 0,
|
|
acceptedChildCount: 0,
|
|
rejectedChildren: [],
|
|
selectedChildNodeId: null,
|
|
childQualitySummary: [],
|
|
propagationPerformed: false,
|
|
resolvedChildNodeId: null,
|
|
parentNodeId: null,
|
|
parentStatusBefore: null,
|
|
parentStatusAfter: null,
|
|
parentConfidenceBefore: null,
|
|
parentConfidenceAfter: null,
|
|
evidenceConfidenceBefore: null,
|
|
evidenceConfidenceAfter: null,
|
|
completenessBefore: null,
|
|
completenessAfter: null,
|
|
conclusionConfidenceBefore: null,
|
|
conclusionConfidenceAfter: null,
|
|
resolvedDirectChildren: 0,
|
|
unresolvedDirectChildren: 0,
|
|
contradictoryDirectChildren: 0,
|
|
corroboratingBranchCount: 0,
|
|
conflictingBranchCount: 0,
|
|
duplicateEvidenceCount: 0,
|
|
independentBranchCount: 0,
|
|
interactionSummary: null,
|
|
confidenceCapReason: null,
|
|
ancestorPropagationStoppedReason: null,
|
|
affectedAncestorIds: [],
|
|
nextSelectedSibling: null,
|
|
parentResolved: false,
|
|
decompositionPerformed: false,
|
|
childUnknownCount: 0,
|
|
childNodeIds: [],
|
|
atomicityReason: null,
|
|
questionComplexityAccepted: null,
|
|
primaryConceptCount: null,
|
|
cognitiveLoad: null,
|
|
complexityReasons: [],
|
|
decompositionTriggeredByQuestionComplexity: false,
|
|
previousQuestion,
|
|
finalQuestion: null,
|
|
selectedUnknownBefore: null,
|
|
selectedUnknownAfter: null,
|
|
unresolvedCandidateCount: 0,
|
|
eligibleCandidateCount: 0,
|
|
candidateNodeIds: [],
|
|
resolvedCurrentTurnNodeIds: [],
|
|
noQuestionReason: null,
|
|
reasoningPatternValidation: null,
|
|
patternCompatibleNodeCount: 0,
|
|
incompatibleNodeIds: [],
|
|
compatibilityFailures: [],
|
|
replacementActions: [],
|
|
graphReasoningIntegrity: null,
|
|
plainLanguageNormalisations: [],
|
|
unknownSelectionExplanation: explainUnknownSelection(
|
|
situationGraph,
|
|
situationGraph.resolvedNodeIds || [],
|
|
),
|
|
}),
|
|
...(rejectedProposalSnapshot && { rejectedProposalSnapshot }),
|
|
},
|
|
statusCode:
|
|
applicationResult.stage === "application" ||
|
|
applicationResult.stage === "result_validation"
|
|
? 500
|
|
: 400,
|
|
};
|
|
}
|
|
|
|
// ── v2: process incoming findings (no graph mutation) ────
|
|
let appendedFindings = [];
|
|
|
|
if (incomingFindings && Array.isArray(incomingFindings) && incomingFindings.length > 0) {
|
|
const validated = validateFindings(incomingFindings);
|
|
|
|
// Filter out rejected findings for display only — do NOT modify currentSummary.
|
|
// Direct concatenation of Finding text into Current Understanding would bypass
|
|
// the authoritative case/update reasoning that evaluates Finding context.
|
|
// Per v1 handoff contract: "Noted as not directly relevant" and similar markers
|
|
// are display-only; they must never be appended to summary or graph state.
|
|
const validForDisplay = validated.findings.filter((f) => f.evaluation !== "rejected");
|
|
|
|
// Normalize and include approved findings in the response (display passthrough)
|
|
const normalized = validForDisplay.map((f) => ({
|
|
id: f.id,
|
|
proposition: f.proposition,
|
|
status: f.status,
|
|
userDisposition: f.userDisposition,
|
|
originatingTargetNodeId: f.originatingTargetNodeId,
|
|
contributionId: f.contributionId,
|
|
sourceObservation: f.sourceObservation,
|
|
createdAt: f.createdAt,
|
|
}));
|
|
appendedFindings = normalized;
|
|
}
|
|
|
|
// ── Post-authoritative finding-informed understanding seam ────
|
|
const authoritativeSummary =
|
|
applicationResult.updatedSituationGraph?.currentSummary ?? "";
|
|
const experimentalSummary = produceFindingInformedSummary(
|
|
authoritativeSummary,
|
|
appendedFindings,
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
stage: "update_applied",
|
|
updatedSituationGraph: applicationResult.updatedSituationGraph,
|
|
proposal: applicationResult.graphUpdate,
|
|
selectedQuestion: applicationResult.selectedQuestion,
|
|
affectedNodeIds: applicationResult.affectedNodeIds,
|
|
resolvedUnknownNodeIds: applicationResult.resolvedUnknownNodeIds,
|
|
previousActiveUnknownNodeId:
|
|
applicationResult.previousActiveUnknownNodeId,
|
|
newActiveUnknownNodeId: applicationResult.newActiveUnknownNodeId,
|
|
changesApplied: applicationResult.changesApplied,
|
|
appendedFindings,
|
|
summary: experimentalSummary,
|
|
diagnostics: buildUpdateDiagnostics({
|
|
promptVersion,
|
|
modelName,
|
|
responseDurationMs,
|
|
normalisationsApplied: parsedProposal.normalisationsApplied,
|
|
graph: applicationResult.updatedSituationGraph,
|
|
graphReferenceValidation: applicationResult.graphReferenceValidation,
|
|
selectedQuestion: applicationResult.selectedQuestion,
|
|
previousReasoningState: applicationResult.previousReasoningState,
|
|
reasoningState: applicationResult.reasoningState,
|
|
resolvedReasoningNodeIds: applicationResult.resolvedReasoningNodeIds,
|
|
emergentReasoningNodeCreated:
|
|
applicationResult.emergentReasoningNodeCreated,
|
|
emergentReasoningNodeId: applicationResult.emergentReasoningNodeId,
|
|
emergentReasoningNodeReason:
|
|
applicationResult.emergentReasoningNodeReason,
|
|
atomicityAssessment: applicationResult.atomicityAssessment,
|
|
atomicityDecisionReason: applicationResult.atomicityDecisionReason,
|
|
decompositionDepth: applicationResult.decompositionDepth,
|
|
decompositionAttempted: applicationResult.decompositionAttempted,
|
|
decompositionAccepted: applicationResult.decompositionAccepted,
|
|
decompositionStoppedReason:
|
|
applicationResult.decompositionStoppedReason,
|
|
proposedChildCount: applicationResult.proposedChildCount,
|
|
acceptedChildCount: applicationResult.acceptedChildCount,
|
|
rejectedChildren: applicationResult.rejectedChildren,
|
|
selectedChildNodeId: applicationResult.selectedChildNodeId,
|
|
childQualitySummary: applicationResult.childQualitySummary,
|
|
propagationPerformed: applicationResult.propagationPerformed,
|
|
resolvedChildNodeId: applicationResult.resolvedChildNodeId,
|
|
parentNodeId: applicationResult.parentNodeId,
|
|
parentStatusBefore: applicationResult.parentStatusBefore,
|
|
parentStatusAfter: applicationResult.parentStatusAfter,
|
|
parentConfidenceBefore: applicationResult.parentConfidenceBefore,
|
|
parentConfidenceAfter: applicationResult.parentConfidenceAfter,
|
|
evidenceConfidenceBefore: applicationResult.evidenceConfidenceBefore,
|
|
evidenceConfidenceAfter: applicationResult.evidenceConfidenceAfter,
|
|
completenessBefore: applicationResult.completenessBefore,
|
|
completenessAfter: applicationResult.completenessAfter,
|
|
conclusionConfidenceBefore:
|
|
applicationResult.conclusionConfidenceBefore,
|
|
conclusionConfidenceAfter: applicationResult.conclusionConfidenceAfter,
|
|
resolvedDirectChildren: applicationResult.resolvedDirectChildren,
|
|
unresolvedDirectChildren: applicationResult.unresolvedDirectChildren,
|
|
contradictoryDirectChildren:
|
|
applicationResult.contradictoryDirectChildren,
|
|
corroboratingBranchCount: applicationResult.corroboratingBranchCount,
|
|
conflictingBranchCount: applicationResult.conflictingBranchCount,
|
|
duplicateEvidenceCount: applicationResult.duplicateEvidenceCount,
|
|
independentBranchCount: applicationResult.independentBranchCount,
|
|
interactionSummary: applicationResult.interactionSummary,
|
|
confidenceCapReason: applicationResult.confidenceCapReason,
|
|
ancestorPropagationStoppedReason:
|
|
applicationResult.ancestorPropagationStoppedReason,
|
|
affectedAncestorIds: applicationResult.affectedAncestorIds,
|
|
nextSelectedSibling: applicationResult.nextSelectedSibling,
|
|
parentResolved: applicationResult.parentResolved,
|
|
decompositionPerformed: applicationResult.decompositionPerformed,
|
|
childUnknownCount: applicationResult.childUnknownCount,
|
|
childNodeIds: applicationResult.childNodeIds,
|
|
atomicityReason: applicationResult.atomicityReason,
|
|
questionComplexityAccepted:
|
|
applicationResult.questionComplexityAccepted,
|
|
primaryConceptCount: applicationResult.primaryConceptCount,
|
|
cognitiveLoad: applicationResult.cognitiveLoad,
|
|
complexityReasons: applicationResult.complexityReasons,
|
|
decompositionTriggeredByQuestionComplexity:
|
|
applicationResult.decompositionTriggeredByQuestionComplexity,
|
|
previousQuestion: applicationResult.previousQuestion,
|
|
finalQuestion: applicationResult.finalQuestion,
|
|
selectedUnknownBefore: applicationResult.selectedUnknownBefore,
|
|
selectedUnknownAfter: applicationResult.selectedUnknownAfter,
|
|
unresolvedCandidateCount: applicationResult.unresolvedCandidateCount,
|
|
eligibleCandidateCount: applicationResult.eligibleCandidateCount,
|
|
candidateNodeIds: applicationResult.candidateNodeIds,
|
|
resolvedCurrentTurnNodeIds:
|
|
applicationResult.resolvedCurrentTurnNodeIds,
|
|
noQuestionReason: applicationResult.noQuestionReason,
|
|
reasoningPatternValidation:
|
|
applicationResult.reasoningPatternValidation,
|
|
patternCompatibleNodeCount:
|
|
applicationResult.patternCompatibleNodeCount,
|
|
incompatibleNodeIds: applicationResult.incompatibleNodeIds,
|
|
compatibilityFailures: applicationResult.compatibilityFailures,
|
|
replacementActions: applicationResult.replacementActions,
|
|
graphReasoningIntegrity: applicationResult.graphReasoningIntegrity,
|
|
plainLanguageNormalisations:
|
|
applicationResult.plainLanguageNormalisations,
|
|
reasoningPattern:
|
|
applicationResult.selectedQuestion?.reasoningPattern ?? null,
|
|
questionFamily:
|
|
applicationResult.selectedQuestion?.questionFamily ?? null,
|
|
allowedQuestionFamilies:
|
|
applicationResult.selectedQuestion?.allowedQuestionFamilies ?? [],
|
|
rejectedQuestionFamilies:
|
|
applicationResult.selectedQuestion?.rejectedQuestionFamilies ?? [],
|
|
selectedQuestionTemplate:
|
|
applicationResult.selectedQuestion?.selectedQuestionTemplate ?? null,
|
|
reasoningPatternReason:
|
|
applicationResult.selectedQuestion?.reasoningPatternReason ?? null,
|
|
unknownSelectionExplanation: buildUnknownSelectionDiagnostics(
|
|
applicationResult.updatedSituationGraph,
|
|
applicationResult.updatedSituationGraph.resolvedNodeIds || [],
|
|
applicationResult.selectedQuestion,
|
|
),
|
|
}),
|
|
assessment: assessInvestigationState({
|
|
situationGraph: applicationResult.updatedSituationGraph,
|
|
selectedQuestion: applicationResult.selectedQuestion,
|
|
noQuestionReason: applicationResult.noQuestionReason ?? null,
|
|
diagnostics: {
|
|
promptVersion,
|
|
modelName,
|
|
responseDurationMs,
|
|
validationStatus: "valid",
|
|
nodeCount: applicationResult.updatedSituationGraph?.nodes?.length ?? 0,
|
|
edgeCount: applicationResult.updatedSituationGraph?.edges?.length ?? 0,
|
|
reasoningPattern: applicationResult.selectedQuestion?.reasoningPattern ?? null,
|
|
},
|
|
}),
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
stage: "proposal_ready",
|
|
proposal: parsedProposal.proposal,
|
|
diagnostics: buildUpdateDiagnostics({
|
|
promptVersion,
|
|
modelName,
|
|
responseDurationMs,
|
|
normalisationsApplied: parsedProposal.normalisationsApplied,
|
|
graph: situationGraph,
|
|
graphReferenceValidation,
|
|
selectedQuestion: null,
|
|
previousReasoningState: buildReasoningState(situationGraph),
|
|
reasoningState: buildReasoningState(situationGraph),
|
|
resolvedReasoningNodeIds: [],
|
|
emergentReasoningNodeCreated: false,
|
|
emergentReasoningNodeId: null,
|
|
emergentReasoningNodeReason: null,
|
|
atomicityAssessment: null,
|
|
atomicityDecisionReason: null,
|
|
decompositionDepth: 0,
|
|
decompositionAttempted: false,
|
|
decompositionAccepted: false,
|
|
decompositionStoppedReason: null,
|
|
proposedChildCount: 0,
|
|
acceptedChildCount: 0,
|
|
rejectedChildren: [],
|
|
selectedChildNodeId: null,
|
|
childQualitySummary: [],
|
|
propagationPerformed: false,
|
|
resolvedChildNodeId: null,
|
|
parentNodeId: null,
|
|
parentStatusBefore: null,
|
|
parentStatusAfter: null,
|
|
parentConfidenceBefore: null,
|
|
parentConfidenceAfter: null,
|
|
evidenceConfidenceBefore: null,
|
|
evidenceConfidenceAfter: null,
|
|
completenessBefore: null,
|
|
completenessAfter: null,
|
|
conclusionConfidenceBefore: null,
|
|
conclusionConfidenceAfter: null,
|
|
resolvedDirectChildren: 0,
|
|
unresolvedDirectChildren: 0,
|
|
contradictoryDirectChildren: 0,
|
|
corroboratingBranchCount: 0,
|
|
conflictingBranchCount: 0,
|
|
duplicateEvidenceCount: 0,
|
|
independentBranchCount: 0,
|
|
interactionSummary: null,
|
|
confidenceCapReason: null,
|
|
ancestorPropagationStoppedReason: null,
|
|
affectedAncestorIds: [],
|
|
nextSelectedSibling: null,
|
|
parentResolved: false,
|
|
decompositionPerformed: false,
|
|
childUnknownCount: 0,
|
|
childNodeIds: [],
|
|
atomicityReason: null,
|
|
questionComplexityAccepted: null,
|
|
primaryConceptCount: null,
|
|
cognitiveLoad: null,
|
|
complexityReasons: [],
|
|
decompositionTriggeredByQuestionComplexity: false,
|
|
previousQuestion,
|
|
finalQuestion: null,
|
|
selectedUnknownBefore: null,
|
|
selectedUnknownAfter: null,
|
|
unresolvedCandidateCount: 0,
|
|
eligibleCandidateCount: 0,
|
|
candidateNodeIds: [],
|
|
resolvedCurrentTurnNodeIds: [],
|
|
noQuestionReason: null,
|
|
reasoningPatternValidation: null,
|
|
patternCompatibleNodeCount: 0,
|
|
incompatibleNodeIds: [],
|
|
compatibilityFailures: [],
|
|
replacementActions: [],
|
|
graphReasoningIntegrity: null,
|
|
plainLanguageNormalisations: [],
|
|
reasoningPattern: null,
|
|
questionFamily: null,
|
|
allowedQuestionFamilies: [],
|
|
rejectedQuestionFamilies: [],
|
|
selectedQuestionTemplate: null,
|
|
reasoningPatternReason: null,
|
|
unknownSelectionExplanation: buildUnknownSelectionDiagnostics(
|
|
situationGraph,
|
|
situationGraph.resolvedNodeIds || [],
|
|
null,
|
|
),
|
|
}),
|
|
assessment: assessInvestigationState({
|
|
situationGraph,
|
|
selectedQuestion: null,
|
|
noQuestionReason: null,
|
|
diagnostics: {
|
|
promptVersion,
|
|
modelName,
|
|
responseDurationMs,
|
|
validationStatus: "valid",
|
|
nodeCount: situationGraph?.nodes?.length ?? 0,
|
|
edgeCount: situationGraph?.edges?.length ?? 0,
|
|
reasoningPattern: null,
|
|
},
|
|
}),
|
|
};
|
|
}
|
|
|
|
// ── Episode-aware reconsideration seam ─────────────────────
|
|
|
|
/**
|
|
* Reason over a completed focused episode and return a parsed GraphUpdateProposal.
|
|
*
|
|
* This is the smallest sibling to updateCase that reuses existing provider,
|
|
* model, and parsing mechanics — only the prompt path differs.
|
|
*/
|
|
export async function reconsiderCompletedEpisode(epiParams, deps = {}) {
|
|
const { situationGraph: epiGraph, targetNodeId, contributions, findings, episode: rawEpisode } = epiParams;
|
|
|
|
// Accept prepared episode or produce it deterministically
|
|
|
|
let episode;
|
|
|
|
if (rawEpisode) {
|
|
episode = rawEpisode;
|
|
} else if (epiParams?.turns || epiParams?.eligibleCanonicalFindings) {
|
|
// Already a prepared episode passed as first positional arg
|
|
episode = epiParams;
|
|
} else {
|
|
episode = prepareCompletedEpisode(epiParams);
|
|
}
|
|
|
|
if (!episode?.turns && !episode?.eligibleCanonicalFindings) {
|
|
return {
|
|
success: false,
|
|
stage: "input_validation",
|
|
error: "Invalid prepared episode — requires turns or findings",
|
|
statusCode: 400,
|
|
};
|
|
}
|
|
|
|
const getModelName = deps.getModelName;
|
|
const buildEpisodePrompt = deps.buildEpisodePrompt;
|
|
const parseProposal = deps.parseProposal;
|
|
const provider = epiParams.provider ?? deps.provider;
|
|
|
|
let modelName = null;
|
|
let rawResponse;
|
|
const startedAt = Date.now();
|
|
|
|
try {
|
|
// Preserve original priority (deps.modelName > deps.config.OLLAMA_MODEL > getModelName) with assertConfig fallback only when no deps provide a model
|
|
let resolvedConfig = undefined;
|
|
if (deps.modelName == null && deps.config?.OLLAMA_MODEL == null && deps.config !== null) {
|
|
const hasOtherDeps = Object.keys(deps).some((k) => k !== "config" && k !== "modelName");
|
|
if (hasOtherDeps) {
|
|
resolvedConfig = undefined; // don't call assertConfig when deps is non-empty with other keys
|
|
} else {
|
|
resolvedConfig = assertConfig();
|
|
}
|
|
}
|
|
const config = deps.config ?? resolvedConfig;
|
|
modelName =
|
|
deps.modelName ??
|
|
(config != null ? config.OLLAMA_MODEL : undefined) ??
|
|
getModelName?.() ??
|
|
null;
|
|
|
|
const promptBuilder = buildEpisodePrompt ?? buildEpisodeAwareGraphPrompt;
|
|
const prompt = promptBuilder({ episode });
|
|
|
|
const prov = provider ?? getProvider();
|
|
rawResponse = await prov.generateReconstruction(prompt, modelName);
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
stage: "provider",
|
|
error: "Graph update proposal generation failed",
|
|
providerErrors: [
|
|
sanitiseErrorMessage(
|
|
error,
|
|
"Provider failed to generate graph update proposal from episode",
|
|
),
|
|
],
|
|
diagnostics: {
|
|
promptVersion: null,
|
|
modelName,
|
|
responseDurationMs: Date.now() - startedAt,
|
|
normalisationsApplied: [],
|
|
},
|
|
statusCode: 502,
|
|
};
|
|
}
|
|
|
|
const parser = parseProposal ?? parseGraphUpdateProposal;
|
|
const parsedProposal = parser(rawResponse);
|
|
const responseDurationMs = Date.now() - startedAt;
|
|
|
|
if (!parsedProposal.success) {
|
|
return {
|
|
success: false,
|
|
stage: "proposal_validation",
|
|
error: "Invalid graph update proposal",
|
|
proposalErrors: parsedProposal.errors,
|
|
diagnostics: {
|
|
promptVersion: null,
|
|
modelName,
|
|
responseDurationMs,
|
|
normalisationsApplied: parsedProposal.normalisationsApplied,
|
|
},
|
|
statusCode: 502,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
stage: "proposal_ready",
|
|
proposal: parsedProposal.proposal,
|
|
diagnostics: {
|
|
promptVersion: null,
|
|
modelName,
|
|
responseDurationMs,
|
|
normalisationsApplied: parsedProposal.normalisationsApplied,
|
|
},
|
|
};
|
|
}
|