Quality Gates
Documenta os checks automáticos que protegem a qualidade do código. Atualizado em: 11/set/2026 — pre-commit vira o tier rápido; suíte unitária + integração passam para o pre-push (big-fat-refactor-03, Lane A).
Fonte da verdade
Cada gate vive uma única vez no registry scripts/gates.registry (id|stages|command|scope|tier|category|blocking). Desde #638 o registry é doc-only — o runner scripts/gates.sh foi removido e nenhum runner executa os gates declarados aqui em runtime. O pre-commit roda os gates via bash scripts/quality-gates.sh <docs|config|code> (runner linear — ver .husky/pre-commit); o pre-push chama o mesmo runner com a categoria push; o CI chama o comando de cada gate diretamente por job (ver .github/workflows/ci.yml, cujo header mapeia job → gates).
| Runner | Onde roda |
|---|---|
bash scripts/quality-gates.sh <docs|config|code> | Pre-commit (.husky/pre-commit) — gates adaptativos por categoria |
bash scripts/quality-gates.sh push | Pre-push (.husky/pre-push) — tier pesado local: code + contagem de testes + unit + integração |
| Comandos diretos por job | CI (.github/workflows/ci.yml) — gate pesado autoritativo |
Cada commit passa por 3 camadas de verificação antes de chegar à produção:
[Commit] → pre-commit (gates rápidos por categoria) → [Push] → pre-push (changelog + drift + secret scan + suíte unitária + integração) → CI (gate pesado: lint, typecheck, unit, build, integração, E2E smoke, docs) → [Deploy manual]Pré-commit (local — .husky/pre-commit)
Roda a cada git commit. Bloqueia o commit se falhar. Gates adaptativos por categoria do diff: docs roda só os always; config adiciona typecheck; code adiciona o registro de error codes. A suíte unitária e a contagem de testes não rodam aqui — vivem no pre-push (ver abaixo), para que fazer um commit pequeno não seja a ação mais lenta do repositório.
Nível 0 — todas as categorias (rápidos)
| Gate | Comando | O que verifica |
|---|---|---|
| Lint-staged | npx lint-staged | Biome apenas nos arquivos staged |
| Biome | pnpm lint | Código inteiro (formatação + lint) |
as any regression | bash scripts/count-as-any.sh | Baseline em scripts/thresholds.sh |
| Routes SQL gate | bash scripts/no-sql-in-routes.sh | Sem SQL literal em rotas (#618) |
| Memo hooks | bash scripts/count-memo-hooks.sh | Sem useMemo/useCallback manuais novos |
| Boundary lint | bash scripts/boundary-lint.sh | Importações entre camadas |
| Portability boundary | bash scripts/no-cloudflare-in-core.sh | Sem runtime Cloudflare em workers/src/{domain,application,ports}; ratchet em {services,events} (ADR-0044) |
| Import lint | bash scripts/import-lint.sh | Restrições de import em routes/ |
| getDB call sites | bash scripts/count-getdb.sh | Contagem de getDB() no baseline |
| SQL×schema lint | bash scripts/schema-sql-lint.sh | SQL inconsistente com o schema |
| Docs lint | pnpm docs:lint | markdownlint |
| Index completeness | bash scripts/check-index-completeness.sh | Todo .md listado no index.md da pasta |
| Doc commands | bash scripts/verify-doc-commands.sh | Todo pnpm X documentado existe no package.json |
| A11y patterns | bash scripts/validate-a11y-specs.sh | Padrões de acessibilidade presentes no código |
Nível 1 — config + code
| Gate | Comando | O que verifica |
|---|---|---|
| Type check | pnpm typecheck | tsc --build — compilação TS inteira |
Nível 2 — code only
| Gate | Comando | O que verifica |
|---|---|---|
| Error-code registry | npx tsx scripts/validate-error-codes.ts | Todo HttpError usa código registrado (#634) |
Pré-push (local — .husky/pre-push)
Roda a cada git push. Desde a big-fat-refactor-03 (Lane A) é o tier pesado local: além dos gates rápidos do pre-commit, roda a suíte unitária completa, a suíte de integração e a contagem de testes.
| Gate | O que verifica |
|---|---|
Tier code completo | bash scripts/quality-gates.sh push roda, primeiro, tudo que o pre-commit roda (gates rápidos + typecheck + error codes) |
| Test count | bash scripts/verify-test-counts.sh — contagem de testes no baseline |
| Unit tests | pnpm -r test — Workers + App + Packages (~1766 testes) |
| Integração | pnpm test:integration — vitest-pool-workers + D1 (21 arquivos, ~4 s) |
| Changelog | bash scripts/validate-changelog.sh — tag v* tem entry no CHANGELOG |
| Doc drift mecânico | README/router.ts, README/permissões, configs referenciadas, version badge |
| Guarda online-only (#703) | deploy-ijcp.yml e deploy-demo.yml mantêm VITE_ONLINE_ONLY=true + scripts/verify-online-build.sh; turbo.json repassa VITE_ONLINE_ONLY; package.json/app/src/config.ts mantêm build:online/marcador BUILD_MODE |
| Secret scan | bash scripts/secret-scan.sh <base> — segredos adicionados no diff (diff-scoped) |
| Doc audit assíncrono | reasonix em background (não bloqueia o push) |
Por que o split (big-fat-refactor-03, Lane A). A suíte unitária inteira rodava a cada
git commit, o que fazia do commit a ação mais lenta do repositório e desencorajava commits pequenos e frequentes. Ela passou a rodar no pre-push: continua na máquina do desenvolvedor e continua antes de qualquer coisa sair dela, mas uma vez por push em vez de uma vez por commit. O CI reexecuta as suítes a cada push/PR (unit-tests-ci,integration-tests), então nada se perde a jusante.
verify-test-counts.shsaiu do pre-commit e também ganhou um step no jobcoredo CI: o pre-push é pulável com--no-verify, e sem o step no CI o ratchet viraria advisory por acidente.A guarda #703 foi adicionada em 2026-08-21 (#685/#703): cobre os vetores exatos que reverteram o demo do v1.0.5 para o build legado.
CI (GitHub Actions — .github/workflows/ci.yml)
Roda em todo push para main e em todo pull_request. É o gate pesado autoritativo (#633): cada job chama diretamente os comandos dos gates do estágio ci do registry — o header do ci.yml mapeia job → gates, e a união das jobs cobre o estágio ci inteiro.
Exceção: o job e2e se auto-pula quando o diff é docs-only (CHANGELOG.md, README.md, DESIGN.md, CONTEXT*.md, docs/**, .specs/**) — fail-open: qualquer dúvida sobre o base ⇒ o job roda. Detalhes: docs/research/pre-push-doc-drift-audit-e2e-docs-only-2026.md.
| Job | Gates do estágio ci | O que verifica |
|---|---|---|
security | semgrep | Semgrep (policy única: p/typescript + .semgrep/rules.yml) |
core | audit, lint, typecheck, unit-tests-ci, no-sql-in-routes, no-cloudflare-in-core, verify-test-counts, docs-lint, build-app, validate-csp, pre-deploy-check | Biome, tsc --build, testes unitários (paralelo entre workspaces), SQL em rotas, fronteira de portabilidade Cloudflare, ratchet de contagem de testes, markdownlint, dependências (advisory), build do app, CSP, pré-deploy |
integration | integration-tests | Testes de integração (vitest-pool-workers, D1) |
e2e | e2e-smoke + step test:e2e:online | Playwright smoke (9 specs críticas, playwright.smoke.config.ts) + online (09/12/13/14, playwright.online.config.ts) — job auto-pulado em diffs docs-only |
docs | docs-build, docs-links, docs-staleness | build do VitePress, dead links (lychee), staleness |
Ordem preservada nos steps de cada job: o
corerodabuild-appantes devalidate-csp/pre-deploy-check(eles leem oapp/public/_headersgerado pelo build); odocsrodadocs-buildantes dedocs-links(ele crawleiadocs/.vitepress/dist).⚠️ O CI não passa
--coverageaopnpm -r testporque o@vitest/coverage-v8é incompatível com o runtimeworkerdusado nos testes de integração do Workers.
Code Coverage
ℹ️ Gate informativo — badge + warning, não bloqueia merge (#643). O CI não roda cobertura (incompatibilidade do
@vitest/coverage-v8com o runtimeworkerd), então os thresholds só se aplicam localmente quando os testes rodam comVITEST_COVERAGE=1(modo full doscripts/ci-local.sh).
Como rodar:
VITEST_COVERAGE=1 pnpm -r test # gera app/coverage + workers/coverage
pnpm coverage:check # relatório informativo vs metasscripts/check-coverage.sh (re-criado em #643) compara os relatórios gerados contra as metas de scripts/thresholds.sh e sempre sai 0 — avisa, nunca bloqueia. Também roda como DA-005 no bash scripts/doc-audit.sh. O badge de cobertura no README aponta para o Codecov (app.codecov.io/gh/barateza/neemias).
Metas (espelhadas nos vitest.config.ts — enforcement local; comparação do check-coverage.sh lê de scripts/thresholds.sh):
| Escopo | Lines | Branches |
|---|---|---|
| app/ (global) | 20 | 15 |
| app/src/db/** | 80 | 70 |
| app/src/modules/auth/** | 25 | 15 |
| app/src/modules/sync/** | 40 | 35 |
| app/src/storage/** | 45 | 35 |
| workers/ (global) | 70 | 65 |
⚠️ Metas acima da cobertura atual são intencionais (ex.: workers ~61% lines / ~57% branches, sync ~30% lines):
pnpm coverage:checkavisa até a cobertura alcançá-las. Quando alcançar, levantar as metas emscripts/thresholds.sh+vitest.config.ts(manter os dois em sincronia).
Módulos (modules/)
Os módulos vivem no próprio monorepo, em modules/packages/ (workspace aninhado com lockfile próprio — @neemias/modules, pnpm 12.3.1). Não existe mais um repositório separado (barateza/neemias-modules foi de-submoduled e arquivado). O CI instala o workspace de módulos separadamente (pnpm --dir modules install --frozen-lockfile --config.autoInstallPeers=true — o valor precisa bater com o autoInstallPeers gravado em modules/pnpm-lock.yaml) e valida os módulos com os mesmos gates do core: pnpm lint + pnpm typecheck + pnpm -r test.
Deploy
Há dois workflows de deploy no CI (além do ci.yml de qualidade):
.github/workflows/deploy-demo.yml— auto-deploy da demo (app.neemias.app/api.neemias.app) em todo push paramain(ouworkflow_dispatch)..github/workflows/deploy-ijcp.yml— deploy da produção IJCP (ijcp.neemias.app/api-ijcp.neemias.app) por tagijcp-v*(ouworkflow_dispatch).
Ambos rodam no runner auto-hospedado: migrações D1 → worker → frontend (online-only, guardado por verify-online-build.sh — #703) → Pages → health. O deploy manual continua disponível como fallback:
- Worker:
pnpm deploy:worker(wrangler) - Docs:
pnpm deploy:docs(Cloudflare Pages) - Tudo:
bash scripts/deploy.sh(comVITE_ONLINE_ONLY=true+ guarda paraENV=prod)
Tags v* são validadas localmente pelo pre-push (gate Changelog). Documentação detalhada: Deployment
Histórico
| Data | Mudança | Issue |
|---|---|---|
| 2026-09-11 | Pre-commit vira o tier rápido; suíte unitária + integração + contagem de testes passam para o pre-push; contagem de testes ganha step no job core do CI (o pre-push é pulável com --no-verify); novo gate no-cloudflare-in-core (ADR-0044) com ratchet CF_CORE_BASELINE | big-fat-refactor-03 |
| 2026-08-11 | Coverage vira gate informativo: check-coverage.sh recriado (compara vs metas em thresholds.sh, nunca bloqueia), thresholds por diretório no app/vitest.config.ts (db 80/70, auth 25/15, sync 40/35, storage 45/35), workers 70/65, badge Codecov no README, DA-005 no doc-audit.sh | #643 |
| 2026-08-11 | RTM removido (scripts generate-rtm.ts, generate-rtm-from-code.sh, ci-validate-rtm.sh + artefatos gerados); drift i18n com tolerância de 5 commits (1→5); staleness 30→90 dias; doc-audit.sh reduzido a route count vs README; CI docs job perde rtm-freshness e docs-i18n-fixture | #640 |
| 2026-08-11 | Baseline store baselines.sh + baselines.registry removidos; thresholds em scripts/thresholds.sh (4 constantes); spec-status.sh simplificado para listagem plana | #639 |
| 2026-08-11 | CI chama comandos diretamente por job; runner scripts/gates.sh removido; gates.registry vira doc-only; pre-commit usa quality-gates.sh | #638 |
| 2026-08-10 | CI vira o gate pesado autoritativo (lint/typecheck/unit/build/integração/E2E/docs via fatias do estágio ci); pre-push encolhe para changelog + doc-drift + secret scan; meta-gate de cobertura de CI no gates.sh verify | #633 |
| 2026-08-09 | Quality-gates reconciliado com a realidade; meta-gate de wiring no gates.sh verify | #634 |
| 2026-08-09 | error-code-registry, a11y-specs, index-completeness, doc-commands wired no pre-commit; rtm-freshness no CI | #634 |
| 2026-08-09 | check-coverage.sh e mutation-matrix.sh retirados | #634 |
| 2026-08-09 | Registry declarativo scripts/gates.registry + runner scripts/gates.sh | #630 |
| 2026-08-09 | Baseline store scripts/baselines.registry + baselines.sh | #631 |
| 2026-06-21 | Typecheck adicionado ao pre-commit | #193 |
| 2026-06-21 | count-as-any adicionado ao pre-commit + CI | #196 |
| 2026-06-19 | CI workflow inicial | — |