Skip to content

[METRIC] Churn: exclude lockfiles and generated files from the churn family #235

Description

@rafaeldearaujop

[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)

  • analysis module — churn_calculator.py (is_delivery_file, filtro em calculate_churn) e churn_detail.py (mesmo filtro)
  • aggregator — sem mudança necessária (só valores mudam)
  • python schema — sem mudança (nenhum campo novo)
  • report writer — sem mudança (renderiza os mesmos campos)
  • narrative findings — sem mudança
  • TS types — sem mudança
  • platform UI — sem mudança
  • docs/METRICS.md atualizado — §1: parágrafo "Exclusion (churn family only)" com a assimetria e o porquê; §14: nota de que churn_events semanal aplica a exclusão e lines_changed não
  • CHANGELOG — passo do release (CLAUDE.md, Release Checklist, item 4), não pré-requisito de merge. Deve registrar a mudança de definição de churn_* (ataque 1).

Critérios de aceite (antes do merge)

  • Dry run do engine com este patch em 2–3 repositórios da org, pelo
    menos um onde dependências sejam atualizadas manualmente (não por
    dependabot/renovate). Anexar à PR churn_lines_affected,
    churn_by_origin e churn_top_files antes e depois. A medição desta
    issue é 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.
  • CI verde no job cli.
  • PR descreve a mudança de definição para o CHANGELOG do próximo release.

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 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.pydecidido: não (ataques 3 e 5).
  • Marcar arquivos de cerimônia de release em churn_top_filesdecidido: 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".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: metricNova métrica ou alteração de métrica existente

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions