fix(focused-investigation): suppress incompatible question strategies

This commit is contained in:
2026-10-08 17:48:53 +01:00
parent 92ae463d10
commit f74fe6de10
6 changed files with 672 additions and 16 deletions
+106 -3
View File
@@ -1,4 +1,7 @@
import { formulateQuestion } from "@/lib/graph/question-formulator.js";
import {
formulateQuestion,
isStrategyQuestionTemplateCompatible,
} from "@/lib/graph/question-formulator.js";
const FOCUSED_ANSWER_SCHEMA_FIELDS = [
"targetNodeId",
@@ -35,6 +38,16 @@ export const focusedDeconstructJsonSchema = {
additionalProperties: false,
};
export const strategyCompatibilityAssessmentJsonSchema = {
type: "object",
properties: {
strategyKey: { type: "string" },
relevance: { type: "string", enum: ["compatible", "incompatible"] },
},
required: ["strategyKey", "relevance"],
additionalProperties: false,
};
const FORBIDDEN_GRAPH_MUTATION_FIELDS = [
"addedNodes",
"updatedNodes",
@@ -49,14 +62,19 @@ const FORBIDDEN_GRAPH_MUTATION_FIELDS = [
/**
* Formulate a question for the explicitly user-selected unresolved node.
*
* Input: { situationGraph, targetNodeId }
* Input: { situationGraph, targetNodeId, strategyCompatibilityVerdict? }
* Validates that target exists, is an unknown, and is not resolved.
* Calls formulateQuestion with only factual explicit-user-targeting context.
* Does NOT invoke selectActiveUnknownCandidate, determineGraphBackedQuestion,
* global ranking, or global recommendation.
* Does NOT mutate activeUnknownNodeId, selectedQuestion, SituationGraph, or resolvedNodeIds.
*/
export function formulateQuestionForTarget({ situationGraph, targetNodeId }) {
export function formulateQuestionForTarget({
situationGraph,
targetNodeId,
strategyCompatibilityVerdict,
strategyTemplateCompatibility,
}) {
const nodesById = new Map(situationGraph?.nodes?.map((n) => [n.id, n]) || []);
const targetNode = nodesById.get(targetNodeId);
@@ -80,6 +98,12 @@ export function formulateQuestionForTarget({ situationGraph, targetNodeId }) {
_explicitTargetNodeId: targetNodeId,
_explicitTargetLabel: targetNode.label,
_explicitTargetDescription: targetNode.description,
...(strategyCompatibilityVerdict === undefined
? {}
: { strategyCompatibilityVerdict }),
...(strategyTemplateCompatibility === undefined
? {}
: { strategyTemplateCompatibility }),
};
const result = formulateQuestion({ node: targetNode, graph: situationGraph, context });
@@ -89,6 +113,7 @@ export function formulateQuestionForTarget({ situationGraph, targetNodeId }) {
targetNodeId,
question: result.question,
strategy: result.strategy ?? null,
investigationStrategy: result.investigationStrategy,
reasoningPattern: result.reasoningPattern,
reasoningPatternReason: result.reasoningPatternReason,
reason: result.reason,
@@ -99,6 +124,84 @@ export function formulateQuestionForTarget({ situationGraph, targetNodeId }) {
};
}
export function buildStrategyCompatibilityAssessmentPrompt({
targetLabel,
targetDescription,
centralStatement,
proposedStrategy,
}) {
return `Assess whether one already-proposed investigation strategy is relevant to one explicitly selected uncertainty.
Return exactly one JSON object and JSON only.
You may only assess the relevance of the supplied proposed strategy to the supplied selected uncertainty.
Do not choose another uncertainty. Do not propose another strategy. Do not write the final question. Do not decide the user's conclusion. Do not mutate or recommend changes to any graph.
Return "compatible" only when this exact strategy would help investigate this exact uncertainty. Return "incompatible" when it would not.
Selected uncertainty:
- label: ${targetLabel}
- description: ${targetDescription}
Case context:
- central statement: ${centralStatement}
Proposed strategy:
- strategyKey: ${proposedStrategy.key}
- strategy meaning: ${proposedStrategy.meaning}
Return exactly:
{"strategyKey":"${proposedStrategy.key}","relevance":"compatible" | "incompatible"}`;
}
export function validateStrategyCompatibilityVerdict(result, proposedStrategy) {
if (!result || typeof result !== "object" || Array.isArray(result)) return null;
const keys = Object.keys(result);
if (
keys.length !== 2 ||
!keys.includes("strategyKey") ||
!keys.includes("relevance") ||
typeof result.strategyKey !== "string" ||
result.strategyKey !== proposedStrategy?.key ||
(result.relevance !== "compatible" && result.relevance !== "incompatible")
) {
return null;
}
return { strategyKey: result.strategyKey, relevance: result.relevance };
}
export async function assessStrategyCompatibility({
provider,
modelName,
targetLabel,
targetDescription,
centralStatement,
proposedStrategy,
signal,
}) {
const prompt = buildStrategyCompatibilityAssessmentPrompt({
targetLabel,
targetDescription,
centralStatement,
proposedStrategy,
});
const wrapper = await provider.generateReconstruction(
prompt,
modelName,
strategyCompatibilityAssessmentJsonSchema,
{ signal },
);
const verdict = validateStrategyCompatibilityVerdict(
wrapper?.response,
proposedStrategy,
);
return verdict
? { success: true, verdict }
: { success: false, status: "invalid" };
}
export { isStrategyQuestionTemplateCompatible };
/**
* Build a focused answer deconstruction prompt for one explicitly user-chosen investigation.
*/
+48 -9
View File
@@ -1198,6 +1198,37 @@ function constrainStrategyToReasoningPattern(strategy, reasoningPattern) {
return allowedStrategies.includes(strategy.key) ? strategy : null;
}
function applyStrategyCompatibilityVerdict(proposedStrategy, verdict) {
// No verdict is the backward-compatible path. A supplied verdict can only
// preserve this exact proposal or suppress it; it cannot select a substitute.
if (verdict === undefined) return proposedStrategy;
if (!proposedStrategy || !verdict || typeof verdict !== "object" || Array.isArray(verdict)) {
return null;
}
const keys = Object.keys(verdict);
if (
keys.length !== 2 ||
!keys.includes("strategyKey") ||
!keys.includes("relevance") ||
typeof verdict.strategyKey !== "string" ||
(verdict.relevance !== "compatible" && verdict.relevance !== "incompatible") ||
verdict.strategyKey !== proposedStrategy.key
) {
return null;
}
return verdict.relevance === "compatible" ? proposedStrategy : null;
}
export function isStrategyQuestionTemplateCompatible(strategy) {
if (!strategy) return true;
return !(
strategy.key === "baseline_reconstruction" &&
isWhetherPropositionMeaning(strategy.meaning)
);
}
function selectQuestionFamily({
node,
graph,
@@ -2047,7 +2078,7 @@ export function formulateQuestion({ node, graph, context = {} }) {
}
}
const investigationStrategy = selectInvestigationStrategy({
const proposedInvestigationStrategy = selectInvestigationStrategy({
node,
graph,
context: {
@@ -2055,12 +2086,20 @@ export function formulateQuestion({ node, graph, context = {} }) {
reasoningPattern: reasoningPatternSelection.pattern,
},
});
const investigationStrategy = applyStrategyCompatibilityVerdict(
proposedInvestigationStrategy,
context.strategyCompatibilityVerdict,
);
const templateCompatibleStrategy =
context.strategyTemplateCompatibility === "incompatible"
? null
: investigationStrategy;
const questionFamilySelection = selectQuestionFamily({
node,
graph,
reasoningPattern: reasoningPatternSelection.pattern,
investigationStrategy,
investigationStrategy: templateCompatibleStrategy,
});
let question = buildQuestionFromFamily({
@@ -2069,7 +2108,7 @@ export function formulateQuestion({ node, graph, context = {} }) {
reasoningPattern: reasoningPatternSelection.pattern,
questionFamily: questionFamilySelection.family,
selectedQuestionTemplate: questionFamilySelection.template,
investigationStrategy,
investigationStrategy: templateCompatibleStrategy,
});
question = sanitizeQuestionText(question);
@@ -2080,11 +2119,11 @@ export function formulateQuestion({ node, graph, context = {} }) {
if (
!validateFormulatedQuestion(
question,
investigationStrategy?.meaning || fallbackMeaning,
templateCompatibleStrategy?.meaning || fallbackMeaning,
)
) {
question = sanitizeQuestionText(
(investigationStrategy &&
(templateCompatibleStrategy &&
isClaimLikeUnknown(
node,
normaliseText(
@@ -2112,11 +2151,11 @@ export function formulateQuestion({ node, graph, context = {} }) {
return {
question,
reason: investigationStrategy
? `Formulated from graph context using the ${investigationStrategy.key} investigation strategy.`
reason: templateCompatibleStrategy
? `Formulated from graph context using the ${templateCompatibleStrategy.key} investigation strategy.`
: "Formulated as a neutral clarification question because no narrower investigation strategy clearly applied.",
strategy: investigationStrategy?.key ?? null,
investigationStrategy,
strategy: templateCompatibleStrategy?.key ?? null,
investigationStrategy: templateCompatibleStrategy,
reasoningPattern: reasoningPatternSelection.pattern,
reasoningPatternReason: reasoningPatternSelection.reason,
questionFamily: questionFamilySelection.family,