reasoning: normalise graph relationship contract at proposal boundary
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user