mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 18:02:17 +03:00
feat(quality): quality-gate ratchet + anti-hallucination/rule-enforcement guardrails (Phases 0-6) (#3471)
* feat(quality): generic ratchet comparator (multi-metric, regression-only)
* chore(ci): Fase 0 quality-gate fixes — reconcile coverage gate (40->60), tier npm audit, wire orphaned contract gates, re-enable cheap husky pre-commit
* feat(quality): ratchet engine (collector + frozen baseline + CI job) and provider-consistency gate
- collect-metrics.mjs: emits quality-metrics.json (ESLint warnings + coverage when present)
- quality-baseline.json: frozen baseline (eslintWarnings=3482, regression-only)
- ci.yml: quality-gate job (ratchet + step summary + artifact) and check:provider-consistency in lint job
- check-provider-consistency.ts: every REGISTRY id must be a canonical provider (found krutrim half-registered → allowlisted as known pre-existing, blocks any NEW orphan)
- TDD: 9 tests (5 ratchet + 4 provider-consistency)
* feat(quality): Fase 2 anti-hallucination gates — fetch-targets, openapi-routes, deps allowlist
- check-fetch-targets: every dashboard fetch(/api/...) resolves to a real route.ts; found 7 pre-existing dashboard->route mismatches frozen as KNOWN_MISSING for triage
- check-openapi-routes: every openapi.yaml path resolves to a real route; found 1 stale spec entry (agent-bridge agents/{id}/state) frozen as KNOWN_STALE_SPEC
- check-deps: anti-slopsquatting allowlist (105 deps); new deps need explicit human-reviewed entry
- all wired into CI lint/docs jobs; TDD +12 tests (21 total across 5 gates)
* docs(quality): add quality-gates report + implementation plan to repo root
* feat(quality): Fase 3a — file-size ratchet (freeze 91 files >800 LOC, cap 800 for new)
- check-file-size.mjs: frozen files can only shrink; new files must be <= cap (kills the next 12k-line god-component)
- file-size-baseline.json: 91 files frozen at current LOC (largest 12883)
- wired into CI lint job; TDD 5 tests; --update ratchets the baseline down on shrink
* feat(quality): Fase 3b — duplication ratchet (jscpd@4, baseline 5.72%)
- check-duplication.mjs: runs jscpd@4 (pinned; v5 is an incompatible Rust rewrite) over src+open-sse, fails if duplication % rises vs frozen baseline (5.72%, measured: 1358 clones / 22967 dup lines). Targets the executor copy-paste (48/50 override execute() wholesale)
- wired into the parallel quality-gate CI job (off the lint critical path); TDD 4 tests; --update ratchets down
- snapshot now complete: coverage ~82.6%, eslint 3482 (98.5% no-explicit-any), duplication 5.72%, 91 files >800 LOC
* feat(quality): Fase 4a — anti test-masking gate
- check-test-masking.mjs: for each MODIFIED test file in a PR, flags net assert removal + new assert.ok(true) tautologies (base...HEAD diff). Directly enforces CLAUDE.md 'never weaken asserts to go green'
- wired into pr-test-policy CI job (reuses base fetch); no-op outside PR; TDD 5 tests
* feat(quality): Fase 4b — coverage ratchet (conservative floors, CI consumes merged coverage)
- quality-baseline.json: coverage.{statements,lines,functions,branches} floors (80/80/82/73, real ~82.58/82.58/84.23/75.22 with margin; tighten via --update after a green main run)
- check-quality-ratchet.mjs: --allow-missing (local quality:gate skips coverage.* without a coverage run; CI runs strict)
- ci.yml quality-gate job: needs test-coverage + downloads merged coverage-report so the ratchet enforces 'coverage cannot drop'
- TDD +1 test (6 total)
* feat(quality): Fase 6 — 8 new gates (Rule #11/#12, migrations, known-symbols, route-guard, complexity, docs-symbols, db-rules)
Deterministic gates, each freezing pre-existing violations in a documented allowlist (ratchet) so they pass now and block only NEW regressions:
- check-error-helper (Rule #12): 7 executors/handlers forwarding raw err.message frozen
- check-public-creds (Rule #11): 5 literal client_ids (Claude/Codex/Qwen/Kimi/Copilot) frozen
- check-migration-numbering: gaps 026/055 + dup 041 frozen (prevents the git-rm-deleted-migration incident)
- check-known-symbols: 93 executors conformance + 15 combo strategies + 18 translator pairs
- check-route-guard-membership (#15/#17): all 25 spawn-capable routes verified local-only (0 gaps)
- check-complexity: cyclomatic>15 / fn-length>80 ratchet (baseline 1739)
- check-docs-symbols: 30 stale doc /api refs frozen (docs hallucination)
- check-db-rules (#2/#5): 25 unexported db modules + 15 raw-SQL routes frozen
Wired into CI (lint / docs-sync-strict / quality-gate jobs). 115 TDD tests, all green. ESLint ratchet held at 3482.
* docs(quality): Phase 7 plan (security/dead-code/mutation/community tooling) — GATED to 2026-06-16
Stored, not active. 7 suggested gates + all discussed OSS/Community tools (SonarQube Community + osv-scanner + CodeQL + knip + sonarjs + type-coverage + lockfile-lint + Stryker + size-limit + axe-core + semcheck + agent-lsp + Qlty). Activation gate: do not start before 2026-06-16 (use Phases 0-6 in production for 1 week, validate in practice, then evolve).
This commit is contained in:
committed by
GitHub
parent
802f54916b
commit
5f2722bd6c
84
.github/workflows/ci.yml
vendored
84
.github/workflows/ci.yml
vendored
@@ -24,6 +24,12 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# tsx gates below (known-symbols, route-guard-membership) import modules that
|
||||
# open SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
|
||||
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
@@ -37,6 +43,16 @@ jobs:
|
||||
- run: npm run check:cycles
|
||||
- run: npm run check:route-validation:t06
|
||||
- run: npm run check:any-budget:t11
|
||||
- run: npm run check:provider-consistency
|
||||
- run: npm run check:fetch-targets
|
||||
- run: npm run check:deps
|
||||
- run: npm run check:file-size
|
||||
- run: npm run check:error-helper
|
||||
- run: npm run check:migration-numbering
|
||||
- run: npm run check:public-creds
|
||||
- run: npm run check:db-rules
|
||||
- run: npm run check:known-symbols
|
||||
- run: npm run check:route-guard-membership
|
||||
- run: npm run check:docs-sync
|
||||
- run: npm run typecheck:core
|
||||
# typecheck:noimplicit:core is a forward-looking gate (noImplicitAny).
|
||||
@@ -45,6 +61,46 @@ jobs:
|
||||
- run: npm run typecheck:noimplicit:core
|
||||
continue-on-error: true
|
||||
|
||||
quality-gate:
|
||||
name: Quality Ratchet
|
||||
runs-on: ubuntu-latest
|
||||
needs: test-coverage
|
||||
if: ${{ always() && needs.test-coverage.result == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
# Coverage mergeada (coverage-summary.json) p/ o ratchet de cobertura.
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/
|
||||
- run: npm run quality:collect
|
||||
# Catraca: falha se qualquer métrica regredir vs quality-baseline.json (commitado).
|
||||
# Hoje: contagem de warnings do ESLint. Fase 4 estende com cobertura (lida do
|
||||
# coverage mergeado). Tamanho de arquivo e duplicação têm gates dedicados.
|
||||
- name: Ratchet check
|
||||
run: node scripts/quality/check-quality-ratchet.mjs --summary .artifacts/quality-ratchet.md
|
||||
# Catraca de duplicação (jscpd@4 sobre src+open-sse). Roda neste job (paralelo)
|
||||
# para não pesar no caminho crítico do lint.
|
||||
- name: Duplication ratchet
|
||||
run: npm run check:duplication
|
||||
- name: Complexity ratchet
|
||||
run: npm run check:complexity
|
||||
- name: Append summary
|
||||
if: always()
|
||||
run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY"
|
||||
- name: Upload ratchet report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: quality-ratchet
|
||||
path: .artifacts/quality-ratchet.md
|
||||
if-no-files-found: warn
|
||||
|
||||
docs-sync-strict:
|
||||
name: Docs Sync (Strict)
|
||||
runs-on: ubuntu-latest
|
||||
@@ -56,6 +112,19 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:docs-all
|
||||
# Previously-orphaned contract gates (existed as files, never wired anywhere).
|
||||
# All exit 0 today: cli-i18n is a hard gate, openapi-coverage is a ratchet
|
||||
# (floor ~36), openapi-security-tiers is advisory (Hard Rules #15/#17).
|
||||
- name: CLI i18n consistency
|
||||
run: npm run check:cli-i18n
|
||||
- name: OpenAPI route coverage (ratchet)
|
||||
run: npm run check:openapi-coverage
|
||||
- name: OpenAPI security-tier consistency (advisory)
|
||||
run: npm run check:openapi-security-tiers
|
||||
- name: OpenAPI spec paths resolve to real routes (anti-hallucination)
|
||||
run: npm run check:openapi-routes
|
||||
- name: Doc /api refs resolve to real routes (anti-hallucination)
|
||||
run: npm run check:docs-symbols
|
||||
- name: i18n translation drift (warn)
|
||||
run: node scripts/i18n/check-translation-drift.mjs --warn
|
||||
|
||||
@@ -125,6 +194,9 @@ jobs:
|
||||
run: git fetch --no-tags origin "${GITHUB_BASE_REF}" --depth=1
|
||||
- name: Validate source changes include tests
|
||||
run: node scripts/check/check-pr-test-policy.mjs --summary-file .artifacts/pr-test-policy.md
|
||||
# Anti test-masking: flag net assert removal / new assert.ok(true) in changed tests.
|
||||
- name: Detect test-masking (weakened assertions)
|
||||
run: npm run check:test-masking
|
||||
- name: Publish PR test policy summary
|
||||
if: always()
|
||||
run: |
|
||||
@@ -360,10 +432,12 @@ jobs:
|
||||
find . -maxdepth 3 -type f | sort
|
||||
exit 1
|
||||
fi
|
||||
# Gate aligned to the project's local coverage bar (npm run test:coverage
|
||||
# uses 40/40/40/40). The previous 75/70 gate never ran on main (the
|
||||
# coverage shards always failed → this job was skipped), so it was never
|
||||
# actually enforced and is inconsistent with the repo's real standard.
|
||||
# Gate aligned to the project's local coverage bar: `npm run test:coverage`
|
||||
# gates at 60/60/60/60, so CI must match it (the previous CI floor of 40
|
||||
# silently undershot the local bar — a real drift). Real merged coverage is
|
||||
# ~79/79/82/75, so 60 is a conservative floor with headroom; the Fase-4
|
||||
# coverage ratchet (quality-baseline.json) layers "must not drop vs baseline"
|
||||
# on top of this floor.
|
||||
npx c8 report \
|
||||
--temp-directory coverage-shards \
|
||||
--reports-dir coverage \
|
||||
@@ -374,7 +448,7 @@ jobs:
|
||||
--exclude=tests/** \
|
||||
--exclude=**/*.test.* \
|
||||
--check-coverage \
|
||||
--statements 40 --lines 40 --functions 40 --branches 40
|
||||
--statements 60 --lines 60 --functions 60 --branches 60
|
||||
- name: Build coverage summary
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -203,8 +203,5 @@ pr_reviews*.json
|
||||
|
||||
# internal setup prompts with personal credentials — never commit
|
||||
CODEX-SETUP-PROMPT.md
|
||||
-home-diegosouzapw-dev-automações-bots-yt-downloader-20260504 .txt
|
||||
PLANO-QUALITY-GATES.md
|
||||
# Quality ratchet — métricas efêmeras (baseline é commitado, métricas não)
|
||||
quality-metrics.json
|
||||
RELATORIO-QUALITY-GATES.md
|
||||
-home-diegosouzapw-dev-automações-bots-yt-downloader-20260410 .txt
|
||||
|
||||
@@ -1,31 +1,12 @@
|
||||
# #!/usr/bin/env sh
|
||||
# if ! command -v npx >/dev/null 2>&1; then
|
||||
# echo "⚠️ npx not found in PATH — skipping pre-commit hooks"
|
||||
# echo " Run 'npm run lint && npm run check:any-budget:t11' manually before pushing."
|
||||
# exit 0
|
||||
# fi
|
||||
#!/usr/bin/env sh
|
||||
if ! command -v npx >/dev/null 2>&1; then
|
||||
echo "⚠️ npx not found in PATH — skipping pre-commit hooks"
|
||||
echo " Run 'npm run lint && npm run check:any-budget:t11' manually before pushing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# npx lint-staged
|
||||
# node scripts/check/check-docs-sync.mjs
|
||||
# npm run check:any-budget:t11
|
||||
|
||||
# # Strict env-doc sync (FASE 2)
|
||||
# node scripts/check/check-env-doc-sync.mjs
|
||||
|
||||
# # CLI i18n consistency check — all t() keys must exist in en.json (FASE 8.3)
|
||||
# node scripts/check/check-cli-i18n.mjs
|
||||
|
||||
# # i18n docs drift advisory (FASE 5) — warn-only on pre-commit; CI enforces strict.
|
||||
# node scripts/i18n/check-translation-drift.mjs --warn || \
|
||||
# echo "⚠️ i18n drift detected. Run 'npm run i18n:run' to update locale mirrors."
|
||||
|
||||
# # i18n UI coverage advisory (FASE 6) — pre-commit warns; CI enforces strict.
|
||||
# node scripts/i18n/check-ui-keys-coverage.mjs --threshold=80 || \
|
||||
# echo "⚠️ UI i18n coverage below 80% for at least one locale."
|
||||
|
||||
# # OpenAPI coverage check — fails if coverage < 99% (FASE 08 content audit)
|
||||
# node scripts/check/check-openapi-coverage.mjs
|
||||
|
||||
# # OpenAPI security tier consistency check — fails if x-loopback-only / x-always-protected
|
||||
# # annotations diverge from routeGuard.ts compile-time constants (FASE 08 content audit)
|
||||
# node scripts/check/check-openapi-security-tiers.mjs
|
||||
# Cheap, deterministic local gates (re-enabled). Slower checks (i18n drift,
|
||||
# openapi coverage/security-tiers, env-doc sync) run in CI to keep commits fast.
|
||||
npx lint-staged
|
||||
node scripts/check/check-docs-sync.mjs
|
||||
npm run check:any-budget:t11
|
||||
|
||||
159
PLANO-QUALITY-GATES-FASE7.md
Normal file
159
PLANO-QUALITY-GATES-FASE7.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Fase 7 — Quality Gates: Segurança, Dead-Code, Mutação & Ferramental Community
|
||||
|
||||
> **Para workers agênticos:** SUB-SKILL OBRIGATÓRIA: `superpowers:subagent-driven-development` (recomendado) ou `superpowers:executing-plans`, tarefa-a-tarefa. Cada tarefa aqui é um subsistema independente → **expandir em sub-plano bite-sized próprio** no momento da execução. Hard Rule #18 (TDD/VPS) em tudo.
|
||||
|
||||
> # ⏳ PORTÃO DE ATIVAÇÃO — NÃO INICIAR ANTES DE **2026-06-16**
|
||||
> **Este plano está GUARDADO, não ativo.** Decisão do owner (2026-06-09): finalizar 100% as Fases 0–6 (PR #3471), **usar em produção por 1 semana** para validar na prática, e só então evoluir. **Data cravada de início da Fase 7: 2026-06-16.** Não ativar antes — o objetivo da semana é coletar sinal real (falsos-positivos dos gates, custo de CI, atrito) antes de adicionar mais.
|
||||
> **Pré-condições para ativar:** (1) PR #3471 (Fases 0–6) mergeada e rodada ≥1 semana; (2) re-home do PR para `release/v3.8.18` resolvido; (3) as issues #3483–#3501 com decisões aplicadas ou conscientemente adiadas.
|
||||
|
||||
**Goal:** Maximizar a cobertura de quality gates do OmniRoute adicionando catracas de **segurança** (Sonar/osv/CodeQL → zero na timeline), **dead-code**, **complexidade cognitiva**, **type-coverage**, **mutação**, **bundle-size**, **a11y** e completando o anti-slopsquatting — usando **somente ferramentas Community/OSS** (projeto é open-source, zero SaaS pago, dados na box).
|
||||
|
||||
**Architecture:** Reusa o motor existente — toda métrica numérica entra como `{value, direction}` em `quality-baseline.json` (catraca só-regressão) ou vira um `scripts/check/check-*.mjs` dedicado (padrão `check-t11-any-budget.mjs`). Gates pesados vão no job paralelo `quality-gate`; gates rápidos no `lint`; mutação/visual em job nightly separado. Tudo só-regressão (sem flag-day).
|
||||
|
||||
**Tech Stack (tudo OSS/Community):** SonarQube **Community Build** (self-hosted) · osv-scanner (Google) · CodeQL (GitHub, grátis p/ público) · knip · eslint-plugin-sonarjs · type-coverage · lockfile-lint · dpdm · Stryker (`@stryker-mutator/*`) · size-limit · `@axe-core/playwright` · semcheck · agent-lsp (MCP) · Qlty CLI (OSS, opcional). ESLint 9 flat · c8 · Node native test runner · GitHub Actions.
|
||||
|
||||
---
|
||||
|
||||
## Princípio (igual às Fases 0–6)
|
||||
|
||||
Toda catraca é **só-regressão**: congela o baseline atual, bloqueia QUALQUER piora, decai a zero/melhor com o tempo via `--update`. Nenhum gate exige limpeza imediata (flag-day). Cada ferramenta nova que vira dependência **deve ser adicionada a `dependency-allowlist.json`** (o gate `check-deps` da Fase 2 vai exigir — é o ponto de revisão humana).
|
||||
|
||||
## Mapa de arquivos (criar/modificar)
|
||||
|
||||
| Arquivo | Responsabilidade |
|
||||
|---|---|
|
||||
| `quality-baseline.json` (modificar) | + `vulnCount`, `codeqlAlerts`, `sonarIssues`, `cognitiveComplexity`, `typeCoveragePct`, `deadExports` |
|
||||
| `scripts/quality/collect-metrics.mjs` (modificar) | + coletores: osv-scanner, CodeQL count, Sonar API, knip, type-coverage, sonarjs |
|
||||
| `scripts/check/check-vuln-ratchet.mjs` (criar) | osv-scanner → vulnCount (catraca) |
|
||||
| `scripts/check/check-dead-code.mjs` (criar) | knip → exports/files/deps mortos (catraca) |
|
||||
| `scripts/check/check-cognitive-complexity.mjs` + `eslint.sonarjs.config.mjs` (criar) | sonarjs/cognitive-complexity em config isolado (não polui o count principal) |
|
||||
| `scripts/check/check-type-coverage.mjs` (criar) | type-coverage % (catraca up) |
|
||||
| `scripts/check/check-lockfile.mjs` (criar) | lockfile-lint (host/https/integrity) |
|
||||
| `scripts/check/check-pr-evidence.mjs` (criar) | exige output de comando no corpo do PR (Rule #18 mecânica) |
|
||||
| `scripts/check/check-bundle-size.mjs` + `.size-limit.json` (criar) | size-limit → orçamento de bundle |
|
||||
| `tests/e2e/a11y.spec.ts` (criar) | `@axe-core/playwright` nas páginas-chave |
|
||||
| `stryker.conf.json` (criar) | mutação nos ~8 módulos críticos (nightly) |
|
||||
| `sonar-project.properties` (modificar) | remover `coverage`/`cpd` exclusions; ativar new-code gate |
|
||||
| `.github/workflows/ci.yml` (modificar) | wirar novos gates (lint / quality-gate / nightly) + `qualitygate.wait` no Sonar |
|
||||
| `semcheck.yaml` (criar) | semcheck: docs↔código (camada fuzzy LLM, opcional) |
|
||||
| `.mcp.json` / config de agentes (modificar) | registrar agent-lsp (LSP-in-the-loop) |
|
||||
| `dependency-allowlist.json` (modificar) | + osv-scanner, knip, sonarjs, type-coverage, lockfile-lint, stryker, size-limit, axe-core, dpdm |
|
||||
|
||||
---
|
||||
|
||||
## Tarefas (cada uma = 1 sub-plano bite-sized na execução)
|
||||
|
||||
### Task 1 — Ativar SonarQube Community + "Clean as You Code" (gate de segurança nativo)
|
||||
- **Tool:** SonarQube Community Build (self-hosted, grátis).
|
||||
- **Files:** `sonar-project.properties`, `.github/workflows/ci.yml` (job `sonarqube`).
|
||||
- **Approach:** setar secrets `SONAR_TOKEN`/`SONAR_HOST_URL`; **remover** `sonar.coverage.exclusions=**/*` e `sonar.cpd.exclusions=**/*` (hoje neutralizam o Sonar); ativar o quality gate **new-code / "Clean as You Code"** (código novo não pode adicionar issue/bug/vuln/hotspot; legado grandfathered) + adicionar `-Dsonar.qualitygate.wait=true` para **bloquear** o PR (hoje o job é inerte: secrets-gated, sem wait).
|
||||
- **Acceptance:** PR que introduz um code-smell/bug/vuln em código novo falha o gate; legado não bloqueia. Documentar suppressions legítimas (já há h1–h6 no properties).
|
||||
|
||||
### Task 2 — Catraca de vulnerabilidades (osv-scanner)
|
||||
- **Tool:** osv-scanner (Google/OSV, OSS) — `--format json`, on-box.
|
||||
- **Files:** `scripts/check/check-vuln-ratchet.mjs`, `quality-baseline.json` (+`vulnCount`), `collect-metrics.mjs`, `ci.yml` (job `quality-gate`), `dependency-allowlist.json`.
|
||||
- **Approach:** rodar `osv-scanner --format json` sobre os lockfiles → contar vulns → métrica `vulnCount {direction: down}`. Catraca: não pode subir, decai a zero. Mantém o `npm audit` escalonado da Fase 0 como bloqueio de crítico imediato; osv é o ratchet de timeline.
|
||||
- **Acceptance:** nova dep com vuln conhecida sobe o count → falha; remediar/remover baixa → `--update`.
|
||||
|
||||
### Task 3 — Catraca de alertas CodeQL
|
||||
- **Tool:** GitHub CodeQL (já roda; grátis p/ repo público).
|
||||
- **Files:** `scripts/check/check-codeql-ratchet.mjs`, `quality-baseline.json` (+`codeqlAlerts`), `ci.yml`.
|
||||
- **Approach:** puxar a contagem de alertas abertos via `gh api /repos/{owner}/{repo}/code-scanning/alerts?state=open` → métrica `codeqlAlerts {down}`. (Respeitar Hard Rule #14 — dismiss só com justificativa; alertas dismissed não contam.)
|
||||
- **Acceptance:** novo alerta CodeQL sobe o count → sinaliza; resolver baixa.
|
||||
|
||||
### Task 4 — Dead-code / unused-exports / unused-deps (knip)
|
||||
- **Tool:** knip (OSS, v6+) — `--reporter json`.
|
||||
- **Files:** `scripts/check/check-dead-code.mjs`, `quality-baseline.json` (+`deadExports`/`unusedDeps`), `knip.json` (config), `collect-metrics.mjs`, `ci.yml`, `dependency-allowlist.json`.
|
||||
- **Approach:** `knip --reporter json` sobre os workspaces `src/`+`open-sse/` → contar unused files/exports/deps → catraca `down`. Config knip ciente do monorepo + Next 16.
|
||||
- **Acceptance:** novo export/dep morto sobe → falha; remoção baixa.
|
||||
|
||||
### Task 5 — Complexidade cognitiva (eslint-plugin-sonarjs, config isolado)
|
||||
- **Tool:** eslint-plugin-sonarjs (OSS) — `sonarjs/cognitive-complexity`.
|
||||
- **Files:** `eslint.sonarjs.config.mjs` (config standalone, NÃO o principal — não polui o `eslintWarnings=3482`), `scripts/check/check-cognitive-complexity.mjs`, `quality-baseline.json` (+`cognitiveComplexity`), `ci.yml` (job `quality-gate`), `dependency-allowlist.json`.
|
||||
- **Approach:** mesmo molde do `check-complexity` (Fase 6) mas com `sonarjs/cognitive-complexity` num config isolado; contar violações → catraca `down`. (Complementa a complexidade ciclomática core já existente.)
|
||||
- **Acceptance:** função acima do limite cognitivo sobe o count → falha.
|
||||
|
||||
### Task 6 — Type-coverage ratchet
|
||||
- **Tool:** type-coverage (OSS).
|
||||
- **Files:** `scripts/check/check-type-coverage.mjs`, `quality-baseline.json` (+`typeCoveragePct {up}`), `dependency-allowlist.json`.
|
||||
- **Approach:** `type-coverage --detail --json` → % de símbolos tipados → catraca `up`. Complementa o `check:any-budget` (count de `any` por arquivo) com a visão %-global.
|
||||
- **Acceptance:** queda do % tipado → falha.
|
||||
|
||||
### Task 7 — Lockfile policy (lockfile-lint)
|
||||
- **Tool:** lockfile-lint (OSS, v5).
|
||||
- **Files:** `scripts/check/check-lockfile.mjs`, `ci.yml` (lint), `dependency-allowlist.json`.
|
||||
- **Approach:** `lockfile-lint --path package-lock.json --type npm --validate-https --validate-integrity --allowed-hosts npm` → gate pass/fail (não é ratchet; é política anti-poisoning). Complementa o `check-deps` (Fase 2).
|
||||
- **Acceptance:** lockfile com host não-https/sem integrity → falha.
|
||||
|
||||
### Task 8 — Completar anti-slopsquatting (registry-existence + age-cooldown)
|
||||
- **Tool:** npm registry API (`npm view <pkg> time.created`).
|
||||
- **Files:** `scripts/check/check-deps.mjs` (estender), test.
|
||||
- **Approach:** além do allowlist-diff atual, para uma dep NOVA: verificar que existe no registry E que foi publicada há ≥72h (age-cooldown contra "registra o nome alucinado em horas"). Base: CSA 2026 (19,7% de nomes alucinados; 43% reaparecem).
|
||||
- **Acceptance:** dep nova inexistente no registry ou publicada há <72h → falha (a menos que allowlistada com justificativa).
|
||||
|
||||
### Task 9 — Pisos de cobertura por módulo crítico (peça adiada da Fase 4)
|
||||
- **Files:** `scripts/quality/collect-metrics.mjs` (estender), `quality-baseline.json`.
|
||||
- **Approach:** emitir `coverage.<modulo>.lines` (lido do `coverage-summary.json` por-arquivo) para ~8 módulos de alto risco: `open-sse/handlers/chatCore.ts`, `open-sse/services/combo.ts`, `open-sse/services/accountFallback.ts`, `src/sse/services/auth.ts`, `src/server/authz/routeGuard.ts`, `open-sse/utils/error.ts`, `open-sse/utils/publicCreds.ts`, `src/shared/utils/circuitBreaker.ts`. Cada um vira métrica `up`. **Calibrar a partir do coverage mergeado real do 1º run verde na main.**
|
||||
- **Acceptance:** queda de cobertura num módulo crítico → falha, mesmo que o global não caia.
|
||||
|
||||
### Task 10 — Evidence-in-PR-body (peça adiada da Fase 5)
|
||||
- **Files:** `scripts/check/check-pr-evidence.mjs`, `ci.yml` (job `pr-test-policy`).
|
||||
- **Approach:** se o corpo do PR afirma "tests pass"/"added endpoint X"/"fixed Y" sem um bloco de **output de comando** anexado (typecheck/test/grep), falha (torna a Rule #18 mecânica — "evidence before assertions"). Heurístico, no contexto de PR.
|
||||
- **Acceptance:** PR alegando sucesso sem output anexado → falha.
|
||||
|
||||
### Task 11 — Mutation testing nos módulos críticos (Stryker, nightly)
|
||||
- **Tool:** Stryker (`@stryker-mutator/core` + runner; OSS).
|
||||
- **Files:** `stryker.conf.json`, `ci.yml` (job NIGHTLY separado — não no PR), `dependency-allowlist.json`.
|
||||
- **Approach:** escopar Stryker aos ~8 módulos críticos da Task 9 (não repo-wide — é caro + c8 já OOM-prone). Mutantes sobreviventes = **testes tautológicos** (passam sem provar nada) → complementa o `check-test-masking` da Fase 4. Rodar nightly/weekly, não por-PR.
|
||||
- **Acceptance:** mutation score por módulo crítico vira métrica (catraca `up`, nightly).
|
||||
|
||||
### Task 12 — Bundle-size / perf budget (size-limit)
|
||||
- **Tool:** size-limit (OSS).
|
||||
- **Files:** `.size-limit.json`, `scripts/check/check-bundle-size.mjs`, `ci.yml`, `dependency-allowlist.json`.
|
||||
- **Approach:** definir orçamento por bundle Next 16; size-limit emite tamanhos → catraca `down` (bundle não pode inchar).
|
||||
- **Acceptance:** PR que estoura o orçamento de bundle → falha.
|
||||
|
||||
### Task 13 — a11y gate (axe-core + Playwright)
|
||||
- **Tool:** `@axe-core/playwright` (OSS; Playwright já existe).
|
||||
- **Files:** `tests/e2e/a11y.spec.ts`, `ci.yml` (job `test-e2e`), `dependency-allowlist.json`.
|
||||
- **Approach:** rodar axe nas páginas-chave do dashboard; congelar violações atuais (catraca `down`). Atende o item a11y/visual do plano t15.
|
||||
- **Acceptance:** nova violação a11y → falha; correção baixa.
|
||||
|
||||
### Task 14 — semcheck (camada fuzzy docs↔código, opcional/LLM)
|
||||
- **Tool:** semcheck (OSS, MIT) — `fail-on-issues`.
|
||||
- **Files:** `semcheck.yaml`, `ci.yml` (advisory).
|
||||
- **Approach:** regras ligando `docs/**` ao módulo de código que documentam; pega docs que descrevem o que o código NÃO faz (camada fuzzy sobre o determinístico `check-docs-symbols` da Fase 6). É LLM → rodar advisory/non-blocking ou em label, por custo.
|
||||
- **Acceptance:** doc que descreve comportamento inexistente → flag (advisory).
|
||||
|
||||
### Task 15 — agent-lsp (LSP-in-the-loop para os agentes)
|
||||
- **Tool:** agent-lsp (MCP server, OSS).
|
||||
- **Files:** config MCP dos agentes (`.mcp.json`/equivalente).
|
||||
- **Approach:** expor `tsserver`/agent-lsp aos agentes para `blast_radius`/diagnostics/`preview_edit` ANTES de escrever — vira "símbolo inventado" de catch-de-review para impossibilidade-no-edit. Pareia com `typecheck:core` como gate pré-PR (compile-before-claim).
|
||||
- **Acceptance:** agentes resolvem símbolo/import via LSP; menos alucinação de símbolo na origem.
|
||||
|
||||
### Task 16 — dpdm circular-deps JSON cross-check (opcional)
|
||||
- **Tool:** dpdm (OSS, v4) — `--circular --output`.
|
||||
- **Approach:** cross-check JSON de ciclos complementando o `check-cycles.mjs` existente (AST-TS mais preciso). Catraca de contagem de ciclos. Baixa prioridade (já temos check-cycles).
|
||||
|
||||
### Task 17 — Avaliar Qlty CLI como consolidador (opcional, spike)
|
||||
- **Tool:** Qlty CLI (OSS, grátis).
|
||||
- **Approach:** spike: avaliar se Qlty (Baseline analysis + 70 analyzers) substitui N scripts caseiros sem perder o controle/determinismo. Decisão build-vs-buy. Não obrigatório.
|
||||
|
||||
---
|
||||
|
||||
## Wiring & CI (resumo)
|
||||
- **lint job:** check-lockfile, check-cognitive-complexity (rápido?), check-type-coverage.
|
||||
- **quality-gate job (paralelo):** check-vuln-ratchet, check-dead-code, check-codeql-ratchet, check-cognitive-complexity (se lento), check-bundle-size.
|
||||
- **pr-test-policy job:** check-pr-evidence.
|
||||
- **sonarqube job:** Clean-as-You-Code + `qualitygate.wait`.
|
||||
- **NIGHTLY job (novo):** Stryker (mutação), semcheck (advisory), a11y full.
|
||||
- Todas as métricas numéricas → `quality-baseline.json` (motor da Fase 1). Toda dep nova → `dependency-allowlist.json`.
|
||||
|
||||
## Self-Review
|
||||
- **Cobertura do spec:** 7 gates sugeridos = Task 1-3 (segurança), 4 (knip), 9 (coverage por módulo), 10 (evidence), 11 (mutação), 12 (bundle), 13 (a11y). "Todas as ferramentas discutidas" = Tasks 1-8, 11-17 (Sonar/osv/CodeQL/knip/sonarjs/type-coverage/lockfile/dpdm/stryker/size-limit/axe/semcheck/agent-lsp/Qlty). ✓
|
||||
- **Community/OSS only:** confirmado — Sonar Community Build, todos os demais OSS, zero SaaS pago. ✓
|
||||
- **Sem flag-day:** toda catraca é só-regressão, calibrada do estado atual. ✓
|
||||
- **Consistência:** todas as métricas usam o formato `{value, direction}` do motor da Fase 1; deps novas passam pelo `check-deps`+`dependency-allowlist.json`. ✓
|
||||
|
||||
## Handoff (na ativação, 2026-06-16+)
|
||||
Quando o portão abrir: começar pela **Task 1-3 (catraca de segurança)** e **Task 4 (knip)** — maior retorno. Cada Task vira um sub-plano `writing-plans` bite-sized próprio. Recomendado: Subagent-Driven, 1 subagente por Task, com auditoria (trust-but-verify) e o ratchet `eslintWarnings`/`check-deps` validando que cada adição não regride o que já temos.
|
||||
688
PLANO-QUALITY-GATES.md
Normal file
688
PLANO-QUALITY-GATES.md
Normal file
@@ -0,0 +1,688 @@
|
||||
# Plano de Implementação — Quality Gates & Catraca Anti-Alucinação
|
||||
|
||||
> **Para workers agênticos:** SUB-SKILL OBRIGATÓRIA: use `superpowers:subagent-driven-development` (recomendado) ou `superpowers:executing-plans` para executar este plano tarefa-a-tarefa. Os passos usam checkbox (`- [ ]`) para rastreio. **Cada fix de bug obedece à Hard Rule #18** (teste falha→passa OU validação ao vivo no VPS). Não burle Husky (`--no-verify`) sem aprovação. Veja o diagnóstico completo em [`RELATORIO-QUALITY-GATES.md`](./RELATORIO-QUALITY-GATES.md).
|
||||
|
||||
**Goal:** Generalizar a catraca de qualidade do OmniRoute (hoje só para `any`) para todas as métricas relevantes — cobertura, duplicação, tamanho de arquivo, complexidade — e adicionar gates determinísticos anti-alucinação, tudo no padrão `check-*.mjs` já existente, sem SaaS novo.
|
||||
|
||||
**Architecture:** Camadas incrementais. (0) reativa/reconcilia o que já existe; (1) constrói o **motor de catraca** (`quality-baseline.json` commitado + coletor + comparador genérico que clona o `check-t11-any-budget.mjs`); (2) adiciona gates determinísticos que matam os ímãs de alucinação (provider-consistency, fetch-targets, openapi-routes); (3) catraca de duplicação+tamanho; (4) catraca de cobertura + anti test-masking; (5) skill `/babysit` + evidência. Toda catraca é **só-regressão** (baseline congelado) — nunca um piso absoluto que exija limpeza flag-day.
|
||||
|
||||
**Tech Stack:** Node ≥20 ESM (`.mjs`/`.ts` via `tsx`), ESLint 9 flat config, c8, jscpd v5, eslint-plugin-sonarjs v4, GitHub Actions, Node native test runner (`node --import tsx --test`), `gh` CLI + GraphQL.
|
||||
|
||||
**Escopo / sub-planos (scope-check):** Fases 0, 1 e 2 estão totalmente bite-sized aqui. Fases 3, 4 e 5 são subsistemas independentes — cada uma deve ser **expandida no próprio sub-plano** (via `writing-plans`) no momento da execução, a partir das specs/critérios de aceitação definidos aqui. Cada fase entrega software funcional e testável por si só.
|
||||
|
||||
---
|
||||
|
||||
## Mapa de arquivos (o que será criado/modificado)
|
||||
|
||||
| Arquivo | Responsabilidade |
|
||||
|---------|------------------|
|
||||
| `quality-baseline.json` (criar, **commitar**) | Baseline congelado: por métrica `{value, direction}` (`down`=menor-é-melhor, `up`=maior-é-melhor) |
|
||||
| `scripts/quality/collect-metrics.mjs` (criar) | Roda os coletores → emite `quality-metrics.json` |
|
||||
| `scripts/quality/check-quality-ratchet.mjs` (criar) | Comparador genérico: falha em qualquer regressão; com `--update` ratcheta o baseline |
|
||||
| `scripts/check/check-fetch-targets.mjs` (criar) | Todo `fetch("/api/...")` do dashboard resolve para um `route.ts` real |
|
||||
| `scripts/check/check-provider-consistency.ts` (criar) | ids de provider batem entre `providers.ts` ↔ `providerRegistry.ts` ↔ `validation.ts` |
|
||||
| `scripts/check/check-openapi-routes.mjs` (criar) | Toda `path` do `openapi.yaml` ↔ `route.ts` real (bidirecional) |
|
||||
| `scripts/check/check-deps.mjs` (criar) | Anti-slopsquatting: allowlist + existência no registry + age-cooldown |
|
||||
| `.github/workflows/ci.yml` (modificar) | Novo job `quality-gate`; reconciliar gate de cobertura; escalonar audit; plugar scripts órfãos |
|
||||
| `.husky/pre-commit` (modificar) | Reativar a parte barata |
|
||||
| `package.json` (modificar) | Novos scripts `check:*` / `quality:*` |
|
||||
| `eslint.config.mjs` (modificar, Fase 3) | `max-lines`, `max-lines-per-function`, `complexity`, `sonarjs/cognitive-complexity` (warn) |
|
||||
| `.claude/skills/babysit/SKILL.md` (criar, Fase 5) | Skill `/babysit` |
|
||||
| `tests/unit/quality-ratchet.test.ts` etc. (criar) | Testes TDD de cada gate |
|
||||
|
||||
---
|
||||
|
||||
# FASE 0 — Reativar & Reconciliar (quick wins, sem tooling novo)
|
||||
|
||||
### Task 0.1: Reconciliar o gate de cobertura do CI (40 → baseline real)
|
||||
|
||||
**Contexto:** `ci.yml:377` gata em `40/40/40/40`; local gata `60`; comentário renderiza `60`; baseline real ≈ 79–82%. O 40 torna o gate quase banguela.
|
||||
|
||||
**Files:**
|
||||
- Modify: `.github/workflows/ci.yml:376-377`
|
||||
|
||||
- [ ] **Step 1: Confirmar o baseline real de cobertura**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
npm run test:coverage 2>&1 | tail -20
|
||||
node -e "const c=require('./coverage/coverage-summary.json').total; console.log(c.statements.pct,c.lines.pct,c.functions.pct,c.branches.pct)"
|
||||
```
|
||||
Expected: 4 números (ex.: `79.8 79.8 82.2 75.2`). Anote-os.
|
||||
|
||||
- [ ] **Step 2: Subir o gate do CI para `baseline_real - 2` (headroom anti-flake)**
|
||||
|
||||
Em `.github/workflows/ci.yml`, troque a linha `--statements 40 --lines 40 --functions 40 --branches 40` pelos valores `(real-2)` de cada métrica (ex.: `--statements 77 --lines 77 --functions 80 --branches 73`). Mantenha como **piso**; a catraca da Fase 4 cuidará do "não-cair".
|
||||
|
||||
- [ ] **Step 3: Alinhar o script local e o display do comentário** para os mesmos números (procure `60` em `package.json` `test:coverage` e em `scripts/check/test-report-summary.mjs`).
|
||||
|
||||
- [ ] **Step 4: Verificar que o CI não quebra** — abrir um PR de teste (ou rodar `act`/push numa branch) e confirmar que o job `test-coverage` fica verde com o novo piso.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
```bash
|
||||
git add .github/workflows/ci.yml package.json scripts/check/test-report-summary.mjs
|
||||
git commit -m "fix(ci): reconcile coverage gate to real baseline (40->~78) across CI/local/report"
|
||||
```
|
||||
|
||||
### Task 0.2: Escalonar `npm audit` (critical=bloqueia / high=avisa)
|
||||
|
||||
**Files:** Modify: `package.json:112`
|
||||
|
||||
- [ ] **Step 1: Trocar o script `audit:deps`** de `npm audit --audit-level=moderate && npm run audit:electron` para:
|
||||
```json
|
||||
"audit:deps": "npm audit --audit-level=critical && (npm audit --audit-level=high || echo '::warning::high-severity advisories present (non-blocking)') && npm run audit:electron",
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rodar e observar o comportamento**
|
||||
```bash
|
||||
npm run audit:deps; echo "exit=$?"
|
||||
```
|
||||
Expected: `exit=0` se não houver critical; mensagem de warning se houver high.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
```bash
|
||||
git add package.json && git commit -m "chore(ci): tier npm audit (critical blocks, high warns)"
|
||||
```
|
||||
|
||||
### Task 0.3: Plugar os 3 scripts órfãos no CI
|
||||
|
||||
**Contexto:** `check:cli-i18n`, `check:openapi-coverage`, `check:openapi-security-tiers` existem, dão `exit 1`, mas não rodam em lugar nenhum (Hard Rules #15/#17).
|
||||
|
||||
**Files:** Modify: `.github/workflows/ci.yml` (job `docs-sync-strict` ou `lint`)
|
||||
|
||||
- [ ] **Step 1: Rodar os 3 localmente para confirmar verde no estado atual**
|
||||
```bash
|
||||
npm run check:cli-i18n && npm run check:openapi-coverage && npm run check:openapi-security-tiers; echo "exit=$?"
|
||||
```
|
||||
Expected: `exit=0` (se algum falhar, corrija a deriva antes de plugar).
|
||||
|
||||
- [ ] **Step 2: Adicionar os 3 como steps** no job `docs-sync-strict` do `ci.yml`, após `check:docs-all`.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
```bash
|
||||
git add .github/workflows/ci.yml && git commit -m "ci: wire orphaned gates (cli-i18n, openapi-coverage, openapi-security-tiers)"
|
||||
```
|
||||
|
||||
### Task 0.4: Reativar a parte barata do pre-commit do Husky
|
||||
|
||||
**Files:** Modify: `.husky/pre-commit`
|
||||
|
||||
- [ ] **Step 1: Descomentar SÓ as 3 linhas baratas e determinísticas:**
|
||||
```sh
|
||||
npx lint-staged
|
||||
node scripts/check/check-docs-sync.mjs
|
||||
npm run check:any-budget:t11
|
||||
```
|
||||
(Deixe i18n/openapi comentados por enquanto — eles são mais lentos; rodam no CI.)
|
||||
|
||||
- [ ] **Step 2: Testar o hook** com um commit trivial e medir o tempo:
|
||||
```bash
|
||||
time git commit --allow-empty -m "chore: test pre-commit hook"
|
||||
git reset --soft HEAD~1
|
||||
```
|
||||
Expected: hook roda lint-staged + 2 checks em poucos segundos.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
```bash
|
||||
git add .husky/pre-commit && git commit -m "chore(husky): re-enable cheap pre-commit gates (lint-staged, docs-sync, any-budget)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# FASE 1 — Motor de Catraca (o coração) ⭐
|
||||
|
||||
> Generaliza o `check-t11-any-budget.mjs` (catraca de `any` por arquivo) para um motor de catraca genérico, multi-métrica, que lê um baseline commitado e falha em qualquer regressão. Começa com 2 métricas (warnings de ESLint + cobertura) e é estendido nas fases seguintes.
|
||||
|
||||
### Task 1.1: Comparador genérico de catraca (TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/quality/check-quality-ratchet.mjs`
|
||||
- Test: `tests/unit/quality-ratchet.test.ts`
|
||||
|
||||
- [ ] **Step 1: Escrever o teste que falha**
|
||||
```ts
|
||||
// tests/unit/quality-ratchet.test.ts
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const SCRIPT = path.resolve("scripts/quality/check-quality-ratchet.mjs");
|
||||
|
||||
function run(baseline, metrics, extraArgs = []) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ratchet-"));
|
||||
const bPath = path.join(dir, "baseline.json");
|
||||
const mPath = path.join(dir, "metrics.json");
|
||||
fs.writeFileSync(bPath, JSON.stringify(baseline));
|
||||
fs.writeFileSync(mPath, JSON.stringify(metrics));
|
||||
try {
|
||||
const out = execFileSync("node", [SCRIPT, "--baseline", bPath, "--metrics", mPath, ...extraArgs], { encoding: "utf8" });
|
||||
return { code: 0, out, dir, bPath };
|
||||
} catch (e) {
|
||||
return { code: e.status, out: (e.stdout || "") + (e.stderr || ""), dir, bPath };
|
||||
}
|
||||
}
|
||||
|
||||
test("passes when metrics equal baseline", () => {
|
||||
const b = { metrics: { eslintWarnings: { value: 100, direction: "down" }, "coverage.lines": { value: 80, direction: "up" } } };
|
||||
assert.equal(run(b, { eslintWarnings: 100, "coverage.lines": 80 }).code, 0);
|
||||
});
|
||||
|
||||
test("fails when a 'down' metric regresses (more warnings)", () => {
|
||||
const b = { metrics: { eslintWarnings: { value: 100, direction: "down" } } };
|
||||
const r = run(b, { eslintWarnings: 101 });
|
||||
assert.equal(r.code, 1);
|
||||
assert.match(r.out, /eslintWarnings/);
|
||||
});
|
||||
|
||||
test("fails when an 'up' metric regresses (coverage drops)", () => {
|
||||
const b = { metrics: { "coverage.lines": { value: 80, direction: "up" } } };
|
||||
assert.equal(run(b, { "coverage.lines": 79 }).code, 1);
|
||||
});
|
||||
|
||||
test("passes on improvement; --update ratchets the baseline", () => {
|
||||
const b = { metrics: { eslintWarnings: { value: 100, direction: "down" } } };
|
||||
const r = run(b, { eslintWarnings: 90 }, ["--update"]);
|
||||
assert.equal(r.code, 0);
|
||||
const updated = JSON.parse(fs.readFileSync(r.bPath, "utf8"));
|
||||
assert.equal(updated.metrics.eslintWarnings.value, 90);
|
||||
});
|
||||
|
||||
test("fails (code 2) when a baseline metric is missing from collected metrics", () => {
|
||||
const b = { metrics: { eslintWarnings: { value: 100, direction: "down" } } };
|
||||
assert.equal(run(b, {}).code, 1);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rodar o teste e ver falhar**
|
||||
```bash
|
||||
node --import tsx --test tests/unit/quality-ratchet.test.ts
|
||||
```
|
||||
Expected: FAIL (script não existe ainda).
|
||||
|
||||
- [ ] **Step 3: Implementar o comparador**
|
||||
```js
|
||||
#!/usr/bin/env node
|
||||
// scripts/quality/check-quality-ratchet.mjs
|
||||
// Catraca genérica multi-métrica. Clona o espírito de check-t11-any-budget.mjs:
|
||||
// um baseline congelado por métrica; falha em qualquer regressão; só anda num sentido.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const cwd = process.cwd();
|
||||
function getArg(name, fallback) {
|
||||
const i = process.argv.indexOf(name);
|
||||
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
|
||||
}
|
||||
const BASELINE = path.resolve(getArg("--baseline", path.join(cwd, "quality-baseline.json")));
|
||||
const METRICS = path.resolve(getArg("--metrics", path.join(cwd, "quality-metrics.json")));
|
||||
const SUMMARY = getArg("--summary", null);
|
||||
const UPDATE = process.argv.includes("--update");
|
||||
const EPS = 0.01;
|
||||
|
||||
function load(p) {
|
||||
if (!fs.existsSync(p)) { console.error(`[quality-ratchet] arquivo ausente: ${p}`); process.exit(2); }
|
||||
return JSON.parse(fs.readFileSync(p, "utf8"));
|
||||
}
|
||||
|
||||
const baseline = load(BASELINE);
|
||||
const metrics = load(METRICS);
|
||||
const failures = [];
|
||||
const improvements = [];
|
||||
const rows = [];
|
||||
|
||||
for (const [key, spec] of Object.entries(baseline.metrics)) {
|
||||
const current = metrics[key];
|
||||
const base = spec.value;
|
||||
const dir = spec.direction; // "down" = menor-é-melhor | "up" = maior-é-melhor
|
||||
if (current === undefined) { failures.push(`métrica "${key}" ausente em ${path.basename(METRICS)}`); rows.push([key, base, "—", "MISSING"]); continue; }
|
||||
let status = "ok";
|
||||
if (dir === "down") {
|
||||
if (current > base + EPS) { failures.push(`${key}: ${current} > baseline ${base} (não pode aumentar)`); status = "REGRESSÃO"; }
|
||||
else if (current < base - EPS) { improvements.push([key, current]); status = "↑ melhorou"; }
|
||||
} else {
|
||||
if (current < base - EPS) { failures.push(`${key}: ${current} < baseline ${base} (não pode cair)`); status = "REGRESSÃO"; }
|
||||
else if (current > base + EPS) { improvements.push([key, current]); status = "↑ melhorou"; }
|
||||
}
|
||||
rows.push([key, base, current, status]);
|
||||
}
|
||||
|
||||
if (SUMMARY) {
|
||||
const md = ["# Quality Ratchet", "", "| Métrica | Baseline | Atual | Status |", "|---|---|---|---|",
|
||||
...rows.map(([k, b, c, s]) => `| ${k} | ${b} | ${c} | ${s} |`), "",
|
||||
failures.length ? `**${failures.length} regressão(ões) — gate BLOQUEADO.**` : "**Sem regressões — gate OK.**"].join("\n");
|
||||
fs.mkdirSync(path.dirname(SUMMARY), { recursive: true });
|
||||
fs.writeFileSync(SUMMARY, md + "\n");
|
||||
}
|
||||
|
||||
if (UPDATE && failures.length === 0 && improvements.length) {
|
||||
for (const [key, val] of improvements) baseline.metrics[key].value = val;
|
||||
fs.writeFileSync(BASELINE, JSON.stringify(baseline, null, 2) + "\n");
|
||||
console.log(`[quality-ratchet] baseline ratcheado: ${improvements.length} métrica(s) melhoraram`);
|
||||
}
|
||||
|
||||
if (failures.length) { console.error("[quality-ratchet] FALHOU:\n" + failures.map((f) => " ✗ " + f).join("\n")); process.exit(1); }
|
||||
console.log(`[quality-ratchet] OK (${rows.length} métricas, ${improvements.length} melhoraram)`);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Rodar o teste e ver passar**
|
||||
```bash
|
||||
node --import tsx --test tests/unit/quality-ratchet.test.ts
|
||||
```
|
||||
Expected: PASS (5/5).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
```bash
|
||||
git add scripts/quality/check-quality-ratchet.mjs tests/unit/quality-ratchet.test.ts
|
||||
git commit -m "feat(quality): generic ratchet comparator (multi-metric, regression-only)"
|
||||
```
|
||||
|
||||
### Task 1.2: Coletor de métricas (ESLint warnings + cobertura)
|
||||
|
||||
**Files:** Create: `scripts/quality/collect-metrics.mjs`
|
||||
|
||||
- [ ] **Step 1: Implementar o coletor**
|
||||
```js
|
||||
#!/usr/bin/env node
|
||||
// scripts/quality/collect-metrics.mjs — emite quality-metrics.json
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
const cwd = process.cwd();
|
||||
const out = {};
|
||||
|
||||
// 1) ESLint: contagem de warnings (errors devem ser 0; o lint já gata isso)
|
||||
function eslintCounts() {
|
||||
let stdout;
|
||||
try {
|
||||
stdout = execFileSync("npx", ["eslint", ".", "--format", "json"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
||||
} catch (e) { stdout = e.stdout?.toString() || "[]"; } // eslint sai !=0 quando há errors
|
||||
const results = JSON.parse(stdout);
|
||||
out.eslintWarnings = results.reduce((n, r) => n + (r.warningCount || 0), 0);
|
||||
out.eslintErrors = results.reduce((n, r) => n + (r.errorCount || 0), 0);
|
||||
}
|
||||
|
||||
// 2) Cobertura: lê coverage/coverage-summary.json se existir
|
||||
function coverage() {
|
||||
const p = path.join(cwd, "coverage", "coverage-summary.json");
|
||||
if (!fs.existsSync(p)) return;
|
||||
const t = JSON.parse(fs.readFileSync(p, "utf8")).total;
|
||||
out["coverage.statements"] = t.statements.pct;
|
||||
out["coverage.lines"] = t.lines.pct;
|
||||
out["coverage.functions"] = t.functions.pct;
|
||||
out["coverage.branches"] = t.branches.pct;
|
||||
}
|
||||
|
||||
eslintCounts();
|
||||
coverage();
|
||||
fs.writeFileSync(path.join(cwd, "quality-metrics.json"), JSON.stringify(out, null, 2) + "\n");
|
||||
console.log("[collect-metrics]", JSON.stringify(out));
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rodar e inspecionar a saída**
|
||||
```bash
|
||||
node scripts/quality/collect-metrics.mjs && cat quality-metrics.json
|
||||
```
|
||||
Expected: JSON com `eslintWarnings`, `eslintErrors` e (se houver coverage) os 4 `coverage.*`. **Anote `eslintWarnings`.**
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
```bash
|
||||
git add scripts/quality/collect-metrics.mjs && echo "quality-metrics.json" >> .gitignore
|
||||
git add .gitignore && git commit -m "feat(quality): metrics collector (eslint warnings + coverage)"
|
||||
```
|
||||
|
||||
### Task 1.3: Congelar o baseline inicial
|
||||
|
||||
**Files:** Create: `quality-baseline.json` (**commitado**)
|
||||
|
||||
- [ ] **Step 1: Gerar o baseline a partir das métricas reais** (use os números anotados):
|
||||
```json
|
||||
{
|
||||
"_comment": "Catraca: 'down' nao pode aumentar, 'up' nao pode cair. Atualize via 'npm run quality:ratchet -- --update' (so em melhora).",
|
||||
"metrics": {
|
||||
"eslintWarnings": { "value": <N_REAL>, "direction": "down" },
|
||||
"coverage.statements": { "value": <S>, "direction": "up" },
|
||||
"coverage.lines": { "value": <L>, "direction": "up" },
|
||||
"coverage.functions": { "value": <F>, "direction": "up" },
|
||||
"coverage.branches": { "value": <B>, "direction": "up" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Validar a catraca contra si mesma**
|
||||
```bash
|
||||
node scripts/quality/collect-metrics.mjs
|
||||
node scripts/quality/check-quality-ratchet.mjs; echo "exit=$?"
|
||||
```
|
||||
Expected: `[quality-ratchet] OK` e `exit=0`.
|
||||
|
||||
- [ ] **Step 3: Provar que pega regressão** (teste manual): edite `quality-metrics.json` somando 1 a `eslintWarnings`, rode o comparador, confirme `exit=1`, depois descarte a edição.
|
||||
|
||||
- [ ] **Step 4: Adicionar scripts npm**
|
||||
```json
|
||||
"quality:collect": "node scripts/quality/collect-metrics.mjs",
|
||||
"quality:ratchet": "node scripts/quality/check-quality-ratchet.mjs",
|
||||
"quality:gate": "npm run quality:collect && npm run quality:ratchet"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
```bash
|
||||
git add quality-baseline.json package.json
|
||||
git commit -m "feat(quality): freeze initial quality baseline (eslint warnings + coverage)"
|
||||
```
|
||||
|
||||
### Task 1.4: Wire no CI (job + artefato + comentário no PR)
|
||||
|
||||
**Files:** Modify: `.github/workflows/ci.yml`
|
||||
|
||||
- [ ] **Step 1: Adicionar job `quality-gate`** (depois de `test-coverage`, para reusar `coverage/coverage-summary.json`):
|
||||
```yaml
|
||||
quality-gate:
|
||||
name: Quality Ratchet
|
||||
runs-on: ubuntu-latest
|
||||
needs: test-coverage
|
||||
if: ${{ always() && needs.test-coverage.result == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: '24', cache: 'npm' }
|
||||
- run: npm ci
|
||||
- uses: actions/download-artifact@v4
|
||||
with: { name: coverage-report, path: coverage/ }
|
||||
- run: npm run quality:collect
|
||||
- run: node scripts/quality/check-quality-ratchet.mjs --summary .artifacts/quality-ratchet.md
|
||||
- if: always()
|
||||
run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY"
|
||||
- if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: quality-ratchet, path: .artifacts/quality-ratchet.md }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Adicionar comentário no PR** clonando o job `coverage-pr-comment` (marcador `<!-- omniroute-quality-ratchet -->`, lê `.artifacts/quality-ratchet.md`). Reuse o mesmo `github-script` de upsert de comentário.
|
||||
|
||||
- [ ] **Step 3: Validar num PR de teste** — confirmar que o job aparece, o step summary mostra a tabela, e o comentário é postado.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
```bash
|
||||
git add .github/workflows/ci.yml
|
||||
git commit -m "ci(quality): add quality-ratchet job with PR comment + artifact"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# FASE 2 — Gates Determinísticos Anti-Alucinação
|
||||
|
||||
> Cada gate ataca um ímã de alucinação específico (ver §2.4 do relatório). Todos no padrão `check-*.mjs`. **`check-fetch-targets` vem com código completo** (parsing determinístico de arquivos, sem risco de inventar nomes de export). **`check-provider-consistency` e `check-openapi-routes` começam com um Step 0 de verificação dos nomes reais** — propositalmente, porque fabricar a forma do import/spec seria o exato anti-padrão que este plano combate.
|
||||
|
||||
### Task 2.1: `check-fetch-targets.mjs` — toda rota chamada pelo dashboard existe (TDD)
|
||||
|
||||
**Ataca:** ímã nº2 (300 paths `fetch("/api/...")` sem ligação com as rotas).
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/check/check-fetch-targets.mjs`
|
||||
- Test: `tests/unit/check-fetch-targets.test.ts`
|
||||
|
||||
- [ ] **Step 1: Escrever o teste que falha**
|
||||
```ts
|
||||
// tests/unit/check-fetch-targets.test.ts
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { resolveApiPathToRoute } from "../../scripts/check/check-fetch-targets.mjs";
|
||||
|
||||
test("matches a static route file", () => {
|
||||
const files = new Set(["src/app/api/usage/route.ts"]);
|
||||
assert.equal(resolveApiPathToRoute("/api/usage", files), true);
|
||||
});
|
||||
|
||||
test("matches a dynamic [param] segment", () => {
|
||||
const files = new Set(["src/app/api/providers/[id]/models/route.ts"]);
|
||||
assert.equal(resolveApiPathToRoute("/api/providers/abc-123/models", files), true);
|
||||
});
|
||||
|
||||
test("rejects a hallucinated route", () => {
|
||||
const files = new Set(["src/app/api/usage/route.ts"]);
|
||||
assert.equal(resolveApiPathToRoute("/api/providers/refresh", files), false);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rodar e ver falhar**
|
||||
```bash
|
||||
node --import tsx --test tests/unit/check-fetch-targets.test.ts
|
||||
```
|
||||
Expected: FAIL (módulo não existe).
|
||||
|
||||
- [ ] **Step 3: Implementar o gate**
|
||||
```js
|
||||
#!/usr/bin/env node
|
||||
// scripts/check/check-fetch-targets.mjs
|
||||
// Todo fetch("/api/...") em src/app/(dashboard) deve resolver para um route.ts real.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const cwd = process.cwd();
|
||||
const DASH = path.join(cwd, "src/app/(dashboard)");
|
||||
const API = path.join(cwd, "src/app/api");
|
||||
|
||||
// allowlist de paths dinâmicos/externos que o checker não consegue resolver estaticamente
|
||||
const IGNORE = [/^\/api\/v1\//, /\$\{/, /` \+/]; // /v1 é a superfície OpenAI-compat; templates
|
||||
|
||||
function walk(dir, acc = []) {
|
||||
if (!fs.existsSync(dir)) return acc;
|
||||
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) walk(p, acc);
|
||||
else if (/\.(ts|tsx)$/.test(e.name)) acc.push(p);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function collectRouteFiles() {
|
||||
return new Set(walk(API).filter((p) => /route\.tsx?$/.test(p)).map((p) => path.relative(cwd, p).replace(/\\/g, "/")));
|
||||
}
|
||||
|
||||
export function resolveApiPathToRoute(apiPath, routeFiles) {
|
||||
// apiPath ex.: /api/providers/abc/models → src/app/api/providers/[id]/models/route.ts
|
||||
const segs = apiPath.replace(/^\//, "").replace(/[?#].*$/, "").split("/"); // ["api","providers","abc","models"]
|
||||
for (const rf of routeFiles) {
|
||||
const rsegs = rf.replace(/^src\/app\//, "").replace(/\/route\.tsx?$/, "").split("/"); // ["api","providers","[id]","models"]
|
||||
if (rsegs.length !== segs.length) continue;
|
||||
const ok = rsegs.every((rs, i) => rs === segs[i] || /^\[.*\]$/.test(rs));
|
||||
if (ok) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractFetchPaths(file) {
|
||||
const src = fs.readFileSync(file, "utf8");
|
||||
const re = /(?:fetch|fetchJson|apiFetch)\(\s*["'`](\/api\/[^"'`?]+)/g;
|
||||
const out = [];
|
||||
let m;
|
||||
while ((m = re.exec(src))) out.push(m[1]);
|
||||
return out;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const routeFiles = collectRouteFiles();
|
||||
const misses = [];
|
||||
for (const f of walk(DASH)) {
|
||||
for (const apiPath of extractFetchPaths(f)) {
|
||||
if (IGNORE.some((rx) => rx.test(apiPath))) continue;
|
||||
if (!resolveApiPathToRoute(apiPath, routeFiles)) misses.push(`${path.relative(cwd, f)} → ${apiPath}`);
|
||||
}
|
||||
}
|
||||
if (misses.length) {
|
||||
console.error(`[check-fetch-targets] ${misses.length} fetch(es) para rota inexistente:\n` + misses.map((m) => " ✗ " + m).join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`[check-fetch-targets] OK (${routeFiles.size} rotas conhecidas)`);
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) main();
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Rodar o teste unitário e ver passar**
|
||||
```bash
|
||||
node --import tsx --test tests/unit/check-fetch-targets.test.ts
|
||||
```
|
||||
Expected: PASS (3/3).
|
||||
|
||||
- [ ] **Step 5: Rodar o gate no repo real** (modo descoberta — pode encontrar misses legítimos)
|
||||
```bash
|
||||
node scripts/check/check-fetch-targets.mjs; echo "exit=$?"
|
||||
```
|
||||
Se encontrar misses reais: triagem — ou são rotas faltantes (bug), ou paths dinâmicos a adicionar ao `IGNORE`. **Não** mascare; documente cada exceção no `IGNORE` com comentário.
|
||||
|
||||
- [ ] **Step 6: Adicionar script + commit**
|
||||
```json
|
||||
"check:fetch-targets": "node scripts/check/check-fetch-targets.mjs"
|
||||
```
|
||||
```bash
|
||||
git add scripts/check/check-fetch-targets.mjs tests/unit/check-fetch-targets.test.ts package.json
|
||||
git commit -m "feat(check): gate that every dashboard fetch() targets a real API route"
|
||||
```
|
||||
|
||||
### Task 2.2: `check-provider-consistency.ts` — ids batem entre os 3 arquivos
|
||||
|
||||
**Ataca:** ímã nº1 (split de provider; 229 vs 155 vs N já divergem).
|
||||
|
||||
**Files:** Create: `scripts/check/check-provider-consistency.ts` (rodado via `tsx`); Test: `tests/unit/check-provider-consistency.test.ts`
|
||||
|
||||
- [ ] **Step 0 (VERIFICAÇÃO — obrigatório antes de codar):** descobrir os nomes reais de export, para **não inventar**:
|
||||
```bash
|
||||
grep -nE "export (const|function) " src/shared/constants/providers.ts | grep -iE "provider|section" | head
|
||||
grep -nE "getOrCreateAiProviders|_PROVIDER_SECTIONS|AI_PROVIDERS" src/shared/constants/providers.ts | head
|
||||
grep -nE "baseUrl|^\s*['\"][a-z0-9-]+['\"]\s*:" open-sse/config/providerRegistry.ts | head
|
||||
grep -nE "case ['\"]|=== ['\"]|SPECIALTY_VALIDATORS" src/lib/providers/validation.ts | head
|
||||
```
|
||||
Anote: como enumerar os ids canônicos em runtime, a forma das chaves do `providerRegistry.ts`, e onde `validation.ts` lista os providers cobertos.
|
||||
|
||||
- [ ] **Step 1: Escrever o teste** com fixtures sintéticas (3 conjuntos de ids), afirmando que o diff detecta um id presente em um e ausente em outro. (Implemente a lógica como função pura `diffProviderSets(canonical, registry, validators)` que retorna `{missingInRegistry, missingInValidators, orphanInRegistry}`.)
|
||||
|
||||
- [ ] **Step 2: Rodar e ver falhar.**
|
||||
|
||||
- [ ] **Step 3: Implementar** importando os ids reais (descobertos no Step 0) e cruzando-os. Reusar o padrão de `scripts/check/check-docs-counts-sync.mjs` (que **já conta** executors/oauth/strategies vs docs — é o template direto). Para ids que legitimamente vivem só num lugar, manter um `KNOWN_EXCEPTIONS` allowlist com comentário por entrada.
|
||||
|
||||
- [ ] **Step 4: Rodar no repo real**, triar as divergências reais (as contagens já divergem — algumas serão bugs de registro pela metade, outras exceções legítimas).
|
||||
|
||||
- [ ] **Step 5: Commit** (`feat(check): provider-id consistency gate across providers.ts/registry/validation`).
|
||||
|
||||
**Critério de aceitação:** o gate falha quando um id existe em `providers.ts` mas não no `providerRegistry.ts` (ou vice-versa), e quando um model no registry referencia um provider id desconhecido.
|
||||
|
||||
### Task 2.3: `check-openapi-routes.mjs` — spec ↔ rotas (bidirecional)
|
||||
|
||||
**Ataca:** docs-hallucination (endpoint inventado no `openapi.yaml`).
|
||||
|
||||
**Files:** Create: `scripts/check/check-openapi-routes.mjs`; Test correspondente.
|
||||
|
||||
- [ ] **Step 0 (VERIFICAÇÃO):** confirmar o caminho e a forma do spec:
|
||||
```bash
|
||||
ls docs/reference/openapi.yaml && grep -nE "^\s{2,4}/[a-z]" docs/reference/openapi.yaml | head
|
||||
```
|
||||
Confirmar como `check-openapi-coverage.mjs` já parseia o YAML (reusar o parser dele).
|
||||
|
||||
- [ ] **Step 1–5 (TDD):** teste com spec sintética → implementar: toda `path` do spec resolve para um `route.ts` (reusando `resolveApiPathToRoute` da Task 2.1) e toda rota não-interna aparece no spec (com allowlist para rotas LOCAL_ONLY/internas). Commit.
|
||||
|
||||
### Task 2.4: `check-deps.mjs` — anti-slopsquatting
|
||||
|
||||
**Ataca:** pacotes alucinados (CSA: 19,7% das amostras IA).
|
||||
|
||||
**Files:** Create: `scripts/check/check-deps.mjs`; Test.
|
||||
|
||||
- [ ] **Step 0 (VERIFICAÇÃO):** decidir política — allowlist = todas as deps atuais do `package.json`/lockfile como baseline; novas deps exigem (a) existir no registry, (b) idade ≥ 72h, (c) entrada explícita.
|
||||
- [ ] **Step 1–5 (TDD):** teste com `package.json` sintético adicionando uma dep nova → gate falha se a dep não está no baseline E (não existe no registry OU foi publicada há <72h). Usar `npm view <pkg> time.created` para idade. Reusar o padrão diff-base...HEAD de `check-pr-test-policy.mjs`. Commit.
|
||||
|
||||
### Task 2.5 (opcional): lints Rule #11/#12 via `no-restricted-syntax`
|
||||
|
||||
- [ ] Estender o `eslint.config.mjs` (bloco `no-restricted-syntax` já usado p/ a regra de busca turca) para sinalizar `NextResponse.json({ error: "<string>" })` cru (exigir `buildErrorBody()`) e literais de `clientIdDefault`/`clientSecretDefault` no `providerRegistry.ts` (exigir `resolvePublicCred()`). Ratchet via contagem (adoção 11% → sobe). TDD com fixtures de ESLint.
|
||||
|
||||
**Plugar tudo da Fase 2 no CI:** adicionar `check:fetch-targets`, `check:provider-consistency`, `check:openapi-routes`, `check:deps` ao job `lint` (ou `docs-sync-strict`) do `ci.yml`, e ao pre-commit barato quando rápidos o suficiente.
|
||||
|
||||
---
|
||||
|
||||
# FASE 3 — Catraca de Duplicação + Tamanho (mata-slop) — *expandir em sub-plano*
|
||||
|
||||
**Justificativa:** GitClear mostra duplicação 4–8× na era IA; é a assinatura nº1 de slop. Nenhum gate hoje (Sonar CPD excluído).
|
||||
|
||||
**Specs (criar sub-plano `writing-plans` a partir daqui):**
|
||||
|
||||
1. **Adicionar `jscpd` v5** como devDependency. Rodar `jscpd --reporters json src open-sse` → `quality-metrics.json` ganha `duplication.pct`. **[verificar no install]** o schema JSON do v5 antes de parsear (ver ressalva no relatório §4.2).
|
||||
2. **Estender `collect-metrics.mjs`** com um coletor de duplicação (lê o JSON do jscpd) e um coletor de **tamanho de arquivo** (conta LOC de cada arquivo em `src`+`open-sse`, emite `fileSize.<path>` para os arquivos já acima de um teto, ex. >700 LOC — congelando os 64 atuais).
|
||||
3. **Adicionar regras ESLint** em `eslint.config.mjs` como `warn`: `max-lines` (700), `max-lines-per-function` (80), `complexity` (15), `sonarjs/cognitive-complexity` (15). Coletor adiciona `eslintWarnings` por categoria (a contagem já está na catraca da Fase 1; opcionalmente segregar `complexityWarnings`).
|
||||
4. **Estender `quality-baseline.json`** com `duplication.pct` (`down`) e os `fileSize.*` dos arquivos grandes (`down` — só podem encolher; arquivos novos têm teto absoluto).
|
||||
5. **Teto absoluto para arquivos novos:** o gate falha se um arquivo **não** presente no baseline nasce acima do teto (ex. 700 LOC) — impede o próximo god-component.
|
||||
|
||||
**Critérios de aceitação:** PR que aumenta a duplicação % falha; PR que cresce qualquer um dos 64 arquivos grandes falha; PR que cria arquivo novo >700 LOC falha; refator que encolhe um arquivo grande ratcheta o baseline para baixo (via `--update`).
|
||||
|
||||
---
|
||||
|
||||
# FASE 4 — Catraca de Cobertura + Anti Test-Masking — *expandir em sub-plano*
|
||||
|
||||
**Specs:**
|
||||
|
||||
1. **`check-coverage-ratchet`** já é coberto pelo motor da Fase 1 (as 4 métricas `coverage.*` com `direction: "up"`). Confirmar que o job `quality-gate` roda **após** o merge dos 8 shards de cobertura no CI (não localmente, onde a cobertura é parcial). Adicionar **epsilon** maior (ex. 0.5) para `coverage.branches` por causa do não-determinismo do v8.
|
||||
2. **Pisos por módulo crítico:** estender `collect-metrics.mjs` para emitir `coverage.<modulo>.lines` para uma lista curta de módulos de alto risco (lendo o `coverage-summary.json` por arquivo): `open-sse/handlers/chatCore.ts`, `open-sse/services/combo.ts`, `open-sse/services/accountFallback.ts`, `src/sse/services/auth.ts`, `src/server/authz/routeGuard.ts`, `open-sse/utils/error.ts`, `open-sse/utils/publicCreds.ts`, `src/shared/utils/circuitBreaker.ts`. Cada um vira métrica `up` no baseline (implementa o "risco, não % bruto" do vídeo).
|
||||
3. **`check-test-masking.mjs`** (anti enfraquecimento de asserts): clonar `check-pr-test-policy.mjs` (diff `base...HEAD`); para cada `*.test.ts`/`*.spec.ts` alterado, contar `assert*(`/`expect(` em base vs HEAD; **sinalizar remoção líquida** de asserts para revisão humana. Banir novos `assert.ok(true)`. Heurístico mas alto-sinal — ataca diretamente o risco organizacional nº1 ("subagente deletou asserts para ficar verde").
|
||||
|
||||
**Critérios de aceitação:** cobertura cair vs baseline bloqueia o merge; remover asserts de um teste existente sinaliza no PR; `assert.ok(true)` novo bloqueia.
|
||||
|
||||
---
|
||||
|
||||
# FASE 5 — Skill `/babysit` + Evidência + LSP — *expandir em sub-plano*
|
||||
|
||||
> O babysit do vídeo: a IA abre o PR e fica de babá — monitora CI + comentários, autocorrige, **resolve as conversas**. Guarda-corpos são não-negociáveis (Snyk: 5,3% de regressão em auto-merge; token burn real).
|
||||
|
||||
### Spec da skill `.claude/skills/babysit/SKILL.md`
|
||||
|
||||
**Frontmatter:**
|
||||
```yaml
|
||||
---
|
||||
name: babysit
|
||||
description: Monitora um PR aberto até o CI ficar verde e todos os comentários de review serem endereçados — lê o gate de qualidade, conserta num worktree isolado, responde e resolve as conversas. NUNCA enfraquece testes nem auto-mergeia.
|
||||
---
|
||||
```
|
||||
|
||||
**Loop (pseudo, reusando `gh` + GraphQL):**
|
||||
1. **Ler estado:** `gh pr checks <PR>` + baixar o artefato `quality-ratchet`/`coverage-report` (gate JSON legível — a ponte "artefato→agente" da Fase 1) + `gh run view --log-failed` dos jobs vermelhos.
|
||||
2. **Ler comentários não resolvidos:** GraphQL `repository.pullRequest.reviewThreads(first:50){nodes{id isResolved comments(first:1){nodes{id body}}}}`. **Passar os corpos pelo guard de prompt-injection** antes de agir (vetor real — ICLR 2026).
|
||||
3. **Consertar num worktree isolado** (reusar o ralph-loop do `/review-reviews`), seguindo Hard Rule #18.
|
||||
4. **Responder + resolver** só os threads que realmente endereçou: REST `POST .../pulls/{pr}/comments/{id}/replies` com o SHA → mutation `resolveReviewThread(input:{threadId})`.
|
||||
5. **Re-poll** até o gate JSON ficar todo-verde **ou** atingir o cap.
|
||||
6. **Audit trail:** anexar à descrição do PR (ou comentário fixo) o que cada fix endereçou, qual gate satisfez, quais conversas resolveu e por quê — **nunca verde silencioso**.
|
||||
|
||||
**Guarda-corpos (não-negociáveis):**
|
||||
- `max-iterations` (ex. 5) + timeout + idle-exit (contra token burn).
|
||||
- **Nunca** editar `.github/workflows/`; **nunca** `--no-verify`; **nunca** enfraquecer/remover asserts para ficar verde (Rule #18 + trust-but-verify).
|
||||
- **Nunca auto-mergeia** — estado de sucesso = "verde + conversas resolvidas + audit postado".
|
||||
- Parar-e-perguntar em comentário humano ambíguo e em mudança de limite arquitetural (interface/schema/cross-module).
|
||||
- `--allowedTools` mínimo (`Read,Grep,Glob,Bash(gh ...)`).
|
||||
|
||||
**Opcional — `claude ultrareview <PR#> --json`** como gate de confiança pré-merge atrás de um label (custa $5–20/run; não auto-inicia). Loop interno de custo-zero = `/code-review --fix` local.
|
||||
|
||||
### Spec — Evidência obrigatória ("evidence-before-assertions")
|
||||
- Tornar a skill `verify`/`verification-before-completion` obrigatória antes de abrir PR; exigir o **output literal** do `typecheck:core`/`test`/`grep` colado no corpo do PR (o "tool receipt"). Adicionar um `check-pr-evidence.mjs` que rejeita PRs cujo corpo afirma "added endpoint X / tests pass" sem bloco de output anexado. Formaliza a Rule #18.
|
||||
|
||||
### Spec — LSP-in-the-loop (opcional)
|
||||
- Registrar `agent-lsp` (MCP) ou o `tsserver` para os agentes terem `blast_radius`/diagnostics e `preview_edit` antes de escrever — vira "símbolo inventado" de catch-de-review para impossibilidade-no-edit. Pareia com `typecheck:core` como gate pré-PR (compile-before-claim).
|
||||
|
||||
**Critérios de aceitação:** a skill `/babysit <PR#>` leva um PR de vermelho a verde sem auto-merge, resolve só as conversas que endereçou, respeita o cap de iterações, e deixa rastro auditável; nenhum assert é enfraquecido.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (checklist do autor)
|
||||
|
||||
- **Cobertura do spec:** Fases 0–2 mapeiam 1:1 com as recomendações do relatório §5; Fases 3–5 cobrem duplicação/tamanho, cobertura/masking e babysit/evidência. ✓
|
||||
- **Sem placeholders nos passos bite-sized:** Fases 0/1/2.1 têm comandos e código reais. As Fases 2.2–2.4 usam um Step-0 de verificação **deliberado** (não placeholder) para não fabricar nomes de export — coerente com o objetivo anti-alucinação. ✓
|
||||
- **Consistência de tipos:** `resolveApiPathToRoute` (Task 2.1) é reusada na Task 2.3; o comparador usa o mesmo formato `{value, direction}` em todas as fases; `quality-metrics.json` é a interface única coletor↔comparador. ✓
|
||||
- **Ordem de dependência:** Fase 1 (motor) precede 3/4 (que só adicionam métricas ao baseline); Fase 0 destrava o resto. ✓
|
||||
|
||||
## Handoff de Execução
|
||||
|
||||
**Plano salvo em `PLANO-QUALITY-GATES.md`.** Duas opções:
|
||||
|
||||
1. **Subagent-Driven (recomendado)** — um subagente fresco por task, review entre tasks. SUB-SKILL: `superpowers:subagent-driven-development`.
|
||||
2. **Inline** — executar nesta sessão com checkpoints. SUB-SKILL: `superpowers:executing-plans`.
|
||||
|
||||
**Recomendação:** começar pela **Fase 0** (quick wins, baixo risco) e **Fase 1** (motor de catraca) numa branch `feat/quality-ratchet`, validar num PR de teste, e só então abrir as fases anti-alucinação. Fases 3–5 viram sub-planos próprios.
|
||||
219
RELATORIO-QUALITY-GATES.md
Normal file
219
RELATORIO-QUALITY-GATES.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# Relatório — Quality Gates, Catraca & Anti-Alucinação no OmniRoute
|
||||
|
||||
> **Data:** 2026-06-09
|
||||
> **Origem:** Auditoria do projeto (5 subagentes Opus em paralelo mapeando todas as pastas exceto `node_modules`/`_references`/`dist`) + análise da transcrição do vídeo *"Qualidade de código"* (Stupid Button Club, 2026-05-04) + pesquisa web 2026 (4 frentes, últimos ~3 meses).
|
||||
> **Companheiro:** Veja [`PLANO-QUALITY-GATES.md`](./PLANO-QUALITY-GATES.md) para o plano de implementação bite-sized (TDD).
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR
|
||||
|
||||
1. **O OmniRoute já é muito mais maduro** que o projeto "Strawberry" do vídeo: tem CI com 20 jobs, gate de cobertura, ESLint 9 flat, SonarQube, 14 scripts `check-*.mjs` e **uma catraca real já funcionando** (`check-t11-any-budget.mjs` — orçamento de `any` por arquivo que só pode encolher). O vídeo descreve onde queremos chegar; nós já estamos a meio caminho.
|
||||
2. **Mas faltam exatamente as catracas que o vídeo prega.** Não há baseline congelado de métricas, nem gate de **duplicação**, nem de **tamanho de arquivo**, e o gate de cobertura é um **piso fixo** — não uma catraca "nunca piorar".
|
||||
3. **Há derivas (drifts) reais que pegamos na auditoria:** o gate de cobertura **no CI é `40/40/40/40`** (ci.yml:377), não os `60/60/60/60` que o CLAUDE.md anuncia (esse é só o script local). O Husky está **100% comentado** (zero gate local). O SonarQube tem `coverage` e `cpd` **excluídos** (`sonar-project.properties:9-10`) — as duas métricas mais úteis contra "slop" estão desligadas. E 3 scripts `check-*` existem mas **não rodam em lugar nenhum**.
|
||||
4. **Os maiores ímãs de alucinação são estruturais:** o split de provider em 3 arquivos gigantes em 2 workspaces (`providers.ts` ↔ `providerRegistry.ts` ↔ `validation.ts`, com contagens que já divergem: 229 ids vs 155 blocos vs N validadores), os 300 paths `fetch("/api/...")` hardcoded sem ligação de compilação com as rotas, e o arquivo de **12.760 linhas** (`providers/[id]/page.tsx`) que nenhum agente consegue segurar em contexto.
|
||||
5. **2026 confirma a tese do vídeo com dados:** GitClear (211M linhas) mostra duplicação crescendo 4–8× na era da IA; o paper SlopCodeBench prova que *instrução de prompt sozinha não impede a degradação* — só gates determinísticos seguram. O ecossistema 2026 tem ferramentas maduras para cada métrica (jscpd v5, knip v6, eslint-plugin-sonarjs v4, osv-scanner, Qlty, Sonar "Clean as You Code"/"AI Code Assurance").
|
||||
6. **A jogada é em camadas:** (a) reativar/reconciliar o que já existe; (b) construir o **motor de catraca** (baseline.json + coletor + comparador, clonando o `any-budget`); (c) adicionar **gates determinísticos anti-alucinação** (provider-consistency, fetch-target, openapi-routes); (d) catraca de **duplicação + tamanho**; (e) catraca de **cobertura** + detecção de test-masking; (f) skill **`/babysit`** com guarda-corpos. Detalhe no plano.
|
||||
|
||||
---
|
||||
|
||||
## 1. O que o vídeo ensina (insights destilados)
|
||||
|
||||
O vídeo é uma fala sem roteiro sobre *qualidade de código no mundo em que a IA escreve ~100% do código*. Pontos centrais:
|
||||
|
||||
| # | Insight | Citação/essência |
|
||||
|---|---------|------------------|
|
||||
| 1 | **O humano virou o gargalo.** | "Eu acabei virando o gargalo da IA. Fazer o babysit das coisas do request básicas é o gargalo. Não consigo entregar 4 tarefas ao mesmo tempo se preciso ler 10.000 linhas/dia." |
|
||||
| 2 | **Quality Gate = portão que a IA tem que passar.** | Todo PR passa por um portão; a IA fica em loop se autocorrigindo até ficar verde, em vez de o humano revisar e pedir refação. |
|
||||
| 3 | **Baseline + Catraca (ratchet).** | "Tu congela o baseline e o repositório só pode melhorar a partir dali ou empatar." A catraca anda num sentido só. |
|
||||
| 4 | **Regra de ouro.** | "Cada PR pode adicionar código, mas não pode aumentar nenhuma das métricas — nem por uma violação, nem por uma linha, nem por 0,1 ponto percentual." |
|
||||
| 5 | **Métricas do baseline.** | Violações de ESLint (483 em 120 arquivos), duplicação de código (2,2% via JSCPD), cobertura (%), arquivos acima do limite de tamanho (19 arquivos; o maior com 4.600 linhas). |
|
||||
| 6 | **Pipeline de CI.** | `npm ci` → `npm audit` (critical=bloqueia / high=avisa) → `npm run lint` → `test:coverage` → **script node de quality gate** que compara métricas atuais vs `baseline.json` e falha em qualquer regressão → comentário no PR + **upload de artefatos** que o agente lê para se autocorrigir. |
|
||||
| 7 | **Artefatos legíveis pelo agente.** | "Não adianta cuspir isso no PR. O agente precisa ter acesso ao que está dando errado." |
|
||||
| 8 | **Babysit skill.** | "Recomendo criar uma skill de babysit": a IA monitora o CI + comentários dos revisores, endereça os comentários e **resolve as conversas** para dar rastreabilidade no GitHub. |
|
||||
| 9 | **Comentários perto do código (legibilidade p/ agente).** | Mudou de ideia: antes era contra comentários ("o código é a documentação"); agora, no mundo de agentes, comentário explicando *o quê* e *por quê* perto do código vale mais que um MD gigante, porque o harness faz `grep` no arquivo e lê o comentário junto. |
|
||||
| 10 | **É "só" colar ferramentas.** | "Não é nada excepcional. Eu só estou colando um monte de ferramentas e chamando de quality gate." Pode-se usar SonarQube ou GitHub Code Quality no lugar do script caseiro. |
|
||||
| 11 | **Por que a IA não faz certo de primeira.** | Modelos top já sabem (foram treinados com os livros), mas são "preguiçosos" porque output imperfeito = mais tokens vendidos. A catraca força o nível. |
|
||||
|
||||
**Tradução para o nosso contexto:** o vídeo descreve um sistema que **já temos em embrião** (o `any-budget` é exatamente a catraca da regra de ouro, só que para uma métrica). O salto é (1) generalizar a catraca para todas as métricas, (2) reconciliar os drifts, e (3) fechar os buracos anti-alucinação que são específicos do nosso tamanho.
|
||||
|
||||
---
|
||||
|
||||
## 2. Onde o OmniRoute está hoje (panorama auditado)
|
||||
|
||||
### 2.1 O que já temos (e o vídeo nem sonha)
|
||||
|
||||
- **CI robusto** (`.github/workflows/ci.yml`, ~25 KB, 20 jobs): lint + audit + cycles + route-validation + any-budget + docs-sync + typecheck (core e noimplicit) + build + package-artifact + electron-smoke + unit (8 shards) + Node 24/26 compat + coverage (8 shards + merge) + SonarQube + e2e (9 shards) + integration + security.
|
||||
- **Catraca real já existente:** `scripts/check/check-t11-any-budget.mjs` — array `{file, maxAny}` (a maioria `0`), strip de comentários, anotações de falso-positivo, `exit 1` em regressão. **É o template exato da catraca do vídeo.**
|
||||
- **14 scripts `check-*.mjs`** (cycles, route-validation, any-budget, docs-sync, docs-counts, env-doc-sync, deprecated-versions, doc-links, cli-i18n, openapi-coverage, openapi-security-tiers, pr-test-policy, node-runtime, test-report-summary) — vários já são *gates de consistência fonte-vs-derivado*, o mesmo padrão que precisamos para anti-alucinação.
|
||||
- **PR test policy:** `check-pr-test-policy.mjs` já força "mudou código de produção ⇒ mudou teste" (diff base...HEAD).
|
||||
- **Cobertura sumarizada + comentada no PR:** `test-report-summary.mjs` + `coverage/coverage-summary.json` + job `coverage-pr-comment` (comentário com marcador `<!-- omniroute-coverage-report -->`). **Isto é exatamente o "artefato legível pelo agente" do vídeo** — já construído.
|
||||
- **Disciplina TDD institucionalizada** (Hard Rule #18: todo fix precisa de teste falha→passa ou validação ao vivo no VPS).
|
||||
- **SonarQube** configurado (job no CI + `sonar-project.properties`).
|
||||
- **Skills agênticas de review** já existem: `/review-prs`, `/review-reviews` (bateria de 8 reviewers + ralph-loop), `/code-review`, `/generate-release` (a única com babysit real de CI, mas de workflows de *release*, não do `ci.yml` do PR).
|
||||
|
||||
### 2.2 O que está DESLIGADO ou inerte ⚠️ (achados da auditoria)
|
||||
|
||||
| Item | Estado | Evidência | Impacto |
|
||||
|------|--------|-----------|---------|
|
||||
| **Husky** | 100% comentado (pre-commit **e** pre-push) | `.husky/pre-commit`, `.husky/pre-push` (todas as linhas com `#`) | Zero enforcement local — lint-staged, docs-sync, any-budget, env-doc-sync, openapi checks e o `test:unit` de pre-push dependem 100% do CI. |
|
||||
| **Gate de cobertura no CI** | **40/40/40/40** (não 60) | `ci.yml:377` `--statements 40 --lines 40 --functions 40 --branches 40` | O comentário do PR renderiza contra 60, o script local gata 60, o RELEASE_CHECKLIST diz 75/70, e o baseline real é ~79–82%. **O único número que bloqueia merge é 40** → gate de cobertura quase banguela. |
|
||||
| **Sonar coverage** | Excluído | `sonar-project.properties:9` `sonar.coverage.exclusions=**/*` | Sonar ignora cobertura de todo arquivo. |
|
||||
| **Sonar CPD (duplicação)** | Excluído | `sonar-project.properties:10` `sonar.cpd.exclusions=**/*` | Sonar não detecta copy-paste — a assinatura nº1 de slop de IA. |
|
||||
| **SonarQube job** | Inerte | `ci.yml` roda scan só se `PR && SONAR_TOKEN != '' && SONAR_HOST_URL != ''`; sem `qualitygate.wait` | Em runs sem secret, escreve "skipped"; mesmo quando roda, nunca falha o build. |
|
||||
| **`npm audit`** | Plano (`moderate`), não escalonado | `package.json:112` `--audit-level=moderate` | O vídeo prega critical=bloqueia / high=avisa. O nosso é um nível único. |
|
||||
| **3 scripts órfãos** | Sem CI nem husky | `check:cli-i18n`, `check:openapi-coverage`, `check:openapi-security-tiers` | Existem, dão `exit 1`, mas não rodam em lugar nenhum (Hard Rules #15/#17 guardadas só por um deles). |
|
||||
| **`typecheck:noimplicit:core`** | `continue-on-error: true` | `ci.yml:45-46` | Warn-only "forward-looking". |
|
||||
|
||||
### 2.3 Hotspots de tamanho (sem gate hoje)
|
||||
|
||||
`64 arquivos > 1000 LOC`, `194 > 500 LOC` (src + open-sse, sem testes). Top:
|
||||
|
||||
| LOC | Arquivo | Risco para edição por IA |
|
||||
|-----|---------|--------------------------|
|
||||
| **12.760** | `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` | God-component: **192 `useState`**, 21 `useEffect`, 87 `fetch()` inline, 34 tipos inline. Nenhuma IA segura em contexto; qualquer edição arrisca apagar estado não relacionado. |
|
||||
| 5.977 | `open-sse/handlers/chatCore.ts` | God-handler: 58 funções, invariantes demais, blast radius alto. |
|
||||
| 4.590 | `open-sse/config/providerRegistry.ts` | Array gigante providers+models+OAuth; mistura `resolvePublicCred()` e literais crus. |
|
||||
| 4.456 | `open-sse/services/combo.ts` | 14 estratégias num `if/else if` sem enum/exhaustiveness — estratégia desconhecida vira no-op silencioso. |
|
||||
| 4.349 | `src/app/(dashboard)/dashboard/combos/page.tsx` | 51 `useState`, mesmo padrão god-component. |
|
||||
| 4.205 | `src/lib/providers/validation.ts` | Mega-função com closures e `SPECIALTY_VALIDATORS` definidos *dentro* da função. |
|
||||
| 3.776 | `open-sse/handlers/imageGeneration.ts` | Branching multi-provider num handler. |
|
||||
| 3.076 | `src/shared/constants/providers.ts` | 229 ids em 27 consts agrupados via `Proxy` — sem lista plana. |
|
||||
| 2.869 | `open-sse/executors/chatgpt-web.ts` | Sessão web reversa; classe só começa na linha 2443. |
|
||||
| 2.278 | `src/app/api/providers/[id]/models/route.ts` | God-route: importa ~30 módulos provider-específicos e ramifica por provider num GET. |
|
||||
|
||||
### 2.4 Ímãs de alucinação (ranqueados, da auditoria)
|
||||
|
||||
1. **Split de provider em 3 arquivos / 2 workspaces (nº1).** Não há lista plana de providers — 229 ids escondidos atrás de 27 consts + merge via `Proxy` (`AI_PROVIDERS`). Uma IA não consegue enumerar "quais providers existem" barato → **inventa ids plausíveis** (variantes `*-web`/`*-cli` inexistentes) ou registra no grupo errado. As três contagens (`providers.ts` 229 ids ↔ `providerRegistry.ts` 155 blocos ↔ `validation.ts` N validadores) **já divergem**, então não há cross-check autoritativo. *(Esse é o tema recorrente das nossas memórias de alucinação — ex.: ids inventados, modelos inexistentes.)*
|
||||
2. **300 paths `fetch("/api/...")` hardcoded** no dashboard (659 call sites), sem client tipado. Refatore uma rota e os call sites apodrecem silenciosamente; uma IA editando a UI inventa rota (`/api/providers/[id]/refresh`) ou assume `res.error.message` numa rota que devolve `{error:"..."}`. Sem ligação de símbolo entre os 659 call sites e os 488 `route.ts`.
|
||||
3. **Dual chat stack (armadilha documentada).** Seleção/fallback de conta vive em `src/sse/` (não `open-sse/`). Uma IA pedida para "consertar fallback de conta" edita `open-sse/handlers/chatCore.ts` (errado) em vez de `src/sse/services/auth.ts`. Nada no código cruza os dois stacks. *(Já erramos um diagnóstico público por isso.)*
|
||||
4. **Estratégias de combo inventadas.** 14 nomes reais soterrados num `if/else` de strings (sem enum exportado) → IA inventa nomes plausíveis-mas-falsos (`"latency-optimized"`, `"failover"`) que passam no typecheck como string e viram no-op.
|
||||
5. **Métodos de executor inventados.** O padrão real é "sobrescreve `execute()` inteiro" (48/50 executors), sem hooks documentados → IA inventa `buildRequest`/`parseChunk`/`mapError` que não existem em `BaseExecutor`.
|
||||
6. **Helpers de erro inventados** + queda para `err.message` cru (viola Rule #12) quando o helper inventado "falha"; 5 web executors hoje **não importam helper nenhum**.
|
||||
7. **AGENTS.md de DB defasado:** documenta 21 migrations / 22 módulos quando o real é **94 / 75** — uma IA lendo isso acredita em conjuntos de tabelas/módulos que não existem mais.
|
||||
8. **Route-guard omitido:** rotas novas spawn-capazes (`/api/services/`, `/api/mcp/`) devem entrar em `LOCAL_ONLY_API_PREFIXES`; a convenção está só no CLAUDE.md, não num teste que a IA veja (parcialmente coberta por 1 script órfão).
|
||||
|
||||
---
|
||||
|
||||
## 3. Gap analysis — modelo do vídeo vs OmniRoute
|
||||
|
||||
| Métrica/peça do vídeo | OmniRoute hoje | Gap |
|
||||
|-----------------------|----------------|-----|
|
||||
| `npm ci` determinístico | ✅ em todos os 14 jobs | — |
|
||||
| `npm audit` critical=bloqueia / high=avisa | ⚠️ `--audit-level=moderate` (nível único) | **Escalonar** em dois invokes |
|
||||
| `lint` | ✅ bloqueante | — |
|
||||
| `test` + cobertura | ✅ mas piso **40** no CI (drift) | **Reconciliar** p/ baseline real + catraca |
|
||||
| **Contagem de ESLint congelada** | ❌ (lint é 0-erros, mas warnings livres) | **Construir** (ratchet de violações) |
|
||||
| **Duplicação % (JSCPD)** | ❌ (Sonar CPD excluído, sem jscpd) | **Construir** (jscpd + catraca) |
|
||||
| **Limite de tamanho de arquivo** | ❌ (sem `max-lines`, sem script) | **Construir** (ESLint max-lines + catraca, freeze dos 64) |
|
||||
| **`baseline.json` congelado** | ❌ (nenhum baseline de métricas no repo) | **Construir** (o coração da catraca) |
|
||||
| **Script comparador (regra de ouro)** | 🟡 existe **para `any`** (`any-budget`) | **Generalizar** p/ todas as métricas |
|
||||
| **Sumário markdown + artefatos p/ o agente** | ✅ (coverage summary + PR comment + artifact) | **Reusar** wholesale |
|
||||
| **Babysit skill (monitora CI + resolve conversas)** | 🟡 `review-prs`/`review-reviews`/`generate-release` parciais; nenhuma resolve threads do PR nem loopa no `ci.yml` | **Construir** `/babysit` |
|
||||
| **Comentários perto do código p/ legibilidade de agente** | 🟡 `routeGuard.ts` é exemplar; resto irregular | **Padrão cultural** (Karpathy/guidelines) |
|
||||
|
||||
---
|
||||
|
||||
## 4. O que o mundo faz em 2026 (pesquisa, últimos ~3 meses)
|
||||
|
||||
> Todas as fontes abaixo vêm com URL + data nas seções de origem (ver §7). Onde a pesquisa **não conseguiu confirmar** algo de fonte primária, está marcado **[não-verificado]** — honestidade de engenharia.
|
||||
|
||||
### 4.1 Catraca / baseline-freeze (a "catraca" do vídeo)
|
||||
|
||||
- **betterer** — o tool canônico de ratchet (snapshot de métrica → `.betterer.results`; CI falha se piora, auto-atualiza se melhora). **[caveat]** Baixa velocidade: último commit no `master` em **ago/2025**, releases vazias no GitHub. Viável, mas **não** apostar como peça load-bearing de longo prazo.
|
||||
- **eslint-formatter-ratchet** — formatter que congela contagem de violações de ESLint. **Ativamente mantido** (commit 2026-03-17). Mais estreito (só ESLint) mas é a trajetória oposta ao betterer.
|
||||
- **SonarQube "Clean as You Code" (new-code conditions)** — o padrão baseline-freeze mais maduro: o quality gate aplica condições **só ao código novo** (branch de referência), grandfathering do legado. Atual.
|
||||
- **SonarQube "AI Code Assurance" (2026.1.0)** — gate específico para código gerado por IA (tag o projeto → workflow de assurance + "Sonar way for AI Code" mais estrito). **[não-verificado]** se *bloqueia* o PR (docs canônicas deram 404; descrito como "enforced quality gate" mas mecânica de bloqueio não confirmada em fonte única).
|
||||
- **Qlty CLI (qlty.sh)** — o produto 2026 mais aderente: CLI Rust **OSS e grátis** (v0.630.0, 2026-05-08) que agrega 70+ analisadores; tem **Baseline analysis** (= a catraca), **Quality Gates** com veredito go/no-go e coverage gates. **[caveat]** `qlty metrics` (a tabela LOC/complexidade) **não tem flag JSON** — só `qlty check --sarif`/`qlty smells --sarif`; o ratchet de tamanho/complexidade por arquivo ainda precisa do JSON do ESLint.
|
||||
- **Code Climate Quality → Qlty** — a marca clássica de ratchet virou empresa separada (Qlty, nov/2024). Write-ups antigos de "Code Climate" = Qlty hoje.
|
||||
|
||||
### 4.2 Ferramentas de métrica por tipo (todas com JSON p/ alimentar a catraca)
|
||||
|
||||
| Métrica | Tool 2026 | Comando JSON | Status |
|
||||
|---------|-----------|--------------|--------|
|
||||
| Duplicação | **jscpd v5** (reescrita Rust) | `jscpd --reporters json` (ou `sarif`) | Muito ativo (v5.0.4, 2026-06-08). **[caveat]** schema JSON v4→v5 não confirmado — verificar no install. |
|
||||
| Tamanho/fn-length/ciclomática | **ESLint core** (`max-lines`, `max-lines-per-function`, `complexity`) | `eslint --format json` | Built-in ESLint 9 |
|
||||
| Complexidade cognitiva | **eslint-plugin-sonarjs** (`sonarjs/cognitive-complexity`) | `eslint --format json` | Mantido (v4.0.3, 2026-04-16; agora no monorepo SonarJS — o repo standalone foi arquivado, mas o pacote está vivo). **[caveat]** README das rules deu 404; presença de S3776 em v4 é alta-confiança mas confirmar no install. |
|
||||
| Dead code / unused exports / unused deps | **knip** (vence ts-prune **arquivado** + depcheck **arquivado**) | `knip --reporter json` | Muito ativo (v6.16.1, 2026-06-06) |
|
||||
| Ciclos | **check-cycles.mjs** (já temos) + **dpdm** opcional | `dpdm --circular --output deps.json` | dpdm ativo (v4.2.0, 2026-05-09); madge estagnado |
|
||||
| Vulnerabilidades | **osv-scanner** (Google/OSV) | `osv-scanner --format json` | Muito ativo (push 2026-06-08) |
|
||||
| Política de lockfile (gate, não métrica) | **lockfile-lint** | `lockfile-lint --validate-https --validate-integrity` | Mantido (v5.0.0, 2026-01-25) |
|
||||
|
||||
> **Realidade do ratchet:** não existe tool único 2026 que emita *todas* as métricas como um JSON limpo. O padrão robusto é **N tools que emitem JSON + um reducer Node** que monta `metrics-summary.json` + o comparador que falha só em regressão (exatamente o que o `any-budget` já faz para uma métrica).
|
||||
|
||||
### 4.3 Anti-alucinação (2026)
|
||||
|
||||
- **LSP-in-the-loop / `agent-lsp` (MCP)** — servidor MCP que dá ao agente fatos verificáveis do language server (definições, referências, tipos, diagnostics, `blast_radius`) e `preview_edit` antes de escrever. Funciona com Claude Code. **Fit alto:** vira "símbolo inventado" de catch-de-review para *impossibilidade-no-edit*. (v0.13.0, 2026-06-04 — pequeno mas ativo.)
|
||||
- **Slopsquatting / pacotes alucinados** — **CSA Research Note (2026-04-19):** **19,7%** de 2,23M amostras de código IA continham nomes de pacote alucinados; 205k nomes fabricados únicos; **43%** reaparecem em re-runs (registráveis por atacantes). Defesas: **allowlist** de deps para agentes, **registry existence check** antes de instalar, **age-cooldown** (24–72h), lockfile-exact, scripts de install desabilitados. **Fit alto:** novo `check-deps.mjs`.
|
||||
- **Semcheck** (v1.2.1, fev/2026) — CLI que usa LLM para verificar que a implementação bate com o spec/doc, via `semcheck.yaml` ligando doc↔código; roda em pre-commit e Actions com `fail-on-issues`. Feito para pegar **"docs que descrevem features não implementadas"**. **Fit muito alto** para nossos incidentes recorrentes de docs alucinadas — porém é fuzzy (LLM); pareie com checks determinísticos.
|
||||
- **OpenAPI drift determinístico** — check que toda `path` do `openapi.yaml` resolve para um `route.ts` real (e vice-versa). Rápido, sem LLM, pega "endpoint inventado". **Fit alto** para `docs/reference/openapi.yaml`.
|
||||
- **Skill `verify` / `verification-before-completion`** (já no nosso ambiente) — "evidence before assertions": o veredito PASS/FAIL repousa **só** no que o app rodando demonstrou; rejeita "rodei os testes" como prova. **Fit altíssimo:** é a formalização da nossa Hard Rule #18 — exigir o *output literal* do comando colado no PR ("tool receipt").
|
||||
- **Adversarial review (críticos de sessão fresca)** — agentes Skeptic/Architect/Minimalist leem o diff *contra o spec* ("o autor está comprometido — vai racionalizar"); símbolos/APIs inventados viram violação de spec. Mapeia no nosso `/review-reviews`.
|
||||
- **SlopCodeBench (arXiv 2603.24755, ~mai/2026)** — sem mitigação, erosão estrutural aumentou em **77%** das trajetórias; o código de agente acumula verbosidade ~7× e erosão ~5× mais rápido que repos humanos. **Mitigações só-de-prompt ("anti-slop", "plan-first") melhoram o início mas NÃO param a degradação iterativa.** → **justificativa empírica** de que precisamos de gates determinísticos, não instrução.
|
||||
- **GPT-5.5 System Card (2026-04-23)** — figuras oficiais são modestas (23% mais provável de acerto factual; 3% menos erros num set propenso). O headline de **"queda de 60% em alucinação / 88,7% SWE-bench" é imprensa secundária [não-verificado]**, não a seção de factualidade do system card. Upgrade de modelo ajuda na margem, não substitui gate.
|
||||
|
||||
### 4.4 Babysit loops (2026)
|
||||
|
||||
- **Claude Code "auto-fix in the cloud"** (Anthropic, lançado 2026-03-27): "observa seus PRs na nuvem, resolvendo falhas de CI e comentários de review automaticamente; empurra fixes quando claro, pergunta quando ambíguo." Não auto-mergeia.
|
||||
- **Devin Autofix** (2026-02-10): auto-conserta comentários de review + lint/CI; endereça comentários de bots, mas deixa julgamento humano nas conversas humanas.
|
||||
- **CodeRabbit Autofix** (early access abr/2026): coleta o bloco **"Prompt for AI Agents"** de cada comentário, aplica fix, roda build-verification; **nada mergeia automaticamente**.
|
||||
- **Greptile `greploop` + skill `check-pr`** (MIT) — "dispara review → conserta comentários → re-review até 5/5 de confiança e zero comentários". **Template quase-exato** para a nossa `/babysit`.
|
||||
- **Claude `claude ultrareview <PR#> --json`** (subcomando não-interativo, research preview): bloqueia até terminar, `exit 0/1`, payload de bugs verificados parseável. **Não auto-inicia** e custa $5–20/run → usar atrás de label, não em todo push. (O `/code-review ultra` local com `--fix` é o loop interno de custo-zero.)
|
||||
- **Resolver threads de review:** não há comando `gh` nativo (cli/cli#12419). Padrão de 2 passos GraphQL: `reviewThreads(first:50){nodes{id isResolved...}}` → `mutation { resolveReviewThread(input:{threadId}) }`, respondendo antes com o SHA do commit via REST.
|
||||
- **Guarda-corpos (críticos):**
|
||||
- **Snyk Agent Fix field test (2026): ~5,3% de regressão** ("1 em 19 fixes auto-mergeados introduz problema novo") → **forte argumento contra auto-merge**.
|
||||
- **Token burn:** loops ingênuos realimentam a conversa crescente → prompt incha → alucina do próprio histórico. Uber capou gasto em **$1.500/mês/dev/tool** (abr/2026). Mitigação: **max-iterations**, time limit, idle-exit.
|
||||
- **Test-masking:** o babysit **NÃO** pode enfraquecer/remover asserts para ficar verde (= nossa Rule #18 + memória "trust but verify"). Revisão humana fica nos limites arquiteturais (interface/schema/cross-service).
|
||||
- **Audit trail:** deixar rastro humano-legível (qual fix endereçou o quê, qual gate satisfez, quais conversas resolveu) — nunca um verde silencioso. Reforça o guard de prompt-injection sobre os *corpos de comentário* que o agente ingere (21% dos reviews do ICLR 2026 eram IA; injeção embutida em código é vetor real).
|
||||
|
||||
---
|
||||
|
||||
## 5. Recomendações (ranqueadas) → ver o PLANO
|
||||
|
||||
> Build-vs-buy: **construir in-repo** os gates determinísticos (zero SaaS, dados não saem da box, reusa o harness `check-*.mjs`). **Avaliar Qlty CLI** depois, se quisermos consolidar N scripts num tool. **Não** depender de CodeRabbit/Greptile/Diamond como *o* gate de "não-piorar-métrica" — são opiniões de LLM, não contadores determinísticos.
|
||||
|
||||
**Fase 0 — Reativar & reconciliar (quick wins, sem tooling novo):**
|
||||
1. Reativar pre-commit barato do Husky (lint-staged + docs-sync + any-budget).
|
||||
2. Reconciliar o gate de cobertura: subir o CI de 40 → baseline real (com headroom) e alinhar os 4 lugares que divergem.
|
||||
3. Escalonar `npm audit` (critical=bloqueia / high=avisa).
|
||||
4. Plugar os 3 scripts órfãos (`cli-i18n`, `openapi-coverage`, `openapi-security-tiers`) no CI.
|
||||
|
||||
**Fase 1 — Motor de catraca (o coração):**
|
||||
5. `quality-baseline.json` commitado + `collect-metrics.mjs` (coletor) + `check-quality-ratchet.mjs` (comparador, clone do `any-budget`) + job de CI + artefato + comentário no PR (clone do `coverage-pr-comment`).
|
||||
|
||||
**Fase 2 — Gates determinísticos anti-alucinação:**
|
||||
6. `check-provider-consistency.mjs` (o ímã nº1), `check-fetch-targets.mjs`, `check-openapi-routes.mjs`, allow-list de estratégias/translators/executors, lint Rule #11/#12, `check-deps.mjs` (slopsquatting).
|
||||
|
||||
**Fase 3 — Catraca de duplicação + tamanho (mata-slop):**
|
||||
7. jscpd + ESLint `max-lines`/`max-lines-per-function`/`complexity` + `sonarjs/cognitive-complexity`, congelando os 64 arquivos grandes (catraca só-pode-encolher).
|
||||
|
||||
**Fase 4 — Catraca de cobertura + anti test-masking:**
|
||||
8. `check-coverage-ratchet.mjs` (cobertura não cai vs baseline) + pisos por módulo crítico + `check-test-masking.mjs` (delta de contagem de asserts em testes alterados).
|
||||
|
||||
**Fase 5 — Skill `/babysit` + evidência + LSP:**
|
||||
9. Skill `/babysit` (gh pr checks + reviewThreads + worktree de fix + resolveReviewThread + loop-até-verde, com guarda-corpos), "evidence-before-assertions" obrigatório no corpo do PR, e (opcional) `agent-lsp` MCP.
|
||||
|
||||
---
|
||||
|
||||
## 6. Riscos & ressalvas (honestidade de engenharia)
|
||||
|
||||
- **Flag-day risk:** ligar qualquer gate num projeto que nunca o teve deixa tudo vermelho. **Toda** catraca aqui é *só-regressão* (baseline congelado), nunca um piso absoluto que exige limpeza imediata — exatamente o ponto do vídeo.
|
||||
- **Custo de IA:** o babysit pode queimar tokens. Guarda-corpos (max-iterations, sem auto-merge, sem editar `.github/workflows/`) são não-negociáveis.
|
||||
- **Ressalvas de pesquisa não-verificadas:** betterer baixa-velocidade; Sonar "AI Code Assurance" bloqueio-de-PR não confirmado; "GPT-5.5 −60% alucinação" é imprensa, não system card; schema JSON do jscpd v5 e S3776 do sonarjs v4 a confirmar no install; `qlty metrics` sem JSON. Nenhuma decisão do plano depende criticamente de um item não-verificado.
|
||||
- **Trust-but-verify:** estes números internos (CI=40, husky off, sonar exclusions, 12.760 LOC, any-budget como catraca) foram **conferidos manualmente** contra os arquivos, não só relatados pelos subagentes.
|
||||
|
||||
---
|
||||
|
||||
## 7. Fontes (consolidadas)
|
||||
|
||||
**Catraca / ratchet:** betterer `github.com/phenomnomnominal/betterer` (último master ago/2025); eslint-formatter-ratchet `github.com/Jmsa/eslint-formatter-ratchet` (commit 2026-03-17); SonarQube Clean as You Code `docs.sonarsource.com/.../clean-as-you-code/about-new-code/`; Sonar AI Code Assurance `sonarsource.com/solutions/ai/ai-code-assurance/` + community thread 2026.1.0; Qlty `github.com/qltysh/qlty` (v0.630.0, 2026-05-08), `docs.qlty.sh`; Code Climate→Qlty `codeclimate.com/legacy/...` (2024-11-11).
|
||||
|
||||
**Métricas:** jscpd `github.com/kucherenko/jscpd` (v5.0.4, 2026-06-08); ESLint v10 `eslint.org/blog/2026/02/eslint-v10.0.0-released/` (2026-02-06); eslint-plugin-sonarjs `npm` (v4.0.3, 2026-04-16) + `github.com/SonarSource/SonarJS`; knip `knip.dev` (v6.16.1, 2026-06-06); dpdm `github.com/acrazing/dpdm` (v4.2.0, 2026-05-09); osv-scanner `google.github.io/osv-scanner` (push 2026-06-08); lockfile-lint `github.com/lirantal/lockfile-lint` (v5.0.0, 2026-01-25); GitClear `gitclear.com/ai_assistant_code_quality_2025_research`.
|
||||
|
||||
**Anti-alucinação:** agent-lsp `github.com/blackwell-systems/agent-lsp` (v0.13.0, 2026-06-04); CSA Slopsquatting `labs.cloudsecurityalliance.org/research/...slopsquatting...20260419...` (2026-04-19); Nesbitt package defenses `nesbitt.io/2026/04/09/...` (2026-04-09); Semcheck `github.com/rejot-dev/semcheck` (v1.2.1, fev/2026); verify skill `github.com/Piebald-AI/claude-code-system-prompts/.../skill-verify-skill.md`; Claude best practices `code.claude.com/docs/en/best-practices`; SlopCodeBench `arxiv.org/pdf/2603.24755`; GPT-5.5 system card `deploymentsafety.openai.com/gpt-5-5` (2026-04-23); adversarial review `asdlc.io/patterns/adversarial-code-review/`; OpenAPI drift `speakeasy.com/blog/openapi-spec-drift-detection`.
|
||||
|
||||
**Babysit:** Claude auto-fix cloud `producthunt.com/products/claude-code-auto-fix-in-the-cloud` (2026-03-27); Devin Autofix `cognition.ai/blog/closing-the-agent-loop-...` (2026-02-10); CodeRabbit Autofix `coderabbit.ai/blog/fix-all-issues-with-ai-agents` (2026-02-19); Greptile skills `github.com/greptileai/skills`; Claude GitHub Actions/Code Review/ultrareview `code.claude.com/docs/en/{github-actions,code-review,ultrareview}`; Nx self-healing `nx.dev/blog/autonomous-ai-workflows-with-nx` (2026-02-03); Snyk Agent Fix 5,3% `safeguard.sh/resources/blog/snyk-agent-fix-autofix-field-test-2026`; resolveReviewThread `nakamasato.medium.com/...` + `github.com/cli/cli/issues/12419`; ICLR 2026 AI review `blog.pebblous.ai/report/iclr-2026-ai-peer-review-crisis`.
|
||||
|
||||
---
|
||||
|
||||
*Relatório gerado a partir de auditoria paralela do código + transcrição do vídeo + pesquisa web 2026. Próximo passo: aprovar o [`PLANO-QUALITY-GATES.md`](./PLANO-QUALITY-GATES.md) e escolher por onde começar (recomendação: Fase 0 → Fase 1).*
|
||||
4
complexity-baseline.json
Normal file
4
complexity-baseline.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.",
|
||||
"count": 1739
|
||||
}
|
||||
110
dependency-allowlist.json
Normal file
110
dependency-allowlist.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"_comment": "Allowlist anti-slopsquatting (check-deps.mjs). Toda dep nova exige adicao EXPLICITA aqui apos verificar que e legitima.",
|
||||
"allowed": [
|
||||
"@aws-sdk/client-bedrock-runtime",
|
||||
"@dnd-kit/core",
|
||||
"@dnd-kit/sortable",
|
||||
"@dnd-kit/utilities",
|
||||
"@huggingface/transformers",
|
||||
"@lobehub/icons",
|
||||
"@modelcontextprotocol/sdk",
|
||||
"@monaco-editor/react",
|
||||
"@ngrok/ngrok",
|
||||
"@playwright/test",
|
||||
"@swc/helpers",
|
||||
"@tailwindcss/postcss",
|
||||
"@testing-library/jest-dom",
|
||||
"@testing-library/react",
|
||||
"@types/bcryptjs",
|
||||
"@types/better-sqlite3",
|
||||
"@types/bun",
|
||||
"@types/keytar",
|
||||
"@types/mdx",
|
||||
"@types/node",
|
||||
"@types/react",
|
||||
"@types/react-dom",
|
||||
"@types/ws",
|
||||
"@vitejs/plugin-react",
|
||||
"@xyflow/react",
|
||||
"axios",
|
||||
"bcryptjs",
|
||||
"better-sqlite3",
|
||||
"bottleneck",
|
||||
"c8",
|
||||
"commander",
|
||||
"concurrently",
|
||||
"cross-env",
|
||||
"csv-stringify",
|
||||
"electron",
|
||||
"electron-builder",
|
||||
"electron-updater",
|
||||
"eslint",
|
||||
"eslint-config-next",
|
||||
"express",
|
||||
"fetch-socks",
|
||||
"fflate",
|
||||
"fumadocs-core",
|
||||
"fumadocs-mdx",
|
||||
"fumadocs-ui",
|
||||
"glob",
|
||||
"gray-matter",
|
||||
"http-proxy-middleware",
|
||||
"https-proxy-agent",
|
||||
"husky",
|
||||
"ink",
|
||||
"ink-spinner",
|
||||
"ink-text-input",
|
||||
"ioredis",
|
||||
"jose",
|
||||
"js-yaml",
|
||||
"jsdom",
|
||||
"jsonc-parser",
|
||||
"keytar",
|
||||
"lint-staged",
|
||||
"lowdb",
|
||||
"lucide-react",
|
||||
"marked",
|
||||
"marked-terminal",
|
||||
"mermaid",
|
||||
"monaco-editor",
|
||||
"next",
|
||||
"next-intl",
|
||||
"next-themes",
|
||||
"node-loader",
|
||||
"node-machine-id",
|
||||
"open",
|
||||
"ora",
|
||||
"parse5",
|
||||
"pino",
|
||||
"pino-abstract-transport",
|
||||
"pino-pretty",
|
||||
"playwright",
|
||||
"prettier",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react-is",
|
||||
"react-markdown",
|
||||
"react-reconciler",
|
||||
"recharts",
|
||||
"selfsigned",
|
||||
"sql.js",
|
||||
"sqlite-vec",
|
||||
"tailwindcss",
|
||||
"tls-client-node",
|
||||
"tsx",
|
||||
"typescript",
|
||||
"typescript-eslint",
|
||||
"undici",
|
||||
"update-notifier",
|
||||
"uuid",
|
||||
"vitest",
|
||||
"wait-on",
|
||||
"wreq-js",
|
||||
"ws",
|
||||
"wtfnode",
|
||||
"xxhash-wasm",
|
||||
"yazl",
|
||||
"zod",
|
||||
"zustand"
|
||||
]
|
||||
}
|
||||
4
duplication-baseline.json
Normal file
4
duplication-baseline.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"_comment": "Catraca de duplicacao (check-duplication.mjs, jscpd@4 sobre src+open-sse, min-tokens 50). So pode cair. --update ratcheta.",
|
||||
"percentage": 5.72
|
||||
}
|
||||
64
eslint.complexity.config.mjs
Normal file
64
eslint.complexity.config.mjs
Normal file
@@ -0,0 +1,64 @@
|
||||
// eslint.complexity.config.mjs
|
||||
// STANDALONE flat config for the complexity ratchet (scripts/check/check-complexity.mjs).
|
||||
// Intentionally does NOT extend the project's main eslint.config.mjs — it enables ONLY two
|
||||
// ESLint CORE rules so its violation count is isolated from the main lint's ratcheted
|
||||
// warning budget (3482). No plugins, no extra dependency: just the TypeScript parser
|
||||
// (typescript-eslint) so .ts/.tsx files can be parsed.
|
||||
//
|
||||
// complexity — cyclomatic complexity ceiling per function
|
||||
// max-lines-per-function — function-length ceiling (skips blank lines + comments)
|
||||
//
|
||||
// Run via:
|
||||
// npx eslint --no-config-lookup --config eslint.complexity.config.mjs --format json src open-sse
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
/** @type {import("eslint").Linter.Config[]} */
|
||||
const complexityConfig = [
|
||||
{
|
||||
files: ["src/**/*.{ts,tsx}", "open-sse/**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
},
|
||||
// Ignore ALL inline directive comments. Source files carry
|
||||
// `// eslint-disable-next-line react-hooks/...` / `@next/next/...` directives that
|
||||
// reference rules from plugins this standalone config deliberately does NOT load.
|
||||
// Without this, ESLint emits an error-severity "Definition for rule ... was not
|
||||
// found" for each such directive — polluting (and destabilizing) the violation
|
||||
// count. noInlineConfig keeps the count to exactly our two rules.
|
||||
linterOptions: {
|
||||
noInlineConfig: true,
|
||||
reportUnusedDisableDirectives: "off",
|
||||
},
|
||||
// ONLY these two core rules. Keep this list minimal so the reported violation
|
||||
// count is exactly "functions over the complexity / length thresholds".
|
||||
rules: {
|
||||
complexity: ["error", 15],
|
||||
"max-lines-per-function": [
|
||||
"error",
|
||||
{ max: 80, skipBlankLines: true, skipComments: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
// Ignore everything that is not first-party src/open-sse production code so the count
|
||||
// is not polluted by tests, type declarations, or build output.
|
||||
{
|
||||
ignores: [
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/__tests__/**",
|
||||
"**/*.d.ts",
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
".build/**",
|
||||
"dist/**",
|
||||
"coverage/**",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default complexityConfig;
|
||||
97
file-size-baseline.json
Normal file
97
file-size-baseline.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
|
||||
"cap": 800,
|
||||
"frozen": {
|
||||
"open-sse/config/providerRegistry.ts": 4677,
|
||||
"open-sse/executors/antigravity.ts": 1533,
|
||||
"open-sse/executors/base.ts": 1175,
|
||||
"open-sse/executors/chatgpt-web.ts": 2870,
|
||||
"open-sse/executors/claude-web.ts": 1057,
|
||||
"open-sse/executors/codex.ts": 1439,
|
||||
"open-sse/executors/cursor.ts": 1391,
|
||||
"open-sse/executors/deepseek-web.ts": 1116,
|
||||
"open-sse/executors/duckduckgo-web.ts": 917,
|
||||
"open-sse/executors/grok-web.ts": 1871,
|
||||
"open-sse/executors/muse-spark-web.ts": 1284,
|
||||
"open-sse/executors/perplexity-web.ts": 867,
|
||||
"open-sse/handlers/audioSpeech.ts": 952,
|
||||
"open-sse/handlers/chatCore.ts": 5978,
|
||||
"open-sse/handlers/imageGeneration.ts": 3777,
|
||||
"open-sse/handlers/responseSanitizer.ts": 1080,
|
||||
"open-sse/handlers/search.ts": 1441,
|
||||
"open-sse/handlers/videoGeneration.ts": 1026,
|
||||
"open-sse/mcp-server/schemas/tools.ts": 1437,
|
||||
"open-sse/mcp-server/server.ts": 1457,
|
||||
"open-sse/mcp-server/tools/advancedTools.ts": 1118,
|
||||
"open-sse/services/accountFallback.ts": 1631,
|
||||
"open-sse/services/batchProcessor.ts": 828,
|
||||
"open-sse/services/browserBackedChat.ts": 850,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
"open-sse/services/combo.ts": 4457,
|
||||
"open-sse/services/rateLimitManager.ts": 1017,
|
||||
"open-sse/services/tokenRefresh.ts": 1896,
|
||||
"open-sse/services/usage.ts": 3042,
|
||||
"open-sse/translator/request/openai-to-gemini.ts": 822,
|
||||
"open-sse/translator/response/openai-responses.ts": 873,
|
||||
"open-sse/utils/cursorAgentProtobuf.ts": 1499,
|
||||
"open-sse/utils/stream.ts": 2593,
|
||||
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1417,
|
||||
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1020,
|
||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 2680,
|
||||
"src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": 1105,
|
||||
"src/app/(dashboard)/dashboard/cache/page.tsx": 841,
|
||||
"src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": 894,
|
||||
"src/app/(dashboard)/dashboard/cloud-agents/page.tsx": 913,
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": 4350,
|
||||
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1481,
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1007,
|
||||
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2570,
|
||||
"src/app/(dashboard)/dashboard/health/page.tsx": 1091,
|
||||
"src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx": 847,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/page.tsx": 12883,
|
||||
"src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 906,
|
||||
"src/app/(dashboard)/dashboard/providers/page.tsx": 1925,
|
||||
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1127,
|
||||
"src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx": 819,
|
||||
"src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx": 932,
|
||||
"src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 880,
|
||||
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1072,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 851,
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1580,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924,
|
||||
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1016,
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1015,
|
||||
"src/app/api/oauth/[provider]/[action]/route.ts": 897,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2287,
|
||||
"src/app/api/providers/[id]/test/route.ts": 842,
|
||||
"src/app/api/usage/analytics/route.ts": 1355,
|
||||
"src/app/api/v1/models/catalog.ts": 1403,
|
||||
"src/lib/cloudflaredTunnel.ts": 934,
|
||||
"src/lib/db/apiKeys.ts": 1490,
|
||||
"src/lib/db/core.ts": 1813,
|
||||
"src/lib/db/migrationRunner.ts": 1100,
|
||||
"src/lib/db/models.ts": 1132,
|
||||
"src/lib/db/providers.ts": 993,
|
||||
"src/lib/db/proxies.ts": 1031,
|
||||
"src/lib/db/settings.ts": 1101,
|
||||
"src/lib/evals/evalRunner.ts": 961,
|
||||
"src/lib/memory/retrieval.ts": 1171,
|
||||
"src/lib/modelsDevSync.ts": 934,
|
||||
"src/lib/providers/validation.ts": 4201,
|
||||
"src/lib/tailscaleTunnel.ts": 1189,
|
||||
"src/lib/usage/callLogs.ts": 971,
|
||||
"src/shared/components/OAuthModal.tsx": 956,
|
||||
"src/shared/components/RequestLoggerV2.tsx": 951,
|
||||
"src/shared/components/analytics/charts.tsx": 1558,
|
||||
"src/shared/constants/cliTools.ts": 875,
|
||||
"src/shared/constants/pricing.ts": 1447,
|
||||
"src/shared/constants/providers.ts": 3121,
|
||||
"src/shared/constants/sidebarVisibility.ts": 990,
|
||||
"src/shared/services/cliRuntime.ts": 1073,
|
||||
"src/shared/validation/schemas.ts": 2490,
|
||||
"src/sse/handlers/chat.ts": 1381,
|
||||
"src/sse/services/auth.ts": 2198
|
||||
}
|
||||
}
|
||||
23
package.json
23
package.json
@@ -110,7 +110,28 @@
|
||||
"i18n:check-ui-coverage": "node scripts/i18n/check-ui-keys-coverage.mjs",
|
||||
"check:node-runtime": "node --import tsx scripts/check/check-supported-node-runtime.ts",
|
||||
"check:pack-artifact": "node --import tsx scripts/build/validate-pack-artifact.ts",
|
||||
"audit:deps": "npm audit --audit-level=moderate && npm run audit:electron",
|
||||
"check:cli-i18n": "node scripts/check/check-cli-i18n.mjs",
|
||||
"check:openapi-coverage": "node scripts/check/check-openapi-coverage.mjs",
|
||||
"check:openapi-security-tiers": "node scripts/check/check-openapi-security-tiers.mjs",
|
||||
"check:provider-consistency": "node --import tsx scripts/check/check-provider-consistency.ts",
|
||||
"check:fetch-targets": "node scripts/check/check-fetch-targets.mjs",
|
||||
"check:openapi-routes": "node scripts/check/check-openapi-routes.mjs",
|
||||
"check:deps": "node scripts/check/check-deps.mjs",
|
||||
"check:file-size": "node scripts/check/check-file-size.mjs",
|
||||
"check:duplication": "node scripts/check/check-duplication.mjs",
|
||||
"check:test-masking": "node scripts/check/check-test-masking.mjs",
|
||||
"check:error-helper": "node scripts/check/check-error-helper.mjs",
|
||||
"check:migration-numbering": "node scripts/check/check-migration-numbering.mjs",
|
||||
"check:public-creds": "node scripts/check/check-public-creds.mjs",
|
||||
"check:db-rules": "node scripts/check/check-db-rules.mjs",
|
||||
"check:docs-symbols": "node scripts/check/check-docs-symbols.mjs",
|
||||
"check:known-symbols": "node --import tsx scripts/check/check-known-symbols.ts",
|
||||
"check:route-guard-membership": "node --import tsx scripts/check/check-route-guard-membership.ts",
|
||||
"check:complexity": "node scripts/check/check-complexity.mjs",
|
||||
"quality:collect": "node scripts/quality/collect-metrics.mjs",
|
||||
"quality:ratchet": "node scripts/quality/check-quality-ratchet.mjs",
|
||||
"quality:gate": "npm run quality:collect && npm run quality:ratchet -- --allow-missing",
|
||||
"audit:deps": "npm audit --audit-level=critical && (npm audit --audit-level=high || echo '::warning::high-severity advisories present (non-blocking)') && npm run audit:electron",
|
||||
"audit:electron": "npm --prefix electron audit --audit-level=moderate",
|
||||
"typecheck:core": "tsc --pretty false -p tsconfig.typecheck-core.json",
|
||||
"typecheck:noimplicit:core": "tsc --pretty false -p tsconfig.typecheck-noimplicit-core.json",
|
||||
|
||||
11
quality-baseline.json
Normal file
11
quality-baseline.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"_comment": "Catraca de qualidade. 'down' = nao pode aumentar; 'up' = nao pode cair. Atualize via 'npm run quality:ratchet -- --update' (somente quando melhora). Cada valor e um numero REAL medido, nunca um chute. Cobertura entra na Fase 4 a partir de um run de cobertura mergeada no CI.",
|
||||
"metrics": {
|
||||
"eslintWarnings": { "value": 3482, "direction": "down" },
|
||||
"coverage.statements": { "value": 80, "direction": "up" },
|
||||
"coverage.lines": { "value": 80, "direction": "up" },
|
||||
"coverage.functions": { "value": 82, "direction": "up" },
|
||||
"coverage.branches": { "value": 73, "direction": "up" }
|
||||
},
|
||||
"_coverage_note": "Pisos conservadores (real ~82,58/82,58/84,23/75,22 em COVERAGE_PLAN.md 2026-05-13, ~2pt de margem p/ evitar false-fail antes de calibrar no CI). Apos o 1o run verde de coverage mergeada na main, aperte com 'npm run quality:ratchet -- --update'."
|
||||
}
|
||||
87
scripts/check/check-complexity.mjs
Normal file
87
scripts/check/check-complexity.mjs
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/check/check-complexity.mjs
|
||||
// Catraca de complexidade de código. Roda o ESLint sobre src+open-sse usando um config
|
||||
// flat STANDALONE (eslint.complexity.config.mjs) que liga APENAS duas regras CORE do
|
||||
// ESLint — `complexity` (ciclomática) e `max-lines-per-function` (tamanho de função) —
|
||||
// e compara a contagem total de violações contra um baseline congelado
|
||||
// (complexity-baseline.json). Falha se a contagem SUBIR. Completa a dimensão
|
||||
// "complexity" do snapshot de qualidade, ao lado de duplicação/tamanho-de-arquivo.
|
||||
//
|
||||
// O config dedicado evita poluir a contagem de warnings do lint principal (ratcheada
|
||||
// em exatamente 3482): este gate roda isolado, com seu próprio par de regras. --update
|
||||
// ratcheta (a contagem só pode CAIR).
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const BASELINE_PATH = path.resolve(
|
||||
process.argv.includes("--baseline")
|
||||
? process.argv[process.argv.indexOf("--baseline") + 1]
|
||||
: path.join(ROOT, "complexity-baseline.json")
|
||||
);
|
||||
const UPDATE = process.argv.includes("--update");
|
||||
const CONFIG_PATH = path.join(ROOT, "eslint.complexity.config.mjs");
|
||||
const ESLINT_ARGS = [
|
||||
"eslint",
|
||||
"--no-config-lookup",
|
||||
"--config",
|
||||
CONFIG_PATH,
|
||||
"--format",
|
||||
"json",
|
||||
"src",
|
||||
"open-sse",
|
||||
];
|
||||
|
||||
/** Avalia a contagem atual de violações contra o baseline. */
|
||||
export function evaluateComplexity(current, baseline) {
|
||||
return {
|
||||
regressed: current > baseline,
|
||||
improved: current < baseline,
|
||||
};
|
||||
}
|
||||
|
||||
function measureComplexityCount() {
|
||||
let stdout;
|
||||
try {
|
||||
stdout = execFileSync("npx", ["--yes", ...ESLINT_ARGS], {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
} catch (err) {
|
||||
// ESLint sai com código !=0 quando há erros (e nossas regras são "error"); o relatório
|
||||
// JSON ainda vai no stdout. Só relançamos se não houver stdout parseável.
|
||||
stdout = err.stdout ? String(err.stdout) : "";
|
||||
if (!stdout.trim()) throw err;
|
||||
}
|
||||
const report = JSON.parse(stdout);
|
||||
return report.reduce((sum, file) => sum + file.errorCount, 0);
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(BASELINE_PATH)) {
|
||||
console.error(`[complexity] FAIL — ${path.basename(BASELINE_PATH)} ausente.`);
|
||||
process.exit(2);
|
||||
}
|
||||
const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8"));
|
||||
const current = measureComplexityCount();
|
||||
const { regressed, improved } = evaluateComplexity(current, baseline.count);
|
||||
|
||||
if (UPDATE && improved) {
|
||||
console.log(`[complexity] baseline ratcheado: ${current} (era ${baseline.count})`);
|
||||
baseline.count = current;
|
||||
fs.writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2) + "\n");
|
||||
}
|
||||
if (regressed) {
|
||||
console.error(
|
||||
`[complexity] REGRESSÃO — ${current} violações > baseline ${baseline.count}\n` +
|
||||
` → quebre a função em helpers menores (reduza ramos/tamanho) ou rode\n` +
|
||||
` 'node scripts/check/check-complexity.mjs --update' se a contagem caiu legitimamente.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`[complexity] OK — ${current} violações (baseline ${baseline.count})`);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();
|
||||
252
scripts/check/check-db-rules.mjs
Normal file
252
scripts/check/check-db-rules.mjs
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/check/check-db-rules.mjs
|
||||
// Gate de convenções de banco (CLAUDE.md Hard Rules #2 e #5). Três verificações:
|
||||
// (a) Todo módulo de domínio em src/lib/db/*.ts deve ser re-exportado por
|
||||
// src/lib/localDb.ts (camada de compat). Um módulo db NOVO que não é
|
||||
// re-exportado (e não está congelado) falha — força a decisão consciente
|
||||
// de expor ou justificar (Hard Rule #2).
|
||||
// (b) src/lib/localDb.ts é APENAS camada de re-export: nada de lógica
|
||||
// (function/class/arrow de negócio). Mata o anti-padrão de "só uma
|
||||
// funçãozinha aqui" que vira regra de negócio fora dos módulos db/.
|
||||
// (c) Nenhum SQL cru em src/app/api/**/route.ts ou open-sse/handlers/*.ts.
|
||||
// SQL deve viver em src/lib/db/ (Hard Rule #5). Ofensores pré-existentes
|
||||
// são congelados; QUALQUER novo SQL cru em rota/handler falha.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const cwd = process.cwd();
|
||||
const DB_DIR = path.join(cwd, "src/lib/db");
|
||||
const LOCAL_DB = path.join(cwd, "src/lib/localDb.ts");
|
||||
const API_DIR = path.join(cwd, "src/app/api");
|
||||
const HANDLERS_DIR = path.join(cwd, "open-sse/handlers");
|
||||
|
||||
// (a) Módulos db/ que NÃO são re-exportados por localDb.ts hoje. Congelados
|
||||
// para a catraca ficar verde e bloquear QUALQUER módulo novo não re-exportado.
|
||||
// CADA UM é dívida: ou é consumido por import direto de "@/lib/db/X" (legítimo,
|
||||
// não precisa de re-export) ou deveria ser re-exportado. NÃO adicione novos aqui
|
||||
// sem justificativa — esse é o ponto do gate (Hard Rule #2).
|
||||
const KNOWN_UNEXPORTED = new Set([
|
||||
"_rowTypes", // só tipos de linha (sem runtime API), consumido localmente pelos CRUDs F2
|
||||
"cleanup", // rotina de manutenção, chamada por jobs/rotas via import direto
|
||||
"cliToolState", // estado de CLI tools, import direto pelos consumidores
|
||||
"comboForecast", // previsão de combo, import direto
|
||||
"commandCodeAuth", // auth de command-code, import direto
|
||||
"compression", // núcleo de compressão, import direto
|
||||
"compressionScheduler", // scheduler, import direto
|
||||
"detailedLogs", // logs detalhados, import direto
|
||||
"discovery", // discovery de modelos, import direto
|
||||
"domainState", // estado de domínio/circuit breaker, import direto
|
||||
"encryption", // util de cripto at-rest, import direto
|
||||
"healthCheck", // health check de DB, import direto
|
||||
"jsonMigration", // migração JSON→SQLite (one-shot), import direto
|
||||
"migrationRunner", // runner de migrations, import direto
|
||||
"notion", // integração Notion, import direto
|
||||
"obsidian", // integração Obsidian, import direto
|
||||
"pluginMetrics", // métricas de plugin, import direto
|
||||
"prompts", // prompts salvos, import direto
|
||||
"providerStats", // stats de provider, import direto
|
||||
"recovery", // recuperação de DB, import direto
|
||||
"secrets", // secrets store, import direto
|
||||
"serviceModels", // modelos de serviços embutidos, import direto
|
||||
"stateReset", // reset de estado de resiliência, import direto
|
||||
"stats", // agregações de stats, import direto
|
||||
"tierConfig", // config de tier, import direto
|
||||
]);
|
||||
|
||||
// (c) Ofensores de SQL cru PRÉ-EXISTENTES em rotas/handlers. Congelados para a
|
||||
// catraca ficar verde e bloquear QUALQUER nova rota/handler com SQL inline.
|
||||
// CADA UM é dívida da Hard Rule #5: mover para um módulo src/lib/db/. NÃO
|
||||
// adicione novos aqui sem justificativa — crie/estenda um módulo db/ em vez disso.
|
||||
// (Chaves = caminho relativo POSIX a partir da raiz do repo.)
|
||||
const KNOWN_RAW_SQL = new Set([
|
||||
"src/app/api/analytics/auto-routing/route.ts", // SELECT … FROM usage_logs
|
||||
"src/app/api/cache/entries/route.ts", // semantic_cache COUNT/DELETE inline
|
||||
"src/app/api/db-backups/exportAll/route.ts", // SELECT key_value/combos/connections/keys
|
||||
"src/app/api/db-backups/import/route.ts", // SELECT sqlite_master + COUNTs
|
||||
"src/app/api/gamification/federation/leaderboard/route.ts", // SELECT community_servers
|
||||
"src/app/api/gamification/federation/score/route.ts", // SELECT community_servers
|
||||
"src/app/api/logs/export/route.ts", // SELECT de proxy_logs
|
||||
"src/app/api/oauth/cursor/auto-import/route.ts", // SELECT no itemTable do Cursor (DB externo)
|
||||
"src/app/api/oauth/kiro/auto-import/route.ts", // SELECT no SQLite do Kiro (DB externo)
|
||||
"src/app/api/provider-metrics/route.ts", // SELECT … FROM call_logs (agregação)
|
||||
"src/app/api/search/stats/route.ts", // SELECT … FROM call_logs
|
||||
"src/app/api/settings/export-json/route.ts", // SELECT * de usage_history/domain_*
|
||||
"src/app/api/skills/[id]/route.ts", // UPDATE skills SET dinâmico
|
||||
"src/app/api/usage/analytics/route.ts", // SELECT … FROM usage_history/daily_usage_summary
|
||||
"src/app/api/v1/search/analytics/route.ts", // SELECT … FROM call_logs (request_type=search)
|
||||
]);
|
||||
|
||||
// Módulos sempre excluídos da checagem (a): não são domínio re-exportável.
|
||||
const DB_MODULE_EXCLUDE = new Set(["core", "localDb", "index"]);
|
||||
|
||||
function walk(dir, acc = []) {
|
||||
if (!fs.existsSync(dir)) return acc;
|
||||
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) walk(p, acc);
|
||||
else acc.push(p);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Lista os módulos de domínio em src/lib/db (top-level *.ts), excluindo
|
||||
// core/localDb/index, *.d.ts e qualquer subdiretório (migrations/, adapters/, __tests__/).
|
||||
export function collectDbModules(dbDir = DB_DIR) {
|
||||
if (!fs.existsSync(dbDir)) return [];
|
||||
return fs
|
||||
.readdirSync(dbDir, { withFileTypes: true })
|
||||
.filter((e) => e.isFile() && /\.ts$/.test(e.name) && !/\.d\.ts$/.test(e.name))
|
||||
.map((e) => e.name.replace(/\.ts$/, ""))
|
||||
.filter((name) => !DB_MODULE_EXCLUDE.has(name))
|
||||
.sort();
|
||||
}
|
||||
|
||||
// Extrai os nomes de módulo re-exportados de localDb.ts a partir de
|
||||
// `... from "./db/X"` (cobre export {…}, export * e export type {…}).
|
||||
export function extractReexportedModules(localDbSource) {
|
||||
const re = /from\s+["']\.\/db\/([A-Za-z0-9_]+)["']/g;
|
||||
const out = new Set();
|
||||
let m;
|
||||
while ((m = re.exec(localDbSource))) out.add(m[1]);
|
||||
return out;
|
||||
}
|
||||
|
||||
// (a) Módulos db/ que não são re-exportados e não estão congelados.
|
||||
export function findMissingReexports(dbModules, reexported, allowlist = KNOWN_UNEXPORTED) {
|
||||
return dbModules.filter((mod) => !reexported.has(mod) && !allowlist.has(mod));
|
||||
}
|
||||
|
||||
// (b) localDb.ts deve conter SOMENTE import/export + comentários (sem lógica).
|
||||
// Remove comentários e strings, depois procura declarações de runtime.
|
||||
export function hasLogic(localDbSource) {
|
||||
const stripped = localDbSource
|
||||
// comentários de bloco
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
// comentários de linha
|
||||
.replace(/\/\/[^\n]*/g, "")
|
||||
// template strings
|
||||
.replace(/`(?:\\[\s\S]|[^\\`])*`/g, '""')
|
||||
// strings simples/duplas (paths de import etc.)
|
||||
.replace(/"(?:\\.|[^"\\])*"/g, '""')
|
||||
.replace(/'(?:\\.|[^'\\])*'/g, '""');
|
||||
|
||||
// function/class declaradas, ou atribuição a função (const X = (…) =>, const X = function).
|
||||
const logicPatterns = [
|
||||
/(^|[^.\w])function\s+[A-Za-z_$]/, // function decl (não method .foo())
|
||||
/(^|[^.\w])class\s+[A-Za-z_$]/, // class decl
|
||||
/(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(?:async\s*)?\(/, // const X = (…) ... (arrow/call)
|
||||
/(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(?:async\s+)?function\b/, // const X = function
|
||||
];
|
||||
return logicPatterns.some((rx) => rx.test(stripped));
|
||||
}
|
||||
|
||||
// SQL cru é sempre uma STRING passada a db.prepare()/exec(): casamos os padrões
|
||||
// SÓ dentro de literais de string (não em código JS — `import … from`, `.set(`,
|
||||
// `new Set(`, `delete x` etc. são falsos positivos se varrermos o código todo).
|
||||
const SQL_PATTERNS = [
|
||||
/\bSELECT\b[\s\S]*?\bFROM\b/i, // SELECT … FROM (multi-linha)
|
||||
/\bINSERT\s+INTO\b/i,
|
||||
/\bUPDATE\b[\s\S]*?\bSET\b/i, // UPDATE … SET (multi-linha)
|
||||
/\bDELETE\s+FROM\b/i,
|
||||
/\bCREATE\s+TABLE\b/i,
|
||||
];
|
||||
|
||||
// Remove comentários (linha // … e blocos /* */) — SQL em comentário não conta.
|
||||
function stripComments(source) {
|
||||
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
||||
}
|
||||
|
||||
// Extrai o conteúdo de todos os literais de string (template, aspas duplas, aspas
|
||||
// simples) de um trecho de código já sem comentários. Retorna a concatenação dos
|
||||
// corpos — é nesse corpo que SQL cru vive.
|
||||
export function extractStringLiterals(code) {
|
||||
const re = /`(?:\\[\s\S]|[^\\`])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g;
|
||||
const out = [];
|
||||
let m;
|
||||
while ((m = re.exec(code))) {
|
||||
// tira as aspas/crases delimitadoras
|
||||
out.push(m[0].slice(1, -1));
|
||||
}
|
||||
return out.join("\n | ||||