/** * lib/canonical-hash.ts — Deterministic content hash for pagespec JSON. * * Used by `ba-develop/cli/compute-page-diff` (Phase 3a targeted re-runs) to * detect whether a pagespec's machine block changed since the last successful * run. The hash is stable regardless of object **key order** — re-serialising * the same spec with keys in a different order yields the same hash, so a * cosmetic reorder by `ba-create-prd` never reads as a false "modified". * * Array order IS significant (columns / actions / filters carry display order), * so `[a, b]` and `[b, a]` intentionally hash differently. * * Pure: no I/O, no clock, no randomness — same input → same output forever. */ import { createHash } from 'node:crypto' /** * Recursively sort object keys and drop `undefined` values so that * `JSON.stringify` produces a canonical, order-independent string. Arrays keep * their element order; primitives pass through untouched. */ export function canonicalize(value: unknown): unknown { if (value === null || typeof value !== 'object') return value if (Array.isArray(value)) return value.map(canonicalize) const sorted: Record = {} for (const key of Object.keys(value as Record).sort()) { const v = (value as Record)[key] if (v !== undefined) sorted[key] = canonicalize(v) } return sorted } /** * Deterministic sha256 (base64) of any JSON-serialisable value, prefixed * `sha256-` so the algorithm is self-describing in the snapshot file. */ export function canonicalHash(value: unknown): string { const json = JSON.stringify(canonicalize(value)) return 'sha256-' + createHash('sha256').update(json).digest('base64') }