crapkit

Nove ferramentas somente leitura sobre pontuações CRAP por função (complexidade vezes risco não coberto), classificadas por churn, para repositórios Python e JavaScript.

Documentação

crapkit

ci PyPI Python License: MIT crapkit MCP server

crapkit init, coverage and worklist --top 5 on a small Python repo, then a shell heredoc adding a function at ccn 7: the per-edit advisory reports it and exits 2, and the commit gate refuses the staged file with exit 6

O crapkit pontua cada função do seu repositório com base na complexidade multiplicada pelo risco não coberto, classifica as piores pelo quão frequentemente o arquivo muda e bloqueia commits que adicionam mais. Ele lê Python, TypeScript, TSX, JavaScript, Swift, Go, Rust, shell, PowerShell, C e C++, Objective-C, Vue, Java e Zig por meio do lizard, e une a cobertura de ramos por função do artefato istanbul ou coverage.py que o seu próprio comando de teste já gera. Comandos JSON usam chaves ordenadas e um esquema versionado para scripts, agentes de codificação e o servidor MCP opcional.

CRAP = ccn^2 * (1 - cov)^3 + ccn

O nome não é nosso: C.R.A.P. (Change Risk Anti-Patterns) foi cunhado para o crap4j por Alberto Savoia e Bob Evans em 2007.

ccn é o menor entre a complexidade ciclomática padrão e a modificada, ambas lidas em uma única passada do lizard. cov é a cobertura de ramos dentro do intervalo da função; sem ramos, ela recorre à cobertura de instruções e, sem instruções, a invocado-ou-não, de modo que uma função linear executada pela metade nunca é lida como totalmente coberta.

Acima do teto, a cobertura não pode salvar você. Decomponha. No alvo padrão de 6, uma função com ccn 7 e 100% de cobertura ainda pontua 7 e ainda falha no portão. A única jogada que resolve é dividir a função.

Por que 6 e não 30. O limite convencional do crap4j de 30 é uma pontuação CRAP: ele deixa passar um ccn 5 não testado (25 + 5 = 30) e também um ccn 30 totalmente coberto. O padrão do crapkit é um teto de complexidade, porque a cobertura pode, no máximo, colapsar o CRAP para ccn, e uma função que você não consegue cobrir além de ccn 6 é uma função que você decompõe. Defina target = 30 em crapkit.toml se você quiser o número do crap4j. Um repositório com dívida existente não precisa: ratchet seed marca as funções de hoje acima do teto com a pontuação de hoje, o portão então julga apenas as funções que uma alteração toca, e as marcas só podem cair, então a adoção nunca começa com uma parede de vermelho. Ao lado do crap4py, radon, xenon, wily e SonarQube: docs/comparison.md.

O crapkit pontua apenas arquivos rastreados pelo git. Código-fonte que você não git addou é invisível para ele.

Comece comQuando
Instalação e o início em 60 segundosVocê quer a primeira pontuação em um repositório Git existente.
Python ou TypeScript quickstartVocê quer um exemplo prático desde a configuração até uma verificação aprovada.
AdoçãoVocê precisa escolher escopos, conectar testes ou introduzir uma catraca para dívida existente.
AtualizaçãoVocê já tem execuções salvas, marcas de catraca ou um plugin instalado.
Subcomandos e referência JSON/MCPVocê está escrevendo scripts de comandos ou conectando um agente de codificação.

O início em 60 segundos

pip install crapkit
cd your-repo
crapkit init        # write crapkit.toml and ignore measurement output
crapkit doctor      # check scopes, test commands and coverage dependencies
crapkit coverage    # runs the lane, joins coverage, stores a scored run
crapkit worklist    # the ranked risk map
crapkit ratchet seed
git add crapkit.toml crapkit-ratchet.tsv .gitignore

Não é um repositório Python? uvx crapkit init executa os mesmos comandos e não adiciona nada ao seu manifesto: veja Um repositório que não é Python.

init detecta pytest, Vitest e Jest a partir dos próprios arquivos do repositório. Revise a configuração gerada antes de executar seus comandos. Quando a detecção deixa uma faixa comentada, preencha-a usando as receitas de faixa. Faça commit dos arquivos de adoção e então execute crapkit verify para estabelecer um veredito aprovado. Instale o portão de commit quando a configuração e a catraca estiverem prontas.

coverage pontua, worklist classifica:

$ crapkit coverage
run 1 @ fae4db93108: 2 functions scored: 2 measured, 1 over ceiling 6, CRAP load 41.0, grade F
-> next: crapkit worklist

$ crapkit worklist
worklist @ fae4db93108 (run 1, floor ccn>=5, churn 12mo) - 1 of 1 active (worklist_top 50), 0 dormant
  risk     14.0  ccn  14  crap    38.5  cov  50%    1c/1a  calc/grade.py:7  classify( score , attempts , late , bonus )

risk 14.0 é ccn vezes um peso de churn de um: um repositório com um único commit não tem dispersão de commits para ponderar, então cada commit conta uma vez e a classificação é por ordem de complexidade até o histórico crescer (Risco). crap 38.5 e cov 50% são a pontuação e a cobertura por trás dela.

ratchet seed assina a dívida de hoje com a pontuação de hoje. A partir daí, as marcas só caem, então o repositório pode melhorar e nunca piorar enquanto você a reduz.

Uma coisa impede a maioria das primeiras execuções: o plugin de cobertura. init escreve uma faixa que chama o seu próprio executor de testes, e o executor precisa do seu pacote de cobertura instalado: pytest-cov para pytest, @vitest/coverage-v8 (fixado na sua versão principal do vitest) para vitest. Sem ele, a faixa não produz artefato e coverage sai com código 5 citando o erro do próprio executor. Para pytest, init testa o python que sua faixa executará e imprime o comando de instalação quando pytest_cov estiver ausente; pip install "crapkit[py]" puxa o plugin junto com o crapkit quando os dois compartilham um venv. Em um PATH do Windows contendo apenas o lançador py, ele escreve py, não um python3 que a faixa nunca poderia executar, e quando o cmd.exe não consegue iniciar o interpretador (saída 9009, o alias da Store), ele nomeia isso em vez de adivinhar o pytest-cov. Um repositório que não fixa lockfile e carrega seu próprio .venv recebe o interpretador desse venv na faixa, quando esse interpretador consegue importar pytest, em vez do python que o shell responder. Os dois quickstarts abaixo percorrem um repositório real de ponta a ponta.

No Windows, um comando de faixa é lido pelo cmd.exe, o shell que o executará, não pelo sh. Aspas duplas são a citação portátil. Um valor com aspas simples é recusado no carregamento da configuração com saída 3, porque o cmd.exe entregaria cinco palavras ao pytest e a faixa não escreveria artefato:

# the lane in crapkit.toml
command = "python -m pytest -m 'not live and not perf' --cov=calc --cov-branch --cov-report=json:.crapkit/cov/py.json"

$ crapkit doctor
crapkit: lane 'py': positional argument 'live' narrows a full-suite coverage run; drop it, attach it to the flag it belongs to (-n8, --numprocesses=8), or set full_suite = false deliberately (cmd.exe does not treat ' as a quote: write the value in double quotes); a suite whose testpaths cannot be collected in one process needs one lane per testpath, each with full_suite = false and its own artifact

Escreva -m "not live and not perf". Acentos circunflexos, segmentos && e |, redirecionamentos e argumentos citados vazios são lidos do jeito que o shell os lê, então uma faixa encadeada (cd tests && python -m pytest --cov ...) é verificada um segmento por vez. doctor lê uma faixa da mesma forma e FALHA em uma cujo executor não iniciar.

Instalação

pip install crapkit

Essa é a versão no PyPI. Para a ponta não lançada de main, ou a partir de um clone local (execute na raiz do clone):

pip install git+https://github.com/JeanFrancoisGagne/crapkit.git
pip install .

Um repositório que não é Python

O crapkit é uma ferramenta de linha de comando, nunca uma dependência do código que ele pontua. Um repositório TypeScript, Go ou Rust não adiciona nada ao seu próprio manifesto. Com uv na máquina, uvx busca o crapkit em um cache próprio e o executa:

$ uvx crapkit init
wrote crapkit.toml with 1 scope(s): src
detected 1 lane(s) from this repo's own files: js - next: run `crapkit coverage`
added to .gitignore: .crapkit/

$ uvx crapkit coverage
$ uvx crapkit worklist

A faixa ainda executa o seu próprio executor de testes, então Vitest ou Jest e seu pacote de cobertura vêm do node_modules do repositório como hoje. uv tool install crapkit ou pipx install crapkit coloca um comando crapkit no PATH uma vez, que é o que o portão de commit e o plugin do Claude Code chamam. O uv traz seu próprio Python quando a máquina não tem nenhum.

Requer Python 3.11 ou mais novo e Git no PATH. A CLI tem uma dependência de runtime, lizard>=1.24.0; um espelho de pacotes precisa de ambas as distribuições. Instale no ambiente que você pretende usar e então verifique crapkit --version. O pip install -e ".[dev]" em Desenvolvimento é uma coisa diferente: ele adiciona o extra de teste, para pessoas que alteram o crapkit.

Projetos Python podem instalar pip install "crapkit[py]" no ambiente de teste para incluir pytest-cov e coverage.py com capacidade de subprocesso. Uma instalação de ferramenta separada ainda precisa do plugin de cobertura no ambiente que executa a suíte.

Análise e pontuação rodam localmente e não enviam telemetria. Comandos configurados de faixa, mutação e alerta rodam com suas permissões e podem contatar serviços ou alterar arquivos. Revise esses comandos antes de executar o Crapkit em um repositório que você não confia (SECURITY.md).

$ crapkit --version
crapkit 0.8.0

python -m crapkit funciona de forma idêntica ao script de console e é o que se usa a partir de um checkout de código-fonte. Todo subcomando aceita --repo PATH (padrão: o crapkit.toml mais próximo no nível ou acima do diretório atual, então um workspace de monorepo encontra a raiz), e com ele você nunca precisa cd no repositório que está pontuando; Subcomandos mostra onde a flag vai.

Atualização

Mantenha as versões da CLI e do plugin alinhadas, meça cobertura fresca após atualizar e revise qualquer recusa de identidade da catraca antes de ressemear. O leitor atual é a versão 11 de análise. Ele renomeia uma vez cada def Python com uma lista de parâmetros de tipo PEP 695 e cada def aninhada três ou mais níveis de profundidade, e lista um def cujo corpo está na linha dos dois pontos; crapkit ratchet prune remove as marcas deixadas sob os nomes antigos. Marcas de callback mais antigas de JavaScript e TypeScript podem exigir um mapeamento revisado. Siga o guia de atualização para estado salvo, registros portáteis e bloqueios do lançador no Windows.

Atualizando da 0.4.4

Este exemplo histórico descreve a transição da 0.4.4 para a 0.4.5, da versão 7 para a 8 de análise. Ele é mantido para explicar a recusa, citada como o crapkit a imprime hoje:

$ crapkit verify
crapkit: ratchet marks were recorded under [crapkit-analysis=7 lizard=1.24.0] but this run measures [crapkit-analysis=8 lizard=1.24.0] — CRAP scores are not comparable across metric versions; run `crapkit coverage`, then re-baseline with `crapkit ratchet seed`

Essa transição mudou a complexidade cognitiva, não ccn ou a fórmula CRAP. Mudanças posteriores no leitor também afetam a identidade da função. Use o guia de atualização atual ao migrar de qualquer versão mais antiga para o leitor de hoje.

O bloqueio do exe no Windows

Um servidor MCP ativo pode manter crapkit.exe aberto e fazer uma atualização falhar com o erro 32 do Windows. Pare esse servidor ou sua sessão de agente, execute novamente a atualização com o mesmo instalador e reinicie o cliente. Veja o procedimento de atualização no Windows.

O plugin do Claude Code

claude plugin marketplace add JeanFrancoisGagne/crapkit
claude plugin install crapkit@crapkit

Dois comandos, instalados uma vez por usuário, e todo repositório na máquina o recebe. O plugin traz três skills, o servidor MCP do lado de leitura e um hook consultivo PostToolUse que nomeia qualquer função que uma edição empurrou acima do teto. O Claude alcança duas das skills sozinho, crapkit e crapkit-recover; a terceira você digita, como /crapkit:crapkit-onboard, porque conectar um repositório acontece uma vez e sua descrição não tem lugar na janela de todo turno. Ele não adiciona arquivos ao seu repositório e precisa da CLI do crapkit no PATH.

Um repositório sem crapkit.toml custa um no-op silencioso por edição: 68 ms no Windows pelo lançador crapkit.exe que o Claude Code inicia, onde um python -c pass puro levou 32 ms na mesma máquina. Após atualizar a CLI, atualize o marketplace antes de atualizar o plugin instalado:

claude plugin marketplace update crapkit
claude plugin update crapkit@crapkit --scope user
crapkit doctor --plugin-root

Reinicie sessões existentes do Claude Code para aplicar a atualização do plugin. A verificação acima compara arquivos instalados com a CLI no PATH; ela não recarrega uma sessão em execução.

O hook registra em Edit|Write, que é toda escrita que nomeia um arquivo. Um agente que escreve seu código-fonte por um heredoc de shell não nomeia nenhum, então um evento Bash é julgado pela árvore de trabalho. Essa metade é sua para registrar, porque custa dois spawns de git por chamada de shell. Adicione uma segunda entrada PostToolUse às suas próprias configurações, mesmo comando, matcher Bash:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "crapkit claude-hook --protocol 1", "timeout": 20 }
        ]
      }
    ]
  }
}

O custo é um git rev-parse --show-toplevel e um git status --porcelain -z -uall por chamada de shell em qualquer repositório git, quer o crapkit o meça ou não: cerca de 50 ms juntos no próprio checkout do crapkit no Windows, e mais em uma árvore maior. O que volta são os arquivos *.py sujos ou não rastreados escritos nos últimos 12 segundos, no máximo 25, cada um julgado como uma edição. Somente Python, então um repositório TypeScript ou Go paga os dois spawns e não ouve nada.

Codex

O Codex pode instalar o plugin do mesmo marketplace pelo seu próprio gerenciador:

codex plugin marketplace add https://github.com/JeanFrancoisGagne/crapkit.git
codex plugin add crapkit@crapkit

Use as três skills e o servidor MCP no Codex. As instruções do hook consultivo acima configuram o evento PostToolUse do Claude Code. Para atualizar uma instalação existente do Codex:

codex plugin marketplace upgrade crapkit
codex plugin add crapkit@crapkit
codex plugin list --marketplace crapkit --json

Verifique o plugin instalado do Codex com um crapkit doctor --plugin-root PATH explícito. Veja atualizações de plugin para escolher esse caminho e iniciar uma nova sessão MCP. Um runtime com diretório de skills mas sem marketplace compatível pode copiar plugin/skills/*; outros clientes MCP usam a configuração stdio.

Linguagens

14 idiomas, dois parsers de cobertura. A cobertura entra onde existe um parser; todo o resto é pontuado apenas pela complexidade.

IdiomaArquivosCobertura
Python.pycoverage.py
TypeScript.tsistanbul
TSX.tsxistanbul
JavaScript.js .jsx .mjs .cjsistanbul
Vue.vueistanbul, quando sua execução do vitest reporta arquivos .vue
Swift.swiftnenhum: apenas cc
Go.gonenhum: apenas cc
Rust.rsnenhum: apenas cc
shell.sh .bashnenhum: apenas cc
PowerShell.ps1 .psm1nenhum: apenas cc
C e C++.c .cc .cpp .cxx .h .hppnenhum: apenas cc
Objective-C.m .mmnenhum: apenas cc
Java.javanenhum: apenas cc
Zig.zignenhum: apenas cc

Um escopo apenas-cc declara coverage_optional = true, pontua crap = ccn e não precisa de lane. Nada nele é provisório: o teto ainda vale e o portão ainda recusa uma função acima dele. Adicione uma lane de cobertura no dia em que um parser existir e o mesmo escopo começa a entrar na cobertura.

crapkit init escreve essa chave por conta própria, em todo escopo cujos idiomas não têm parser, e a deixa de fora de qualquer escopo que uma lane ainda possa medir. Então o início de 60 segundos acima roda sem alterações em um repositório Go, Rust ou shell: crapkit coverage o pontua sem lane alguma, e essa execução é a linha de base que worklist, next-item, ratchet seed e verify leem.

Três leitores são do próprio crapkit. O lizard não traz nenhum para shell ou PowerShell, então o crapkit conta as funções deles por conta própria. Seu leitor Rust pontua um match de 7 braços como ccn 2 (registrado como lizard #494), então o crapkit conta cada braço não-curinga como um case de C e aposenta a substituição no dia em que o upstream corrigir. A coluna cognitiva cobra esse mesmo bloco uma vez, da forma como o Sonar cobra um switch.

Setas de expressão em arrays e listas de argumentos são medidas separadamente. Em TypeScript, envolva o corpo de uma seta em parênteses quando ele contiver < antes de uma vírgula, como x => (pair<T,U>(x)) ou x => (x < 0). Sem esse delimitador, a análise recusa o arquivo porque este leitor não consegue distinguir argumentos de tipo de um separador de expressão. Declarações de parâmetros de seta genéricos continuam suportadas.

Funções na mesma linha têm identificadores de ocorrência separados. Marcas de catraca existentes com identidades antigas ambíguas exigem um mapeamento revisado; veja identidade de função na mesma linha.

O portão

Use o aviso ao editar, o portão ao commitar e verify para o veredito completo. A prévia e os hooks diferem no que suas evidências disponíveis podem provar:

SuperfícieDisparaPoder
crapkit claude-hookapós a edição de um agente chegaravisório. Nomeia a violação no stderr. Não bloqueia nada, porque o PostToolUse roda após a escrita
crapkit rescore FILE --gatequando você pede, após a primeira execução de coberturaprévia. Uma prévia mais estrita do portão de commit, em menos de um segundo, antes de você fazer o stage: uma marca de catraca perdoa uma função apenas enquanto seu CRAP estiver na marca ou abaixo dela. Sem execução por trás, saída 1 e no snapshot
crapkit hook-precommitgit commitbloqueia. O hook sai com 6; o git reporta 1. Apenas blobs com stage, então custa o tamanho do commit e não precisa de cobertura
crapkit verifyantes de você dar push, e no CIo veredito. Portão, catraca, novas falhas de teste, cobertura de diff, contra a linha de base confiável

Ambos os hooks isentam uma função para a qual a catraca commitada já tem uma marca, então tocar dívida assinada nunca recusa um commit. verify é o que falha uma marca que sobe. Desde 0.4.5 seu portão isenta uma função tocada cujo CRAP novo está na marca ou abaixo dela, a regra que rescore --gate já aplicava; empurre-a além da marca e o portão dispara de novo. O hook de pré-commit ainda isenta apenas pela existência da marca, de propósito: um blob com stage não tem cobertura, então não há CRAP novo para comparar. Ele reporta cada contagem de isenção no stderr (staged function(s) carry a ratchet mark and were not gated) e diz o mesmo sobre um arquivo com stage que nenhum [[scope]] reivindica, então um novo diretório de nível superior não pode ficar sem portão em silêncio.

A raiz do Crapkit pode ficar abaixo do topo do Git. Uma config em packages/api aplica o portão aos arquivos com stage desse pacote como caminhos relativos ao projeto, como app/m.py. Regras de caminho e raiz também cobrem argumentos absolutos, nomes de arquivo literais e configurações de diff do Git.

O Git roda hooks fora do venv ativado do seu shell. Um python puro deve resolver para um interpretador que tenha o crapkit instalado, ou escreva-o por extenso (exec /path/to/venv/Scripts/python -m crapkit hook-precommit).

Rota 1: .git/hooks/pre-commit (local, não commitado)

cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
exec python -m crapkit hook-precommit
EOF
chmod +x .git/hooks/pre-commit

O mesmo arquivo do PowerShell. Out-File e > escrevem uma marca de ordem de byte (UTF-16 no 5.1) na frente do shebang, e o git então recusa todo commit com error: cannot spawn .git/hooks/pre-commit: No such file or directory (medido no git 2.43 para Windows) sem nunca rodar o portão; Set-Content -Encoding ascii não escreve marca. O Git roda o hook com seu próprio sh, então o interpretador é escrito com barras e entre aspas, e nenhum chmod é necessário no Windows:

$python = (Get-Command python).Source -replace '\\', '/'
Set-Content -Path .git/hooks/pre-commit -Encoding ascii -NoNewline -Value "#!/bin/sh`nexec '$python' -m crapkit hook-precommit`n"

crapkit doctor avisa quando o arquivo de hook que o git iria executar começa com uma marca de ordem de byte.

Rota 2: um diretório de hooks commitado

A rota inteira, de um repositório que ainda não tem githooks/:

mkdir -p githooks
cat > githooks/pre-commit <<'EOF'
#!/bin/sh
exec python -m crapkit hook-precommit
EOF
chmod +x githooks/pre-commit
printf 'githooks/pre-commit text eol=lf\n' >> .gitattributes
git add .gitattributes githooks/pre-commit
git update-index --chmod=+x githooks/pre-commit
git commit -m "add crapkit gate hook"
git config core.hooksPath githooks

O --chmod vai entre o add e o commit. Ele escreve o bit executável no índice, então um commit que já aconteceu não o carrega: rode-o depois e git ls-tree HEAD still says 100644, que é um hook que checkouts Unix pulam silenciosamente. A linha .gitattributes é a metade mais difícil da mesma falha: sob o padrão do Windows core.autocrlf, o hook faz checkout de CRLF e #!/bin/sh\r morre no Linux e macOS com um erro de interpretador inválido. crapkit doctor avisa quando um arquivo sob core.hooksPath não está 100755 no índice e imprime a linha update-index para ele.

O Git não lê um caminho de hooks de um arquivo commitado, então essa linha git config pertence aos seus passos de configuração no CONTRIBUTING. Todo clone arma o portão com ela.

Rota 3: o framework pre-commit

O crapkit traz um .pre-commit-hooks.yaml declarando id: crapkit-gate. No seu .pre-commit-config.yaml:

repos:
  - repo: https://github.com/JeanFrancoisGagne/crapkit
    # crapkit's release step rewrites this line to the tag it just cut
    rev: v0.8.0
    hooks:
      - id: crapkit-gate

Esse arquivo não arma nada por conta própria. O framework escreve .git/hooks/pre-commit quando você diz a ele, e até lá git commit não roda portão algum e não diz nada:

pip install pre-commit
pre-commit install

pre-commit install é a linha que todo clone precisa, da mesma forma que a Rota 2 precisa da sua linha git config core.hooksPath.

rev é uma ref de git que o pre-commit resolve contra aquele remoto. Fixe uma tag de release, não um branch: pre-commit autoupdate só se move entre tags, e um main em movimento mudaria seu portão sob você.

Rota 4: CI

Um job de CI roda em um clone novo, que não tem armazenamento de .crapkit/, então um crapkit verify puro sai com 1. Rodar coverage primeiro faria a árvore do próprio PR ser a linha de base, um portão que nunca pode falhar. A linha de base portátil é o mecanismo:

# on the default branch, after a passing verify: commit this file
crapkit verify --emit-baseline crapkit-baseline.tsv

# in the PR job, against the committed baseline
crapkit verify --baseline-tsv crapkit-baseline.tsv --github

--github emite anotações ::error file=... que caem no diff do PR; --sarif PATH escreve SARIF 2.1.0 para upload de varredura de código. Atualize a linha de base commitada sempre que a verificação do branch padrão passar.

Duas coisas que o job tem que fazer antes dessas linhas rodarem. Instale o crapkit, pip install crapkit, and pin the version the way Route 3 pins rev: uma instalação sem pin move seu portão no dia em que um release sair. Busque o histórico inteiro. actions/checkout clona um commit por padrão, verify lê o diff contra o commit da linha de base do git, e um clone raso não tem esse commit:

$ crapkit verify --baseline-tsv crapkit-baseline.tsv
crapkit: baseline commit a74260f321f is not an ancestor of HEAD in this shallow clone, which does not hold it; set fetch-depth: 0 on the checkout or run git fetch --unshallow

Isso é saída 4 em um git clone --depth 1 de um repositório cuja linha de base verifica em profundidade total. Em um clone completo, a mesma saída culpa o que costumava culpar, um rebase ou um amend que reescreveu o histórico, e pede uma linha de base nova em vez disso. verify --base e hook-precommit --base procuram o ponto de fork com git merge-base, e no mesmo clone eles recusam com saída 4 e a mesma correção:

$ crapkit hook-precommit --base c47a37b1df69c434ba42eec5979ddad03d2bf1e4
crapkit: git merge-base c47a37b1df69c434ba42eec5979ddad03d2bf1e4 HEAD failed in /home/runner/work/app/app: fatal: Not a valid commit name c47a37b1df69c434ba42eec5979ddad03d2bf1e4; this shallow clone does not hold every commit: set fetch-depth: 0 on the checkout or run git fetch --unshallow

Quando o clone tem ambos os commits, mas não aquele de onde eles bifurcam, a linha lê no merge base between REF and HEAD in ROOT, seguida da mesma correção. Defina fetch-depth: 0 na etapa de checkout, que é o que o próprio .github/workflows/ci.yml do crapkit faz.

O job inteiro do PR, no GitHub Actions:

on: pull_request
jobs:
  crapkit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # verify needs the baseline's commit
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install crapkit
      - run: pip install -e ".[dev]"   # your own test dependencies
      - run: crapkit verify --baseline-tsv crapkit-baseline.tsv --github

A segunda instalação é a que as pessoas esquecem. verify reexecuta suas lanes, então o job precisa do que seu comando de teste precisar: o plugin de cobertura, npm ci, um banco de dados, tudo isso. Sem eles, a lane não escreve artefato e verify sai com 5 citando o erro do próprio runner, que é um job quebrado e não um veredito.

Como é uma recusa

$ git commit -m "add route"
crapkit gate: 1 staged function(s) exceed the complexity ceiling of 6:
  ccn   7  app/m.py:9  route( a , b , c , d )
decompose before committing (coverage cannot save a function above the target).

Esse commit saiu com 1, não 6. O Git colapsa qualquer hook falho para 1, então 6 é um código que você só vê rodando o hook você mesmo: crapkit hook-precommit sai com 6 em uma violação e 0 caso contrário. O bloco de stderr acima é o mesmo de qualquer forma.

CRAPKIT_OVERRIDE_REASON não é um bypass. Definir isso roteia o commit pela auditoria completa de três registros: uma linha de alerta via alert_command, uma entrada de catraca com stage no commit e uma linha no log de substituições. Tudo isso cai ou nada cai, e um alert_command não definido recusa a substituição por completo. Veja docs/ratchet.md.

A GitHub Action

action.yml na raiz deste repositório é uma action composta, então um revisor vê os números do crapkit no pull request sem instalar nada. Quatro linhas a adicionam a um workflow, e toda entrada tem um padrão:

      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: JeanFrancoisGagne/crapkit@v0.8.0

O job inteiro em que essas quatro linhas ficam:

on: pull_request
jobs:
  crapkit:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write             # the comment, and nothing else
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0               # the diff, and verify's baseline commit
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"       # the interpreter the install below lands in
      - run: pip install -e ".[dev]"   # whatever your lanes need to run
      - uses: JeanFrancoisGagne/crapkit@v0.8.0
        with:
          gate: "false"

Essa etapa pip install é a que as pessoas esquecem, e é a mesma que a Rota 4 acima nomeia: a action instala o crapkit e nada mais, então suas lanes ainda precisam do que seu comando de teste precisar. Sem ela, a lane não escreve artefato e o comentário diz isso.

fetch-depth: 0 é a outra. actions/checkout clona um único commit; a action lê os arquivos alterados do pull request do git e verify lê o diff contra o commit da linha de base. Com um clone raso, a lista de arquivos volta vazia e o comentário classifica o repositório inteiro em vez do diff.

A action instala o crapkit de $GITHUB_ACTION_PATH, que é seu próprio checkout da ref que você fixou em uses:. Então um pin deixado na tag do mês passado pontua sua árvore com o crapkit do mês passado em vez do que quer que tenha lançado desde então, e fixar uma tag é toda a política de versão; os trechos acima nomeiam o release atual.

Como é o comentário

Um comentário por pull request, editado no lugar a cada push. Uma linha oculta <!-- crapkit-action --> é como a próxima execução o encontra, então um branch de quinze pushes carrega um comentário e não quinze. Em um evento push, não há pull request para carregá-lo, e o mesmo texto vai para o log do job em vez disso.

Renderizado de três payloads salvos: um pull request que adiciona um route() sem teste (ccn 8) ao lado de um legacy_router() com marca de catraca, em um repositório cujo diff_uncovered_max é 3. Os payloads estão sob tests/fixtures/action_comment/, e a suíte de unidades fixa este bloco à renderização deles:

<!-- crapkit-action -->

## crapkit

4 functions in 2 files, 2 over ceiling 6, CRAP load 149.59, grade F.

**verify failed, exit 6: complexity gate.**

- gate: `app/calc.py:34` `route( a , b , c , d )` ccn 8, cov 0%, crap 72.0 -> decompose
- uncovered lines in `app/calc.py`: 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45

Run 3 against baseline 1, 1 changed file: 1 gate violation, 0 ratchet regressions, 0 new test failures, 11 uncovered changed lines.

### Worklist: 1 changed file

| File | Function | ccn | risk | remedy |
|---|---|---:|---:|---|
| `app/calc.py:34` | `route( a , b , c , d )` | 8 | 4.0 | decompose |
| `app/calc.py:19` | `legacy_router( a , b , c , d , e )` | 8 | 4.0 | decompose (accepted debt) |

A primeira linha é a execução que crapkit coverage escreveu: funções e arquivos, quantos estão acima do teto (over ceiling 6, ou over their ceilings (6; reports 12, util 4) quando escopos definem o seu próprio), carga e nota CRAP, com a primeira linha de erro de uma pista reprovada anexada como ; lane 'js' failed: .... Quando coverage --json morreu antes de um resumo, a linha cita o objeto de erro que ele imprimiu em vez disso: `crapkit coverage` saiu com código 5: a pista 'py' não consegue importar pytest-cov; instale pytest-cov.

O veredito abre com o código de saída e a regra que ele representa (complexity gate, ratchet regressions, new test failures, diff-coverage ceiling N), depois um marcador por constatação: cada violação de portão com sua função, ccn, cobertura, CRAP e remédio; cada regressão de catraca como registrada -> nova; cada nova falha de teste por id; e as primeiras vinte linhas alteradas sem cobertura, um marcador por arquivo, com uma contagem do restante. A linha de contagens fecha isso. Uma verificação que passou é uma linha: **verificação passou.** Execução 2 contra linha de base 1, 7 arquivos alterados.

As linhas são a lista de trabalho classificada para os arquivos que a solicitação de pull alterou, piores primeiro, top delas, com as linhas que uma constatação nomeia listadas primeiro. risk é ccn vezes peso de rotatividade, o número pelo qual crapkit worklist classifica, e remedy é o próprio veredito da execução para essa função: decompose, split-lines, add-tests ou ok. (accepted debt) marca uma função que a catraca confirmada carrega uma marca para, então um legacy_router intocado não parece como a nova função da própria solicitação de pull. Uma solicitação de pull que não toca em nenhuma função classificada recebe o cabeçalho e nenhuma tabela.

As duas contagens de arquivos descrevem o mesmo diff, contado duas vezes. 39 changed files é git diff --name-only base.sha...HEAD, os commits da própria ramificação, e é o que a tabela é filtrada. A contagem na linha do veredito é o que verify mediu do mesmo ponto de bifurcação. Com delta: "false" o segundo é 0, porque não há nada atrás do checkout para medir.

As entradas

EntradaPadrãoO que faz
gate"false""true" sai com o código próprio de crapkit verify, então uma constatação falha na verificação. Em uma solicitação de pull com delta também sai com 1 quando a execução base não foi feita, porque um veredito sem execução base não julgou nenhuma função alterada. Qualquer outra coisa sai com 0 e o comentário é a saída inteira
delta"true"pontua o commit base da solicitação de pull primeiro, então o veredito cobre os commits que a solicitação de pull adiciona. Custa uma segunda execução de pista; "false" pontua o checkout sozinho, e o veredito então não julga nenhuma função alterada
top"5"linhas da lista de trabalho renderizadas na tabela
python-version"3.12"o interpretador no qual actions/setup-python instala crapkit. Combine-o com a versão que seu próprio passo setup-python nomeou, ou as pistas rodam em um interpretador que suas dependências nunca alcançaram

gate: "false" é o padrão de propósito. Uma equipe adota a ação antes de decidir quais constatações devem impedir um merge, e uma verificação que falha no primeiro dia é desligada no segundo dia.

O que a linha do veredito cobre

Em uma solicitação de pull, os commits que a solicitação de pull adiciona. A ação pontua o ponto de bifurcação primeiro, depois o checkout, e então executa crapkit verify --base <fork>, que mede o diff a partir daí e toma a execução do ponto de bifurcação como sua linha de base. Então o portão julga as funções no diff que um revisor está lendo, e um repositório que já estava acima do seu teto antes da ramificação começar não falha em toda solicitação de pull que o toca.

O ponto de bifurcação é git merge-base de base.sha e HEAD, não base.sha em si. base.sha é a ponta da ramificação base quando o evento disparou, então uma ramificação base que se moveu depois que a ramificação bifurcou carrega commits que HEAD nunca viu, e uma execução lá não seria nem a linha de base que a verificação quer nem um diff que alguém está revisando.

A execução base acontece em uma árvore de trabalho destacada sob RUNNER_TEMP, e seu armazenamento é copiado sobre o do checkout para que ambas as execuções fiquem em um lugar. O custo é duas execuções de pista em uma solicitação de pull: sua suíte roda uma vez no ponto de bifurcação e uma vez no checkout. Defina delta: "false" para pular a execução base, e o veredito cai para o checkout contra sua própria execução, que relata a saúde da própria árvore e não julga nenhuma função alterada. O comentário diz isso no lugar de verify passed:

**verify judged no changed function:** the base run was not made (no base commit). Run 2 against baseline 2, 0 changed files.

Três coisas deixam a execução base não feita: um clone raso que não contém o ponto de bifurcação, um ponto de bifurcação mais antigo que seu crapkit.toml, e uma pista que não rodará contra essa árvore. O passo registra crapkit base scoring exited N e escreve o motivo em crapkit-base.reason nas palavras que o comentário então cita, clone raso não contém o ponto de bifurcação de <sha>; defina fetch-depth: 0 no checkout, sem crapkit.toml utilizável no ponto de bifurcação <sha>: ..., or pista falhou no ponto de bifurcação <sha>: ... com a primeira linha de erro da pista. O veredito cai da mesma maneira que delta: "false" faz, e a catraca ainda roda, então saída 7 lá é uma constatação. O que difere é o status do trabalho. Com gate: "true" em uma solicitação de pull cuja execução base foi tentada e não feita, o passo de saída sai com 1 e imprime o motivo, porque o clone padrão de profundidade 1 de actions/checkout de outra forma transformaria toda solicitação de pull em uma verificação verde que não julgou nada. Um evento push e delta: "false" nunca tentam a execução base, então eles mantêm o próprio código da verificação; um push não tem commit base e nenhuma solicitação de pull para comentar.

Um requisito que a execução base adiciona: a pista tem que medir a árvore em que roda. Uma pista que alcança uma cópia instalada do seu pacote em vez do checkout medirá o código da solicitação de pull enquanto está no commit base, e as duas execuções então descrevem a mesma árvore. crapkit verify recusa uma execução cujos artefatos nomeiam arquivos fora da árvore (saída 5), o que pega a versão barulhenta disso; uma pista fixada em um caminho fora da árvore de trabalho é a silenciosa. Aponte a pista para a árvore, ou defina delta: "false".

--reuse-artifacts é o que mantém cada uma dessas execuções em uma passagem da sua suíte. coverage rodou as pistas momentos antes nessa árvore, e a verificação analisa esses artefatos em vez de rodar a suíte inteira uma segunda vez para os mesmos números.

É por isso que crapkit coverage tem que sair com 0 para haver um veredito. Quando sai com qualquer outra coisa (uma pista que falhou, um artefato que recusou), a ação não executa verify: em um runner que mantém seu espaço de trabalho entre trabalhos (clean: false), um verify --reuse-artifacts sobre uma medição falhada leu o artefato que a pista morta tinha deixado de uma execução anterior, passou por cima dele, e fez dessa execução a linha de base confiável. O comentário então carrega a falha de cobertura no lugar do veredito:

**no verdict: `crapkit coverage` exited 5 (lane 'py' failed: lane 'py' wrote no artifact on its last attempt; the .crapkit/cov/py.json on disk predates it); verify did not run.**

O parêntese é a primeira linha da falha de pista que o resumo carrega. Quando toda pista falhou, coverage imprime um objeto de erro em vez de um resumo e os erros de pista estão apenas no log do trabalho, e a linha diz isso: (toda pista falhou (1 de 1); os erros de pista estão no log do trabalho). With portão: "true" o trabalho sai com o código de cobertura.

O outro portão que julga um delta é a linha de base portátil em Rota 4: confirme crapkit-baseline.tsv na ramificação padrão e execute crapkit verify --baseline-tsv crapkit-baseline.tsv em um passo seu. Não precisa de uma segunda execução de pista, e precisa de alguém para manter esse arquivo atualizado.

O comentário é postado com gh api e o próprio GITHUB_TOKEN do trabalho, que precisa de pull-requests: write. Duas coisas que ele não pode fazer: uma solicitação de pull de um fork recebe um token somente leitura, então o POST é um 403 lá, e um runner auto-hospedado sem o CLI gh no PATH falha nesse passo. Ambos deixam o texto renderizado no log do trabalho.

Subcomandos

crapkit clean --dry-run --json pré-visualiza recuperação de mutação temporária abandonada. Veja políticas de recursos para workers de análise compartilhados, tempo de vida de processo e logs limitados.

Todo subcomando aceita --repo PATH, e a flag vai depois do subcomando. Sem ela, a raiz é o crapkit.toml mais próximo no nível ou acima do diretório atual (ADR 0002): de um workspace de monorepo crapkit worklist lê a configuração raiz que reivindica o workspace, diz crapkit: using crapkit.toml at /repo no stderr, e lê um argumento de caminho relativo de onde você está. claude-hook é a única exceção: não tem --repo, porque tira sua raiz do arquivo nomeado no payload de hook que lê.

$ crapkit worklist --repo /path/to/repo --scope util --top 1
worklist @ a7c5c85ac37 (run 1, floor ccn>=5, churn 12mo) - 1 of 3 active (--top 1), 0 dormant
  risk      5.4  ccn   5  crap    30.0  cov   0%    5c/1a  util/stats.py:1  bucket( value , low , high )

Antes disso, argparse lê o caminho como o nome do subcomando e sai com 2 sem nunca mencionar --repo:

$ crapkit --repo /path/to/repo worklist --top 1
crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from 'inventory', 'coverage', ...)

--json imprime um objeto JSON de chaves ordenadas no stdout, sempre carregando um campo schema.

ComandoO que faz
clean [--dry-run] [--json]Recupera worktrees de mutação temporária abandonados. Preserva execuções ativas e pools de mutação intencionais. --dry-run relata recuperações planejadas. Ele não mexe em evidências de teste: o runner de desenvolvimento do próprio crapkit, tools/testing/run.py, poda .crapkit/test-runs/ com --retention-days e --retention-count. --json mantém test_runs com todos os arrays vazios.
initExamina o código-fonte rastreado em escopos por diretório, escreve um crapkit.toml inicial autovalidado cujas pistas reportam para .crapkit/cov/, e anexa .crapkit/ mais os resíduos de cada runner ao .gitignore. Escreve um [[lane]] ativo quando consegue detectar o runner de testes, caso contrário um template comentado. Recusa sobrescrever uma configuração existente.
doctor [--show-files] [--json] [--tune] [--plugin-root [PATH]]Verifica se a configuração ainda descreve o repositório: chaves desconhecidas (com as grafias aceitas), escopos com zero arquivos, código-fonte rastreado que nenhum escopo reivindica, escopos que nenhuma pista cobre, cwds e comandos de pista que não resolvem mais, lizard importável, arquivos grandes demais. Ele lê cada comando de pista com o shell que o executará, então um caminho de interpretador entre aspas é uma palavra e um runner após && também é verificado, e ele FALHA uma pista cujo runner não resolve ou que o shell não consegue iniciar, nomeando a palavra a mudar; um nome simples é procurado no PATH e um runner escrito como caminho é procurado no diretório em que a pista roda, então .venv/bin/python responde o mesmo de qualquer diretório em que você execute doctor; cada runner distinto é testado uma vez, não uma vez por pista. Ele AVISA sobre uma pista gravando seu artefato na raiz do repositório, uma pista coveragepy ou istanbul sem results_artifact (as verificações de worker travado e sem novas falhas estão desligadas para ela, seja qual for o runner que a pista indicar), um hook commitado sob core.hooksPath que não é executável no índice, um diretório cujas funções são todas untested enquanto seus testes existem, e um escopo que uma pista mede sem template [crapkit.scoped_tests] por trás, que é o passo 4 do loop sem nada para rodar. --tune imprime sugestões de paralelismo e não escreve nada. --plugin-root PATH não lê repositório nenhum: verifica um plugin instalado contra o crapkit no PATH (o nome simples que seus hooks e servidor MCP iniciam) tanto na versão quanto no hook --protocol, e FALHA quando o PATH não carrega crapkit nenhum, uma linha por divergência e silêncio quando concordam; PATH é a raiz do plugin ou qualquer diretório acima dela, ~/.claude incluído (apenas manifests chamados crapkit contam, e a instalação mais nova vence), e sem PATH ele procura no cache de plugins do Claude Code. Uma raiz que ele encontrou em vez de uma que você digitou é nomeada primeiro, como crapkit doctor: checking PATH. Veja docs/agent-json.md.
inventory [--db PATH] [--export PATH] [--json]Uma passada do lizard sobre cada arquivo no escopo em um snapshot SQLite, cacheado por hash de conteúdo. --db é o único jeito de apontar crapkit para um store fora de .crapkit/, e só este comando o aceita.
coverage [--lane NAME] [--reuse-artifacts] [--reuse-unchanged] [--export PATH] [--sarif PATH] [--github] [--json]Roda as pistas, junta cobertura de branch a um inventário novo, escreve uma execução pontuada. Uma pista com falha é registrada, não fatal: seus escopos caem para no-lane e a execução é tipada partial, então nunca pode servir de baseline. Veja docs/lanes.md.
verify [--baseline ID | --base REF | --baseline-tsv PATH] [--emit-baseline PATH] [--override REASON] [--reuse-artifacts] [--reuse-unchanged] [--no-tighten] [--sarif PATH] [--github] [--json]O veredito completo contra o baseline confiável: portão nas funções tocadas, catraca, sem novas falhas de teste, teto opcional de cobertura de diff. Os três seletores de baseline são mutuamente exclusivos; --baseline ID também ignora a regra de taint (O baseline confiável), e --baseline-tsv lê um arquivo com carimbo de commit para que um clone novo verifique sem store. --no-tighten passa o veredito sem reescrever a catraca. Descobertas que uma árvore suja produziu são marcadas dirty e contadas à parte. Ele lê cada artefato istanbul uma vez para cobertura, linhas mortas e seu digest, e pula a caminhada de artefatos em diff vazio; pular a execução inteira em árvore inalterada foi medido e rejeitado, porque uma chave feita de HEAD mais os nomes sujos não vê uma segunda edição em um arquivo que já estava sujo.
worklist [--top N] [--scope NAME] [--batches N] [--json]O mapa de risco: cada função admitida ranqueada por ccn * churn weight, com piso por worklist_floor, com código simples quente e qualquer coisa acima do teto admitida além desse piso. Ele ranqueia linhas concluídas e linhas no-lane também, marcadas ok e no-lane, então nunca esvazia; next-item carrega a condição de parada. Cada linha carrega o crap e o cov da função da execução ranqueada e seu ratchet_mark quando o arquivo de marcas commitado assina por ele, e o cabeçalho conta as linhas ativas que o teto escondeu: 50 of 3980 active (worklist_top 50). --scope NAME (repetível) é exato, não substring; um nome que nenhum [[scope]] declara é erro de configuração, saída 3, nomeando os escopos declarados. --batches N adiciona uma visão batches[] cortando a lista ativa em no máximo N lotes disjuntos por arquivo com arquivos que mudam juntos mantidos juntos, dos mesmos pares cacheados que coupling lê; as chaves normais permanecem.
next-item [--top N] [--exclude FRAG] [--scope NAME] [--claim]A fila acionável como JSON, com churn, estimativas de orçamento e linhas descobertas. Mesma execução e mesmo piso de admissão que worklist, uma visão diferente dela: linhas no-lane são puladas e contadas em skipped_no_lane, e o que sobra é ranqueado por crap decrescente em vez de por risco, então o item que ele entrega muitas vezes não é a primeira linha da worklist. --exclude FRAG (repetível) pula itens cujo caminho ou nome de função contém FRAG; --scope NAME (repetível) é exato, não substring, e um nome que nenhum [[scope]] declara é erro de configuração, saída 3, nomeando os escopos declarados. --claim guarda o que ele entrega para que uma segunda sessão pule. stale é verdadeiro quando o commit da execução ranqueada não é HEAD, o mesmo campo que worklist carrega. Cada item carrega um handle: o identificador simples, ou (anonymous)#N para uma função sem nome, que é a forma de nome que sobrevive à edição que o item pede.
claims [list | release PATH NAME | release --all] [--json]As reivindicações abertas, e o jeito de devolver uma sem esperar uma verificação. release aceita o identificador simples, o nome longo inteiro, ou o handle sob o qual a reivindicação foi feita, que é o único que seleciona uma única reivindicação (anonymous).
brief FILE NAME [--batch N] [--json]O pacote de início de edição para uma função: seu próprio texto source, toda função no arquivo, a linha pontuada e o teto do escopo, a marca da catraca e no que o portão vai se prender, linhas descobertas, gêmeos de duplicação, churn de arquivo, parceiros de acoplamento, as notas da configuração, e os comandos literais para o resto do loop. Mais handle, remedy e o mesmo est_splits / est_uncovered_paths que a fila imprime, e um commands.refresh que escreve uma execução (refresh_writes_run) em vez de reler a obsoleta. NAME aceita o identificador simples, o nome longo que next-item imprimiu, a linha inicial da função, (anonymous)#N para uma função impressa (anonymous) contando as funções anônimas do arquivo do topo, ou NAME#2 para a segunda de várias funções que um arquivo dá um nome só. --batch N descarta os posicionais e emite packets[] no lugar: o topo N da fila, construído de uma leitura do store e uma passada de duplicação sobre o snapshot para o lote inteiro (lote de 5: 11,8 s para 5,2 s, saída byte-idêntica a cinco chamadas separadas).
explain FILE NAME [--history] [--tests] [--json]A pontuação de uma função entre execuções mais sua marca. NAME resolve exato primeiro: uma função cujo identificador simples ou nome longo é exatamente NAME vence, e só quando nada casa exato ele cai para correspondência de prefixo, então route explica route em vez de todo route_* ao lado. Ele também aceita a linha inicial da função, a forma que brief aceita, que é como você abre uma impressa (anonymous). --history adiciona os commits que a tocaram (git log -L), cada um carregando sua mensagem body, --tests os testes que a cobriram, o que precisa de contextos do coverage.py ligados (receita). --json emite o mesmo conteúdo que um objeto schema 1.
rescore FILE ... [--gate] [--json]Complexidade nova para arquivos nomeados sobre a cobertura obsoleta da execução mais recente, juntada por nome. Uma função em um intervalo de linhas que outra compartilha, e um def Python cujo corpo começa na linha em que sua assinatura termina, pontua não testado, como a execução de cobertura pontua. Aviso: não escreve execução. --gate aplica a política do hook de pré-commit à mesma seleção que o hook usa (funções que a árvore mudou desde HEAD), menos funções cujo CRAP está no nível ou abaixo da marca da catraca, e sai com 6. Uma função marcada além da marca é portada; o hook de pré-commit isenta pela existência da marca em vez disso, porque um blob staged não tem cobertura para pontuar.
ratchet seed | prune | merge | move | report [--baseline ID] [--enforce] [--json]O ciclo de vida da marca: semear dívida nova, podar código sumido (uma marca cujo arquivo o git renomeou a segue), mesclar como driver git, mover marcas de re-caminhos, relatório lê burn-down do histórico git do próprio arquivo. seed e prune aceitam --baseline ID para ler uma execução nomeada em vez da escolha do verify, recusado pelas razões que verify --baseline recusa uma. Veja docs/ratchet.md.
runs [list | prune [--keep N]] [--json]Histórico de execuções, e retenção. list marca a execução que verify compara contra hoje baseline, e imprime verdict=- para uma execução que não produz veredito em vez de uma que falhou. Veja O baseline confiável. --keep (padrão 5) é um piso nas execuções confiáveis mais novas, não um teto: o par de digest, todo baseline de verify que passa, toda execução que um override nomeia, e a execução não-hook mais nova também são mantidos. prune roda VACUUM depois.
overrides [--json]A trilha de auditoria de overrides: quem concedeu o quê, quando, e por quê.
trend [--json]Totais por execução confiável: funções, contagem acima do alvo, carga CRAP, média, resumo por escopo. Ele lê uma tabela de resumo por execução em vez de reescanear cada linha pontuada, e preenche essa tabela para qualquer execução sem uma, então escreve no store (melhor esforço: um .crapkit/ somente leitura custa a velocidade, não o comando).
digest [--alert]O delta entre as duas execuções mais novas com conjuntos de pistas idênticos. Silencioso quando nada mudou. --alert canaliza o corpo para alert_command no stdin. Linhas simples, nunca JSON.
report [--out PATH]Uma página HTML autocontida escrita em .crapkit/report.html (ou --out PATH, relativo ao repositório, ou um caminho absoluto que você nomear), com o caminho impresso no stdout. Ela renderiza o que worklist --json e trend --json já respondem nos padrões: a worklist ranqueada limitada a worklist_top, as notas por escopo da execução mais nova, a série de tendência, e um banner nomeando toda pista obsoleta. Ela não mede nada e não abre conexão de rede. Cada linha carrega o CRAP e a cobertura da função, e imprime a chamada crapkit explain para o resto: linhas escuras, histórico, a marca. Ela lê os mesmos resumos por execução que trend lê, e os escreve nos mesmos termos.
duplication [--min-lines N] [--similarity F] [--top N] [--json]Funções quase duplicadas por shingles de linha normalizados com pontuação de contenção. Padrões: --min-lines 8, --similarity 0.8, --top 50. Empates têm ordem estável entre seeds de hash. Um --top positivo limita candidatos retidos e saída; entradas densas ainda exigem comparações de pares. Uma função e seu closure aninhado nunca formam par.
coupling [--min-support N] [--min-confidence F] [--top N] [--json]Pares de arquivos que continuam caindo nos mesmos commits. Padrões: --min-support 5 commits compartilhados, --min-confidence 0.5 proporção máxima de direção, --top 50. Commits em massa nunca acoplam pares, e um repositório jovem não retorna nada no suporte padrão. Os pares classificados são armazenados em cache em .crapkit/coupling-cache-v1.json, com chave baseada em HEAD, na janela de churn, na data UTC de hoje, no formato de caminho e em um resumo do conjunto rastreado, e compartilhados com brief e worklist --batches (quente: 1,05 s a 0,11 s em um repositório de 72 mil commits). A data faz parte dessa chave, então a primeira execução após a meia-noite UTC reconstrói os pares em um HEAD inalterado. --top lê o cache, porque ele trunca essa mesma ordem; --min-support ou --min-confidence fora de seus padrões fazem uma pergunta mais ampla do que o arquivo responde, então eles o ignoram e recalculam.
mutate [--files F ...] [--max-mutants N] [--drop-pool] [--json]Teste de mutação com escopo de diff: inverte comparações, mudanças de limite, conectivos booleanos e literais booleanos em linhas alteradas, executa mutation_command por mutante, lista sobreviventes. --files substitui o escopo de diff pelo arquivo inteiro. Ambas as listas passam primeiro pelo corpus pontuado, o mesmo predicado que coverage usa (escopos, exclusões, o corte de arquivos de teste, max_file_bytes): um arquivo de teste, um caminho excluído, um arquivo acima de max_file_bytes ou um arquivo que nenhum escopo reivindica é nomeado em stderr e nunca mutado, --json o lista sob outside_corpus, e quando nada resta, stdout diz nothing to mutate no exit 0 sem iniciar a suíte. --max-mutants (padrão 100) limita a execução e o aviso de limite vai apenas para stderr, então mutants em --json é a contagem limitada. Arquivos Shell e PowerShell são recusados por nome em stderr em vez de mutados: < e > são redirecionamentos lá, não comparações. Cada worker usa uma worktree mantida, incluindo um; veja worktrees de mutação. --drop-pool os remove e sai.
test-scoped FILE ...Executa o template [crapkit.scoped_tests] de cada escopo proprietário nos arquivos (citados, o escopo de prefixo mais longo vence). Um template sem {files} é executado como escrito, que é como um escopo cujos testes vivem fora de seus próprios caminhos executa sua suíte inteira. Apenas código de saída; um runner não zero sai com 1.
hook-precommit [--base REF]O gate somente de cc em blobs staged. Sem cobertura, sem snapshot, sem cache em todo o repositório. Exit 6 em uma violação. --base REF compara o índice com a base de merge de REF e HEAD, a forma que um checkout de CI executa.
claude-hook [--protocol N]Lê um payload PostToolUse do Claude Code de stdin e julga o arquivo que editou: ccn contra o teto do escopo, em funções que a edição mudou, menos funções que uma marca de catraca já cobre. Apenas consultivo: a edição já foi aplicada, e hook-precommit continua sendo o ponto de aplicação. Exit 2 e um aviso em stderr é a única coisa que ele sempre diz, um bloco por arquivo julgado (uma linha de cabeçalho, uma linha por função violadora, uma linha de fechamento): sem crapkit.toml acima do arquivo editado, um arquivo sem escopo, em meio a rebase ou merge, um --protocol diferente de 1, código-fonte que analisa para nenhuma função, ou qualquer falha interna todos saem com 0 em silêncio. A raiz é o primeiro crapkit.toml acima do arquivo editado; a caminhada para em uma entrada .git, então uma worktree nunca empresta a configuração do pai. Um evento Bash não nomeia nenhum arquivo, então ele julga a árvore de trabalho em vez disso: os arquivos *.py sujos ou não rastreados tocados nos últimos 12 segundos, no máximo 25, cada um através da mesma escada, e silêncio para uma árvore limpa ou um cwd fora de qualquer repositório. Essa metade dispara apenas onde você registra um matcher Bash (O plugin do Claude Code). Ele não abre snapshot e não escreve nada.
watch [--interval SECONDS] [--cycles N]Reclassifica arquivos rastreados conforme eles mudam (polling de mtime, padrão 2s, isolado por subprocesso para que um erro de sintaxe meio salvo nunca mate o watcher). --cycles N faz polling exatamente N vezes e sai com 0; sem ele, o loop roda até ctrl-c.
help [TOPIC]A ajuda à qual git, npm e docker respondem. Sem TOPIC, imprime a lista de comandos; com um, imprime a ajuda do próprio subcomando, a mesma página que crapkit TOPIC --help. Um TOPIC que não nomeia nenhum subcomando sai com 3.
mcpUm servidor MCP stdio sem dependência extra, expondo doze ferramentas de leitura chamadas verb_noun, cada uma com um título e esquema de saída. As ferramentas chamam o CLI para inspecionar pontuações atuais, gates de arquivo-fonte e de arquivo editado. Elas não fazem reivindicações e não executam verificação; chamadas podem escrever caches ou armazenar metadados. Veja o contrato e a configuração do MCP.

Lendo a saída

Flags: por que um número de cobertura está ausente

FlagSignificadoPontuado
measuredUm artefato de lane falou sobre esta função.Real cov.
untestedUm lane cobre o escopo, mas seu artefato é silencioso sobre esta função, o que normalmente significa que nenhum teste importa o arquivo.cov = 0. Uma lacuna de teste, e uncovered_lines volta null porque nenhum artefato pode nomear linhas que nunca viu.
no-laneA lista de scopes de nenhum lane nomeia o escopo desta função.cov = 0. Uma lacuna de ferramenta, não uma lacuna de teste. next-item nunca distribui uma e as conta em skipped_no_lane; worklist as classifica e marca a linha no-lane, porque uma lacuna de conexão é um risco que você precisa enxergar.
cc-onlyO escopo define coverage_optional = true, então nenhum número de cobertura pode existir.crap = ccn, e remedy só pode ser ok ou decompose. uncovered_lines volta null com uma nota nomeando essa configuração.

O resumo de cobertura conta todos os quatro como measured / untested / no_lane / cc_only.

Remédio: o que fazer a respeito

RemédioCondiçãoAção
decomposeccn > ceilingDivida. Nenhuma quantidade de cobertura resolve isso.
split-linesccn <= ceiling, crap > ceiling, e outra função compartilha suas linhas de código, ou o corpo de um def Python começa na linha em que sua assinatura terminaColoque cada definição em suas próprias linhas e meça novamente. A cobertura não consegue distinguir funções em uma única linha, então a pontuação permanece como não coberta, independentemente do que os testes façam. Um def Python de uma linha compartilha sua linha com a instrução def, que é executada na importação, então coverage.py não consegue mostrar uma chamada. O mesmo ocorre com um corpo na última linha de uma assinatura que se estende por várias linhas, ou um que continua a partir da linha dos dois-pontos dentro de colchetes ou após uma barra invertida: mova o corpo para sua própria linha após a assinatura.
add-testsccn <= ceiling e crap > ceilingCubra os ramos.
okcrap <= ceilingNada.

Nota e carga CRAP

A nota é a parcela de funções acima do teto: A+ em exatamente zero, A abaixo de 2%, B abaixo de 5%, C abaixo de 10%, D abaixo de 20%, F em 20% ou mais. crap_load ao lado é a soma simples da pontuação CRAP de cada função, então ela se move quando uma função melhora, mesmo que a letra não mude.

Risco: o que classifica a lista de trabalho

risk = ccn * churn weight. O peso é uma soma ponderada no tempo dos commits do arquivo na janela de churn: cada commit contribui com um peso logístico que sobe para 0,5 para o commit mais recente no log e cai para perto de zero para o mais antigo, então cinco edições no mês passado superam cinquenta de dois anos atrás. A janela ancora no commit mais recente, nunca no relógio de parede, então uma árvore fixa é classificada de forma idêntica para sempre.

A idade não é a entrada, a posição no log é. Um log cujos commits compartilham um único timestamp não tem intervalo para ponderar, então cada commit conta uma vez: um repositório de um commit pesa cada arquivo em 1,0, classifica por ccn e não promove nada abaixo do piso, porque um top 10% de pesos iguais seria todos os arquivos. Commits com minutos de diferença já classificam. Este repositório tinha oito commits, todos feitos no mesmo dia:

$ crapkit worklist --scope util
worklist @ a7c5c85ac37 (run 1, floor ccn>=5, churn 12mo) - 3 of 3 active (worklist_top 50), 0 dormant
  risk      5.4  ccn   5  crap    30.0  cov   0%    5c/1a  util/stats.py:1  bucket( value , low , high )
  risk      4.5  ccn   9  crap    90.0  cov   0%    1c/1a  util/curve.py:1  curve( scores , mode , floor , ceiling , skip_none )
  risk      4.3  ccn   4  crap     4.2  cov  75%    5c/1a  util/stats.py:13  spread( values , cap )  ok

bucket em ccn 5 supera curve em ccn 9 porque cinco commits o tocaram e um tocou curve. Esse é exatamente o ponto de ponderar por churn. spread carrega o marcador ok: já está no teto ou abaixo dele, listado mesmo assim, e next-item não o distribuiria.

A lista se divide em duas: ativos (arquivos com commits na janela) e dormentes (churn zero, mantidos fora da fila, mas contados). Duas regras alcançam abaixo do worklist_floor. Um arquivo cujo peso de churn está no top 10% é promovido para ccn 3, que é por que spread aparece acima em ccn 4. E uma função acima do teto é admitida qualquer que seja seu ccn, então o piso nunca pode segurar dívida.

A linha de base confiável

Cada verify mede a árvore de trabalho contra uma execução anterior, a linha de base confiável. crapkit runs list marca qual é essa hoje.

Quais execuções se qualificam. Uma execução coverage, ou um verify que passou. Um verify falho nunca se qualifica, e também não se qualifica uma execução partial (um lane falhou, então algum escopo caiu para no-lane) nem um registro de substituição hook, que não carrega linhas pontuadas. Em runs list, verdict=- marca uma execução que não produz veredito, em vez de uma que falhou: apenas verify gera um veredito. Quatro leitores fazem esta única pergunta e obtêm esta única resposta: a seleção de linha de base aqui, ratchet seed, prune, e o amortecimento de aperto que compara uma marca contra a execução anterior do mesmo commit. Uma marca não pode mais ser aprovada em uma execução que verify recusou.

O que a avança. Qualquer execução qualificada. coverage escreve uma onde quer que HEAD esteja, então um cron de painel avança a linha de base exatamente como CI faz. Um verify que passa a avança e aperta a catraca no caminho.

A regra de contaminação. Um verify falho registrou descobertas contra uma árvore. Até que algum verify passe, execuções feitas após essa falha não se tornam a linha de base: escolher uma moveria o ponto de comparação além das descobertas, a função sinalizada pararia de contar como tocada, e nada olharia para ela novamente. verify diz qual execução ela recusou e cai para a execução mais recente antes da falha.

$ crapkit runs list
run   1 @ 88012a148f6 2026-08-23T09:27:46Z coverage  verdict=-      lanes=py  baseline
run   2 @ 803bdde8556 2026-08-23T09:27:53Z verify    verdict=FAILED lanes=py
run   3 @ 803bdde8556 2026-08-23T09:28:02Z coverage  verdict=-      lanes=py

$ crapkit verify
warning: run 3 is not the baseline: verify run 2 FAILED with 1 finding(s) and no passing verify has cleared it since — measuring against run 1 @ 88012a148f6 instead, so those findings stay visible. Fix them, or pass `--baseline 3` to accept the newer run deliberately.
verify FAILED @ d89068de7f3 vs baseline 88012a148f6 (2 changed files)
  GATE  crap     72.0  ccn   8 cov 0%  calc/legacy.py:7  legacy_router( a , b , c , d , e )  -> decompose
  findings: 1 committed / 0 dirty (uncommitted edits and untracked files)

A execução 3 é uma execução coverage que alguém fez na árvore que a execução 2 recusou, e ela pontua a mesma função ccn-8. Sem a regra, ela teria se tornado a linha de base, legacy_router teria deixado de ser uma função tocada, e essa linha de portão nunca mais seria impressa.

A saída, duas vezes. Corrija as descobertas e deixe um verify passar, o que limpa a contaminação para sempre. Ou aceite a execução mais recente de propósito com verify --baseline 3: um id explícito ignora a regra, e o histórico de execuções registra qual execução o veredito usou. Nada aqui toca um repositório que nunca executou verify: sem falha para proteger, coverage sozinho sempre avança a linha de base.

ratchet seed --baseline ID e ratchet prune --baseline ID usam o mesmo nome que verify --baseline ID, admitidos pela mesma regra. Quando uma verificação falha fixa a seed em uma execução que ela não pode ler ou assinar, essa é a saída (docs/ratchet.md).

Quando o id que você passa não pode servir. Um --baseline ID nomeando uma execução real que não é uma candidata diz qual execução é, por que, e quais podem:

$ crapkit verify --baseline 3
crapkit: run 3 is an inventory run (no coverage was measured) and cannot serve as a baseline; trusted runs: 1, 2; pass `--baseline 2` for the newest

Códigos de saída

CódigoSignificado
0OK. Para verify e hook-precommit: o portão passou.
1Sobrecarregado. Três coisas não relacionadas, listadas abaixo da tabela.
2Erro de uso do argparse: flag desconhecida, posicional ausente. Levantado antes do tratamento de erro do próprio crapkit.
3Erro de configuração: crapkit.toml ausente ou não analisável, uma linguagem ou parser desconhecido, um comando de lane que o shell que o executa lê como um conjunto estreitado, uma incompatibilidade de métrica-stamp da catraca (Atualizando da 0.4.4), um arquivo test-scoped sob nenhum escopo ou sob um escopo sem template.
4Erro de Git: não é um repositório, um commit de linha de base reescrito fora do histórico.
5Erro de ferramenta: lizard não importável, um lane que não produziu artefato, um que mediu uma árvore diferente, um que mediu esta árvore e a relatou em caminhos absolutos (a junção é relativa à raiz, então esses também não correspondem a nada; a recusa nomeia o próprio switch do executor, relative_files = true sob [tool.coverage.run] para um lane coveragepy, a opção cwd/root do reporter para um istanbul), um lane que expirou além de suas tentativas, um comando de alerta de substituição que falhou. Um timeout_seconds mata toda a árvore de processos, então nenhum conjunto órfão continua rodando atrás da falha.
6Violação de portão. Uma função que o diff tocou está acima do teto e além de qualquer marca de catraca que carrega: uma edição que deixa uma função marcada no teto ou abaixo dele é a dívida que o repositório assinou e é isenta. Também rescore --gate, que aplica a mesma regra, e hook-precommit, que isenta pela existência da marca em vez disso.
7Regressão de catraca que o diff nunca tocou. Uma função marcada pontua pior que sua marca de nível alto registrada; uma tocada além de sua marca reporta 6.
8Novas falhas de teste contra a execução de linha de base. Falhas que a linha de base já tinha não contam.
9Teto de cobertura de diff violado: diff_uncovered_max está definido e mais linhas alteradas do que isso nunca foram executadas. Um arquivo alterado que nenhum artefato de lane menciona conta cada linha de suas funções.

Saída 1 significa uma de três coisas

CI não consegue distinguir uma falha de um veredito de política limpo apenas pelo código. Qual você obteve depende do comando:

ComandoO que a saída 1 significa
doctorUma descoberta FAIL. Isso é um veredito, não uma falha. Um WARN (um diretório não medido, ou um lane escrevendo seu artefato na raiz do repositório) e um note (um arquivo acima de max_file_bytes, ou nenhum lane declarado) ambos saem com 0.
ratchet report --enforceA política de dívida foi violada. Também um veredito.
qualquer outra coisaUm erro inesperado: "sem snapshot ainda, execute crapkit coverage primeiro", um nome brief que não corresponde a nenhuma função, um executor test-scoped que saiu com código não zero.

verify reporta a primeira de 6, 7, 8, 9 que dispara, nessa ordem. Uma violação de portão e uma regressão de catraca juntas reportam 6. Uma execução que leva qualquer uma delas falha, então ela nem avança a linha de base nem aperta a catraca, incluindo a saída 9.

Início rápido: Python

Um repositório com calc/grade.py, tests/test_grade.py, e um pyproject.toml. Faça commit primeiro; crapkit lê git ls-files. Instale o plugin de cobertura primeiro, porque o lane init escreve execuções pytest --cov e essas flags vêm de pytest-cov:

pip install pytest-cov

(pip install "crapkit[py]" puxa ambos de uma vez quando crapkit compartilha o venv do conjunto.)

Se seu conjunto dirige seu próprio CLI através de subprocess.run, adicione [tool.coverage.run] patch = ["subprocess"] to pyproject.toml and keep coverage>=7.10.6: pytest-cov 7.0.0 removeu a medição de subprocesso, então sem essa chave cada ponto de entrada pontua 0% e nada avisa. docs/lanes.md tem a regra completa.

1. Estruturar a configuração

$ crapkit init
wrote crapkit.toml with 1 scope(s): calc
detected 1 lane(s) from this repo's own files: py - next: run `crapkit coverage`
added to .gitignore: .crapkit/, .coverage, __pycache__/

init fareja o código-fonte rastreado em um escopo por diretório de origem de nível superior, e detecta um lane de cobertura a partir do que o repositório já tem: um arquivo de marcador pytest (pyproject.toml, pytest.ini, setup.cfg) escreve um [[lane]] ao vivo, e também um script test ou vitest/jest em package.json. Um arquivo de bloqueio ao lado deles nomeia o ambiente: uv.lock, poetry.lock, pdm.lock ou Pipfile.lock torna o lane uv run python -m pytest … (e o run correspondente para o resto), porque um python puro se liga a qualquer venv que o shell tenha ativo, em vez do que o repositório fixa — veja O interpretador ao qual um lane se liga. Qualquer que seja o que detecta, também deixa templates comentados para os executores que não encontrou, e esses carregam o mesmo lançador, então descomentar um não pode devolver o python puro. Cada lane que escreve reporta em .crapkit/cov/, que é por que a lista .gitignore é tão curta: veja Onde os artefatos vivem.

[crapkit]
target = 6

[[scope]]
name = "calc"
paths = ["calc"]
languages = ["python"]

[exclude]
# A leading **/ matches zero or more directories, so each glob below reaches the
# repo root and every nested copy. Test directories leave the corpus on their own.
globs = [
  "**/node_modules/**",
  "**/dist/**",
  "**/build/**",
  "**/vendor/**",
  "**/generated/**",
  "**/__generated__/**",
  "**/*.generated.*",
  "**/*.test.*",
  "**/*.spec.*",
  "**/test_*.py",
  "**/*_test.py",
  "**/conftest.py",
  "**/*_test.go",
  "**/*.config.ts",
  "**/*.config.js",
  "**/*.config.mts",
]

[[lane]]
name = "py"
command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json --junitxml=.crapkit/cov/junit-py.xml --continue-on-collection-errors"
artifact = ".crapkit/cov/py.json"
results_artifact = ".crapkit/cov/junit-py.xml"
parser = "coveragepy"
scopes = ["calc"]

# Declare one [[lane]] per coverage command, then run `crapkit coverage`.
# [[lane]]
# name = "js"
# command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml"
# artifact = ".crapkit/cov/js/coverage-final.json"
# results_artifact = ".crapkit/cov/js/junit.xml"
# parser = "istanbul"
# scopes = ["<your-scope>"]

# `crapkit test-scoped FILES` runs one command per scope, with {files}
# replaced by that scope's files, each quoted; a template with no {files}
# runs as written, which is how a scope whose tests live elsewhere runs them.
[crapkit.scoped_tests]
# calc: no test file under calc/, so the whole suite runs, from tests/
calc = "python -m pytest tests -q -p no:cacheprovider"

O último bloco é o que um loop de agente precisa. crapkit test-scoped sai com código 3 para um arquivo cujo escopo não declara nenhum template, e AGENTS.md o torna o passo 4 do loop de redução. Cada chave está em docs/configuration.md.

2. Verifique a configuração contra o repositório

$ crapkit doctor
resources: up to 8 analysis worker(s) per pool, 8 shared slot(s); lane log limit 16777216 bytes per file
ok   config keys all recognized
ok   scope 'calc': 1 file
ok   every tracked source file belongs to a scope
ok   1 lane(s) declared
ok   lane 'py': python -> /home/you/ledger/.venv/bin/python (pytest 8.3.3, pytest-cov 7.1.0)
ok   lizard 1.24.0
doctor: no problems found

doctor imprime uma linha por verificação e sai com código 1 apenas em um FAIL. WARN e note relatam e saem com código 0.

3. Pontue o repositório e leia a fila

$ crapkit coverage
run 1 @ fae4db93108: 2 functions scored: 2 measured, 1 over ceiling 6, CRAP load 41.0, grade F
-> next: crapkit worklist

$ crapkit worklist
worklist @ fae4db93108 (run 1, floor ccn>=5, churn 12mo) - 1 of 1 active (worklist_top 50), 0 dormant
  risk     14.0  ccn  14  crap    38.5  cov  50%    1c/1a  calc/grade.py:7  classify( score , attempts , late , bonus )

Colunas: risk, ccn, o crap e o cov da função na execução classificada (- em uma execução somente de inventário), <commits>c/<authors>a na janela de churn, path:line, o nome longo da função e, em seguida, um marcador nas linhas que a fila de redução não distribuirá (ok, no-lane). O cabeçalho conta as linhas ativas em relação ao total, então 50 of 3980 active (worklist_top 50) says what the cap hid, and reads (--top N) quando a flag define o limite. --json também carrega ccn_std, weight e ratchet_mark.

worklist é o mapa de risco, não uma lista de tarefas. Ele também classifica linhas concluídas, então não esvazia quando a redução termina. next-item é a outra visão dessa execução: ela remove as linhas no-lane, classifica por crap, e seu empty: true é a condição de parada.

4. Pegue o item do topo

$ crapkit next-item
{"commit": "fae4db93108b4841a00959f9117430679e7250ca", "empty": false, "item": {"authors": 1, "ccn": 14, "ccn_std": 14, "cognitive": 13, "commits": 1, "cov": 0.5, "crap": 38.5, "end": 28, "est_splits": 3, "est_uncovered_paths": 7, "flag": "measured", "function": "classify( score , attempts , late , bonus )", "handle": "classify", "nesting": 3, "nloc": 22, "occurrence": 1, "path": "calc/grade.py", "remedy": "decompose", "scope": "calc", "start": 7, "target": 6, "uncovered_lines": [9, 11, 15, 17, 19, 24, 25, 26, 27, 28]}, "run_id": 1, "schema": 1, "skipped_no_lane": 0, "stale": false}

remedy: "decompose", est_splits: 3 (isso precisa de aproximadamente três partes para caber abaixo de 6), e uncovered_lines nomeando as dez linhas que nenhum teste percorre. handle é a forma de nome a passar de volta, e stale: false diz que a execução ainda descreve o HEAD. Cada campo está em docs/agent-json.md.

5. Semeie a catraca

Arme o portão de dívida antes de corrigir qualquer coisa. ratchet seed registra cada função acima do alvo com sua pontuação atual, e a partir daí nada pode piorar.

$ crapkit ratchet seed
crapkit-ratchet.tsv: added 1, tightened 0 - 1 mark(s) vs run 1 (fae4db93108)

$ git add crapkit.toml crapkit-ratchet.tsv .gitignore && git commit -m "adopt crapkit"

6. Corrija e verifique

Extraia até que cada peça fique no teto ou abaixo dele. Aqui classify se tornou _validate, _adjusted, _band e um classify que apenas os sequencia, com a tabela de casos empurrada para testes parametrizados. Faça o commit da correção e então:

$ crapkit verify
verify OK @ 8d10c13303d vs baseline fae4db93108 (5 changed files) ratchet: 1 dropped, 0 tightened -> git add crapkit-ratchet.tsv

$ crapkit coverage
run 3 @ 8d10c13303d: 5 functions scored: 5 measured, 0 over ceiling 6, CRAP load 19.0, grade A+
-> next: crapkit worklist

Carga CRAP de 41.0 para 19.0, nota F para A+. verify reexecuta as pistas e verifica três coisas contra a linha de base confiável: cada função que o diff tocou está no teto ou abaixo dele, nenhuma função marcada piorou e nenhum teste que passou na linha de base falha agora. A saída 0 avança a linha de base e aperta crapkit-ratchet.tsv no lugar, então a marca quitada sai do arquivo: acompanhe com git commit -am "ratchet: classify repaid". O ciclo de vida completo da marca está em docs/ratchet.md.

crapkit next-item agora retorna empty: true com um objeto reasons dizendo qual final você obteve. Isso é a maior parte da condição de parada, não toda ela: AGENTS.md declara a regra completa e lê o resto de reasons.

Início rápido: TypeScript

Um repositório vitest com src/grade.ts e test/grade.test.ts.

1. Estruture a configuração

$ crapkit init
wrote crapkit.toml with 1 scope(s): src
detected 1 lane(s) from this repo's own files: js - next: run `crapkit coverage`
added to .gitignore: .crapkit/

A pista que init escreveu é npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml. Ela lê o reporter json do vitest de .crapkit/cov/js/coverage-final.json; a flag reportsDirectory é o que mantém esse relatório fora da sua raiz. A metade junit é o results_artifact da pista, que as verificações de worker travado e sem novas falhas leem; ambos os reporters são nomeados porque --reporter=junit sozinho substituiria a saída de console que você assiste durante a suíte. Qualquer coisa que produza um coverage-final.json do istanbul funciona; veja docs/lanes.md para as receitas jest e pytest, um pacote um diretório abaixo e uma raiz crapkit abaixo do topo do repositório.

init escreve a linha vitest sob [crapkit.scoped_tests] comentada, porque ela não consegue ver qual configuração vitest uma execução com escopo de arquivo precisa. Até você descomentar essa linha ou escrever a sua própria, crapkit doctor avisa com WARN scope 'src' has a lane but no [crapkit.scoped_tests] template. The WARN leaves doctor's exit at 0; it means crapkit test-scoped exits 3 on files under src/.

2. Instale um provedor de cobertura

Este é o passo que impede a maioria dos usuários de TypeScript. O vitest não inclui provedor de cobertura por padrão. Sem um, init e doctor ficam ambos felizes e coverage morre com código de saída 5:

$ crapkit coverage
crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/coverage-final.json (command exit 1); lane log: /repo/.crapkit/lane-js.log; last output: $ npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml

 MISSING DEPENDENCY  Cannot find dependency '@vitest/coverage-v8'

(exit 1)
crapkit: every lane failed (1 of 1); the errors are above

Essa falha não escreve nenhuma execução. Todas as pistas falharam, então coverage sai antes de abrir um armazenamento: ainda não há .crapkit/crap.sqlite e os ids de execução abaixo ainda começam em 1.

Instale o provedor e fixe a versão principal você mesmo. Sem fixar, o npm resolve o provedor mais novo contra seu vitest mais antigo e recusa a árvore:

npm i -D "@vitest/coverage-v8@<your vitest major>"
PerguntaResposta
Qual provedor?Qualquer um funciona. @vitest/coverage-v8 é o padrão do vitest e não precisa de configuração. @vitest/coverage-istanbul também funciona e precisa de coverage.provider = "istanbul" na sua configuração vitest.
Qual parser crapkit?Ambos alimentam parser = "istanbul". O nome do provedor e o nome do parser não estão relacionados: a saída v8 é remapeada para o esquema JSON do istanbul antes de ser escrita.
Qual versão?A versão principal do provedor precisa corresponder à do vitest. No vitest 2, é npm i -D "@vitest/coverage-v8@2"; no vitest 3, npm i -D "@vitest/coverage-v8@3". Remova a fixação e o npm responde ERESOLVE unable to resolve dependency tree, nomeando o peer que não conseguiu satisfazer.

O artefato que o crapkit quer é coverage-final.json, escrito pelo reporter de cobertura json do vitest, que está ativado por padrão. Se sua configuração vitest define coverage.reporter explicitamente, mantenha "json" na lista.

O vitest não escreve nenhum relatório de cobertura quando a execução falha. A pista que init escreveu já carrega --coverage.reportOnFailure, então um teste vermelho ainda produz o artefato. Se você escrever a pista manualmente, ou preferir manter a chave ao lado de suas outras configurações de cobertura, coverage.reportOnFailure = true na configuração vitest faz o mesmo trabalho; qualquer um dos dois é suficiente. O bloco completo está em docs/lanes.md.

3. Pontue o repositório

$ crapkit coverage
run 1 @ 8bfbe613fcd: 2 functions scored: 2 measured, 1 over ceiling 6, CRAP load 56.68, grade F
-> next: crapkit worklist

$ crapkit worklist
worklist @ 8bfbe613fcd (run 1, floor ccn>=5, churn 12mo) - 1 of 1 active (worklist_top 50), 0 dormant
  risk     15.0  ccn  15  crap    52.4  cov  45%    1c/1a  src/grade.ts:8  classify ( row Row )

classify é ccn 15 contra um teto de 6: uma função segurando a penalidade de atraso-e-tentativa, as faixas de letras, a regra de rebaixamento e o caso nulo.

4. Semeie a catraca e faça o commit

ratchet seed registra cada função acima do alvo com a pontuação que ela tem hoje, então nada pode piorar enquanto você reduz esta.

$ crapkit ratchet seed
crapkit-ratchet.tsv: added 1, tightened 0 - 1 mark(s) vs run 1 (8bfbe613fcd)

$ git add crapkit.toml crapkit-ratchet.tsv .gitignore && git commit -m "adopt crapkit"

5. Corrija

Acima do teto, a cobertura não pode ajudar, então classify é dividida em vez de testada. penalty, band e demote saem como suas próprias funções exportadas, e classify mantém o caso nulo e o bônus:

export function classify(row: Row): string {
  if (row.score === null) {
    return "N/A";
  }
  let score = row.score - penalty(row.attempts, row.late);
  if (row.bonus && score < 90) {
    score += 3;
  }
  return demote(band(score), row);
}

rescore --gate julga essa edição apenas pela complexidade, antes do passo lento:

$ crapkit rescore src/grade.ts --gate
rescore vs run 1 @ 8bfbe613fcd (coverage STALE, complexity fresh)
   ccn   cov     crap  remedy      function
     5    0%     30.0  add-tests   src/grade.ts:22  band ( score )
     5    0%     30.0  add-tests   src/grade.ts:38  demote ( letter , row Row )
     4    0%     20.0  add-tests   src/grade.ts:8  penalty ( attempts , late )
     4   45%      6.7  add-tests   src/grade.ts:48  classify ( row Row )
     4   75%      4.2  ok          src/grade.ts:59  average ( scores Array )
gate: 4 changed function(s) judged, 0 over ceiling 6

Saída 0 e a linha gate:: cada peça alterada está em 6 ou abaixo. A coluna crap está alta porque sua metade de cobertura ainda é a da execução 1, de antes de três dessas funções existirem, e add-tests é a instrução literal para o passo 6.

6. Cubra as novas peças

rescore --gate passou pela complexidade, não pela cobertura. penalty, band e demote são três funções que nenhum teste jamais chamou, então cada uma recebe um teste de tabela:

describe("band", () => {
  it.each([[95, "A"], [85, "B"], [75, "C"], [65, "D"], [10, "F"]])(
    "scores %i as %s", (score, expected) => expect(band(score)).toBe(expected));
});

Execute a suíte uma vez antes do passo lento:

$ npx vitest run
 Test Files  1 passed (1)
      Tests  21 passed (21)

Pule este passo e o passo 7 falha em vez de passar. Execute em uma cópia deste repositório com o passo 6 omitido, verify reexecuta as pistas contra a árvore real e três funções que a suíte antiga nunca chamou voltam acima do teto:

$ crapkit verify
verify FAILED @ 0296156ff21 vs baseline 0e646697946 (1 changed files)
  GATE  crap     17.8  ccn   5 cov 20%  src/grade.ts:38  demote ( letter , row Row )  -> add-tests
  GATE  crap     12.4  ccn   5 cov 33%  src/grade.ts:22  band ( score )  -> add-tests
  GATE  crap     10.8  ccn   4 cov 25%  src/grade.ts:8  penalty ( attempts , late )  -> add-tests

7. Verifique

$ crapkit verify
verify OK @ 2af3433d979 vs baseline 8bfbe613fcd (3 changed files) ratchet: 1 dropped, 0 tightened -> git add crapkit-ratchet.tsv

$ crapkit coverage
run 3 @ 2af3433d979: 5 functions scored: 5 measured, 0 over ceiling 6, CRAP load 22.0, grade A+
-> next: crapkit worklist

Carga CRAP de 56.68 para 22.0, nota F para A+, e a marca semeada no passo 4 sumiu: verify a removeu assim que classify pontuou abaixo do teto, reescrevendo o crapkit-ratchet.tsv rastreado no lugar. Faça o commit junto com sua alteração. Marcas só caem.

Uma verificação também pode imprimir warning: N changed line(s) have no coverage acima de seu veredito; esse bloco é apenas informativo, a menos que diff_uncovered_max esteja definido (docs/configuration.md). Ele imprime warning: N function(s) over the ceiling carry no ratchet mark quando a árvore contém dívida que ratchet seed nunca assinou: o portão julga apenas funções tocadas e a verificação da catraca compara apenas marcas, então a perda de cobertura em tal função passaria despercebida. A contagem é unmarked_over_target em --json, não dispara nenhum código de saída e é zero em um repositório sem dívida.

Documentação

PáginaCobre
O manualComece aqui para qualquer coisa mais profunda. O manual ilustrado: o que é crapkit, como cada peça funciona e onde cada comando ganha seu lugar. Também em docs/handbook.html, autocontido, então abre direto de um clone.
docs/adoption.mdA camada de julgamento sobre os inícios rápidos: granularidade de escopo, exclude vs lane, fiação scoped_tests, o perigo de contaminação na primeira verificação.
docs/configuration.mdCada chave crapkit.toml: tipo, padrão e o que faz.
docs/lanes.mdO modelo de pistas, receitas vitest, jest e pytest, reutilização de artefatos, reteste de flake, contêineres.
docs/resources.mdOrçamentos de worker, limpeza de comandos, rotação de logs e limpeza segura.
docs/ratchet.mdSemeadura, poda, o driver de merge do git, carimbos de métrica, política de dívida, substituições.
docs/upgrading.mdInstalações existentes: análise e versões-chave, estado salvo, alinhamento de plugins e atualizações no Windows.
docs/portable-records.mdExportações sem perda, linhas de base e catracas portáteis, incluindo nomes de arquivo com delimitadores.
docs/agent-json.mdA superfície de máquina: schema, cada campo de payload, exemplos reais capturados.
docs/comparison.mdOnde crapkit se posiciona ao lado de radon, xenon, wily, coverage.py e SonarQube, e como eles rodam juntos.
AGENTS.mdO loop de redução que um agente executa e as regras para alterar o próprio crapkit.
plugin/Três habilidades e o servidor MCP para Claude Code e Codex, com instruções de hook PostToolUse consultivas para Claude Code.

crapkit.schema.json é a autoridade sobre a forma do arquivo de configuração.

Desenvolvimento

pip install -e ".[dev]"
git config core.hooksPath git-hooks
python tools/testing/run.py

O extra de desenvolvimento inclui pytest, pytest-cov, pytest-xdist e coverage.py. O runner compartilhado é dono do cronograma de testes unitários e E2E; use --unit-workers 1 para reprodução unitária serial ou --coverage para cobertura de branch combinada e JUnit. A linha git config arma o portão de complexidade. Veja CONTRIBUTING.md para desenvolvimento e o relatório de implementação verificado para resultados completos de fonte Windows e wheel Linux, benchmarks focados e seus limites.

Histórico do mantenedor e do projeto

crapkit é criado e mantido por Jean-François Gagné. Leia o histórico do projeto para o problema que ele aborda e como ele se encaixa no trabalho dele em software e IA.

Licença

MIT. Veja LICENSE.