fix(graph): preserve grammar for question-like unknown labels
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formulateQuestion } from "@/lib/graph/question-formulator.js";
|
||||
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||
|
||||
function makeGraphFor(node, extra = {}) {
|
||||
return makeGraph({
|
||||
centralStatement: extra.centralStatement || "Decision context",
|
||||
nodes: [node, ...(extra.nodes || [])],
|
||||
edges: extra.edges || [],
|
||||
activeUnknownNodeId: node.id,
|
||||
resolvedNodeIds: extra.resolvedNodeIds || [],
|
||||
currentSummary: "Test summary",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Experiment 58A.4 — question formulation for pre-question-shaped labels
|
||||
*
|
||||
* Defect: when an unknown's label is already question or clause shaped,
|
||||
* the engine interpolates it raw into a generic template frame, producing
|
||||
* ungrammatical output such as "What would clarify are the projected office
|
||||
* savings from relocation realistic in this situation?".
|
||||
*
|
||||
* The fix must generalise structurally to ANY interrogative/clause label,
|
||||
* not just the specific patterns seen so far.
|
||||
*/
|
||||
describe("question formulation — pre-question labels (58A.4)", () => {
|
||||
|
||||
// ─── Helper: run formulateQuestion on an unknown and extract the question text ───
|
||||
function questionFor(label, extra = {}) {
|
||||
const node = makeNode({
|
||||
id: "n-test",
|
||||
label,
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
...extra,
|
||||
});
|
||||
const result = formulateQuestion({
|
||||
node,
|
||||
graph: makeGraphFor(node),
|
||||
});
|
||||
return result.question;
|
||||
}
|
||||
|
||||
// ─── Test 1: the exact defect from 58A.3 — wh-question label ───
|
||||
it("produces a grammatical question when label is a wh-question", () => {
|
||||
const q = questionFor(
|
||||
"Are the projected office savings from relocation realistic?",
|
||||
);
|
||||
|
||||
// Must NOT contain template injection artefact
|
||||
expect(q).not.toMatch(/what would clarify are .* in this situation/i);
|
||||
// Should be grammatical — either a standalone wh-question or wrapped properly
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
// The core meaning must survive
|
||||
expect(q.toLowerCase()).toMatch(/office savings|relocation/);
|
||||
});
|
||||
|
||||
// ─── Test 2: clause-shaped statement label (no question mark) ───
|
||||
it("produces a grammatical question when label is a declarative clause", () => {
|
||||
const q = questionFor(
|
||||
"The projected office savings from relocation are unrealistic",
|
||||
);
|
||||
|
||||
// Should be grammatical question form
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).toMatch(/office savings|relocation/);
|
||||
});
|
||||
|
||||
// ─── Test 3: yes/no question label ───
|
||||
it("produces a grammatical question when label is a yes/no question", () => {
|
||||
const q = questionFor("Is the budget sufficient for this project?");
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).toMatch(/budget|sufficient/);
|
||||
});
|
||||
|
||||
// ─── Test 4: whether-clause label ───
|
||||
it("produces a grammatical question when label starts with 'whether'", () => {
|
||||
const q = questionFor(
|
||||
"Whether the new pricing strategy will increase revenue",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).toMatch(/pricing|revenue/);
|
||||
});
|
||||
|
||||
// ─── Test 5: what-question label ───
|
||||
it("produces a grammatical question when label is a what-question", () => {
|
||||
const q = questionFor(
|
||||
"What are the key risks of this project?",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
// Should not double-wrap with "what would clarify"
|
||||
expect(q.toLowerCase()).not.toMatch(/what would clarify what/i);
|
||||
expect(q.toLowerCase()).toMatch(/key risks/);
|
||||
});
|
||||
|
||||
// ─── Test 6: how-question label ───
|
||||
it("produces a grammatical question when label is a how-question", () => {
|
||||
const q = questionFor(
|
||||
"How do we measure success for this initiative?",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).not.toMatch(/what would clarify how/i);
|
||||
expect(q.toLowerCase()).toMatch(/measure success/);
|
||||
});
|
||||
|
||||
// ─── Test 7: why-question label ───
|
||||
it("produces a grammatical question when label is a why-question", () => {
|
||||
const q = questionFor(
|
||||
"Why did the previous quarter underperform?",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).not.toMatch(/what would clarify why/i);
|
||||
expect(q.toLowerCase()).toMatch(/quarter|underperform/);
|
||||
});
|
||||
|
||||
// ─── Test 8: who-question label ───
|
||||
it("produces a grammatical question when label is a who-question", () => {
|
||||
const q = questionFor(
|
||||
"Who is responsible for the client onboarding process?",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).not.toMatch(/what would clarify who/i);
|
||||
expect(q.toLowerCase()).toMatch(/responsible|client/);
|
||||
});
|
||||
|
||||
// ─── Test 9: when-question label ───
|
||||
it("produces a grammatical question when label is a when-question", () => {
|
||||
const q = questionFor(
|
||||
"When does the current contract expire?",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).not.toMatch(/what would clarify when/i);
|
||||
expect(q.toLowerCase()).toMatch(/contract|expire/);
|
||||
});
|
||||
|
||||
// ─── Test 10: where-question label ───
|
||||
it("produces a grammatical question when label is a where-question", () => {
|
||||
const q = questionFor(
|
||||
"Where should the new warehouse be located?",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).not.toMatch(/what would clarify where/i);
|
||||
expect(q.toLowerCase()).toMatch(/warehouse|located/);
|
||||
});
|
||||
|
||||
// ─── Test 11: non-question labels should NOT be affected ───
|
||||
it("still applies template frames to declarative labels", () => {
|
||||
const q = questionFor(
|
||||
"Office lease exit penalty amount",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
// For a normal declarative label, the template frame should apply
|
||||
// The key is that it's grammatical, not that it uses a specific frame
|
||||
expect(q.toLowerCase()).toMatch(/lease|penalty/);
|
||||
});
|
||||
|
||||
// ─── Test 12: evidence-fallback path with pre-question label ───
|
||||
it("produces grammatical output via evidence fallback for question labels", () => {
|
||||
const q = questionFor(
|
||||
"Are the projected office savings from relocation realistic?",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
// The output should be a single coherent question
|
||||
const questionMarks = (q.match(/\?/g) || []).length;
|
||||
expect(questionMarks).toBe(1);
|
||||
});
|
||||
|
||||
// ─── Test 13: clause without question mark — statement form ───
|
||||
it("handles label shaped like a that-clause", () => {
|
||||
const q = questionFor(
|
||||
"That the migration will reduce operational costs by at least 20%",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).toMatch(/migration|cost/);
|
||||
});
|
||||
|
||||
// ─── Test 14: complex interrogative with embedded clause ───
|
||||
it("handles label shaped like an indirect question", () => {
|
||||
const q = questionFor(
|
||||
"How much funding we need to complete the product launch",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).toMatch(/funding|launch/);
|
||||
});
|
||||
|
||||
// ─── Test 15: the exact output from 58A.2 template — should not recur ───
|
||||
it("does not produce the 58A.2 malformed question", () => {
|
||||
const q = questionFor(
|
||||
"Are the projected office savings from relocation realistic?",
|
||||
);
|
||||
|
||||
expect(q).not.toBe(
|
||||
"What was the comparable state before are the projected office savings from relocation realistic?",
|
||||
);
|
||||
expect(q).not.toMatch(/before .* are .* realistic/);
|
||||
});
|
||||
|
||||
// ─── Test 16: short yes/no question label ───
|
||||
it("handles very short interrogative labels", () => {
|
||||
const q = questionFor("Is this the right approach?");
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).toMatch(/right|approach/);
|
||||
});
|
||||
|
||||
// ─── Test 17: label with trailing punctuation already stripped in extractMeaning ───
|
||||
it("handles label that is interrogative but loses its question mark via stripTrailingPunctuation", () => {
|
||||
// This tests the internal pipeline: extractMeaning strips trailing ?,
|
||||
// then the meaning feeds into buildNeutralClarificationQuestion or evidence fallback
|
||||
const q = questionFor(
|
||||
"What is the total addressable market for this segment?",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
const questionMarks = (q.match(/\?/g) || []).length;
|
||||
expect(questionMarks).toBe(1);
|
||||
});
|
||||
|
||||
// ─── Test 18: long complex interrogative label ───
|
||||
it("handles very long interrogative labels without template doubling", () => {
|
||||
const q = questionFor(
|
||||
"Are the current employee satisfaction scores sufficient to justify continuing the remote work policy?",
|
||||
);
|
||||
|
||||
expect(q.endsWith("?")).toBe(true);
|
||||
expect(q.toLowerCase()).not.toMatch(/what would clarify are .* sufficient/i);
|
||||
expect(q.toLowerCase()).toMatch(/satisfaction|remote/);
|
||||
});
|
||||
|
||||
// ─── Test 19: interrogative label in definition reasoning pattern ───
|
||||
it("handles interrogative labels through definition path", () => {
|
||||
const node = makeNode({
|
||||
id: "n-def",
|
||||
label: "What does 'customer' mean in this context?",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
const result = formulateQuestion({
|
||||
node,
|
||||
graph: makeGraphFor(node),
|
||||
});
|
||||
|
||||
expect(result.question.endsWith("?")).toBe(true);
|
||||
const questionMarks = (result.question.match(/\?/g) || []).length;
|
||||
expect(questionMarks).toBe(1);
|
||||
});
|
||||
|
||||
// ─── Test 20: interrogative label in evidence gathering pattern ───
|
||||
it("handles interrogative labels through evidence path", () => {
|
||||
const node = makeNode({
|
||||
id: "n-evidence",
|
||||
label: "What additional data do we need to validate the hypothesis?",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
const result = formulateQuestion({
|
||||
node,
|
||||
graph: makeGraphFor(node),
|
||||
});
|
||||
|
||||
expect(result.question.endsWith("?")).toBe(true);
|
||||
const questionMarks = (result.question.match(/\?/g) || []).length;
|
||||
expect(questionMarks).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user