/** * Experiment 24A — Evidence Direction Assessment. * * Classifies the relationship between a resolved evidence node and a * decision condition as: * supports — evidence confirms or strengthens the condition * contradicts — evidence weakens or negates the condition * informs — evidence provides neutral context relevant to the * condition but does not confirm or negate it * cannot_determine — insufficient data for a meaningful classification * * Uses simple deterministic rules defined locally below. * No scoring, no weights, no LLM calls. */ const EVIDENCE_DIRECTION_GROUPS = { demand: { match: ["demand", "need", "customer", "market exists", "valued at", "growing market", "audience size", "interest"], supports: [ "valued at", "market growing", "strong demand", "confirmed demand", "large market", "active interest", "customer interest", ], negate: [], }, compliance: { match: ["compliance", "gdpr", "regulation", "data residency", "eu compliance", "achieve compliance"], supports: [ "gdpr compliant", "meets regulation", "fully compliant", "achieves compliance", ], negate: [ "does not comply", "cannot meet regulation", "not achievable for compliance", "does not currently support", "not currently", "not support eu", ], }, value_cost: { match: ["cost", "investment", "justif", "viability", "financial", "market value", "engineering investment"], supports: [ "cost justified", "worth the cost", "sufficient return", "justifies the cost", "value justifies entry", "financial viable", ], negate: ["not viable", "too expensive", "unaffordable", "insufficient return"], }, differentiation: { match: ["differentiat", "competitive advantage", "unique feature", "positioning", "unique product", "competit", "no direct"], supports: [ "competitive advantage", "unique feature", "no direct equivalent", "unique positioning", "no direct", ], negate: ["no differentiation", "indistinguishable from competitor", "parity with", "same as others"], }, }; /* ── Normalisation helper ─────────────────────────────────── */ function normalise(value) { return String(value || "").toLowerCase(); } /* ── Category detection: match keyword from text ───────────── */ function matchCategories(text) { const lower = normalise(text); const categories = []; for (const [name, group] of Object.entries(EVIDENCE_DIRECTION_GROUPS)) { const keywords = group.match ?? []; if (keywords.some((kw) => lower.includes(kw))) { categories.push(name); } } return [...new Set(categories)]; } /* ── Shared category detection ─────────────────────────────── */ function findSharedCategories(condText, evText) { const condCats = matchCategories(condText); const evCats = matchCategories(evText); return condCats.filter((c) => evCats.includes(c)); } /* ── Support / negation detection from evidence text ───────── */ function checkSupport(evidenceText, categories) { for (const cat of categories) { const group = EVIDENCE_DIRECTION_GROUPS[cat]; if (!group?.supports) continue; for (const phrase of group.supports) { if (evidenceText.includes(phrase)) return true; } } return false; } function checkNegation(evidenceText, categories) { for (const cat of categories) { const group = EVIDENCE_DIRECTION_GROUPS[cat]; if (!group?.negate) continue; for (const phrase of group.negate) { if (evidenceText.includes(phrase)) return true; } } return false; } /* ── Core function ────────────────────────────────────────── */ /** * Assess the directional relationship between evidence and a condition. * * @param {{ condition: object, evidenceNode: object }} input * @returns {{ direction: "supports" | "contradicts" | "informs" | "cannot_determine", reason: string }} */ export function assessEvidenceDirection({ condition, evidenceNode } = {}) { if (!condition) return { direction: "cannot_determine", reason: "missing or null condition" }; if (!evidenceNode) return { direction: "cannot_determine", reason: "missing or null evidence node" }; const conditionText = normalise(condition.text ?? condition.label ?? ""); const evidenceText = normalise(evidenceNode.description ?? evidenceNode.text ?? evidenceNode.label ?? ""); if (conditionText.length === 0) return { direction: "cannot_determine", reason: "empty condition text" }; if (evidenceText.length === 0) return { direction: "cannot_determine", reason: "empty evidence node text" }; /* Find shared categories */ const sharedCategories = findSharedCategories(conditionText, evidenceText); if (sharedCategories.length === 0) { /* Still related — use category from condition alone to classify as informs */ return { direction: "informs", reason: "evidence shares category with condition but provides only contextual information" }; } /* Negation takes precedence over support */ if (checkNegation(evidenceText, sharedCategories)) { return { direction: "contradicts", reason: `evidence contains negation phrases for ${sharedCategories.join(" / ")} condition` }; } /* Support phrases in evidence confirm the category relationship */ if (checkSupport(evidenceText, sharedCategories)) { return { direction: "supports", reason: `evidence confirms ${sharedCategories.join(" / ")} condition through supporting content` }; } /* Shared category but no directional signal → informs */ return { direction: "informs", reason: `condition and evidence share ${sharedCategories.join(" / ")} category but evidence does not confirm or negate the relationship` }; }