experiment: test structured evidence to consequence reasoning

This commit is contained in:
2026-08-07 19:02:37 +01:00
parent cd9328ef8e
commit dd3b8e505d
3 changed files with 526 additions and 4 deletions
+3 -3
View File
@@ -141,8 +141,8 @@ Answer before continuing:
--- ---
*Created by Experiment 34. Updated by Experiments 3853, 54A54P. Branch: `feature/user-workspace-ux-v0.7`.* *Created by Experiment 34. Updated by Experiments 3853, 54A54Q. Branch: `feature/user-workspace-ux-v0.7`.*
### Return-to-Work Note (Experiment 54P) ### Return-to-Work Note (Experiment 54Q)
Experiment 54N failed on a higher-level consequence judgement for competing causes sharing one diagnostic goal. Experiment 54O showed the underlying evidence distinction (staff vs supplier) was available when asked directly. Experiment 54P tested whether making those evidence needs explicit restores the consequence judgement; results across three cases all passed. No winner or next question was generated in either experiment. Same host/model retained; no production behaviour changed. Whether this explains the 54N failure or only narrows it remains open. Branch: `feature/user-workspace-ux-v0.7`. First test/file to inspect when resuming: `tests/reconstruction/semantic-evidence-consequence.test.js`. Experiment 54N failed when moving directly from hypotheses to consequence on the staff-capacity versus supplier-lead-time case. Experiment 54O showed evidence discrimination works when asked directly. Experiment 54P showed explicit supplied evidence restores consequence judgement. Experiment 54Q tested whether one structured semantic call can first expose evidence needs and then preserve the consequence — it succeeded across all three cases including the original 54N failure. No winner or question generated; same host/model; no production behaviour changed. Whether this explains the 54N failure or only narrows it remains open. Branch: `feature/user-workspace-ux-v0.7`. First test/file to inspect when resuming: `tests/reconstruction/semantic-structured-evidence-consequence.test.js`.
+211 -1
View File
@@ -7595,7 +7595,7 @@ No winner was chosen. No next question was generated. All invariant violations a
**Explicit evidence differences recovered the correct consequence in all tested cases.** **Explicit evidence differences recovered the correct consequence in all tested cases.**
When the model is given explicit evidence needs as structured input, it can correctly infer whether those needs imply materially different investigation directions — including on the exact case where Experiment 54N failed. The bridge from evidence distinction to consequence judgement appears functional under controlled conditions. When the model is given explicit evidence needs as structured input, it can correctly infer whether those needs imply materially different investigation directions — including on the exact case where Experiment 54N failed. In the three tested cases, explicit evidence distinctions were sufficient for the model to produce the expected consequence judgement.
### Limitations ### Limitations
@@ -7607,3 +7607,213 @@ When the model is given explicit evidence needs as structured input, it can corr
### Status ### Status
**Pending Rob's review.** No production code changed. No schemas modified. No active engine behaviour changed. Branch: `feature/user-workspace-ux-v0.7`. First file to inspect when resuming: `tests/reconstruction/semantic-evidence-consequence.test.js`. **Pending Rob's review.** No production code changed. No schemas modified. No active engine behaviour changed. Branch: `feature/user-workspace-ux-v0.7`. First file to inspect when resuming: `tests/reconstruction/semantic-evidence-consequence.test.js`.
## Experiment 54Q — Can One Structured Semantic Step Preserve the Evidence Distinction and the Consequence? (2026-08-07)
### Objective
First, tighten Experiment 54P so its conclusion stays within the evidence.
Then test one narrow bridge: if the model must explicitly identify the evidence needed for each hypothesis before stating whether those needs change what must be established next, can it recover the correct consequence in one semantic call?
This is passive and test-only. Do not generate a next question. Do not choose a winning hypothesis. Do not change production behaviour.
### Hypothesis
A structured output that requires the model to expose the evidence distinction before the consequence may preserve information that was lost in Experiment 54N. If the original 54N failure reappears even with explicit intermediate evidence fields, the problem lies elsewhere. If it succeeds, the result shows that a single structured semantic call can retain the intermediate distinction in these cases.
### Context Budget
Read only:
- `docs/current-handoff.md`;
- Experiment 54P only in `docs/design-evolution-log.md`;
- `tests/reconstruction/semantic-evidence-consequence.test.js`;
- `tests/reconstruction/semantic-hypothesis-evidence-needs.test.js`;
- `.env.local` only for `OLLAMA_BASE_URL` and `OLLAMA_MODEL`.
Not read: full experiment history; production prompts; graph files; assessor; Behaviour Selection; question-selection code; UI.
### Configuration
Host: `http://192.168.1.111:11434` (same as 54P)
Model: `qwen-claude:latest` (same as 54P)
No localhost fallback. No experiment-specific model variable.
### Number of Live Inference Calls
Exactly **3** live Ollama calls — one per case.
### Input Contract
Each call receives:
```json
{
"problem": "...",
"hypothesisA": "...",
"hypothesisB": "..."
}
```
### Output Contract
Return exactly:
```json
{
"evidenceForA": ["short evidence needs"],
"evidenceForB": ["short evidence needs"],
"changesInformationNeededNext": true | false,
"reason": "one short sentence"
}
```
No winner. No confidence. No score. No next question. No chain-of-thought. The evidence lists are concise outputs, not reasoning traces.
### Semantic Instruction Used
> Identify the main evidence needed to investigate each hypothesis. Then decide whether those evidence needs mean materially different information must be established next. Return true when the evidence sets investigate meaningfully different things and false when they are materially the same despite wording differences. Do not choose which hypothesis is correct and do not generate a question.
No examples. No expected answers.
### Case 1 — Original 54N Failure
**Problem:** "Orders are arriving late and customers have started complaining."
**Hypothesis A:** "Delivery delays are being caused by insufficient staff capacity."
**Hypothesis B:** "Delivery delays are being caused by unreliable supplier lead times."
**Fixed human reference:** `changesInformationNeededNext: true`
Evidence A should materially involve: staffing, capacity, workload/throughput.
Evidence B should materially involve: supplier lead times, delivery reliability.
### Case 2 — Same Cause, Paraphrased
**Problem:** "Orders are arriving late and customers have started complaining."
**Hypothesis A:** "The team may not have enough capacity to process orders on time."
**Hypothesis B:** "Insufficient staff capacity may be causing the order delays."
**Fixed human reference:** `changesInformationNeededNext: false`
Both evidence sets should materially concern: staff capacity, workload, processing throughput.
### Case 3 — Different Causes, Different Domain
**Problem:** "Website sales have fallen sharply over the last month."
**Hypothesis A:** "The fall may be caused by a recent increase in product prices."
**Hypothesis B:** "The fall may be caused by a technical checkout problem."
**Fixed human reference:** `changesInformationNeededNext: true`
Evidence A should concern: pricing, conversion/customer response.
Evidence B should concern: checkout errors, technical/funnel evidence.
### Results
#### Case 1 — Staff Capacity vs Supplier Lead Time
**Model output:**
```json
{
"evidenceForA": ["staff scheduling and availability data", "order processing speed per shift", "employee overtime and workload logs"],
"evidenceForB": ["supplier on-time delivery historical rates", "vendor promised vs actual arrival dates", "purchase order fulfillment delays"],
"changesInformationNeededNext": true,
"reason": "Investigating A requires internal workforce productivity metrics while investigating B requires external vendor performance and logistics data."
}
```
**Evidence classification:** `evidence_correct` ✓ — Evidence A focused on staffing/capacity/workload (staff scheduling, processing speed, overtime logs). Evidence B focused on supplier/lead times/delivery reliability (on-time rates, promised vs actual dates, fulfillment delays). Materially distinct sets.
**Consequence classification:** `consequence_correct` ✓ — Returned `true` with reason correctly distinguishing internal workforce metrics from external vendor data. The original 54N failure case recovered.
#### Case 2 — Same Cause, Paraphrased (Control)
**Model output:**
```json
{
"evidenceForA": ["Current order volume vs. team processing capacity", "Staff utilization rates and overtime hours", "Workload distribution across team members"],
"evidenceForB": ["Current order volume vs. staffing capacity", "Employee utilization metrics and overtime data", "Headcount relative to peak demand periods"],
"changesInformationNeededNext": false,
"reason": "Both hypotheses target the exact same root cause (capacity constraints), requiring identical operational workload and staffing metrics to validate."
}
```
**Evidence classification:** `evidence_correct` ✓ — 8 overlapping terms across evidence sets; both focused on capacity/throughput/metrics. Materially equivalent despite different phrasing.
**Consequence classification:** `consequence_correct` ✓ — Returned `false`, correctly identifying both hypotheses as targeting the same root cause.
#### Case 3 — Pricing vs Checkout
**Model output:**
{
"evidenceForA": ["Price change logs", "Pre/post price sales data", "Customer pricing complaints"],
"evidenceForB": ["Checkout error logs", "Payment gateway status reports", "Recent deployment records"],
"changesInformationNeededNext": true,
"reason": "One requires financial and market metrics while the other requires IT system and code deployment data."
}
**Evidence classification:** `evidence_correct` ✓ — Evidence A focused on pricing (price change logs, pre/post sales data, pricing complaints). Evidence B focused on checkout/technical (error logs, payment gateway status, deployment records). Materially distinct.
**Consequence classification:** `consequence_correct` ✓ — Returned `true`, correctly distinguishing financial/market investigation from IT/system investigation.
### Evidence-Correct Count
**3 of 3** cases classified correctly.
### Evidence-Failed Count
**0 of 3** cases classified incorrectly.
### Consequence-Correct Count
**3 of 3** cases classified correctly.
### Consequence-Failed Count
**0 of 3** cases classified incorrectly.
### Did Any Case Have Correct Evidence but Wrong Consequence?
No. No such case was found.
### Did Any Case Have Wrong Evidence but Correct Consequence?
No. No such case was found.
### Did Case 1 Recover the Original 54N Failure?
Yes. The model returned `changesInformationNeededNext: true` with evidence correctly distinguishing staffing/capacity from supplier/lead-time domains — the same distinction where Experiment 54N returned `false`.
### Did the Model Choose a Winner?
No. No winner selection detected in any output.
### Did the Model Generate a Next Question?
No. No question generation detected in any output.
### Comparison with Experiments 54N / 54O / 54P
**With 54N:** Experiment 54N (Case 3) returned `changesInformationNeededNext: false` — collapsing competing causes into one investigation direction. In 54Q, the same problem and hypotheses produced `true` with evidence correctly distinguishing the two domains. This suggests the 54N failure was not a lack of capability but a loss or misrouting of intermediate evidence information in the model's processing pipeline.
**With 54O:** Experiment 54O showed the model can distinguish evidence needs directly (3/3). In 54Q, the model both identified evidence needs AND correctly judged their consequence in one call (3/3), showing these two operations work together when structured as a single output contract.
**With 54P:** Experiment 54P supplied fixed human-evidence lists and recovered correct consequences (3/3). In 54Q, the model itself identified the evidence needs in addition to judging their consequence — both operations succeed within a single call. This is one step beyond 54P: instead of two separate operations (identify evidence → judge consequence), they are combined into one structured output.
### Focused Test Result
3 of 3 evidence classifications and 3 of 3 consequence classifications matched fixed human references. No invariant violations detected (no winner selection, no scores, no next questions). All three outputs produced semantically coherent evidence lists and correctly classified the consequence for each case. The original 54N failure case (staff capacity vs supplier lead time) recovered when the model was required to expose its evidence reasoning before deciding the consequence.
### Experiment Conclusion
**Structured evidence plus consequence succeeded across all tested cases.**
In the three tested cases, explicit evidence distinctions were sufficient for the model to produce the expected consequence judgement. The evidence distinction and consequence remained aligned in every case: correct evidence maps to correct consequence, and no case produced a mismatch in either direction.
### Limitations
- Three cases only; limited domain coverage (one delivery scenario, one paraphrase control, one e-commerce scenario).
- No test of whether the model can preserve this alignment when the prompt is less structured or contains more noise.
- Same host/model used throughout — results may vary with different configurations.
- Does not establish generalisation beyond these specific inputs.
- The evidence lists are generated outputs, not human-fixed references; their correctness was verified semantically against fixed expectations but could vary across runs or prompts.
### Status
**Pending Rob's review.** No production code changed. No schemas modified. No active engine behaviour changed. Branch: `feature/user-workspace-ux-v0.7`. First file to inspect when resuming: `tests/reconstruction/semantic-structured-evidence-consequence.test.js`.
@@ -0,0 +1,312 @@
import { describe, it, expect } from "vitest";
import { config } from "dotenv";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
config({ path: path.resolve(__dirname, "../../.env.local") });
const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL;
const OLLAMA_MODEL = process.env.OLLAMA_MODEL;
if (!OLLAMA_BASE_URL || !OLLAMA_MODEL) {
throw new Error("OLLAMA_BASE_URL and OLLAMA_MODEL must be set in .env.local");
}
/**
* Make one live Ollama chat call: identify evidence needed for each hypothesis,
* then decide whether those evidence needs mean materially different information
* must be established next.
*/
async function callStructuredEvidenceConsequence(problem, hypothesisA, hypothesisB) {
const instruction = `Identify the main evidence needed to investigate each hypothesis. Then decide whether those evidence needs mean materially different information must be established next. Return true when the evidence sets investigate meaningfully different things and false when they are materially the same despite wording differences. Do not choose which hypothesis is correct and do not generate a question.
Return valid JSON only in this shape:
{
"evidenceForA": ["short evidence needs"],
"evidenceForB": ["short evidence needs"],
"changesInformationNeededNext": true | false,
"reason": "one short sentence"
}`;
const messages = [
{ role: "system", content: instruction.trim() },
{
role: "user",
content: `Problem: ${JSON.stringify(problem)}
Hypothesis A: ${JSON.stringify(hypothesisA)}
Hypothesis B: ${JSON.stringify(hypothesisB)}`,
},
];
const res = await fetch(`${OLLAMA_BASE_URL}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: OLLAMA_MODEL,
messages,
format: "json",
stream: false,
}),
});
if (!res.ok) {
throw new Error(`Ollama API error: ${res.status} ${res.statusText}`);
}
const data = await res.json();
const rawContent = data.message?.content ?? "";
const cleaned = rawContent.replace(/```(?:json)?\s*/g, "").replace(/```\s*/g, "");
return JSON.parse(cleaned.trim());
}
// ──────────────────────────────────────────────
// Fixed human-reference ground truth (pre-written)
// ──────────────────────────────────────────────
const CASES = [
{
id: "Case 1 — Original 54N Failure (Staff Capacity vs Supplier Lead Time)",
problem: "Orders are arriving late and customers have started complaining.",
hypothesisA: "Delivery delays are being caused by insufficient staff capacity.",
hypothesisB: "Delivery delays are being caused by unreliable supplier lead times.",
reference: {
changesInformationNeededNext: true,
evidenceForA_semantic: ["staffing", "capacity", "workload/throughput"],
evidenceForB_semantic: ["supplier lead times", "delivery reliability"]
},
},
{
id: "Case 2 — Same Cause, Paraphrased (Control)",
problem: "Orders are arriving late and customers have started complaining.",
hypothesisA: "The team may not have enough capacity to process orders on time.",
hypothesisB: "Insufficient staff capacity may be causing the order delays.",
reference: {
changesInformationNeededNext: false,
evidenceForA_semantic: ["staff capacity", "workload", "processing throughput"],
evidenceForB_semantic: ["staff capacity", "workload", "processing throughput"]
},
},
{
id: "Case 3 — Different Causes, Different Domain (Pricing vs Checkout)",
problem: "Website sales have fallen sharply over the last month.",
hypothesisA: "The fall may be caused by a recent increase in product prices.",
hypothesisB: "The fall may be caused by a technical checkout problem.",
reference: {
changesInformationNeededNext: true,
evidenceForA_semantic: ["pricing", "conversion/customer response"],
evidenceForB_semantic: ["checkout errors", "technical/funnel evidence"]
},
},
];
// ──────────────────────────────────────────────
// Semantic evaluation against fixed human references
// ──────────────────────────────────────────────
function evaluateEvidence(modelResult, caseType) {
const result = modelResult;
let issues = [];
let notes = [];
// Must provide evidence lists
if (!Array.isArray(result.evidenceForA) || result.evidenceForA.length === 0) {
issues.push("missing_evidence_A: evidenceForA is empty or not an array");
}
if (!Array.isArray(result.evidenceForB) || result.evidenceForB.length === 0) {
issues.push("missing_evidence_B: evidenceForB is empty or not an array");
}
// Check invariant violations (no winner, no next question)
const allText = JSON.stringify(result).toLowerCase();
const hasWinnerSelection = allText.includes("preferred") ||
allText.includes("more likely") ||
allText.includes("should go with") ||
allText.includes("is the winner");
// Manual semantic judge per case type
const evA = result.evidenceForA?.join(" ").toLowerCase() ?? "";
const evB = result.evidenceForB?.join(" ").toLowerCase() ?? "";
let evidence_correct = true;
let evidence_details = [];
if (caseType === "different_causes") {
// Case 1: A should be staffing/capacity, B should be supplier/lead times
const hasStaffingKeywords = ["staff", "capacity", "workload", "throughput", "personnel", "headcount", "roster"].some(k => evA.includes(k));
const hasSupplierKeywords = ["supplier", "lead time", "delivery reliability", "vendor", "supply chain", "procurement", "fulfillment partner"].some(k => evB.includes(k));
if (!hasStaffingKeywords) {
evidence_correct = false;
evidence_details.push("evidence_A_lacks_staffing/capacity focus");
}
if (!hasSupplierKeywords) {
evidence_correct = false;
evidence_details.push("evidence_B_lacks_supplier/lead-time focus");
}
// Check that they are distinct (not overlapping heavily on same domain)
const staffInBoth = ["staff", "capacity"].some(k => evA.includes(k) && evB.includes(k));
if (staffInBoth) {
evidence_details.push("WARNING: staffing term appears in both evidence sets — may indicate conflation");
}
}
if (caseType === "same_cause_paraphrase") {
// Case 2: Both should reference the same domain (staff capacity)
const evA_words = new Set(evA.split(/\s+/));
const evB_words = new Set(evB.split(/\s+/));
const overlapCount = [...evA_words].filter(w => evB_words.has(w)).length;
if (overlapCount < 3) {
evidence_correct = false;
evidence_details.push("evidence_paraphrase: too few overlapping terms — may not be materially equivalent");
} else {
evidence_details.push(`evidence_paraphrase: ${overlapCount} overlapping terms suggests material equivalence`);
}
// Both should mention staffing/capacity
const hasStaffing = ["staff", "capacity"].every(k => evA.includes(k) && evB.includes(k));
if (!hasStaffing) {
evidence_details.push("evidence_paraphrase: staffing/capacity concept not present in both sets");
}
}
if (caseType === "different_domains") {
// Case 3: A should be pricing, B should be checkout/technical
const hasPricingKeywords = ["price", "pricing", "cost", "charge", "monetary"].some(k => evA.includes(k));
const hasCheckoutKeywords = ["checkout", "cart", "payment", "funnel", "technical", "error", "browser", "device", "load time"].some(k => evB.includes(k));
if (!hasPricingKeywords) {
evidence_correct = false;
evidence_details.push("evidence_A_lacks_pricing focus");
}
if (!hasCheckoutKeywords) {
evidence_correct = false;
evidence_details.push("evidence_B_lacks_checkout/technical focus");
}
}
return {
evidence_correct,
evidence_failed: !evidence_correct,
evidence_details,
notes,
issues,
has_winner_selection: hasWinnerSelection,
raw: result,
};
}
function evaluateConsequence(modelResult, reference) {
const result = modelResult;
let issues = [];
let notes = [];
if (result.changesInformationNeededNext !== reference.changesInformationNeededNext) {
issues.push(`consequence_mismatch: expected ${reference.changesInformationNeededNext}, got ${result.changesInformationNeededNext}`);
}
if (!result.reason || typeof result.reason !== "string") {
issues.push("missing_reason: no reason field provided");
}
return {
consequence_correct: issues.length === 0,
consequence_failed: issues.length > 0,
issues,
notes,
};
}
// ──────────────────────────────────────────────
// Test suite
// ──────────────────────────────────────────────
describe("Experiment 54Q — Structured Evidence plus Consequence in One Call", () => {
const results = [];
const timings = [];
for (const c of CASES) {
it(c.id, async () => {
const start = Date.now();
const result = await callStructuredEvidenceConsequence(c.problem, c.hypothesisA, c.hypothesisB);
const elapsed = Date.now() - start;
timings.push({ caseId: c.id, ms: elapsed });
let caseType;
if (c.id.includes("Case 1")) caseType = "different_causes";
else if (c.id.includes("Case 2")) caseType = "same_cause_paraphrase";
else caseType = "different_domains";
let evidenceEval = evaluateEvidence(result, caseType);
let consequenceEval = evaluateConsequence(result, c.reference);
results.push({
case: c,
modelResult: result,
evidence: evidenceEval,
consequence: consequenceEval,
timingMs: elapsed,
});
// Structural assertions (invariants)
expect(result.evidenceForA).toBeDefined();
expect(result.evidenceForB).toBeDefined();
expect(Array.isArray(result.evidenceForA)).toBe(true);
expect(Array.isArray(result.evidenceForB)).toBe(true);
expect(typeof result.changesInformationNeededNext).toBe("boolean");
expect(result.reason).toBeDefined();
// No winner selection invariant
const allText = JSON.stringify(result).toLowerCase();
expect(allText).not.toContain("preferred");
expect(allText).not.toContain("more likely");
// Boolean must match reference
expect(result.changesInformationNeededNext).toBe(c.reference.changesInformationNeededNext);
}, 120000);
}
it("Experiment 54Q: aggregate results", () => {
const evidenceCorrect = results.filter(r => r.evidence.evidence_correct).length;
const evidenceFailed = results.filter(r => r.evidence.evidence_failed).length;
const consequenceCorrect = results.filter(r => r.consequence.consequence_correct).length;
const consequenceFailed = results.filter(r => r.consequence.consequence_failed).length;
// Report findings to console for manual review
console.log("\n=== Experiment 54Q Results ===");
for (const r of results) {
console.log(`\n--- ${r.case.id} ---`);
console.log("Evidence A:", JSON.stringify(r.modelResult.evidenceForA));
console.log("Evidence B:", JSON.stringify(r.modelResult.evidenceForB));
console.log("Consequence:", r.modelResult.changesInformationNeededNext);
console.log("Reason:", r.modelResult.reason);
console.log("Evidence correct:", r.evidence.evidence_correct, r.evidence.evidence_details);
console.log("Consequence correct:", r.consequence.consequence_correct, r.consequence.issues);
}
console.log(`\nEvidence-correct: ${evidenceCorrect}/${results.length}`);
console.log(`Evidence-failed: ${evidenceFailed}/${results.length}`);
console.log(`Consequence-correct: ${consequenceCorrect}/${results.length}`);
console.log(`Consequence-failed: ${consequenceFailed}/${results.length}`);
const totalMs = timings.reduce((s, t) => s + t.ms, 0);
console.log(`Total time: ${totalMs}ms`);
console.log(`Average time: ${(totalMs / timings.length).toFixed(1)}ms per call`);
console.log(`Fastest: ${Math.min(...timings.map(t => t.ms))}ms`);
console.log(`Slowest: ${Math.max(...timings.map(t => t.ms))}ms`);
// Critical case assertion: Case 1 must get consequence correct (the original 54N failure)
const case1 = results.find(r => r.case.id.includes("Case 1"));
expect(case1.consequence.consequence_correct).toBe(true);
// No winner selection in any result
for (const r of results) {
expect(r.evidence.has_winner_selection).toBe(false);
}
});
});