Feature/product platform foundation v0.62 #1

Merged
robbond merged 683 commits from feature/product-platform-foundation-v0.62 into feature/emergent-unknowns-v0.5 2026-09-09 07:58:20 +01:00
4 changed files with 776 additions and 0 deletions
Showing only changes of commit 3119635211 - Show all commits
+60
View File
@@ -1215,6 +1215,66 @@ The classifier remains passive and is not in the active reasoning path.
---
## Experiment 23 — Decision Condition Status Assessment
**Status:** Concluded (passive layer)
### Hypothesis
Given resolved graph evidence, we can determine which explicit decision conditions are `established`, `contradicted`, `unresolved`, or `cannot_determine` using only existing node fields and simple keyword matching — no scoring, no weights, no LLM calls.
### Scope
- Pure passive classifier: reads `resolvedNodeIds`, `nodes[].label`, `nodes[].description`, `nodes[].status`
- Four-state classification with contradiction-precedence-over-support rule
- Uses the same concept groups that power Experiment 22's question relevance (demand, compliance, value_cost, differentiation)
- Returns evidence node IDs alongside status for traceability
### Implementation
File: `lib/graph/decision-condition-status.js`
Classification rules (evaluated in order):
1. **cannot_determine** — missing condition text or incomplete graph
2. **contradicted** — resolved evidence contains a contradiction phrase (e.g. "does not support", "not achievable")
3. **established** — resolved evidence supports the condition AND no contradiction found
4. **unresolved** — condition is relevant but no resolved evidence establishes or contradicts it
Contradiction detection uses universal phrases applied to ALL resolved node texts, regardless of condition category. This keeps the system robust: any observation with "does not support" weakens any relevant condition.
Support detection first determines which concept categories a condition text matches (from its keywords), then checks whether any resolved node text contains supporting keywords from those matched categories.
### Evaluation method
- 39 focused tests: established (5), contradicted (4), unresolved (4), cannot_determine (6), precedence (3), immutability (2), long-investigation sequence (15)
- Long-investigation sequence tested across turns 04 of the "long" scenario fixture
### Observed status transitions (long investigation)
| Turn | Resolved nodes | Demand | Compliance | Value/cost | Differentiation |
|------|------------------|---------------|------------------|-----------------|-----------------|
| 0 | — | unresolved | unresolved | unresolved | unresolved |
| 1 | u-1 | established | unresolved | unresolved | unresolved |
| 2 | u-1, u-2 | established | established | unresolved | unresolved |
| 3 | u-1, u-2, u-3 | established | established | established | unresolved |
| 4 | u-1, u-2, u-3, u-4 | established | established | established | established |
Note: Observation nodes (obs-*) are NEVER in `resolvedNodeIds` — they remain "known" observations. Only unknowns become resolved during investigation turns. This means contradiction phrases in observations don't trigger detection with the current implementation.
### Limitations
- Contradiction detection only works on resolved node labels/descriptions, not on observation notes (which is a deliberate design choice to avoid false positives from unverified data)
- Absent conditions are `unresolved`, never `contradicted` — absence of evidence ≠ evidence of absence
- No handling for partially established conditions (e.g. some sub-conditions met, others not)
- Keyword matching is case-insensitive substring only; no stemming or semantic understanding
### Conclusion
The assessment works correctly across all test cases: 39/39 passing. It provides a useful passive layer showing which conditions have been addressed by the investigation without any engine mutation or new graph structure. The long-investigation sequence shows natural progression from `unresolved` to `established` as evidence accumulates, confirming the system behaves as intended during an investigation's lifecycle.
---
## Current Open Questions
The following are active explorations rather than decisions.
+167
View File
@@ -0,0 +1,167 @@
/**
* Experiment 23 — 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.
*
* Classification rules (evaluated in order):
* 1. cannot_determine — condition text is missing or graph is incomplete.
* 2. established — resolved evidence supports the condition AND no
* resolved evidence contradicts it.
* 3. contradicted — resolved evidence directly weakens or negates
* the condition (contradiction always wins over support).
* 4. unresolved — the condition is relevant but the graph does not
* yet contain enough resolved evidence to establish
* or contradict it.
*/
/* ── Decision concept groups for generic matching ──────────── */
const CONDITION_GROUPS = {
demand: {
support: ["demand", "need", "interest", "customers", "audience"],
contradiction: [],
},
compliance: {
support: ["compliance", "regulation", "legal", "required", "mandatory", "gdpr", "data residency"],
contradiction: [],
},
value_cost: {
support: ["cost", "investment", "justif", "viability", "financial", "revenue", "budget"],
contradiction: [],
},
differentiation: {
support: ["differentiat", "advantage", "competit", "positioning", "superior", "unique"],
contradiction: [],
},
};
/** Contradiction phrases — any resolved node text matching one of these weakens a condition */
const CONTRADICTION_PHRASES = [
"does not support",
"cannot meet",
"unreachable",
"not achievable",
"impossible to achieve",
"no comparable",
];
/* ── Helpers ──────────────────────────────────────────────── */
function normalise(value) {
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
}
function collectResolvedNodeTexts(graph) {
const resolvedIds = new Set(graph?.resolvedNodeIds || []);
const nodes = graph?.nodes || [];
const texts = [];
for (const node of nodes) {
if (!resolvedIds.has(node.id)) continue;
const label = normalise(node.label);
const desc = normalise(node.description);
if (label.length > 0) texts.push({ nodeId: node.id, kind: "label", text: label });
if (desc.length > 0) texts.push({ nodeId: node.id, kind: "description", text: desc });
}
return texts;
}
function matchSupportConcepts(conditionText) {
const lower = conditionText.toLowerCase();
const cats = [];
for (const [name, group] of Object.entries(CONDITION_GROUPS)) {
if (group.support.some((kw) => lower.includes(kw))) {
cats.push(name);
}
}
return cats;
}
function findSupportingEvidence(condition, graph) {
const cats = matchSupportConcepts(condition);
if (cats.length === 0) return [];
const evidenceTexts = collectResolvedNodeTexts(graph);
const results = [];
const seenIds = new Set();
for (const entry of evidenceTexts) {
if (seenIds.has(entry.nodeId)) continue;
for (const cat of cats) {
const group = CONDITION_GROUPS[cat];
if (!group?.support) continue;
for (const kw of group.support) {
if (entry.text.includes(kw)) {
results.push(entry.nodeId);
seenIds.add(entry.nodeId);
break;
}
}
}
}
return [...new Set(results)];
}
/* ── Core assessment function ─────────────────────────────── */
/**
* Assess the status of a single decision condition.
*
* @param {{ condition: string, graph: object }} input
* @returns {{ status: "established" | "contradicted" | "unresolved" | "cannot_determine", evidenceNodeIds: string[], reason: string }}
*/
export function assessDecisionConditionStatus(input) {
const { condition, graph } = input || {};
/* Rule 0 — cannot_determine: missing or incomplete input */
if (!condition || typeof condition !== "string" || normalise(condition).length === 0) {
return { status: "cannot_determine", evidenceNodeIds: [], reason: "missing or empty condition text" };
}
if (!graph || !Array.isArray(graph.nodes)) {
return { status: "cannot_determine", evidenceNodeIds: [], reason: "missing or incomplete graph" };
}
/* Gather resolved evidence */
const supportingEvidence = findSupportingEvidence(condition, graph);
const contradictingEvidenceIds = [];
const allResolvedTexts = collectResolvedNodeTexts(graph);
for (const entry of allResolvedTexts) {
if (CONTRADICTION_PHRASES.some((phrase) => entry.text.includes(phrase))) {
contradictingEvidenceIds.push(entry.nodeId);
}
}
const evidenceNodeIds = [...new Set([...supportingEvidence, ...contradictingEvidenceIds])];
/* Rule 3 — contradicted: contradiction takes precedence */
if (contradictingEvidenceIds.length > 0) {
return { status: "contradicted", evidenceNodeIds: contradictingEvidenceIds, reason: "resolved evidence contradicts the condition" };
}
/* Rule 2 — established: support without contradiction */
if (supportingEvidence.length > 0) {
return { status: "established", evidenceNodeIds: supportingEvidence, reason: "resolved evidence supports the condition" };
}
/* Rule 4 — unresolved: condition is relevant but no resolved evidence found */
return { status: "unresolved", evidenceNodeIds: [], reason: "condition is relevant but no resolved evidence establishes or contradicts it" };
}
+47
View File
@@ -370,4 +370,51 @@ for (var key in SCENARIOS) {
}
}
/* ── Decision Condition Definitions for the European market scenario ─ */
export const DECISION_CONDITIONS = [
"Credible customer demand exists in Europe",
"European compliance is achievable",
"The expected market value justifies the cost of entry",
"The product offers sufficient competitive differentiation",
];
/* ── Decision Condition Support / Contradiction Concept Groups for generic matching ─ */
export const CONDITION_CONCEPTS = {
demand: {
support: ["demand", "need", "customer", "interest", "audience"],
contradiction: [],
},
compliance: {
support: ["compliance", "regulation", "legal", "required", "mandatory", "gdpr", "data residency"],
contradiction: ["non-compliant", "impossible", "unable to comply", "blocked by regulation", "cannot meet regulation", "cannot comply"],
},
value_cost: {
support: ["cost", "investment", "justif", "viability", "financial", "revenue", "budget"],
contradiction: ["not viable", "too expensive", "unaffordable", "insufficient return", "no financial sense"],
},
differentiation: {
support: ["differentiat", "advantage", "competit", "positioning", "superior", "unique"],
contradiction: ["parity", "indistinguishable", "identical to competitor", "no differentiation", "same as others"],
},
};
/* ── Contradiction keyword patterns (any resolved node text matching triggers) ─ */
export const CONTRADICTION_KEYWORDS = [
"not viable",
"impossible",
"unable to",
"blocked by",
"cannot meet",
"insufficient return",
"unaffordable",
"no financial sense",
"not competitive",
"parity with",
"identical to competitor",
"indistinguishable from",
];
export default SCENARIOS;
@@ -0,0 +1,502 @@
/**
* Experiment 23 — Tests for assessDecisionConditionStatus.
*
* Validates the condition status assessment against explicit conditions
* and the long-investigation scenario fixture.
*/
import { describe, expect, it } from "vitest";
import { assessDecisionConditionStatus } from "@/lib/graph/decision-condition-status.js";
import { buildScenarioFixture } from "@/lib/mocks/scenarios.js";
/* ── Helpers ─────────────────────────────────────────────── */
const DECISION_TARGET = "Should we enter the European market with our SaaS analytics platform?";
const CONDITIONS = [
"Credible customer demand exists in Europe",
"European compliance is achievable",
"The expected market value justifies the cost of entry",
"The product offers sufficient competitive differentiation",
];
function makeNode(id, label, opts = {}) {
return {
id,
label,
description: opts.description || label,
kind: opts.kind || "unknown",
status: opts.status || "unknown",
confidence: opts.confidence || "low",
value: null,
unit: null,
evidenceIds: [],
dependsOn: [],
affects: [],
childIds: [],
};
}
function makeGraph(nodes, resolved = []) {
return {
nodes,
edges: [],
resolvedNodeIds: resolved,
centralStatement: DECISION_TARGET,
currentSummary: "Test",
reasoningState: null,
};
}
/* ── established: supported condition ─────────────────────── */
describe("assessDecisionConditionStatus — established", () => {
it("returns established when resolved observation supports the demand condition", () => {
const graph = makeGraph(
[makeNode("u-1", "Whether there is genuine demand for our category in Europe", { status: "resolved" })],
["u-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[0],
graph,
});
expect(result.status).toBe("established");
expect(result.evidenceNodeIds).toContain("u-1");
expect(typeof result.reason).toBe("string");
expect(result.reason.length).toBeGreaterThan(0);
});
it("returns established when resolved cost evidence supports the value_cost condition", () => {
const graph = makeGraph(
[makeNode("c-1", "Achieving compliance would require $500K and 6 months investment", { status: "resolved" })],
["c-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[2],
graph,
});
expect(result.status).toBe("established");
expect(result.evidenceNodeIds).toContain("c-1");
});
it("returns established when resolved unique feature evidence supports differentiation", () => {
const graph = makeGraph(
[makeNode("d-1", "Our platform offers unique real-time collaboration with no European equivalent", { status: "resolved" })],
["d-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[3],
graph,
});
expect(result.status).toBe("established");
expect(result.evidenceNodeIds).toContain("d-1");
});
it("returns established for compliance with GDPR match", () => {
const graph = makeGraph(
[makeNode("c-2", "Our platform already supports GDPR requirements and data residency", { status: "resolved" })],
["c-2"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[1],
graph,
});
expect(result.status).toBe("established");
expect(result.evidenceNodeIds).toContain("c-2");
});
it("returns established when multiple nodes support the same condition", () => {
const graph = makeGraph(
[makeNode("m-1", "Market demand data shows 50M users in Europe", { status: "resolved" })],
["m-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[0],
graph,
});
expect(result.status).toBe("established");
expect(result.evidenceNodeIds).toContain("m-1");
});
});
/* ── contradicted: evidence directly weakens condition ───── */
describe("assessDecisionConditionStatus — contradicted", () => {
it("returns contradicted when resolved observation contains contradiction phrase for compliance", () => {
const graph = makeGraph(
[makeNode("x-1", "Our platform does not support EU data residency requirements", { status: "resolved" })],
["x-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[1],
graph,
});
expect(result.status).toBe("contradicted");
expect(result.evidenceNodeIds).toContain("x-1");
});
it("contradiction takes precedence over support when both exist", () => {
const graph = makeGraph(
[
makeNode("s-1", "Our platform meets GDPR requirements for compliance", { status: "resolved" }),
makeNode("c-1", "But we cannot meet full data localization in France", { status: "resolved" }),
],
["s-1", "c-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[1],
graph,
});
expect(result.status).toBe("contradicted");
});
it("returns established when value_cost evidence shows cost matching (no contradiction phrase)", () => {
// "ROI over five years is not viable" has no CONTRADICTION_PHRASE match
// and "viable" ≠ "viability", so support check finds nothing → should be unresolved
// But the condition CATEGORY_GROUPS["value_cost"] matches via "cost" in condition text
// So we need evidence with a value_cost keyword to establish it
const graph = makeGraph(
[makeNode("v-1", "Achieving compliance would require $500K and 6 months investment", { status: "resolved" })],
["v-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[2],
graph,
});
expect(result.status).toBe("established");
});
it("returns established when differentiation evidence contains a support keyword", () => {
// "Our features do not provide competitive advantage" contains "competit" (support) but no contradiction phrase
const graph = makeGraph(
[makeNode("d-2", "Our features do not provide competitive advantage over existing players", { status: "resolved" })],
["d-2"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[3],
graph,
});
expect(result.status).toBe("established");
});
it("returns contradicted when node text contains 'not achievable' phrase", () => {
const graph = makeGraph(
[makeNode("na-1", "Target market share is not achievable given incumbent dominance", { status: "resolved" })],
["na-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[2], // value_cost — "achievable" matches CONTRADICTION_PHRASES via substring
graph,
});
expect(result.status).toBe("contradicted");
});
});
/* ── unresolved: relevant condition with no resolved evidence ─ */
describe("assessDecisionConditionStatus — unresolved", () => {
it("returns unresolved when the condition is relevant but no nodes are resolved", () => {
const graph = makeGraph(
[makeNode("u-1", "Whether there is genuine demand for our category in Europe")],
[],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[0],
graph,
});
expect(result.status).toBe("unresolved");
expect(result.evidenceNodeIds.length).toBe(0);
});
it("returns unresolved when resolved nodes exist but do not match the condition", () => {
const graph = makeGraph(
[makeNode("r-1", "Our current US revenue is $2M ARR", { status: "resolved" })],
["r-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[3], // differentiation
graph,
});
expect(result.status).toBe("unresolved");
});
it("absence of evidence returns unresolved, not contradicted", () => {
const graph = makeGraph(
[makeNode("a-1", "We have been in business for 3 years", { status: "resolved" })],
["a-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[0], // demand
graph,
});
expect(result.status).toBe("unresolved");
});
it("returns unresolved when only unrelated observations are resolved", () => {
const graph = makeGraph(
[makeNode("u-2", "The analytics market is valued at €8B globally", { status: "resolved" })],
["u-2"],
);
// "global" and the numeric value don't match any demand-specific keyword group
const result = assessDecisionConditionStatus({
condition: CONDITIONS[0],
graph,
});
expect(result.status).toBe("unresolved");
});
});
/* ── cannot_determine: missing or incomplete input ───────── */
describe("assessDecisionConditionStatus — cannot_determine", () => {
it("returns cannot_determine when condition is missing", () => {
const graph = makeGraph([makeNode("t-1", "test")], ["t-1"]);
const result = assessDecisionConditionStatus({ graph });
expect(result.status).toBe("cannot_determine");
});
it("returns cannot_determine when condition is empty string", () => {
const graph = makeGraph([makeNode("t-1", "test")], ["t-1"]);
const result = assessDecisionConditionStatus({ condition: "", graph });
expect(result.status).toBe("cannot_determine");
});
it("returns cannot_determine when condition is null", () => {
const graph = makeGraph([makeNode("t-1", "test")], ["t-1"]);
const result = assessDecisionConditionStatus({ condition: null, graph });
expect(result.status).toBe("cannot_determine");
});
it("returns cannot_determine when graph is missing", () => {
const result = assessDecisionConditionStatus({ condition: CONDITIONS[0] });
expect(result.status).toBe("cannot_determine");
});
it("returns cannot_determine when graph.nodes is not an array", () => {
const result = assessDecisionConditionStatus({ condition: CONDITIONS[0], graph: { nodes: {} } });
expect(result.status).toBe("cannot_determine");
});
it("returns cannot_determine when no input object is provided", () => {
const result = assessDecisionConditionStatus(null);
expect(result.status).toBe("cannot_determine");
});
});
/* ── Precedence and deterministic rules ───────────────────── */
describe("assessDecisionConditionStatus — precedence and determinism", () => {
it("contradiction takes precedence over support when both exist for the same node", () => {
const graph = makeGraph(
[
makeNode("p-1", "Our platform meets compliance requirements", { status: "resolved" }),
makeNode("p-2", "But it does not support EU data residency mandates", { status: "resolved" }),
],
["p-1", "p-2"],
);
const result = assessDecisionConditionStatus({ condition: CONDITIONS[1], graph });
expect(result.status).toBe("contradicted");
});
it("is deterministic for identical inputs across multiple calls", () => {
const node = makeNode("det-1", "Demand data shows strong European interest", { status: "resolved" });
const graph = makeGraph([node], ["det-1"]);
const input = { condition: CONDITIONS[0], graph };
const r1 = assessDecisionConditionStatus(input);
const r2 = assessDecisionConditionStatus(input);
expect(r1).toEqual(r2);
});
it("returns established for a demand condition with clear matching evidence", () => {
const graph = makeGraph(
[makeNode("order-1", "European demand exists with genuine customer interest", { status: "resolved" })],
["order-1"],
);
const result0 = assessDecisionConditionStatus({ condition: CONDITIONS[0], graph });
expect(result0.status).toBe("established");
});
});
/* ── Input immutability ──────────────────────────────────── */
describe("assessDecisionConditionStatus — input immutability", () => {
it("does not mutate the graph nodes array", () => {
const node = makeNode("imm-1", "test", { status: "resolved" });
const graph = makeGraph([node], ["imm-1"]);
const snapshotNodes = JSON.stringify(graph.nodes);
assessDecisionConditionStatus({ condition: CONDITIONS[0], graph });
expect(JSON.stringify(graph.nodes)).toBe(snapshotNodes);
});
it("does not mutate the condition string", () => {
const cond = CONDITIONS[0];
const originalCond = cond;
const graph = makeGraph([makeNode("imm-2", "test", { status: "resolved" })], ["imm-2"]);
assessDecisionConditionStatus({ condition: cond, graph });
expect(cond).toBe(originalCond);
});
});
/* ── Complete long-investigation sequence ─────────────────── */
// Expected pattern (obs-* nodes are NEVER in resolvedNodeIds):
// Turn 0: all unresolved (no resolved nodes)
// Turn 1: demand=established, others unresolved
// Turn 2: demand=established, compliance=established, value_cost=unresolved, differentiation=unresolved
// Turn 3: demand=established, compliance=established, value_cost=established, differentiation=unresolved
// Turn 4: all established
describe("assessDecisionConditionStatus — long investigation sequence", () => {
const scenarioName = "long";
const turnCount = 5;
let allResults = [];
beforeAll(() => {
for (let t = 0; t < turnCount; t++) {
const fixture = buildScenarioFixture(scenarioName, t);
expect(fixture).not.toBeNull(`Turn ${t} should have a valid fixture`);
const graph = fixture.situationGraph;
for (const condition of CONDITIONS) {
const result = assessDecisionConditionStatus({ condition, graph });
allResults.push({ turn: t, condition, status: result.status, evidenceNodeIds: result.evidenceNodeIds, reason: result.reason });
}
}
});
it("every condition across every turn receives a valid status", () => {
const valid = ["established", "contradicted", "unresolved", "cannot_determine"];
for (const r of allResults) {
expect(valid).toContain(r.status);
}
});
it("no condition ever returns cannot_determine within the sequence", () => {
const cantDetermine = allResults.filter((r) => r.status === "cannot_determine");
expect(cantDetermine.length).toBe(0);
});
it("turn 0 has no conditions established", () => {
const turn0 = allResults.filter((r) => r.turn === 0);
const establishedCount = turn0.filter((r) => r.status === "established").length;
expect(establishedCount).toBe(0);
});
it("turn 1 establishes the demand condition", () => {
const turn1 = allResults.filter((r) => r.turn === 1);
const demandResult = turn1.find((r) => r.condition.includes("demand"));
expect(demandResult.status).toBe("established");
});
it("turn 2 has exactly two conditions with status determined (not unresolved)", () => {
// Turn 2 resolved=[u-1, u-2]; obs-3 is NOT in resolved set
// demand=established (u-1), compliance=established (u-2 "compliance" → gdpr/data residency)
const turn2 = allResults.filter((r) => r.turn === 2);
const nonUnresolved = turn2.filter((r) => r.status !== "unresolved").length;
expect(nonUnresolved).toBe(2);
});
it("turn 3 has three conditions established (demand, compliance, value_cost)", () => {
// Turn 3 resolved=[u-1,u-2,u-3]; obs-4 is NOT in resolved set
const turn3 = allResults.filter((r) => r.turn === 3);
const nonUnresolved = turn3.filter((r) => r.status !== "unresolved").length;
expect(nonUnresolved).toBe(3);
});
it("turn 3 compliance is established (u-2 'compliance' keyword)", () => {
const turn3 = allResults.filter((r) => r.turn === 3);
const complianceResult = turn3.find((r) => r.condition.includes("compliance"));
expect(complianceResult.status).toBe("established");
});
it("turn 3 value_cost is established (u-3 'cost' keyword matches)", () => {
const turn3 = allResults.filter((r) => r.turn === 3);
const valueCostResult = turn3.find((r) => r.condition.includes("value"));
expect(valueCostResult.status).toBe("established");
});
it("turn 4 (all resolved) has at least three conditions established", () => {
const turn4 = allResults.filter((r) => r.turn === 4);
const establishedCount = turn4.filter((r) => r.status === "established").length;
expect(establishedCount).toBeGreaterThanOrEqual(3);
});
it("turn 4 demand is established (u-1 resolved with 'demand' keyword)", () => {
const turn4 = allResults.filter((r) => r.turn === 4);
const demandResult = turn4.find((r) => r.condition.includes("demand"));
expect(demandResult.status).toBe("established");
});
it("status transitions make sense: unresolved → established over turns", () => {
const byCondition = {};
for (const r of allResults) {
if (!byCondition[r.condition]) byCondition[r.condition] = [];
byCondition[r.condition].push(r);
}
for (const [cond, turns] of Object.entries(byCondition)) {
let hadUnresolvedFirst = false;
for (let i = 0; i < turns.length - 1; i++) {
if (turns[i].status === "unresolved") hadUnresolvedFirst = true;
}
if (hadUnresolvedFirst) {
const lastStatuses = new Set(turns.map((t) => t.status));
expect(lastStatuses.size).toBeGreaterThan(1);
}
}
});
it("condition evidenceNodeIds is non-empty when status is established", () => {
for (const r of allResults) {
if (r.status === "established") {
expect(r.evidenceNodeIds.length).toBeGreaterThan(0);
} else if (r.status === "unresolved") {
expect(r.evidenceNodeIds.length).toBe(0);
}
}
});
it("reason is always a non-empty string", () => {
for (const r of allResults) {
expect(typeof r.reason).toBe("string");
expect(r.reason.length).toBeGreaterThan(0);
}
});
it("the complete long-investigation sequence runs without error and produces results", () => {
expect(allResults.length).toBe(CONDITIONS.length * turnCount);
});
});