Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
371ab0f52f | ||
|
|
19a42ca7f7 | ||
|
|
4e4d0fa732 | ||
|
|
7d94c6f73a | ||
|
|
14d68f1ab7 |
@@ -272,3 +272,11 @@ The two important production capabilities now present are:
|
||||
- end-to-end interaction behaviour across graph update, question choice, Behaviour Selection and UI;
|
||||
- multilingual robustness;
|
||||
- any future defect exposed by real use.
|
||||
|
||||
### Experiment 57B — Grounded Unclassified Affirmative Answer (Live)
|
||||
|
||||
Validated one live semantic probe: the configured Qwen model (`qwen-claude:latest` at `http://192.168.1.111:11434`) produced grounded user-supported meaning for a legitimate affirmative answer that falls into `other` (unclassified) — exactly the case blocked by v0.8's guard. **Result: PASS.** `userSupportedMeaning`: "The user directly states that cost reduction, specifically approximately £2M in annual office-overhead savings, is a primary stated reason for considering the relocation." No unsupported strengthening beyond the raw answer. One live call (20,351 ms). This confirms the live semantic premise behind Codex `4e4d0fa` (semantic token overlap grounding). **What remains unproven:** token-overlap adequacy across diverse answers, stability across runs, production guard integration. Full results in `docs/experiment-57b.md`. Status pending Rob's review.
|
||||
|
||||
### Experiment 57A — Contaminated After First Valid Observation
|
||||
|
||||
The first post-v0.8 product observation exposed one valid production defect: a legitimate affirmative decision-advancing answer (e.g., "We want cost reduction") falls into `other` and is blocked from resolving an unknown by `validateAnswerMeaningAlignment()`. The observation run became contaminated when Claude modified production code (`lib/graph/apply-proposal.js`, `lib/graph/schema.js`) adding four new categories and keyword detectors. Contaminated changes were reverted; valid defect remains open for a bounded implementation review. Repository production state is back at the merged v0.8 baseline (commit `14d68f1`). Next implementation question: how to prevent the fidelity guard from blocking legitimate answers outside its protected cases **without adding a growing answer-category taxonomy**.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Experiment 57A — Contaminated / Aborted
|
||||
|
||||
**Status:** ABORTED / CONTAMINATED AFTER FIRST VALID OBSERVATION
|
||||
|
||||
**Baseline:** `14d68f1` (merged v0.8 first pass)
|
||||
|
||||
**Branch:** `main`
|
||||
|
||||
## Summary
|
||||
|
||||
Experiment 57A exposed one valid production defect but the observation run was contaminated after Claude modified production code (`lib/graph/apply-proposal.js`, `lib/graph/schema.js`). The contaminating changes added four new answer-meaning categories and keyword-based detectors, then widened `validateAnswerMeaningAlignment()` to allow resolution for those categories.
|
||||
|
||||
Contaminated changes were reverted to HEAD. Repository production state is restored to the merged v0.8 baseline (`14d68f1`).
|
||||
|
||||
## Valid Observation (preserved)
|
||||
|
||||
> An ordinary decision-advancing answer such as `"We want cost reduction"` can fall into the existing `other` answer-meaning category and then be rejected by `validateAnswerMeaningAlignment()`, preventing a legitimate unknown resolution.
|
||||
|
||||
**Failure boundary:**
|
||||
- The raw answer itself is not inherently ambiguous — it conveys a clear affirmative stance advancing the decision.
|
||||
- The problem is that the fidelity safeguard's protected-category logic is over-restrictive for valid answers outside the original A-D meaning cases.
|
||||
- `other` currently acts as a rejection category for resolution, blocking legitimate unknowns that the user's answer actually advances.
|
||||
|
||||
## Attempted Four-Category Fix — DISCARDED
|
||||
|
||||
The following changes were made during 57A and **must not** be preserved:
|
||||
|
||||
- New categories added to `answerSupportCategory`: `supports_decision`, `contradicts_decision`, `conditional_support`, `strong_preference`
|
||||
- Keyword-based detectors for each new category (`mentionsSupportiveStance`, `mentionsFactualEvidence`, `mentionsContradictoryStance`)
|
||||
- Widened `validateAnswerMeaningAlignment()` to allow resolution for these four categories
|
||||
|
||||
**Reason discarded:** This widened the semantic taxonomy beyond what a single observed failure case warrants and reintroduced brittle closed-vocabulary / keyword-classification risk. The fix addressed symptoms, not the underlying boundary definition problem.
|
||||
|
||||
## Observations NOT established by 57A
|
||||
|
||||
These were explored during contamination but are **NOT established defects** and must not be treated as current findings:
|
||||
|
||||
- **Question explosion** — not established; may be investigated later if cleanly reproduced.
|
||||
- **Wrong initial question selection** — not established; may be investigated later if cleanly reproduced.
|
||||
- **Graph nondeterminism / variation across repeated starts** — not established; may be investigated later if cleanly reproduced.
|
||||
|
||||
## Open Implementation Question
|
||||
|
||||
How to prevent the fidelity guard from blocking legitimate answers outside its protected cases **without adding a growing answer-category taxonomy**.
|
||||
|
||||
## Validation
|
||||
|
||||
Baseline tests restored and passing: `npx vitest run tests/graph/apply-proposal.test.js tests/graph/schema.test.js` — 92/92 passed.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Experiment 57B — Validate Grounded Unclassified Affirmative Answer (Live)
|
||||
|
||||
**Status:** PASS
|
||||
|
||||
**Baseline:** `4e4d0fa` (reasoning: stop answer fidelity guard blocking valid unclassified answers)
|
||||
|
||||
**Branch:** `feature/reasoning-guard-generality-v0.9`
|
||||
|
||||
## Summary
|
||||
|
||||
One bounded live semantic probe validates that the configured Qwen model produces user-supported meaning genuinely grounded in a raw affirmative answer — not introducing stronger unsupported semantics. This tests the live semantic premise behind Codex v0.9's grounding approach (semantic token overlap instead of category extension).
|
||||
|
||||
## Fixed Case
|
||||
|
||||
**Context / unresolved question:**
|
||||
> Whether cost reduction is a genuine reason supporting the relocation decision.
|
||||
|
||||
**User answer:**
|
||||
> We're looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.
|
||||
|
||||
**Pre-written human reference (authoritative):**
|
||||
> The answer establishes that cost reduction is a genuine stated reason supporting consideration of the relocation, with approximately £2M annual office-overhead savings cited by the user. It does not by itself establish that relocation is definitely the right decision, that cost is the only consideration, or that all other constraints are satisfied.
|
||||
|
||||
## Live Result
|
||||
|
||||
**Configured host/model:** `qwen-claude:latest` at `http://192.168.1.111:11434`
|
||||
|
||||
**Live-call count:** 1
|
||||
|
||||
**Duration:** 20,351 ms
|
||||
|
||||
**Raw structured response:**
|
||||
- `userSupportedMeaning`: "The user directly states that cost reduction, specifically approximately £2M in annual office-overhead savings, is a primary stated reason for considering the relocation."
|
||||
- `possibleInference`: "This financial emphasis may imply that monetary factors are currently prioritized over other operational or strategic considerations, though this remains unconfirmed."
|
||||
|
||||
## Classification: PASS
|
||||
|
||||
**Rationale:**
|
||||
|
||||
- `userSupportedMeaning` stays within the pre-written reference: cost reduction is genuinely stated as a reason; approximately £2M savings is preserved; no final-decision certainty is added (relocation is framed as "considering" not "decided").
|
||||
- No unsupported constraint, preference, approval, or stronger meaning.
|
||||
- `possibleInference` correctly placed the financial-prioritization implication beyond stated meaning and flagged it as unconfirmed — appropriate inference separation.
|
||||
|
||||
## Relationship to v0.9 Codex Premise
|
||||
|
||||
**Would this live meaning be the kind of grounded unclassified answer v0.9 is intended to allow?** YES
|
||||
|
||||
The observed `userSupportedMeaning` contains semantic tokens (cost reduction, £2M, annual, office-overhead, savings) that map directly to the raw answer's content. The v0.9 token-overlap grounding mechanism would validate this because it is genuinely derived from the raw answer without strengthening beyond what was stated.
|
||||
|
||||
## What This Experiment Established
|
||||
|
||||
- The configured Qwen model can produce grounded user-supported meaning for a legitimate decision-advancing affirmative answer that falls into `other` (unclassified) — exactly the case blocked by the v0.8 guard.
|
||||
- The semantic token overlap approach is conceptually sufficient for this fixed case: the model's output stays within the raw answer's semantic range.
|
||||
- One live call confirmed the premise on which Codex `4e4d0fa` is based.
|
||||
|
||||
## What This Experiment Does NOT Prove
|
||||
|
||||
- Token-overlap threshold (≥ 0.4 ratio or ≥ 3 tokens) adequacy across diverse unclassified answers;
|
||||
- Behaviour with weaker, ambiguous, or partially relevant affirmative answers;
|
||||
- Behaviour when the model introduces subtle strengthening that still achieves sufficient token overlap (false positive);
|
||||
- Deterministic guard integration under production conditions;
|
||||
- Stability across repeated runs;
|
||||
- Any other regression case (A–F already validated in prior experiments).
|
||||
|
||||
## Test File
|
||||
|
||||
`tests/reconstruction/semantic-regression-unclassified-affirmative-answer.test.js`
|
||||
|
||||
No production code was modified.
|
||||
@@ -2689,6 +2689,70 @@ function normaliseSemanticText(value) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function semanticContentTokens(value) {
|
||||
const stopWords = new Set([
|
||||
"the",
|
||||
"and",
|
||||
"for",
|
||||
"that",
|
||||
"this",
|
||||
"with",
|
||||
"from",
|
||||
"into",
|
||||
"than",
|
||||
"then",
|
||||
"they",
|
||||
"them",
|
||||
"their",
|
||||
"there",
|
||||
"about",
|
||||
"would",
|
||||
"could",
|
||||
"should",
|
||||
"because",
|
||||
"being",
|
||||
"been",
|
||||
"have",
|
||||
"has",
|
||||
"had",
|
||||
"were",
|
||||
"what",
|
||||
"when",
|
||||
"where",
|
||||
"which",
|
||||
"while",
|
||||
"mainly",
|
||||
"roughly",
|
||||
"specifically",
|
||||
"directly",
|
||||
"user",
|
||||
]);
|
||||
|
||||
return normaliseSemanticText(value)
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.split(" ")
|
||||
.filter((token) => token.length > 2 && !stopWords.has(token));
|
||||
}
|
||||
|
||||
function semanticOverlapRatio(sourceText, candidateText) {
|
||||
const source = new Set(semanticContentTokens(sourceText));
|
||||
const candidate = new Set(semanticContentTokens(candidateText));
|
||||
|
||||
if (candidate.size === 0) return 1;
|
||||
|
||||
const overlap = [...candidate].filter((token) => source.has(token)).length;
|
||||
return overlap / candidate.size;
|
||||
}
|
||||
|
||||
function rawAnswerSupportsUnclassifiedMeaning(answer, userSupportedMeaning) {
|
||||
const overlapRatio = semanticOverlapRatio(answer, userSupportedMeaning);
|
||||
const overlappingTokens = semanticContentTokens(userSupportedMeaning).filter(
|
||||
(token) => semanticContentTokens(answer).includes(token),
|
||||
).length;
|
||||
|
||||
return overlapRatio >= 0.4 || overlappingTokens >= 3;
|
||||
}
|
||||
|
||||
function hasConditionalQualification(text) {
|
||||
const value = normaliseSemanticText(text);
|
||||
return (
|
||||
@@ -2893,6 +2957,23 @@ function validateAnswerMeaningCompatibilityWithRawAnswer({ answer, proposal }) {
|
||||
}
|
||||
}
|
||||
|
||||
if (rawAnswerProfile.category === "other") {
|
||||
if (supportedMeaningProfile.category !== "other") {
|
||||
errors.push(
|
||||
"answerMeaning.userSupportedMeaning introduces a stronger reasoning category than the raw answer establishes.",
|
||||
);
|
||||
} else if (
|
||||
!rawAnswerSupportsUnclassifiedMeaning(
|
||||
answer,
|
||||
proposal.answerMeaning.userSupportedMeaning,
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
"answerMeaning.userSupportedMeaning introduces unsupported meaning beyond what the raw answer itself states.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -2964,9 +3045,13 @@ function validateAnswerMeaningAlignment(proposal) {
|
||||
}
|
||||
|
||||
if (supportCategory === "other") {
|
||||
if (resolved || containsConstraintBoundaryLanguage(proposalText)) {
|
||||
if (
|
||||
resolved &&
|
||||
containsConstraintBoundaryLanguage(proposalText) &&
|
||||
!containsConstraintBoundaryLanguage(meaningText)
|
||||
) {
|
||||
errors.push(
|
||||
"Proposal cannot resolve or strengthen answerMeaning that does not clearly establish one of the protected reasoning categories.",
|
||||
"Proposal cannot resolve beyond an unclassified answer by introducing an unsupported constraint or preference/trade-off distinction.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1475,6 +1475,125 @@ describe("applyValidatedProposal", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("57A regression: a legitimate decision-advancing answer classified as other may resolve its intended unknown", () => {
|
||||
const graph = makeCommercialUpdateFixture();
|
||||
const parentId = graph.activeUnknownNodeId;
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
answer:
|
||||
"We're looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.",
|
||||
previousQuestion:
|
||||
"What problem would this need to solve to justify continuing development?",
|
||||
proposal: {
|
||||
addedNodes: [
|
||||
makeNode({
|
||||
id: "n-other-people-problem",
|
||||
label: "Whether other people experience this problem",
|
||||
description:
|
||||
"Need to know whether other people experience this problem, because that must be established before deciding whether the problem is broadly important.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId,
|
||||
}),
|
||||
],
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: parentId,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue:
|
||||
"A concrete problem exists: reducing office overhead by roughly £2M annually is the main outcome being sought.",
|
||||
reason:
|
||||
"The answer directly states the practical decision-driving outcome the work is intended to achieve.",
|
||||
},
|
||||
],
|
||||
addedEdges: [
|
||||
makeEdge({
|
||||
id: "e-commercial-parent-other-people-problem",
|
||||
fromNodeId: parentId,
|
||||
toNodeId: "n-other-people-problem",
|
||||
relationship: "depends_on",
|
||||
confidence: "medium",
|
||||
description:
|
||||
"After establishing the concrete problem in the current context, the next unknown is whether it also exists for other people.",
|
||||
}),
|
||||
],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [parentId],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: {
|
||||
nodeId: "n-other-people-problem",
|
||||
question:
|
||||
"What makes you think other people experience this problem too?",
|
||||
reason:
|
||||
"The answer establishes the problem in this case; the next consequential unknown is whether it generalises beyond this case.",
|
||||
},
|
||||
answerMeaning: {
|
||||
userSupportedMeaning:
|
||||
"The main reason for considering this is cost reduction, specifically about £2M in annual office-overhead savings.",
|
||||
possibleInference:
|
||||
"If those savings are real and recurring, that could make the problem commercially important.",
|
||||
supportCategory: "other",
|
||||
resolutionGuidance: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(parentId);
|
||||
expect(result.stage).toBeUndefined();
|
||||
});
|
||||
|
||||
it("unsafe unclassified answer: other is not automatically trusted when the proposal adds stronger unsupported meaning", () => {
|
||||
const graph = makeCommercialUpdateFixture();
|
||||
const parentId = graph.activeUnknownNodeId;
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
answer:
|
||||
"We're looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.",
|
||||
previousQuestion:
|
||||
"What problem would this need to solve to justify continuing development?",
|
||||
proposal: {
|
||||
addedNodes: [],
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: parentId,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue:
|
||||
"There is proven broad market demand for a product that delivers these savings.",
|
||||
reason:
|
||||
"The answer was treated as establishing commercial demand rather than only the user's own cost-reduction goal.",
|
||||
},
|
||||
],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [parentId],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: null,
|
||||
answerMeaning: {
|
||||
userSupportedMeaning:
|
||||
"There is proven broad market demand for a product that delivers these savings.",
|
||||
possibleInference:
|
||||
"The savings target could imply broader applicability if others share similar overhead pressures.",
|
||||
supportCategory: "other",
|
||||
resolutionGuidance: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.stage).toBe("proposal_compatibility");
|
||||
expect(result.errors.join(" ")).toContain(
|
||||
"unsupported meaning beyond what the raw answer itself states",
|
||||
);
|
||||
});
|
||||
|
||||
it("Regression C: rejects unresolved uncertainty being treated as resolved", () => {
|
||||
const { graph, riskUnknownId } = makeRiskClarificationFixture();
|
||||
|
||||
@@ -1690,7 +1809,7 @@ describe("applyValidatedProposal", () => {
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.stage).toBe("proposal_compatibility");
|
||||
expect(result.errors.join(" ")).toContain(
|
||||
"does not clearly establish one of the protected reasoning categories",
|
||||
"unsupported constraint or preference/trade-off distinction",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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");
|
||||
}
|
||||
|
||||
// Fixed case from Experiment 57A — the affirmative answer that exposed the defect
|
||||
const UNRESOLVED_QUESTION = "Whether cost reduction is a genuine reason supporting the relocation decision.";
|
||||
const USER_ANSWER = "We're looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.";
|
||||
|
||||
// Pre-written human semantic reference (authoritative)
|
||||
const HUMAN_REFERENCE = `The answer establishes that cost reduction is a genuine stated reason supporting consideration of the relocation, with approximately £2M annual office-overhead savings cited by the user. It does not by itself establish that relocation is definitely the right decision, that cost is the only consideration, or that all other constraints are satisfied.`;
|
||||
|
||||
// Live call
|
||||
async function callUnclassifiedAffirmative() {
|
||||
const instruction = `You are validating whether a model-produced interpretation of a raw user answer stays grounded in what was actually stated.
|
||||
|
||||
Pre-written human semantic reference (authoritative): ${HUMAN_REFERENCE}
|
||||
|
||||
Context / unresolved question: ${UNRESOLVED_QUESTION}
|
||||
|
||||
User's answer: "${USER_ANSWER}"
|
||||
|
||||
Return valid JSON only in this shape:
|
||||
{
|
||||
"userSupportedMeaning": "short statement of what the user actually established",
|
||||
"possibleInference": "short statement or null"
|
||||
}
|
||||
|
||||
Rules:
|
||||
- userSupportedMeaning must stay within what the raw answer directly establishes.
|
||||
- possibleInference captures a plausible implication that goes beyond the raw answer (or null).
|
||||
- Do not strengthen the meaning into a final decision, hard constraint, preference judgment, or approval.`;
|
||||
|
||||
const messages = [
|
||||
{ role: "system", content: instruction.trim() },
|
||||
{ role: "user", content: USER_ANSWER },
|
||||
];
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
// Test suite — single live call, Experiment 57B
|
||||
describe("Experiment 57B - Grounded unclassified affirmative answer", () => {
|
||||
let result;
|
||||
let durationMs;
|
||||
|
||||
it("57B: one live call — unclassified affirmative answer stays grounded", async () => {
|
||||
const start = Date.now();
|
||||
result = await callUnclassifiedAffirmative();
|
||||
durationMs = Date.now() - start;
|
||||
|
||||
// Minimal structural assertions
|
||||
expect(result.userSupportedMeaning).toBeDefined();
|
||||
expect(typeof result.userSupportedMeaning).toBe("string");
|
||||
expect(result.userSupportedMeaning.trim().length).toBeGreaterThan(0);
|
||||
if (result.possibleInference !== null) {
|
||||
expect(typeof result.possibleInference).toBe("string");
|
||||
}
|
||||
}, 120000);
|
||||
|
||||
it("57B: raw result report", () => {
|
||||
console.log("\n========== Experiment 57B Results ==========");
|
||||
console.log(`\nContext / unresolved question: ${UNRESOLVED_QUESTION}`);
|
||||
console.log(`User answer: "${USER_ANSWER}"`);
|
||||
console.log(`Pre-written human reference:\n${HUMAN_REFERENCE}`);
|
||||
console.log(`\nRaw structured response:`);
|
||||
console.log(` userSupportedMeaning: "${result?.userSupportedMeaning}"`);
|
||||
console.log(` possibleInference: ${result?.possibleInference ?? "null"}`);
|
||||
console.log(`\nDuration: ${durationMs}ms`);
|
||||
console.log("\n========== End of Experiment 57B ==========\n");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user