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

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 com | Quando |
|---|---|
| Instalação e o início em 60 segundos | Você quer a primeira pontuação em um repositório Git existente. |
| Python ou TypeScript quickstart | Você quer um exemplo prático desde a configuração até uma verificação aprovada. |
| Adoção | Você precisa escolher escopos, conectar testes ou introduzir uma catraca para dívida existente. |
| Atualização | Você já tem execuções salvas, marcas de catraca ou um plugin instalado. |
| Subcomandos e referência JSON/MCP | Você 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.
| Idioma | Arquivos | Cobertura |
|---|---|---|
| Python | .py | coverage.py |
| TypeScript | .ts | istanbul |
| TSX | .tsx | istanbul |
| JavaScript | .js .jsx .mjs .cjs | istanbul |
| Vue | .vue | istanbul, quando sua execução do vitest reporta arquivos .vue |
| Swift | .swift | nenhum: apenas cc |
| Go | .go | nenhum: apenas cc |
| Rust | .rs | nenhum: apenas cc |
| shell | .sh .bash | nenhum: apenas cc |
| PowerShell | .ps1 .psm1 | nenhum: apenas cc |
| C e C++ | .c .cc .cpp .cxx .h .hpp | nenhum: apenas cc |
| Objective-C | .m .mm | nenhum: apenas cc |
| Java | .java | nenhum: apenas cc |
| Zig | .zig | nenhum: 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ície | Dispara | Poder |
|---|---|---|
crapkit claude-hook | após a edição de um agente chegar | avisório. Nomeia a violação no stderr. Não bloqueia nada, porque o PostToolUse roda após a escrita |
crapkit rescore FILE --gate | quando você pede, após a primeira execução de cobertura | pré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-precommit | git commit | bloqueia. 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 verify | antes de você dar push, e no CI | o 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
| Entrada | Padrão | O 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.
| Comando | O 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. |
init | Examina 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. |
mcp | Um 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
| Flag | Significado | Pontuado |
|---|---|---|
measured | Um artefato de lane falou sobre esta função. | Real cov. |
untested | Um 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-lane | A 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-only | O 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édio | Condição | Ação |
|---|---|---|
decompose | ccn > ceiling | Divida. Nenhuma quantidade de cobertura resolve isso. |
split-lines | ccn <= 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 termina | Coloque 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-tests | ccn <= ceiling e crap > ceiling | Cubra os ramos. |
ok | crap <= ceiling | Nada. |
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ódigo | Significado |
|---|---|
| 0 | OK. Para verify e hook-precommit: o portão passou. |
| 1 | Sobrecarregado. Três coisas não relacionadas, listadas abaixo da tabela. |
| 2 | Erro de uso do argparse: flag desconhecida, posicional ausente. Levantado antes do tratamento de erro do próprio crapkit. |
| 3 | Erro 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. |
| 4 | Erro de Git: não é um repositório, um commit de linha de base reescrito fora do histórico. |
| 5 | Erro 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. |
| 6 | Violaçã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. |
| 7 | Regressã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. |
| 8 | Novas falhas de teste contra a execução de linha de base. Falhas que a linha de base já tinha não contam. |
| 9 | Teto 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:
| Comando | O que a saída 1 significa |
|---|---|
doctor | Uma 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 --enforce | A política de dívida foi violada. Também um veredito. |
| qualquer outra coisa | Um 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>"
| Pergunta | Resposta |
|---|---|
| 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ágina | Cobre |
|---|---|
| O manual | Comece 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.md | A 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.md | Cada chave crapkit.toml: tipo, padrão e o que faz. |
| docs/lanes.md | O modelo de pistas, receitas vitest, jest e pytest, reutilização de artefatos, reteste de flake, contêineres. |
| docs/resources.md | Orçamentos de worker, limpeza de comandos, rotação de logs e limpeza segura. |
| docs/ratchet.md | Semeadura, poda, o driver de merge do git, carimbos de métrica, política de dívida, substituições. |
| docs/upgrading.md | Instalações existentes: análise e versões-chave, estado salvo, alinhamento de plugins e atualizações no Windows. |
| docs/portable-records.md | Exportações sem perda, linhas de base e catracas portáteis, incluindo nomes de arquivo com delimitadores. |
| docs/agent-json.md | A superfície de máquina: schema, cada campo de payload, exemplos reais capturados. |
| docs/comparison.md | Onde crapkit se posiciona ao lado de radon, xenon, wily, coverage.py e SonarQube, e como eles rodam juntos. |
| AGENTS.md | O 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.