experiment: add passive question importance test
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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" };
|
||||
}
|
||||
Reference in New Issue
Block a user