Initial curated projectbrain source from 8a7b1e0651dbdab97d744cbd2e653ab3970ac6c5
This commit is contained in:
commit
5aa3e8d685
700 files changed
+132801
No files matched your search
@@ -0,0 +1,252 @@
|
||||
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);
|
||||
Reference in new issue
Block a user