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;
|
return proposal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -401,6 +401,35 @@ describe("applyValidatedProposal", () => {
|
|||||||
expect(result.stage).toBe("proposal_compatibility");
|
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", () => {
|
it("rejects duplicate IDs", () => {
|
||||||
const { graph, proposal, ids } = makeApplicationFixture();
|
const { graph, proposal, ids } = makeApplicationFixture();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { parseGraphUpdateProposal } from "@/lib/graph/update-proposal.js";
|
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 = {}) {
|
function makeValidProposal(overrides = {}) {
|
||||||
return {
|
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", () => {
|
it("unknown node enum values still fail", () => {
|
||||||
const result = parseGraphUpdateProposal({
|
const result = parseGraphUpdateProposal({
|
||||||
...makeValidProposal(),
|
...makeValidProposal(),
|
||||||
|
|||||||
Reference in New Issue
Block a user