@sensio-erp/diffai-guard
v0.3.0
Published
SonarQube-style quality gate for TypeScript/JavaScript: source-code rules + TypeScript semantic checks + diffai signals, with ratings, a configurable quality gate, SARIF output and a CI exit code.
Maintainers
Readme
@sensio-erp/diffai-guard
Um SonarQube-like para o ecossistema diffai: analisa código-fonte (regras via AST) + os sinais destilados do diffai (
.diffai/*.json), calcula métricas e ratings, aplica um Quality Gate com código de saída para CI e gera dashboard HTML e SARIF (também abre no Notepad o resumo, se pedido).
Instalação
npm i -g @sensio-erp/diffai-guard # global → comando `diffai-guard`
# ou sem instalar:
npx @sensio-erp/diffai-guard --repo . --allO que ele faz
Motor híbrido, em três fontes:
- Regras sobre o código-fonte (ts-morph), cada achado com linha,
rule key, tipo e severidade:- Segurança/correção:
no-eval, credenciais hard-coded,http://inseguro,catchvazio,==/!=. - Segurança (SAST):
command-injection(exec/execSync com comando dinâmico),sql-injection(query SQL concatenada/interpolada),unsafe-regex(ReDoS — grupo quantificado aninhado),prototype-pollution(atribuição a__proto__/constructor.prototype),insecure-randomness(Math.random()para token/senha/session),weak-crypto(md5/sha1,createCipher),insecure-cookie(cookie semhttpOnly/secure),unvalidated-redirect(redirect direto dereq.query/params/body). - Async/Promises:
no-floating-promises(promise não tratada),no-async-foreach,require-await,await-in-loop. - Estilo/manutenção:
prefer-const(umletnunca reatribuído),no-var,console/debugger, complexidade cognitiva, aninhamento profundo, excesso de parâmetros,anyexplícito, código comentado,no-magic-numbers(INFO, tunável),TODO/FIXME.
- Segurança/correção:
- Checker semântico (diagnósticos do compilador TypeScript): pega código que vai falhar em execução ou está morto —
reatribuir uma
const(semantic:const-reassignment, TS2588 →TypeError), variável não declarada (semantic:no-undef, TS2304/2552 →ReferenceError), uso antes da declaração (semantic:use-before-declaration, TS2448/2454), chamar algo que não é função (semantic:not-callable, TS2349 →TypeError), número errado de argumentos (semantic:wrong-arg-count, TS2554/2555) e declarações nunca usadas (semantic:unused-code, TS6133/6196/6198). Os primeiros são BUG (BLOCKER/MAJOR);unused-codeé CODE_SMELL. Globais Node ausentes (TS2591) e módulos não resolvidos (TS2307) são ignorados de propósito — zero falso-positivo comconsole/processou imports entre arquivos. - Sinais do diffai: fatos semânticos que um parser não vê — regra de negócio alterada sem testes, mudança de API/payload/permissões, regressões e riscos que a IA do diffai apontou.
E entrega os quatro pilares estilo Sonar:
| Pilar | Detalhe |
| --- | --- |
| Taxonomia + severidades | BUG / VULNERABILITY / CODE_SMELL / SECURITY_HOTSPOT · BLOCKER→INFO |
| Quality Gate | Condições configuráveis (Sonar way por padrão) → PASS/FAIL e exit code para CI |
| Métricas & ratings | Reliability / Security / Maintainability / Security-Review (A–E), dívida técnica, duplicação (CPD por tokens), cobertura de linha/branch/new-code, LOC |
| Dashboard HTML | Relatório navegável (sonar-report.html) + resumo .txt (Notepad) + .json (máquina) |
| SARIF + CI | sonar-report.sarif (2.1.0) p/ GitHub Code Scanning, new code por linha (--base) e supressão inline |
| Leak period | Achados novos desde a execução anterior (.diffai/baseline.json), com contadores dedicados no Quality Gate |
Fluxo
.diffai/*.json (diffai) código-fonte do repo
│ │
diffai signals regras AST + checker semântico (ts-morph) ── escopo "new code" (git diff; .diffai é fallback)
└───────────────┬───────────────┘
issues (taxonomia Sonar)
│
métricas + ratings + duplicação + cobertura
│
Quality Gate (PASS/FAIL)
│
sonar-report.{json,html,txt,sarif} ──► browser / Notepad / GitHub Code ScanningUso
Depois de instalado, o comando é diffai-guard:
# analisa só o "new code" — arquivos alterados no git (fallback: .diffai) — e abre o dashboard
diffai-guard # sem --repo: usa o diretório atual
# analisa TODO o código-fonte do repo
diffai-guard --repo C:\caminho\do\repo --all
# uso em CI: sem abrir nada; sai com codigo != 0 se o gate falhar
diffai-guard --repo C:\caminho\do\repo --all --open none
# abrir o resumo no Notepad em vez do navegador
diffai-guard --repo C:\caminho\do\repo --open notepadEm desenvolvimento (sem instalar), rode a partir do fonte:
npm install
npx tsx src/cli/bin.ts --repo C:\caminho\do\repo --allNo PowerShell/npm, prefira
npx tsx src/cli/bin.ts ...anpm start -- ...(o npm consome as flags--repo).
Flags
| Flag | Efeito |
| --- | --- |
| --repo <path> | repositório a analisar (padrão: diretório atual) |
| --all | analisa todo o código-fonte, não só os arquivos alterados |
| --base <ref> | escopo "new code" por linha: só reporta issues nas linhas alteradas em <ref>...HEAD (estilo PR). Sem --base, usa as mudanças pendentes (git diff HEAD) |
| --open <browser\|notepad\|none> | o que abrir ao final (padrão browser) |
| --no-fail | sempre sai com código 0, mesmo com gate FAIL |
| --config <path> | analyzer.config.json (padrão: <repo>/analyzer.config.json) |
| --init | escreve analyzer.config.example.jsonc (todas as regras, comentadas) em --repo e sai — não sobrescreve se já existir |
| --run-diffai / --diffai-path <path> | roda o diffai antes de analisar |
| --ignore <pastas> | pastas (nome exato) a pular ao procurar repositórios git em subpastas, separadas por vírgula — ex.: --ignore pasta1,pasta2 |
| --freeze-baseline | compara com .diffai/baseline.json sem sobrescrevê-lo (use em checks de PR; deixe as execuções na main atualizarem o baseline) |
Múltiplos repositórios (auto-descoberta de .git)
Se --repo (ou o diretório atual) não tiver um .git diretamente, o diffai-guard percorre as subpastas em busca de repositórios git — útil em uma pasta "mono" que contém vários projetos clonados lado a lado. Ele não desce dentro de uma pasta assim que encontra um .git nela (evita repos aninhados/submódulos duplicados) e já ignora por padrão node_modules, dist, build, out, coverage, .diffai — use --ignore para pular outras pastas específicas.
# c:\projetos\ contém repoA\, repoB\, terceiros\ (sem .git direto em c:\projetos\)
diffai-guard --repo c:\projetos --all --ignore terceirosCada repositório encontrado é analisado normalmente e grava seu próprio <repo>/.diffai/sonar-report.*. Quando mais de um repositório é encontrado, os relatórios individuais não são abertos automaticamente — em vez disso é gerado e aberto um relatório combinado em <pasta-raiz>/.diffai/sonar-report.html, com uma seção recolhível (<details>) por repositório, cada uma com seu próprio Quality Gate, ratings, métricas e lista de issues.
Códigos de saída
0 gate PASS · 1 gate FAIL (salvo --no-fail) · 2 erro na análise.
Configuração (analyzer.config.json)
Opcional, na raiz do repo (ou em qualquer caminho, via --config <path>). Precedência: defaults < analyzer.config.example.jsonc na raiz < analyzer.config.json na raiz < --config <path>. Aceita JSONC — comentários // e /* */ são removidos automaticamente antes do parse.
Se não existir analyzer.config.json mas houver um analyzer.config.example.jsonc na raiz do repo (por exemplo, o que o --init gerou), ele é usado automaticamente — sem precisar renomear nem passar --config.
{
"qualityGate": {
"conditions": [
{ "metric": "reliability_rating", "op": "LTE", "threshold": "A" },
{ "metric": "coverage", "op": "GTE", "threshold": 80, "onlyIfMeasured": true }
]
},
"rules": {
"no-console": { "enabled": false },
"explicit-any": { "severity": "INFO" },
"diffai:permissions-change": { "severity": "MAJOR" }
},
"exclude": ["generated", "vendor"]
}rules habilita/desabilita (enabled: false) ou re-severidade (severity) qualquer regra do catálogo — regras de código-fonte (source:*), do checker semântico (semantic:*) e os sinais do diffai (diffai:*). A chave aceita o rule key completo (diffai:permissions-change) ou só o sufixo (permissions-change).
Arquivos de teste (*.test.ts, *.spec.ts, *.test.tsx, *.spec.tsx e os equivalentes .js/.jsx) nunca são analisados — a ferramenta os ignora sempre, sem precisar de exclude. exclude é para o resto: pastas geradas, vendor, ou uma pasta de testes que não segue esse padrão de nome (ex.: __tests__). É substring "crua" (não glob), então evite trechos curtos/comuns que possam bater em outro lugar sem querer.
analyzer.config.example.jsonc traz todas as regras disponíveis, comentadas em português, já com a severidade padrão de cada uma. Rode:
diffai-guard --init # escreve ./analyzer.config.example.jsonc
diffai-guard --init --repo <path> # escreve <path>/analyzer.config.example.jsoncEle já é lido automaticamente se ficar na raiz do repo (não precisa de --config). Para customizar, copie/renomeie para analyzer.config.json e ajuste enabled/severity.
Saídas (<repo>/.diffai/)
sonar-report.json (máquina) · sonar-report.sarif (SARIF 2.1.0 → GitHub Code Scanning / viewers do VS Code) · sonar-report.html (dashboard) · sonar-report.txt (Notepad) · baseline.json (fingerprints da execução, usado pelo leak period — ver abaixo).
Quando múltiplos repositórios são descobertos (ver Múltiplos repositórios acima), o mesmo trio sonar-report.{json,html,txt} é gerado também na pasta raiz informada em --repo, agregando todos os repositórios (sem .sarif, já que cada repo já tem o seu).
Supressão inline
Silencie um falso-positivo pontual com um comentário — casa por chave completa (source:no-eval) ou curta (no-eval); sem regra listada = todas:
// analyzer-disable-next-line no-magic-numbers
const timeout = 3000;
save(); // analyzer-disable-line no-floating-promises
// analyzer-disable-file explicit-any (arquivo inteiro)New code / CI
--base <ref> cobra apenas as linhas alteradas em <ref>...HEAD (via git diff), tornando o Quality Gate justo em repositórios grandes. Com o exit code, bloqueia PRs:
npx tsx src/cli/bin.ts --repo . --base origin/main --open none # exit 1 se o gate falhar
# depois: upload de sonar-report.sarif no github/codeql-action/upload-sarifLeak period (achados novos)
Sem um servidor central, o diffai-guard mantém seu próprio "leak period": cada issue tem um fingerprint estável (rule key + arquivo + mensagem — a linha é ignorada de propósito, já que refatorações a deslocam). A cada execução, o fingerprint é comparado com .diffai/baseline.json da execução anterior:
- achado ausente no baseline anterior →
isNew: true(aparece com o badge NEW no dashboard/txt e a propriedadeisNewno SARIF/JSON); - ao final, o baseline é sobrescrito com os fingerprints desta execução — a próxima rodada compara contra este resultado.
Isso alimenta métricas de Quality Gate dedicadas: new_issues, new_bugs, new_vulnerabilities, new_blocker_issues, new_critical_issues (ver exemplos comentados em analyzer.config.example.jsonc).
Como o baseline é sobrescrito a cada execução, use --freeze-baseline em checks de PR (para não corromper a referência da branch principal com o estado da PR) e deixe as execuções na main/master atualizá-lo normalmente:
# check de PR: compara, não sobrescreve
npx tsx src/cli/bin.ts --repo . --base origin/main --open none --freeze-baseline
# execução na main: atualiza o baseline para a próxima comparação
npx tsx src/cli/bin.ts --repo . --all --open noneNa primeira execução (sem baseline ainda), tudo é considerado novo — isso estabelece a dívida inicial do projeto.
Arquitetura
Clean Architecture + Dependency Injection — cada recurso externo atrás de um port (src/application/ports.ts):
src/
domain/ # Issue, Rating, métricas, Quality Gate e leak period/baseline (puro)
rules/ # catálogo de regras de código (ts-morph) + interface Rule
schemas/ # Zod: entrada (diffai) + saída (analysis-report) + config
application/# ports + AnalyzeUseCase (orquestra só com ports)
infra/ # engine ts-morph, semantic-analyzer, scope-resolver, diffai-signals, duplicação (CPD por tokens), cobertura, baseline-store, writers (html/txt/json), opener, git-repo-finder, combined-report-writer
config/ # config-loader (defaults < arquivo < flags)
cli/ # Commander + composition rootAdicionar uma regra = 1 objeto em src/rules/catalog.ts. Trocar qualquer adapter = 1 linha no composition root.
Comandos
npm run typecheck # tsc --noEmit
npm test # vitest
npm run lint # eslint
npm run build # dist/Licença
MIT
