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:
@@ -134,6 +134,19 @@ The JSON object must contain exactly these top-level fields:
|
|||||||
31. If the answer explicitly states a hard constraint, state that directly in userSupportedMeaning.
|
31. If the answer explicitly states a hard constraint, state that directly in userSupportedMeaning.
|
||||||
32. Populate resolutionGuidance when the user's meaning genuinely implies must_remain_unresolved, may_resolve, or must_resolve. Keep it null only when no existing resolution state actually applies.
|
32. Populate resolutionGuidance when the user's meaning genuinely implies must_remain_unresolved, may_resolve, or must_resolve. Keep it null only when no existing resolution state actually applies.
|
||||||
|
|
||||||
|
## Decision Option Structure Rules
|
||||||
|
When the user presents mutually exclusive candidate actions for one unresolved choice:
|
||||||
|
|
||||||
|
1. Create exactly one node of kind "unknown" to carry the decision question (the existing mechanism). Do not add a separate "decision" node kind. Keep that unknown as-is or create it fresh — do not duplicate it into every option.
|
||||||
|
|
||||||
|
2. For each candidate path, create exactly one node of kind "option". The option's label names the alternative; its description states what that alternative entails.
|
||||||
|
|
||||||
|
3. Link each option to the decision-context unknown using relationship "contained_in" (edge: option → unknown). Shared membership already implies these options are alternatives of each other — do not add an "alternative_to" edge between options.
|
||||||
|
|
||||||
|
4. Attach consequences and evidence to the specific option they belong to via existing edge types ("causes", "may_cause", etc.). Each consequence's fromNodeId explicitly identifies its parent option. Do not collapse all alternatives into one generic trade-off description on a single node.
|
||||||
|
|
||||||
|
5. A do-nothing / stay-put / current-state path is an option when it is genuinely one of the alternatives — represent it with kind "option" and label it clearly. Do not introduce an "is_baseline", "is_default", or "is_status_quo" field; baseline meaning is carried by label and consequences alone in this implementation.
|
||||||
|
|
||||||
## Contract: structuralActionRequired Declaration Rule
|
## Contract: structuralActionRequired Declaration Rule
|
||||||
|
|
||||||
When answerMeaning.userSupportedMeaning is populated you MUST set structuralActionRequired to match what your proposal outputs:
|
When answerMeaning.userSupportedMeaning is populated you MUST set structuralActionRequired to match what your proposal outputs:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const SituationKind = /** @type {const} */ ({
|
|||||||
assumption: "assumption",
|
assumption: "assumption",
|
||||||
unknown: "unknown",
|
unknown: "unknown",
|
||||||
conclusion: "conclusion",
|
conclusion: "conclusion",
|
||||||
|
option: "option",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SituationStatus = /** @type {const} */ ({
|
export const SituationStatus = /** @type {const} */ ({
|
||||||
@@ -83,6 +84,7 @@ export const SituationRelationship = /** @type {const} */ ({
|
|||||||
measures: "measures",
|
measures: "measures",
|
||||||
compares_with: "compares_with",
|
compares_with: "compares_with",
|
||||||
updates: "updates",
|
updates: "updates",
|
||||||
|
contained_in: "contained_in",
|
||||||
other: "other",
|
other: "other",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -79,13 +79,13 @@ describe("buildGraphUpdatePrompt", () => {
|
|||||||
it("lists enum values", () => {
|
it("lists enum values", () => {
|
||||||
const prompt = buildGraphUpdatePrompt(makeContext());
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
||||||
expect(prompt).toContain(
|
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(
|
expect(prompt).toContain(
|
||||||
"known | unknown | provisional | supported | weakened | contradicted | resolved",
|
"known | unknown | provisional | supported | weakened | contradicted | resolved",
|
||||||
);
|
);
|
||||||
expect(prompt).toContain(
|
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");
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ describe("enum values completeness", () => {
|
|||||||
"assumption",
|
"assumption",
|
||||||
"unknown",
|
"unknown",
|
||||||
"conclusion",
|
"conclusion",
|
||||||
|
"option",
|
||||||
];
|
];
|
||||||
const actual = Object.values(SituationKind);
|
const actual = Object.values(SituationKind);
|
||||||
expect(actual).toEqual(expect.arrayContaining(expected));
|
expect(actual).toEqual(expect.arrayContaining(expected));
|
||||||
@@ -482,6 +483,7 @@ describe("enum values completeness", () => {
|
|||||||
"measures",
|
"measures",
|
||||||
"compares_with",
|
"compares_with",
|
||||||
"updates",
|
"updates",
|
||||||
|
"contained_in",
|
||||||
"other",
|
"other",
|
||||||
];
|
];
|
||||||
const actual = Object.values(SituationRelationship);
|
const actual = Object.values(SituationRelationship);
|
||||||
@@ -495,3 +497,301 @@ describe("enum values completeness", () => {
|
|||||||
expect(actual).toContain("high");
|
expect(actual).toContain("high");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Experiment 60A.3 — Decision Option Vocabulary Boundary ──────────
|
||||||
|
|
||||||
|
describe("60A.3 option node kind", () => {
|
||||||
|
it("accepts 'option' as a valid SituationKind on a node", () => {
|
||||||
|
const result = situationNodeSchema.safeParse({
|
||||||
|
id: "n-opt-a",
|
||||||
|
label: "Option A",
|
||||||
|
description: "Candidate path A",
|
||||||
|
kind: "option",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
value: null,
|
||||||
|
unit: null,
|
||||||
|
evidenceIds: [],
|
||||||
|
dependsOn: [],
|
||||||
|
affects: [],
|
||||||
|
parentId: null,
|
||||||
|
childIds: [],
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("existing node kinds are still accepted", () => {
|
||||||
|
for (const kind of Object.values(SituationKind)) {
|
||||||
|
const result = situationNodeSchema.safeParse({
|
||||||
|
id: "n-x",
|
||||||
|
label: "Test",
|
||||||
|
description: "Test",
|
||||||
|
kind,
|
||||||
|
status: "known",
|
||||||
|
confidence: "high",
|
||||||
|
value: null,
|
||||||
|
unit: null,
|
||||||
|
evidenceIds: [],
|
||||||
|
dependsOn: [],
|
||||||
|
affects: [],
|
||||||
|
parentId: null,
|
||||||
|
childIds: [],
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still rejects invalid node kind", () => {
|
||||||
|
const result = situationNodeSchema.safeParse({
|
||||||
|
id: "n-x",
|
||||||
|
label: "Test",
|
||||||
|
description: "Test",
|
||||||
|
kind: "decision",
|
||||||
|
status: "known",
|
||||||
|
confidence: "high",
|
||||||
|
value: null,
|
||||||
|
unit: null,
|
||||||
|
evidenceIds: [],
|
||||||
|
dependsOn: [],
|
||||||
|
affects: [],
|
||||||
|
parentId: null,
|
||||||
|
childIds: [],
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects 'alternative' as a node kind", () => {
|
||||||
|
const result = situationNodeSchema.safeParse({
|
||||||
|
id: "n-x",
|
||||||
|
label: "Test",
|
||||||
|
description: "Test",
|
||||||
|
kind: "alternative",
|
||||||
|
status: "known",
|
||||||
|
confidence: "high",
|
||||||
|
value: null,
|
||||||
|
unit: null,
|
||||||
|
evidenceIds: [],
|
||||||
|
dependsOn: [],
|
||||||
|
affects: [],
|
||||||
|
parentId: null,
|
||||||
|
childIds: [],
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects 'baseline' as a node kind", () => {
|
||||||
|
const result = situationNodeSchema.safeParse({
|
||||||
|
id: "n-x",
|
||||||
|
label: "Test",
|
||||||
|
description: "Test",
|
||||||
|
kind: "baseline",
|
||||||
|
status: "known",
|
||||||
|
confidence: "high",
|
||||||
|
value: null,
|
||||||
|
unit: null,
|
||||||
|
evidenceIds: [],
|
||||||
|
dependsOn: [],
|
||||||
|
affects: [],
|
||||||
|
parentId: null,
|
||||||
|
childIds: [],
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects 'outcome' as a node kind", () => {
|
||||||
|
const result = situationNodeSchema.safeParse({
|
||||||
|
id: "n-x",
|
||||||
|
label: "Test",
|
||||||
|
description: "Test",
|
||||||
|
kind: "outcome",
|
||||||
|
status: "known",
|
||||||
|
confidence: "high",
|
||||||
|
value: null,
|
||||||
|
unit: null,
|
||||||
|
evidenceIds: [],
|
||||||
|
dependsOn: [],
|
||||||
|
affects: [],
|
||||||
|
parentId: null,
|
||||||
|
childIds: [],
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("60A.3 contained_in relationship", () => {
|
||||||
|
it("accepts 'contained_in' as a valid SituationRelationship on an edge", () => {
|
||||||
|
const result = situationEdgeSchema.safeParse({
|
||||||
|
id: "e-opt-a-to-ctx",
|
||||||
|
fromNodeId: "n-opt-a",
|
||||||
|
toNodeId: "n-decision-ctx",
|
||||||
|
relationship: "contained_in",
|
||||||
|
confidence: "medium",
|
||||||
|
description: "Option A belongs to decision context",
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("existing relationships are still accepted", () => {
|
||||||
|
for (const rel of Object.values(SituationRelationship)) {
|
||||||
|
const result = situationEdgeSchema.safeParse({
|
||||||
|
id: "e-x",
|
||||||
|
fromNodeId: "n-a",
|
||||||
|
toNodeId: "n-b",
|
||||||
|
relationship: rel,
|
||||||
|
confidence: "high",
|
||||||
|
description: "test",
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid relationship type", () => {
|
||||||
|
const result = situationEdgeSchema.safeParse({
|
||||||
|
id: "e-x",
|
||||||
|
fromNodeId: "n-a",
|
||||||
|
toNodeId: "n-b",
|
||||||
|
relationship: "contains_option",
|
||||||
|
confidence: "high",
|
||||||
|
description: "test",
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects 'alternative_to' as a SituationRelationship", () => {
|
||||||
|
const result = situationEdgeSchema.safeParse({
|
||||||
|
id: "e-x",
|
||||||
|
fromNodeId: "n-a",
|
||||||
|
toNodeId: "n-b",
|
||||||
|
relationship: "alternative_to",
|
||||||
|
confidence: "high",
|
||||||
|
description: "test",
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects 'option_for' as a SituationRelationship", () => {
|
||||||
|
const result = situationEdgeSchema.safeParse({
|
||||||
|
id: "e-x",
|
||||||
|
fromNodeId: "n-a",
|
||||||
|
toNodeId: "n-b",
|
||||||
|
relationship: "option_for",
|
||||||
|
confidence: "high",
|
||||||
|
description: "test",
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("60A.3 native two-option graph fixture", () => {
|
||||||
|
it("constructs and validates a complete two-option decision graph", () => {
|
||||||
|
const unknownCtx = makeNode({
|
||||||
|
id: "n-decision-ctx",
|
||||||
|
label: "Which path leaves us better off?",
|
||||||
|
description: "Unresolved decision context for the current choice.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
const optionA = makeNode({
|
||||||
|
id: "n-option-a",
|
||||||
|
label: "Path A — Act",
|
||||||
|
description: "Candidate action A with its own consequences.",
|
||||||
|
kind: "option",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
const optionB = makeNode({
|
||||||
|
id: "n-option-b",
|
||||||
|
label: "Path B — Do nothing",
|
||||||
|
description: "Candidate action B (do-nothing / stay-put).",
|
||||||
|
kind: "option",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
const consequenceA = makeNode({
|
||||||
|
id: "n-cons-a",
|
||||||
|
label: "Benefit of Path A",
|
||||||
|
description: "Consequence that applies to option A.",
|
||||||
|
kind: "metric",
|
||||||
|
status: "known",
|
||||||
|
confidence: "high",
|
||||||
|
value: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const consequenceB = makeNode({
|
||||||
|
id: "n-cons-b",
|
||||||
|
label: "Cost of Path B",
|
||||||
|
description: "Consequence that applies to option B.",
|
||||||
|
kind: "observation",
|
||||||
|
status: "known",
|
||||||
|
confidence: "high",
|
||||||
|
});
|
||||||
|
|
||||||
|
const graph = makeGraph({
|
||||||
|
centralStatement: "Test two-option decision.",
|
||||||
|
nodes: [unknownCtx, optionA, optionB, consequenceA, consequenceB],
|
||||||
|
edges: [
|
||||||
|
makeEdge({
|
||||||
|
id: "e-opt-a-contained",
|
||||||
|
fromNodeId: optionA.id,
|
||||||
|
toNodeId: unknownCtx.id,
|
||||||
|
relationship: "contained_in",
|
||||||
|
confidence: "high",
|
||||||
|
description: "Option A belongs to decision context",
|
||||||
|
}),
|
||||||
|
makeEdge({
|
||||||
|
id: "e-opt-b-contained",
|
||||||
|
fromNodeId: optionB.id,
|
||||||
|
toNodeId: unknownCtx.id,
|
||||||
|
relationship: "contained_in",
|
||||||
|
confidence: "high",
|
||||||
|
description: "Option B belongs to decision context",
|
||||||
|
}),
|
||||||
|
makeEdge({
|
||||||
|
id: "e-cons-a-to-opt-a",
|
||||||
|
fromNodeId: consequenceA.id,
|
||||||
|
toNodeId: optionA.id,
|
||||||
|
relationship: "supports",
|
||||||
|
confidence: "high",
|
||||||
|
description: "Consequence supports option A",
|
||||||
|
}),
|
||||||
|
makeEdge({
|
||||||
|
id: "e-cons-b-to-opt-b",
|
||||||
|
fromNodeId: consequenceB.id,
|
||||||
|
toNodeId: optionB.id,
|
||||||
|
relationship: "weakens",
|
||||||
|
confidence: "high",
|
||||||
|
description: "Consequence weakens option B",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
activeUnknownNodeId: unknownCtx.id,
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "2 options under 1 decision context.",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(graph.nodes.length).toBe(5);
|
||||||
|
expect(graph.edges.length).toBe(4);
|
||||||
|
|
||||||
|
// Both options share the same decision-context unknown
|
||||||
|
const optionEdges = graph.edges.filter((e) => e.relationship === "contained_in");
|
||||||
|
expect(optionEdges.length).toBe(2);
|
||||||
|
expect(optionEdges[0].toNodeId).toBe(optionEdges[1].toNodeId);
|
||||||
|
|
||||||
|
// Each option has its own consequence attached
|
||||||
|
const aConsequences = graph.edges.filter(
|
||||||
|
(e) => e.toNodeId === optionA.id,
|
||||||
|
);
|
||||||
|
const bConsequences = graph.edges.filter(
|
||||||
|
(e) => e.toNodeId === optionB.id,
|
||||||
|
);
|
||||||
|
expect(aConsequences.length).toBeGreaterThan(0);
|
||||||
|
expect(bConsequences.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// No decision node kind exists
|
||||||
|
expect(graph.nodes.some((n) => n.kind === "option")).toBe(true);
|
||||||
|
expect(graph.nodes.some((n) => n.kind === "unknown")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user