fix: continue question selection after graph updates

This commit is contained in:
2026-08-03 11:28:49 +01:00
parent b00928d6fb
commit 3e2edd2edc
7 changed files with 509 additions and 2 deletions
+127 -2
View File
@@ -1407,15 +1407,24 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) {
dependsOnLabels: [],
},
{
label: "What happens when this problem is not resolved",
label: "Whether other people experience this problem",
description:
"Need to know what happens when this problem is not resolved, because that is needed before judging whether the problem matters.",
"Need to know whether other people experience this problem, because that must be established before deciding whether the problem is broadly important.",
dependsOnLabels: ["Who experiences this problem"],
},
{
label: "How often this problem happens",
description:
"Need to know how often this problem happens, because that helps judge whether it is a real recurring problem.",
dependsOnLabels: [
"Who experiences this problem",
"Whether other people experience this problem",
],
},
{
label: "What happens when this problem is not resolved",
description:
"Need to know what happens when this problem is not resolved, because that is needed before judging whether the problem matters.",
dependsOnLabels: ["Who experiences this problem"],
},
{
@@ -1424,6 +1433,7 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) {
"Need to know how people deal with this problem today, because that is needed before comparing alternatives or value.",
dependsOnLabels: [
"Who experiences this problem",
"Whether other people experience this problem",
"What happens when this problem is not resolved",
"How often this problem happens",
],
@@ -1435,6 +1445,7 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) {
"Need to know whether people actively look for help with this problem, because that is needed before judging demand or willingness to pay.",
dependsOnLabels: [
"Who experiences this problem",
"Whether other people experience this problem",
"What happens when this problem is not resolved",
"How often this problem happens",
"How people deal with this problem today",
@@ -1446,6 +1457,7 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) {
"Need to know whether people would pay to solve this problem, because that can only be judged after the problem itself is established.",
dependsOnLabels: [
"Who experiences this problem",
"Whether other people experience this problem",
"What happens when this problem is not resolved",
"How often this problem happens",
"How people deal with this problem today",
@@ -1635,6 +1647,81 @@ function isSelectableUnresolvedUnknown(graph, nodeId) {
);
}
function listUnresolvedUnknownCandidates(
graph,
resolvedCurrentTurnNodeIds = [],
) {
const resolvedCurrentTurnSet = new Set(resolvedCurrentTurnNodeIds || []);
return (graph.nodes || []).filter(
(node) =>
node.kind === "unknown" &&
!["resolved", "contradicted"].includes(node.status) &&
!(graph.resolvedNodeIds || []).includes(node.id) &&
!resolvedCurrentTurnSet.has(node.id),
);
}
function listEligibleUnknownCandidates(graph, resolvedCurrentTurnNodeIds = []) {
return listUnresolvedUnknownCandidates(
graph,
resolvedCurrentTurnNodeIds,
).filter(
(node) =>
(scoreUnknownCandidate(graph, node, graph.resolvedNodeIds || [])
.unresolvedParentUnknownCount ?? 0) === 0,
);
}
function selectOrderedSiblingCandidate(
graph,
candidateNodeIds = [],
resolvedCurrentTurnNodeIds = [],
) {
const candidates = candidateNodeIds
.map((nodeId) => findNodeById(graph, nodeId))
.filter(Boolean);
if (candidates.length < 2) {
return null;
}
const parentId = candidates[0]?.parentId ?? null;
if (!parentId || !candidates.every((node) => node.parentId === parentId)) {
return null;
}
const parentNode = findNodeById(graph, parentId);
const orderedIds = (parentNode?.childIds || []).filter((nodeId) =>
candidateNodeIds.includes(nodeId),
);
const fallbackOrderedIds = (graph.nodes || [])
.filter((node) => candidateNodeIds.includes(node.id))
.map((node) => node.id);
const orderedCandidateIds =
orderedIds.length > 0 ? orderedIds : fallbackOrderedIds;
const eligibleIds = new Set(
listEligibleUnknownCandidates(graph, resolvedCurrentTurnNodeIds).map(
(node) => node.id,
),
);
const selectedId = orderedCandidateIds.find((nodeId) =>
eligibleIds.has(nodeId),
);
if (!selectedId) {
return null;
}
return {
status: "selected",
nodeId: selectedId,
reason:
"Resolved a sibling tie using the deterministic decomposition order after the update left multiple equally scored follow-up children.",
};
}
function selectedQuestionBelongsToChild(graph, selectedQuestion) {
if (!selectedQuestion?.nodeId) return false;
return Boolean(findNodeById(graph, selectedQuestion.nodeId)?.parentId);
@@ -2356,6 +2443,9 @@ export function applyValidatedProposal({
reasoningResolution.reasoningStateOverride,
);
updatedSituationGraph.reasoningState = nextReasoningState;
const resolvedCurrentTurnNodeIds = [
...new Set(proposalSnapshot.resolvedUnknownNodeIds || []),
];
deterministicSelection = isSelectableUnresolvedUnknown(
updatedSituationGraph,
decompositionResult.selectedChildNodeId,
@@ -2371,6 +2461,27 @@ export function applyValidatedProposal({
updatedSituationGraph.resolvedNodeIds,
);
if (deterministicSelection?.status === "ambiguous") {
const orderedSiblingSelection = selectOrderedSiblingCandidate(
updatedSituationGraph,
deterministicSelection.tiedCandidateIds || [],
resolvedCurrentTurnNodeIds,
);
if (orderedSiblingSelection) {
deterministicSelection = orderedSiblingSelection;
}
}
const unresolvedCandidates = listUnresolvedUnknownCandidates(
updatedSituationGraph,
resolvedCurrentTurnNodeIds,
);
const eligibleCandidates = listEligibleUnknownCandidates(
updatedSituationGraph,
resolvedCurrentTurnNodeIds,
);
const atomicityAssessment = decompositionResult.atomicityAssessment;
const answerabilityAssessment = decompositionResult.answerabilityAssessment;
const decompositionDepth = decompositionResult.decompositionDepth;
@@ -2500,6 +2611,15 @@ export function applyValidatedProposal({
)
? finalSelectedQuestion?.nodeId
: null);
const noQuestionReason = finalSelectedQuestion?.question
? null
: deterministicSelection?.status === "ambiguous"
? "Eligible unresolved candidates remain tied after update-time reselection."
: eligibleCandidates.length === 0
? unresolvedCandidates.length === 0
? "No unresolved unknown candidates remain after this update."
: "Unresolved unknowns remain, but none are currently eligible for direct investigation."
: "Question formulation did not produce a valid next question despite eligible unresolved candidates.";
const resultGraphValidation = situationGraphSchema.safeParse(
updatedSituationGraph,
@@ -2580,6 +2700,11 @@ export function applyValidatedProposal({
decompositionResult.decompositionTriggeredByAnswerability ?? false,
previousQuestion,
finalQuestion: finalSelectedQuestion?.question ?? null,
unresolvedCandidateCount: unresolvedCandidates.length,
eligibleCandidateCount: eligibleCandidates.length,
candidateNodeIds: eligibleCandidates.map((node) => node.id),
resolvedCurrentTurnNodeIds,
noQuestionReason,
plainLanguageNormalisations,
propagationPerformed,
resolvedChildNodeId,
+26
View File
@@ -195,6 +195,11 @@ function buildUpdateDiagnostics({
rejectedQuestionFamilies,
selectedQuestionTemplate,
reasoningPatternReason,
unresolvedCandidateCount,
eligibleCandidateCount,
candidateNodeIds,
resolvedCurrentTurnNodeIds,
noQuestionReason,
}) {
return {
promptVersion: promptVersion ?? "v0.4",
@@ -281,6 +286,11 @@ function buildUpdateDiagnostics({
rejectedQuestionFamilies: rejectedQuestionFamilies ?? [],
selectedQuestionTemplate: selectedQuestionTemplate ?? null,
reasoningPatternReason: reasoningPatternReason ?? null,
unresolvedCandidateCount: unresolvedCandidateCount ?? 0,
eligibleCandidateCount: eligibleCandidateCount ?? 0,
candidateNodeIds: candidateNodeIds ?? [],
resolvedCurrentTurnNodeIds: resolvedCurrentTurnNodeIds ?? [],
noQuestionReason: noQuestionReason ?? null,
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
};
}
@@ -660,6 +670,11 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
finalQuestion: null,
selectedUnknownBefore: null,
selectedUnknownAfter: null,
unresolvedCandidateCount: 0,
eligibleCandidateCount: 0,
candidateNodeIds: [],
resolvedCurrentTurnNodeIds: [],
noQuestionReason: null,
plainLanguageNormalisations: [],
unknownSelectionExplanation: explainUnknownSelection(
situationGraph,
@@ -759,6 +774,12 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
finalQuestion: applicationResult.finalQuestion,
selectedUnknownBefore: applicationResult.selectedUnknownBefore,
selectedUnknownAfter: applicationResult.selectedUnknownAfter,
unresolvedCandidateCount: applicationResult.unresolvedCandidateCount,
eligibleCandidateCount: applicationResult.eligibleCandidateCount,
candidateNodeIds: applicationResult.candidateNodeIds,
resolvedCurrentTurnNodeIds:
applicationResult.resolvedCurrentTurnNodeIds,
noQuestionReason: applicationResult.noQuestionReason,
plainLanguageNormalisations:
applicationResult.plainLanguageNormalisations,
reasoningPattern:
@@ -850,6 +871,11 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
finalQuestion: null,
selectedUnknownBefore: null,
selectedUnknownAfter: null,
unresolvedCandidateCount: 0,
eligibleCandidateCount: 0,
candidateNodeIds: [],
resolvedCurrentTurnNodeIds: [],
noQuestionReason: null,
plainLanguageNormalisations: [],
reasoningPattern: null,
questionFamily: null,
+3
View File
@@ -504,6 +504,9 @@ function buildFoundationalDirectQuestion(node) {
if (/^who experiences this problem$/i.test(label)) {
return "Who experiences this problem?";
}
if (/^whether other people experience this problem$/i.test(label)) {
return "What makes you think other people experience this problem too?";
}
if (/^what happens when this problem is not resolved$/i.test(label)) {
return "What happens when this problem is not resolved?";
}