test: verify ambiguity handling across domains
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# v0.6 Ambiguity Generalisation
|
||||
|
||||
## Hypothesis
|
||||
|
||||
If the selector truly handles unjustified contradiction ties generically, it should return ambiguity across multiple domains without preferring one explanation by wording alone.
|
||||
|
||||
## Scenarios
|
||||
|
||||
1. Revenue increased by 18%, but cash in the bank fell over the same period.
|
||||
2. Customer satisfaction scores increased, but complaints also increased.
|
||||
3. Average delivery time decreased by 25%, but order cancellations increased.
|
||||
4. Website traffic doubled, but sales remained unchanged.
|
||||
5. Production output increased by 30%, but quality defects also increased.
|
||||
|
||||
## Observed behaviour
|
||||
|
||||
All five fixtures produced the same pattern:
|
||||
|
||||
- candidate count: 2
|
||||
- selector status: `ambiguous`
|
||||
- tie reason: `No justified distinction between leading unknowns.`
|
||||
- no explanation was favoured
|
||||
- one broad investigation question was produced from the central contradiction
|
||||
- neutral label renaming did not collapse ambiguity into a winner
|
||||
|
||||
## Repeated failure patterns
|
||||
|
||||
None observed across two or more scenarios.
|
||||
|
||||
The current ambiguity handling generalised cleanly across the five contradiction fixtures.
|
||||
|
||||
## Corrections
|
||||
|
||||
No production correction was required in this task.
|
||||
|
||||
## Lessons learned
|
||||
|
||||
- The current ambiguity path appears domain-agnostic when structure and semantic weights remain intentionally non-discriminating.
|
||||
- Central-statement-based tie questions are broad enough to avoid prematurely backing one branch.
|
||||
- The most useful regression signal is whether ambiguity survives neutral relabelling, not whether one label sorts ahead of another in display order.
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||
|
||||
function buildAmbiguityFixture({
|
||||
key,
|
||||
scenario,
|
||||
summaryLabel,
|
||||
contradictionLabel,
|
||||
observationLabels,
|
||||
unknownLabels,
|
||||
disallowedQuestionTerms,
|
||||
}) {
|
||||
const summary = makeNode({
|
||||
id: `${key}-summary`,
|
||||
label: summaryLabel,
|
||||
description: "Summary of the situation from the scenario text",
|
||||
kind: "state",
|
||||
status: "provisional",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
const contradiction = makeNode({
|
||||
id: `${key}-contradiction`,
|
||||
label: contradictionLabel,
|
||||
description: contradictionLabel,
|
||||
kind: "relationship",
|
||||
status: "supported",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
const observations = observationLabels.map((label, index) =>
|
||||
makeNode({
|
||||
id: `${key}-obs-${index + 1}`,
|
||||
label,
|
||||
description: label,
|
||||
kind: "observation",
|
||||
status: "supported",
|
||||
confidence: "high",
|
||||
}),
|
||||
);
|
||||
|
||||
const unknowns = unknownLabels.map((label, index) =>
|
||||
makeNode({
|
||||
id: `${key}-unknown-${index + 1}`,
|
||||
label,
|
||||
description: label,
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
}),
|
||||
);
|
||||
|
||||
const edges = [
|
||||
...observations.map((node) =>
|
||||
makeEdge({
|
||||
id: `${node.id}-supports-summary`,
|
||||
fromNodeId: node.id,
|
||||
toNodeId: summary.id,
|
||||
relationship: "supports",
|
||||
description: `${node.label} supports the summary.`,
|
||||
}),
|
||||
),
|
||||
...unknowns.map((node) =>
|
||||
makeEdge({
|
||||
id: `${node.id}-depends-summary`,
|
||||
fromNodeId: node.id,
|
||||
toNodeId: summary.id,
|
||||
relationship: "depends_on",
|
||||
description: `${node.label} is an unresolved factor for this situation.`,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
return {
|
||||
key,
|
||||
scenario,
|
||||
disallowedQuestionTerms,
|
||||
graph: makeGraph({
|
||||
centralStatement: scenario,
|
||||
nodes: [summary, contradiction, ...observations, ...unknowns],
|
||||
edges,
|
||||
activeUnknownNodeId: null,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: `Ambiguity fixture for ${key}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export const ambiguityGeneralisationFixtures = [
|
||||
buildAmbiguityFixture({
|
||||
key: "revenue-cash",
|
||||
scenario:
|
||||
"Revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||
summaryLabel: "Revenue rose while cash fell",
|
||||
contradictionLabel:
|
||||
"Contradiction between revenue improvement and lower cash reserves.",
|
||||
observationLabels: [
|
||||
"Revenue increased by 18%.",
|
||||
"Cash in the bank decreased over the same period.",
|
||||
],
|
||||
unknownLabels: [
|
||||
"Possible explanation for the contradiction from one side of the situation.",
|
||||
"Possible explanation for the contradiction from another side of the situation.",
|
||||
],
|
||||
disallowedQuestionTerms: [
|
||||
"accounts receivable",
|
||||
"capex",
|
||||
"debt repayments",
|
||||
"working capital",
|
||||
],
|
||||
}),
|
||||
buildAmbiguityFixture({
|
||||
key: "satisfaction-complaints",
|
||||
scenario:
|
||||
"Customer satisfaction scores increased, but complaints also increased.",
|
||||
summaryLabel: "Satisfaction scores rose while complaints also rose",
|
||||
contradictionLabel:
|
||||
"Contradiction between higher satisfaction scores and higher complaint volume.",
|
||||
observationLabels: [
|
||||
"Customer satisfaction scores increased.",
|
||||
"Complaints increased.",
|
||||
],
|
||||
unknownLabels: [
|
||||
"Possible explanation for why the positive signal and negative signal moved together.",
|
||||
"Another possible explanation for why the positive signal and negative signal moved together.",
|
||||
],
|
||||
disallowedQuestionTerms: [
|
||||
"net promoter",
|
||||
"ticket backlog",
|
||||
"call deflection",
|
||||
"support queue",
|
||||
],
|
||||
}),
|
||||
buildAmbiguityFixture({
|
||||
key: "delivery-cancellations",
|
||||
scenario:
|
||||
"Average delivery time decreased by 25%, but order cancellations increased.",
|
||||
summaryLabel: "Delivery became faster while cancellations increased",
|
||||
contradictionLabel:
|
||||
"Contradiction between faster delivery and more order cancellations.",
|
||||
observationLabels: [
|
||||
"Average delivery time decreased by 25%.",
|
||||
"Order cancellations increased.",
|
||||
],
|
||||
unknownLabels: [
|
||||
"Possible explanation for why the faster result did not reduce the negative result.",
|
||||
"Another possible explanation for why the faster result did not reduce the negative result.",
|
||||
],
|
||||
disallowedQuestionTerms: [
|
||||
"fulfilment",
|
||||
"last mile",
|
||||
"warehouse",
|
||||
"routing",
|
||||
],
|
||||
}),
|
||||
buildAmbiguityFixture({
|
||||
key: "traffic-sales",
|
||||
scenario: "Website traffic doubled, but sales remained unchanged.",
|
||||
summaryLabel: "Website traffic doubled while sales stayed flat",
|
||||
contradictionLabel:
|
||||
"Contradiction between much higher traffic and unchanged sales.",
|
||||
observationLabels: [
|
||||
"Website traffic doubled.",
|
||||
"Sales remained unchanged.",
|
||||
],
|
||||
unknownLabels: [
|
||||
"Possible explanation for why the stronger signal did not change the outcome.",
|
||||
"Another possible explanation for why the stronger signal did not change the outcome.",
|
||||
],
|
||||
disallowedQuestionTerms: [
|
||||
"conversion funnel",
|
||||
"campaign attribution",
|
||||
"landing page",
|
||||
"checkout flow",
|
||||
],
|
||||
}),
|
||||
buildAmbiguityFixture({
|
||||
key: "output-defects",
|
||||
scenario:
|
||||
"Production output increased by 30%, but quality defects also increased.",
|
||||
summaryLabel: "Production output rose while defects also rose",
|
||||
contradictionLabel:
|
||||
"Contradiction between higher output and more quality defects.",
|
||||
observationLabels: [
|
||||
"Production output increased by 30%.",
|
||||
"Quality defects increased.",
|
||||
],
|
||||
unknownLabels: [
|
||||
"Possible explanation for why the gain came with a worsening result.",
|
||||
"Another possible explanation for why the gain came with a worsening result.",
|
||||
],
|
||||
disallowedQuestionTerms: [
|
||||
"scrap rate",
|
||||
"throughput",
|
||||
"yield",
|
||||
"root cause",
|
||||
],
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
formulateQuestion,
|
||||
formulateTieResolutionQuestion,
|
||||
} from "@/lib/graph/question-formulator.js";
|
||||
import {
|
||||
explainUnknownSelection,
|
||||
selectActiveUnknownCandidate,
|
||||
} from "@/lib/graph/utils.js";
|
||||
import { ambiguityGeneralisationFixtures } from "@/tests/fixtures/ambiguity-generalisation.js";
|
||||
|
||||
function neutraliseUnknownLabels(graph) {
|
||||
let counter = 0;
|
||||
return {
|
||||
...graph,
|
||||
nodes: graph.nodes.map((node) => {
|
||||
if (node.kind !== "unknown") return { ...node };
|
||||
counter += 1;
|
||||
return {
|
||||
...node,
|
||||
label: `Unknown ${String.fromCharCode(64 + counter)}`,
|
||||
description: `Unknown factor ${counter}.`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function isSingleQuestion(question) {
|
||||
return (question.match(/\?/g) || []).length === 1;
|
||||
}
|
||||
|
||||
describe("ambiguity generalisation", () => {
|
||||
it("preserves ambiguity across contradiction scenarios without favouring one explanation", () => {
|
||||
const summary = ambiguityGeneralisationFixtures.map((fixture) => {
|
||||
const explanation = explainUnknownSelection(fixture.graph, []);
|
||||
const selection = selectActiveUnknownCandidate(fixture.graph, []);
|
||||
const neutralExplanation = explainUnknownSelection(
|
||||
neutraliseUnknownLabels(fixture.graph),
|
||||
[],
|
||||
);
|
||||
const tieQuestion = formulateTieResolutionQuestion({
|
||||
graph: fixture.graph,
|
||||
});
|
||||
const representativeUnknown = fixture.graph.nodes.find(
|
||||
(node) => node.kind === "unknown",
|
||||
);
|
||||
const fallbackQuestion = formulateQuestion({
|
||||
node: representativeUnknown,
|
||||
graph: fixture.graph,
|
||||
});
|
||||
|
||||
const lowerQuestion = tieQuestion.question.toLowerCase();
|
||||
for (const term of fixture.disallowedQuestionTerms) {
|
||||
expect(lowerQuestion).not.toContain(term.toLowerCase());
|
||||
}
|
||||
|
||||
expect(explanation.status).toBe("ambiguous");
|
||||
expect(selection.status).toBe("ambiguous");
|
||||
expect(selection.selectedNode).toBeNull();
|
||||
expect(explanation.selectedNodeId).toBeNull();
|
||||
expect(explanation.candidates).toHaveLength(2);
|
||||
expect(explanation.summary.selectedReason).toBe(
|
||||
"No justified distinction between leading unknowns.",
|
||||
);
|
||||
expect(explanation.alphabeticalUsedAsReasoning).toBe(false);
|
||||
expect(neutralExplanation.status).toBe("ambiguous");
|
||||
expect(isSingleQuestion(tieQuestion.question)).toBe(true);
|
||||
expect(tieQuestion.question.toLowerCase()).not.toContain(" and ");
|
||||
expect(tieQuestion.question.toLowerCase()).not.toContain(" or ");
|
||||
|
||||
return {
|
||||
scenario: fixture.scenario,
|
||||
candidateCount: explanation.candidates.length,
|
||||
ambiguityStatus: explanation.status,
|
||||
tieReason: explanation.summary.selectedReason,
|
||||
investigationStrategy: tieQuestion.strategy,
|
||||
question: tieQuestion.question,
|
||||
explanationFavoured: explanation.selectedNodeId !== null,
|
||||
};
|
||||
});
|
||||
|
||||
expect(summary).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"ambiguityStatus": "ambiguous",
|
||||
"candidateCount": 2,
|
||||
"explanationFavoured": false,
|
||||
"investigationStrategy": null,
|
||||
"question": "What changed during the period that could explain why Revenue increased by 18%, but cash in the bank fell over the same period?",
|
||||
"scenario": "Revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||
"tieReason": "No justified distinction between leading unknowns.",
|
||||
},
|
||||
{
|
||||
"ambiguityStatus": "ambiguous",
|
||||
"candidateCount": 2,
|
||||
"explanationFavoured": false,
|
||||
"investigationStrategy": null,
|
||||
"question": "What changed during the period that could explain why Customer satisfaction scores increased, but complaints also increased?",
|
||||
"scenario": "Customer satisfaction scores increased, but complaints also increased.",
|
||||
"tieReason": "No justified distinction between leading unknowns.",
|
||||
},
|
||||
{
|
||||
"ambiguityStatus": "ambiguous",
|
||||
"candidateCount": 2,
|
||||
"explanationFavoured": false,
|
||||
"investigationStrategy": null,
|
||||
"question": "What changed during the period that could explain why Average delivery time decreased by 25%, but order cancellations increased?",
|
||||
"scenario": "Average delivery time decreased by 25%, but order cancellations increased.",
|
||||
"tieReason": "No justified distinction between leading unknowns.",
|
||||
},
|
||||
{
|
||||
"ambiguityStatus": "ambiguous",
|
||||
"candidateCount": 2,
|
||||
"explanationFavoured": false,
|
||||
"investigationStrategy": null,
|
||||
"question": "What changed during the period that could explain why Website traffic doubled, but sales remained unchanged?",
|
||||
"scenario": "Website traffic doubled, but sales remained unchanged.",
|
||||
"tieReason": "No justified distinction between leading unknowns.",
|
||||
},
|
||||
{
|
||||
"ambiguityStatus": "ambiguous",
|
||||
"candidateCount": 2,
|
||||
"explanationFavoured": false,
|
||||
"investigationStrategy": null,
|
||||
"question": "What changed during the period that could explain why Production output increased by 30%, but quality defects also increased?",
|
||||
"scenario": "Production output increased by 30%, but quality defects also increased.",
|
||||
"tieReason": "No justified distinction between leading unknowns.",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user