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
@@ -1,6 +1,13 @@
import { formulateQuestionForTarget } from "@/lib/graph/focused-investigation";
import { getProvider, getProviderModelName } from "@/lib/llm/provider";
import {
assessStrategyCompatibility,
formulateQuestionForTarget,
isStrategyQuestionTemplateCompatible,
} from "@/lib/graph/focused-investigation";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
const STRATEGY_ASSESSMENT_TIMEOUT_MS = 10000;
async function post(request) {
try {
const body = await request.json();
@@ -19,18 +26,59 @@ async function post(request) {
);
}
const result = formulateQuestionForTarget({
const proposed = formulateQuestionForTarget({
situationGraph: body.situationGraph,
targetNodeId: body.targetNodeId,
});
if (!result.success) {
if (!proposed.success) {
return Response.json(
{ success: false, error: result.error },
{ success: false, error: proposed.error },
{ status: 400 },
);
}
let result = proposed;
if (proposed.investigationStrategy) {
const controller = new AbortController();
const timeout = setTimeout(
() => controller.abort(),
STRATEGY_ASSESSMENT_TIMEOUT_MS,
);
let strategyCompatibilityVerdict = {
strategyKey: proposed.investigationStrategy.key,
relevance: "unavailable",
};
try {
const assessment = await assessStrategyCompatibility({
provider: getProvider(),
modelName: getProviderModelName(),
targetLabel: body.situationGraph.nodes.find((node) => node.id === body.targetNodeId)?.label,
targetDescription: body.situationGraph.nodes.find((node) => node.id === body.targetNodeId)?.description,
centralStatement: body.situationGraph.centralStatement,
proposedStrategy: proposed.investigationStrategy,
signal: controller.signal,
});
if (assessment.success) strategyCompatibilityVerdict = assessment.verdict;
} catch {
// A timed-out or unavailable assessment is explicitly non-authorizing.
} finally {
clearTimeout(timeout);
}
result = formulateQuestionForTarget({
situationGraph: body.situationGraph,
targetNodeId: body.targetNodeId,
strategyCompatibilityVerdict,
strategyTemplateCompatibility:
strategyCompatibilityVerdict.relevance === "compatible" &&
!isStrategyQuestionTemplateCompatible(proposed.investigationStrategy)
? "incompatible"
: undefined,
});
}
return Response.json({
success: true,
targetNodeId: result.targetNodeId,
+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,
@@ -0,0 +1,186 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
const generateReconstruction = vi.fn();
vi.mock("@/lib/llm/provider", () => ({
getProvider: () => ({ generateReconstruction }),
getProviderModelName: () => "test-model",
}));
vi.mock("@/lib/supabase/api-auth.js", () => ({
withAuthenticatedApi: (handler) => handler,
}));
function makeRecruitmentGraph() {
const target = makeNode({
id: "recruitment-target",
label: "Staffing certainty gap",
description:
"Whether additional skilled staff needed for expansion can be recruited",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
return makeGraph({
centralStatement:
"We need to compare our current baseline against the comparable state before staffing is secured for expansion.",
currentSummary: "Test summary",
nodes: [target],
edges: [],
activeUnknownNodeId: target.id,
resolvedNodeIds: [],
});
}
function makeHistoricalGraph() {
const target = makeNode({
id: "historical-target",
label: "Baseline conversion rate",
description:
"Need the previous baseline because the change cannot be assessed without it.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
return makeGraph({
centralStatement: "Assess the change against the comparable prior baseline.",
currentSummary: "Test summary",
nodes: [target],
edges: [],
activeUnknownNodeId: target.id,
resolvedNodeIds: [],
});
}
function makeRequest(graph, targetNodeId = graph.nodes[0].id) {
return new Request("http://localhost/api/focused-investigation/formulate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ situationGraph: graph, targetNodeId }),
});
}
describe("POST /api/focused-investigation/formulate semantic compatibility", () => {
beforeEach(() => {
generateReconstruction.mockReset();
});
afterEach(() => {
vi.useRealTimers();
});
it("suppresses an assessor-incompatible strategy without changing the selected target or graph", async () => {
const graph = makeRecruitmentGraph();
const snapshot = JSON.stringify(graph);
generateReconstruction.mockResolvedValue({
response: {
strategyKey: "baseline_reconstruction",
relevance: "incompatible",
},
});
const { POST } = await import("@/app/api/focused-investigation/formulate/route.js");
const response = await POST(makeRequest(graph));
const json = await response.json();
expect(response.status).toBe(200);
expect(json.targetNodeId).toBe("recruitment-target");
expect(json.strategy).toBeNull();
expect(json.question).toBe(
"What evidence would clarify whether additional skilled staff needed for expansion can be recruited?",
);
expect(JSON.stringify(graph)).toBe(snapshot);
expect(generateReconstruction).toHaveBeenCalledTimes(1);
});
it("does not let a compatible semantic verdict authorize a grammatically incompatible baseline template", async () => {
const graph = makeRecruitmentGraph();
generateReconstruction.mockResolvedValue({
response: {
strategyKey: "baseline_reconstruction",
relevance: "compatible",
},
});
const { POST } = await import("@/app/api/focused-investigation/formulate/route.js");
const response = await POST(makeRequest(graph));
const json = await response.json();
expect(json.strategy).toBeNull();
expect(json.question.toLowerCase()).not.toContain("before whether");
expect(json.question).toContain("What evidence would clarify whether");
});
it("preserves a compatible historical strategy when its template is suitable", async () => {
const graph = makeHistoricalGraph();
generateReconstruction.mockResolvedValue({
response: {
strategyKey: "baseline_reconstruction",
relevance: "compatible",
},
});
const { POST } = await import("@/app/api/focused-investigation/formulate/route.js");
const response = await POST(makeRequest(graph));
const json = await response.json();
expect(json.strategy).toBe("baseline_reconstruction");
expect(json.question).toContain("What was the comparable state before");
});
it("treats invalid or mismatched assessment output as non-authorizing", async () => {
const graph = makeRecruitmentGraph();
generateReconstruction.mockResolvedValue({
response: { strategyKey: "evidence_gathering", relevance: "compatible" },
});
const { POST } = await import("@/app/api/focused-investigation/formulate/route.js");
const response = await POST(makeRequest(graph));
const json = await response.json();
expect(json.strategy).toBeNull();
expect(json.question).toContain("What evidence would clarify whether");
});
it("treats provider unavailability as non-authorizing fallback", async () => {
const graph = makeRecruitmentGraph();
generateReconstruction.mockRejectedValue(
Object.assign(new Error("provider unavailable"), { code: "PROVIDER_UNAVAILABLE" }),
);
const { POST } = await import("@/app/api/focused-investigation/formulate/route.js");
const response = await POST(makeRequest(graph));
const json = await response.json();
expect(response.status).toBe(200);
expect(json.strategy).toBeNull();
expect(json.question).toContain("What evidence would clarify whether");
});
it("aborts the one assessment invocation at the route deadline and falls back", async () => {
vi.useFakeTimers();
const graph = makeRecruitmentGraph();
let observedSignal;
generateReconstruction.mockImplementation((_, __, ___, { signal }) => {
observedSignal = signal;
return new Promise((_, reject) => {
signal.addEventListener("abort", () => {
const error = new Error("assessment aborted");
error.name = "AbortError";
reject(error);
}, { once: true });
});
});
const { POST } = await import("@/app/api/focused-investigation/formulate/route.js");
const responsePromise = POST(makeRequest(graph));
await vi.advanceTimersByTimeAsync(10000);
const response = await responsePromise;
const json = await response.json();
expect(observedSignal.aborted).toBe(true);
expect(json.strategy).toBeNull();
expect(json.question).toContain("What evidence would clarify whether");
});
});
@@ -65,6 +65,40 @@ describe("formulateQuestionForTarget — explicit node selection", () => {
expect(JSON.stringify(graph)).toBe(snapshot);
});
it("forwards an ephemeral incompatible compatibility verdict without changing the selected target or graph", () => {
const unknown = makeNode({
id: "historical-target",
label: "Earlier comparable state",
description: "Whether the prior staffing level was sufficient for expansion",
kind: "unknown",
status: "unknown",
});
const graph = makeGraph({
centralStatement:
"Compare the prior staffing baseline with the level needed before the expansion.",
currentSummary: "initial summary",
nodes: [unknown],
edges: [],
activeUnknownNodeId: unknown.id,
resolvedNodeIds: [],
});
const snapshot = JSON.stringify(graph);
const result = formulateQuestionForTarget({
situationGraph: graph,
targetNodeId: unknown.id,
strategyCompatibilityVerdict: {
strategyKey: "baseline_reconstruction",
relevance: "incompatible",
},
});
expect(result.success).toBe(true);
expect(result.targetNodeId).toBe(unknown.id);
expect(result.strategy).toBeNull();
expect(JSON.stringify(graph)).toBe(snapshot);
});
it("does not invoke global selection (selectActiveUnknownCandidate / determineGraphBackedQuestion)", () => {
const graph = makeTestGraph();
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" });
+246
View File
@@ -1614,6 +1614,252 @@ describe("60B.75 — real decision detection for sufficiency question", () => {
});
});
// ── Experiment 3 Iteration 3 — Deterministic Reproduction Recovery ──────────
// Hypothesis: A Whether ... uncertainty passed into baseline_reconstruction
// interpolates into a template requiring a noun phrase, producing malformed output.
// Fixed input: "Whether additional skilled staff needed for expansion can be recruited"
describe("Experiment 3 — Whether proposition in baseline reconstruction", () => {
it("suppresses the reproduced malformed baseline template at the focused-investigation compatibility boundary", () => {
const unknown = makeNode({
id: "n-experiment3-staffing",
label: "Staffing certainty gap",
description:
"Whether additional skilled staff needed for expansion can be recruited",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement:
"We need to compare our current baseline against the comparable state before staffing is secured for expansion.",
nodes: [unknown],
edges: [],
activeUnknownNodeId: unknown.id,
resolvedNodeIds: [],
currentSummary: "Test summary",
});
const proposed = formulateQuestion({ node: unknown, graph });
// Preserve the deterministic reproduction that identified the bug.
expect(proposed.strategy).toBe("baseline_reconstruction");
expect(proposed.question).toBe(
"What was the comparable state before whether additional skilled staff needed for expansion can be recruited?",
);
const corrected = formulateQuestion({
node: unknown,
graph,
context: {
strategyCompatibilityVerdict: {
strategyKey: "baseline_reconstruction",
relevance: "compatible",
},
strategyTemplateCompatibility: "incompatible",
},
});
expect(corrected.strategy).toBeNull();
expect(corrected.question).toBe(
"What evidence would clarify whether additional skilled staff needed for expansion can be recruited?",
);
expect(corrected.question.toLowerCase()).not.toContain("before whether");
});
});
// ── Experiment 14 — Deterministic Compatibility Verdict Contract ───────────
describe("Experiment 14 — strategy compatibility verdict", () => {
function makeBaselineWhetherFixture() {
const unknown = makeNode({
id: "n-experiment14-staffing",
label: "Staffing certainty gap",
description:
"Whether additional skilled staff needed for expansion can be recruited",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraphFor(unknown, {
centralStatement:
"We need to compare our current baseline against the comparable state before staffing is secured for expansion.",
});
return { unknown, graph };
}
it("preserves existing strategy and question when no verdict is supplied", () => {
const { unknown, graph } = makeBaselineWhetherFixture();
const result = formulateQuestion({ node: unknown, graph });
expect(result.strategy).toBe("baseline_reconstruction");
expect(result.question).toBe(
"What was the comparable state before whether additional skilled staff needed for expansion can be recruited?",
);
});
it("permits the matching proposed strategy when the verdict is compatible", () => {
const { unknown, graph } = makeBaselineWhetherFixture();
const result = formulateQuestion({
node: unknown,
graph,
context: {
strategyCompatibilityVerdict: {
strategyKey: "baseline_reconstruction",
relevance: "compatible",
},
},
});
expect(result.strategy).toBe("baseline_reconstruction");
expect(result.question).toBe(
"What was the comparable state before whether additional skilled staff needed for expansion can be recruited?",
);
});
it("suppresses an incompatible proposed strategy and uses the existing evidence path", () => {
const { unknown, graph } = makeBaselineWhetherFixture();
const graphSnapshot = JSON.stringify(graph);
const result = formulateQuestion({
node: unknown,
graph,
context: {
strategyCompatibilityVerdict: {
strategyKey: "baseline_reconstruction",
relevance: "incompatible",
},
},
});
expect(result.strategy).toBeNull();
expect(result.question).toBe(
"What evidence would clarify whether additional skilled staff needed for expansion can be recruited?",
);
expect(JSON.stringify(graph)).toBe(graphSnapshot);
});
it("does not let invalid or mismatched verdicts authorise a proposed strategy", () => {
const { unknown, graph } = makeBaselineWhetherFixture();
const invalid = formulateQuestion({
node: unknown,
graph,
context: {
strategyCompatibilityVerdict: {
strategyKey: "baseline_reconstruction",
relevance: "unknown",
},
},
});
const mismatched = formulateQuestion({
node: unknown,
graph,
context: {
strategyCompatibilityVerdict: {
strategyKey: "evidence_gathering",
relevance: "compatible",
},
},
});
expect(invalid.strategy).toBeNull();
expect(mismatched.strategy).toBeNull();
expect(invalid.question).toContain("What evidence would clarify whether");
expect(mismatched.question).toContain("What evidence would clarify whether");
});
it("allows a compatible historical strategy for a coherent Whether-proposition", () => {
const unknown = makeNode({
id: "n-experiment14-historical",
label: "Earlier comparable state",
description: "Whether the prior staffing level was sufficient for expansion",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraphFor(unknown, {
centralStatement:
"Compare the prior staffing baseline with the level needed before the expansion.",
});
const result = formulateQuestion({
node: unknown,
graph,
context: {
strategyCompatibilityVerdict: {
strategyKey: "baseline_reconstruction",
relevance: "compatible",
},
},
});
expect(result.strategy).toBe("baseline_reconstruction");
});
});
// ── Experiment 6 — Graph-Context Causal Comparison ─────────────────────────────
// Hypothesis: baseline_reconstruction strategy on the recruitment unknown is driven
// by historical-baseline vocabulary in graph-level context (centralStatement), not
// in the recruitment uncertainty itself. Removing that vocabulary should prevent
// the specific baseline-selection route while preserving all node-level input.
describe("Experiment 6 — Isolate graph-context influence on strategy selection", () => {
it("removing baseline vocabulary from centralStatement changes strategy away from baseline_reconstruction", () => {
// ── shared open-question fixture (identical in both variants) ──
const openQuestion = makeNode({
id: "n-experiment3-staffing",
label: "Staffing certainty gap",
description:
"Whether additional skilled staff needed for expansion can be recruited",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
// ── Variant A — baseline context present (existing fixture) ──
const graphA = makeGraph({
centralStatement:
"We need to compare our current baseline against the comparable state before staffing is secured for expansion.",
nodes: [openQuestion],
edges: [],
activeUnknownNodeId: openQuestion.id,
resolvedNodeIds: [],
currentSummary: "Test summary",
});
// ── Variant B — baseline context absent (neutral wording) ──
const graphB = makeGraph({
centralStatement:
"Current staffing levels are being reviewed as we prepare for the planned expansion.",
nodes: [openQuestion],
edges: [],
activeUnknownNodeId: openQuestion.id,
resolvedNodeIds: [],
currentSummary: "Test summary",
});
// ── Execute both variants ──
const resultA = formulateQuestion({ node: openQuestion, graph: graphA });
const resultB = formulateQuestion({ node: openQuestion, graph: graphB });
// Record for report
console.log("[Exp6-A] strategy:", resultA.strategy, "| question:", resultA.question);
console.log("[Exp6-B] strategy:", resultB.strategy, "| question:", resultB.question);
// Causal contrast: baseline_reconstruction requires BOTH graph-level AND node-level
// baseline vocabulary to match the first (strict) selection path.
// Variant A must select baseline_reconstruction.
expect(resultA.strategy).toBe("baseline_reconstruction");
// Variant B must NOT select baseline_reconstruction (graph-level baseline removed).
expect(resultB.strategy).not.toBe("baseline_reconstruction");
// The malformed "before whether" output must disappear in Variant B.
expect(resultB.question.toLowerCase()).not.toContain(
"before whether",
);
});
});
// ── 60B.80 — sufficiency re-selection after proposal application (no false State B) ────
describe("60B.80 — no false State B after proposal adds unresolved material factor", () => {