[METRIC] Churn: exclude lockfiles and generated files from the churn family
Em uma frase: package-lock.json é reescrito a cada bump de
dependência — isso não é instabilidade, é manutenção mecânica — e hoje ele
é o 2º arquivo "mais instável" do relatório deste repo e responde por
42% das linhas de churn.
Referências arquivo:linha apontam para main @ da16235. Esta issue é
independente: aplica, testa e valida sozinha sobre main. Toda
preocupação da análise adversária tem disposição final.
Hipótese
Essa métrica nos ajuda a entender como IA muda software delivery porque
churn é a explicação por trás da estabilização — o churn_top_files é
o drilldown que diz onde o retrabalho acontece e o churn_lines_affected
diz quanto. Quando um lockfile domina os dois, a investigação começa no
arquivo errado e o volume de "retrabalho" está inflado por algo que nenhuma
pessoa (nem IA) escreveu. Reduzir esse ruído é literalmente o foco declarado
do Stage 2 em CLAUDE.md ("Engine: tighten signal quality, reduce noise").
Definição
Comportamento atual (o problema)
calculate_churn() (iris/analysis/churn_calculator.py:52-58) e
calculate_churn_detail() (iris/analysis/churn_detail.py:116-125) iteram
todos os commit.files sem filtro. Medido neste repo, --days 90 --churn-days 14, main @ da16235:
Arquivo em churn_top_files |
Toques |
Fixes |
Cadeia |
iris/cli.py |
22 |
6 |
feature → unknown → config → … |
platform/package-lock.json |
18 |
0 |
config → config → config → config → … |
platform/package.json |
17 |
2 |
fix → fix → config → … |
pyproject.toml |
16 |
2 |
feature → config → … |
platform/lib/translations.ts |
15 |
6 |
feature → feature → fix → … |
CHANGELOG.md |
14 |
3 |
feature → unknown → config → … |
O lockfile tem 18 toques, zero fixes e uma cadeia inteiramente config — é
churn por construção, não por instabilidade. E 4 dos 6 maiores
"churners" são arquivos de cerimônia de release (pyproject.toml,
package.json, CHANGELOG.md, cli.py::VERSION — o próprio checklist de
release do CLAUDE.md manda tocar todos eles a cada versão).
Comportamento proposto
Predicado is_delivery_file(path) -> bool em churn_calculator.py
(módulo já cabeado no aggregator — evita módulo novo e o gate da
check_analysis_chain):
- Exclui (por basename, case-insensitive):
package-lock.json,
npm-shrinkwrap.json, pnpm-lock.yaml, yarn.lock, bun.lockb,
poetry.lock, Pipfile.lock, uv.lock, Cargo.lock, go.sum,
composer.lock, Gemfile.lock, packages.lock.json, mix.lock; e
qualquer basename contendo .min.js, .min.css ou .generated..
- Mantém manifestos (
package.json, pyproject.toml, Cargo.toml): um
bump de versão ali é evento real de entrega.
Aplicado em um ponto — o loop de calculate_churn — o que cobre de uma
vez churn_events, churn_lines_affected, churn_by_origin,
churn_by_intent (origin_metrics.py:54, intent_metrics.py:79 chamam a
mesma função) e o churn_events semanal de activity_timeline.py:167.
Aplicado também no loop de churn_detail, cobrindo churn_top_files e
churn_couplings (o lockfile deixa de "acoplar" com o manifesto a cada bump).
Deliberadamente não aplicado a metrics/stabilization.py nem
analysis/stability_map.py — decisão e porquê na adversária (ataques 3 e 5)
e no METRICS.md §1.
Efeito medido (este repo, 90d, churn_days=14)
| Campo |
Antes |
Depois |
Δ |
churn_events |
52 |
51 |
−1 |
churn_lines_affected |
12.726 |
7.351 |
−42% |
churn_by_origin.BOT.churn_lines_affected |
5.517 |
150 |
−97% (dependabot) |
churn_by_intent.CONFIG.churn_lines_affected |
5.792 |
417 |
−93% |
churn_top_files |
lockfile em #2 |
lockfile ausente; dashboard/page.tsx entra em #8 |
— |
activity_timeline (semanas 08-10 e 08-24) |
26 / 22 |
25 / 21 |
−1 cada |
stabilization_ratio |
0,7111 |
0,7111 |
0 (não filtrado) |
Neste repo o ruído estava 97% em commits BOT (dependabot), que a maioria
das comparações por origem já exclui — logo aqui o ganho é de legibilidade do
headline e do drilldown, não de correção de viés. Num repo onde pessoas
bumpam dependências à mão, as mesmas linhas cairiam em HUMAN e distorceriam
churn_by_origin — aí seria viés (ver ataque 6 e o critério de aceite).
Fonte do sinal
git log (--numstat: caminhos e linhas por commit). Nenhuma leitura de
conteúdo, nenhuma dependência nova.
Risco de ranqueamento individual
Nenhum. O filtro opera em caminhos de arquivo; não introduz nem toca campos
com autor. Se algo, reduz um vetor de distorção por origem (ataque 6).
Chain checklist (releasa completa)
Critérios de aceite (antes do merge)
Ao mesclar
[DEBT] de anotação por cli_version em
platform/lib/queries/temporal.ts: churn_lines_affected cai ~40% para o
mesmo histórico, e a detecção período-a-período do dashboard não consulta a
versão da CLI (o payload carrega cli_version, iris/platform/push.py:35; a
rota persiste, route.ts:153). Sem anotação, o gráfico marca a queda como
"mudança notável". Abrir depois do release, com a queda real observada
na primeira run ingerida — abrir antes seria issue especulativa.
Notas / prior art
Paper — Y. Liu, R. Widyasari, Y. Zhao, I. C. Irsan, J. Chen, D. Lo,
Debt Behind the AI Boom: A Large-Scale Empirical Study of AI-Generated Code
in the Wild, arXiv:2603.28592v2 [cs.SE], 2026. §III-B, p.4: antes de
medir, os autores excluem "files that are unlikely to reflect production code
quality (e.g., tests, documentation, configuration files, auto-built
artifacts, and vendored dependencies)". O análogo para o Iris não é "o que
rodar num linter", e sim "o que conta como entrega" — por isso a lista aqui é
mais estreita que a do paper: só o que se reescreve mecanicamente.
GitClear. O new_code_churn (docs/METRICS.md §19) já se descreve como
"file-level proxy for GitClear's line-level metric" — há precedente interno
de reconhecer que arquivo-a-arquivo é grosseiro.
Precedente interno de exclusão por caminho. churn_detail.py:85-87
(_TEST_SUFFIXES) já exclui pares teste+fonte do acoplamento.
Evidência de validação (isolada: main + só esta mudança)
- Unitário (
tests/test_churn_delivery_filter.py, 7 testes): lockfiles
em qualquer profundidade e caixa (Cargo.lock, Gemfile.lock,
platform/package-lock.json); .min.js/.min.css/.generated.;
manifestos e código como entrega (package.json, pyproject.toml,
src/lock.py, src/mylockfile.json); churn só-lockfile = 0; commit misto
conta só linhas de entrega; comportamento sem exclusões inalterado
(guarda de regressão); lockfile nunca aparece em top_churning_files nem
em couplings.
- Suíte completa: 480 pass / 0 fail (
LC_ALL=C; 473 da base + 7
novos). Não existia nenhum teste de churn no repo antes desta issue.
scripts/check_analysis_chain.py: OK (22 wired, 15 opt-out — inalterado).
- A/B end-to-end (engine base vs engine com esta mudança, mesmo
repo-alvo, runs em par no mesmo minuto — a janela --days é calculada
em UTC por dia; gh desabilitado; 90 dias, churn_days=14): mudaram
exatamente churn_events, churn_lines_affected, churn_by_origin,
churn_by_intent, churn_top_files e activity_timeline (churn
semanal); nenhuma chave adicionada ou removida; stabilization_ratio,
files_touched, durability_*, origin_funnel, origem e todas as chaves
de PR idênticos. Tempo 4,6 s → 4,6 s.
Como reproduzir
LC_ALL=C pytest tests/ -q # 480 passed
python scripts/check_analysis_chain.py # Analysis chain OK
mkdir -p /tmp/nogh && printf '#!/bin/sh\nexit 1\n' > /tmp/nogh/gh && chmod +x /tmp/nogh/gh
cd /tmp && PATH=/tmp/nogh:$PATH iris <repo> --days 90 --churn-days 14 --no-push --out /tmp/after
# em /tmp/after/90d/*metrics.json: churn_top_files sem lockfile; churn_lines_affected menor
Análise adversária (pós-implementação) — toda linha com disposição final
| # |
Ataque |
Resposta |
Disposição |
| 1 |
A lista é um rule set, e rule sets reescrevem a história. Adicionar flake.lock amanhã muda churn_lines_affected de todo repo retroativamente. |
Verdadeiro. Lockfiles são um conjunto fechado e estável (ao contrário de regras de lint); o efeito na manchete stabilization_ratio é zero; o payload carrega cli_version (iris/platform/push.py:35) e a rota o armazena (route.ts:153) — mas platform/lib/queries/temporal.ts não o consulta. |
✅ Disposição tripla: (a) decidido — a lista só muda em release minor, nunca em patch (registrado no METRICS.md §1); (b) CHANGELOG no release (chain); (c) follow-up condicional — ao mesclar, abrir [DEBT] para anotar deltas que cruzam cli_version em temporal.ts, com a queda real de churn_lines_affected observada na primeira run ingerida (ver "Ao mesclar"). |
| 2 |
Só basename. vendor/, dist/index.js, *_pb2.py, __generated__/, *.snap, pubspec.lock, Podfile.lock, flake.lock, deno.lock não são cobertos. |
Estreiteza deliberada: cada adição alimenta o ataque 1. A lista atual cobre os ecossistemas de repo_kind.py (_MANIFEST_FILES). |
✅ Decidido: lista inicial estreita. Ampliar só com evidência de um repo real onde o padrão domine o churn — o dry run do critério de aceite é a primeira oportunidade. |
| 3 |
Inconsistência com stability_map. O mapa por diretório (stability_map.py:73-78) ainda conta o lockfile. |
Aplicar o filtro lá afetaria também a componente de estabilização do mapa. |
✅ Decidido: manter a assimetria. Churn é medida de volume (linhas), onde um bump de lockfile pesa mais que uma semana de código; stabilization é razão sobre arquivos, onde o mesmo lockfile é 1 em centenas e, tocado uma vez, está genuinamente estabilizado. stability_map segue a definição de stabilization — assimetria entre famílias, coerente dentro de cada uma. Rationale no METRICS.md §1. |
| 4 |
Inconsistência dentro de uma linha do activity_timeline. lines_changed semanal inclui lockfiles; churn_events semanal exclui. |
lines_changed é volume total, não churn — semanticamente distinto, mas um leitor pode estranhar. |
✅ Decidido e feito: nota no METRICS.md §14 explicitando que os dois campos contam universos diferentes de arquivos. |
| 5 |
stabilization_ratio não filtrado é defensável? |
Medido: com filtro amplo (config+docs+lock) a manchete subiria ≤ +1,4 pp; com só lockfiles, menos. |
✅ Decidido junto com o ataque 3: não filtrar. Mudar a métrica-manchete do produto por ≤ 1,4 pp quebraria a comparabilidade do 79%/64% publicado sem insight novo. |
| 6 |
Medido em n = 1 repo, onde o ruído era 97% BOT. A alegação de "viés em repos com bump manual" é raciocínio, não medição. |
Correto: o mecanismo é o mesmo; a magnitude em outros repos não foi medida. |
✅ Critério de aceite (seção acima): dry run em 2–3 repos da org, incluindo um com bumps manuais, com números anexados à PR. |
| 7 |
Marcar arquivos de cerimônia de release no churn_top_files — proposto na análise, ausente aqui. |
Exigiria campo novo em ChurnFileEntry (Python e TS) e mudança de UI. |
✅ Decidido: não fazer, sem follow-up por ora. CLAUDE.md (Coding Principles): "Avoid speculative extensibility". Reabrir só se um dry run mostrar que a cerimônia de release confunde leitores reais. |
Fora do escopo
- Filtrar
stabilization.py / stability_map.py — decidido: não (ataques 3 e 5).
- Marcar arquivos de cerimônia de release em
churn_top_files — decidido: não por ora (ataque 7).
- Exclusão por diretório (
vendor/, dist/) — decidido: não (ataque 2).
- Anotação de
cli_version na detecção temporal → follow-up condicional, ao mesclar (ataque 1).
Issues relacionadas (verificado contra as 63 issues do repo, abertas e fechadas — sem duplicata)
Nenhuma issue existente menciona lockfiles, arquivos gerados ou exclusão de
arquivos do churn (busca por título e corpo em 2026-09-08). A vizinha mais
próxima em tema é #214 (aberta, comparador de paridade do
metrics.json): quando existir, ele é a ferramenta natural para medir o
efeito desta mudança campo a campo em vez do diff manual usado aqui.
Referências
- Paper: arXiv:2603.28592v2 — §III-B "Commit-Level Quality Analysis" (p.4). Replicação: https://github.com/yueyueL/tech-debt-ai-coding
- Código (
main @ da16235): iris/analysis/churn_calculator.py:35-85; iris/analysis/churn_detail.py:85-87,108-128; iris/analysis/origin_metrics.py:54; iris/analysis/intent_metrics.py:79; iris/analysis/activity_timeline.py:141,167; iris/analysis/stability_map.py:73-78; iris/metrics/stabilization.py:36-63; iris/analysis/repo_kind.py:42 (_MANIFEST_FILES); iris/platform/push.py:35; platform/lib/queries/temporal.ts.
- Docs:
docs/METRICS.md §1, §12, §13, §14, §19; CLAUDE.md → "Focus Areas (Stage 2)", "Release Checklist", "Coding Principles".
[METRIC] Churn: exclude lockfiles and generated files from the churn family
Referências
arquivo:linhaapontam paramain @ da16235. Esta issue éindependente: aplica, testa e valida sozinha sobre
main. Todapreocupação da análise adversária tem disposição final.
Hipótese
Essa métrica nos ajuda a entender como IA muda software delivery porque
churn é a explicação por trás da estabilização — o
churn_top_fileséo drilldown que diz onde o retrabalho acontece e o
churn_lines_affecteddiz quanto. Quando um lockfile domina os dois, a investigação começa no
arquivo errado e o volume de "retrabalho" está inflado por algo que nenhuma
pessoa (nem IA) escreveu. Reduzir esse ruído é literalmente o foco declarado
do Stage 2 em
CLAUDE.md("Engine: tighten signal quality, reduce noise").Definição
Comportamento atual (o problema)
calculate_churn()(iris/analysis/churn_calculator.py:52-58) ecalculate_churn_detail()(iris/analysis/churn_detail.py:116-125) iteramtodos os
commit.filessem filtro. Medido neste repo,--days 90 --churn-days 14,main @ da16235:churn_top_filesiris/cli.pyplatform/package-lock.jsonplatform/package.jsonpyproject.tomlplatform/lib/translations.tsCHANGELOG.mdO lockfile tem 18 toques, zero fixes e uma cadeia inteiramente
config— échurn por construção, não por instabilidade. E 4 dos 6 maiores
"churners" são arquivos de cerimônia de release (
pyproject.toml,package.json,CHANGELOG.md,cli.py::VERSION— o próprio checklist derelease do
CLAUDE.mdmanda tocar todos eles a cada versão).Comportamento proposto
Predicado
is_delivery_file(path) -> boolemchurn_calculator.py(módulo já cabeado no aggregator — evita módulo novo e o gate da
check_analysis_chain):package-lock.json,npm-shrinkwrap.json,pnpm-lock.yaml,yarn.lock,bun.lockb,poetry.lock,Pipfile.lock,uv.lock,Cargo.lock,go.sum,composer.lock,Gemfile.lock,packages.lock.json,mix.lock; equalquer basename contendo
.min.js,.min.cssou.generated..package.json,pyproject.toml,Cargo.toml): umbump de versão ali é evento real de entrega.
Aplicado em um ponto — o loop de
calculate_churn— o que cobre de umavez
churn_events,churn_lines_affected,churn_by_origin,churn_by_intent(origin_metrics.py:54,intent_metrics.py:79chamam amesma função) e o
churn_eventssemanal deactivity_timeline.py:167.Aplicado também no loop de
churn_detail, cobrindochurn_top_filesechurn_couplings(o lockfile deixa de "acoplar" com o manifesto a cada bump).Deliberadamente não aplicado a
metrics/stabilization.pynemanalysis/stability_map.py— decisão e porquê na adversária (ataques 3 e 5)e no METRICS.md §1.
Efeito medido (este repo, 90d,
churn_days=14)churn_eventschurn_lines_affectedchurn_by_origin.BOT.churn_lines_affectedchurn_by_intent.CONFIG.churn_lines_affectedchurn_top_filesdashboard/page.tsxentra em #8activity_timeline(semanas 08-10 e 08-24)stabilization_ratioNeste repo o ruído estava 97% em commits BOT (dependabot), que a maioria
das comparações por origem já exclui — logo aqui o ganho é de legibilidade do
headline e do drilldown, não de correção de viés. Num repo onde pessoas
bumpam dependências à mão, as mesmas linhas cairiam em
HUMANe distorceriamchurn_by_origin— aí seria viés (ver ataque 6 e o critério de aceite).Fonte do sinal
git log (
--numstat: caminhos e linhas por commit). Nenhuma leitura deconteúdo, nenhuma dependência nova.
Risco de ranqueamento individual
Nenhum. O filtro opera em caminhos de arquivo; não introduz nem toca campos
com autor. Se algo, reduz um vetor de distorção por origem (ataque 6).
Chain checklist (releasa completa)
churn_calculator.py(is_delivery_file, filtro emcalculate_churn) echurn_detail.py(mesmo filtro)churn_eventssemanal aplica a exclusão elines_changednãoCLAUDE.md, Release Checklist, item 4), não pré-requisito de merge. Deve registrar a mudança de definição dechurn_*(ataque 1).Critérios de aceite (antes do merge)
menos um onde dependências sejam atualizadas manualmente (não por
dependabot/renovate). Anexar à PR
churn_lines_affected,churn_by_originechurn_top_filesantes e depois. A medição destaissue é n = 1 e o ruído estava em BOT; o critério confirma (ou refuta)
o efeito sobre a comparação IA × humano em repos onde o ruído cai em
HUMAN.
cli.Ao mesclar
[DEBT]de anotação porcli_versionemplatform/lib/queries/temporal.ts:churn_lines_affectedcai ~40% para omesmo histórico, e a detecção período-a-período do dashboard não consulta a
versão da CLI (o payload carrega
cli_version,iris/platform/push.py:35; arota persiste,
route.ts:153). Sem anotação, o gráfico marca a queda como"mudança notável". Abrir depois do release, com a queda real observada
na primeira run ingerida — abrir antes seria issue especulativa.
Notas / prior art
Paper — Y. Liu, R. Widyasari, Y. Zhao, I. C. Irsan, J. Chen, D. Lo,
Debt Behind the AI Boom: A Large-Scale Empirical Study of AI-Generated Code
in the Wild, arXiv:2603.28592v2 [cs.SE], 2026. §III-B, p.4: antes de
medir, os autores excluem "files that are unlikely to reflect production code
quality (e.g., tests, documentation, configuration files, auto-built
artifacts, and vendored dependencies)". O análogo para o Iris não é "o que
rodar num linter", e sim "o que conta como entrega" — por isso a lista aqui é
mais estreita que a do paper: só o que se reescreve mecanicamente.
GitClear. O
new_code_churn(docs/METRICS.md§19) já se descreve como"file-level proxy for GitClear's line-level metric" — há precedente interno
de reconhecer que arquivo-a-arquivo é grosseiro.
Precedente interno de exclusão por caminho.
churn_detail.py:85-87(
_TEST_SUFFIXES) já exclui pares teste+fonte do acoplamento.Evidência de validação (isolada:
main+ só esta mudança)tests/test_churn_delivery_filter.py, 7 testes): lockfilesem qualquer profundidade e caixa (
Cargo.lock,Gemfile.lock,platform/package-lock.json);.min.js/.min.css/.generated.;manifestos e código como entrega (
package.json,pyproject.toml,src/lock.py,src/mylockfile.json); churn só-lockfile = 0; commit mistoconta só linhas de entrega; comportamento sem exclusões inalterado
(guarda de regressão); lockfile nunca aparece em
top_churning_filesnemem
couplings.LC_ALL=C; 473 da base + 7novos). Não existia nenhum teste de churn no repo antes desta issue.
scripts/check_analysis_chain.py: OK (22 wired, 15 opt-out — inalterado).repo-alvo, runs em par no mesmo minuto — a janela
--daysé calculadaem UTC por dia;
ghdesabilitado; 90 dias,churn_days=14): mudaramexatamente
churn_events,churn_lines_affected,churn_by_origin,churn_by_intent,churn_top_fileseactivity_timeline(churnsemanal); nenhuma chave adicionada ou removida;
stabilization_ratio,files_touched,durability_*,origin_funnel, origem e todas as chavesde PR idênticos. Tempo 4,6 s → 4,6 s.
Como reproduzir
Análise adversária (pós-implementação) — toda linha com disposição final
flake.lockamanhã mudachurn_lines_affectedde todo repo retroativamente.stabilization_ratioé zero; o payload carregacli_version(iris/platform/push.py:35) e a rota o armazena (route.ts:153) — masplatform/lib/queries/temporal.tsnão o consulta.[DEBT]para anotar deltas que cruzamcli_versionemtemporal.ts, com a queda real dechurn_lines_affectedobservada na primeira run ingerida (ver "Ao mesclar").vendor/,dist/index.js,*_pb2.py,__generated__/,*.snap,pubspec.lock,Podfile.lock,flake.lock,deno.locknão são cobertos.repo_kind.py(_MANIFEST_FILES).stability_map. O mapa por diretório (stability_map.py:73-78) ainda conta o lockfile.stability_mapsegue a definição de stabilization — assimetria entre famílias, coerente dentro de cada uma. Rationale no METRICS.md §1.activity_timeline.lines_changedsemanal inclui lockfiles;churn_eventssemanal exclui.lines_changedé volume total, não churn — semanticamente distinto, mas um leitor pode estranhar.stabilization_rationão filtrado é defensável?churn_top_files— proposto na análise, ausente aqui.ChurnFileEntry(Python e TS) e mudança de UI.CLAUDE.md(Coding Principles): "Avoid speculative extensibility". Reabrir só se um dry run mostrar que a cerimônia de release confunde leitores reais.Fora do escopo
stabilization.py/stability_map.py— decidido: não (ataques 3 e 5).churn_top_files— decidido: não por ora (ataque 7).vendor/,dist/) — decidido: não (ataque 2).cli_versionna detecção temporal → follow-up condicional, ao mesclar (ataque 1).Issues relacionadas (verificado contra as 63 issues do repo, abertas e fechadas — sem duplicata)
Nenhuma issue existente menciona lockfiles, arquivos gerados ou exclusão de
arquivos do churn (busca por título e corpo em 2026-09-08). A vizinha mais
próxima em tema é #214 (aberta, comparador de paridade do
metrics.json): quando existir, ele é a ferramenta natural para medir oefeito desta mudança campo a campo em vez do diff manual usado aqui.
Referências
main @ da16235):iris/analysis/churn_calculator.py:35-85;iris/analysis/churn_detail.py:85-87,108-128;iris/analysis/origin_metrics.py:54;iris/analysis/intent_metrics.py:79;iris/analysis/activity_timeline.py:141,167;iris/analysis/stability_map.py:73-78;iris/metrics/stabilization.py:36-63;iris/analysis/repo_kind.py:42(_MANIFEST_FILES);iris/platform/push.py:35;platform/lib/queries/temporal.ts.docs/METRICS.md§1, §12, §13, §14, §19;CLAUDE.md→ "Focus Areas (Stage 2)", "Release Checklist", "Coding Principles".