fix: commit scope-aware condition status integration
This commit is contained in:
@@ -1,29 +1,38 @@
|
||||
/**
|
||||
* Experiment 23/24B — Decision Condition Status Assessment.
|
||||
* Experiment 23/24B/25B — 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.
|
||||
*
|
||||
* Uses Experiment 24A's `assessEvidenceDirection` to classify each
|
||||
* linked observation's relationship to the condition as supports /
|
||||
* contradicts / informs / cannot_determine, then applies status rules:
|
||||
* Uses Experiment 24A's assessEvidenceDirection and Experiment 25A's
|
||||
* assessEvidenceConditionScope to classify each linked observation's
|
||||
* relationship to the condition as supports / contradicts / informs /
|
||||
* cannot_determine, then applies scope-aware status rules:
|
||||
*
|
||||
* Scope-aware classification (Experiment 25B):
|
||||
* direct_match + supports → established
|
||||
* direct_match + contradicts → contradicted
|
||||
* partial_match → unresolved (even if direction = supports/contradicts)
|
||||
* different_timeframe → unresolved (evidence does not directly answer the condition)
|
||||
* unrelated → ignore for status purposes
|
||||
* cannot_determine → do not establish or contradict
|
||||
*
|
||||
* Classification rules (evaluated in order):
|
||||
* 1. cannot_determine — condition text is missing or graph is incomplete.
|
||||
* 2. established — at least one linked evidence node returns supports
|
||||
* 2. established — at least one direct-scope linked evidence node returns supports
|
||||
* AND none returns contradicts.
|
||||
* 3. contradicted — at least one linked evidence node returns contradicts
|
||||
* (contradiction always wins over support).
|
||||
* 4. unresolved — evidence only returns informs, or no usable linked
|
||||
* evidence exists.
|
||||
* 3. contradicted — at least one direct-scope linked evidence node returns contradicts
|
||||
* (contradiction always wins over support within direct scope).
|
||||
* 4. unresolved — no direct-scope evidence with directional signal, or partial/different/unrelated scope only.
|
||||
*
|
||||
* IMPORTANT: Do not mark a condition established merely because its unknown is resolved.
|
||||
* The actual evidence text from connected observations determines status.
|
||||
*/
|
||||
|
||||
import { assessEvidenceDirection } from "./evidence-direction.js";
|
||||
import { assessEvidenceConditionScope } from "./evidence-condition-scope.js";
|
||||
|
||||
/* ── Helpers ──────────────────────────────────────────────── */
|
||||
|
||||
@@ -63,6 +72,28 @@ function findLinkedObservations(graph, unknownId) {
|
||||
return observations;
|
||||
}
|
||||
|
||||
/** Determine which concept categories a condition text belongs to. */
|
||||
|
||||
function matchSupportConcepts(conditionText) {
|
||||
const lower = conditionText.toLowerCase();
|
||||
const cats = [];
|
||||
|
||||
if (lower.includes("demand") || lower.includes("need") || lower.includes("interest") || lower.includes("audience")) {
|
||||
cats.push("demand");
|
||||
}
|
||||
if (lower.includes("compliance") || lower.includes("gdpr") || lower.includes("regulation") || lower.includes("data residency")) {
|
||||
cats.push("compliance");
|
||||
}
|
||||
if (lower.includes("cost") || lower.includes("investment") || lower.includes("justif") || lower.includes("viability") || lower.includes("market value")) {
|
||||
cats.push("value_cost");
|
||||
}
|
||||
if (lower.includes("differentiat") || lower.includes("advantage") || lower.includes("competit") || lower.includes("positioning") || lower.includes("unique")) {
|
||||
cats.push("differentiation");
|
||||
}
|
||||
|
||||
return cats;
|
||||
}
|
||||
|
||||
/* ── Core assessment function ─────────────────────────────── */
|
||||
|
||||
/**
|
||||
@@ -120,7 +151,7 @@ export function assessDecisionConditionStatus(input) {
|
||||
|
||||
if (!unknownNode) {
|
||||
/* Focused tests: single node serves as both evidence and unknown.
|
||||
Accept any resolved unknown node as potential evidence target */
|
||||
Accept any resolved unknown node as potential evidence target. */
|
||||
const allResolvedUnknowns = graph.nodes.filter((n) => n.kind === "unknown" && (graph.resolvedNodeIds || []).includes(n.id));
|
||||
if (allResolvedUnknowns.length > 0) {
|
||||
unknownNode = allResolvedUnknowns[0];
|
||||
@@ -143,8 +174,7 @@ export function assessDecisionConditionStatus(input) {
|
||||
const linkedObs = findLinkedObservations(graph, unknownId);
|
||||
|
||||
if (linkedObs.length > 0) {
|
||||
/* Use evidence direction classifier for each linked observation. */
|
||||
|
||||
/* Use scope-aware evidence direction assessment (Experiment 25B). */
|
||||
return assessConditionViaEvidenceDirection(condition, firstCategory, linkedObs);
|
||||
}
|
||||
|
||||
@@ -152,84 +182,95 @@ export function assessDecisionConditionStatus(input) {
|
||||
return assessConditionViaKeywords(condition, graph, firstCategory, unknownId, linkedObs);
|
||||
}
|
||||
|
||||
/** Determine which concept categories a condition text belongs to. */
|
||||
/* ── Scope-aware direction assessment (Experiment 25B) ─── */
|
||||
|
||||
function matchSupportConcepts(conditionText) {
|
||||
const lower = conditionText.toLowerCase();
|
||||
const cats = [];
|
||||
|
||||
if (lower.includes("demand") || lower.includes("need") || lower.includes("interest") || lower.includes("audience")) {
|
||||
cats.push("demand");
|
||||
}
|
||||
if (lower.includes("compliance") || lower.includes("gdpr") || lower.includes("regulation") || lower.includes("data residency")) {
|
||||
cats.push("compliance");
|
||||
}
|
||||
if (lower.includes("cost") || lower.includes("investment") || lower.includes("justif") || lower.includes("viability") || lower.includes("market value")) {
|
||||
cats.push("value_cost");
|
||||
}
|
||||
if (lower.includes("differentiat") || lower.includes("advantage") || lower.includes("competit") || lower.includes("positioning") || lower.includes("unique")) {
|
||||
cats.push("differentiation");
|
||||
}
|
||||
|
||||
return cats;
|
||||
}
|
||||
|
||||
/* ── Evidence-direction based assessment (for graphs with edges) ─ */
|
||||
/**
|
||||
* Assess all linked observations and derive condition status considering
|
||||
* both evidence direction AND evidence-condition scope.
|
||||
*
|
||||
* Rule: only direct_match scope evidence can establish or contradict.
|
||||
* partial_match, different_timeframe, unrelated, cannot_determine leave
|
||||
* the condition unresolved even when direction points elsewhere.
|
||||
*/
|
||||
|
||||
function assessConditionViaEvidenceDirection(condition, category, linkedObs) {
|
||||
const directions = [];
|
||||
const usableDirections = [];
|
||||
const evidenceNodeIds = [];
|
||||
|
||||
for (const obs of linkedObs) {
|
||||
const text = normalise(obs.label || obs.description || "");
|
||||
if (text.length === 0) continue;
|
||||
|
||||
const result = assessEvidenceDirection({ condition: { text: condition }, evidenceNode: obs });
|
||||
directions.push(result);
|
||||
const directionResult = assessEvidenceDirection({ condition: { text: condition }, evidenceNode: obs });
|
||||
const scopeResult = assessEvidenceConditionScope({
|
||||
condition: { text: condition },
|
||||
evidenceNode: obs,
|
||||
});
|
||||
|
||||
if (result.direction !== "cannot_determine") {
|
||||
evidenceNodeIds.push(obs.id);
|
||||
} else {
|
||||
/* Record all directional signals for evidenceNodeIds. */
|
||||
|
||||
if (scopeResult.scope === "direct_match" || directionResult.direction !== "cannot_determine") {
|
||||
evidenceNodeIds.push(obs.id);
|
||||
}
|
||||
|
||||
/* Only direct_match scope contributes directional signal to status.
|
||||
partial_match, different_timeframe, unrelated, cannot_determine
|
||||
are relevant but do not directly answer the condition being assessed. */
|
||||
|
||||
if (scopeResult.scope !== "direct_match") continue;
|
||||
|
||||
if (directionResult.direction === "cannot_determine") continue;
|
||||
|
||||
usableDirections.push({ direction: directionResult.direction, evidenceId: obs.id });
|
||||
}
|
||||
|
||||
const usableDirections = directions.filter((d) => d.direction !== "cannot_determine");
|
||||
/* No direct-match evidence with a directional signal → unresolved. */
|
||||
|
||||
if (usableDirections.length === 0) {
|
||||
return { status: "unresolved", evidenceNodeIds: [], reason: `${category} linked observations provide no directional signal` };
|
||||
return { status: "unresolved", evidenceNodeIds, reason: `${category} linked observations provide no direct-scope directional signal` };
|
||||
}
|
||||
|
||||
const hasContradicts = directions.some((d) => d.direction === "contradicts");
|
||||
const hasSupports = directions.some((d) => d.direction === "supports");
|
||||
const anyInformsOrCanD = directions.some(
|
||||
(d) => d.direction === "informs" || d.direction === "cannot_determine",
|
||||
);
|
||||
const hasContradicts = usableDirections.some((d) => d.direction === "contradicts");
|
||||
const hasSupports = usableDirections.some((d) => d.direction === "supports");
|
||||
|
||||
if (hasContradicts) {
|
||||
return { status: "contradicted", evidenceNodeIds, reason: `${category} linked evidence contradicts the condition` };
|
||||
return { status: "contradicted", evidenceNodeIds, reason: `${category} direct-scope linked evidence contradicts the condition` };
|
||||
}
|
||||
|
||||
if (hasSupports && !hasContradicts) {
|
||||
return { status: "established", evidenceNodeIds, reason: `${category} linked evidence supports the condition without contradiction` };
|
||||
if (hasSupports) {
|
||||
return { status: "established", evidenceNodeIds, reason: `${category} direct-scope linked evidence supports the condition without contradiction` };
|
||||
}
|
||||
|
||||
if (anyInformsOrCanD || usableDirections.every((d) => d.direction === "informs")) {
|
||||
return { status: "unresolved", evidenceNodeIds, reason: `${category} linked evidence only provides contextual information` };
|
||||
}
|
||||
|
||||
return { status: "unresolved", evidenceNodeIds: [], reason: `${category} condition assessed but no directional signal obtained` };
|
||||
return { status: "unresolved", evidenceNodeIds, reason: `${category} linked evidence provides context only within direct scope` };
|
||||
}
|
||||
|
||||
/* ── Keyword-based assessment (fallback for tests/fixtures without edges) ─ */
|
||||
|
||||
/**
|
||||
* Fallback when no edge-linked observations exist.
|
||||
* Inspects resolved nodes using keywords, but scope-aware: only direct_match
|
||||
* nodes can establish or contradict; all other scopes leave unresolved.
|
||||
*/
|
||||
|
||||
function assessConditionViaKeywords(condition, graph, category, unknownId, linkedObs = []) {
|
||||
const allResolved = graph.nodes.filter((n) => n.status === "resolved");
|
||||
|
||||
/* Check contradiction phrases in ALL resolved evidence. */
|
||||
/* Check contradiction phrases in ALL resolved evidence — but scope-aware. */
|
||||
const CONTRADICTION_PHRASES = ["does not support", "cannot meet", "unreachable", "not achievable", "impossible to achieve", "no comparable"];
|
||||
|
||||
for (const node of allResolved) {
|
||||
const text = normalise(node.label || node.description || "");
|
||||
if (text.length === 0) continue;
|
||||
|
||||
/* Check scope before applying contradiction. */
|
||||
|
||||
const scopeResult = assessEvidenceConditionScope({
|
||||
condition: { text: condition },
|
||||
evidenceNode: node,
|
||||
});
|
||||
|
||||
if (scopeResult.scope !== "direct_match") continue;
|
||||
|
||||
if (CONTRADICTION_PHRASES.some((phrase) => text.includes(phrase))) {
|
||||
return { status: "contradicted", evidenceNodeIds: [node.id], reason: `${category} linked evidence contradicts the condition` };
|
||||
}
|
||||
@@ -248,21 +289,37 @@ function assessConditionViaKeywords(condition, graph, category, unknownId, linke
|
||||
const keywords = SUPPORT_KEYWORDS[category] || [];
|
||||
const supportingNodes = new Set();
|
||||
|
||||
/* Inspect matched unknown node label for support keywords. */
|
||||
/* Inspect matched unknown node label for support keywords (scope-aware). */
|
||||
if (unknownId) {
|
||||
const unkNode = graph.nodes.find((n) => n.id === unknownId);
|
||||
if (unkNode) {
|
||||
const text = normalise(unkNode.label || unkNode.description || "");
|
||||
if (keywords.some((kw) => text.includes(kw))) {
|
||||
supportingNodes.add(unkNode.id);
|
||||
const scopeResult = assessEvidenceConditionScope({
|
||||
condition: { text: condition },
|
||||
evidenceNode: unkNode,
|
||||
});
|
||||
|
||||
if (scopeResult.scope === "direct_match") {
|
||||
const text = normalise(unkNode.label || unkNode.description || "");
|
||||
if (keywords.some((kw) => text.includes(kw))) {
|
||||
supportingNodes.add(unkNode.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Also inspect linked observations for support keywords. */
|
||||
/* Also inspect linked observations for support keywords (scope-aware). */
|
||||
if (linkedObs && linkedObs.length > 0) {
|
||||
for (const obs of linkedObs) {
|
||||
const text = normalise(obs.label || obs.description || "");
|
||||
if (text.length === 0) continue;
|
||||
|
||||
const scopeResult = assessEvidenceConditionScope({
|
||||
condition: { text: condition },
|
||||
evidenceNode: obs,
|
||||
});
|
||||
|
||||
if (scopeResult.scope !== "direct_match") continue;
|
||||
|
||||
if (keywords.some((kw) => text.includes(kw))) {
|
||||
supportingNodes.add(obs.id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user