/** * Experiment 23 — Decision Condition Status Assessment. * * Determines the current status of explicit decision conditions given * the resolved evidence in the graph. A pure passive layer that reads * only existing node fields and edges. No new graph structure, no LLM * calls, no mutation. * * Classification rules (evaluated in order): * 1. cannot_determine — condition text is missing or graph is incomplete. * 2. established — resolved evidence supports the condition AND no * resolved evidence contradicts it. * 3. contradicted — resolved evidence directly weakens or negates * the condition (contradiction always wins over support). * 4. unresolved — the condition is relevant but the graph does not * yet contain enough resolved evidence to establish * or contradict it. */ /* ── Decision concept groups for generic matching ──────────── */ const CONDITION_GROUPS = { demand: { support: ["demand", "need", "interest", "customers", "audience"], contradiction: [], }, compliance: { support: ["compliance", "regulation", "legal", "required", "mandatory", "gdpr", "data residency"], contradiction: [], }, value_cost: { support: ["cost", "investment", "justif", "viability", "financial", "revenue", "budget"], contradiction: [], }, differentiation: { support: ["differentiat", "advantage", "competit", "positioning", "superior", "unique"], contradiction: [], }, }; /** Contradiction phrases — any resolved node text matching one of these weakens a condition */ const CONTRADICTION_PHRASES = [ "does not support", "cannot meet", "unreachable", "not achievable", "impossible to achieve", "no comparable", ]; /* ── Helpers ──────────────────────────────────────────────── */ function normalise(value) { return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); } function collectResolvedNodeTexts(graph) { const resolvedIds = new Set(graph?.resolvedNodeIds || []); const nodes = graph?.nodes || []; const texts = []; for (const node of nodes) { if (!resolvedIds.has(node.id)) continue; const label = normalise(node.label); const desc = normalise(node.description); if (label.length > 0) texts.push({ nodeId: node.id, kind: "label", text: label }); if (desc.length > 0) texts.push({ nodeId: node.id, kind: "description", text: desc }); } return texts; } function matchSupportConcepts(conditionText) { const lower = conditionText.toLowerCase(); const cats = []; for (const [name, group] of Object.entries(CONDITION_GROUPS)) { if (group.support.some((kw) => lower.includes(kw))) { cats.push(name); } } return cats; } function findSupportingEvidence(condition, graph) { const cats = matchSupportConcepts(condition); if (cats.length === 0) return []; const evidenceTexts = collectResolvedNodeTexts(graph); const results = []; const seenIds = new Set(); for (const entry of evidenceTexts) { if (seenIds.has(entry.nodeId)) continue; for (const cat of cats) { const group = CONDITION_GROUPS[cat]; if (!group?.support) continue; for (const kw of group.support) { if (entry.text.includes(kw)) { results.push(entry.nodeId); seenIds.add(entry.nodeId); break; } } } } return [...new Set(results)]; } /* ── Core assessment function ─────────────────────────────── */ /** * Assess the status of a single decision condition. * * @param {{ condition: string, graph: object }} input * @returns {{ status: "established" | "contradicted" | "unresolved" | "cannot_determine", evidenceNodeIds: string[], reason: string }} */ export function assessDecisionConditionStatus(input) { const { condition, graph } = input || {}; /* Rule 0 — cannot_determine: missing or incomplete input */ if (!condition || typeof condition !== "string" || normalise(condition).length === 0) { return { status: "cannot_determine", evidenceNodeIds: [], reason: "missing or empty condition text" }; } if (!graph || !Array.isArray(graph.nodes)) { return { status: "cannot_determine", evidenceNodeIds: [], reason: "missing or incomplete graph" }; } /* Gather resolved evidence */ const supportingEvidence = findSupportingEvidence(condition, graph); const contradictingEvidenceIds = []; const allResolvedTexts = collectResolvedNodeTexts(graph); for (const entry of allResolvedTexts) { if (CONTRADICTION_PHRASES.some((phrase) => entry.text.includes(phrase))) { contradictingEvidenceIds.push(entry.nodeId); } } const evidenceNodeIds = [...new Set([...supportingEvidence, ...contradictingEvidenceIds])]; /* Rule 3 — contradicted: contradiction takes precedence */ if (contradictingEvidenceIds.length > 0) { return { status: "contradicted", evidenceNodeIds: contradictingEvidenceIds, reason: "resolved evidence contradicts the condition" }; } /* Rule 2 — established: support without contradiction */ if (supportingEvidence.length > 0) { return { status: "established", evidenceNodeIds: supportingEvidence, reason: "resolved evidence supports the condition" }; } /* Rule 4 — unresolved: condition is relevant but no resolved evidence found */ return { status: "unresolved", evidenceNodeIds: [], reason: "condition is relevant but no resolved evidence establishes or contradicts it" }; }