Implemented Investigation Strategy
This commit is contained in:
@@ -80,6 +80,14 @@ export default function DiagnosticsView({ result }) {
|
|||||||
? `${validationIcons.valid} valid`
|
? `${validationIcons.valid} valid`
|
||||||
: `${validationIcons.invalid} invalid`,
|
: `${validationIcons.invalid} invalid`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Investigation strategy",
|
||||||
|
value:
|
||||||
|
diagnostics.investigationStrategy?.key ||
|
||||||
|
diagnostics.investigationStrategy ||
|
||||||
|
result.selectedQuestion?.strategy ||
|
||||||
|
"?",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const errors = [
|
const errors = [
|
||||||
|
|||||||
@@ -656,6 +656,7 @@ export function applyValidatedProposal({ situationGraph, proposal }) {
|
|||||||
formulatedQuestion?.question || deterministicSelection.question,
|
formulatedQuestion?.question || deterministicSelection.question,
|
||||||
reason: formulatedQuestion?.reason || deterministicSelection.reason,
|
reason: formulatedQuestion?.reason || deterministicSelection.reason,
|
||||||
strategy: formulatedQuestion?.strategy,
|
strategy: formulatedQuestion?.strategy,
|
||||||
|
investigationStrategy: formulatedQuestion?.investigationStrategy,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function buildUpdateDiagnostics({
|
|||||||
normalisationsApplied,
|
normalisationsApplied,
|
||||||
graph,
|
graph,
|
||||||
graphReferenceValidation,
|
graphReferenceValidation,
|
||||||
|
selectedQuestion,
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
promptVersion: promptVersion ?? "v0.4",
|
promptVersion: promptVersion ?? "v0.4",
|
||||||
@@ -66,6 +67,10 @@ function buildUpdateDiagnostics({
|
|||||||
errors: [],
|
errors: [],
|
||||||
},
|
},
|
||||||
normalisationsApplied: normalisationsApplied ?? [],
|
normalisationsApplied: normalisationsApplied ?? [],
|
||||||
|
investigationStrategy:
|
||||||
|
selectedQuestion?.investigationStrategy ??
|
||||||
|
selectedQuestion?.strategy ??
|
||||||
|
null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +289,7 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
|||||||
normalisationsApplied: parsedProposal.normalisationsApplied,
|
normalisationsApplied: parsedProposal.normalisationsApplied,
|
||||||
graph: situationGraph,
|
graph: situationGraph,
|
||||||
graphReferenceValidation: graphReferenceValidation,
|
graphReferenceValidation: graphReferenceValidation,
|
||||||
|
selectedQuestion: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
statusCode:
|
statusCode:
|
||||||
@@ -313,6 +319,7 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
|||||||
normalisationsApplied: parsedProposal.normalisationsApplied,
|
normalisationsApplied: parsedProposal.normalisationsApplied,
|
||||||
graph: applicationResult.updatedSituationGraph,
|
graph: applicationResult.updatedSituationGraph,
|
||||||
graphReferenceValidation: applicationResult.graphReferenceValidation,
|
graphReferenceValidation: applicationResult.graphReferenceValidation,
|
||||||
|
selectedQuestion: applicationResult.selectedQuestion,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -328,6 +335,7 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
|||||||
normalisationsApplied: parsedProposal.normalisationsApplied,
|
normalisationsApplied: parsedProposal.normalisationsApplied,
|
||||||
graph: situationGraph,
|
graph: situationGraph,
|
||||||
graphReferenceValidation,
|
graphReferenceValidation,
|
||||||
|
selectedQuestion: null,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,41 @@ function toGerundPhrase(phrase) {
|
|||||||
return [gerund, ...rest].join(" ");
|
return [gerund, ...rest].join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectStrategy({ node, graph, relatedNodes, combinedText, meaning }) {
|
function buildInvestigationStrategy({
|
||||||
|
key,
|
||||||
|
reason,
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
relatedNodes,
|
||||||
|
meaning,
|
||||||
|
actionPhrase,
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
reason,
|
||||||
|
nodeId: node?.id ?? null,
|
||||||
|
nodeLabel: node?.label ?? null,
|
||||||
|
meaning,
|
||||||
|
actionPhrase,
|
||||||
|
relatedNodeIds: relatedNodes.map((relatedNode) => relatedNode.id),
|
||||||
|
centralStatement: graph?.centralStatement ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectInvestigationStrategy({ node, graph, context = {} }) {
|
||||||
|
const relatedNodes = collectRelatedNodes(node, graph);
|
||||||
|
const meaning = extractMeaning(node);
|
||||||
|
const combinedText = [
|
||||||
|
node?.label,
|
||||||
|
node?.description,
|
||||||
|
...relatedNodes.map((relatedNode) => relatedNode.label),
|
||||||
|
...relatedNodes.map((relatedNode) => relatedNode.description),
|
||||||
|
graph?.centralStatement,
|
||||||
|
...(context.resolvedValues || []),
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
const text = normaliseText(combinedText);
|
const text = normaliseText(combinedText);
|
||||||
const nodeText = normaliseText(
|
const nodeText = normaliseText(
|
||||||
`${node?.label || ""} ${node?.description || ""}`,
|
`${node?.label || ""} ${node?.description || ""}`,
|
||||||
@@ -172,21 +206,22 @@ function detectStrategy({ node, graph, relatedNodes, combinedText, meaning }) {
|
|||||||
nodeText,
|
nodeText,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (/\b(customer|user|buyer|stakeholder|recipient|audience)\b/.test(text)) {
|
|
||||||
return { strategy: "actor/customer", meaning, actionPhrase };
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasBaselineLanguage =
|
const hasBaselineLanguage =
|
||||||
/\b(before|previous|baseline|prior|comparable state)\b/.test(text);
|
/\b(before|previous|baseline|prior|comparable state)\b/.test(text);
|
||||||
const hasPrimaryBaselineLanguage =
|
const hasPrimaryBaselineLanguage =
|
||||||
/\b(before|previous|baseline|prior|comparable state)\b/.test(nodeText);
|
/\b(before|previous|baseline|prior|comparable state)\b/.test(nodeText);
|
||||||
|
|
||||||
if (hasBaselineLanguage && hasPrimaryBaselineLanguage) {
|
if (hasBaselineLanguage && hasPrimaryBaselineLanguage) {
|
||||||
return { strategy: "baseline", meaning, actionPhrase };
|
return buildInvestigationStrategy({
|
||||||
}
|
key: "baseline_reconstruction",
|
||||||
|
reason:
|
||||||
if (/\b(when|timing|timeline|duration|sequence|milestone)\b/.test(text)) {
|
"Selected because the unknown explicitly references a missing previous or baseline state.",
|
||||||
return { strategy: "transition/timing", meaning, actionPhrase };
|
node,
|
||||||
|
graph,
|
||||||
|
relatedNodes,
|
||||||
|
meaning,
|
||||||
|
actionPhrase,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasDefinitionLanguage =
|
const hasDefinitionLanguage =
|
||||||
@@ -206,84 +241,116 @@ function detectStrategy({ node, graph, relatedNodes, combinedText, meaning }) {
|
|||||||
/\b(metric|measure|measurable|roi|revenue projection|benchmark)\b/.test(
|
/\b(metric|measure|measurable|roi|revenue projection|benchmark)\b/.test(
|
||||||
text,
|
text,
|
||||||
);
|
);
|
||||||
|
const hasEvidenceLanguage =
|
||||||
|
/\b(evidence|proof|validate|validation|signal|demand)\b/.test(text) ||
|
||||||
|
node?.kind === "reported_claim" ||
|
||||||
|
node?.kind === "conclusion" ||
|
||||||
|
/\b(claim|assertion|true|false)\b/.test(text);
|
||||||
|
const hasContradictionLanguage =
|
||||||
|
/\b(contradiction|contradict|conflict|inconsistent|inconsistency|disagree|mismatch)\b/.test(
|
||||||
|
`${text} ${relatedText}`,
|
||||||
|
) ||
|
||||||
|
relatedNodes.some(
|
||||||
|
(relatedNode) =>
|
||||||
|
relatedNode.status === "contradicted" ||
|
||||||
|
relatedNode.kind === "conclusion",
|
||||||
|
);
|
||||||
|
|
||||||
if (hasDecisionValueLanguage && hasMeasurementLanguage) {
|
if (hasPrimaryDefinitionLanguage || hasDefinitionLanguage) {
|
||||||
return { strategy: "measurement", meaning, actionPhrase };
|
return buildInvestigationStrategy({
|
||||||
}
|
key: "definition",
|
||||||
|
reason:
|
||||||
if (hasPrimaryDefinitionLanguage) {
|
"Selected because the unknown is primarily about clarifying what a term means in this case.",
|
||||||
return { strategy: "definition", meaning, actionPhrase };
|
node,
|
||||||
|
graph,
|
||||||
|
relatedNodes,
|
||||||
|
meaning,
|
||||||
|
actionPhrase,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDecisionValueLanguage || hasCriteriaLanguage) {
|
if (hasDecisionValueLanguage || hasCriteriaLanguage) {
|
||||||
return { strategy: "decision criterion", meaning, actionPhrase };
|
return buildInvestigationStrategy({
|
||||||
|
key: "decision_threshold",
|
||||||
|
reason:
|
||||||
|
"Selected because the unknown determines the threshold for making or justifying a decision.",
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
relatedNodes,
|
||||||
|
meaning,
|
||||||
|
actionPhrase,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasConstraintLanguage && hasPrimaryConstraintLanguage) {
|
if (hasPrimaryBaselineLanguage || hasBaselineLanguage) {
|
||||||
return { strategy: "constraint", meaning, actionPhrase };
|
return buildInvestigationStrategy({
|
||||||
|
key: "baseline_reconstruction",
|
||||||
|
reason:
|
||||||
|
"Selected because reconstructing the prior state is the most direct way to resolve the unknown.",
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
relatedNodes,
|
||||||
|
meaning,
|
||||||
|
actionPhrase,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDefinitionLanguage) {
|
if (hasContradictionLanguage) {
|
||||||
return { strategy: "definition", meaning, actionPhrase };
|
return buildInvestigationStrategy({
|
||||||
|
key: "contradiction_resolution",
|
||||||
|
reason:
|
||||||
|
"Selected because the graph context indicates conflicting claims or inconsistent states that must be reconciled.",
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
relatedNodes,
|
||||||
|
meaning,
|
||||||
|
actionPhrase,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasBaselineLanguage) {
|
if (hasEvidenceLanguage || hasMeasurementLanguage || hasConstraintLanguage) {
|
||||||
return { strategy: "baseline", meaning, actionPhrase };
|
return buildInvestigationStrategy({
|
||||||
|
key: "evidence_gathering",
|
||||||
|
reason:
|
||||||
|
hasConstraintLanguage && hasPrimaryConstraintLanguage
|
||||||
|
? "Selected because evidence about the practical limiting factor is needed before the unknown can be resolved."
|
||||||
|
: "Selected because resolving the unknown requires evidence, signals, or measurable confirmation.",
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
relatedNodes,
|
||||||
|
meaning,
|
||||||
|
actionPhrase,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasConstraintLanguage) {
|
return buildInvestigationStrategy({
|
||||||
return { strategy: "constraint", meaning, actionPhrase };
|
key: "definition",
|
||||||
}
|
reason:
|
||||||
|
"Selected as the deterministic fallback because clarifying the exact meaning of the unknown is the narrowest first step.",
|
||||||
if (/\b(evidence|proof|validate|validation|signal|demand)\b/.test(text)) {
|
node,
|
||||||
return { strategy: "evidence", meaning, actionPhrase };
|
graph,
|
||||||
}
|
relatedNodes,
|
||||||
|
meaning,
|
||||||
if (hasMeasurementLanguage) {
|
actionPhrase,
|
||||||
return { strategy: "measurement", meaning, actionPhrase };
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
/\b(objective|goal|outcome|problem|job to be done|benefit)\b/.test(text)
|
|
||||||
) {
|
|
||||||
return { strategy: "objective", meaning, actionPhrase };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
node?.kind === "reported_claim" ||
|
|
||||||
node?.kind === "conclusion" ||
|
|
||||||
/\b(claim|assertion|true|false)\b/.test(text)
|
|
||||||
) {
|
|
||||||
return { strategy: "evidence", meaning, actionPhrase };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { strategy: "generic clarification", meaning, actionPhrase };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildQuestion({ strategy, meaning, actionPhrase }) {
|
function buildQuestionFromStrategy(strategy) {
|
||||||
switch (strategy) {
|
switch (strategy.key) {
|
||||||
case "decision criterion":
|
case "decision_threshold":
|
||||||
return actionPhrase
|
return strategy.actionPhrase
|
||||||
? `What outcome would demonstrate enough value to justify ${toGerundPhrase(actionPhrase)}?`
|
? `What outcome would demonstrate enough value to justify ${toGerundPhrase(strategy.actionPhrase)}?`
|
||||||
: "What outcome would be sufficient to justify this decision?";
|
: "What outcome would be sufficient to justify this decision?";
|
||||||
case "definition":
|
case "definition":
|
||||||
return `What does ${meaning} mean in this situation?`;
|
return `What does ${strategy.meaning} mean in this situation?`;
|
||||||
case "evidence":
|
case "evidence_gathering":
|
||||||
return `What evidence would show whether ${meaning} is true?`;
|
return `What evidence would show whether ${strategy.meaning} is true?`;
|
||||||
case "baseline":
|
case "baseline_reconstruction":
|
||||||
return `What was the comparable state before ${meaning}?`;
|
return `What was the comparable state before ${strategy.meaning}?`;
|
||||||
case "actor/customer":
|
case "contradiction_resolution":
|
||||||
return "Who experiences the problem or receives the value in this situation?";
|
return `What fact would resolve the contradiction about ${strategy.meaning}?`;
|
||||||
case "objective":
|
|
||||||
return "What outcome is this decision or effort meant to achieve?";
|
|
||||||
case "constraint":
|
|
||||||
return "What constraint most limits the available options in this situation?";
|
|
||||||
case "measurement":
|
|
||||||
return `What measure would determine whether ${meaning} is sufficient?`;
|
|
||||||
case "transition/timing":
|
|
||||||
return `When does ${meaning} become relevant in the decision or change?`;
|
|
||||||
default:
|
default:
|
||||||
return `What specific fact would resolve whether ${meaning} is true?`;
|
return `What specific fact would resolve whether ${strategy.meaning} is true?`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,36 +399,22 @@ function validateFormulatedQuestion(question, meaning) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formulateQuestion({ node, graph, context = {} }) {
|
export function formulateQuestion({ node, graph, context = {} }) {
|
||||||
const relatedNodes = collectRelatedNodes(node, graph);
|
const investigationStrategy = selectInvestigationStrategy({
|
||||||
const meaning = extractMeaning(node);
|
|
||||||
const combinedText = [
|
|
||||||
node?.label,
|
|
||||||
node?.description,
|
|
||||||
...relatedNodes.map((relatedNode) => relatedNode.label),
|
|
||||||
...relatedNodes.map((relatedNode) => relatedNode.description),
|
|
||||||
graph?.centralStatement,
|
|
||||||
...(context.resolvedValues || []),
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
const detected = detectStrategy({
|
|
||||||
node,
|
node,
|
||||||
graph,
|
graph,
|
||||||
relatedNodes,
|
context,
|
||||||
combinedText,
|
|
||||||
meaning,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let question = buildQuestion(detected);
|
let question = buildQuestionFromStrategy(investigationStrategy);
|
||||||
|
|
||||||
if (!validateFormulatedQuestion(question, meaning)) {
|
if (!validateFormulatedQuestion(question, investigationStrategy.meaning)) {
|
||||||
question = `What evidence would resolve whether ${meaning} is true?`;
|
question = `What evidence would resolve whether ${investigationStrategy.meaning} is true?`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
question,
|
question,
|
||||||
reason: `Formulated from graph context using the ${detected.strategy} strategy.`,
|
reason: `Formulated from graph context using the ${investigationStrategy.key} investigation strategy.`,
|
||||||
strategy: detected.strategy,
|
strategy: investigationStrategy.key,
|
||||||
|
investigationStrategy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-5
@@ -67,7 +67,11 @@ export const questionPriorityGeneralisationFixtures = [
|
|||||||
"hire-bottleneck",
|
"hire-bottleneck",
|
||||||
],
|
],
|
||||||
prohibitedFirstTopics: ["salary", "job advert", "programming language"],
|
prohibitedFirstTopics: ["salary", "job advert", "programming language"],
|
||||||
acceptableQuestionStrategies: ["decision criterion", "constraint"],
|
acceptableQuestionStrategies: [
|
||||||
|
"decision_threshold",
|
||||||
|
"evidence_gathering",
|
||||||
|
"definition",
|
||||||
|
],
|
||||||
notes:
|
notes:
|
||||||
"The first question should establish whether more engineering capacity is justified before compensation or implementation details.",
|
"The first question should establish whether more engineering capacity is justified before compensation or implementation details.",
|
||||||
graph: makeScenarioGraph({
|
graph: makeScenarioGraph({
|
||||||
@@ -143,7 +147,11 @@ export const questionPriorityGeneralisationFixtures = [
|
|||||||
"paint colour",
|
"paint colour",
|
||||||
"finance provider",
|
"finance provider",
|
||||||
],
|
],
|
||||||
acceptableQuestionStrategies: ["decision criterion", "constraint"],
|
acceptableQuestionStrategies: [
|
||||||
|
"decision_threshold",
|
||||||
|
"evidence_gathering",
|
||||||
|
"definition",
|
||||||
|
],
|
||||||
notes:
|
notes:
|
||||||
"The first question should establish whether the fleet is failing a threshold that justifies replacement.",
|
"The first question should establish whether the fleet is failing a threshold that justifies replacement.",
|
||||||
graph: makeScenarioGraph({
|
graph: makeScenarioGraph({
|
||||||
@@ -219,7 +227,7 @@ export const questionPriorityGeneralisationFixtures = [
|
|||||||
"office location",
|
"office location",
|
||||||
"advertising channel",
|
"advertising channel",
|
||||||
],
|
],
|
||||||
acceptableQuestionStrategies: ["actor/customer", "decision criterion"],
|
acceptableQuestionStrategies: ["definition", "decision_threshold"],
|
||||||
notes:
|
notes:
|
||||||
"The first question should clarify the customer or value case for expansion before rollout logistics.",
|
"The first question should clarify the customer or value case for expansion before rollout logistics.",
|
||||||
graph: makeScenarioGraph({
|
graph: makeScenarioGraph({
|
||||||
@@ -291,7 +299,7 @@ export const questionPriorityGeneralisationFixtures = [
|
|||||||
"project-remaining-benefit",
|
"project-remaining-benefit",
|
||||||
],
|
],
|
||||||
prohibitedFirstTopics: ["sunk cost", "project logo", "final launch date"],
|
prohibitedFirstTopics: ["sunk cost", "project logo", "final launch date"],
|
||||||
acceptableQuestionStrategies: ["decision criterion", "objective"],
|
acceptableQuestionStrategies: ["decision_threshold", "definition"],
|
||||||
notes:
|
notes:
|
||||||
"The first question should establish remaining value or success threshold before sunk-cost framing or launch timing.",
|
"The first question should establish remaining value or success threshold before sunk-cost framing or launch timing.",
|
||||||
graph: makeScenarioGraph({
|
graph: makeScenarioGraph({
|
||||||
@@ -367,7 +375,11 @@ export const questionPriorityGeneralisationFixtures = [
|
|||||||
"payment provider",
|
"payment provider",
|
||||||
"tier name",
|
"tier name",
|
||||||
],
|
],
|
||||||
acceptableQuestionStrategies: ["actor/customer", "decision criterion"],
|
acceptableQuestionStrategies: [
|
||||||
|
"definition",
|
||||||
|
"decision_threshold",
|
||||||
|
"baseline_reconstruction",
|
||||||
|
],
|
||||||
notes:
|
notes:
|
||||||
"The first question should establish who values paid support or what outcome would justify offering it before pricing details.",
|
"The first question should establish who values paid support or what outcome would justify offering it before pricing details.",
|
||||||
graph: makeScenarioGraph({
|
graph: makeScenarioGraph({
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { formulateQuestion } from "@/lib/graph/question-formulator.js";
|
import {
|
||||||
|
formulateQuestion,
|
||||||
|
selectInvestigationStrategy,
|
||||||
|
} from "@/lib/graph/question-formulator.js";
|
||||||
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
|
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||||
|
|
||||||
function makeGraphFor(node, extra = {}) {
|
function makeGraphFor(node, extra = {}) {
|
||||||
@@ -14,7 +17,7 @@ function makeGraphFor(node, extra = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("formulateQuestion", () => {
|
describe("formulateQuestion", () => {
|
||||||
it("commercial viability plus build decision produces a decision-criterion question", () => {
|
it("commercial viability plus build decision produces a decision-threshold question", () => {
|
||||||
const unknown = makeNode({
|
const unknown = makeNode({
|
||||||
id: "n-commercial",
|
id: "n-commercial",
|
||||||
label: "Uncertainty regarding the commercial value of the product",
|
label: "Uncertainty regarding the commercial value of the product",
|
||||||
@@ -42,7 +45,7 @@ describe("formulateQuestion", () => {
|
|||||||
|
|
||||||
const result = formulateQuestion({ node: unknown, graph });
|
const result = formulateQuestion({ node: unknown, graph });
|
||||||
|
|
||||||
expect(result.strategy).toBe("decision criterion");
|
expect(result.strategy).toBe("decision_threshold");
|
||||||
expect(result.question).toContain("What outcome");
|
expect(result.question).toContain("What outcome");
|
||||||
expect(result.question.toLowerCase()).toContain("justify");
|
expect(result.question.toLowerCase()).toContain("justify");
|
||||||
});
|
});
|
||||||
@@ -100,7 +103,7 @@ describe("formulateQuestion", () => {
|
|||||||
graph: makeGraphFor(unknown),
|
graph: makeGraphFor(unknown),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.strategy).toBe("evidence");
|
expect(result.strategy).toBe("evidence_gathering");
|
||||||
expect(result.question).toContain("What evidence");
|
expect(result.question).toContain("What evidence");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,33 +123,41 @@ describe("formulateQuestion", () => {
|
|||||||
graph: makeGraphFor(unknown),
|
graph: makeGraphFor(unknown),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.strategy).toBe("baseline");
|
expect(result.strategy).toBe("baseline_reconstruction");
|
||||||
expect(result.question).toContain("What was the comparable state before");
|
expect(result.question).toContain("What was the comparable state before");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("unknown customer produces an actor/customer question", () => {
|
it("conflicting claim produces a contradiction-resolution question", () => {
|
||||||
const unknown = makeNode({
|
const unknown = makeNode({
|
||||||
id: "n-customer",
|
id: "n-conflict",
|
||||||
label: "Target customer",
|
label: "Conflicting churn claim",
|
||||||
description:
|
description:
|
||||||
"Need to know the customer because value depends on who receives it.",
|
"Need to resolve the inconsistency because the current figures contradict each other.",
|
||||||
kind: "unknown",
|
kind: "unknown",
|
||||||
status: "unknown",
|
status: "unknown",
|
||||||
confidence: "high",
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
const contradiction = makeNode({
|
||||||
|
id: "n-contradiction",
|
||||||
|
label: "Contradicted report",
|
||||||
|
description: "Two sources disagree about churn.",
|
||||||
|
kind: "conclusion",
|
||||||
|
status: "contradicted",
|
||||||
|
confidence: "low",
|
||||||
|
childIds: [unknown.id],
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = formulateQuestion({
|
const result = formulateQuestion({
|
||||||
node: unknown,
|
node: unknown,
|
||||||
graph: makeGraphFor(unknown),
|
graph: makeGraphFor(unknown, { nodes: [contradiction] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.strategy).toBe("actor/customer");
|
expect(result.strategy).toBe("contradiction_resolution");
|
||||||
expect(result.question).toContain(
|
expect(result.question).toContain("resolve the contradiction");
|
||||||
"Who experiences the problem or receives the value",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("constraint unknown produces a constraint question", () => {
|
it("constraint unknown uses evidence-gathering within the fixed strategy set", () => {
|
||||||
const unknown = makeNode({
|
const unknown = makeNode({
|
||||||
id: "n-constraint",
|
id: "n-constraint",
|
||||||
label: "Budget constraint",
|
label: "Budget constraint",
|
||||||
@@ -162,8 +173,87 @@ describe("formulateQuestion", () => {
|
|||||||
graph: makeGraphFor(unknown),
|
graph: makeGraphFor(unknown),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.strategy).toBe("constraint");
|
expect(result.strategy).toBe("evidence_gathering");
|
||||||
expect(result.question).toContain("What constraint most limits");
|
expect(result.question).toContain("What evidence");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the same unknown can produce different questions when paired with different strategies", () => {
|
||||||
|
const unknown = makeNode({
|
||||||
|
id: "n-same-unknown",
|
||||||
|
label: "Value threshold",
|
||||||
|
description: "Need to resolve the value threshold.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "high",
|
||||||
|
});
|
||||||
|
|
||||||
|
const decisionGraph = makeGraphFor(unknown, {
|
||||||
|
centralStatement: "We are deciding whether to launch this product.",
|
||||||
|
nodes: [
|
||||||
|
makeNode({
|
||||||
|
id: "n-decision",
|
||||||
|
label: "Launch decision",
|
||||||
|
description: "Decision depends on the value threshold.",
|
||||||
|
kind: "state",
|
||||||
|
status: "known",
|
||||||
|
confidence: "medium",
|
||||||
|
childIds: [unknown.id],
|
||||||
|
value: "Deciding whether to launch the product",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const definitionGraph = makeGraphFor(unknown, {
|
||||||
|
centralStatement:
|
||||||
|
"The team uses the term value threshold inconsistently.",
|
||||||
|
nodes: [
|
||||||
|
makeNode({
|
||||||
|
id: "n-definition",
|
||||||
|
label: "Definition disagreement",
|
||||||
|
description:
|
||||||
|
"Need a definition of value threshold before comparing options.",
|
||||||
|
kind: "state",
|
||||||
|
status: "known",
|
||||||
|
confidence: "medium",
|
||||||
|
childIds: [unknown.id],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const decisionResult = formulateQuestion({
|
||||||
|
node: unknown,
|
||||||
|
graph: decisionGraph,
|
||||||
|
});
|
||||||
|
const definitionResult = formulateQuestion({
|
||||||
|
node: unknown,
|
||||||
|
graph: definitionGraph,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decisionResult.strategy).toBe("decision_threshold");
|
||||||
|
expect(definitionResult.strategy).toBe("definition");
|
||||||
|
expect(decisionResult.question).not.toBe(definitionResult.question);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strategy selection is deterministic and explainable", () => {
|
||||||
|
const unknown = makeNode({
|
||||||
|
id: "n-threshold",
|
||||||
|
label: "Success threshold",
|
||||||
|
description:
|
||||||
|
"Need the success threshold because the decision depends on it.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "high",
|
||||||
|
});
|
||||||
|
const graph = makeGraphFor(unknown, {
|
||||||
|
centralStatement: "We need to decide whether to continue investing.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = selectInvestigationStrategy({ node: unknown, graph });
|
||||||
|
const second = selectInvestigationStrategy({ node: unknown, graph });
|
||||||
|
|
||||||
|
expect(first).toEqual(second);
|
||||||
|
expect(first.key).toBe("decision_threshold");
|
||||||
|
expect(first.reason).toContain("threshold");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("question is singular and answerable", () => {
|
it("question is singular and answerable", () => {
|
||||||
|
|||||||
@@ -127,27 +127,27 @@ describe("question priority generalisation", () => {
|
|||||||
{
|
{
|
||||||
"nodeId": "hire-success-criteria",
|
"nodeId": "hire-success-criteria",
|
||||||
"scenario": "Should we hire another engineer?",
|
"scenario": "Should we hire another engineer?",
|
||||||
"strategy": "decision criterion",
|
"strategy": "decision_threshold",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nodeId": "van-reliability-threshold",
|
"nodeId": "van-reliability-threshold",
|
||||||
"scenario": "Should we replace the delivery vans?",
|
"scenario": "Should we replace the delivery vans?",
|
||||||
"strategy": "decision criterion",
|
"strategy": "decision_threshold",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nodeId": "country-value-threshold",
|
"nodeId": "country-value-threshold",
|
||||||
"scenario": "Should we launch in another country?",
|
"scenario": "Should we launch in another country?",
|
||||||
"strategy": "actor/customer",
|
"strategy": "decision_threshold",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nodeId": "project-benefit-threshold",
|
"nodeId": "project-benefit-threshold",
|
||||||
"scenario": "Should we continue a project that is over budget?",
|
"scenario": "Should we continue a project that is over budget?",
|
||||||
"strategy": "decision criterion",
|
"strategy": "decision_threshold",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nodeId": "support-value-threshold",
|
"nodeId": "support-value-threshold",
|
||||||
"scenario": "Should we introduce a paid support tier?",
|
"scenario": "Should we introduce a paid support tier?",
|
||||||
"strategy": "actor/customer",
|
"strategy": "baseline_reconstruction",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|||||||
Reference in New Issue
Block a user