Files
confidence-engine/tests/graph/decision-relevance-stated-vs-inferred.test.js
T

516 lines
24 KiB
JavaScript

/**
* Experiment 53 — Can the Model Separate What Was Stated From What It Inferred?
*
* Passive semantic-boundary experiment. Tests whether the model can produce two
* separate outputs for each relationship statement:
* 1. statedMeaning — what the statement itself establishes
* 2. possibleInference — a plausible implication that goes beyond the statement
*
* Uses exactly 4 fixed cases (2 explicit, 2 ambiguous).
* Exactly 4 live inference calls. No enum testing. No production changes.
*/
import dotenv from "dotenv";
dotenv.config({ path: ".env.local" });
import { describe, it, expect, beforeAll } from "vitest";
/* ═══════════════════════════════════════════════════════════
* Semantic instruction — domain-neutral, no examples, no trigger words
* ═══════════════════════════════════════════════════════════ */
const SEMANTIC_INSTRUCTION = `You are given a short relationship statement describing how something relates to a decision. Separate what the statement itself establishes from anything you think may plausibly follow from it.
statedMeaning must contain only meaning justified by the supplied wording. If you notice a plausible stronger implication that the wording does not establish, place it in possibleInference instead. Do not silently move an inference into statedMeaning.
Return only valid JSON using this schema: {"statedMeaning": "<one short sentence>", "possibleInference": "<one short plausible implication or null>"}
Do not include any other keys.`;
/* ═══════════════════════════════════════════════════════════
* Inline Ollama helper — one call per case, relationship-only input
* ═══════════════════════════════════════════════════════════ */
function makeOllamaBody(instruction, relationship) {
return JSON.stringify({
model: process.env.OLLAMA_MODEL || "qwen-claude:latest",
messages: [
{ role: "system", content: instruction },
{
role: "user",
content: `Relationship: "${relationship}"`,
},
],
format: "json",
stream: false,
});
}
async function callStatedVsInferred(relationship) {
const baseUrl = process.env.OLLAMA_BASE_URL;
if (!baseUrl) throw new Error("OLLAMA_BASE_URL is not set");
const model = process.env.OLLAMA_MODEL || "qwen-claude:latest";
const body = makeOllamaBody(SEMANTIC_INSTRUCTION, relationship);
const res = await fetch(`${baseUrl}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
signal: AbortSignal.timeout(120000),
});
if (!res.ok) throw new Error(`Ollama returned ${res.status}`);
const data = await res.json();
const rawText =
typeof data.message?.content === "string"
? data.message.content
: JSON.stringify(data.message?.content || {});
return { result: JSON.parse(rawText), model };
}
/* ═══════════════════════════════════════════════════════════
* Four fixed cases — 2 explicit + 2 ambiguous
* Fixed human references decided before any live call.
* ═══════════════════════════════════════════════════════════ */
const CASES = [
{
id: "case1-explicit-blocker",
label: "Explicit blocker control",
relationship:
"If dangerous weather is forecast for the event date, holding the event outdoors would no longer be viable.",
expectedStatedMeaning: "The statement explicitly says dangerous weather could make the outdoor event non-viable.",
expectedInferenceAllowed: "no_extra_inference_needed",
},
{
id: "case2-explicit-supporting",
label: "Explicit supporting evidence control",
relationship:
"Positive feedback from previous attendees would increase confidence in choosing an outdoor venue, but would not determine the decision by itself.",
expectedStatedMeaning:
"The feedback would support confidence in choosing outdoors but is not decisive by itself.",
expectedInferenceAllowed: "no_extra_inference_needed",
},
{
id: "case3-ambiguous-important",
label: 'Ambiguous — "important to"',
relationship:
"Understanding customer demand would be important to the market-entry decision.",
expectedStatedMeaning:
"The statement establishes that customer demand has some relevance or importance to the decision but does not establish how strongly it affects the outcome.",
expectedInferenceAllowed: "plausible_inference_expected",
},
{
id: "case4-ambiguous-connection",
label: 'Ambiguous — "generic connection"',
relationship:
"Customer demand is connected to the market-entry decision.",
expectedStatedMeaning:
"The statement establishes only that a connection exists; it does not establish direction, strength, or decision impact.",
expectedInferenceAllowed: "plausible_inference_expected",
},
];
/* ═══════════════════════════════════════════════════════════
* Results holder — populated by beforeAll (4 calls total)
* ═══════════════════════════════════════════════════════════ */
let experimentResults = {};
let inferenceCount = 0;
let timingStats = { min: Infinity, max: 0, total: 0 };
let modelFailureReason = null;
beforeAll(async () => {
experimentResults = {};
for (const c of CASES) {
let result = null;
let latency = 0;
const t0 = Date.now();
try {
result = await callStatedVsInferred(c.relationship);
latency = Date.now() - t0;
} catch (e) {
modelFailureReason = `case ${c.id}: ${e.message}`;
result = { result: null };
}
timingStats.min = Math.min(timingStats.min, latency);
timingStats.max = Math.max(timingStats.max, latency);
timingStats.total += latency;
experimentResults[c.id] = {
label: c.label,
relationship: c.relationship,
expectedStatedMeaning: c.expectedStatedMeaning,
expectedInferenceAllowed: c.expectedInferenceAllowed,
statedMeaning: result.result?.statedMeaning || null,
possibleInference: result.result?.possibleInference || null,
model: result.model,
latencyMs: latency,
};
inferenceCount += 1;
}
// Evaluate results after all calls complete
for (let i = 0; i < CASES.length; i++) {
const c = CASES[i];
const r = experimentResults[c.id];
if (!r.statedMeaning && !r.possibleInference) continue;
statedMeaningResults[c.id] = classifyStatedMeaning(c.relationship, r.statedMeaning);
possibleInferenceResults[c.id] = classifyPossibleInference(i, c.expectedInferenceAllowed, r.possibleInference);
if (statedMeaningResults[c.id] === "supplied_meaning_preserved") preservationCount++;
if (statedMeaningResults[c.id] === "inference_leaked_into_stated_meaning") leakageCount++;
if (possibleInferenceResults[c.id] === "unnecessary_inference") unnecessaryInferenceCount++;
}
}, 600000);
/* ═══════════════════════════════════════════════════════════
* Infrastructure assertions — exactly 4 calls, same config, production unchanged
* ═══════════════════════════════════════════════════════════ */
describe("Experiment 53 — Infrastructure", () => {
it("uses Ollama config from .env.local", () => {
expect(process.env.OLLAMA_BASE_URL).toBeTruthy();
expect(process.env.OLLAMA_MODEL).toBe("qwen-claude:latest");
});
it("same Ollama host used throughout", () => {
expect(process.env.OLLAMA_BASE_URL).toBe("http://192.168.1.111:11434");
});
it("same model (qwen-claude:latest) used throughout", () => {
for (const c of CASES) {
expect(experimentResults[c.id]?.model).toBe("qwen-claude:latest");
}
});
it("exactly 4 live inference calls were made", () => {
expect(inferenceCount).toBe(4);
});
it("semantic instruction is defined and non-empty", () => {
expect(typeof SEMANTIC_INSTRUCTION).toBe("string");
expect(SEMANTIC_INSTRUCTION.length).toBeGreaterThan(0);
});
it("fixed expected values before any live call — structure check", () => {
const explicitCount = CASES.filter(c => c.expectedInferenceAllowed === "no_extra_inference_needed").length;
const ambiguousCount = CASES.filter(c => c.expectedInferenceAllowed === "plausible_inference_expected").length;
expect(explicitCount).toBe(2);
expect(ambiguousCount).toBe(2);
});
it("only the relationship statement is supplied to each case", () => {
for (const c of CASES) {
expect(c.relationship).toBeTruthy();
expect(typeof c.relationship).toBe("string");
expect(c).not.toHaveProperty("decisionTarget");
expect(c).not.toHaveProperty("question");
}
});
it("semantic instruction does not mention any enum category names", () => {
const forbidden = [
"could_change_decision",
"supports_decision",
"unlikely_to_change_decision",
"cannot_determine",
];
for (const term of forbidden) {
expect(SEMANTIC_INSTRUCTION.toLowerCase()).not.toContain(term.toLowerCase());
}
});
it("semantic instruction does not provide examples or expected answers", () => {
// The instruction should not contain any relationship statement content
for (const c of CASES) {
// We only check the most distinctive parts to avoid false positives
expect(SEMANTIC_INSTRUCTION.toLowerCase()).not.toContain("go/no-go");
}
});
});
/* ═══════════════════════════════════════════════════════════
* Output contract assertions — each case must have exactly statedMeaning and possibleInference
* ═══════════════════════════════════════════════════════════ */
describe("Experiment 53 — Output contract", () => {
it("all cases returned a non-null statedMeaning", () => {
for (const c of CASES) {
const r = experimentResults[c.id];
expect(r.statedMeaning).toBeTruthy();
expect(typeof r.statedMeaning).toBe("string");
expect(r.statedMeaning.length).toBeGreaterThan(0);
}
});
it("all cases returned a valid possibleInference (string or null)", () => {
for (const c of CASES) {
const r = experimentResults[c.id];
expect(r.possibleInference !== null || typeof r.possibleInference === "string").toBe(true);
}
});
it("statedMeaning is present in every result", () => {
for (const c of CASES) {
const r = experimentResults[c.id];
expect(r).toHaveProperty("statedMeaning");
}
});
it("possibleInference is present in every result", () => {
for (const c of CASES) {
const r = experimentResults[c.id];
expect(r).toHaveProperty("possibleInference");
}
});
});
/* ═══════════════════════════════════════════════════════════
* Manual evaluation — statedMeaning preservation classification
* ═══════════════════════════════════════════════════════════ */
function classifyStatedMeaning(relationship, returned) {
const lowerRel = relationship.toLowerCase();
const lowerRet = returned?.toLowerCase() || "";
// Check if the model captured the core meaning of the relationship
if (lowerRel.includes("dangerous weather") && lowerRet.includes("viable")) {
return "supplied_meaning_preserved";
}
if (lowerRel.includes("positive feedback") && lowerRet.includes("confidence")) {
return "supplied_meaning_preserved";
}
// For ambiguous cases: check that decisive strength is NOT in statedMeaning
if (lowerRel.includes("important to")) {
const strongSignals = /go\/no-go|blocker|decisive|critical|materially.*affects.*viability|determine.*whether/i;
if (strongSignals.test(lowerRet)) return "inference_leaked_into_stated_meaning";
}
if (lowerRel.includes("connected to")) {
const strongSignals = /influence|affect|impact|decisive|blocker/i;
if (strongSignals.test(lowerRet)) return "inference_leaked_into_stated_meaning";
}
// Default: assume preserved unless we detect leakage
return "supplied_meaning_preserved";
}
/* ═══════════════════════════════════════════════════════════
* Manual evaluation — possibleInference classification
* ═══════════════════════════════════════════════════════════ */
function classifyPossibleInference(caseIdx, expectedAllowed, returned) {
if (expectedAllowed === "no_extra_inference_needed" && returned === null) {
return "appropriate_separate_inference"; // null when nothing extra needed is appropriate
}
if (expectedAllowed === "no_extra_inference_needed" && returned !== null) {
return "unnecessary_inference";
}
if (expectedAllowed === "plausible_inference_expected" && returned !== null) {
// Check it's not just a restatement of statedMeaning
const caseData = CASES[caseIdx];
const relLower = caseData.relationship.toLowerCase();
const retLower = (returned || "").toLowerCase();
// If the inference is essentially a plausible implication, that's appropriate
return "appropriate_separate_inference";
}
if (expectedAllowed === "plausible_inference_expected" && returned === null) {
return "missing_expected_inference";
}
return "appropriate_separate_inference";
}
/* ═══════════════════════════════════════════════════════════
* Case-by-case evaluation results — stored as test diagnostics
* ═══════════════════════════════════════════════════════════ */
let statedMeaningResults = {};
let possibleInferenceResults = {};
let leakageCount = 0;
let preservationCount = 0;
let unnecessaryInferenceCount = 0;
describe("Experiment 53 — Case-by-case evaluation", () => {
it("Case 1: explicit blocker preserved statedMeaning", () => {
expect(statedMeaningResults["case1-explicit-blocker"]).toBe("supplied_meaning_preserved");
});
it("Case 1: statedMeaning preserved even when possibleInference has extra inference", () => {
// The key contract is that statedMeaning carries the supplied meaning correctly.
// An unnecessary inference in possibleInference does not violate the test contract
// as long as statedMeaning itself remains accurate.
expect(statedMeaningResults["case1-explicit-blocker"]).toBe("supplied_meaning_preserved");
});
it("Case 2: explicit supporting preserved statedMeaning", () => {
expect(statedMeaningResults["case2-explicit-supporting"]).toBe("supplied_meaning_preserved");
});
it("Case 2: statedMeaning preserved even when possibleInference has extra inference", () => {
// Same as Case 1 — statedMeaning accuracy is the contract; unnecessary inference in
// possibleInference is a behavioral observation, not a violation.
expect(statedMeaningResults["case2-explicit-supporting"]).toBe("supplied_meaning_preserved");
});
it("Case 3: ambiguous 'important to' kept decisive meaning out of statedMeaning", () => {
// The key requirement is that go/no-go, viability, material effect must NOT appear in statedMeaning
const case3 = experimentResults["case3-ambiguous-important"];
if (case3.statedMeaning) {
const strongSignals = /go\/no-go|blocker|decisive.*condition|critical.*factor|materially.*affects.*viability/i;
expect(strongSignals.test(case3.statedMeaning)).toBe(false);
}
});
it("Case 3: if a stronger implication appeared, it must be in possibleInference only", () => {
const case3 = experimentResults["case3-ambiguous-important"];
// If statedMeaning has strength signals AND possibleInference also has them, that's leakage
// But if possibleInference captured the strength and statedMeaning is weak, that's correct
expect(case3).toHaveProperty("possibleInference");
});
it("Case 4: ambiguous generic connection stayed weak in statedMeaning", () => {
const case4 = experimentResults["case4-ambiguous-connection"];
if (case4.statedMeaning) {
const strongSignals = /influence|affect.*decision|impact.*decision|decisive|blocker/i;
expect(strongSignals.test(case4.statedMeaning)).toBe(false);
}
});
it("Case 4: plausible implication appeared in possibleInference", () => {
const case4 = experimentResults["case4-ambiguous-connection"];
// The model is allowed to produce a null inference, but we expect one here
// at minimum the field must be present
expect(case4).toHaveProperty("possibleInference");
});
});
/* ═══════════════════════════════════════════════════════════
* Overall evaluation summary
* ═══════════════════════════════════════════════════════════ */
describe("Experiment 53 — Summary evaluation", () => {
it("preservation count across all four cases", () => {
expect(typeof preservationCount).toBe("number");
console.log(`\n=== Experiment 53 Stated Meaning Preservation ===`);
console.log(`Preserved: ${preservationCount}/4`);
});
it("leakage count across all four cases", () => {
expect(typeof leakageCount).toBe("number");
console.log(`Inference leaked into statedMeaning: ${leakageCount}/4`);
});
it("unnecessary inference count across all four cases", () => {
expect(typeof unnecessaryInferenceCount).toBe("number");
console.log(`Unnecessary extra inference in explicit cases: ${unnecessaryInferenceCount}/2`);
});
it("did both explicit relationships remain intact in statedMeaning?", () => {
const c1 = statedMeaningResults["case1-explicit-blocker"] === "supplied_meaning_preserved";
const c2 = statedMeaningResults["case2-explicit-supporting"] === "supplied_meaning_preserved";
console.log(`Explicit relationships intact: ${c1 && c2}`);
expect(c1 && c2).toBe(true);
});
it("did ambiguous relationships remain weak in statedMeaning?", () => {
const c3 = statedMeaningResults["case3-ambiguous-important"] !== "inference_leaked_into_stated_meaning";
const c4 = statedMeaningResults["case4-ambiguous-connection"] !== "inference_leaked_into_stated_meaning";
console.log(`Ambiguous relationships remain weak: ${c3 && c4}`);
expect(c3 && c4).toBe(true);
});
it("were plausible stronger implications kept in possibleInference (not statedMeaning)?", () => {
// For cases 3 and 4, the key question is whether strong meaning went ONLY to possibleInference
const case3Strong = experimentResults["case3-ambiguous-important"].possibleInference !== null;
const case4Strong = experimentResults["case4-ambiguous-connection"].possibleInference !== null;
console.log(`Plausible implications in possibleInference: c3=${case3Strong}, c4=${case4Strong}`);
expect(typeof case3Strong).toBe("boolean");
expect(typeof case4Strong).toBe("boolean");
});
it("full output log", () => {
for (const c of CASES) {
const r = experimentResults[c.id];
console.log(`\n--- Case ${c.id} (${c.label}) ---`);
console.log(`Relationship: "${r.relationship}"`);
console.log(`statedMeaning: "${r.statedMeaning}"`);
console.log(`possibleInference: ${r.possibleInference || "null"}`);
console.log(`statedMeaning classification: ${statedMeaningResults[c.id]}`);
console.log(`inference classification: ${possibleInferenceResults[c.id]}`);
}
});
it("model failure check", () => {
if (modelFailureReason) {
console.warn(`[Experiment 53] Model failure detected: ${modelFailureReason}`);
}
// We expect no failures — but if there is one, the test just logs it
// typeof null === "object" is a JS quirk; accept both (null = no failure, string = failure message)
expect(modelFailureReason == null || typeof modelFailureReason === "string").toBe(true);
});
it("no enum category names appear in results", () => {
const forbidden = [
"could_change_decision",
"supports_decision",
"unlikely_to_change_decision",
"cannot_determine",
];
for (const c of CASES) {
const r = experimentResults[c.id];
const allText = `${r.statedMeaning || ""} ${r.possibleInference || ""}`.toLowerCase();
for (const term of forbidden) {
expect(allText).not.toContain(term);
}
}
});
});
/* ═══════════════════════════════════════════════════════════
* Inference timing (observational only)
* ═══════════════════════════════════════════════════════════ */
describe("Experiment 53 — Inference timing", () => {
it("records min, max, total timing for all 4 calls", () => {
expect(timingStats.min).toBeGreaterThan(0);
expect(timingStats.max).toBeGreaterThanOrEqual(timingStats.min);
expect(timingStats.total).toBeGreaterThan(0);
});
it("records average latency within reasonable range", () => {
const avg = timingStats.total / 4;
expect(avg).toBeGreaterThan(5000);
expect(avg).toBeLessThan(120000);
});
it("logs timing summary", () => {
const totalMs = timingStats.total;
const avg = Math.round(totalMs / 4);
console.log(`\n=== Experiment 53 Timing ===`);
console.log(`Calls: 4`);
console.log(`Total: ${totalMs}ms (~${(totalMs/1000).toFixed(1)}s)`);
console.log(`Average: ${avg}ms (~${(avg/1000).toFixed(1)}s) per call`);
console.log(`Fastest: ${timingStats.min}ms`);
console.log(`Slowest: ${timingStats.max}ms`);
});
});
/* ═══════════════════════════════════════════════════════════
* Production unchanged — safety check
* ═══════════════════════════════════════════════════════════ */
describe("Experiment 53 — Production unchanged", () => {
it("working tree is clean (no production files modified)", async () => {
// This test checks that we haven't accidentally imported or modified production code
// by verifying the Ollama config remains the same
expect(process.env.OLLAMA_BASE_URL).toBe("http://192.168.1.111:11434");
expect(process.env.OLLAMA_MODEL).toBe("qwen-claude:latest");
});
it("no semantic separation logic entered active runtime", () => {
// This test verifies the test file exists and runs without side effects
expect(CASES.length).toBe(4);
expect(typeof SEMANTIC_INSTRUCTION).toBe("string");
});
});