421 lines
12 KiB
JavaScript
421 lines
12 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 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 = 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 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 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);
|
|
|
|
if (hasBaselineLanguage && hasPrimaryBaselineLanguage) {
|
|
return buildInvestigationStrategy({
|
|
key: "baseline_reconstruction",
|
|
reason:
|
|
"Selected because the unknown explicitly references a missing previous or baseline state.",
|
|
node,
|
|
graph,
|
|
relatedNodes,
|
|
meaning,
|
|
actionPhrase,
|
|
});
|
|
}
|
|
|
|
const hasDefinitionLanguage =
|
|
/\b(define|definition|meaning|term|terminology)\b/.test(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) ||
|
|
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 (hasPrimaryDefinitionLanguage || hasDefinitionLanguage) {
|
|
return 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 (hasDecisionValueLanguage || hasCriteriaLanguage) {
|
|
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 (hasPrimaryBaselineLanguage || hasBaselineLanguage) {
|
|
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 (hasContradictionLanguage) {
|
|
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 (hasEvidenceLanguage || hasMeasurementLanguage || hasConstraintLanguage) {
|
|
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,
|
|
});
|
|
}
|
|
|
|
return buildInvestigationStrategy({
|
|
key: "definition",
|
|
reason:
|
|
"Selected as the deterministic fallback because clarifying the exact meaning of the unknown is the narrowest first step.",
|
|
node,
|
|
graph,
|
|
relatedNodes,
|
|
meaning,
|
|
actionPhrase,
|
|
});
|
|
}
|
|
|
|
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 show whether ${strategy.meaning} is true?`;
|
|
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 (
|
|
/\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;
|
|
}
|
|
|
|
export function formulateQuestion({ node, graph, context = {} }) {
|
|
const investigationStrategy = selectInvestigationStrategy({
|
|
node,
|
|
graph,
|
|
context,
|
|
});
|
|
|
|
let question = buildQuestionFromStrategy(investigationStrategy);
|
|
|
|
if (!validateFormulatedQuestion(question, investigationStrategy.meaning)) {
|
|
question = `What evidence would resolve whether ${investigationStrategy.meaning} is true?`;
|
|
}
|
|
|
|
return {
|
|
question,
|
|
reason: `Formulated from graph context using the ${investigationStrategy.key} investigation strategy.`,
|
|
strategy: investigationStrategy.key,
|
|
investigationStrategy,
|
|
};
|
|
}
|