feat: define graph update proposal contract
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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: [],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user