import { readFile } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; const rootDir = process.cwd(); const rules = [ { file: "packages/api/src/sse/event-bus.ts", required: [], forbidden: [ { pattern: /\bRoleSseAudience\b/, message: "role-based SSE audience types must not reappear" }, { pattern: /\broleAudience\s*\(/, message: "role-derived SSE audiences must not be emitted" }, { pattern: /\bBROADCAST_SENT\b/, message: "broadcast SSE event resurrection needs explicit architecture review" }, ], }, { file: "packages/api/src/sse/subscription-policy.ts", required: [ { pattern: /\bderiveUserSseSubscription\b/, message: "subscription derivation must stay centralized in deriveUserSseSubscription", }, ], forbidden: [ { pattern: /\broleAudience\s*\(/, message: "subscription policy must not derive role audiences" }, ], }, { file: "apps/web/src/app/api/sse/timeline/route.ts", required: [ { pattern: /\bderiveUserSseSubscription\s*\(/, message: "timeline SSE route must derive audiences server-side from the authenticated user", }, ], forbidden: [ { pattern: /\bsearchParams\b/, message: "timeline SSE route must not accept client-provided audience scoping" }, { pattern: /\baudience\b/, message: "timeline SSE route must not parse raw audience values from the client" }, ], }, { file: "docker-compose.prod.yml", required: [ { pattern: /image:\s+\$\{APP_IMAGE:\?set APP_IMAGE\}/, message: "production compose must deploy the immutable app image", }, { pattern: /image:\s+\$\{MIGRATOR_IMAGE:\?set MIGRATOR_IMAGE\}/, message: "production compose must deploy the immutable migrator image", }, { pattern: /http:\/\/localhost:3000\/api\/ready/, message: "production compose must gate app health on the readiness endpoint", }, { pattern: /RATE_LIMIT_BACKEND:\s+\$\{RATE_LIMIT_BACKEND:-redis\}/, message: "production compose must intentionally pin the Redis-backed rate-limit path", }, ], forbidden: [ { pattern: /\bbuild:/, message: "production compose must not build application images on the host" }, ], }, { file: ".github/workflows/release-image.yml", required: [ { pattern: /push:\s*\n\s*branches:\s*\[main\]/, message: "image releases must build automatically on pushes to main", }, { pattern: /workflow_dispatch:/, message: "image release must remain manually callable for rebuilds and tag overrides", }, { pattern: /target:\s+runner/, message: "release workflow must keep publishing the runner image", }, { pattern: /target:\s+migrator/, message: "release workflow must keep publishing the migrator image", }, ], forbidden: [], }, { file: ".github/workflows/deploy-staging.yml", required: [ { pattern: /docker-compose\.prod\.yml tooling\/deploy/, message: "staging deploy must ship the canonical production compose bundle", }, ], forbidden: [], }, { file: ".github/workflows/deploy-prod.yml", required: [ { pattern: /docker-compose\.prod\.yml tooling\/deploy/, message: "production deploy must ship the canonical production compose bundle", }, ], forbidden: [], }, { file: "tooling/deploy/deploy-compose.sh", required: [ { pattern: /COMPOSE_FILE="\$\{COMPOSE_FILE:-docker-compose\.prod\.yml\}"/, message: "deploy script must default to the canonical production compose file", }, { pattern: /READY_URL="\$\{READY_URL:-http:\/\/127\.0\.0\.1:\$\{APP_HOST_PORT:-3000\}\/api\/ready\}"/, message: "deploy script must wait on the readiness endpoint", }, { pattern: /docker compose -f "\$\{COMPOSE_FILE\}" config -q/, message: "deploy script must validate the rendered compose file before pulling images", }, ], forbidden: [], }, ]; const violations = []; for (const rule of rules) { const absolutePath = path.join(rootDir, rule.file); const source = await readFile(absolutePath, "utf8"); for (const requirement of rule.required) { if (!requirement.pattern.test(source)) { violations.push(`${rule.file}: missing guardrail anchor: ${requirement.message}`); } } for (const forbidden of rule.forbidden) { if (forbidden.pattern.test(source)) { violations.push(`${rule.file}: forbidden pattern matched: ${forbidden.message}`); } } } if (violations.length > 0) { console.error("Architecture guardrail check failed:"); for (const violation of violations) { console.error(`- ${violation}`); } process.exit(1); } console.log("Architecture guardrails passed.");