164 lines
6.7 KiB
JavaScript
164 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const root = process.cwd();
|
|
const errors = [];
|
|
const localArtifacts = [];
|
|
const info = [];
|
|
const showLocalArtifacts =
|
|
process.env.PROJECTBRAIN_AUDIT_VERBOSE_LOCAL_ARTIFACTS === "1";
|
|
|
|
const requireFile = (path, label = path) => {
|
|
if (!existsSync(join(root, path))) errors.push(`Missing ${label}: ${path}`);
|
|
};
|
|
|
|
const requireText = (path, text, label = text) => {
|
|
const fullPath = join(root, path);
|
|
if (!existsSync(fullPath)) {
|
|
errors.push(`Missing file for text check: ${path}`);
|
|
return;
|
|
}
|
|
const content = readFileSync(fullPath, "utf8");
|
|
if (!content.includes(text)) errors.push(`Missing ${label} in ${path}`);
|
|
};
|
|
|
|
const warnIfExists = (path, label = path) => {
|
|
if (existsSync(join(root, path))) localArtifacts.push(`Generated or legacy artifact present: ${label}`);
|
|
};
|
|
|
|
const rootPackage = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
const backendPackage = JSON.parse(readFileSync(join(root, "backend/package.json"), "utf8"));
|
|
|
|
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;
|
|
};
|
|
const workspaceAllowBuilds = existsSync(join(root, "pnpm-workspace.yaml"))
|
|
? parseWorkspaceAllowBuilds(readFileSync(join(root, "pnpm-workspace.yaml"), "utf8"))
|
|
: new Set();
|
|
|
|
if (rootPackage.packageManager !== "pnpm@11.5.2") {
|
|
errors.push("Root package.json must pin packageManager to pnpm@11.5.2 so Docker/Corepack does not float to a different pnpm release.");
|
|
}
|
|
const requiredPnpmBuiltDeps = ["@prisma/client", "@prisma/engines", "@swc/core", "esbuild", "prisma"];
|
|
for (const dependency of requiredPnpmBuiltDeps) {
|
|
if (!workspaceAllowBuilds.has(dependency)) {
|
|
errors.push(`pnpm-workspace.yaml allowBuilds must approve ${dependency} for Docker builds with pnpm 11.`);
|
|
}
|
|
}
|
|
|
|
console.log("ProjectBrain doctor");
|
|
console.log("===================");
|
|
|
|
[
|
|
"README.md",
|
|
".env.example",
|
|
"backend/.env.example",
|
|
"docs/PROJECTBRAIN_DOCUMENTATION_INDEX.md",
|
|
"docs/FULL_VERSION_ROADMAP.md",
|
|
"docs/VERTICAL_SLICE_CORE_FLOW_PLAN.md",
|
|
"docs/PROJECTBRAIN_STATUS_AND_GATES.md",
|
|
"docs/P2_LOCAL_RUNTIME_VERIFICATION.md",
|
|
"docs/RUNTIME_ROUND_5_BACKEND_HEALTH_AND_SUITE.md",
|
|
"docs/PROJECTBRAIN_RUNTIME_COMMANDS.md",
|
|
"backend/scripts/smoke-p2.mjs",
|
|
"backend/scripts/smoke-fixture-api.mjs",
|
|
"backend/scripts/smoke-real-read-pilot.mjs",
|
|
"backend/scripts/smoke-vertical-slice.mjs",
|
|
"backend/scripts/smoke-runtime-suite.mjs",
|
|
"backend/scripts/docker-entrypoint.sh",
|
|
"scripts/check-database-readiness.mjs",
|
|
"public/site.webmanifest",
|
|
"public/favicon.svg",
|
|
"public/favicon.ico",
|
|
].forEach((file) => requireFile(file));
|
|
|
|
[
|
|
["validate:all", "root validation"],
|
|
["audit:release", "release hygiene audit"],
|
|
["audit:runtime", "runtime readiness audit"],
|
|
["audit:database", "database readiness audit"],
|
|
["smoke:backend:fixtures", "fixture smoke"],
|
|
["smoke:backend:real-read-pilot", "real-read pilot smoke"],
|
|
["smoke:backend:vertical-slice", "vertical slice smoke"],
|
|
["smoke:backend:runtime-suite", "runtime suite smoke"],
|
|
["smoke:backend:database-suite", "database suite smoke"],
|
|
["smoke:backend:brainstorms", "Brainstorm CPU smoke"],
|
|
["smoke:backend:initiatives", "Initiative smoke"],
|
|
].forEach(([script, label]) => {
|
|
if (!rootPackage.scripts?.[script]) errors.push(`Missing ${label} script: ${script}`);
|
|
});
|
|
|
|
[
|
|
["smoke:p2", "P2 database smoke"],
|
|
["smoke:fixtures", "fixture smoke"],
|
|
["smoke:real-read-pilot", "real read smoke"],
|
|
["smoke:vertical-slice", "vertical slice smoke"],
|
|
["smoke:runtime-suite", "runtime suite smoke"],
|
|
["smoke:database-suite", "database suite smoke"],
|
|
["smoke:brainstorms", "Brainstorm CPU smoke"],
|
|
["smoke:initiatives", "Initiative smoke"],
|
|
["db:deploy", "Prisma migrate deploy"],
|
|
["db:status", "Prisma migration status"],
|
|
["db:validate", "Prisma schema validate"],
|
|
].forEach(([script, label]) => {
|
|
if (!backendPackage.scripts?.[script]) errors.push(`Missing backend ${label} script: ${script}`);
|
|
});
|
|
|
|
requireText("index.html", "<title>ProjectBrain</title>", "ProjectBrain title");
|
|
requireText("index.html", "ProjectBrain", "ProjectBrain browser metadata");
|
|
requireText("src/features/projects/pages/RuntimeReadiness.tsx", "Backend health", "backend health card");
|
|
requireText("src/features/projects/pages/RuntimeReadiness.tsx", "Database readiness", "database readiness card");
|
|
requireText("src/features/projects/pages/RuntimeReadiness.tsx", "Vertical slice smoke", "vertical slice smoke card");
|
|
requireText("src/shared/api/realClient.ts", "getHealth", "real backend health client");
|
|
requireText("src/shared/api/realClient.ts", "getSystemReadiness", "real backend system readiness client");
|
|
requireText("src/shared/hooks/useProjectData.ts", "useBackendHealthData", "backend health hook");
|
|
requireText("src/shared/hooks/useProjectData.ts", "useBackendSystemReadinessData", "backend system readiness hook");
|
|
requireText("docs/PROJECTBRAIN_STATUS_AND_GATES.md", "P2", "P2 gate documentation");
|
|
|
|
[
|
|
"node_modules",
|
|
"backend/node_modules",
|
|
"dist",
|
|
"backend/dist",
|
|
"checkpoints",
|
|
"src/App.css",
|
|
"src/globals.css",
|
|
].forEach((path) => warnIfExists(path));
|
|
|
|
info.push(`Package manager: ${rootPackage.packageManager ?? "not pinned"}`);
|
|
info.push(`Approved pnpm workspace allowBuilds: ${Array.from(workspaceAllowBuilds).sort().join(", ") || "none"}`);
|
|
info.push(`Frontend package: ${rootPackage.name}@${rootPackage.version}`);
|
|
info.push(`Backend package: ${backendPackage.name}@${backendPackage.version}`);
|
|
info.push("Recommended local command order:");
|
|
info.push(" 1. pnpm install");
|
|
info.push(" 2. pnpm run audit:database");
|
|
info.push(" 3. pnpm run validate:all");
|
|
info.push(" 4. pnpm run audit:all");
|
|
info.push(" 5. docker compose up -d --build");
|
|
info.push(" 6. pnpm run smoke:backend:system-readiness");
|
|
info.push(" 7. pnpm run smoke:backend:runtime-suite && pnpm --filter projectbrain-api run smoke:blueprints");
|
|
info.push(" 8. pnpm run smoke:backend:database-suite");
|
|
|
|
for (const line of info) console.log(`INFO ${line}`);
|
|
if (showLocalArtifacts) {
|
|
for (const line of localArtifacts) console.log(`INFO ${line}`);
|
|
}
|
|
for (const line of errors) console.error(`FAIL ${line}`);
|
|
|
|
if (errors.length) {
|
|
console.error(`\nFAIL: ${errors.length} blocking doctor issue(s) found.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log("\nPASS: ProjectBrain doctor found no blocking runtime/release scaffold issues.");
|