import { graphUpdateSchema } from "./schema.js"; const TOP_LEVEL_ARRAY_FIELDS = [ "addedNodes", "updatedNodes", "addedEdges", "removedEdgeIds", "resolvedUnknownNodeIds", "affectedNodeIds", ]; const TOP_LEVEL_NULLABLE_FIELDS = ["selectedQuestion"]; 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; } function fillMissingNullableFields(proposal, normalisationsApplied) { if (!proposal || typeof proposal !== "object") return proposal; for (const field of TOP_LEVEL_NULLABLE_FIELDS) { if (!(field in proposal)) { proposal[field] = null; normalisationsApplied.push({ path: [field], change: "Filled missing optional nullable field with null", }); } } 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); normalised = fillMissingNullableFields(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: [], }; }