Skip to content

[METRIC] Origin classifier: attribution by evidence source (email domain, agent author markers) #234

Description

@rafaeldearaujop

[METRIC] Origin classifier: attribution by evidence source (email domain, agent author markers)

Em uma frase: hoje o Iris só reconhece um commit como IA se ele tiver
um trailer no texto; commits autorados diretamente por um agente
(Claude, Copilot coding agent, Devin, Cursor Agent) caem em HUMANO ou BOT,
contaminando a baseline humana ou sumindo da comparação IA × humano.

Referências arquivo:linha apontam para main @ da16235 (antes desta
mudança). Esta issue é independente: aplica, testa e valida sozinha sobre
main. Toda preocupação levantada na análise adversária tem disposição
final — decidida, mitigada, transformada em critério de aceite desta issue,
convertida em condição ("ao mesclar…") ou desmembrada em PR separada.


Hipótese

Essa métrica nos ajuda a entender como IA muda software delivery porque
toda comparação IA × humano do Iris depende de classificar corretamente
quem escreveu cada commit
— e a forma dominante de autoria por IA está
mudando de assistência (pessoa commita, IA co-assina) para agência (o
agente commita como autor, sem trailer). Se esses commits entram na baseline
humana, a diferença IA × humano encolhe artificialmente; se entram em BOT,
os commits mais "IA" de todos somem de durability_by_origin,
fix_target_by_origin, cascade_rate_by_origin e acceptance_by_origin,
que iteram só HUMAN e AI_ASSISTED.

O registro de qual evidência identificou cada commit responde a uma
pergunta operacional que hoje não tem resposta: quanto da nossa visibilidade
de IA depende de o hook prepare-commit-msg estar instalado?

Definição

Comportamento atual (o problema)

detect_tool() lê apenas commit.attribution_trailers
(iris/analysis/origin_classifier.py:132-136). O campo Commit.author_email
existe (iris/models/commit.py:30) e é preenchido pelo git_reader
(iris/ingestion/git_reader.py:243), mas nunca é consultado. Teste sintético
no classificador real, main @ da16235:

Commit sem trailer Classificação hoje Deveria ser
autor Claude, e-mail [email protected] HUMAN AI_ASSISTED (Claude)
autor Cursor Agent HUMAN AI_ASSISTED (Cursor)
autor copilot-swe-agent[bot] BOT AI_ASSISTED (Copilot)
autor devin-ai-integration[bot] BOT AI_ASSISTED (Devin)
autor google-labs-jules[bot] BOT AI (fora de escopo: Jules não está em _AI_TOOL_PATTERNS)
autor Claude Dupont, e-mail [email protected] HUMAN HUMAN ✓

O padrão da primeira linha é exatamente o da Fig. 1 do paper (repositório
claudes-c-compiler: 3.957 commits com Claude como autor, ~500K linhas
em semanas).

Comportamento proposto

Ordem de avaliação em classify_origin() — primeiro match vence:

  1. Trailer Co-authored-by / Assisted-by / Made-with nomeia uma
    ferramenta de _AI_TOOL_PATTERNSAI_ASSISTED, evidência trailer.
    (Inalterado.)
  2. Domínio do e-mail do autor está em _AI_EMAIL_DOMAINS (match por
    domínio ou subdomínio, nunca por padrão de texto) → AI_ASSISTED,
    evidência author_email. A lista contém apenas anthropic.com — o
    único domínio citado pelo paper; entradas novas só com commit observado.
  3. Nome do autor carrega marcador de agente ([bot] ou a palavra
    Agent, delimitada por não-alfanuméricos) e nomeia uma ferramenta de
    _AI_TOOL_PATTERNS e não está em _BOT_NAMESAI_ASSISTED,
    evidência author_name.
  4. _BOT_AUTHOR_PATTERNS casa o nome → BOT. (Inalterado — o
    human_review_coverage.py:43,175 importa esse regex diretamente e não é
    afetado.)
  5. HUMAN.

Novas APIs em origin_classifier.py:

  • detect_tool_evidence(commit) -> tuple[str, str] | None(tool, evidence),
    com evidence ∈ {"trailer", "author_email", "author_name"}.
  • detect_tool(commit) passa a delegar para detect_tool_evidence (API
    pública preservada).
  • Constantes EVIDENCE_TRAILER, EVIDENCE_AUTHOR_EMAIL, EVIDENCE_AUTHOR_NAME.

Novo campo em ReportMetrics (iris/models/metrics.py, após
commit_origin_distribution):

origin_evidence_distribution: dict[str, int] | None
# ex.: {"trailer": 68} · {"trailer": 40, "author_email": 12, "author_name": 9}
# None quando não há commits AI_ASSISTED

Populado em iris/metrics/aggregator.py logo após ai_detection_coverage_pct,
iterando origin_classified e contando a evidência dos AI_ASSISTED.
Emitido automaticamente por ReportMetrics.to_dict() (asdict). Aceito
pela rota /api/ingest sem mudança de contrato: o schema zod usa
.passthrough() (platform/src/app/api/ingest/route.ts:45) e o OpenAPI
declara additionalProperties: true (platform/openapi.yaml:489); o payload
é persistido em JSONB (platform/supabase/migrations/008_iris_tables.sql:42).

Render no relatório (iris/reports/writer.py, bloco de cobertura de IA,
:262-272): a frase de evidência é anexada à linha de cobertura existente,
ordenada por contagem decrescente — chaves i18n ai_evidence_breakdown,
evidence_trailer, evidence_author_email, evidence_author_name em EN e
PT-BR:

> AI detection coverage: 100% of commits have AI attribution. Attribution evidence: 67 via trailer.
> Cobertura de detecção AI: 100% dos commits possuem atribuição AI. Evidência de atribuição: 67 via trailer.

Tipo TS (platform/src/types/metrics.ts, bloco // Origin):
origin_evidence_distribution?: Partial<Record<"trailer" | "author_email" | "author_name", number>>.

Efeitos downstream (todos intencionais)

Consumidor O que muda com agentes reclassificados BOT/HUMAN → AI
aggregator.py:100-110 ai_detection_coverage_pct sobe (numerador e denominador não-bot mudam)
analysis/acceptance_rate.py:81-86 pula BOT; agentes passam a entrar nas taxas de aceitação, com ferramenta
analysis/cascade_detector.py:128-134 BOT não é gatilho; agentes passam a ser gatilhos válidos
analysis/attribution_gap.py:71 varre HUMAN; commits Claude/Cursor Agent deixam de ser "gap" porque estão atribuídos
build_tool_map()durability_by_tool, cascade_rate_by_tool, revert_by_tool ganham os agentes
analysis/human_review_coverage.py não muda (usa _BOT_AUTHOR_PATTERNS em autores de review)

Fonte do sinal

múltiplas fontes — git log (nome e e-mail do autor) + co-author / trailers
do hook. Todas offline; nenhuma chamada de rede nova.

Risco de ranqueamento individual

Baixo e não-novo. A classificação é por classe de origem (HUMAN /
AI_ASSISTED / BOT), não por pessoa; origin_evidence_distribution é um
agregado por repositório com três chaves, sem nomes. A regra 3 só se aplica a
contas de agente (marcador [bot]/Agent), nunca a pessoas — e a regra
2 casa por domínio de e-mail de vendor, o que exclui pessoas por
construção ([email protected] continua humano). Nenhum campo novo
carrega identidade.

Chain checklist (releasa completa)

  • analysis module (iris/analysis/origin_classifier.py) — detect_tool_evidence, _AI_EMAIL_DOMAINS, _AGENT_AUTHOR_MARKER, _EXPLICIT_BOT_NAME
  • aggregator (iris/metrics/aggregator.py) — popula origin_evidence_distribution
  • python schema (iris/models/metrics.py) — campo novo, None por padrão
  • report writer (iris/reports/writer.py) — frase de evidência na linha de cobertura; chaves i18n EN + PT-BR; testado em test_writer_evidence_breakdown.py
  • narrative findings (iris/reports/narrative.py) — decidido: não se aplica. A distribuição é descritiva; não há threshold natural que separe "bom" de "ruim" (um repo 100% trailer e um repo 100% agente são ambos legítimos). Um finding aqui seria opinião, não achado.
  • TS types (platform/src/types/metrics.ts) — campo opcional adicionado; tsc, vitest, eslint, prettier e next build verdes
  • platform UI — decidido: adiado por desenho. CLAUDE.md (Design Philosophy): "A new UI view must be justified by a metric that already tells a useful story in the engine." Nenhum repo real ainda mostrou uma distribuição com mais de uma fonte de evidência; quando o dry run do critério de aceite mostrar uma, o painel de cobertura de IA é o lugar natural (sem view nova).
  • docs/METRICS.md atualizado — linha em §4 + mapa "Module → fields"
  • CHANGELOG — passo do release (CLAUDE.md, Release Checklist, item 4), não pré-requisito de merge. Deve registrar a mudança de semântica (ver adversária, ataque 4).

Critérios de aceite (antes do merge)

  • Dry run do engine com este patch em ≥ 1 repositório da org que tenha
    commits autorados por agente
    (Copilot coding agent, Devin, Cursor
    background agent ou Claude autor). Anexar à PR o
    commit_origin_distribution e o origin_evidence_distribution antes e
    depois. Aqui o delta é zero por construção (todo commit de IA tem
    trailer) — a cobertura desse cenário é por teste de integração, e um
    dry run real é o que confirma o comportamento em campo.
  • CI verde em ambos os jobs (cli e platform).
  • PR descreve a mudança de semântica para o CHANGELOG do próximo release.

Ao mesclar

Dois itens nascem depois do merge, não antes — abri-los agora seria
criar issues especulativas:

  1. [DEBT] de anotação por cli_version em
    platform/lib/queries/temporal.ts: a reclassificação muda
    commit_origin_distribution e ai_detection_coverage_pct 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). Abrir com a descontinuidade real
    observada na primeira run ingerida após o release.
  2. PR pequena no hook: prepare_commit_msg.sh passa a detectar por env
    var as ferramentas que a org usa além do Claude Code, e ganha um teste de
    paridade entre o guard do hook e _AI_TOOL_PATTERNS.

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-A "AI Attribution
Rules", p.3: quatro fontes de evidência em metadata do Git — (1) actor
login
(ex. copilot-swe-agent[bot]), (2) author email (ex.
[email protected]), (3) author name (ex. Cursor Agent), (4) trailer
Co-authored-by. 29 ferramentas deixaram rastros identificáveis. §IV-D,
p.6: 100 commits amostrados, 99 verificáveis, 99/99 corretamente
atribuídos
por dois anotadores independentes. O Iris hoje lê uma das
quatro fontes (trailer); com esta mudança passa a ler três — o actor
login não existe offline.

Precedente interno. intent_classifier.py já registra
confidence_reason por commit (iris/models/intent.py:33); esta mudança
espelha esse padrão para origem.

Cobertura de ferramentas — reativa, não por auditoria. _AI_TOOL_PATTERNS
tem 9 ferramentas; o paper identificou 29. A tentação é auditar a lista
inteira — mas isso contradiz a regra adotada aqui para domínios de e-mail
("só com commit observado") e adicionaria padrões que a org talvez nunca
acione. Ferramentas novas entram quando um commit real as revela, pelo
procedimento já documentado no CLAUDE.md ("When adding a new AI tool…").
O que é concreto hoje e fica como PR pequena separada: o hook
prepare_commit_msg.sh detecta por env var só $CLAUDE_CODE (:52)
enquanto seu guard reconhece 10 nomes (:103), e não há teste que mantenha
guard e tabela em paridade.


Evidência de validação (isolada: main + só esta mudança)

Python

  • Unitário (tests/test_origin_classifier_evidence.py, 15 testes): a
    matriz da tabela acima, mais Gemini Rossi / Claudemir Silva /
    random-user-ai → HUMAN; dependabot[bot] / github-actions[bot] /
    kody-ai / copilot-autofix[bot] → BOT; trailer vence e-mail e nome;
    e-mail vazio tolerado; subdomínio de vendor casa; domínios não
    verificados
    (cursor.com, devin.ai) não classificam.
  • Integração (tests/test_origin_evidence_integration.py, 5 testes):
    histórico sintético de 10 commits atravessa aggregate()
    commit_origin_distribution == {"HUMAN": 3, "AI_ASSISTED": 4, "BOT": 3},
    origin_evidence_distribution == {"trailer": 1, "author_email": 1, "author_name": 2},
    ai_detection_coverage_pct == round(4/7*100, 1), campo ausente sem IA,
    e acceptance/cascade/attribution_gap executam com agentes presentes.
  • Render (tests/test_writer_evidence_breakdown.py, 4 testes): frase
    em EN e PT-BR, ordenação por contagem decrescente, posição na linha de
    cobertura, ausência quando não há evidência (payloads antigos).
  • Suíte completa: 497 pass / 0 fail (LC_ALL=C; 473 da base + 24
    novos). check_analysis_chain.py: OK (22 wired, 15 opt-out — inalterado).

Platform (platform/, com npm ci --legacy-peer-deps como na CI)

  • npx tsc --noEmit ✓ · npx vitest run 276/276 ✓ · npx eslint .
    0 erros (79 warnings pré-existentes, nenhum em metrics.ts) ✓ ·
    npx prettier --check src/types/metrics.ts ✓ · npm run build com os
    placeholders de env da CI ✓ (48 páginas estáticas).

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 com granularidade de dia, e runs em momentos diferentes podem cair em
janelas diferentes; gh desabilitado para determinismo; 90 dias,
churn_days=14):

  • metrics.json: nenhuma chave alterada ou removida; uma adicionada —
    origin_evidence_distribution = {"trailer": 67}.
  • report.md EN e PT-BR: exatamente uma linha difere — a de cobertura,
    que ganha a frase de evidência.
  • Delta zero neste repo por construção: aqui todo commit de IA tem trailer,
    e há 0 commits HUMAN em 90 dias — por isso o critério de aceite exige um
    dry run em repo com agentes.

Como reproduzir

LC_ALL=C pytest tests/ -q                       # 497 passed
python scripts/check_analysis_chain.py          # Analysis chain OK
cd platform && npm ci --legacy-peer-deps && npx tsc --noEmit && npx vitest run && npx eslint . && cd ..
# A/B determinístico: gh falso no PATH, cwd fora do repo, base e after em sequência imediata
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

Análise adversária (pós-implementação) — toda linha com disposição final

# Ataque Resposta Disposição
1 Domínios de e-mail não verificados.anthropic.com é citado pelo paper; cursor.com, cursor.sh, devin.ai, cognition.ai seriam suposições. Entradas erradas são inofensivas (nunca casam) mas são peso morto e passam falsa impressão de cobertura. Decidido: só anthropic.com. Cursor e Devin já são cobertos pela regra de nome (Cursor Agent, devin-ai-integration[bot]). test_unverified_vendor_domains_are_not_listed documenta o escopo; novas entradas só com commit observado.
2 copilot-autofix[bot] fica BOT, copilot-swe-agent[bot] vira AI — dois produtos Copilot com vereditos diferentes. A lista curada _BOT_NAMES agrupa copilot-autofix em "GitHub core". Decidido: mantém BOT. O eixo é o que a conta faz, não a marca: autofix reage a alertas de code scanning (classe de dependabot/snyk-bot); o coding agent autora entrega sob pedido. Rationale no comentário de _EXPLICIT_BOT_NAME; test_explicit_bot_names_keep_curated_verdict fixa o comportamento.
3 Superfície de falso-positivo do marcador Agent. Um handle humano como gemini-agent-dev casaria. Exige marcador e nome de ferramenta; humanos com nome de ferramenta testados como HUMAN. Risco residual real mas estreito. Mitigado; limitação documentada no comentário de _AGENT_AUTHOR_MARKER.
4 Reclassificação reescreve o histórico. Um repo pode passar de 30% para 45% de IA na versão seguinte da CLI sem mudança no código. Métricas já ingeridas não são recomputadas. Comportamento correto, mas é descontinuidade de série. O payload carrega cli_version (iris/platform/push.py:35), a rota o armazena (route.ts:153), mas platform/lib/queries/temporal.ts não o consulta. Disposição dupla: (a) CHANGELOG no release (chain); (b) follow-up condicional — ao mesclar, abrir [DEBT] para anotar deltas que cruzam cli_version em platform/lib/queries/temporal.ts, com os números reais da primeira run ingerida. Não se abre antes: seria issue especulativa dormindo até o merge. Esta issue não depende dela para estar correta; ela evita o falso alarme visual.
5 Não testado num repo real com agentes. O A/B aqui tem delta zero por construção. Coberto por teste de integração com commits sintéticos; não substitui campo. Critério de aceite (seção acima): dry run em ≥ 1 repo da org com commits de agente antes do merge, com distribuições anexadas à PR.
6 Jules e outros agentes ficam BOT. A regra 3 exige ferramenta em _AI_TOOL_PATTERNS; google-labs-jules[bot] não está lá. Lacuna de cobertura, não de mecanismo — o mecanismo promove qualquer agente cuja ferramenta esteja na tabela. Testado e documentado. Decidido: cobertura é reativa. Ferramentas entram em _AI_TOOL_PATTERNS quando um commit observado as revela — a mesma regra adotada nesta issue para _AI_EMAIL_DOMAINS. Auditar uma lista de 29 ferramentas de paper adicionaria regex que a org talvez nunca acione. O que sobra de concreto (hook detecta só $CLAUDE_CODE; paridade hook↔tabela) vira PR pequena separada (ver "Ao mesclar").
7 Commits vindos da API do GitHub (path de PR) podem não ter e-mail. author_email default ""; a regra 2 exige @ e não casa. A regra 3 funciona com o login (copilot-swe-agent[bot] é o login da GitHub App). ✅ Mitigado.
8 Custo. detect_tool_evidence roda até 3× por commit. Regex em strings curtas; A/B mediu 4,6 s antes e depois. ✅ Mitigado (medido).
9 A frase de evidência no relatório é ruído quando há uma só fonte ("67 via trailer"). Verdadeiro para repos 100% trailer — e é exatamente a informação: toda a visibilidade depende do hook. Uma frase curta na linha já existente, sem seção nova. ✅ Decidido: manter; custo de uma frase, informação operacional real.

Fora do escopo

  • Nome de autor sem marcador de agente (falso-positivo medido: Claude Dupont).
  • Detecção por env var no hook e teste de paridade hook↔tabela → PR pequena separada (ver "Ao mesclar").
  • Anotação de cli_version na detecção temporal do dashboard → follow-up condicional (ver "Ao mesclar").
  • Recomputar métricas já ingeridas.
  • Novo view na platform (adiado por desenho; ver chain).

Issues relacionadas (verificado contra as 63 issues do repo, abertas e fechadas — sem duplicata)

Referências

  • Paper: arXiv:2603.28592v2 — §III-A (p.3), §IV-D (p.6), Fig. 1 (p.2), Table I (p.5). Replicação: https://github.com/yueyueL/tech-debt-ai-coding
  • Código (main @ da16235): iris/analysis/origin_classifier.py:39-59 (_AI_TOOL_PATTERNS), :63-92 (_BOT_NAMES), :98-101 (_BOT_AUTHOR_PATTERNS), :104-136 (classify_origin, detect_tool); iris/models/commit.py:28-35; iris/ingestion/git_reader.py:40-43,236-246; iris/metrics/aggregator.py:91-125; iris/reports/writer.py:262-272; iris/i18n.py:599-612,1480-1493; iris/analysis/human_review_coverage.py:43,175; platform/src/types/metrics.ts:198-202; platform/src/app/api/ingest/route.ts:45,153; platform/openapi.yaml:489.
  • Regras do projeto: CLAUDE.md → "When adding a new AI tool…", Absolute Rule build(deps): Bump pyjwt from 2.8.0 to 2.12.0 in /platform/scripts #5, Design Philosophy, Release Checklist; docs/guides/ai-attribution-policy.md; docs/METRICS.md §4.

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