reasoning: normalise graph relationship contract at proposal boundary

This commit is contained in:
2026-08-10 06:16:47 +01:00
parent cf6c5cb57f
commit 46503b4507
3 changed files with 163 additions and 0 deletions
+13
View File
@@ -62,6 +62,19 @@ function applyKnownEnumAliases(proposal, normalisationsApplied) {
});
}
if (Array.isArray(proposal.addedEdges)) {
proposal.addedEdges = proposal.addedEdges.map((edge, index) => {
if (edge?.relationship === "affects") {
normalisationsApplied.push({
path: ["addedEdges", index, "relationship"],
change: "Converted affects to other",
});
return { ...edge, relationship: "other" };
}
return edge;
});
}
return proposal;
}
+29
View File
@@ -401,6 +401,35 @@ describe("applyValidatedProposal", () => {
expect(result.stage).toBe("proposal_compatibility");
});
it("accepts a proposal edge normalised from affects to canonical other before application", () => {
const { graph, proposal } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
addedEdges: [
makeEdge({
id: "e-other-edge",
fromNodeId: "n-complaint-count",
toNodeId: "n-quality-deterioration",
relationship: "other",
confidence: "medium",
description:
"Canonical generic relationship after proposal-boundary normalisation.",
}),
],
},
});
expect(result.success).toBe(true);
expect(
result.updatedSituationGraph.edges.some(
(edge) => edge.id === "e-other-edge" && edge.relationship === "other",
),
).toBe(true);
});
it("rejects duplicate IDs", () => {
const { graph, proposal, ids } = makeApplicationFixture();
+121
View File
@@ -1,5 +1,7 @@
import { describe, expect, it } from "vitest";
import { parseGraphUpdateProposal } from "@/lib/graph/update-proposal.js";
import { applyGraphUpdate } from "@/lib/graph/utils.js";
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
function makeValidProposal(overrides = {}) {
return {
@@ -144,6 +146,125 @@ describe("parseGraphUpdateProposal", () => {
});
});
it("normalises the observed external relationship wording affects to canonical other at the proposal boundary", () => {
const result = parseGraphUpdateProposal({
...makeValidProposal(),
addedEdges: [
{
id: "e-affects",
fromNodeId: "n-driver",
toNodeId: "n-unknown",
relationship: "affects",
confidence: "medium",
description: "Model-style relationship wording from live output.",
},
],
});
expect(result.success).toBe(true);
expect(result.proposal.addedEdges[0].relationship).toBe("other");
expect(result.normalisationsApplied).toEqual(
expect.arrayContaining([
expect.objectContaining({ change: "Converted affects to other" }),
]),
);
});
it("leaves already-valid canonical relationship values unchanged", () => {
const canonicalRelationships = [
"supports",
"weakens",
"contradicts",
"depends_on",
"causes",
"may_cause",
"measures",
"compares_with",
"updates",
"other",
];
for (const relationship of canonicalRelationships) {
const result = parseGraphUpdateProposal({
...makeValidProposal(),
addedEdges: [
{
id: `e-${relationship}`,
fromNodeId: "n-driver",
toNodeId: "n-unknown",
relationship,
confidence: "medium",
description: `Canonical relationship ${relationship}`,
},
],
});
expect(result.success).toBe(true);
expect(result.proposal.addedEdges[0].relationship).toBe(relationship);
}
});
it("still rejects unsupported arbitrary relationship values", () => {
const result = parseGraphUpdateProposal({
...makeValidProposal(),
addedEdges: [
{
id: "e-unsupported",
fromNodeId: "n-driver",
toNodeId: "n-unknown",
relationship: "influences_decisively",
confidence: "medium",
description: "Unsupported relationship wording.",
},
],
});
expect(result.success).toBe(false);
});
it("applies only the canonical internal relationship after normalisation", () => {
const parsed = parseGraphUpdateProposal({
...makeValidProposal({
updatedNodes: [],
resolvedUnknownNodeIds: [],
}),
addedEdges: [
{
id: "e-affects",
fromNodeId: "n-driver",
toNodeId: "n-unknown",
relationship: "affects",
confidence: "medium",
description: "Model-style relationship wording from live output.",
},
],
});
expect(parsed.success).toBe(true);
const graph = makeGraph({
centralStatement: "Relationship application fixture",
nodes: [
makeNode({ id: "n-driver", label: "Driver", status: "known" }),
makeNode({ id: "n-unknown", label: "Unknown" }),
],
edges: [],
currentSummary: "Initial summary",
});
const applied = applyGraphUpdate(graph, parsed.proposal);
expect(applied.success).toBe(true);
expect(applied.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({ relationship: "other" }),
]),
);
expect(applied.edges.some((edge) => edge.relationship === "affects")).toBe(
false,
);
});
it("unknown node enum values still fail", () => {
const result = parseGraphUpdateProposal({
...makeValidProposal(),