// Deterministic Fisher-Yates shuffle. // // The permutation is a pure function of the items' keys: the SAME set always yields the SAME order, // and a genuinely different set yields a different order. It replaces the `Math.random()` shuffles // that reordered these lists on every build -- that per-build randomness made the built HTML differ // between two builds of identical source (see scopes.test / the deploy pipeline), which defeats // content-based deploy-skip (only push a scope whose output actually changed). The order is still // arbitrary (hash-seeded), never ranked or featured; it just stays stable until the data changes. // // The array is canonicalised (sorted by key) before shuffling, so the result depends only on the // SET of keys, not the order they happened to arrive in (member/tool fetches have no stable order). /** FNV-1a over the joined keys -> 32-bit unsigned seed. */ function seedFromKeys(keys: readonly string[]): number { let h = 0x811c9dc5; const s = keys.join(''); for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); } return h >>> 0; } /** mulberry32 PRNG: tiny, deterministic, good enough for shuffling display order. */ function mulberry32(seed: number): () => number { let a = seed >>> 0; return () => { a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /** * Shuffle `arr` in place, deterministically, seeded from `keyOf(item)` across the whole set. * Same items in, same order out; different set, different order. */ export function seededShuffleInPlace(arr: T[], keyOf: (item: T) => string): void { if (arr.length < 2) return; // Canonicalise first so incoming order can't affect the result. arr.sort((a, b) => { const ka = keyOf(a); const kb = keyOf(b); return ka < kb ? -1 : ka > kb ? 1 : 0; }); const rand = mulberry32(seedFromKeys(arr.map(keyOf))); for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } }