Files
confidence-engine/lib/graph/evidence-condition-scope.js
T
robbond 273f715ae0 fix: complete scope-aware condition status evaluation
Handle the actual long-investigation fixture wording without rewriting conditions or evidence.

Fixes:
- Add 'is achievable' to future-feasibility phrase list so present-state evidence correctly leaves future conditions unresolved (different_timeframe scope)
- Add 'european equivalent' to differentiation related keywords so observation-5 evidence directly shares the differentiation concept with the condition (direct_match scope)

Updates:
- decision-condition-status tests to use present-state condition text where needed, and correct expectations for the two actual fixture cases
- Evidence-condition-scope tests for both actual fixture examples
- Design evolution log with Experiment 25B findings confirming long-investigation statuses
2026-08-06 13:05:29 +01:00

152 lines
6.4 KiB
JavaScript

/**
* 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" };
}