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
|
* Determines the current status of explicit decision conditions given
|
||||||
* the resolved evidence in the graph. A pure passive layer that reads
|
* the resolved evidence in the graph. A pure passive layer that reads
|
||||||
* only existing node fields and edges. No new graph structure, no LLM
|
* only existing node fields and edges. No new graph structure, no LLM
|
||||||
* calls, no mutation.
|
* calls, no mutation.
|
||||||
*
|
*
|
||||||
* Uses Experiment 24A's `assessEvidenceDirection` to classify each
|
* Uses Experiment 24A's assessEvidenceDirection and Experiment 25A's
|
||||||
* linked observation's relationship to the condition as supports /
|
* assessEvidenceConditionScope to classify each linked observation's
|
||||||
* contradicts / informs / cannot_determine, then applies status rules:
|
* 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):
|
* Classification rules (evaluated in order):
|
||||||
* 1. cannot_determine — condition text is missing or graph is incomplete.
|
* 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.
|
* AND none returns contradicts.
|
||||||
* 3. contradicted — at least one linked evidence node returns contradicts
|
* 3. contradicted — at least one direct-scope linked evidence node returns contradicts
|
||||||
* (contradiction always wins over support).
|
* (contradiction always wins over support within direct scope).
|
||||||
* 4. unresolved — evidence only returns informs, or no usable linked
|
* 4. unresolved — no direct-scope evidence with directional signal, or partial/different/unrelated scope only.
|
||||||
* evidence exists.
|
|
||||||
*
|
*
|
||||||
* IMPORTANT: Do not mark a condition established merely because its unknown is resolved.
|
* IMPORTANT: Do not mark a condition established merely because its unknown is resolved.
|
||||||
* The actual evidence text from connected observations determines status.
|
* The actual evidence text from connected observations determines status.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assessEvidenceDirection } from "./evidence-direction.js";
|
import { assessEvidenceDirection } from "./evidence-direction.js";
|
||||||
|
import { assessEvidenceConditionScope } from "./evidence-condition-scope.js";
|
||||||
|
|
||||||
/* ── Helpers ──────────────────────────────────────────────── */
|
/* ── Helpers ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -63,6 +72,28 @@ function findLinkedObservations(graph, unknownId) {
|
|||||||
return observations;
|
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 ─────────────────────────────── */
|
/* ── Core assessment function ─────────────────────────────── */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,7 +151,7 @@ export function assessDecisionConditionStatus(input) {
|
|||||||
|
|
||||||
if (!unknownNode) {
|
if (!unknownNode) {
|
||||||
/* Focused tests: single node serves as both evidence and unknown.
|
/* 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));
|
const allResolvedUnknowns = graph.nodes.filter((n) => n.kind === "unknown" && (graph.resolvedNodeIds || []).includes(n.id));
|
||||||
if (allResolvedUnknowns.length > 0) {
|
if (allResolvedUnknowns.length > 0) {
|
||||||
unknownNode = allResolvedUnknowns[0];
|
unknownNode = allResolvedUnknowns[0];
|
||||||
@@ -143,8 +174,7 @@ export function assessDecisionConditionStatus(input) {
|
|||||||
const linkedObs = findLinkedObservations(graph, unknownId);
|
const linkedObs = findLinkedObservations(graph, unknownId);
|
||||||
|
|
||||||
if (linkedObs.length > 0) {
|
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);
|
return assessConditionViaEvidenceDirection(condition, firstCategory, linkedObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,84 +182,95 @@ export function assessDecisionConditionStatus(input) {
|
|||||||
return assessConditionViaKeywords(condition, graph, firstCategory, unknownId, linkedObs);
|
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();
|
* Assess all linked observations and derive condition status considering
|
||||||
const cats = [];
|
* both evidence direction AND evidence-condition scope.
|
||||||
|
*
|
||||||
if (lower.includes("demand") || lower.includes("need") || lower.includes("interest") || lower.includes("audience")) {
|
* Rule: only direct_match scope evidence can establish or contradict.
|
||||||
cats.push("demand");
|
* partial_match, different_timeframe, unrelated, cannot_determine leave
|
||||||
}
|
* the condition unresolved even when direction points elsewhere.
|
||||||
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) ─ */
|
|
||||||
|
|
||||||
function assessConditionViaEvidenceDirection(condition, category, linkedObs) {
|
function assessConditionViaEvidenceDirection(condition, category, linkedObs) {
|
||||||
const directions = [];
|
const usableDirections = [];
|
||||||
const evidenceNodeIds = [];
|
const evidenceNodeIds = [];
|
||||||
|
|
||||||
for (const obs of linkedObs) {
|
for (const obs of linkedObs) {
|
||||||
const text = normalise(obs.label || obs.description || "");
|
const text = normalise(obs.label || obs.description || "");
|
||||||
if (text.length === 0) continue;
|
if (text.length === 0) continue;
|
||||||
|
|
||||||
const result = assessEvidenceDirection({ condition: { text: condition }, evidenceNode: obs });
|
const directionResult = assessEvidenceDirection({ condition: { text: condition }, evidenceNode: obs });
|
||||||
directions.push(result);
|
const scopeResult = assessEvidenceConditionScope({
|
||||||
|
condition: { text: condition },
|
||||||
|
evidenceNode: obs,
|
||||||
|
});
|
||||||
|
|
||||||
if (result.direction !== "cannot_determine") {
|
/* Record all directional signals for evidenceNodeIds. */
|
||||||
evidenceNodeIds.push(obs.id);
|
|
||||||
} else {
|
if (scopeResult.scope === "direct_match" || directionResult.direction !== "cannot_determine") {
|
||||||
evidenceNodeIds.push(obs.id);
|
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) {
|
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 hasContradicts = usableDirections.some((d) => d.direction === "contradicts");
|
||||||
const hasSupports = directions.some((d) => d.direction === "supports");
|
const hasSupports = usableDirections.some((d) => d.direction === "supports");
|
||||||
const anyInformsOrCanD = directions.some(
|
|
||||||
(d) => d.direction === "informs" || d.direction === "cannot_determine",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasContradicts) {
|
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) {
|
if (hasSupports) {
|
||||||
return { status: "established", evidenceNodeIds, reason: `${category} linked evidence supports the condition without contradiction` };
|
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 provides context only within direct scope` };
|
||||||
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` };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Keyword-based assessment (fallback for tests/fixtures without edges) ─ */
|
/* ── 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 = []) {
|
function assessConditionViaKeywords(condition, graph, category, unknownId, linkedObs = []) {
|
||||||
const allResolved = graph.nodes.filter((n) => n.status === "resolved");
|
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"];
|
const CONTRADICTION_PHRASES = ["does not support", "cannot meet", "unreachable", "not achievable", "impossible to achieve", "no comparable"];
|
||||||
|
|
||||||
for (const node of allResolved) {
|
for (const node of allResolved) {
|
||||||
const text = normalise(node.label || node.description || "");
|
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))) {
|
if (CONTRADICTION_PHRASES.some((phrase) => text.includes(phrase))) {
|
||||||
return { status: "contradicted", evidenceNodeIds: [node.id], reason: `${category} linked evidence contradicts the condition` };
|
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 keywords = SUPPORT_KEYWORDS[category] || [];
|
||||||
const supportingNodes = new Set();
|
const supportingNodes = new Set();
|
||||||
|
|
||||||
/* Inspect matched unknown node label for support keywords. */
|
/* Inspect matched unknown node label for support keywords (scope-aware). */
|
||||||
if (unknownId) {
|
if (unknownId) {
|
||||||
const unkNode = graph.nodes.find((n) => n.id === unknownId);
|
const unkNode = graph.nodes.find((n) => n.id === unknownId);
|
||||||
if (unkNode) {
|
if (unkNode) {
|
||||||
const text = normalise(unkNode.label || unkNode.description || "");
|
const scopeResult = assessEvidenceConditionScope({
|
||||||
if (keywords.some((kw) => text.includes(kw))) {
|
condition: { text: condition },
|
||||||
supportingNodes.add(unkNode.id);
|
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) {
|
if (linkedObs && linkedObs.length > 0) {
|
||||||
for (const obs of linkedObs) {
|
for (const obs of linkedObs) {
|
||||||
const text = normalise(obs.label || obs.description || "");
|
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))) {
|
if (keywords.some((kw) => text.includes(kw))) {
|
||||||
supportingNodes.add(obs.id);
|
supportingNodes.add(obs.id);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user