/** * squash-base.ts — Pure resolver for the squash/rebase "reference branch". * * Ported verbatim from the 4.x EF Core agent GOLDEN RULE * (`templates/agents/efcore/squash.md` @ v4.81.0): * * feature/* → snapshot + migrations from develop (feature → develop) * develop → snapshot + migrations from main (develop → prod) * release/* → snapshot + migrations from main (release → prod) * hotfix/* → snapshot + migrations from main (hotfix → prod) * main → BLOCKED (never squash on production) * → develop (4.x default) * * The base branch is the **merge target** — the branch whose migrations are * already shared / applied downstream. A squash may NEVER delete a migration * that lives in this reference branch; it only consolidates the migrations * that are unique to the current branch. * * The v5 rewrite lost this mapping and always preferred `develop` regardless * of the branch type, so a release/hotfix (whose true reference is `main`) * could delete migrations that already exist in production. This restores the * 4.x rule. */ export type SquashBranchType = 'feature' | 'release' | 'hotfix' | 'develop' | 'main' | 'unknown'; export interface BranchNames { /** Production branch name. Default `main`. */ main?: string; /** Integration branch name. Default `develop`. */ develop?: string; } export interface BaseDecision { branchType: SquashBranchType; /** The reference branch to diff / restore against. `null` when blocked. */ baseBranch: string | null; /** True for main / master — squash and rebase must refuse. */ blocked: boolean; /** Human-readable justification (shown to the user before any deletion). */ reason: string; } /** * Resolve the reference branch for a squash, by branch *type* — never by a * blind "develop first" fallback. Mirrors the 4.x `determine_base_branch`. */ export function determineBaseBranch(currentBranch: string, names: BranchNames = {}): BaseDecision { const main = names.main ?? 'main'; const develop = names.develop ?? 'develop'; const branch = (currentBranch ?? '').trim(); const lower = branch.toLowerCase(); // Production branches — never squashable. if (lower === 'main' || lower === 'master' || lower === main.toLowerCase()) { return { branchType: 'main', baseBranch: null, blocked: true, reason: `"${branch}" is a protected production branch — squashing migrations there would rewrite history already applied to prod.`, }; } // develop integrates into main (develop → prod). if (lower === 'develop' || lower === develop.toLowerCase()) { return { branchType: 'develop', baseBranch: main, blocked: false, reason: `develop squashes onto "${main}" (develop → prod): only migrations not yet in ${main} may be consolidated.`, }; } if (lower.startsWith('feature/')) { return { branchType: 'feature', baseBranch: develop, blocked: false, reason: `feature squashes onto "${develop}" (feature → develop): migrations already in ${develop} are preserved.`, }; } if (lower.startsWith('release/')) { return { branchType: 'release', baseBranch: main, blocked: false, reason: `release squashes onto "${main}" (release → prod): migrations already in ${main} are preserved.`, }; } if (lower.startsWith('hotfix/')) { return { branchType: 'hotfix', baseBranch: main, blocked: false, reason: `hotfix squashes onto "${main}" (hotfix → prod): migrations already in ${main} are preserved.`, }; } // 4.x default for anything else: treat develop as the reference. return { branchType: 'unknown', baseBranch: develop, blocked: false, reason: `unknown branch type — defaulting the reference branch to "${develop}".`, }; }