diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a4266e89..acea5eda1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,6 +153,9 @@ jobs: - name: PR policy self-test run: npm run check:pr-policy + - name: Gate-manifest self-test + run: npm run check:gate-manifest + - name: Codebase index coverage run: npm run docs:check-index @@ -177,6 +180,20 @@ jobs: - name: Brand asset check run: npm run brand:check + # More generated-output drift detectors that were local-only in + # verify:cheap — same rationale as the design-system guards above. Without + # them a stale site map, therapy index, or design-system contract merged + # green because no workflow ran them. The gate-manifest self-test now fails + # CI if this list drifts from verify:cheap again. + - name: Site map drift + run: npm run sitemap:check + + - name: Therapy data index drift + run: npm run check:therapy-data-index + + - name: Design-system contract + run: npm run check:design-system-contract + # Fails if a SECURITY DEFINER public function is left executable by # PUBLIC/anon (privilege-escalation / cross-tenant read surface). - name: Function-grant guard diff --git a/package.json b/package.json index 979f266b3..7bce24b72 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "test:e2e:visual": "node scripts/run-playwright.mjs --config=playwright.visual.config.ts", "test:cross-tenant:staging": "node scripts/run-tsx.mjs scripts/test-cross-tenant-staging.ts", "verify:cheap": "node scripts/run-heavy.mjs --npm-script verify:cheap:internal", - "verify:cheap:internal": "npm run check:runtime && npm run check:github-actions && npm run check:ci-scope && npm run check:ci-triage && npm run check:pr-policy && npm run sitemap:check && npm run docs:check-index && npm run check:knip && npm run check:maintainability-budgets && npm run brand:check && npm run check:therapy-data-index && npm run check:type-scale && npm run check:icon-scale && npm run check:design-system-contract && npm run check:function-grants && npm run check:owner-scope && npm run lint && npm run typecheck && npm run test", + "verify:cheap:internal": "npm run check:runtime && npm run check:github-actions && npm run check:ci-scope && npm run check:ci-triage && npm run check:pr-policy && npm run check:gate-manifest && npm run sitemap:check && npm run docs:check-index && npm run check:knip && npm run check:maintainability-budgets && npm run brand:check && npm run check:therapy-data-index && npm run check:type-scale && npm run check:icon-scale && npm run check:design-system-contract && npm run check:function-grants && npm run check:owner-scope && npm run lint && npm run typecheck && npm run test", "verify:pr-local": "node scripts/verify-pr-local.mjs", "verify:ui": "npm run check:runtime && npm run test:e2e:pr", "verify:release": "npm run check:runtime && npm run lint && npm run typecheck && npm run test && npm run build && npm run test:e2e && npm run check:production-readiness && npm run governance:release && npm run eval:quality:release", @@ -57,6 +57,7 @@ "check:maintainability-budgets": "node scripts/check-maintainability-budgets.mjs", "check:ci-scope": "node scripts/ci-change-scope.mjs --self-test", "check:ci-triage": "node scripts/ci-triage.mjs --self-test", + "check:gate-manifest": "node scripts/check-gate-manifest.mjs", "check:pr-policy": "node scripts/pr-policy.mjs --self-test && node scripts/check-pr-policy-workflow.mjs", "check:env-parity": "node scripts/check-env-parity.mjs", "sweep:branch-ledger": "node scripts/sweep-branch-ledger.mjs", diff --git a/scripts/check-gate-manifest.mjs b/scripts/check-gate-manifest.mjs new file mode 100644 index 000000000..abbfb2a40 --- /dev/null +++ b/scripts/check-gate-manifest.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node +// Gate-manifest self-test (maturity L3). +// +// Invariant: every static gate in the local `verify:cheap:internal` chain must +// also run in CI. Without this, a gate added to the local chain can be silently +// missed in `.github/workflows/ci.yml`, so a regression it would catch merges +// green because no workflow runs it. This has already happened twice — the +// `static-pr` job comment records type/icon/brand being promoted after that +// exact miss, and sitemap/therapy-data-index/design-system-contract were a +// second instance. This check makes the drift a hard CI failure instead. +// +// Direction is one-way on purpose: CI may run MORE than the local chain (e.g. +// `format:check`, or heavier build/e2e gates in other jobs). It must never run +// LESS of the local static set. +import { readFileSync } from "node:fs"; + +const pkg = JSON.parse(readFileSync("package.json", "utf8")); +const ci = readFileSync(".github/workflows/ci.yml", "utf8"); + +const localChain = pkg.scripts?.["verify:cheap:internal"] ?? ""; +const localGates = [...localChain.matchAll(/npm run ([\w:.-]+)/g)].map((m) => m[1]); +if (localGates.length === 0) { + console.error("gate-manifest: could not parse verify:cheap:internal from package.json."); + process.exit(1); +} + +// The `npm run