1913 lines
60 KiB
JavaScript
1913 lines
60 KiB
JavaScript
function normaliseText(value) {
|
|
return String(value || "")
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
function sentenceCase(value) {
|
|
const trimmed = String(value || "").trim();
|
|
if (!trimmed) return "this uncertainty";
|
|
return trimmed.charAt(0).toLowerCase() + trimmed.slice(1);
|
|
}
|
|
|
|
function stripTrailingPunctuation(value) {
|
|
return String(value || "")
|
|
.trim()
|
|
.replace(/[.?!:;]+$/g, "")
|
|
.trim();
|
|
}
|
|
|
|
function buildNodeMap(graph) {
|
|
return new Map((graph?.nodes || []).map((node) => [node.id, node]));
|
|
}
|
|
|
|
function collectRelatedNodes(node, graph) {
|
|
if (!node || !graph) return [];
|
|
|
|
const nodesById = buildNodeMap(graph);
|
|
const relatedIds = new Set([
|
|
...(node.dependsOn || []),
|
|
...(node.affects || []),
|
|
...(node.childIds || []),
|
|
]);
|
|
|
|
if (node.parentId) {
|
|
relatedIds.add(node.parentId);
|
|
}
|
|
|
|
for (const edge of graph.edges || []) {
|
|
if (edge.fromNodeId === node.id) {
|
|
relatedIds.add(edge.toNodeId);
|
|
}
|
|
if (edge.toNodeId === node.id) {
|
|
relatedIds.add(edge.fromNodeId);
|
|
}
|
|
}
|
|
|
|
return [...relatedIds].map((nodeId) => nodesById.get(nodeId)).filter(Boolean);
|
|
}
|
|
|
|
function collectResolvedContextValues(graph) {
|
|
const resolvedSet = new Set(graph?.resolvedNodeIds || []);
|
|
|
|
return (graph?.nodes || [])
|
|
.filter((node) => resolvedSet.has(node.id))
|
|
.map((node) => node.value)
|
|
.filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
}
|
|
|
|
function extractMeaning(node) {
|
|
const raw = `${node?.label || ""} ${node?.description || ""}`.trim();
|
|
let meaning = stripTrailingPunctuation(
|
|
String(node?.label || node?.description || "this uncertainty"),
|
|
).trim();
|
|
|
|
const lowered = normaliseText(raw);
|
|
if (
|
|
/\b(customer|user|buyer|stakeholder|recipient|audience)\b/.test(lowered)
|
|
) {
|
|
return "the relevant customer, user, or value recipient";
|
|
}
|
|
|
|
meaning = meaning
|
|
.replace(/^uncertainty regarding\s+/i, "")
|
|
.replace(/^uncertainty about\s+/i, "")
|
|
.replace(/^lack of\s+/i, "")
|
|
.replace(/^unknown\s+/i, "")
|
|
.replace(/^whether\s+/i, "")
|
|
.replace(/^the\s+/, "")
|
|
.trim();
|
|
|
|
if (!meaning) {
|
|
return "this uncertainty";
|
|
}
|
|
|
|
return sentenceCase(meaning);
|
|
}
|
|
|
|
function isDefinitionLikeUnknown(nodeText, text) {
|
|
return (
|
|
/\b(define|definition|meaning|term|terminology)\b/.test(nodeText) ||
|
|
(/\bdefinition\b/.test(text) && /\bdisagreement\b/.test(text)) ||
|
|
(/\b(define|definition|meaning|term|terminology)\b/.test(text) &&
|
|
/\b(unclear|ambiguous|inconsistent|undefined|used inconsistently)\b/.test(
|
|
text,
|
|
))
|
|
);
|
|
}
|
|
|
|
function isClaimLikeUnknown(node, text) {
|
|
return (
|
|
node?.kind === "reported_claim" ||
|
|
node?.kind === "conclusion" ||
|
|
/\b(claim|assertion|true|false|correct|incorrect|happened|happening)\b/.test(
|
|
text,
|
|
) ||
|
|
/^whether\b/i.test(String(node?.label || "").trim())
|
|
);
|
|
}
|
|
|
|
function sanitizeQuestionText(question) {
|
|
return String(question || "")
|
|
.replace(/\)\.\s+/g, ") ")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
function buildNeutralClarificationQuestion(meaning) {
|
|
return `What would clarify ${stripTrailingPunctuation(meaning)} in this situation?`;
|
|
}
|
|
|
|
function buildEvidenceFallbackQuestion(meaning) {
|
|
return `What evidence would confirm or rule out ${stripTrailingPunctuation(meaning)}?`;
|
|
}
|
|
|
|
function extractConstraintClarificationSubject(node) {
|
|
const label = stripTrailingPunctuation(node?.label || "");
|
|
const description = String(node?.description || "");
|
|
const combined = `${label} ${description}`;
|
|
|
|
const labelMatch = label.match(
|
|
/^Whether\s+(.+)\s+is\s+a\s+hard constraint$/i,
|
|
);
|
|
if (labelMatch?.[1]) {
|
|
return labelMatch[1].trim();
|
|
}
|
|
|
|
const descriptionMatch = combined.match(
|
|
/whether\s+(.+?)\s+is\s+a\s+hard constraint\s+or\s+a\s+preference(?:\/|-|\s)trade(?:\/|-|\s)?off/i,
|
|
);
|
|
if (descriptionMatch?.[1]) {
|
|
return descriptionMatch[1].trim();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function buildUserMeaningClarificationQuestion(node) {
|
|
const subject = extractConstraintClarificationSubject(node);
|
|
|
|
if (subject) {
|
|
return `Is ${subject} a hard constraint or a preference/trade-off?`;
|
|
}
|
|
|
|
return buildNeutralClarificationQuestion(extractMeaning(node));
|
|
}
|
|
|
|
function isUserOwnedMeaningBoundaryUnknown(node) {
|
|
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
|
return (
|
|
text.includes("hard constraint") &&
|
|
(text.includes("preference trade off") ||
|
|
text.includes("preference/trade-off") ||
|
|
text.includes("preference or trade off") ||
|
|
text.includes("preference or trade-off"))
|
|
);
|
|
}
|
|
|
|
function collectObservationNodes(graph) {
|
|
return (graph?.nodes || []).filter(
|
|
(node) => node.kind === "observation" && node.status === "supported",
|
|
);
|
|
}
|
|
|
|
function analyseObservationText(text) {
|
|
const normalised = normaliseText(text);
|
|
return {
|
|
text,
|
|
normalised,
|
|
isMeasurementLike:
|
|
/\b(increase|increased|decrease|decreased|fell|rose|doubled|halved|remained|average|score|scores|rate|time|traffic|sales|output|defects|complaints|production|revenue|cash|temperature|quality)\b/.test(
|
|
normalised,
|
|
) || /%|percent/.test(String(text || "")),
|
|
timeframeMentioned:
|
|
/\b(period|timeframe|quarter|month|week|year|day|annual|daily|weekly|monthly|same period)\b/.test(
|
|
normalised,
|
|
),
|
|
scaleMentioned: /\b(average|rate|score|scores|per|percent|%)\b/.test(
|
|
normalised,
|
|
),
|
|
unitMentioned:
|
|
/\b(celsius|fahrenheit|minutes|minute|hours|hour|days|day|units|sales|traffic|cash|revenue|complaints|defects)\b/.test(
|
|
normalised,
|
|
),
|
|
};
|
|
}
|
|
|
|
export const COMPARABILITY_REASONING_NODE_ID = "reasoning:comparability";
|
|
|
|
function readStoredComparabilityState(graph) {
|
|
const reasoningState = graph?.reasoningState;
|
|
if (!reasoningState?.comparabilityStatus) return null;
|
|
|
|
return {
|
|
comparabilityStatus: reasoningState.comparabilityStatus,
|
|
reason:
|
|
reasoningState.comparabilityReason ||
|
|
"Comparability state was carried forward from earlier reasoning.",
|
|
contradictionReasoningAllowed:
|
|
reasoningState.comparabilityStatus === "confirmed",
|
|
};
|
|
}
|
|
|
|
export function assessComparability(graph) {
|
|
const storedState = readStoredComparabilityState(graph);
|
|
if (storedState) {
|
|
return storedState;
|
|
}
|
|
|
|
const observations = collectObservationNodes(graph);
|
|
const centralText = normaliseText(graph?.centralStatement || "");
|
|
const profiles = observations.map((node) =>
|
|
analyseObservationText(`${node.label} ${node.description}`),
|
|
);
|
|
|
|
if (profiles.length < 2) {
|
|
return {
|
|
comparabilityStatus: "confirmed",
|
|
reason: "Fewer than two supported observations need comparison.",
|
|
contradictionReasoningAllowed: true,
|
|
};
|
|
}
|
|
|
|
if (
|
|
profiles.every((profile) => profile.normalised === profiles[0].normalised)
|
|
) {
|
|
return {
|
|
comparabilityStatus: "confirmed",
|
|
reason: "The observations restate the same measurement.",
|
|
contradictionReasoningAllowed: false,
|
|
};
|
|
}
|
|
|
|
if (profiles.some((profile) => !profile.isMeasurementLike)) {
|
|
return {
|
|
comparabilityStatus: "confirmed",
|
|
reason: "The observations are not competing like-for-like measurements.",
|
|
contradictionReasoningAllowed: true,
|
|
};
|
|
}
|
|
|
|
const hasExplicitTimeframe =
|
|
/\b(period|timeframe|quarter|month|week|year|day|same period)\b/.test(
|
|
centralText,
|
|
) || profiles.every((profile) => profile.timeframeMentioned);
|
|
|
|
const hasSharedScale = profiles.every((profile) => profile.scaleMentioned);
|
|
const hasSharedUnits = profiles.every((profile) => profile.unitMentioned);
|
|
|
|
if (!hasExplicitTimeframe || !hasSharedScale || !hasSharedUnits) {
|
|
return {
|
|
comparabilityStatus: "uncertain",
|
|
reason:
|
|
"Comparability between the observations is not yet established across period, scale, or measurement basis.",
|
|
contradictionReasoningAllowed: false,
|
|
};
|
|
}
|
|
|
|
return {
|
|
comparabilityStatus: "uncertain",
|
|
reason:
|
|
"The observations appear comparable in form, but the basis for comparing them is still not established.",
|
|
contradictionReasoningAllowed: false,
|
|
};
|
|
}
|
|
|
|
function buildReasoningStages(comparability, relationship, deferred = false) {
|
|
return [
|
|
{
|
|
stage: "comparability",
|
|
status: comparability.comparabilityStatus,
|
|
outcome: comparability.reason,
|
|
},
|
|
{
|
|
stage: "relationship",
|
|
status: relationship.relationshipStatus,
|
|
outcome: deferred
|
|
? "not assessed until comparability is established"
|
|
: relationship.reason,
|
|
},
|
|
];
|
|
}
|
|
|
|
function extractObservationConcepts(profile) {
|
|
const concepts = new Set();
|
|
const text = profile.normalised;
|
|
const conceptPatterns = [
|
|
["sales", /\bsales\b/],
|
|
["revenue", /\brevenue\b/],
|
|
["cash", /\bcash\b/],
|
|
["complaints", /\bcomplaints?\b/],
|
|
["production", /\bproduction\b/],
|
|
["delivery_time", /\bdelivery time\b|\baverage delivery time\b/],
|
|
["cancellations", /\bcancellations?\b/],
|
|
["satisfaction", /\bsatisfaction\b/],
|
|
["temperature", /\btemperature\b/],
|
|
["ice", /\bice\b/],
|
|
["traffic", /\btraffic\b/],
|
|
["defects", /\bdefects?\b/],
|
|
["quality", /\bquality\b/],
|
|
["staffing", /\bstaff(ing)?\b/],
|
|
["availability", /\bavailable|availability|unavailable\b/],
|
|
["service", /\bservice\b/],
|
|
];
|
|
|
|
for (const [name, pattern] of conceptPatterns) {
|
|
if (pattern.test(text)) concepts.add(name);
|
|
}
|
|
|
|
return [...concepts];
|
|
}
|
|
|
|
function extractObservationDirection(profile) {
|
|
const text = profile.normalised;
|
|
if (/\bunavailable\b/.test(text)) return "unavailable";
|
|
if (/\b(increase|increased|rose|up|doubled)\b/.test(text)) return "up";
|
|
if (/\b(decrease|decreased|fell|down|halved)\b/.test(text)) return "down";
|
|
if (/\b(remained unchanged|unchanged|same)\b/.test(text)) return "flat";
|
|
if (/\bavailable\b/.test(text)) return "available";
|
|
if (/\bmelted\b/.test(text)) return "melted";
|
|
return "unknown";
|
|
}
|
|
|
|
function classifyObservationRelationshipWhenComparable(graph) {
|
|
const observations = collectObservationNodes(graph);
|
|
const profiles = observations.map((node) =>
|
|
analyseObservationText(`${node.label} ${node.description}`),
|
|
);
|
|
|
|
if (profiles.length < 2) {
|
|
return {
|
|
relationshipStatus: "insufficient_information",
|
|
reason:
|
|
"Fewer than two supported observations are available for comparison.",
|
|
contradictionReasoningAllowed: false,
|
|
questionRequired: false,
|
|
questionSuppressedReason:
|
|
"Not enough observations to classify a relationship.",
|
|
};
|
|
}
|
|
|
|
if (
|
|
profiles.every((profile) => profile.normalised === profiles[0].normalised)
|
|
) {
|
|
return {
|
|
relationshipStatus: "duplicate",
|
|
reason: "The observations repeat the same measurement and direction.",
|
|
contradictionReasoningAllowed: false,
|
|
questionRequired: false,
|
|
questionSuppressedReason:
|
|
"Duplicate observations do not justify a follow-up question.",
|
|
};
|
|
}
|
|
|
|
const conceptSets = profiles.map((profile) =>
|
|
extractObservationConcepts(profile),
|
|
);
|
|
const sharedConcepts = conceptSets.reduce((shared, concepts, index) => {
|
|
if (index === 0) return new Set(concepts);
|
|
return new Set(concepts.filter((concept) => shared.has(concept)));
|
|
}, new Set());
|
|
const directions = profiles.map((profile) =>
|
|
extractObservationDirection(profile),
|
|
);
|
|
const conceptUnion = new Set(conceptSets.flat());
|
|
const hasRevenueCashPair =
|
|
conceptUnion.has("revenue") && conceptUnion.has("cash");
|
|
|
|
if (
|
|
sharedConcepts.size > 0 &&
|
|
directions.includes("available") &&
|
|
directions.includes("unavailable")
|
|
) {
|
|
return {
|
|
relationshipStatus: "contradictory",
|
|
reason:
|
|
"The observations assert mutually incompatible states about the same subject.",
|
|
contradictionReasoningAllowed: true,
|
|
questionRequired: true,
|
|
};
|
|
}
|
|
|
|
if (
|
|
sharedConcepts.size > 0 &&
|
|
directions.every((direction) => direction !== "unknown")
|
|
) {
|
|
return {
|
|
relationshipStatus: "potentially_related",
|
|
reason:
|
|
"The observations concern the same subject but do not assert a direct contradiction.",
|
|
contradictionReasoningAllowed: false,
|
|
questionRequired: true,
|
|
};
|
|
}
|
|
|
|
if (
|
|
hasRevenueCashPair &&
|
|
directions.every((direction) => direction !== "unknown")
|
|
) {
|
|
return {
|
|
relationshipStatus: "potentially_related",
|
|
reason:
|
|
"The observations concern connected business signals but do not establish a direct contradiction or cause.",
|
|
contradictionReasoningAllowed: false,
|
|
questionRequired: true,
|
|
};
|
|
}
|
|
|
|
if (
|
|
sharedConcepts.size === 0 &&
|
|
directions.every((direction) => direction !== "unknown")
|
|
) {
|
|
return {
|
|
relationshipStatus: "compatible",
|
|
reason:
|
|
"The observations can coexist without asserting incompatible states about the same subject.",
|
|
contradictionReasoningAllowed: false,
|
|
questionRequired: false,
|
|
questionSuppressedReason:
|
|
"Compatible observations do not justify a contradiction investigation.",
|
|
};
|
|
}
|
|
|
|
return {
|
|
relationshipStatus: "insufficient_information",
|
|
reason:
|
|
"There is not enough structure to classify the relationship safely.",
|
|
contradictionReasoningAllowed: false,
|
|
questionRequired: false,
|
|
questionSuppressedReason:
|
|
"No meaningful relationship structure was established; uncertainty is preserved without fabricating an explanation problem.",
|
|
};
|
|
}
|
|
|
|
export function classifyObservationRelationship(graph) {
|
|
const comparability = assessComparability(graph);
|
|
|
|
if (comparability.comparabilityStatus !== "confirmed") {
|
|
const deferredRelationship = {
|
|
relationshipStatus: "insufficient_information",
|
|
reason:
|
|
"Relationship classification is deferred until comparability is established.",
|
|
contradictionReasoningAllowed: false,
|
|
questionRequired: comparability.comparabilityStatus === "uncertain",
|
|
questionSuppressedReason:
|
|
comparability.comparabilityStatus === "incompatible"
|
|
? "Relationship classification was not attempted because the observations are not yet comparable."
|
|
: undefined,
|
|
relationshipAssessed: false,
|
|
};
|
|
|
|
return {
|
|
...deferredRelationship,
|
|
reasoningStages: buildReasoningStages(
|
|
comparability,
|
|
deferredRelationship,
|
|
true,
|
|
),
|
|
};
|
|
}
|
|
|
|
const classified = classifyObservationRelationshipWhenComparable(graph);
|
|
|
|
return {
|
|
...classified,
|
|
relationshipAssessed: true,
|
|
reasoningStages: buildReasoningStages(comparability, classified, false),
|
|
};
|
|
}
|
|
|
|
export function buildReasoningState(graph, overrides = {}) {
|
|
const relationship = classifyObservationRelationship({
|
|
...graph,
|
|
reasoningState: {
|
|
...(graph?.reasoningState || {}),
|
|
...(overrides || {}),
|
|
},
|
|
});
|
|
|
|
return {
|
|
comparabilityStatus: relationship.reasoningStages[0]?.status ?? null,
|
|
comparabilityReason: relationship.reasoningStages[0]?.outcome ?? null,
|
|
comparabilityEvidence:
|
|
overrides.comparabilityEvidence ??
|
|
graph?.reasoningState?.comparabilityEvidence ??
|
|
[],
|
|
relationshipStatus: relationship.relationshipStatus,
|
|
relationshipReason: relationship.reason,
|
|
relationshipAssessed: relationship.relationshipAssessed,
|
|
contradictionReasoningAllowed: relationship.contradictionReasoningAllowed,
|
|
reasoningStages: relationship.reasoningStages,
|
|
};
|
|
}
|
|
|
|
function buildComparabilityQuestion(graph, assessment) {
|
|
const centralText = normaliseText(graph?.centralStatement || "");
|
|
const mentionsPeriod =
|
|
/\b(period|timeframe|quarter|month|week|year|day)\b/.test(centralText);
|
|
|
|
if (mentionsPeriod) {
|
|
return "Were these figures measured on the same basis and at the same scale?";
|
|
}
|
|
|
|
return "Were these figures measured over the same period and at the same scale?";
|
|
}
|
|
|
|
function detectContradictionContext(graph) {
|
|
const central = stripTrailingPunctuation(
|
|
graph?.centralStatement || "this situation",
|
|
);
|
|
const contradictionNode = (graph?.nodes || []).find((node) => {
|
|
const text = normaliseText(`${node.label} ${node.description}`);
|
|
return (
|
|
node.kind === "relationship" &&
|
|
/\b(contradiction|conflict|inconsistent|mismatch|divergent|opposing)\b/.test(
|
|
text,
|
|
)
|
|
);
|
|
});
|
|
|
|
return {
|
|
centralStatement: central,
|
|
contradictionLabel: stripTrailingPunctuation(
|
|
contradictionNode?.label || "",
|
|
),
|
|
};
|
|
}
|
|
|
|
function buildBroadInvestigationQuestion(graph) {
|
|
const central = sanitizeQuestionText(
|
|
stripTrailingPunctuation(graph?.centralStatement || "these observations"),
|
|
);
|
|
return `What changed during that period that could help explain why ${central}?`;
|
|
}
|
|
|
|
function buildFoundationalDirectQuestion(node) {
|
|
const label = stripTrailingPunctuation(node?.label || "");
|
|
|
|
if (/^who experiences this problem$/i.test(label)) {
|
|
return "Who experiences this problem?";
|
|
}
|
|
if (/^whether other people experience this problem$/i.test(label)) {
|
|
return "What makes you think other people experience this problem too?";
|
|
}
|
|
if (/^what happens when this problem is not resolved$/i.test(label)) {
|
|
return "What happens when this problem is not resolved?";
|
|
}
|
|
if (/^how often this problem happens$/i.test(label)) {
|
|
return "How often does this problem happen?";
|
|
}
|
|
if (/^how people deal with this problem today$/i.test(label)) {
|
|
return "How do people deal with this problem today?";
|
|
}
|
|
if (
|
|
/^whether people actively look for help with this problem$/i.test(label)
|
|
) {
|
|
return "Do people actively look for help with this problem?";
|
|
}
|
|
if (/^whether people would pay to solve this problem$/i.test(label)) {
|
|
return "Would people pay to solve this problem?";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function isRelationshipExplanationUnknown(node, graph) {
|
|
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
|
if (isDirectlyAnswerableObservationChildText(text)) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
collectObservationNodes(graph).length >= 2 &&
|
|
/\b(explain|explanation|divergence|moved differently|difference between|change or event|what changed|why the observations)/.test(
|
|
text,
|
|
)
|
|
);
|
|
}
|
|
|
|
function isBroadCompositeUnknownText(text) {
|
|
return /\b(possible causes|possible reasons|root causes|causes of|drivers of|factors behind|factors affecting|what changed|explanation for why|why .* but|difference between|divergence|moved differently|broad explanation|independent dimensions)\b/.test(
|
|
text,
|
|
);
|
|
}
|
|
|
|
function isCommercialValidationUnknownText(text) {
|
|
return /\b(genuine problem|people would value|pay for it|commercially justified|commercial justification|commercial value|justified confidence|decision support methods|decision support|budget do they currently allocate|willingness to pay|problem existence|seek help)\b/.test(
|
|
text,
|
|
);
|
|
}
|
|
|
|
function hasCompoundAbstractSignals(text) {
|
|
return (
|
|
/\b(timing|measurement|basis|cost|costs|debt|stock|tax|capital spending|mix|segment)\s+(and|or)\s+\b/.test(
|
|
text,
|
|
) ||
|
|
/\b[a-z]+\/[a-z]+\b/.test(text) ||
|
|
/,\s*[a-z]+,\s*[a-z]+/.test(text)
|
|
);
|
|
}
|
|
|
|
function isFocusedAtomicUnknownText(text) {
|
|
return /\b(define|definition|meaning|term|threshold|criterion|criteria|baseline|evidence|measure|metric|denominator|rate|date|period|budget|constraint|customer|actor|owner)\b/.test(
|
|
text,
|
|
);
|
|
}
|
|
|
|
function isDirectlyAnswerableObservationChildText(text) {
|
|
return /\b(whether the two observations reflect different timing|how the two observations were measured|change mainly affecting|one off event during the period|mix shift during the period)\b/.test(
|
|
text,
|
|
);
|
|
}
|
|
|
|
function countPrerequisiteConceptSignals(text) {
|
|
let count = 0;
|
|
|
|
if (/\bproblem\b/.test(text)) count += 1;
|
|
if (/\b(audience|customer|user|buyer|stakeholder|recipient)\b/.test(text))
|
|
count += 1;
|
|
if (/\b(demand|seek help|actively look for help)\b/.test(text)) count += 1;
|
|
if (/\b(pay|willingness to pay|price|pricing)\b/.test(text)) count += 1;
|
|
if (
|
|
/\b(compare|comparison|different from|alternatives|alternative|existing alternatives|existing tools|better than)\b/.test(
|
|
text,
|
|
)
|
|
)
|
|
count += 1;
|
|
if (/\b(value|viability|justified|business case|commercial)\b/.test(text))
|
|
count += 1;
|
|
if (/\b(feasibility|technical)\b/.test(text)) count += 1;
|
|
|
|
return count;
|
|
}
|
|
|
|
function countIndependentAnswerDimensions(node, graph) {
|
|
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
|
const relatedNodes = collectRelatedNodes(node, graph);
|
|
const unresolvedDependencies = relatedNodes.filter(
|
|
(relatedNode) =>
|
|
relatedNode.kind === "unknown" && relatedNode.status !== "resolved",
|
|
).length;
|
|
const conjunctionCount = (text.match(/\b(and|or)\b/g) || []).length;
|
|
const prerequisiteConceptCount = countPrerequisiteConceptSignals(text);
|
|
const implicitConclusion =
|
|
/\b(commercially justified|commercial justification|commercial viability|business case|customer value|market demand|product validation|technical feasibility)\b/.test(
|
|
text,
|
|
) ||
|
|
(/\bevidence that\b/.test(text) &&
|
|
/\b(problem|need|demand|audience|customer|user|alternatives|better than)\b/.test(
|
|
text,
|
|
)) ||
|
|
(/\bwhether\b/.test(text) &&
|
|
/\b(addresses|solve|solves|justifies|supports|demonstrates)\b/.test(
|
|
text,
|
|
) &&
|
|
/\b(problem|value|need|demand|audience|customer|user)\b/.test(text));
|
|
|
|
return {
|
|
prerequisiteConceptCount,
|
|
unresolvedDependencies,
|
|
conjunctionCount,
|
|
implicitConclusion,
|
|
multipleEvidenceDimensions:
|
|
prerequisiteConceptCount >= 2 || conjunctionCount >= 2,
|
|
};
|
|
}
|
|
|
|
export function assessUnknownAnswerability({ node, graph }) {
|
|
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
|
const dimensionSummary = countIndependentAnswerDimensions(node, graph);
|
|
const independentlyAnswerable =
|
|
!dimensionSummary.implicitConclusion &&
|
|
!dimensionSummary.multipleEvidenceDimensions &&
|
|
dimensionSummary.unresolvedDependencies === 0 &&
|
|
dimensionSummary.prerequisiteConceptCount <= 1;
|
|
|
|
if (independentlyAnswerable) {
|
|
return {
|
|
independentlyAnswerable: true,
|
|
reason:
|
|
"This unknown can be answered directly without first resolving several prerequisite investigations.",
|
|
prerequisiteConceptCount: dimensionSummary.prerequisiteConceptCount,
|
|
decompositionRequired: false,
|
|
};
|
|
}
|
|
|
|
return {
|
|
independentlyAnswerable: false,
|
|
reason: dimensionSummary.implicitConclusion
|
|
? "This unknown asks for a higher-level conclusion that depends on several smaller investigations."
|
|
: "This unknown still bundles multiple prerequisite evidence dimensions, so it should be decomposed before it becomes the selected question.",
|
|
prerequisiteConceptCount: Math.max(
|
|
dimensionSummary.prerequisiteConceptCount,
|
|
dimensionSummary.unresolvedDependencies,
|
|
dimensionSummary.conjunctionCount + 1,
|
|
),
|
|
decompositionRequired: true,
|
|
};
|
|
}
|
|
|
|
export function assessUnknownAtomicity({ node, graph }) {
|
|
const nodeText = normaliseText(
|
|
`${node?.label || ""} ${node?.description || ""}`,
|
|
);
|
|
|
|
if (
|
|
isDirectlyAnswerableObservationChildText(nodeText) &&
|
|
!hasCompoundAbstractSignals(nodeText)
|
|
) {
|
|
return {
|
|
atomicity: "atomic",
|
|
reason:
|
|
"This unknown isolates one specific line of enquiry and can be investigated directly.",
|
|
decompositionKind: null,
|
|
};
|
|
}
|
|
|
|
if (isRelationshipExplanationUnknown(node, graph)) {
|
|
return {
|
|
atomicity: "composite",
|
|
reason:
|
|
"This unknown asks for a broad explanation across multiple observations, so it should be decomposed before asking a direct question.",
|
|
decompositionKind: "relationship_explanation",
|
|
};
|
|
}
|
|
|
|
if (hasCompoundAbstractSignals(nodeText)) {
|
|
return {
|
|
atomicity: "composite",
|
|
reason:
|
|
"This unknown still bundles multiple abstract uncertainties together, so it should be decomposed before asking it directly.",
|
|
decompositionKind: "compound_child",
|
|
};
|
|
}
|
|
|
|
if (
|
|
isFocusedAtomicUnknownText(nodeText) &&
|
|
!isBroadCompositeUnknownText(nodeText)
|
|
) {
|
|
return {
|
|
atomicity: "atomic",
|
|
reason:
|
|
"This unknown already targets a single concrete detail that can be investigated directly.",
|
|
decompositionKind: null,
|
|
};
|
|
}
|
|
|
|
if (isBroadCompositeUnknownText(nodeText)) {
|
|
return {
|
|
atomicity: "composite",
|
|
reason:
|
|
"This unknown combines multiple broad candidate explanations, so it should be split into smaller dimensions first.",
|
|
decompositionKind: "broad_explanation",
|
|
};
|
|
}
|
|
|
|
if (isCommercialValidationUnknownText(nodeText)) {
|
|
return {
|
|
atomicity: "composite",
|
|
reason:
|
|
"This unknown combines multiple problem-validation or commercial-validation dimensions, so it should be decomposed before asking a direct question.",
|
|
decompositionKind: "commercial_validation",
|
|
};
|
|
}
|
|
|
|
return {
|
|
atomicity: "atomic",
|
|
reason:
|
|
"No deterministic composite pattern was detected, so the unknown can be investigated directly.",
|
|
decompositionKind: null,
|
|
};
|
|
}
|
|
|
|
const ALL_REASONING_PATTERNS = [
|
|
"decision",
|
|
"explanation",
|
|
"contradiction",
|
|
"definition",
|
|
"diagnosis",
|
|
"comparison",
|
|
"prioritisation",
|
|
];
|
|
|
|
const QUESTION_FAMILIES_BY_PATTERN = {
|
|
decision: [
|
|
"decision_foundation",
|
|
"decision_evidence",
|
|
"decision_threshold",
|
|
"definition",
|
|
],
|
|
explanation: ["explanation", "comparison"],
|
|
contradiction: ["contradiction", "comparison", "explanation"],
|
|
definition: ["definition"],
|
|
diagnosis: ["diagnosis", "comparison"],
|
|
comparison: ["comparison"],
|
|
prioritisation: ["prioritisation", "decision_threshold"],
|
|
};
|
|
|
|
const STRATEGIES_BY_PATTERN = {
|
|
decision: [
|
|
"decision_threshold",
|
|
"evidence_gathering",
|
|
"definition",
|
|
"baseline_reconstruction",
|
|
],
|
|
explanation: ["evidence_gathering", "baseline_reconstruction"],
|
|
contradiction: [
|
|
"contradiction_resolution",
|
|
"baseline_reconstruction",
|
|
"evidence_gathering",
|
|
],
|
|
definition: ["definition"],
|
|
diagnosis: ["evidence_gathering", "baseline_reconstruction"],
|
|
comparison: ["baseline_reconstruction", "evidence_gathering"],
|
|
prioritisation: ["decision_threshold", "evidence_gathering"],
|
|
};
|
|
|
|
function buildParentChain(node, graph) {
|
|
const nodesById = buildNodeMap(graph);
|
|
const chain = [];
|
|
let current = node?.parentId ? nodesById.get(node.parentId) : null;
|
|
|
|
while (current) {
|
|
chain.push(current);
|
|
current = current.parentId ? nodesById.get(current.parentId) : null;
|
|
}
|
|
|
|
return chain;
|
|
}
|
|
|
|
function hasDecisionContext(node, graph, relatedNodes = []) {
|
|
const ancestry = buildParentChain(node, graph);
|
|
const contextText = normaliseText(
|
|
[
|
|
graph?.centralStatement,
|
|
node?.label,
|
|
node?.description,
|
|
...relatedNodes.map((relatedNode) => relatedNode.label),
|
|
...relatedNodes.map((relatedNode) => relatedNode.description),
|
|
...ancestry.map((ancestor) => ancestor.label),
|
|
...ancestry.map((ancestor) => ancestor.description),
|
|
...collectResolvedContextValues(graph),
|
|
]
|
|
.filter(Boolean)
|
|
.join(" "),
|
|
);
|
|
|
|
return /\b(whether to|build|launch|continue|proceed|invest|commercially justified|commercial justification|commercial value|business case|viability)\b/.test(
|
|
contextText,
|
|
);
|
|
}
|
|
|
|
function hasObservationRelationshipTopology(node, graph, relatedNodes = []) {
|
|
const ancestry = buildParentChain(node, graph);
|
|
const topologyNodes = [node, ...relatedNodes, ...ancestry].filter(Boolean);
|
|
|
|
return (
|
|
collectObservationNodes(graph).length >= 2 &&
|
|
topologyNodes.some(
|
|
(candidate) =>
|
|
candidate.kind === "observation" || candidate.kind === "relationship",
|
|
)
|
|
);
|
|
}
|
|
|
|
function isDefinitionPatternCandidate(node, graph) {
|
|
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
|
return isDefinitionLikeUnknown(
|
|
text,
|
|
`${text} ${graph?.centralStatement || ""}`,
|
|
);
|
|
}
|
|
|
|
function isContradictionPatternCandidate(node, graph, relatedNodes = []) {
|
|
const relationshipStatus = graph?.reasoningState?.relationshipStatus ?? null;
|
|
const text = normaliseText(
|
|
[
|
|
node?.label,
|
|
node?.description,
|
|
...relatedNodes.map((relatedNode) => relatedNode.label),
|
|
...relatedNodes.map((relatedNode) => relatedNode.description),
|
|
]
|
|
.filter(Boolean)
|
|
.join(" "),
|
|
);
|
|
|
|
return (
|
|
relationshipStatus === "contradictory" ||
|
|
/\b(contradiction|contradict|conflict|inconsistent|mismatch|opposing)\b/.test(
|
|
text,
|
|
)
|
|
);
|
|
}
|
|
|
|
function isComparisonPatternCandidate(node, graph, relatedNodes = []) {
|
|
if (isRelationshipExplanationUnknown(node, graph)) {
|
|
return false;
|
|
}
|
|
|
|
const text = normaliseText(
|
|
[
|
|
node?.label,
|
|
node?.description,
|
|
...relatedNodes.map((relatedNode) => relatedNode.label),
|
|
...relatedNodes.map((relatedNode) => relatedNode.description),
|
|
]
|
|
.filter(Boolean)
|
|
.join(" "),
|
|
);
|
|
|
|
return (
|
|
(hasObservationRelationshipTopology(node, graph, relatedNodes) ||
|
|
collectObservationNodes(graph).length >= 2 ||
|
|
graph?.reasoningState?.comparabilityStatus === "uncertain") &&
|
|
/\b(compare|comparison|different timing|measured|measurement|basis|scale|period|alternative|alternatives|better than)\b/.test(
|
|
text,
|
|
)
|
|
);
|
|
}
|
|
|
|
function isExplanationPatternCandidate(node, graph, relatedNodes = []) {
|
|
if (isRelationshipExplanationUnknown(node, graph)) return true;
|
|
|
|
const text = normaliseText(
|
|
[
|
|
node?.label,
|
|
node?.description,
|
|
...relatedNodes.map((relatedNode) => relatedNode.label),
|
|
...relatedNodes.map((relatedNode) => relatedNode.description),
|
|
]
|
|
.filter(Boolean)
|
|
.join(" "),
|
|
);
|
|
|
|
return (
|
|
hasObservationRelationshipTopology(node, graph, relatedNodes) &&
|
|
/\b(explain|explanation|why .* but|difference between|divergence|what changed|moved differently)\b/.test(
|
|
text,
|
|
)
|
|
);
|
|
}
|
|
|
|
function isPrioritisationPatternCandidate(node, graph, relatedNodes = []) {
|
|
const text = normaliseText(
|
|
[
|
|
node?.label,
|
|
node?.description,
|
|
graph?.centralStatement,
|
|
...relatedNodes.map((relatedNode) => relatedNode.label),
|
|
]
|
|
.filter(Boolean)
|
|
.join(" "),
|
|
);
|
|
|
|
return /\b(prioritise|prioritize|priority|rank|ranking|trade off|tradeoff|which first)\b/.test(
|
|
text,
|
|
);
|
|
}
|
|
|
|
export function selectReasoningPattern({ node, graph, context = {} }) {
|
|
const relatedNodes = collectRelatedNodes(node, graph);
|
|
const patternContext = {
|
|
hasDecisionContext: hasDecisionContext(node, graph, relatedNodes),
|
|
hasObservationRelationshipTopology: hasObservationRelationshipTopology(
|
|
node,
|
|
graph,
|
|
relatedNodes,
|
|
),
|
|
};
|
|
|
|
if (isDefinitionPatternCandidate(node, graph)) {
|
|
return {
|
|
pattern: "definition",
|
|
reason:
|
|
"Selected definition because the active unknown is about meaning, scope, or term boundaries.",
|
|
context: patternContext,
|
|
};
|
|
}
|
|
|
|
if (isContradictionPatternCandidate(node, graph, relatedNodes)) {
|
|
return {
|
|
pattern: "contradiction",
|
|
reason:
|
|
"Selected contradiction because the graph indicates opposing claims or incompatible observations.",
|
|
context: patternContext,
|
|
};
|
|
}
|
|
|
|
if (isComparisonPatternCandidate(node, graph, relatedNodes)) {
|
|
return {
|
|
pattern: "comparison",
|
|
reason:
|
|
"Selected comparison because the active unknown is about distinguishing measurements, timing, basis, or alternatives.",
|
|
context: patternContext,
|
|
};
|
|
}
|
|
|
|
if (isExplanationPatternCandidate(node, graph, relatedNodes)) {
|
|
return {
|
|
pattern: "explanation",
|
|
reason:
|
|
"Selected explanation because the active unknown is about accounting for a relationship between observations.",
|
|
context: patternContext,
|
|
};
|
|
}
|
|
|
|
if (patternContext.hasDecisionContext) {
|
|
return {
|
|
pattern: "decision",
|
|
reason:
|
|
"Selected decision because the active unknown sits inside a build, continue, invest, or commercial-justification decision context.",
|
|
context: patternContext,
|
|
};
|
|
}
|
|
|
|
if (isPrioritisationPatternCandidate(node, graph, relatedNodes)) {
|
|
return {
|
|
pattern: "prioritisation",
|
|
reason:
|
|
"Selected prioritisation because the active unknown is about ordering options or trade-offs.",
|
|
context: patternContext,
|
|
};
|
|
}
|
|
|
|
return {
|
|
pattern: "diagnosis",
|
|
reason:
|
|
"Selected diagnosis as the default because the active unknown needs clarifying evidence or mechanism-level investigation.",
|
|
context: patternContext,
|
|
};
|
|
}
|
|
|
|
function allowedQuestionFamiliesForPattern(pattern) {
|
|
return QUESTION_FAMILIES_BY_PATTERN[pattern] || [pattern];
|
|
}
|
|
|
|
function rejectedQuestionFamiliesForPattern(pattern) {
|
|
const allowed = new Set(allowedQuestionFamiliesForPattern(pattern));
|
|
return ALL_REASONING_PATTERNS.flatMap((candidatePattern) =>
|
|
(
|
|
QUESTION_FAMILIES_BY_PATTERN[candidatePattern] || [candidatePattern]
|
|
).filter((family) => !allowed.has(family)),
|
|
).filter((family, index, list) => list.indexOf(family) === index);
|
|
}
|
|
|
|
function constrainStrategyToReasoningPattern(strategy, reasoningPattern) {
|
|
if (!strategy) return null;
|
|
const allowedStrategies = STRATEGIES_BY_PATTERN[reasoningPattern] || [];
|
|
return allowedStrategies.includes(strategy.key) ? strategy : null;
|
|
}
|
|
|
|
function selectQuestionFamily({
|
|
node,
|
|
graph,
|
|
reasoningPattern,
|
|
investigationStrategy,
|
|
}) {
|
|
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
|
|
|
if (reasoningPattern === "definition") {
|
|
return { family: "definition", template: "definition_meaning" };
|
|
}
|
|
|
|
if (reasoningPattern === "contradiction") {
|
|
if (/\b(period|timing|basis|scale|measure|measured)\b/.test(text)) {
|
|
return {
|
|
family: "comparison",
|
|
template: "comparison_reconcile_measurement",
|
|
};
|
|
}
|
|
return {
|
|
family: "contradiction",
|
|
template: "contradiction_resolve_opposition",
|
|
};
|
|
}
|
|
|
|
if (reasoningPattern === "explanation") {
|
|
return {
|
|
family: "explanation",
|
|
template: "explanation_broad_investigation",
|
|
};
|
|
}
|
|
|
|
if (reasoningPattern === "comparison") {
|
|
return /\b(period|timing)\b/.test(text) &&
|
|
!/\bhow the two observations were measured|measurement\b/.test(text)
|
|
? { family: "comparison", template: "comparison_timing_basis" }
|
|
: { family: "comparison", template: "comparison_measurement_basis" };
|
|
}
|
|
|
|
if (reasoningPattern === "decision") {
|
|
if (
|
|
/\b(audience|customer|user|buyer|stakeholder|recipient|who experiences)\b/.test(
|
|
text,
|
|
)
|
|
) {
|
|
return { family: "decision_foundation", template: "decision_audience" };
|
|
}
|
|
if (
|
|
/\b(alternative|alternatives|better than|different from|deal with)\b/.test(
|
|
text,
|
|
)
|
|
) {
|
|
return {
|
|
family: "decision_foundation",
|
|
template: "decision_current_alternatives",
|
|
};
|
|
}
|
|
if (/\b(problem|need|demand)\b/.test(text)) {
|
|
return {
|
|
family: "decision_foundation",
|
|
template: "decision_problem_existence",
|
|
};
|
|
}
|
|
if (investigationStrategy?.key === "decision_threshold") {
|
|
return {
|
|
family: "decision_threshold",
|
|
template: "decision_threshold_outcome",
|
|
};
|
|
}
|
|
return {
|
|
family: "decision_evidence",
|
|
template: "decision_evidence_clarification",
|
|
};
|
|
}
|
|
|
|
if (reasoningPattern === "prioritisation") {
|
|
return { family: "prioritisation", template: "prioritisation_tradeoff" };
|
|
}
|
|
|
|
return investigationStrategy?.key === "baseline_reconstruction"
|
|
? { family: "comparison", template: "diagnosis_baseline_comparison" }
|
|
: { family: "diagnosis", template: "diagnosis_evidence" };
|
|
}
|
|
|
|
function buildQuestionFromFamily({
|
|
node,
|
|
graph,
|
|
reasoningPattern,
|
|
questionFamily,
|
|
selectedQuestionTemplate,
|
|
investigationStrategy,
|
|
}) {
|
|
const meaning = extractMeaning(node);
|
|
|
|
if (reasoningPattern === "decision") {
|
|
if (selectedQuestionTemplate === "decision_audience") {
|
|
return "Who experiences this problem?";
|
|
}
|
|
if (selectedQuestionTemplate === "decision_current_alternatives") {
|
|
return "How do people deal with this today?";
|
|
}
|
|
if (selectedQuestionTemplate === "decision_problem_existence") {
|
|
return "What makes you think this is a real problem?";
|
|
}
|
|
if (selectedQuestionTemplate === "decision_threshold_outcome") {
|
|
return buildQuestionFromStrategy(
|
|
investigationStrategy || {
|
|
key: "decision_threshold",
|
|
meaning,
|
|
actionPhrase: null,
|
|
},
|
|
);
|
|
}
|
|
return `What evidence would clarify ${stripTrailingPunctuation(meaning)}?`;
|
|
}
|
|
|
|
if (questionFamily === "definition") {
|
|
return `What does ${meaning} mean in this situation?`;
|
|
}
|
|
|
|
if (reasoningPattern === "comparison") {
|
|
if (selectedQuestionTemplate === "comparison_timing_basis") {
|
|
return `What evidence would clarify whether ${stripTrailingPunctuation(meaning)}?`;
|
|
}
|
|
if (selectedQuestionTemplate === "comparison_measurement_basis") {
|
|
return `What evidence would clarify ${stripTrailingPunctuation(meaning)}?`;
|
|
}
|
|
return `What evidence would clarify ${stripTrailingPunctuation(meaning)}?`;
|
|
}
|
|
|
|
if (reasoningPattern === "contradiction") {
|
|
return investigationStrategy?.key === "contradiction_resolution"
|
|
? buildQuestionFromStrategy(investigationStrategy)
|
|
: `What fact would resolve the contradiction about ${stripTrailingPunctuation(meaning)}?`;
|
|
}
|
|
|
|
if (reasoningPattern === "explanation") {
|
|
return buildBroadInvestigationQuestion(graph);
|
|
}
|
|
|
|
if (reasoningPattern === "prioritisation") {
|
|
return `Which option should be investigated first, and why?`;
|
|
}
|
|
|
|
return investigationStrategy
|
|
? buildQuestionFromStrategy(investigationStrategy)
|
|
: buildNeutralClarificationQuestion(meaning);
|
|
}
|
|
|
|
export function formulateTieResolutionQuestion({ graph }) {
|
|
const comparability = assessComparability(graph);
|
|
if (comparability.comparabilityStatus === "uncertain") {
|
|
const deferredRelationship = classifyObservationRelationship(graph);
|
|
return {
|
|
question: buildComparabilityQuestion(graph, comparability),
|
|
reason:
|
|
"Formulated to confirm whether the observations are comparable before exploring competing explanations.",
|
|
strategy: null,
|
|
investigationStrategy: null,
|
|
selectionStatus: "ambiguous",
|
|
comparabilityStatus: comparability.comparabilityStatus,
|
|
comparabilityReason: comparability.reason,
|
|
contradictionReasoningAllowed:
|
|
comparability.contradictionReasoningAllowed,
|
|
relationshipStatus: deferredRelationship.relationshipStatus,
|
|
relationshipReason: deferredRelationship.reason,
|
|
relationshipAssessed: deferredRelationship.relationshipAssessed,
|
|
reasoningPattern: "comparison",
|
|
reasoningPatternReason:
|
|
"Tie resolution is using the comparison family because comparability is still unresolved.",
|
|
allowedQuestionFamilies: allowedQuestionFamiliesForPattern("comparison"),
|
|
rejectedQuestionFamilies:
|
|
rejectedQuestionFamiliesForPattern("comparison"),
|
|
questionFamily: "comparison",
|
|
selectedQuestionTemplate: "comparison_tie_resolution",
|
|
questionRequired: true,
|
|
reasoningStages: deferredRelationship.reasoningStages,
|
|
};
|
|
}
|
|
|
|
const relationship = classifyObservationRelationship(graph);
|
|
if (!relationship.questionRequired) {
|
|
return {
|
|
question: null,
|
|
reason: relationship.reason,
|
|
strategy: null,
|
|
investigationStrategy: null,
|
|
selectionStatus: "ambiguous",
|
|
comparabilityStatus: comparability.comparabilityStatus,
|
|
comparabilityReason: comparability.reason,
|
|
relationshipStatus: relationship.relationshipStatus,
|
|
relationshipReason: relationship.reason,
|
|
relationshipAssessed: relationship.relationshipAssessed,
|
|
contradictionReasoningAllowed: relationship.contradictionReasoningAllowed,
|
|
reasoningPattern: "comparison",
|
|
reasoningPatternReason:
|
|
"Tie resolution remains in the comparison family because no distinct winning unknown exists.",
|
|
allowedQuestionFamilies: allowedQuestionFamiliesForPattern("comparison"),
|
|
rejectedQuestionFamilies:
|
|
rejectedQuestionFamiliesForPattern("comparison"),
|
|
questionFamily: "comparison",
|
|
selectedQuestionTemplate: "comparison_no_question_required",
|
|
questionRequired: relationship.questionRequired,
|
|
questionSuppressedReason: relationship.questionSuppressedReason,
|
|
reasoningStages: relationship.reasoningStages,
|
|
};
|
|
}
|
|
|
|
if (relationship.relationshipStatus === "potentially_related") {
|
|
return {
|
|
question: buildBroadInvestigationQuestion(graph),
|
|
reason:
|
|
"Formulated as a neutral relationship question because the observations may be related without being contradictory.",
|
|
strategy: null,
|
|
investigationStrategy: null,
|
|
selectionStatus: "ambiguous",
|
|
comparabilityStatus: comparability.comparabilityStatus,
|
|
comparabilityReason: comparability.reason,
|
|
relationshipStatus: relationship.relationshipStatus,
|
|
relationshipReason: relationship.reason,
|
|
relationshipAssessed: relationship.relationshipAssessed,
|
|
contradictionReasoningAllowed: relationship.contradictionReasoningAllowed,
|
|
reasoningPattern: "explanation",
|
|
reasoningPatternReason:
|
|
"Tie resolution is using the explanation family because the observations appear related and need a neutral explanation question.",
|
|
allowedQuestionFamilies: allowedQuestionFamiliesForPattern("explanation"),
|
|
rejectedQuestionFamilies:
|
|
rejectedQuestionFamiliesForPattern("explanation"),
|
|
questionFamily: "explanation",
|
|
selectedQuestionTemplate: "explanation_broad_investigation",
|
|
questionRequired: relationship.questionRequired,
|
|
reasoningStages: relationship.reasoningStages,
|
|
};
|
|
}
|
|
|
|
const { centralStatement, contradictionLabel } =
|
|
detectContradictionContext(graph);
|
|
const focus =
|
|
centralStatement || contradictionLabel || "these conflicting signals";
|
|
const question = sanitizeQuestionText(
|
|
`What changed during the period that could explain why ${focus}?`,
|
|
);
|
|
|
|
return {
|
|
question,
|
|
reason:
|
|
"Formulated to distinguish between tied unresolved explanations without prematurely choosing one branch.",
|
|
strategy: null,
|
|
investigationStrategy: null,
|
|
selectionStatus: "ambiguous",
|
|
comparabilityStatus: comparability.comparabilityStatus,
|
|
comparabilityReason: comparability.reason,
|
|
relationshipStatus: relationship.relationshipStatus,
|
|
relationshipReason: relationship.reason,
|
|
relationshipAssessed: relationship.relationshipAssessed,
|
|
contradictionReasoningAllowed: relationship.contradictionReasoningAllowed,
|
|
reasoningPattern: "contradiction",
|
|
reasoningPatternReason:
|
|
"Tie resolution is using the contradiction family because the graph is trying to distinguish incompatible explanations.",
|
|
allowedQuestionFamilies: allowedQuestionFamiliesForPattern("contradiction"),
|
|
rejectedQuestionFamilies:
|
|
rejectedQuestionFamiliesForPattern("contradiction"),
|
|
questionFamily: "contradiction",
|
|
selectedQuestionTemplate: "contradiction_distinguishing_change",
|
|
questionRequired: relationship.questionRequired,
|
|
reasoningStages: relationship.reasoningStages,
|
|
};
|
|
}
|
|
|
|
function extractActionPhrase(texts) {
|
|
for (const text of texts) {
|
|
const value = String(text || "").trim();
|
|
if (!value) continue;
|
|
|
|
const matches = [
|
|
value.match(/\b(?:whether|deciding|decision) to\s+([^.,;:]+)/i),
|
|
value.match(/\b(?:justify|continuing|proceeding with)\s+([^.,;:]+)/i),
|
|
value.match(
|
|
/\b(build|launch|adopt|buy|continue|proceed|invest in|fund)\s+([^.,;:]+)/i,
|
|
),
|
|
].filter(Boolean);
|
|
|
|
const match = matches[0];
|
|
if (!match) continue;
|
|
|
|
const phrase = (match[1] || `${match[1] || ""} ${match[2] || ""}`)
|
|
.replace(/^to\s+/i, "")
|
|
.trim();
|
|
|
|
if (phrase) {
|
|
return phrase;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function toGerundPhrase(phrase) {
|
|
const trimmed = String(phrase || "").trim();
|
|
if (!trimmed) return "proceeding with this decision";
|
|
|
|
const [firstWord, ...rest] = trimmed.split(/\s+/);
|
|
const lower = firstWord.toLowerCase();
|
|
const irregular = {
|
|
be: "being",
|
|
build: "building",
|
|
continue: "continuing",
|
|
decide: "deciding",
|
|
proceed: "proceeding",
|
|
launch: "launching",
|
|
invest: "investing",
|
|
fund: "funding",
|
|
buy: "buying",
|
|
pay: "paying",
|
|
adopt: "adopting",
|
|
};
|
|
|
|
let gerund = irregular[lower];
|
|
if (!gerund) {
|
|
if (lower.endsWith("e") && !lower.endsWith("ee")) {
|
|
gerund = `${lower.slice(0, -1)}ing`;
|
|
} else {
|
|
gerund = `${lower}ing`;
|
|
}
|
|
}
|
|
|
|
return [gerund, ...rest].join(" ");
|
|
}
|
|
|
|
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 effectiveReasoningPattern =
|
|
context.reasoningPattern ||
|
|
selectReasoningPattern({ node, graph, context }).pattern;
|
|
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 nodeText = normaliseText(
|
|
`${node?.label || ""} ${node?.description || ""}`,
|
|
);
|
|
const relatedText = normaliseText(
|
|
relatedNodes
|
|
.map((relatedNode) => `${relatedNode.label} ${relatedNode.description}`)
|
|
.join(" "),
|
|
);
|
|
const resolvedValues = collectResolvedContextValues(graph);
|
|
const actionPhrase = extractActionPhrase([
|
|
...resolvedValues,
|
|
...relatedNodes.map((relatedNode) => relatedNode.value),
|
|
...relatedNodes.map((relatedNode) => relatedNode.label),
|
|
...relatedNodes.map((relatedNode) => relatedNode.description),
|
|
graph?.centralStatement,
|
|
]);
|
|
|
|
const decisionContext =
|
|
/\b(decision|whether to|build|launch|continue|proceed|invest|allocate)\b/.test(
|
|
`${text} ${relatedText} ${resolvedValues.join(" ")}`,
|
|
) || Boolean(actionPhrase);
|
|
|
|
const hasConstraintLanguage =
|
|
/\b(constraint|limit|budget|deadline|requirement|regulation|capacity)\b/.test(
|
|
text,
|
|
);
|
|
const hasPrimaryConstraintLanguage =
|
|
/\b(constraint|limit|budget|deadline|requirement|regulation|capacity)\b/.test(
|
|
nodeText,
|
|
);
|
|
|
|
const hasBaselineLanguage =
|
|
/\b(before|previous|baseline|prior|comparable state)\b/.test(text);
|
|
const hasPrimaryBaselineLanguage =
|
|
/\b(before|previous|baseline|prior|comparable state)\b/.test(nodeText);
|
|
|
|
let selectedStrategy = null;
|
|
|
|
if (hasBaselineLanguage && hasPrimaryBaselineLanguage) {
|
|
selectedStrategy = buildInvestigationStrategy({
|
|
key: "baseline_reconstruction",
|
|
reason:
|
|
"Selected because the unknown explicitly references a missing previous or baseline state.",
|
|
node,
|
|
graph,
|
|
relatedNodes,
|
|
meaning,
|
|
actionPhrase,
|
|
});
|
|
}
|
|
|
|
const hasDefinitionLanguage = isDefinitionLikeUnknown(nodeText, text);
|
|
const hasPrimaryDefinitionLanguage =
|
|
/\b(define|definition|meaning|term|terminology)\b/.test(nodeText);
|
|
const hasCriteriaLanguage =
|
|
/\b(success criteria|success threshold|threshold|decision criteria|criterion|justify|sufficient)\b/.test(
|
|
nodeText,
|
|
);
|
|
const hasDecisionValueLanguage =
|
|
decisionContext &&
|
|
/\b(value|commercial value|commercial viability|viability|justify|sufficient|success|threshold|criterion)\b/.test(
|
|
text,
|
|
);
|
|
const hasMeasurementLanguage =
|
|
/\b(metric|measure|measurable|roi|revenue projection|benchmark)\b/.test(
|
|
text,
|
|
);
|
|
const hasEvidenceLanguage =
|
|
/\b(evidence|proof|validate|validation|signal|demand)\b/.test(text) ||
|
|
isClaimLikeUnknown(node, 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 (
|
|
!selectedStrategy &&
|
|
(hasPrimaryDefinitionLanguage || hasDefinitionLanguage)
|
|
) {
|
|
selectedStrategy = buildInvestigationStrategy({
|
|
key: "definition",
|
|
reason:
|
|
"Selected because the unknown is primarily about clarifying what a term means in this case.",
|
|
node,
|
|
graph,
|
|
relatedNodes,
|
|
meaning,
|
|
actionPhrase,
|
|
});
|
|
}
|
|
|
|
if (!selectedStrategy && (hasDecisionValueLanguage || hasCriteriaLanguage)) {
|
|
selectedStrategy = buildInvestigationStrategy({
|
|
key: "decision_threshold",
|
|
reason:
|
|
"Selected because the unknown determines the threshold for making or justifying a decision.",
|
|
node,
|
|
graph,
|
|
relatedNodes,
|
|
meaning,
|
|
actionPhrase,
|
|
});
|
|
}
|
|
|
|
if (
|
|
!selectedStrategy &&
|
|
(hasPrimaryBaselineLanguage || hasBaselineLanguage)
|
|
) {
|
|
selectedStrategy = 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 (!selectedStrategy && hasContradictionLanguage) {
|
|
selectedStrategy = 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 (
|
|
!selectedStrategy &&
|
|
(hasEvidenceLanguage || hasMeasurementLanguage || hasConstraintLanguage)
|
|
) {
|
|
selectedStrategy = 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,
|
|
});
|
|
}
|
|
|
|
return constrainStrategyToReasoningPattern(
|
|
selectedStrategy,
|
|
effectiveReasoningPattern,
|
|
);
|
|
}
|
|
|
|
function buildQuestionFromStrategy(strategy) {
|
|
switch (strategy.key) {
|
|
case "decision_threshold":
|
|
return strategy.actionPhrase
|
|
? `What outcome would demonstrate enough value to justify ${toGerundPhrase(strategy.actionPhrase)}?`
|
|
: "What outcome would be sufficient to justify this decision?";
|
|
case "definition":
|
|
return `What does ${strategy.meaning} mean in this situation?`;
|
|
case "evidence_gathering":
|
|
return `What evidence would clarify ${stripTrailingPunctuation(strategy.meaning)}?`;
|
|
case "baseline_reconstruction":
|
|
return `What was the comparable state before ${strategy.meaning}?`;
|
|
case "contradiction_resolution":
|
|
return `What fact would resolve the contradiction about ${strategy.meaning}?`;
|
|
default:
|
|
return `What specific fact would resolve whether ${strategy.meaning} is true?`;
|
|
}
|
|
}
|
|
|
|
function isCompoundQuestion(question) {
|
|
const trimmed = String(question || "").trim();
|
|
const questionMarks = (trimmed.match(/\?/g) || []).length;
|
|
|
|
if (questionMarks !== 1) return true;
|
|
if (/\?\s*(and|or)\b/i.test(trimmed)) return true;
|
|
if (/\b(and|or)\b[^?]{0,80}\?/i.test(trimmed) && /,/.test(trimmed)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function validateFormulatedQuestion(question, meaning) {
|
|
const trimmed = String(question || "").trim();
|
|
const lower = trimmed.toLowerCase();
|
|
const meaningWords = normaliseText(meaning)
|
|
.split(" ")
|
|
.filter((word) => word.length > 3);
|
|
const overlappingWord = meaningWords.find((word) => lower.includes(word));
|
|
|
|
if (!trimmed) return false;
|
|
if ((trimmed.match(/\?/g) || []).length !== 1) return false;
|
|
if (isCompoundQuestion(trimmed)) return false;
|
|
if (/^what is\s+/i.test(trimmed)) return false;
|
|
if (/^how should uncertainty regarding\b/i.test(trimmed)) return false;
|
|
if (/^what would resolve uncertainty regarding\b/i.test(trimmed))
|
|
return false;
|
|
if (/\)\.\s+[A-Z]/.test(trimmed)) return false;
|
|
if (/\bis true\?$/i.test(trimmed) && !/^whether\b/i.test(meaning))
|
|
return false;
|
|
if (
|
|
/\bprice|pricing|price point\b/i.test(trimmed) &&
|
|
!/\bprice\b/i.test(meaning)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
!overlappingWord &&
|
|
!/\b(decision|evidence|constraint|customer|value|outcome)\b/i.test(trimmed)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function countPrimaryConcepts(question) {
|
|
let concepts = 1;
|
|
if (/\band what\b/i.test(question)) concepts += 1;
|
|
if (/\bhow often\b.*\bwhat\b/i.test(question)) concepts += 1;
|
|
if (/\bcost\b.*\bbudget\b|\bbudget\b.*\bcost\b/i.test(question)) {
|
|
concepts += 1;
|
|
}
|
|
if (/\bwho\b.*\bwhat\b|\bwhat\b.*\bwho\b/i.test(question)) concepts += 1;
|
|
return concepts;
|
|
}
|
|
|
|
export function assessQuestionComplexity({ question, selectedUnknown, graph }) {
|
|
const text = String(question || "").trim();
|
|
const lower = text.toLowerCase();
|
|
const reasons = [];
|
|
const compoundQuestionSignals = [];
|
|
const abstractMatches =
|
|
lower.match(
|
|
/\b(justified confidence|financial or operational cost|comparable decision support methods|commercial justification|decision support methods|value recipient)\b/g,
|
|
) || [];
|
|
const primaryConceptCount = countPrimaryConcepts(lower);
|
|
|
|
if ((text.match(/\?/g) || []).length !== 1) {
|
|
reasons.push("multiple_question_marks");
|
|
compoundQuestionSignals.push("multiple_question_marks");
|
|
}
|
|
if (/\band what\b|\bwhat .* and .* what\b/i.test(text)) {
|
|
reasons.push("multiple_requested_answers");
|
|
compoundQuestionSignals.push("joined_requests");
|
|
}
|
|
if (/,[^,]{0,60},/.test(text) || /,\s*(and|or)\b/i.test(text)) {
|
|
reasons.push("list_like_question");
|
|
compoundQuestionSignals.push("comma_list");
|
|
}
|
|
if (/\bcost\b.*\bbudget\b|\bbudget\b.*\bcost\b/i.test(text)) {
|
|
reasons.push("cost_and_budget_combined");
|
|
compoundQuestionSignals.push("distinct_measures_combined");
|
|
}
|
|
if (primaryConceptCount > 1) {
|
|
reasons.push("multiple_primary_concepts");
|
|
}
|
|
if (text.split(/\s+/).length > 20) {
|
|
reasons.push("very_long_question");
|
|
}
|
|
if (abstractMatches.length > 1) {
|
|
reasons.push("abstract_term_chain");
|
|
}
|
|
|
|
const cognitiveLoad =
|
|
reasons.length >= 3 ? "high" : reasons.length === 2 ? "medium" : "low";
|
|
|
|
return {
|
|
acceptable: reasons.length === 0,
|
|
primaryConceptCount,
|
|
compoundQuestionSignals: [...new Set(compoundQuestionSignals)],
|
|
abstractTermCount: abstractMatches.length,
|
|
cognitiveLoad,
|
|
reasons,
|
|
selectedUnknownId: selectedUnknown?.id ?? null,
|
|
graphCentralStatement: graph?.centralStatement ?? null,
|
|
};
|
|
}
|
|
|
|
function applyPlainLanguageNormalisations(question) {
|
|
const normalisations = [];
|
|
let next = String(question || "");
|
|
const replacements = [
|
|
[
|
|
/individuals experiencing insufficient justified confidence/gi,
|
|
"people who struggle to feel confident about a decision",
|
|
"simplified_justified_confidence_phrase",
|
|
],
|
|
[
|
|
/current financial or operational cost/gi,
|
|
"current cost",
|
|
"simplified_cost_phrase",
|
|
],
|
|
[
|
|
/comparable decision support methods/gi,
|
|
"other ways they deal with the problem",
|
|
"simplified_decision_support_phrase",
|
|
],
|
|
[
|
|
/the relevant customer, user, or value recipient/gi,
|
|
"the people affected",
|
|
"simplified_actor_phrase",
|
|
],
|
|
];
|
|
|
|
for (const [pattern, replacement, code] of replacements) {
|
|
if (pattern.test(next)) {
|
|
next = next.replace(pattern, replacement);
|
|
normalisations.push(code);
|
|
}
|
|
}
|
|
|
|
next = sanitizeQuestionText(next);
|
|
return { question: next, normalisations };
|
|
}
|
|
|
|
export function formulateQuestion({ node, graph, context = {} }) {
|
|
if (context.selectionState?.status === "ambiguous") {
|
|
return formulateTieResolutionQuestion({ graph });
|
|
}
|
|
|
|
const reasoningPatternSelection = selectReasoningPattern({
|
|
node,
|
|
graph,
|
|
context,
|
|
});
|
|
const allowedQuestionFamilies = allowedQuestionFamiliesForPattern(
|
|
reasoningPatternSelection.pattern,
|
|
);
|
|
const rejectedQuestionFamilies = rejectedQuestionFamiliesForPattern(
|
|
reasoningPatternSelection.pattern,
|
|
);
|
|
|
|
if (isUserOwnedMeaningBoundaryUnknown(node)) {
|
|
const question = sanitizeQuestionText(
|
|
buildUserMeaningClarificationQuestion(node),
|
|
);
|
|
const questionComplexity = assessQuestionComplexity({
|
|
question,
|
|
selectedUnknown: node,
|
|
graph,
|
|
});
|
|
|
|
return {
|
|
question,
|
|
reason:
|
|
"Formulated as a user-clarification question because this unresolved distinction depends on the user's own meaning rather than external evidence.",
|
|
strategy: null,
|
|
investigationStrategy: null,
|
|
reasoningPattern: reasoningPatternSelection.pattern,
|
|
reasoningPatternReason: reasoningPatternSelection.reason,
|
|
questionFamily: "prioritisation",
|
|
allowedQuestionFamilies,
|
|
rejectedQuestionFamilies,
|
|
selectedQuestionTemplate: "user_meaning_clarification",
|
|
questionComplexity,
|
|
plainLanguageNormalisations: [],
|
|
};
|
|
}
|
|
|
|
const foundationalDirectQuestion = buildFoundationalDirectQuestion(node);
|
|
if (
|
|
foundationalDirectQuestion &&
|
|
reasoningPatternSelection.pattern === "decision"
|
|
) {
|
|
const plainLanguage = applyPlainLanguageNormalisations(
|
|
sanitizeQuestionText(foundationalDirectQuestion),
|
|
);
|
|
const questionComplexity = assessQuestionComplexity({
|
|
question: plainLanguage.question,
|
|
selectedUnknown: node,
|
|
graph,
|
|
});
|
|
|
|
return {
|
|
question: plainLanguage.question,
|
|
reason:
|
|
"Formulated as a direct foundational question because this child unknown should be answered one step at a time.",
|
|
strategy: null,
|
|
investigationStrategy: null,
|
|
reasoningPattern: reasoningPatternSelection.pattern,
|
|
reasoningPatternReason: reasoningPatternSelection.reason,
|
|
questionFamily: "decision_foundation",
|
|
allowedQuestionFamilies,
|
|
rejectedQuestionFamilies,
|
|
selectedQuestionTemplate: "decision_foundation_direct_child",
|
|
questionComplexity,
|
|
plainLanguageNormalisations: plainLanguage.normalisations,
|
|
};
|
|
}
|
|
|
|
const investigationStrategy = selectInvestigationStrategy({
|
|
node,
|
|
graph,
|
|
context: {
|
|
...context,
|
|
reasoningPattern: reasoningPatternSelection.pattern,
|
|
},
|
|
});
|
|
|
|
const questionFamilySelection = selectQuestionFamily({
|
|
node,
|
|
graph,
|
|
reasoningPattern: reasoningPatternSelection.pattern,
|
|
investigationStrategy,
|
|
});
|
|
|
|
let question = buildQuestionFromFamily({
|
|
node,
|
|
graph,
|
|
reasoningPattern: reasoningPatternSelection.pattern,
|
|
questionFamily: questionFamilySelection.family,
|
|
selectedQuestionTemplate: questionFamilySelection.template,
|
|
investigationStrategy,
|
|
});
|
|
|
|
question = sanitizeQuestionText(question);
|
|
const plainLanguage = applyPlainLanguageNormalisations(question);
|
|
question = plainLanguage.question;
|
|
|
|
const fallbackMeaning = extractMeaning(node);
|
|
if (
|
|
!validateFormulatedQuestion(
|
|
question,
|
|
investigationStrategy?.meaning || fallbackMeaning,
|
|
)
|
|
) {
|
|
question = sanitizeQuestionText(
|
|
(investigationStrategy &&
|
|
isClaimLikeUnknown(
|
|
node,
|
|
normaliseText(
|
|
collectRelatedNodes(node, graph)
|
|
.map(
|
|
(relatedNode) =>
|
|
`${relatedNode.label} ${relatedNode.description}`,
|
|
)
|
|
.concat([node?.label, node?.description])
|
|
.filter(Boolean)
|
|
.join(" "),
|
|
),
|
|
)) ||
|
|
reasoningPatternSelection.pattern === "diagnosis"
|
|
? buildEvidenceFallbackQuestion(fallbackMeaning)
|
|
: buildNeutralClarificationQuestion(fallbackMeaning),
|
|
);
|
|
}
|
|
|
|
const questionComplexity = assessQuestionComplexity({
|
|
question,
|
|
selectedUnknown: node,
|
|
graph,
|
|
});
|
|
|
|
return {
|
|
question,
|
|
reason: investigationStrategy
|
|
? `Formulated from graph context using the ${investigationStrategy.key} investigation strategy.`
|
|
: "Formulated as a neutral clarification question because no narrower investigation strategy clearly applied.",
|
|
strategy: investigationStrategy?.key ?? null,
|
|
investigationStrategy,
|
|
reasoningPattern: reasoningPatternSelection.pattern,
|
|
reasoningPatternReason: reasoningPatternSelection.reason,
|
|
questionFamily: questionFamilySelection.family,
|
|
allowedQuestionFamilies,
|
|
rejectedQuestionFamilies,
|
|
selectedQuestionTemplate: questionFamilySelection.template,
|
|
questionComplexity,
|
|
plainLanguageNormalisations: plainLanguage.normalisations,
|
|
};
|
|
}
|