Spaces:
Runtime error
Runtime error
File size: 7,140 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | #!/usr/bin/env node
// scripts/check/check-migration-numbering.mjs
// Gate de numeração de migrations: protege src/lib/db/migrations/ contra regressões
// de numeração. WHY: um incidente destrutivo aconteceu DUAS VEZES — um `git rm` de
// uma migration duplicada durante um merge apagou uma migration REAL da release
// (094/095 em #3365/#3371). Este gate cria uma ligação de CI entre o disco e as
// anomalias já reconhecidas em migrationRunner.ts, falhando em QUALQUER:
// - nome de arquivo sem prefixo numérico zero-padded (NNN_*.sql);
// - prefixo de versão DUPLICADO no disco (exceto duplicatas já reconhecidas);
// - NOVO gap inexplicado na sequência (gaps conhecidos 026/055 são congelados).
// As anomalias conhecidas são derivadas das listas de migrationRunner.ts
// (LEGACY_VERSION_SLOT_MIGRATIONS / SUPERSEDED_DUPLICATE_MIGRATIONS) + a auditoria
// de gaps de sequência. NÃO adicione novos itens sem justificativa — esse é o ponto.
// Stale-enforcement (6A.3): entrada em KNOWN_GAPS / KNOWN_DUPLICATE_VERSIONS que não
// suprime nenhuma anomalia real → gate falha com instrução de remoção.
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { assertNoStale } from "./lib/allowlist.mjs";
const cwd = process.cwd();
const MIGRATIONS_DIR = path.join(cwd, "src/lib/db/migrations");
// Convenção de nome: NNN_descricao.sql (prefixo numérico zero-padded de >= 3 dígitos).
// Mesmo regex usado pelo runner de produção (migrationRunner.ts ~linha 282).
const MIGRATION_NAME_RE = /^(\d{3,})_(.+)\.sql$/;
// ---------------------------------------------------------------------------
// ALLOWLIST 1 — duplicatas de versão CONHECIDAS.
// Fonte: src/lib/db/migrationRunner.ts → SUPERSEDED_DUPLICATE_MIGRATIONS (~L188).
// O runner já aceita estes slots de versão reutilizados (a migration "renomeada"
// foi promovida para um número novo, e o slot antigo é tolerado). Adicionar aqui
// SOMENTE se houver um arquivo físico duplicado no disco (stale-enforcement 6A.3
// detecta entradas sem duplicata física viva e força remoção).
// ---------------------------------------------------------------------------
export const KNOWN_DUPLICATE_VERSIONS = new Set([
// "041" was removed: 041_session_account_affinity.sql no longer exists on disk
// (only 041_compression_receipts.sql remains), so no physical duplicate is present.
// The SUPERSEDED_DUPLICATE_MIGRATIONS entry in migrationRunner.ts handles the runner
// compatibility at runtime without needing an allowlist here. (#6A.3 stale cleanup)
]);
// ---------------------------------------------------------------------------
// ALLOWLIST 2 — gaps de sequência CONHECIDOS.
// Fonte: auditoria do disco (src/lib/db/migrations/) — a sequência pula 026 e 055.
// Estes números nunca tiveram arquivo físico (slots legados que viraram outros
// números via RENAMED_MIGRATION_COMPATIBILITY em migrationRunner.ts). Congelados
// para que o gate bloqueie apenas NOVOS buracos inexplicados na sequência.
// ---------------------------------------------------------------------------
export const KNOWN_GAPS = new Set(["026", "055"]);
function pad3(n) {
return String(n).padStart(3, "0");
}
/**
* Função pura — detecta anomalias de numeração de migrations.
*
* @param {string[]} filenames nomes de arquivo (basename) em src/lib/db/migrations/
* @param {Set<string>} knownDuplicates versões com duplicata reconhecida (ex.: "041")
* @param {Set<string>} knownGaps gaps de sequência reconhecidos (ex.: "026")
* @returns {{ duplicates: Array<{version:string,names:string[]}>, gaps: string[], badNames: string[] }}
*/
export function findMigrationAnomalies(filenames, knownDuplicates, knownGaps) {
const dups = knownDuplicates || new Set();
const gapsAllow = knownGaps || new Set();
const badNames = [];
const byVersion = new Map();
for (const filename of filenames) {
if (!filename.endsWith(".sql")) continue;
const match = filename.match(MIGRATION_NAME_RE);
if (!match) {
badNames.push(filename);
continue;
}
const version = match[1];
if (!byVersion.has(version)) byVersion.set(version, []);
byVersion.get(version).push(filename);
}
// Duplicatas: dois arquivos físicos com o mesmo prefixo, exceto os reconhecidos.
const duplicates = [];
for (const [version, names] of byVersion.entries()) {
if (names.length <= 1) continue;
if (dups.has(version)) continue;
duplicates.push({ version, names: [...names].sort() });
}
duplicates.sort((a, b) => a.version.localeCompare(b.version));
// Gaps: buracos na sequência min..max que não estão na allowlist.
const versions = [...byVersion.keys()].map((v) => parseInt(v, 10)).sort((a, b) => a - b);
const gaps = [];
if (versions.length > 0) {
const min = versions[0];
const max = versions[versions.length - 1];
const present = new Set(versions);
for (let n = min + 1; n < max; n++) {
if (present.has(n)) continue;
const padded = pad3(n);
if (gapsAllow.has(padded)) continue;
gaps.push(padded);
}
}
return { duplicates, gaps, badNames };
}
function listMigrationFilenames() {
if (!fs.existsSync(MIGRATIONS_DIR)) return [];
return fs.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith(".sql"));
}
function main() {
const filenames = listMigrationFilenames();
// Compute raw anomalies WITHOUT allowlists — needed for stale-enforcement (6A.3).
const raw = findMigrationAnomalies(filenames, new Set(), new Set());
const liveGaps = raw.gaps;
const liveDupVersions = raw.duplicates.map((d) => d.version);
assertNoStale(KNOWN_GAPS, liveGaps, "check-migration-numbering:gaps");
assertNoStale(KNOWN_DUPLICATE_VERSIONS, liveDupVersions, "check-migration-numbering:duplicates");
const { duplicates, gaps, badNames } = findMigrationAnomalies(
filenames,
KNOWN_DUPLICATE_VERSIONS,
KNOWN_GAPS
);
const problems = [];
for (const b of badNames) {
problems.push(` ✗ nome inválido (esperado NNN_descricao.sql): ${b}`);
}
for (const d of duplicates) {
problems.push(` ✗ prefixo de versão duplicado ${d.version}: [${d.names.join(", ")}]`);
}
for (const g of gaps) {
problems.push(` ✗ gap inexplicado na sequência: faltando ${g}`);
}
if (problems.length > 0) {
console.error(
`[check-migration-numbering] ${problems.length} anomalia(s) de numeração:\n` +
problems.join("\n") +
`\n → renomeie o arquivo colidente, preencha o gap, ou — se for legítimo — ` +
`adicione o número às allowlists KNOWN_DUPLICATE_VERSIONS / KNOWN_GAPS com ` +
`justificativa rastreável a src/lib/db/migrationRunner.ts.`
);
process.exitCode = 1;
}
if (!process.exitCode) {
console.log(
`[check-migration-numbering] OK (${filenames.length} migrations, ` +
`${KNOWN_GAPS.size} gap(s) conhecido(s), ${KNOWN_DUPLICATE_VERSIONS.size} duplicata(s) conhecida(s))`
);
}
}
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();
|