"use strict"; const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); const repoRoot = path.resolve(__dirname, ".."); const canonicalDir = path.resolve(repoRoot, "packages", "db", "migrations"); const legacyDir = path.resolve(repoRoot, "backend", "migrations"); const defaultReportPath = path.resolve(legacyDir, "stale-sql-report.json"); function parseArgs(argv) { const args = new Set(argv); return { write: args.has("--write"), failOnStale: args.has("--fail-on-stale"), help: args.has("--help"), }; } function ensureDirectoryExists(dirPath, label) { if (!fs.existsSync(dirPath)) { throw new Error(`${label} directory not found: ${dirPath}`); } } function sha256File(filePath) { const hash = crypto.createHash("sha256"); hash.update(fs.readFileSync(filePath)); return hash.digest("hex"); } function listFiles(dirPath) { return fs .readdirSync(dirPath) .filter((name) => fs.statSync(path.join(dirPath, name)).isFile()) .sort((a, b) => a.localeCompare(b)); } function listSqlFiles(dirPath) { return listFiles(dirPath).filter((name) => name.toLowerCase().endsWith(".sql")); } function mapByNameWithHash(dirPath, names) { const map = new Map(); for (const name of names) { map.set(name, { name, path: path.join(dirPath, name), sha256: sha256File(path.join(dirPath, name)), }); } return map; } function buildReport() { ensureDirectoryExists(canonicalDir, "Canonical migrations"); ensureDirectoryExists(legacyDir, "Legacy migrations"); const canonicalSql = listSqlFiles(canonicalDir); const legacySql = listSqlFiles(legacyDir); const legacyNonSql = listFiles(legacyDir).filter( (name) => !name.toLowerCase().endsWith(".sql") ); const canonicalMap = mapByNameWithHash(canonicalDir, canonicalSql); const legacyMap = mapByNameWithHash(legacyDir, legacySql); const staleFiles = []; for (const legacyName of legacySql) { const legacyFile = legacyMap.get(legacyName); const canonicalFile = canonicalMap.get(legacyName); if (!canonicalFile) { staleFiles.push({ filename: legacyName, status: "STALE_ONLY_IN_BACKEND", backend_sha256: legacyFile.sha256, }); continue; } if (legacyFile.sha256 === canonicalFile.sha256) { staleFiles.push({ filename: legacyName, status: "STALE_DUPLICATE_OF_CANONICAL", backend_sha256: legacyFile.sha256, canonical_sha256: canonicalFile.sha256, }); continue; } staleFiles.push({ filename: legacyName, status: "STALE_DIVERGED_FROM_CANONICAL", backend_sha256: legacyFile.sha256, canonical_sha256: canonicalFile.sha256, }); } const canonicalOnly = canonicalSql .filter((name) => !legacyMap.has(name)) .map((name) => ({ filename: name, status: "CANONICAL_ONLY", canonical_sha256: canonicalMap.get(name).sha256, })); return { generated_at: new Date().toISOString(), canonical_dir: path.relative(repoRoot, canonicalDir), legacy_dir: path.relative(repoRoot, legacyDir), stale_sql_files: staleFiles, canonical_only_sql_files: canonicalOnly, legacy_non_sql_files: legacyNonSql, summary: { stale_total: staleFiles.length, stale_only_in_backend_total: staleFiles.filter( (f) => f.status === "STALE_ONLY_IN_BACKEND" ).length, stale_duplicate_total: staleFiles.filter( (f) => f.status === "STALE_DUPLICATE_OF_CANONICAL" ).length, stale_diverged_total: staleFiles.filter( (f) => f.status === "STALE_DIVERGED_FROM_CANONICAL" ).length, canonical_only_total: canonicalOnly.length, }, }; } function printReport(report) { console.log("Stale SQL Tracker"); console.log(`- Canonical: ${report.canonical_dir}`); console.log(`- Legacy: ${report.legacy_dir}`); console.log(`- Generated: ${report.generated_at}`); console.log(""); console.log(`Stale SQL files in legacy dir: ${report.summary.stale_total}`); for (const stale of report.stale_sql_files) { console.log(` - ${stale.filename} :: ${stale.status}`); } console.log(""); console.log(`Canonical-only SQL files: ${report.summary.canonical_only_total}`); for (const canonicalOnly of report.canonical_only_sql_files) { console.log(` - ${canonicalOnly.filename}`); } console.log(""); console.log(`Legacy non-SQL files: ${report.legacy_non_sql_files.length}`); for (const nonSql of report.legacy_non_sql_files) { console.log(` - ${nonSql}`); } } function writeReport(report) { fs.writeFileSync(defaultReportPath, JSON.stringify(report, null, 2) + "\n", "utf8"); console.log(""); console.log(`Wrote stale SQL report: ${path.relative(repoRoot, defaultReportPath)}`); } function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { console.log("Usage: node scripts/db-stale-sql-tracker.js [--write] [--fail-on-stale]"); process.exit(0); } const report = buildReport(); printReport(report); if (options.write) { writeReport(report); } if (options.failOnStale && report.summary.stale_total > 0) { process.exit(1); } } try { main(); } catch (error) { console.error(error.message); process.exit(1); }