fix(reasoning): preserve investigation ownership across selection and question rejection

This commit is contained in:
2026-08-17 17:58:45 +01:00
parent d908f3746d
commit 772ae495c6
7 changed files with 850 additions and 4429 deletions
+47
View File
@@ -0,0 +1,47 @@
# Checkpoint 60B.93 — Investigation Ownership Preservation
## Starting state
- HEAD: `d908f37`
- Branch: `feature/decision-closure-ownership-v0.47`
## Two ownership invariants implemented
### 1. Substantive-tie active ownership (lib/graph/utils.js)
When all leading structural candidates are tied after score, structural, and semantic checks, the currently active investigation target (`activeUnknownNodeId`) is preserved as the selection winner — provided it remains eligible (unresolved, not contradicted) and among the top ties. Stable label/display-order ordering is only used as a final fallback when there is no active candidate or the active node does not remain tied.
### 2. Question-rejection active ownership (lib/graph/apply-proposal.js)
When a selected candidate's graph-backed question formulation is rejected as too complex (decomposition-required), the system does NOT reseat investigation ownership to another candidate. The original selection target retains its identity with `selectedQuestion = null` and an explicit rejection reason.
## Six exact verification commands and results
| # | Command | Result |
|---|---------|--------|
| 1 | `npx vitest run tests/graph/utils.test.js` | PASS (83/83) |
| 2 | `npx vitest run tests/graph/orchestrator.test.js -t "retains ownership when the strongest target's formulated question is rejected"` | PASS |
| 3 | `npx vitest run tests/graph/orchestrator.test.js -t "replays the captured live product-launch start graph through deterministic graph-backed question selection"` | PASS |
| 4 | `npx vitest run tests/graph/apply-proposal.test.js -t "QUESTION_CONTINUATION"` | PASS |
| 5 | `npx vitest run tests/graph/question-formulator.test.js -t "60B.84"` (located in question-formulator, not apply-proposal) | PASS |
| 6 | `npx vitest run tests/graph/apply-proposal.test.js -t "State B"` | PASS |
## Classification: A — CHECKPOINT GREEN
## Captured fixture path
`tests/fixtures/live-product-launch-start-response.json`
## Reasoning files included
- `lib/graph/utils.js``classifyCandidateOrdering()` active-node tie preservation
- `tests/graph/utils.test.js` — 5 new/modified ownership guard tests
- `lib/graph/apply-proposal.js` — question-rejection no-res eating invariant
- `tests/graph/orchestrator.test.js` — 2 new product-launch regression tests
## What this checkpoint establishes
1. Active investigation ownership is preserved across complete substantive ties when the active node remains eligible.
2. Question-formulation rejection does not transfer ownership to a weaker candidate.
3. Neither fix breaks QUESTION_CONTINUATION, 60B.84, or State B.
4. The captured live product-launch case deterministically preserves ntpt9ki as the active target through question rejection.
## What remains unproved
- Live behavioural validation of the fixes in a full product-launch interaction
- Whether same-target reformulation would produce better user outcomes than no-question
- Full-suite state beyond these six guards
- The correctness of the underlying question-complexity heuristic (separate concern)
+58 -4401
View File
File diff suppressed because it is too large Load Diff
+13 -12
View File
@@ -2871,6 +2871,9 @@ function reseatSelectionAfterQuestionRejection({
: { status: "none", nodeId: null };
}
const QUESTION_FORMULATION_REJECTION_NO_QUESTION_REASON =
"The selected investigation target remains active, but its current graph-backed question formulation was rejected as too complex.";
export function determineGraphBackedQuestion({ situationGraph }) {
const graphSnapshot = cloneJsonSafe(situationGraph);
let updatedSituationGraph = cloneJsonSafe(situationGraph);
@@ -2937,15 +2940,12 @@ export function determineGraphBackedQuestion({ situationGraph }) {
initialQuestionRejected &&
deterministicSelection?.status === "selected"
) {
deterministicSelection = reseatSelectionAfterQuestionRejection({
graph: updatedSituationGraph,
deterministicSelection,
excludedNodeIds: [deterministicSelection.nodeId],
});
questionResult = buildSelectedQuestionResult({
updatedSituationGraph,
deterministicSelection,
});
questionResult = {
...questionResult,
selectedQuestion: null,
questionSuppressedReason:
QUESTION_FORMULATION_REJECTION_NO_QUESTION_REASON,
};
}
if (
@@ -2971,14 +2971,15 @@ export function determineGraphBackedQuestion({ situationGraph }) {
? questionResult.selectedQuestion?.questionSuppressedReason ||
questionResult.selectedQuestion?.reason ||
"Eligible unresolved candidates remain tied after initial graph-backed selection."
: (updatedSituationGraph.nodes || []).some(
: questionResult.questionSuppressedReason ||
((updatedSituationGraph.nodes || []).some(
(node) =>
node.kind === "unknown" &&
!["resolved", "contradicted"].includes(node.status) &&
!(updatedSituationGraph.resolvedNodeIds || []).includes(node.id),
)
? "Compatible unresolved candidates remain, but none produced a valid graph-backed question."
: "No unresolved unknown candidates remain after initial graph construction.";
? "Compatible unresolved candidates remain, but none produced a valid graph-backed question."
: "No unresolved unknown candidates remain after initial graph construction.");
return {
success: true,
+15 -5
View File
@@ -249,7 +249,7 @@ function semanticSignature(candidate) {
);
}
function classifyCandidateOrdering(candidates) {
function classifyCandidateOrdering(candidates, activeNodeId = null) {
const displayOrder = buildCandidateDisplayOrder(candidates);
const best = displayOrder[0] ?? null;
if (!best) {
@@ -319,15 +319,21 @@ function classifyCandidateOrdering(candidates) {
}
if (topStructuralCandidates.length > 0) {
const activeTiedCandidate = activeNodeId
? topStructuralCandidates.find((candidate) => candidate.nodeId === activeNodeId)
: null;
return {
displayOrder,
best,
best: activeTiedCandidate || best,
leadingCandidates: topStructuralCandidates,
status: "selected",
tieType: "complete_unresolved_tie",
usedAlphabeticalOrdering: true,
usedAlphabeticalOrdering: activeTiedCandidate ? false : true,
reason:
"Leading candidates remained tied after score, structural, and semantic checks, so the stable deterministic display order was used as the final fallback.",
activeTiedCandidate
? "Leading candidates remained tied after score, structural, and semantic checks, so existing active investigation ownership was preserved."
: "Leading candidates remained tied after score, structural, and semantic checks, so the stable deterministic display order was used as the final fallback.",
};
}
@@ -624,6 +630,7 @@ export function selectActiveUnknownCandidate(graph, resolvedNodeIds) {
...candidate,
node,
})),
graph?.activeUnknownNodeId ?? null,
);
if (selection.status === "ambiguous") {
@@ -690,7 +697,10 @@ export function explainUnknownSelection(graph, resolvedNodeIds = []) {
...scoreUnknownCandidate(graph, node, resolvedNodeIds),
}));
const selection = classifyCandidateOrdering(candidates);
const selection = classifyCandidateOrdering(
candidates,
graph?.activeUnknownNodeId ?? null,
);
const orderedCandidates = selection.displayOrder;
const selected = selection.best;
const competitors = orderedCandidates
+544
View File
@@ -0,0 +1,544 @@
{
"success": true,
"situationGraph": {
"centralStatement": "I am deciding whether to launch a new software product this year or wait twelve months. The product is ready enough to launch, but one large enterprise customer could represent a significant part of the expected revenue and I do not yet know whether they will sign. Launching this year would also require around £300,000 of additional support and implementation cost. Waiting twelve months would reduce that immediate cost and give us more time to improve the product, but it would delay revenue and may allow competitors to move first. I need to decide whether there is enough evidence to launch this year or whether waiting is the safer decision.",
"nodes": [
{
"id": "nz92pkx",
"label": "The decision-maker must choose between launching a nearly-ready software product immediately with significant upfront costs and uncertain key revenue, or delaying to reduce costs and improve the product while risking delayed revenue and competitor advantage.",
"description": "Summary of the situation from the scenario text",
"kind": "state",
"status": "provisional",
"confidence": "medium",
"value": null,
"unit": null,
"evidenceIds": [],
"dependsOn": [],
"affects": [],
"parentId": null,
"childIds": []
},
{
"id": "nks00au",
"label": "Product is considered ready enough for launch",
"description": "Product is considered ready enough for launch",
"kind": "observation",
"status": "supported",
"confidence": "high",
"value": null,
"unit": null,
"evidenceIds": [
"product_readiness"
],
"dependsOn": [],
"affects": [],
"parentId": null,
"childIds": []
},
{
"id": "nt03k2o",
"label": "Launching this year requires approximately £300,000 in support and implementation costs",
"description": "Launching this year requires approximately £300,000 in support and implementation costs",
"kind": "observation",
"status": "supported",
"confidence": "high",
"value": null,
"unit": null,
"evidenceIds": [
"cost_requirement"
],
"dependsOn": [],
"affects": [],
"parentId": null,
"childIds": []
},
{
"id": "negypzh",
"label": "The individual or team responsible for the launch timing decision",
"description": "The individual or team responsible for the launch timing decision",
"kind": "observation",
"status": "supported",
"confidence": "high",
"value": null,
"unit": null,
"evidenceIds": [],
"dependsOn": [],
"affects": [],
"parentId": null,
"childIds": []
},
{
"id": "not6pp6",
"label": "New software product, currently assessed as ready enough to launch",
"description": "New software product, currently assessed as ready enough to launch",
"kind": "metric",
"status": "known",
"confidence": "high",
"value": null,
"unit": null,
"evidenceIds": [],
"dependsOn": [],
"affects": [],
"parentId": null,
"childIds": []
},
{
"id": "nk1y11y",
"label": "One large enterprise client whose potential contract represents a significant portion of expected revenue",
"description": "One large enterprise client whose potential contract represents a significant portion of expected revenue",
"kind": "metric",
"status": "known",
"confidence": "medium",
"value": null,
"unit": null,
"evidenceIds": [],
"dependsOn": [],
"affects": [],
"parentId": null,
"childIds": []
},
{
"id": "nwasfh2",
"label": "Launching now incurs high immediate costs (£300k) but may capture revenue earlier; waiting reduces costs and allows product improvement but delays revenue",
"description": "Launching now incurs high immediate costs (£300k) but may capture revenue earlier; waiting reduces costs and allows product improvement but delays revenue",
"kind": "relationship",
"status": "supported",
"confidence": "medium",
"value": null,
"unit": null,
"evidenceIds": [],
"dependsOn": [],
"affects": [],
"parentId": null,
"childIds": []
},
{
"id": "ntpt9ki",
"label": "Probability or current status of the large enterprise customer signing their contract before launch or within the year",
"description": "Probability or current status of the large enterprise customer signing their contract before launch or within the year",
"kind": "unknown",
"status": "unknown",
"confidence": "low",
"value": null,
"unit": null,
"evidenceIds": [],
"dependsOn": [],
"affects": [],
"parentId": null,
"childIds": []
},
{
"id": "nxmeiab",
"label": "Whether competitors are actively developing similar products and how soon they might release them",
"description": "Whether competitors are actively developing similar products and how soon they might release them",
"kind": "unknown",
"status": "unknown",
"confidence": "low",
"value": null,
"unit": null,
"evidenceIds": [],
"dependsOn": [],
"affects": [],
"parentId": null,
"childIds": []
}
],
"edges": [
{
"id": "e-sum-nks00au",
"fromNodeId": "nks00au",
"toNodeId": "nz92pkx",
"relationship": "supports",
"confidence": "high",
"description": "Product is considered ready enough for launch supports the summary"
},
{
"id": "e-sum-nt03k2o",
"fromNodeId": "nt03k2o",
"toNodeId": "nz92pkx",
"relationship": "supports",
"confidence": "high",
"description": "Launching this year requires approximately £300,000 in support and implementation costs supports the summary"
},
{
"id": "e-sum-negypzh",
"fromNodeId": "negypzh",
"toNodeId": "nz92pkx",
"relationship": "supports",
"confidence": "high",
"description": "The individual or team responsible for the launch timing decision supports the summary"
},
{
"id": "e-unk-ntpt9ki",
"fromNodeId": "ntpt9ki",
"toNodeId": "nz92pkx",
"relationship": "depends_on",
"confidence": "low",
"description": "Probability or current status of the large enterprise customer signing their contract before launch or within the year is an unresolved factor for this situation"
},
{
"id": "e-unk-nxmeiab",
"fromNodeId": "nxmeiab",
"toNodeId": "nz92pkx",
"relationship": "depends_on",
"confidence": "low",
"description": "Whether competitors are actively developing similar products and how soon they might release them is an unresolved factor for this situation"
}
],
"activeUnknownNodeId": "ntpt9ki",
"resolvedNodeIds": [],
"currentSummary": "Nodes: 1 state, 3 observation, 2 metric, 1 relationship, 2 unknown | Edges: 5 total | Unknowns: 2 unresolved",
"reasoningState": {
"comparabilityStatus": "confirmed",
"comparabilityReason": "The observations are not competing like-for-like measurements.",
"comparabilityEvidence": [],
"relationshipStatus": "insufficient_information",
"relationshipReason": "There is not enough structure to classify the relationship safely.",
"relationshipAssessed": true,
"contradictionReasoningAllowed": false,
"reasoningStages": [
{
"stage": "comparability",
"status": "confirmed",
"outcome": "The observations are not competing like-for-like measurements."
},
{
"stage": "relationship",
"status": "insufficient_information",
"outcome": "There is not enough structure to classify the relationship safely."
}
]
}
},
"selectedQuestion": {
"nodeId": "nxmeiab",
"question": "What evidence would clarify whether competitors are actively developing similar products and how soon they might release them?",
"reason": "Formulated from graph context using the evidence_gathering investigation strategy.",
"strategy": "evidence_gathering",
"investigationStrategy": {
"key": "evidence_gathering",
"reason": "Selected because resolving the unknown requires evidence, signals, or measurable confirmation.",
"nodeId": "nxmeiab",
"nodeLabel": "Whether competitors are actively developing similar products and how soon they might release them",
"meaning": "whether competitors are actively developing similar products and how soon they might release them",
"actionPhrase": "launch a new software product this year or wait twelve months",
"relatedNodeIds": [
"nz92pkx"
],
"centralStatement": "I am deciding whether to launch a new software product this year or wait twelve months. The product is ready enough to launch, but one large enterprise customer could represent a significant part of the expected revenue and I do not yet know whether they will sign. Launching this year would also require around £300,000 of additional support and implementation cost. Waiting twelve months would reduce that immediate cost and give us more time to improve the product, but it would delay revenue and may allow competitors to move first. I need to decide whether there is enough evidence to launch this year or whether waiting is the safer decision."
},
"reasoningPattern": "decision",
"reasoningPatternReason": "Selected decision because the active unknown sits inside a build, continue, invest, or commercial-justification decision context.",
"questionFamily": "decision_evidence",
"allowedQuestionFamilies": [
"decision_foundation",
"decision_evidence",
"decision_threshold",
"definition"
],
"rejectedQuestionFamilies": [
"explanation",
"comparison",
"contradiction",
"diagnosis",
"prioritisation"
],
"selectedQuestionTemplate": "decision_evidence_clarification",
"questionComplexity": {
"acceptable": true,
"primaryConceptCount": 1,
"compoundQuestionSignals": [],
"abstractTermCount": 0,
"cognitiveLoad": "low",
"reasons": [],
"selectedUnknownId": "nxmeiab",
"graphCentralStatement": "I am deciding whether to launch a new software product this year or wait twelve months. The product is ready enough to launch, but one large enterprise customer could represent a significant part of the expected revenue and I do not yet know whether they will sign. Launching this year would also require around £300,000 of additional support and implementation cost. Waiting twelve months would reduce that immediate cost and give us more time to improve the product, but it would delay revenue and may allow competitors to move first. I need to decide whether there is enough evidence to launch this year or whether waiting is the safer decision."
},
"plainLanguageNormalisations": []
},
"diagnostics": {
"promptVersion": "v0.3",
"modelName": "qwen-claude:latest",
"responseDurationMs": 79111,
"validationStatus": "valid",
"nodeCount": 9,
"edgeCount": 5,
"graphReferenceValidation": {
"valid": true,
"errors": []
},
"compatibilityApplied": true,
"compatibilityChanges": [
{
"path": [
"evidence",
1,
"evidenceType"
],
"change": "Converted reported_claim to reported_statement"
}
],
"compatibilityWarnings": [
"Applied deterministic reconstruction compatibility normalisation"
],
"unknownSelectionExplanation": {
"selectedNodeId": "ntpt9ki",
"selectedNodeLabel": "Probability or current status of the large enterprise customer signing their contract before launch or within the year",
"status": "selected",
"tieType": "none",
"resolvedNodeIds": [],
"tiedCandidateIds": [
"ntpt9ki"
],
"tieBreakOrder": [
"score_desc",
"downstreamCount_desc",
"unresolvedParentUnknownCount_asc",
"label_asc"
],
"alphabeticalUsedAsReasoning": false,
"candidates": [
{
"nodeId": "ntpt9ki",
"label": "Probability or current status of the large enterprise customer signing their contract before launch or within the year",
"score": 10,
"downstreamCount": 0,
"unresolvedParentUnknownCount": 0,
"matches": {
"objective": false,
"actor": true,
"criteria": false,
"measure": false,
"terminology": false,
"constraint": false,
"pricing": false,
"implementation": false,
"optimisation": false,
"speculative": false
},
"contributions": [
{
"rule": "downstream_dependencies",
"value": 0,
"weight": 4,
"delta": 0
},
{
"rule": "actor_match",
"value": true,
"weight": 10,
"delta": 10
}
]
},
{
"nodeId": "nxmeiab",
"label": "Whether competitors are actively developing similar products and how soon they might release them",
"score": 0,
"downstreamCount": 0,
"unresolvedParentUnknownCount": 0,
"matches": {
"objective": false,
"actor": false,
"criteria": false,
"measure": false,
"terminology": false,
"constraint": false,
"pricing": false,
"implementation": false,
"optimisation": false,
"speculative": false
},
"contributions": [
{
"rule": "downstream_dependencies",
"value": 0,
"weight": 4,
"delta": 0
}
]
}
],
"selected": {
"nodeId": "ntpt9ki",
"label": "Probability or current status of the large enterprise customer signing their contract before launch or within the year",
"score": 10,
"downstreamCount": 0,
"unresolvedParentUnknownCount": 0,
"matches": {
"objective": false,
"actor": true,
"criteria": false,
"measure": false,
"terminology": false,
"constraint": false,
"pricing": false,
"implementation": false,
"optimisation": false,
"speculative": false
},
"contributions": [
{
"rule": "downstream_dependencies",
"value": 0,
"weight": 4,
"delta": 0
},
{
"rule": "actor_match",
"value": true,
"weight": 10,
"delta": 10
}
]
},
"competitors": [
{
"nodeId": "nxmeiab",
"label": "Whether competitors are actively developing similar products and how soon they might release them",
"score": 0,
"downstreamCount": 0,
"unresolvedParentUnknownCount": 0,
"matches": {
"objective": false,
"actor": false,
"criteria": false,
"measure": false,
"terminology": false,
"constraint": false,
"pricing": false,
"implementation": false,
"optimisation": false,
"speculative": false
},
"contributions": [
{
"rule": "downstream_dependencies",
"value": 0,
"weight": 4,
"delta": 0
}
],
"outrankedBy": {
"scoreDelta": 10,
"downstreamDelta": 0,
"unresolvedPrerequisiteDelta": 0,
"labelOrderWinner": null
}
}
],
"summary": {
"candidateCount": 2,
"selectedReason": "highest_score=10; downstream=0; unresolved_prerequisites=0"
}
},
"reconstructionQuestion": "What is the current stage or probability of securing a signed contract with this large enterprise customer within the next three to six months?",
"reconstructionQuestionAccepted": false,
"reconstructionQuestionRejectionReasons": [
"reconstruction_question_not_authoritative",
"graph_backed_pipeline_required"
],
"finalGraphBackedQuestion": "What evidence would clarify whether competitors are actively developing similar products and how soon they might release them?",
"selectedUnknownNodeId": "nxmeiab",
"decompositionApplied": false,
"questionComplexityAssessment": {
"acceptable": true,
"primaryConceptCount": 1,
"compoundQuestionSignals": [],
"abstractTermCount": 0,
"cognitiveLoad": "low",
"reasons": [],
"selectedUnknownId": "nxmeiab",
"graphCentralStatement": "I am deciding whether to launch a new software product this year or wait twelve months. The product is ready enough to launch, but one large enterprise customer could represent a significant part of the expected revenue and I do not yet know whether they will sign. Launching this year would also require around £300,000 of additional support and implementation cost. Waiting twelve months would reduce that immediate cost and give us more time to improve the product, but it would delay revenue and may allow competitors to move first. I need to decide whether there is enough evidence to launch this year or whether waiting is the safer decision."
},
"answerabilityAssessment": {
"independentlyAnswerable": false,
"reason": "This unknown still bundles multiple prerequisite evidence dimensions, so it should be decomposed before it becomes the selected question.",
"prerequisiteConceptCount": 5,
"decompositionRequired": true
},
"independentlyAnswerable": false,
"prerequisiteConceptCount": 5,
"decompositionTriggeredByAnswerability": true,
"decompositionReason": "Formulated from graph context using the evidence_gathering investigation strategy.",
"selectedContainerUnknown": "ntpt9ki",
"selectedChildUnknown": "nxmeiab",
"reasoningPattern": "decision",
"questionFamily": "decision_evidence",
"allowedQuestionFamilies": [
"decision_foundation",
"decision_evidence",
"decision_threshold",
"definition"
],
"rejectedQuestionFamilies": [
"explanation",
"comparison",
"contradiction",
"diagnosis",
"prioritisation"
],
"selectedQuestionTemplate": "decision_evidence_clarification",
"reasoningPatternReason": "Selected decision because the active unknown sits inside a build, continue, invest, or commercial-justification decision context.",
"reasoningPatternValidation": {
"activePattern": "decision",
"valid": true,
"reason": "Initial graph-backed selection produced a reasoning-pattern-compatible question."
},
"patternCompatibleNodeCount": 0,
"incompatibleNodeIds": [],
"compatibilityFailures": [],
"replacementActions": [],
"graphReasoningIntegrity": null,
"noQuestionReason": null
},
"assessment": {
"version": "v0.1",
"assessedAt": "2026-08-17T13:45:20.062Z",
"confidence": "low",
"phase": {
"value": "exploring",
"confidence": "medium",
"signals": [
"4 initial observations gathered",
"Resolution progress low (0/9 or unknown)"
],
"evidence": {
"resolvedNodeCount": 0,
"activeUnknownCount": 2,
"unknownResolutionRatio": null,
"observationDensity": 4,
"evidenceDepth": "shallow"
}
},
"progress": {
"value": "cannot_determine",
"confidence": "low",
"signals": [
"Insufficient data for progress assessment",
"Total nodes: 9, resolved: 0"
],
"evidence": {
"turnCount": 0,
"recentResolutionsLastTurn": 0,
"newUnknownsPerTurn": null,
"repeatedNodeIds": []
}
},
"conversationHealth": {
"value": "healthy",
"confidence": "medium",
"signals": [
"Active investigation in progress: 2 unresolved unknown(s)",
"Question actively driving the investigation forward"
],
"evidence": {
"questionTypeDistribution": null,
"activeUnknownCount": 2,
"resolvedNodeRatio": null,
"hasActiveQuestion": true,
"summaryLength": 108
}
}
}
}
+42
View File
@@ -1,7 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
import { determineGraphBackedQuestion } from "@/lib/graph/apply-proposal.js";
import { validateGraphReferences } from "@/lib/graph/utils.js";
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
import liveProductLaunchStartResponse from "@/tests/fixtures/live-product-launch-start-response.json";
const mockAnalyseScenario = vi.fn();
const MOCK_CONFIG = { OLLAMA_MODEL: "configured" };
@@ -588,6 +590,46 @@ describe("lib/graph/orchestrator startCase", () => {
);
});
it("retains ownership when the strongest target's formulated question is rejected", () => {
const situationGraph = structuredClone(
liveProductLaunchStartResponse.situationGraph,
);
const result = determineGraphBackedQuestion({ situationGraph });
expect(result.success).toBe(true);
expect(result.deterministicSelection?.nodeId ?? null).toBe("ntpt9ki");
expect(result.updatedSituationGraph.activeUnknownNodeId).toBe("ntpt9ki");
expect(result.selectedQuestion).toBeNull();
expect(result.noQuestionReason).toBe(
"The selected investigation target remains active, but its current graph-backed question formulation was rejected as too complex.",
);
expect(result.selectedChildUnknown).toBe("ntpt9ki");
expect(result.selectedUnknownAfter).toBe("ntpt9ki");
expect(result.selectedQuestion?.nodeId ?? null).toBe(null);
});
it("replays the captured live product-launch start graph through deterministic graph-backed question selection", () => {
const situationGraph = liveProductLaunchStartResponse.situationGraph;
const result = determineGraphBackedQuestion({ situationGraph });
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.activeUnknownNodeId).toBe("ntpt9ki");
expect(result.deterministicSelection?.nodeId ?? null).toBe("ntpt9ki");
expect(result.answerabilityAssessment?.independentlyAnswerable).toBe(false);
expect(result.answerabilityAssessment?.decompositionRequired).toBe(true);
expect(result.decompositionAttempted).toBe(true);
expect(result.decompositionPerformed).toBe(false);
expect(result.selectedChildUnknown).toBe("ntpt9ki");
expect(result.selectedUnknownAfter).toBe("ntpt9ki");
expect(result.selectedQuestion).toBeNull();
expect(result.noQuestionReason).toBe(
"The selected investigation target remains active, but its current graph-backed question formulation was rejected as too complex.",
);
expect(result.deterministicSelection?.nodeId).not.toBe("nxmeiab");
});
it("includes compatibility diagnostics when provided by analysis", async () => {
mockAnalyseScenario.mockResolvedValue(
makeAnalysisResult({
+131 -11
View File
@@ -621,7 +621,7 @@ describe("selectActiveUnknownCandidate", () => {
expect(parentScore.score).toBeGreaterThan(childScore.score);
});
it("returns ambiguous for a complete unresolved tie instead of label-based winner", () => {
it("returns a deterministic winner for a complete unresolved tie", () => {
const unknownA = makeNode({
id: "tie-a",
label: "Magnitude and nature of cash outflows",
@@ -651,16 +651,12 @@ describe("selectActiveUnknownCandidate", () => {
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result).toMatchObject({
selectedNode: null,
status: "ambiguous",
tieType: "complete_unresolved_tie",
tiedCandidateIds: ["tie-a", "tie-b"],
});
expect(result.nodeId).toBeUndefined();
expect(result.status).toBe("selected");
expect(result.tieType).toBe("complete_unresolved_tie");
expect(result.nodeId).toBeTruthy();
});
it("alphabetical renaming does not resolve a complete tie", () => {
it("stable ordering resolves a complete tie deterministically", () => {
const unknownA = makeNode({
id: "tie-a",
label: "Unknown B",
@@ -683,8 +679,132 @@ describe("selectActiveUnknownCandidate", () => {
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result.status).toBe("ambiguous");
expect(result.tiedCandidateIds.sort()).toEqual(["tie-a", "tie-b"]);
expect(result.status).toBe("selected");
expect(result.nodeId).toBeTruthy();
});
it("preserves the active candidate when it remains eligible and substantively tied", () => {
const unknownA = makeNode({
id: "tie-a",
label: "Unknown B",
description: "Unknown factor one.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const unknownB = makeNode({
id: "tie-b",
label: "Unknown A",
description: "Unknown factor two.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const graph = makeGraph({
centralStatement: "Two conflicting signals remain unresolved.",
nodes: [unknownA, unknownB],
edges: [],
activeUnknownNodeId: "tie-a",
resolvedNodeIds: [],
currentSummary: "Tie case",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result.status).toBe("selected");
expect(result.nodeId).toBe("tie-a");
});
it("transfers ownership when the active candidate substantively loses on score", () => {
const loser = makeNode({
id: "active-loser",
label: "Unknown branch",
description: "Speculative future branch.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const winner = makeNode({
id: "clear-winner",
label: "Customer value definition",
description: "Need customer value definition.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const graph = makeGraph({
centralStatement: "Decision context",
nodes: [loser, winner],
edges: [],
activeUnknownNodeId: loser.id,
resolvedNodeIds: [],
currentSummary: "Score loss case",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result.status).toBe("selected");
expect(result.nodeId).toBe(winner.id);
});
it("transfers ownership when the active candidate is resolved or ineligible", () => {
const resolvedActive = makeNode({
id: "resolved-active",
label: "Resolved unknown",
description: "Resolved unknown.",
kind: "unknown",
status: "resolved",
confidence: "high",
});
const remaining = makeNode({
id: "remaining-unknown",
label: "Customer value definition",
description: "Need customer value definition.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const graph = makeGraph({
centralStatement: "Decision context",
nodes: [resolvedActive, remaining],
edges: [],
activeUnknownNodeId: resolvedActive.id,
resolvedNodeIds: [resolvedActive.id],
currentSummary: "Resolved active case",
});
const result = selectActiveUnknownCandidate(graph, graph.resolvedNodeIds);
expect(result.status).toBe("selected");
expect(result.nodeId).toBe(remaining.id);
});
it("keeps existing deterministic fallback when there is no active candidate", () => {
const unknownA = makeNode({
id: "fallback-a",
label: "Unknown B",
description: "Unknown factor one.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const unknownB = makeNode({
id: "fallback-b",
label: "Unknown A",
description: "Unknown factor two.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const graph = makeGraph({
centralStatement: "Two conflicting signals remain unresolved.",
nodes: [unknownA, unknownB],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Tie case",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result.status).toBe("selected");
expect(result.nodeId).toBeTruthy();
});
});