import fs from "node:fs"; import path from "node:path"; import process from "node:process"; const root = process.cwd(); const migrationsDir = path.join(root, "backend", "prisma", "migrations"); const schemaPath = path.join(root, "backend", "prisma", "schema.prisma"); const dockerComposePath = path.join(root, "docker-compose.yml"); const backendDockerfilePath = path.join(root, "backend", "Dockerfile"); const rootPackagePath = path.join(root, "package.json"); const workspacePath = path.join(root, "pnpm-workspace.yaml"); const backendEntrypointPath = path.join(root, "backend", "scripts", "docker-entrypoint.sh"); const systemReadinessRoutePath = path.join(root, "backend", "src", "routes", "systemReadiness.ts"); const appPath = path.join(root, "backend", "src", "app.ts"); const initiativeRoutesPath = path.join(root, "backend", "src", "routes", "initiatives.ts"); const initiativeRepoPath = path.join(root, "backend", "src", "repositories", "initiativesRepository.ts"); const initiativeSmokePath = path.join(root, "backend", "scripts", "smoke-initiatives.mjs"); const exists = (filePath) => fs.existsSync(filePath); const read = (filePath) => fs.readFileSync(filePath, "utf8"); const fail = []; const warn = []; const requiredPnpmBuiltDeps = ["@prisma/client", "@prisma/engines", "@swc/core", "esbuild", "prisma"]; const parseWorkspaceAllowBuilds = (workspaceYaml) => { const allowed = new Set(); const allowBuildsIndex = workspaceYaml.indexOf("allowBuilds:"); if (allowBuildsIndex === -1) return allowed; const lines = workspaceYaml.slice(allowBuildsIndex).split("\n").slice(1); for (const line of lines) { if (/^[^\s#]/.test(line)) break; const match = line.match(/^\s*[\'"]?([^\'":]+)[\'"]?\s*:\s*(true|false)\s*(?:#.*)?$/); if (match && match[2] === "true") allowed.add(match[1].trim()); } return allowed; }; if (!exists(schemaPath)) fail.push("Missing backend/prisma/schema.prisma."); if (!exists(migrationsDir)) fail.push("Missing backend/prisma/migrations directory."); const migrationFolders = exists(migrationsDir) ? fs.readdirSync(migrationsDir).filter((entry) => fs.statSync(path.join(migrationsDir, entry)).isDirectory()).sort() : []; const migrationSql = migrationFolders.map((folder) => ({ folder, path: path.join(migrationsDir, folder, "migration.sql"), })).filter((item) => exists(item.path)); if (migrationSql.length === 0) { fail.push("No Prisma migration.sql files were found."); } const legacyDuplicateInitMigration = "20260606200000_init_project_workspace"; if (migrationFolders.includes(legacyDuplicateInitMigration)) { fail.push(`${legacyDuplicateInitMigration} must not exist. It is an obsolete duplicate init migration that recreates ProjectStatus and breaks fresh Docker database startup.`); } const initMigrations = migrationSql.filter((item) => item.folder.includes("init_project_workspace")); if (initMigrations.length !== 1) { fail.push(`Expected exactly one init_project_workspace migration, found ${initMigrations.length}: ${initMigrations.map((item) => item.folder).join(", ") || "none"}.`); } const initIndex = migrationSql.findIndex((item) => item.folder.includes("init_project_workspace")); if (initIndex < 0) { fail.push("No init_project_workspace migration found."); } else if (initIndex !== 0) { fail.push(`Init migration must be first, but it is at position ${initIndex + 1}: ${migrationSql[initIndex].folder}.`); } const projectTableIndex = migrationSql.findIndex((item) => read(item.path).includes('CREATE TABLE "Project"')); const captureTableIndex = migrationSql.findIndex((item) => read(item.path).includes('CREATE TABLE "CaptureItem"')); if (projectTableIndex < 0) fail.push('No migration creates "Project".'); if (captureTableIndex >= 0 && projectTableIndex >= 0 && projectTableIndex > captureTableIndex) { fail.push('Migration order is invalid: "CaptureItem" appears before "Project".'); } const schema = exists(schemaPath) ? read(schemaPath) : ""; const duplicateFields = []; const modelBlocks = [...schema.matchAll(/model\s+(\w+)\s+\{([\s\S]*?)\n\}/g)]; for (const [, modelName, body] of modelBlocks) { const seen = new Set(); for (const rawLine of body.split("\n")) { const line = rawLine.trim(); if (!line || line.startsWith("@@") || line.startsWith("//")) continue; const fieldName = line.split(/\s+/)[0]; if (!fieldName || fieldName.startsWith("@")) continue; if (seen.has(fieldName)) duplicateFields.push(`${modelName}.${fieldName}`); seen.add(fieldName); } } if (duplicateFields.length > 0) fail.push(`Prisma schema contains duplicate field definitions: ${duplicateFields.join(", ")}.`); if (!schema.includes("linux-musl-openssl-3.0.x")) fail.push("Prisma generator must include linux-musl-openssl-3.0.x binaryTarget for the Docker Alpine runtime."); for (const model of ["Project", "CaptureItem", "AcceptedTruthItem", "CurrentStateReport", "DocumentationItem", "BlueprintGraph", "BlueprintNode", "BlueprintNodeRun", "BrainstormSession", "Initiative"]) { if (!schema.includes(`model ${model} `)) fail.push(`Schema is missing model ${model}.`); } const migrationText = migrationSql.map((item) => read(item.path)).join("\n"); for (const table of ["Project", "CaptureItem", "AcceptedTruthItem", "CurrentStateReport", "DocumentationItem", "BlueprintGraph", "BlueprintNode", "BlueprintNodeRun", "BrainstormSession", "Initiative"]) { if (!migrationText.includes(`"${table}"`)) fail.push(`Migrations do not reference table ${table}.`); } if (exists(rootPackagePath)) { const rootPackage = JSON.parse(read(rootPackagePath)); if (rootPackage.packageManager !== "pnpm@11.5.2") { fail.push("Root package.json must pin packageManager to pnpm@11.5.2 for reproducible Docker/Corepack builds."); } if (rootPackage.pnpm) { fail.push("Root package.json must not contain a legacy pnpm field; pnpm 11 ignores it and emits Docker build warnings. Keep approvals in pnpm-workspace.yaml allowBuilds."); } } else { fail.push("Missing root package.json."); } if (exists(workspacePath)) { const workspaceAllowedBuilds = parseWorkspaceAllowBuilds(read(workspacePath)); for (const dependency of requiredPnpmBuiltDeps) { if (!workspaceAllowedBuilds.has(dependency)) { fail.push(`pnpm-workspace.yaml allowBuilds must set ${dependency}: true for Docker installs with pnpm 11.`); } } } else { fail.push("Missing pnpm-workspace.yaml."); } if (exists(dockerComposePath)) { const compose = read(dockerComposePath); if (!compose.includes("projectbrain-postgres")) fail.push("docker-compose.yml does not define projectbrain-postgres."); if (!compose.includes("PROJECTBRAIN_DATA_SOURCE")) fail.push("docker-compose.yml does not configure PROJECTBRAIN_DATA_SOURCE."); if (!compose.includes("VITE_PROJECTBRAIN_CLIENT_MODE")) warn.push("docker-compose.yml does not pass VITE_PROJECTBRAIN_CLIENT_MODE to frontend build args."); if (!compose.includes("PROJECTBRAIN_SEED_ON_START")) warn.push("docker-compose.yml does not expose PROJECTBRAIN_SEED_ON_START."); if (!compose.includes("/api/system/readiness")) fail.push("docker-compose.yml does not healthcheck /api/system/readiness."); if (!compose.includes("condition: service_healthy")) fail.push("docker-compose.yml does not wait for healthy backend before frontend startup."); } else { fail.push("Missing docker-compose.yml."); } if (exists(backendDockerfilePath)) { const dockerfile = read(backendDockerfilePath); if (!dockerfile.includes("prisma:generate")) fail.push("Backend Dockerfile does not run prisma:generate during build."); if (!dockerfile.includes("docker-entrypoint.sh")) fail.push("Backend Dockerfile does not use the database-aware docker entrypoint."); if (!dockerfile.includes("/api/system/readiness")) fail.push("Backend Dockerfile does not expose a system readiness healthcheck."); if (!dockerfile.includes("apk add --no-cache") || !dockerfile.includes("openssl")) fail.push("Backend Dockerfile does not install OpenSSL; Prisma migrations fail at runtime on Alpine without it."); if (!dockerfile.includes("libc6-compat")) fail.push("Backend Dockerfile does not install libc6-compat for native engine compatibility on Alpine."); } else { fail.push("Missing backend/Dockerfile."); } if (exists(backendEntrypointPath)) { const entrypoint = read(backendEntrypointPath); if (!entrypoint.includes("prisma migrate deploy")) fail.push("Backend entrypoint does not run prisma migrate deploy."); if (!entrypoint.includes("PROJECTBRAIN_SEED_ON_START")) fail.push("Backend entrypoint does not support PROJECTBRAIN_SEED_ON_START."); if (!entrypoint.includes("20260606200000_init_project_workspace")) fail.push("Backend entrypoint must guard against the obsolete duplicate init migration when older overlay deployments left it on disk or in _prisma_migrations."); if (!entrypoint.includes("prisma migrate resolve --rolled-back 20260606200000_init_project_workspace")) fail.push("Backend entrypoint must resolve failed obsolete duplicate init migration records as rolled back before migrate deploy."); } else { fail.push("Missing backend/scripts/docker-entrypoint.sh."); } if (!exists(systemReadinessRoutePath)) { fail.push("Missing backend/src/routes/systemReadiness.ts live readiness route."); } else { const route = read(systemReadinessRoutePath); if (!route.includes('/api/system/readiness')) fail.push("System readiness route does not expose /api/system/readiness."); if (!route.includes('prisma.$queryRaw`SELECT 1`')) fail.push("System readiness route does not run a live Prisma SELECT 1 probe."); } if (exists(appPath)) { const app = read(appPath); if (!app.includes("registerSystemReadinessRoutes")) fail.push("Fastify app does not register system readiness routes."); if (!app.includes("registerBrainstormRoutes")) fail.push("Fastify app does not register Brainstorm CPU routes."); if (!app.includes("registerInitiativeRoutes")) fail.push("Fastify app does not register Initiative routes."); } else { fail.push("Missing backend/src/app.ts."); } if (!exists(initiativeRoutesPath)) { fail.push("Missing backend/src/routes/initiatives.ts."); } else { const routes = read(initiativeRoutesPath); for (const signal of [ "/api/projects/:projectId/initiatives", "/api/projects/:projectId/initiatives/:initiativeId", "/api/projects/:projectId/initiatives/:initiativeId/feature-candidates", ]) { if (!routes.includes(signal)) fail.push(`Initiative routes are missing ${signal}.`); } } if (!exists(initiativeRepoPath)) { fail.push("Missing backend/src/repositories/initiativesRepository.ts."); } else { const repo = read(initiativeRepoPath); for (const symbol of ["createInitiativeForProject", "updateInitiativeForProject", "getInitiativeForProject", "createFeatureCandidatesForInitiative", "syncInitiativeReviewAction"]) { if (!repo.includes(symbol)) fail.push(`Initiative repository is missing ${symbol}.`); } } if (!exists(initiativeSmokePath)) fail.push("Missing backend/scripts/smoke-initiatives.mjs."); else { const smoke = read(initiativeSmokePath); if (!smoke.includes("/review-queue/${firstCandidate.reviewItemId}/accept")) fail.push("Initiative smoke does not validate review accept materialization."); if (!smoke.includes("initiativeSync")) fail.push("Initiative smoke does not assert initiativeSync output."); } const result = { status: fail.length === 0 ? "PASS" : "FAIL", migrationRoots: [path.relative(root, migrationsDir).replaceAll("\\", "/")], migrations: migrationSql.map((item) => item.folder), initMigration: initIndex >= 0 ? migrationSql[initIndex]?.folder : null, checks: { projectTableBeforeCapture: projectTableIndex >= 0 && (captureTableIndex < 0 || projectTableIndex < captureTableIndex), backendEntrypoint: exists(backendEntrypointPath), dockerCompose: exists(dockerComposePath), schema: exists(schemaPath), systemReadinessRoute: exists(systemReadinessRoutePath), legacyDuplicateInitMigrationAbsent: !migrationFolders.includes("20260606200000_init_project_workspace"), exactlyOneInitMigration: initMigrations.length === 1, initiativeRoutes: exists(initiativeRoutesPath), initiativeSmoke: exists(initiativeSmokePath), duplicateSchemaFields: duplicateFields.length === 0, prismaAlpineBinaryTarget: schema.includes("linux-musl-openssl-3.0.x"), backendDockerfileInstallsOpenSSL: exists(backendDockerfilePath) && read(backendDockerfilePath).includes("openssl"), pnpmDockerBuildScriptsApproved: exists(workspacePath) && (() => { const allowedBuilds = parseWorkspaceAllowBuilds(read(workspacePath)); return requiredPnpmBuiltDeps.every((dependency) => allowedBuilds.has(dependency)); })(), }, warnings: warn, failures: fail, }; if (process.argv.includes("--json")) { console.log(JSON.stringify(result, null, 2)); } else { console.log(`ProjectBrain database readiness: ${result.status}`); console.log(`Migration roots checked: ${result.migrationRoots.join(", ")}`); console.log(`Migrations: ${migrationSql.length}`); console.log(`Init migration: ${result.initMigration ?? "missing"}`); if (warn.length > 0) { console.log("Warnings:"); for (const item of warn) console.log(`- ${item}`); } if (fail.length > 0) { console.log("Failures:"); for (const item of fail) console.log(`- ${item}`); } } if (fail.length > 0) process.exit(1);