feat: enforce one-concept questions

This commit is contained in:
2026-08-03 08:40:39 +01:00
parent 449cf996dc
commit 3c0f7f5a45
5 changed files with 634 additions and 11 deletions
+111 -11
View File
@@ -1385,10 +1385,74 @@ function describeObservationFocus(context, which) {
}
function buildDecompositionTemplates(parentNode, graph, depth = 0) {
const parentText = normaliseText(
`${parentNode?.label || ""} ${parentNode?.description || ""}`,
);
const context = buildDecompositionContext(graph);
const firstFocus = describeObservationFocus(context, "first");
const secondFocus = describeObservationFocus(context, "second");
if (
/\b(genuine problem|commercially justified|commercial justification|people would value|pay for it|justified confidence|decision support methods|willingness to pay|seek help)\b/.test(
parentText,
)
) {
return [
{
label: "Who experiences this problem",
description:
"Need to know who experiences this problem, because that must be clear before deciding whether it is commercially justified.",
dependsOnLabels: [],
},
{
label: "What happens when this problem is not resolved",
description:
"Need to know what happens when this problem is not resolved, because that is needed before judging whether the problem matters.",
dependsOnLabels: ["Who experiences this problem"],
},
{
label: "How often this problem happens",
description:
"Need to know how often this problem happens, because that helps judge whether it is a real recurring problem.",
dependsOnLabels: ["Who experiences this problem"],
},
{
label: "How people deal with this problem today",
description:
"Need to know how people deal with this problem today, because that is needed before comparing alternatives or value.",
dependsOnLabels: [
"Who experiences this problem",
"What happens when this problem is not resolved",
"How often this problem happens",
],
},
depth === 0
? {
label: "Whether people actively look for help with this problem",
description:
"Need to know whether people actively look for help with this problem, because that is needed before judging demand or willingness to pay.",
dependsOnLabels: [
"Who experiences this problem",
"What happens when this problem is not resolved",
"How often this problem happens",
"How people deal with this problem today",
],
}
: {
label: "Whether people would pay to solve this problem",
description:
"Need to know whether people would pay to solve this problem, because that can only be judged after the problem itself is established.",
dependsOnLabels: [
"Who experiences this problem",
"What happens when this problem is not resolved",
"How often this problem happens",
"How people deal with this problem today",
"Whether people actively look for help with this problem",
],
},
];
}
if (/\btiming or measurement basis\b/i.test(parentNode.label)) {
return [
{
@@ -1434,14 +1498,25 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) {
function buildCompositeUnknownChildren(parentNode, graph, depth = 0) {
const templates = buildDecompositionTemplates(parentNode, graph, depth);
const candidateNodes = templates.map(
(template) =>
findEquivalentDecompositionChild(
graph,
parentNode.id,
template.label,
template.description,
) || {
const labelToId = new Map(
templates.map((template) => [
template.label,
buildDecompositionChildId(parentNode.id, template.label),
]),
);
const candidateNodes = templates.map((template) => {
const existing = findEquivalentDecompositionChild(
graph,
parentNode.id,
template.label,
template.description,
);
const dependsOn = (template.dependsOnLabels || [])
.map((label) => labelToId.get(label))
.filter(Boolean);
return (
existing || {
id: buildDecompositionChildId(parentNode.id, template.label),
label: template.label,
description: template.description,
@@ -1451,12 +1526,13 @@ function buildCompositeUnknownChildren(parentNode, graph, depth = 0) {
value: null,
unit: null,
evidenceIds: [],
dependsOn: [],
dependsOn,
affects: [],
parentId: parentNode.id,
childIds: [],
},
);
}
);
});
const childNodes = [];
const childEdges = [];
const childNodeIds = [];
@@ -1566,9 +1642,14 @@ function runDeterministicDecomposition({
let proposedChildCount = 0;
let acceptedChildCount = 0;
let selectedChildNodeId = null;
let selectedUnknownBefore =
deterministicSelection?.status === "selected"
? deterministicSelection.nodeId
: null;
let decompositionStoppedReason = null;
let rejectedChildren = [];
let childQualitySummary = [];
let decompositionTriggeredByQuestionComplexity = false;
while (workingSelection?.status === "selected" && workingSelection?.nodeId) {
const selectedNode = findNodeById(workingGraph, workingSelection.nodeId);
@@ -1699,6 +1780,8 @@ function runDeterministicDecomposition({
rejectedChildren,
childQualitySummary,
selectedChildNodeId,
selectedUnknownBefore,
decompositionTriggeredByQuestionComplexity,
};
}
@@ -2121,6 +2204,10 @@ export function applyValidatedProposal({
})
: null;
const questionComplexity = formulatedQuestion?.questionComplexity ?? null;
const plainLanguageNormalisations =
formulatedQuestion?.plainLanguageNormalisations ?? [];
const finalSelectedQuestion =
deterministicSelection?.status === "ambiguous"
? {
@@ -2137,6 +2224,8 @@ export function applyValidatedProposal({
reason: formulatedQuestion?.reason || deterministicSelection.reason,
strategy: formulatedQuestion?.strategy,
investigationStrategy: formulatedQuestion?.investigationStrategy,
questionComplexity,
plainLanguageNormalisations,
}
: null;
@@ -2202,6 +2291,17 @@ export function applyValidatedProposal({
rejectedChildren,
selectedChildNodeId,
childQualitySummary,
selectedUnknownBefore: decompositionResult.selectedUnknownBefore,
selectedUnknownAfter: deterministicSelection?.nodeId ?? null,
questionComplexityAccepted: questionComplexity?.acceptable ?? null,
primaryConceptCount: questionComplexity?.primaryConceptCount ?? null,
cognitiveLoad: questionComplexity?.cognitiveLoad ?? null,
complexityReasons: questionComplexity?.reasons ?? [],
decompositionTriggeredByQuestionComplexity:
decompositionResult.decompositionTriggeredByQuestionComplexity ?? false,
previousQuestion,
finalQuestion: finalSelectedQuestion?.question ?? null,
plainLanguageNormalisations,
propagationPerformed,
resolvedChildNodeId,
parentNodeId,