/** * Question Importance — passive classifier for unresolved unknowns. * * A pure-function layer that classifies each unresolved unknown into one of * four importance categories using only actual repository fields. No scoring, * no weights, no new graph structure. Designed to be validated against mock * scenarios without changing engine behaviour in any way. * * Classification rules (in order): * 1. important — Other unresolved unknown(s) depend on this one being resolved first; * OR text contains decision-context patterns ("whether to", "build", * "launch", "continue", "proceed") AND has ≥1 graph connection. * 2. helpful — Text contains evidence-related patterns (evidence, metric, measure, * criteria, validation, proof); OR has ≥2 total connections in the graph. * 3. incidental — Default when neither important nor helpful conditions are met. * 4. cannot_determine — Node label and description are both empty/null. * * IMPORTANT: This module does NOT modify engine behaviour. It must never write to * the graph, change unknown selection, or influence question generation. Validation * is done by running this classifier passively against existing scenario fixtures. */ /* ── Decision-context text patterns (from utils.js classifyUnknownPriority) ── */ const DECISION_PATTERNS = [ /whether to/i, /\bbuild\b/i, /\blaunch\b/i, /\bcontinue.*develop/i, /\bproceed\b/i, ]; /* ── Evidence-related text patterns (from utils.js classifyUnknownPriority) ── */ const EVIDENCE_PATTERNS = [ /evidence|metric|measure|criteria|validation|proof/i, ]; /* ── Normalise node label + description for text matching ── */ function normaliseText(value) { return String(value || "") .toLowerCase() .replace(/[^a-z0-9]+/g, " ") .trim(); } function collectNodeText(node) { return `${node?.label || ""} ${node?.description || ""}`.trim(); } /* ── Collect connected node IDs (union of dependsOn, affects, childIds, and edges) ── */ function collectConnectedIds(node, graph) { if (!node || !graph) return new Set(); const ids = new Set([ ...(node.dependsOn || []), ...(node.affects || []), ...(node.childIds || []), ]); if (node.parentId) ids.add(node.parentId); for (const edge of graph.edges || []) { if (edge.fromNodeId === node.id) ids.add(edge.toNodeId); if (edge.toNodeId === node.id) ids.add(edge.fromNodeId); } return ids; } /* ── Check whether any other unresolved unknown depends on this node ── */ function hasDownstreamUnknownDependents(node, graph, resolvedNodeIds) { if (!node || !graph) return false; const resolvedSet = new Set(resolvedNodeIds || []); const nodesById = new Map(graph.nodes.map((n) => [n.id, n])); // Check explicit dependsOn links pointing back to this node for (const otherNode of graph.nodes) { if (otherNode.id === node.id) continue; if (otherNode.kind !== "unknown") continue; if (resolvedSet.has(otherNode.id)) continue; if (otherNode.dependsOn.includes(node.id)) return true; } // Check edge links where other unknown is source and this is target for (const edge of graph.edges || []) { if (edge.toNodeId !== node.id) continue; const source = nodesById.get(edge.fromNodeId); if (!source) continue; if (source.kind !== "unknown") continue; if (resolvedSet.has(source.id)) continue; return true; } return false; } /* ── Core classification function ── */ export function assessQuestionImportance(input) { // Validate input contract const { node, graph, resolvedNodeIds = [] } = input || {}; if (!node || !graph || typeof node.kind !== "string") { return { category: "cannot_determine", reason: "missing_input" }; } const text = collectNodeText(node); const connectedIds = collectConnectedIds(node, graph); const isImportant = [ hasDownstreamUnknownDependents(node, graph, resolvedNodeIds), DECISION_PATTERNS.some((p) => p.test(text)), ].some(Boolean); if (isImportant && connectedIds.size >= 1) { return { category: "important" }; } // Rule 2 — helpful const isEvidenceText = EVIDENCE_PATTERNS.some((p) => p.test(text)); if (isEvidenceText || connectedIds.size >= 2) { return { category: "helpful" }; } // Rule 4 — cannot_determine for empty nodes if (!text.trim()) { return { category: "cannot_determine", reason: "empty_node_text" }; } // Rule 3 — default to incidental return { category: "incidental" }; }