38 lines
2.7 KiB
TypeScript
38 lines
2.7 KiB
TypeScript
import type { DiagramIRV2 } from "../../model/diagram-ir.js";
|
|
import { validateDiagramIR } from "../../model/diagram-ir.js";
|
|
|
|
export interface ArchitectureSnapshot { id: string; label: string; ir: DiagramIRV2; provenance?: Record<string, unknown> }
|
|
export interface ChangeSet { added: string[]; removed: string[]; modified: string[]; unchanged: string[] }
|
|
export interface TimelapseFrame { id: string; label: string; index: number; ir: DiagramIRV2; changes: { nodes: ChangeSet; edges: ChangeSet }; provenance?: Record<string, unknown> }
|
|
export interface ArchitectureTimelapse { frames: TimelapseFrame[] }
|
|
|
|
function canonical(value: unknown): string {
|
|
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
if (value && typeof value === "object") return `{${Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
|
|
return JSON.stringify(value);
|
|
}
|
|
function inventory(ir: DiagramIRV2, kind: "nodes" | "edges"): Map<string, unknown> {
|
|
return new Map(ir.pages.flatMap((page) => page[kind].map((item) => [`${page.id}/${item.id}`, item] as const)));
|
|
}
|
|
function classify(previous: Map<string, unknown>, current: Map<string, unknown>): ChangeSet {
|
|
const added: string[] = [], removed: string[] = [], modified: string[] = [], unchanged: string[] = [];
|
|
for (const id of [...current.keys()].sort()) {
|
|
if (!previous.has(id)) added.push(id); else if (canonical(previous.get(id)) === canonical(current.get(id))) unchanged.push(id); else modified.push(id);
|
|
}
|
|
for (const id of [...previous.keys()].sort()) if (!current.has(id)) removed.push(id);
|
|
return { added, removed, modified, unchanged };
|
|
}
|
|
|
|
export function createArchitectureTimelapse(snapshots: readonly ArchitectureSnapshot[]): ArchitectureTimelapse {
|
|
if (!Array.isArray(snapshots) || snapshots.length === 0) throw new Error("Timelapse requires at least one snapshot");
|
|
const ids = new Set(snapshots.map((snapshot) => snapshot.id)); if (ids.size !== snapshots.length) throw new Error("Snapshot IDs must be unique");
|
|
let previousNodes = new Map<string, unknown>(), previousEdges = new Map<string, unknown>();
|
|
const frames = snapshots.map((snapshot, index) => {
|
|
const ir = structuredClone(snapshot.ir); validateDiagramIR(ir);
|
|
const currentNodes = inventory(ir, "nodes"), currentEdges = inventory(ir, "edges");
|
|
const frame: TimelapseFrame = { id: snapshot.id, label: snapshot.label, index, ir, changes: { nodes: classify(previousNodes, currentNodes), edges: classify(previousEdges, currentEdges) }, provenance: snapshot.provenance === undefined ? undefined : structuredClone(snapshot.provenance) };
|
|
previousNodes = currentNodes; previousEdges = currentEdges; return frame;
|
|
});
|
|
return { frames };
|
|
}
|