fix(reasoning): exclude terminal nodes from active selector

This commit is contained in:
2026-08-14 09:02:08 +01:00
parent 865565b7af
commit 34eb0cd4e3
4 changed files with 265 additions and 2 deletions
+161 -1
View File
@@ -1,7 +1,10 @@
import { describe, expect, it } from "vitest";
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
import { validateGraphReferences } from "@/lib/graph/utils.js";
import {
selectActiveUnknownCandidate,
validateGraphReferences,
} from "@/lib/graph/utils.js";
function makeComparabilityUpdateFixture() {
const comparabilityUnknown = makeNode({
@@ -3660,6 +3663,163 @@ describe("applyValidatedProposal", () => {
// 60B.11 — prerequisite-aware question targeting
// ============================================
describe("60B.42 — active selector terminal-status guard", () => {
it("excludes known nodes and returns a genuine unresolved candidate", () => {
const knownDecision = makeNode({
id: "n-known-decision",
label: "Known decision node",
description: "This unknown-shaped node is already known and must be excluded.",
kind: "unknown",
status: "known",
confidence: "high",
});
const unresolvedCandidate = makeNode({
id: "n-unresolved-candidate",
label: "Unresolved candidate",
description: "Need customer evidence because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const graph = makeGraph({
centralStatement: "Test graph",
nodes: [knownDecision, unresolvedCandidate],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Known and unresolved candidates present.",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result?.nodeId).toBe("n-unresolved-candidate");
expect(result?.nodeId).not.toBe("n-known-decision");
});
it("returns null when the graph contains only known-status unknown nodes", () => {
const knownOnly = makeNode({
id: "n-known-only",
label: "Known-only node",
description: "This unknown-shaped node is already terminal.",
kind: "unknown",
status: "known",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Known-only graph",
nodes: [knownOnly],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "No unresolved candidates remain.",
});
expect(selectActiveUnknownCandidate(graph, [])).toBeNull();
});
it("excludes resolved nodes even when their IDs are absent from resolvedNodeIds", () => {
const resolvedUnknown = makeNode({
id: "n-resolved-terminal",
label: "Resolved terminal node",
description: "This node is resolved and must not enter scoring.",
kind: "unknown",
status: "resolved",
confidence: "high",
});
const unresolvedCandidate = makeNode({
id: "n-unresolved-fallback",
label: "Fallback unresolved node",
description: "Need unresolved evidence to continue the investigation.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Resolved status guard graph",
nodes: [resolvedUnknown, unresolvedCandidate],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Resolved node omitted from resolvedNodeIds on purpose.",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result?.nodeId).toBe("n-unresolved-fallback");
expect(result?.nodeId).not.toBe("n-resolved-terminal");
});
it("excludes contradicted nodes even when their IDs are absent from resolvedNodeIds", () => {
const contradictedUnknown = makeNode({
id: "n-contradicted-terminal",
label: "Contradicted terminal node",
description: "This node is contradicted and must not enter scoring.",
kind: "unknown",
status: "contradicted",
confidence: "high",
});
const unresolvedCandidate = makeNode({
id: "n-unresolved-after-contradiction",
label: "Remaining unresolved node",
description: "Need remaining unresolved evidence after contradiction.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Contradicted status guard graph",
nodes: [contradictedUnknown, unresolvedCandidate],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Contradicted node omitted from resolvedNodeIds on purpose.",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result?.nodeId).toBe("n-unresolved-after-contradiction");
expect(result?.nodeId).not.toBe("n-contradicted-terminal");
});
it("preserves existing unresolved ranking when both candidates remain genuinely unresolved", () => {
const higherPriority = makeNode({
id: "n-higher-priority",
label: "Customer evidence priority",
description:
"Need customer evidence and success threshold clarity because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const lowerPriority = makeNode({
id: "n-lower-priority",
label: "Implementation detail",
description:
"Need technical implementation detail for a possible future feature.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Ranking preservation graph",
nodes: [higherPriority, lowerPriority],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Two unresolved candidates remain.",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result?.nodeId).toBe("n-higher-priority");
});
});
describe("60B.11 — prerequisite-aware question targeting", () => {
// Test 1: same-proposal selected unknown with no prerequisite is preferred
it("prefers a same-proposal added unknown when model selects it and it has no unresolved depends_on prerequisite", () => {