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
152 lines
6.4 KiB
JavaScript
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" };
|
|
}
|