Files
confidence-engine/lib/graph/update-proposal.js

139 lines
3.4 KiB
JavaScript

import { graphUpdateSchema } from "./schema.js";
const TOP_LEVEL_ARRAY_FIELDS = [
"addedNodes",
"updatedNodes",
"addedEdges",
"removedEdgeIds",
"resolvedUnknownNodeIds",
"affectedNodeIds",
];
function cloneJsonSafe(value) {
if (value == null) return value;
return JSON.parse(JSON.stringify(value));
}
function removeNullArrayEntries(value, path = [], normalisationsApplied = []) {
if (Array.isArray(value)) {
const filtered = [];
value.forEach((item, index) => {
if (item === null) {
normalisationsApplied.push({
path: [...path, index],
change: "Removed null array entry",
});
return;
}
filtered.push(
removeNullArrayEntries(item, [...path, index], normalisationsApplied),
);
});
return filtered;
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [
key,
removeNullArrayEntries(child, [...path, key], normalisationsApplied),
]),
);
}
return value;
}
function applyKnownEnumAliases(proposal, normalisationsApplied) {
if (!proposal || typeof proposal !== "object") return proposal;
if (Array.isArray(proposal.addedNodes)) {
proposal.addedNodes = proposal.addedNodes.map((node, index) => {
if (node?.kind === "reported_statement") {
normalisationsApplied.push({
path: ["addedNodes", index, "kind"],
change: "Converted reported_statement to reported_claim",
});
return { ...node, kind: "reported_claim" };
}
return node;
});
}
return proposal;
}
function fillMissingOptionalArrays(proposal, normalisationsApplied) {
if (!proposal || typeof proposal !== "object") return proposal;
for (const field of TOP_LEVEL_ARRAY_FIELDS) {
if (!(field in proposal)) {
proposal[field] = [];
normalisationsApplied.push({
path: [field],
change: "Filled missing optional array with []",
});
}
}
return proposal;
}
export function parseGraphUpdateProposal(rawResponse) {
const raw = rawResponse;
let parsed;
if (typeof rawResponse === "string") {
try {
parsed = JSON.parse(rawResponse);
} catch (error) {
return {
success: false,
proposal: null,
raw,
normalisationsApplied: [],
errors: [error.message || "Model response is not valid JSON"],
};
}
} else if (rawResponse && typeof rawResponse === "object") {
parsed = cloneJsonSafe(rawResponse);
} else {
return {
success: false,
proposal: null,
raw,
normalisationsApplied: [],
errors: ["Graph update proposal must be a JSON object or JSON string"],
};
}
const normalisationsApplied = [];
let normalised = removeNullArrayEntries(parsed, [], normalisationsApplied);
normalised = applyKnownEnumAliases(normalised, normalisationsApplied);
normalised = fillMissingOptionalArrays(normalised, normalisationsApplied);
const parsedProposal = graphUpdateSchema.safeParse(normalised);
if (!parsedProposal.success) {
return {
success: false,
proposal: null,
raw,
normalisationsApplied,
errors: parsedProposal.error.issues.map((issue) => ({
path: issue.path,
message: issue.message,
code: issue.code,
})),
};
}
return {
success: true,
proposal: parsedProposal.data,
raw,
normalisationsApplied,
errors: [],
};
}