import type { BeadsApiResponse, GraphLink } from "./types"; export interface NodeChange { field: string; // e.g. "status", "priority", "title" from: string; // previous value (stringified) to: string; // new value (stringified) } export interface BeadsDiff { addedNodeIds: Set; // IDs of nodes not in old data removedNodeIds: Set; // IDs of nodes not in new data changedNodes: Map; // ID -> list of field changes addedLinkKeys: Set; // "source->target:type" keys removedLinkKeys: Set; // "source->target:type" keys hasChanges: boolean; // true if anything changed at all } /** * Build a stable key for a link. * Links may have string or object source/target (after force-graph mutation). */ export function linkKey(link: GraphLink): string { const src = typeof link.source === "object" ? (link.source as { id: string }).id : link.source; const tgt = typeof link.target === "object" ? (link.target as { id: string }).id : link.target; return `${src}->${tgt}:${link.type}`; } /** * Compute the diff between old and new beads data. * Compares nodes by ID and links by source->target:type key. */ export function diffBeadsData( oldData: BeadsApiResponse | null, newData: BeadsApiResponse ): BeadsDiff { // If no old data, everything is "added" if (!oldData) { return { addedNodeIds: new Set(newData.graphData.nodes.map((n) => n.id)), removedNodeIds: new Set(), changedNodes: new Map(), addedLinkKeys: new Set(newData.graphData.links.map(linkKey)), removedLinkKeys: new Set(), hasChanges: true, }; } const oldNodeMap = new Map( oldData.graphData.nodes.map((n) => [n.id, n]) ); const newNodeMap = new Map( newData.graphData.nodes.map((n) => [n.id, n]) ); // Node diffs const addedNodeIds = new Set(); const removedNodeIds = new Set(); const changedNodes = new Map(); for (const [id, node] of newNodeMap) { if (!oldNodeMap.has(id)) { addedNodeIds.add(id); } else { const old = oldNodeMap.get(id)!; const changes: NodeChange[] = []; if (old.status !== node.status) { changes.push({ field: "status", from: old.status, to: node.status }); } if (old.priority !== node.priority) { changes.push({ field: "priority", from: String(old.priority), to: String(node.priority), }); } if (old.title !== node.title) { changes.push({ field: "title", from: old.title, to: node.title }); } if ((old.owner || "") !== (node.owner || "")) { changes.push({ field: "owner", from: old.owner || "", to: node.owner || "" }); } if ((old.assignee || "") !== (node.assignee || "")) { changes.push({ field: "assignee", from: old.assignee || "", to: node.assignee || "" }); } if (changes.length > 0) { changedNodes.set(id, changes); } } } for (const id of oldNodeMap.keys()) { if (!newNodeMap.has(id)) { removedNodeIds.add(id); } } // Link diffs const oldLinkKeys = new Set(oldData.graphData.links.map(linkKey)); const newLinkKeys = new Set(newData.graphData.links.map(linkKey)); const addedLinkKeys = new Set(); const removedLinkKeys = new Set(); for (const key of newLinkKeys) { if (!oldLinkKeys.has(key)) addedLinkKeys.add(key); } for (const key of oldLinkKeys) { if (!newLinkKeys.has(key)) removedLinkKeys.add(key); } const hasChanges = addedNodeIds.size > 0 || removedNodeIds.size > 0 || changedNodes.size > 0 || addedLinkKeys.size > 0 || removedLinkKeys.size > 0; return { addedNodeIds, removedNodeIds, changedNodes, addedLinkKeys, removedLinkKeys, hasChanges, }; }