/** * Shallow JSON-record diff utilities for UI summary chips. * *

Lives in `src/lib` (not in a route file) so that callers beyond the * record-draft ReviewDrawer can reuse it — RecordHistoryDrawer, audit row * summary, future audit-trail tooltips. Pre-emptive DRY: surfaced during * the 2026-05-14 eng review of MR !186. * *

Shallow on purpose: ordinis business records are flat-ish at the top * level (business fields, no deep object trees that would require a real * structural diff). Nested-object changes are reported as a single * "changed" entry on the outer key. Good enough for "what's the gist" UI * chips. For full schema-level diff with line-by-line highlighting, see * `ChangelogDiffModal` + its backend `summary.added/removed/changed` * payload. * *

Equality is checked via `JSON.stringify`. Order-sensitive — relies on * V8/JSC preserving insertion order for plain objects (true on every * modern runtime ordinis ships against). Sufficient for record data which * is always object-literal serialized; not safe for Map/Set/Date/RegExp, * none of which appear in record payloads. */ export type ShallowDiffSummary = { added: string[] removed: string[] changed: string[] } /** * @returns null when both inputs are null/undefined (e.g. CLOSE op draft * with no live record). Caller can skip rendering entirely. * Otherwise: per-bucket key lists, never null. */ export function shallowDiffSummary( before: Record | null | undefined, after: Record | null | undefined, ): ShallowDiffSummary | null { if (!before && !after) return null const b = before ?? {} const a = after ?? {} const added: string[] = [] const removed: string[] = [] const changed: string[] = [] for (const k of Object.keys(a)) { if (!(k in b)) { added.push(k) } else if (JSON.stringify(a[k]) !== JSON.stringify(b[k])) { changed.push(k) } } for (const k of Object.keys(b)) { if (!(k in a)) removed.push(k) } return { added, removed, changed } } /** Convenience: empty when all three buckets are empty. */ export function isEmptyDiffSummary(s: ShallowDiffSummary): boolean { return s.added.length === 0 && s.removed.length === 0 && s.changed.length === 0 }