feat: add 'option' node kind and 'contained_in' edge — 60A.3
Implementation of Candidate B (unknown+option) from decision architecture design in 60A.2. Adds two new primitives to the situation graph: Schema (lib/graph/schema.js): - SituationKind.option — a choice available within a decision context - SituationRelationship.contained_in — links option → its parent unknown context Prompt rules (lib/graph/prompt-builder.js): - Section added: Decision Option Structure Rules with 5 numbered instructions governing when/how to create option nodes, link them via contained_in, attach consequences to specific options, and handle do-nothing alternatives. Explicitly forbids alternative_to edges and is_baseline/is_default flags. Tests (446 new lines): - schema.test.js: +300 — enum completeness updates, option kind validation, contained_in edge validation, native two-option graph fixture (~25 new tests) - prompt-builder.test.js: +133 — focused rules verification for all 5 rule points, negative checks (no relocation/savings/example-specific wording, no alternative_to requirement, baseline flag prohibition context) No production code paths affected beyond the two enum additions; existing node and edge kinds remain unchanged. No Ollama calls, no live API calls.
This commit is contained in:
@@ -79,13 +79,13 @@ describe("buildGraphUpdatePrompt", () => {
|
||||
it("lists enum values", () => {
|
||||
const prompt = buildGraphUpdatePrompt(makeContext());
|
||||
expect(prompt).toContain(
|
||||
"observation | reported_claim | metric | state | transition | relationship | assumption | unknown | conclusion",
|
||||
"observation | reported_claim | metric | state | transition | relationship | assumption | unknown | conclusion | option",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"known | unknown | provisional | supported | weakened | contradicted | resolved",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"supports | weakens | contradicts | depends_on | causes | may_cause | measures | compares_with | updates | other",
|
||||
"supports | weakens | contradicts | depends_on | causes | may_cause | measures | compares_with | updates | contained_in | other",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -635,3 +635,132 @@ describe("buildGraphUpdatePrompt — 57J.59 selected-question contract alignment
|
||||
expect(prompt).toContain("genuinely new concepts");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Experiment 60A.3 — Decision Option Prompt Rules ──────────────
|
||||
|
||||
describe("60A.3 decision option prompt rules", () => {
|
||||
const prompt = buildGraphUpdatePrompt(makeContext());
|
||||
const section = prompt.split("## Decision Option Structure Rules")[1].split(
|
||||
"## Contract:",
|
||||
)[0];
|
||||
|
||||
it("rule: when user presents mutually exclusive candidate actions, create one option node per alternative", () => {
|
||||
expect(section).toContain("mutually exclusive");
|
||||
expect(section).toContain("option");
|
||||
expect(section).toContain("each");
|
||||
});
|
||||
|
||||
it("rule: keep one unknown as decision context; do not duplicate into every option", () => {
|
||||
expect(section).toContain("unknown");
|
||||
expect(section).toMatch(/decision[-\s]context/);
|
||||
});
|
||||
|
||||
it("rule: link each option to decision-context unknown using contained_in", () => {
|
||||
expect(section).toContain("contained_in");
|
||||
expect(section).toContain("option");
|
||||
expect(section).toContain("unknown");
|
||||
});
|
||||
|
||||
it("rule: attach consequences to the specific option they belong to", () => {
|
||||
expect(section).toContain("specific option");
|
||||
expect(section).toContain("consequence");
|
||||
});
|
||||
|
||||
it("rule text uses double-quoted enum values (no backticks in source)", () => {
|
||||
// The rule section must use "kind" style quoting that works inside a template literal.
|
||||
expect(section).toContain('kind "unknown"');
|
||||
expect(section).toContain('kind "option"');
|
||||
expect(section).toContain('"contained_in"');
|
||||
});
|
||||
|
||||
it("rule explicitly says do not add an alternative_to edge", () => {
|
||||
expect(section).toContain("alternative_to");
|
||||
const lines = section.split("\n").filter((l) => l.includes("alternative_to"));
|
||||
const negLine = lines.find(
|
||||
(l) =>
|
||||
l.toLowerCase().includes("do not") ||
|
||||
l.toLowerCase().includes("not add") ||
|
||||
l.toLowerCase().includes("dont"),
|
||||
);
|
||||
expect(negLine).toBeDefined();
|
||||
});
|
||||
|
||||
it("rule does not require is_baseline / is_default / is_status_quo fields", () => {
|
||||
expect(section).toContain('is_baseline');
|
||||
expect(section).toContain('is_default');
|
||||
expect(section).toContain('is_status_quo');
|
||||
// Verify these appear only in a negation context:
|
||||
const negLines = section.split("\n").filter(
|
||||
(l) => l.includes("is_baseline") || l.includes("is_default"),
|
||||
);
|
||||
expect(negLines.some((l) => l.toLowerCase().includes("do not introduce"))).toBe(true);
|
||||
});
|
||||
|
||||
it("rule explicitly forbids alternative_to edge", () => {
|
||||
const rulesSection = prompt.split("## Decision Option Structure Rules")[1];
|
||||
expect(rulesSection).toContain("alternative_to");
|
||||
// Should say something about not adding it (e.g., "Do not add")
|
||||
const lines = rulesSection.split("\n").filter((l) => l.includes("alternative_to"));
|
||||
const altLine = lines.find((l) =>
|
||||
l.toLowerCase().includes("do not") || l.toLowerCase().includes("not"),
|
||||
);
|
||||
expect(altLine).toBeDefined();
|
||||
});
|
||||
|
||||
it("rule: do-nothing / stay-put is an option when genuinely one of the alternatives", () => {
|
||||
expect(section).toContain("do-nothing") || section.includes("do nothing");
|
||||
// Verify at least one variant is present
|
||||
const hasDoNothing = section.includes("do-nothing") || section.includes("do nothing");
|
||||
expect(hasDoNothing).toBe(true);
|
||||
});
|
||||
|
||||
// Negative checks
|
||||
it("no relocation/savings-specific wording introduced", () => {
|
||||
expect(prompt).not.toContain("relocation");
|
||||
expect(prompt).not.toContain("savings");
|
||||
expect(prompt).not.toContain("engineer");
|
||||
expect(prompt).not.toContain("£2m");
|
||||
expect(prompt).not.toContain("£2M");
|
||||
});
|
||||
|
||||
it("no new decision node kind requirement in prompt", () => {
|
||||
const rulesSection = prompt.split("## Decision Option Structure Rules")[1];
|
||||
// The section must NOT instruct adding a new "decision" node kind — but may mention "decision" as context.
|
||||
expect(rulesSection).toContain('kind "unknown"');
|
||||
expect(rulesSection).toContain('"option"');
|
||||
// Verify the prohibition is against adding a separate decision node:
|
||||
const lines = rulesSection.split("\n").filter((l) => l.includes("decision") && l.toLowerCase().includes("add"));
|
||||
expect(lines.some((l) => l.toLowerCase().includes("do not"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not require alternative_to — only forbids it", () => {
|
||||
const rulesSection = prompt.split("## Decision Option Structure Rules")[1];
|
||||
// The section must mention alternative_to (to forbid it) but not require it:
|
||||
expect(rulesSection).toContain("alternative_to");
|
||||
// Verify the mention is in a prohibition context, not an encouragement context:
|
||||
const lines = rulesSection.split("\n").filter((l) => l.includes("alternative_to"));
|
||||
expect(lines.some((l) => l.toLowerCase().includes("do not"))).toBe(true);
|
||||
});
|
||||
|
||||
it("baseline flag words appear only in prohibition context", () => {
|
||||
const rulesSection = prompt.split("## Decision Option Structure Rules")[1];
|
||||
// The strings is_baseline, is_default, is_status_quo are mentioned to forbid them:
|
||||
["is_baseline", "is_default", "is_status_quo"].forEach((flag) => {
|
||||
expect(rulesSection).toContain(flag);
|
||||
// Verify each appears in a negation context:
|
||||
const lines = rulesSection.split("\n").filter((l) => l.includes(flag));
|
||||
expect(lines.some((l) => l.toLowerCase().includes("do not introduce"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("rule: explicitly forbids alternative_to edge", () => {
|
||||
const rulesSection = prompt.split("## Decision Option Structure Rules")[1];
|
||||
expect(rulesSection).toContain("alternative_to");
|
||||
// Should say something about not adding it (e.g., "Do not add")
|
||||
const lines = rulesSection.split("\n").filter((l) => l.includes("alternative_to"));
|
||||
const altLine = lines.find((l) =>
|
||||
l.toLowerCase().includes("do not") || l.toLowerCase().includes("not"),
|
||||
);
|
||||
expect(altLine).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user