feat: enforce one-concept questions
This commit is contained in:
@@ -498,6 +498,33 @@ function buildBroadInvestigationQuestion(graph) {
|
||||
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 (/^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 || ""}`);
|
||||
return (
|
||||
@@ -514,6 +541,12 @@ function isBroadCompositeUnknownText(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(
|
||||
@@ -592,6 +625,15 @@ export function assessUnknownAtomicity({ node, graph }) {
|
||||
};
|
||||
}
|
||||
|
||||
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:
|
||||
@@ -998,11 +1040,133 @@ function validateFormulatedQuestion(question, meaning) {
|
||||
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 foundationalDirectQuestion = buildFoundationalDirectQuestion(node);
|
||||
if (foundationalDirectQuestion) {
|
||||
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,
|
||||
questionComplexity,
|
||||
plainLanguageNormalisations: plainLanguage.normalisations,
|
||||
};
|
||||
}
|
||||
|
||||
const investigationStrategy = selectInvestigationStrategy({
|
||||
node,
|
||||
graph,
|
||||
@@ -1016,6 +1180,8 @@ export function formulateQuestion({ node, graph, context = {} }) {
|
||||
: buildNeutralClarificationQuestion(extractMeaning(node));
|
||||
|
||||
question = sanitizeQuestionText(question);
|
||||
const plainLanguage = applyPlainLanguageNormalisations(question);
|
||||
question = plainLanguage.question;
|
||||
|
||||
const fallbackMeaning = extractMeaning(node);
|
||||
if (
|
||||
@@ -1044,6 +1210,12 @@ export function formulateQuestion({ node, graph, context = {} }) {
|
||||
);
|
||||
}
|
||||
|
||||
const questionComplexity = assessQuestionComplexity({
|
||||
question,
|
||||
selectedUnknown: node,
|
||||
graph,
|
||||
});
|
||||
|
||||
return {
|
||||
question,
|
||||
reason: investigationStrategy
|
||||
@@ -1051,5 +1223,7 @@ export function formulateQuestion({ node, graph, context = {} }) {
|
||||
: "Formulated as a neutral clarification question because no narrower investigation strategy clearly applied.",
|
||||
strategy: investigationStrategy?.key ?? null,
|
||||
investigationStrategy,
|
||||
questionComplexity,
|
||||
plainLanguageNormalisations: plainLanguage.normalisations,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user