[DEBT] PR enrichment degrades silently when the GraphQL step fails; the report carries no signal
Em uma frase: dois runs do mesmo engine, no mesmo repo, minutos
um do outro, produziram merge_strategy_dominant_share 1.0 e 0.845,
flow_pr_count ausente e 52, "In PR" 0 e 1 — porque o passo de
enriquecimento via gh api graphql falhou uma vez e caiu para [] sem
deixar rastro no JSON nem no relatório.
Referências arquivo:linha apontam para main @ da16235.
Qual é a dívida?
iris/ingestion/github_reader.py busca PRs em até três passos
(_fetch_prs, :95-145): (1) gh pr list básico; (2) enriquecimento via
gh api graphql (_fetch_pr_enrichment_graphql, :184) que traz o parent
count do merge commit e as refs de commits por PR; (3) reviews via
gh pr list --json number,reviews (:140). O próprio código documenta que
o passo 2 é sujeito a "GitHub GraphQL 504 timeouts" (:89-91).
Quando o passo 2 falha, o código faz except (CalledProcessError, FileNotFoundError): return [] (:235-240) e segue. Consequências
observadas no output:
| Campo |
Com enriquecimento |
Sem (degradado) |
Por quê |
merge_strategy_dominant_share |
0.845 (ground truth por parent count) |
1.0 (heurística de mensagem (#N) → squash) |
merge_strategy_detector.py:25-32 cai no fallback |
flow_pr_count, flow_efficiency_*, time_in_phase_median_hours |
presentes (52 PRs) |
ausentes |
flow precisa das refs de commits |
acceptance_by_origin.*.commits_in_prs / origin_funnel "In PR" |
1 |
0 |
idem |
pr_merged_count |
84 |
84 |
passo 1 funcionou — a degradação é parcial |
Nenhum campo do JSON sinaliza que houve degradação (grep -i "error\|warn\|degrad" nas chaves → vazio), e o relatório apresenta os
números degradados com a mesma confiança dos completos. A mesma classe de
degradação silenciosa existe em cli.py:497-499 (except Exception: prs = []) para a busca inteira.
Por que é dívida?
- Confiança sem base: o leitor não consegue distinguir "este repo usa
squash em 100% dos merges" de "o enriquecimento falhou". São conclusões
opostas sobre a confiabilidade das métricas por commit (o próprio
merge_strategy existe para avisar quando squash "erodes per-commit
signal").
- Séries temporais ruidosas: na platform, um run degradado vira um ponto
fora da curva em flow_* e em merge strategy, indistinguível de mudança
real.
- Contradiz "deterministic outputs" (
CLAUDE.md, Coding Principles):
o mesmo input produz outputs diferentes conforme a rede, e o output não
diz qual dos dois é.
Como chegou aqui?
Degradação graciosa foi a escolha certa para a ausência de gh
(is_gh_available(), :62-64) — Iris deve funcionar sem PRs. A mesma
estratégia foi estendida para falhas transitórias de um passo
intermediário sem acrescentar o sinal correspondente.
Plano de migração
Deadline / gatilho pra pagar
Antes de qualquer análise de flow ou merge strategy ser usada em
decisão (ambas dependem do passo que falha silenciosamente). Custo baixo;
valor alto em confiança.
Evidência
Observado uma vez em dois runs consecutivos idênticos (mesmo engine
main @ da16235, mesmo repo, mesma janela de 90 dias, gh real), com os
valores da tabela acima; não reproduzido em dois runs subsequentes — é
intermitente, consistente com o 504 documentado em github_reader.py:89-91.
Reprodução determinística: monkeypatch de _fetch_pr_enrichment_graphql
para lançar CalledProcessError e comparar merge_strategy_dominant_share
e flow_pr_count com e sem a falha.
Análise adversária
| # |
Ataque |
Resposta |
Status |
| 1 |
Intermitente e observado uma vez — vale uma issue? |
A causa é estrutural (except → [] sem sinal) e o código já documenta o timeout. A frequência não importa quando o efeito é um relatório que afirma o oposto da verdade sem avisar. |
Vale. |
| 2 |
Falhar o run inteiro seria mais honesto que degradar. |
Contraria a decisão de funcionar sem gh. O meio-termo — degradar com sinal — preserva as duas coisas. |
Decidido: degradar com sinal. |
| 3 |
Retry resolve sem mexer no schema. |
Reduz frequência, não elimina; e o problema é a invisibilidade, não a frequência. Retry entra como complemento. |
Ambos. |
| 4 |
Campo novo quebra a ingestão? |
Não: /api/ingest usa .passthrough() (route.ts:45) e additionalProperties: true (openapi.yaml:489); armazenado em JSONB. Tipo TS é follow-up da mesma issue. |
Mitigado. |
Fora do escopo
- Substituir
gh por chamadas REST/GraphQL diretas.
- Cache persistente de PRs entre runs (
window_cache.py é in-process por desenho).
Issues relacionadas (verificado contra as 63 issues do repo, abertas e fechadas — sem duplicata)
Referências
iris/ingestion/github_reader.py:62-64,89-91,95-145,184,222-240,288-300; iris/cli.py:497-499; iris/analysis/merge_strategy_detector.py:25-32,73-89; iris/analysis/flow_efficiency.py; iris/models/metrics.py.
docs/METRICS.md §15 (PR lifecycle), §25 (Flow Efficiency), §28 (Merge Strategy).
CLAUDE.md → Coding Principles ("Favor deterministic outputs"), "Focus Areas (Stage 2) → Ingestion: reliability".
[DEBT] PR enrichment degrades silently when the GraphQL step fails; the report carries no signal
Referências
arquivo:linhaapontam paramain @ da16235.Qual é a dívida?
iris/ingestion/github_reader.pybusca PRs em até três passos(
_fetch_prs,:95-145): (1)gh pr listbásico; (2) enriquecimento viagh api graphql(_fetch_pr_enrichment_graphql,:184) que traz o parentcount do merge commit e as refs de commits por PR; (3) reviews via
gh pr list --json number,reviews(:140). O próprio código documenta queo passo 2 é sujeito a "GitHub GraphQL 504 timeouts" (
:89-91).Quando o passo 2 falha, o código faz
except (CalledProcessError, FileNotFoundError): return [](:235-240) e segue. Consequênciasobservadas no output:
merge_strategy_dominant_share(#N)→ squash)merge_strategy_detector.py:25-32cai no fallbackflow_pr_count,flow_efficiency_*,time_in_phase_median_hoursacceptance_by_origin.*.commits_in_prs/origin_funnel"In PR"pr_merged_countNenhum campo do JSON sinaliza que houve degradação (
grep -i "error\|warn\|degrad"nas chaves → vazio), e o relatório apresenta osnúmeros degradados com a mesma confiança dos completos. A mesma classe de
degradação silenciosa existe em
cli.py:497-499(except Exception: prs = []) para a busca inteira.Por que é dívida?
squash em 100% dos merges" de "o enriquecimento falhou". São conclusões
opostas sobre a confiabilidade das métricas por commit (o próprio
merge_strategyexiste para avisar quando squash "erodes per-commitsignal").
fora da curva em
flow_*e em merge strategy, indistinguível de mudançareal.
CLAUDE.md, Coding Principles):o mesmo input produz outputs diferentes conforme a rede, e o output não
diz qual dos dois é.
Como chegou aqui?
Degradação graciosa foi a escolha certa para a ausência de
gh(
is_gh_available(),:62-64) — Iris deve funcionar sem PRs. A mesmaestratégia foi estendida para falhas transitórias de um passo
intermediário sem acrescentar o sinal correspondente.
Plano de migração
github_reader.py: registrar qual passo degradou (basic,enrichment,reviews) e propagar até ocli.py.ReportMetrics: campopr_enrichment_degraded: list[str] | None(vazio/None quando tudo funcionou) —
iris/models/metrics.py; emitidopor
to_dict().writer.py/narrative.py: caveat no topo da seção de PRs quando ocampo não for vazio (chaves i18n EN + PT-BR): "PR enrichment
incomplete: merge strategy is heuristic; flow metrics unavailable".
merge_strategy_detector.py: quando o parent count estiver ausente pordegradação (não por repo sem PRs), rotular
merge_strategycomounknownem vez de aplicar a heurística — ou manter a heurística emarcar
dominant_sharecomo estimado. Decidir na PR.platform/src/types/metrics.ts: tipo do campo; UI: ícone/tooltip nospainéis de flow e merge strategy quando degradado.
docs/METRICS.md§15/§28: documentar o campo e o comportamento.frequência do problema sem remover o sinal).
_fetch_pr_enrichment_graphql) e asseverar o flag e o caveat.Deadline / gatilho pra pagar
Antes de qualquer análise de flow ou merge strategy ser usada em
decisão (ambas dependem do passo que falha silenciosamente). Custo baixo;
valor alto em confiança.
Evidência
Observado uma vez em dois runs consecutivos idênticos (mesmo engine
main @ da16235, mesmo repo, mesma janela de 90 dias,ghreal), com osvalores da tabela acima; não reproduzido em dois runs subsequentes — é
intermitente, consistente com o 504 documentado em
github_reader.py:89-91.Reprodução determinística: monkeypatch de
_fetch_pr_enrichment_graphqlpara lançar
CalledProcessErrore compararmerge_strategy_dominant_sharee
flow_pr_countcom e sem a falha.Análise adversária
except → []sem sinal) e o código já documenta o timeout. A frequência não importa quando o efeito é um relatório que afirma o oposto da verdade sem avisar.gh. O meio-termo — degradar com sinal — preserva as duas coisas./api/ingestusa.passthrough()(route.ts:45) eadditionalProperties: true(openapi.yaml:489); armazenado em JSONB. Tipo TS é follow-up da mesma issue.Fora do escopo
ghpor chamadas REST/GraphQL diretas.window_cache.pyé in-process por desenho).Issues relacionadas (verificado contra as 63 issues do repo, abertas e fechadas — sem duplicata)
_fetch_prs, as três passadas, o orçamento de nós do GraphQL), mas resolve outro problema: performance (86–98% do tempo de execução). Esta issue é sobre o sinal de falha, e vale em qualquer desenho da busca — se [DEBT] Busca de PR: os três estados numa query GraphQL com alias #215 vier antes, o flagpr_enrichment_degradedentra na query única em vez de nos três passos. Dependência que precisa ser declarada: o critério de aceite de [DEBT] Busca de PR: os três estados numa query GraphQL com alias #215 ("metrics.jsonidêntico campo por campo antes e depois") é exatamente o que a degradação silenciosa descrita aqui torna flaky — dois runs do mesmo código já divergiram emmerge_strategy_dominant_shareeflow_*sem mudança nenhuma. Esta issue é pré-requisito para aquele critério ser confiável (idem para [FEAT] Comparador de paridade do metrics.json, campo a campo #214, o comparador).sys.exit(0)antes da busca. As duas juntas cobrem "PR sumiu e ninguém soube".mergeCommitShae refs de commits — o enriquecimento que aqui falha silenciosamente. [METRIC] Merge strategy detection per repo (merge/squash/rebase) + flag de confiabilidade de métricas por-commit #76 (fechada) — criou omerge_strategycom o fallback heurístico que mascara a falha (100% squash quando o parent count falta).Referências
iris/ingestion/github_reader.py:62-64,89-91,95-145,184,222-240,288-300;iris/cli.py:497-499;iris/analysis/merge_strategy_detector.py:25-32,73-89;iris/analysis/flow_efficiency.py;iris/models/metrics.py.docs/METRICS.md§15 (PR lifecycle), §25 (Flow Efficiency), §28 (Merge Strategy).CLAUDE.md→ Coding Principles ("Favor deterministic outputs"), "Focus Areas (Stage 2) → Ingestion: reliability".