138 lines
4.5 KiB
JavaScript
138 lines
4.5 KiB
JavaScript
/**
|
|
* 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" };
|
|
}
|