fix: continue question selection after graph updates
This commit is contained in:
+127
-2
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user