/** * Experiment 25A — Evidence-Condition Scope Comparison. * * Determines whether a piece of evidence and a decision condition refer to * the same claim and timeframe before direction classification is applied. * * Returns one of: * "direct_match" — same subject, present state in both * "partial_match" — relevant but only addresses part of the condition * "different_timeframe" — present evidence vs future feasibility (or vice versa) * "unrelated" — different subjects entirely * "cannot_determine" — missing or unclear input * * Uses four deterministic rules. No LLM calls, no scoring, no mutation. */ /* ── Shared concept groups (subset of evidence-direction.js categories) ── */ /* ── Normalisation ─────────────────────────────────────────── */ function normalise(value) { return String(value || "").toLowerCase(); } /* ── Present-state detection (condition) ──────────────────── */ const FUTURE_FEASIBILITY_PHRASES = [ "can be achieved", "can achieve", "would require", "will achieve", "could achieve", "able to achieve", "be achieved", "is achievable", "feasible", "worth the cost", ]; function isPresentState(text) { /* Default: anything without future-feasibility markers is present-state */ return !isFutureFeasibility(text); } function isFutureFeasibility(text) { return FUTURE_FEASIBILITY_PHRASES.some((p) => text.includes(p)); } /* ── Concept family matching ──────────────────────────────── */ /** * Each concept has two keyword lists: * core — words that directly identify this concept (e.g. "demand", "compliance"). * related — words that typically co-occur with the concept in evidence text. * A category is "shared" when the condition contains a core word AND the evidence * contains either the same core word OR any related word from that concept family. */ const CONCEPT_FAMILIES = { demand: { core: ["demand", "need", "customer demand", "audience"], related: ["market exists", "valued at", "growing market", "interest", "market size", "growing"], }, compliance: { core: ["compliance", "gdpr", "regulation", "data residency"], related: ["eu compliance", "achieve compliance", "supports gdpr", "meets regulation", "supports data residency", "support eu"], }, value_cost: { core: ["cost", "investment", "viability", "financial viability"], related: ["justifies the cost", "market value", "engineering investment", "return", "worth the cost", "affordable"], }, differentiation: { core: ["competitive differentiation", "unique feature", "differentiat"], related: [ "competitive advantage", "positioning", "no direct equivalent", "unique product", "competit", "european equivalent", ], }, }; function findSharedCategories(condText, evText) { const shared = []; for (const [category, family] of Object.entries(CONCEPT_FAMILIES)) { /* Condition must contain a core word for this category */ const condMatchesCore = family.core.some((kw) => condText.includes(kw)); if (!condMatchesCore) continue; /* Evidence matches if it has either the same core word or any related word */ const evMatches = [...family.core, ...family.related].some((kw) => evText.includes(kw)); if (evMatches) { shared.push(category); } } return shared; } /* ── Core function ───────────────────────────────────────── */ /** * Assess the scope alignment between a decision condition and evidence. * * @param {{ condition: object, evidenceNode: object }} input * @returns {{ scope: "direct_match" | "partial_match" | "different_timeframe" | "unrelated" | "cannot_determine", reason: string }} */ export function assessEvidenceConditionScope({ condition, evidenceNode } = {}) { if (!condition) return { scope: "cannot_determine", reason: "missing or null condition" }; if (!evidenceNode) return { scope: "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 { scope: "cannot_determine", reason: "empty condition text" }; if (evidenceText.length === 0) return { scope: "cannot_determine", reason: "empty evidence node text" }; /* Rule 1 — Timeframe mismatch (applies across all subjects) */ const condIsPresent = isPresentState(conditionText); const condIsFuture = isFutureFeasibility(conditionText); const evIsPresent = isPresentState(evidenceText); const evIsFuture = isFutureFeasibility(evidenceText); if ((condIsPresent && evIsFuture) || (condIsFuture && evIsPresent)) { return { scope: "different_timeframe", reason: "evidence describes present state while condition concerns future feasibility" }; } /* Rule 2 — Shared category check */ const shared = findSharedCategories(conditionText, evidenceText); if (shared.length === 0) { /* Feasibility evidence without shared subject — both are feasibility-oriented */ if (condIsFuture && evIsFuture) { return { scope: "partial_match", reason: "both express future-feasibility but address different subjects" }; } if (condIsFuture || evIsFuture) { return { scope: "different_timeframe", reason: "evidence describes present state while condition concerns future feasibility" }; } return { scope: "unrelated", reason: "condition and evidence do not share a recognisable concept category" }; } /* Rule 3 — Both present-state → direct match */ if (condIsPresent && evIsPresent) { return { scope: "direct_match", reason: `both describe present state in ${shared.join(" / ")} category` }; } /* Rule 4 — Future condition with feasibility evidence → partial match */ if ((condIsFuture || evIsFuture)) { return { scope: "partial_match", reason: `evidence addresses feasibility for ${shared.join(" / ")} but does not fully answer the condition` }; } /* Fallback — cannot determine when neither present nor future detected */ return { scope: "cannot_determine", reason: "neither present-state nor future-feasibility patterns detected in both texts" }; }