/** * squash-integrity.ts — Pure detection of the prod-breaking condition the * squash toolchain used to be BLIND to. * * `status`/`squash` already compute `branch-only = local − reference` (extra * local migrations, safe to consolidate). The dangerous *inverse* was never * checked: a migration that EXISTS on the reference branch (e.g. `origin/main`) * but is MISSING from the current working tree — deleted or renamed locally by a * manual "brutal re-baseline". A plain merge then removes an already-applied * migration from production, and everything downstream breaks. * * This module is the single source of truth for that diff. It is PURE (no git, * no fs) — the caller supplies the reference paths (from `git ls-tree`) and the * present paths (from disk / `git ls-files`); the file-system reads live in * squash/execute.ts and status/execute.ts. */ /** * Reduce any migration file path to its comparable migration key: * the file's base name without the `.cs` / `.Designer.cs` suffix. * Returns `null` for non-migration files (snapshots, non-`.cs`) so they never * pollute the diff. A migration and its `.Designer.cs` sidecar collapse to the * same key — so a reference whose `.Designer.cs` lingers but whose migration is * present is NOT reported missing, and vice-versa. */ export function migrationKey(filePath: string): string | null { const base = filePath.replace(/\\/g, '/').split('/').pop(); if (!base || !base.endsWith('.cs')) return null; if (base.endsWith('ModelSnapshot.cs')) return null; return base.replace(/\.Designer\.cs$/i, '').replace(/\.cs$/i, ''); } /** * The reference-branch migrations that are ABSENT from the working tree. * * Returns the sorted, de-duplicated set of migration keys present in * `referencePaths` but not in `presentPaths`. An empty array means safe: every * migration the reference branch holds is still in the working tree. * * A non-empty result is the exact "casse-prod" signal — these migrations would * be dropped from production at merge. The caller (squash) must REFUSE by * default; only a deliberate, logged `-BruteForce` re-baseline may proceed. */ export function findMissingReferenceMigrations( referencePaths: string[], presentPaths: string[], ): string[] { const present = new Set( presentPaths.map(migrationKey).filter((k): k is string => k !== null), ); const reference = new Set( referencePaths.map(migrationKey).filter((k): k is string => k !== null), ); return Array.from(reference) .filter((k) => !present.has(k)) .sort(); } /** The reference-branch migration keys (names), sorted + de-duplicated. The 🔒 "parent / untouchable" list shown before any squash. */ export function referenceMigrationNames(referencePaths: string[]): string[] { const names = new Set( referencePaths.map(migrationKey).filter((k): k is string => k !== null), ); return Array.from(names).sort(); }