Skip to content

feat(platform): board flow analysis from GitHub Projects V2 - #170

Open
renatoguimaraescb wants to merge 6 commits into
mainfrom
feat/github-projects-flow
Open

renatoguimaraescb wants to merge 6 commits into
mainfrom
feat/github-projects-flow

Conversation

@renatoguimaraescb

@renatoguimaraescb renatoguimaraescb commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Tipo

Feature — novo módulo de ingestão + métricas + dashboard na plataforma.

Closes #252 (issue aberta retroativamente em 2026-09-16, registrando o que este PR já implementava — ver nota de processo no final).

Summary

Adiciona análise de fluxo de entrega a partir de GitHub Projects V2: lead time, tempo por coluna, throughput, WIP aging, CFD e sinais de gargalo — com portões de qualidade de dados na frente de tudo.

O motivo: o engine mede deliberadamente a janela PR-open → merge (flow_efficiency.py) e não diz nada sobre a fila que vem antes. Se a IA encurta a fase de código mas o lead time total não se move, a restrição está fora do código. Esse é o denominador que faltava.

Correção (2026-09-16): esta seção originalmente dizia "Sem UI nesta PR" — isso estava errado. O PR sempre carregou uma página de dashboard completa em /[tenant]/flow (flow-view.tsx, flow-sections.tsx, entrada de menu). Ver "Por que a UI entra nesta PR" abaixo para a justificativa correta, exigida pelo non-goal de "dashboard sprawl" do CLAUDE.md.

Por que a UI entra nesta PR

O CLAUDE.md proíbe "dashboard sprawl: each new view must tie to a validated insight, not a hypothetical user". Esta view não é hipotética: antes de qualquer linha de UI, o coletor + portões foram validados contra um board real de produção (198 itens, 754 eventos) e o dry-run (scripts/board-flow-dryrun.ts) encontrou e corrigiu três defeitos reais de classificação (ver "Validação contra board real" abaixo) — a métrica que a UI mostra já passou por essa validação, não é um número recém-calculado sendo exposto direto.

A decisão de arquitetura que merece review

O desenho óbvio para "quanto tempo cada card ficou em cada coluna" é snapshot periódico + job de diff. Para Projects V2 isso é desnecessário.

A API expõe ProjectV2ItemStatusChangedEvent no timeline do conteúdo, com createdAt, previousStatus, status e wasAutomated. Validei contra um board real antes de escrever qualquer código:

issue criada: 2026-08-14T14:42:35Z | fechada: 2026-08-21T13:21:41Z
14/08 14:42       ADICIONADO ao board
14/08 14:42   0.00d  (vazio) -> Backlog
17/08 14:22   2.99d  Backlog -> Ready for Development
17/08 18:40   0.18d  Ready for Development -> In Progress
18/08 14:36   0.83d  In Progress -> Tests
...
21/08 13:21   1.07d  Monitoring -> Done

Consequências: histórico retroativo no primeiro sync (sem período de acumulação), precisão de segundo (sem janela de ±24h), CFD reconstruível para semanas passadas, e wasAutomated como sinal de movimentação automatizada melhor que heurística de timestamp.

Único buraco: DraftIssue não é Issue e não tem timelineItems. Drafts ficam com history_available = false e são excluídos de métricas de duração — nunca contados como zero.

Como encaixa no que já existe

Espelha a integração Datadog: client.ts + syncOrganization() que nunca lança + upsert idempotente pelos node ids do próprio provider + slot no cron diário que já roda às 04:00 UTC. Métricas em lib/queries/ como funções puras, testadas como cycle-time-flow.ts.

org_integrations já era genérica por provider — só precisou do novo valor no enum.

Tamanho do PR

5.868 linhas / 20 arquivos, acima da faixa de referência do time (RFC 0028 §2.4, que pede justificativa a partir de 251 linhas). Justificativa: coletor, portões de qualidade e migration são uma unidade só — uma métrica sobre dado sem portão de qualidade na frente já se provou enganosa neste mesmo board real (mediana de lead time de fração de dia, causada por cards de setup). Separar UI do coletor foi considerado (era a intenção original, daí o "sem UI" da primeira versão desta descrição) e descartado: a UI é o que torna a validação contra o board real verificável por quem não vai ler os testes, e adiá-la não reduz o tamanho do coletor, que já é a maior parte do diff.

Portões de qualidade

Não são seção secundária. Em board real, dado sem portão produziu mediana de lead time de fração de dia — lisonjeiro e falso, causado por cards de setup criados e fechados minutos depois.

Gate Detecta
synthetic_items Títulos de placeholder inequívocos; ambíguos só com vida curta corroborando
mass_import Rajadas criadas no mesmo minuto e fechadas em minutos — import/backfill de board
done_not_closed Coluna terminal com issue aberta — invalida closedAt como fallback
bulk_movement 5+ itens movidos em 2 min; usa o wasAutomated do GitHub
field_completeness Preenchimento de priority/size/iteration/assignee
assignee_concentration Concentração no top-1
history_coverage Teto honesto da análise de duração — agora também sinaliza itens com histórico truncado (ver "Correções de revisão" abaixo)

Agnóstico de organização

Nada codifica um workflow específico. Nomes de coluna são texto livre, mapeados para buckets de ciclo de vida por config explícita por board e, na ausência dela, por heurísticas genéricas de nome (EN/PT). Coluna sem match é reportada, nunca tratada silenciosamente como não-terminal. Fixtures dos testes são fictícias.

Regras de honestidade implementadas

  • Lead time cai em escada transitions → closedAt → updatedAt; tudo além do primeiro degrau vira approximate. updatedAt nunca inventa lead time para trabalho em andamento.
  • P95 suprimido abaixo de 20 observações; abaixo de 10, só mediana + distribuição bruta.
  • Reentrada em coluna acumula as duas passagens em vez de sobrescrever.
  • Remoção do board é saída, nunca conclusão — throughput não conta.
  • Little's Law vem ao lado do lead time observado, não em vez dele.
  • assignee_concentration descreve o board, nunca uma pessoa: não retorna login, só o percentual (Princípio build(deps): Bump actions/checkout from 4 to 6 #2) — confirmado por revisão de código e coberto por teste de regressão.

Correções de revisão (2026-09-16)

Revisão linha a linha (Claude Sonnet 5) encontrou e corrigiu dois problemas de correção antes do merge:

  1. Colisão de repositório entre owners diferentes. resolveRepositoryId casava só pelo nome nu do repo, ignorando o owner. Um board que abrange mais de um owner GitHub (ou dois orgs com repo de mesmo nome) podia linkar um item ao repositório errado silenciosamente. Corrigido para casar por "owner/repo" derivado de remote_url primeiro — o mesmo fix que a integração Datadog já tinha feito pelo mesmo motivo — caindo para o nome nu só quando o repo não tem remote_url registrado.
  2. history_truncated era escrito e nunca lido. A migration e o sync já marcavam itens cujo timeline excedeu a paginação, mas nenhuma query selecionava a coluna, nenhum tipo a carregava, nenhum portão a checava. Um item com histórico muito longo tinha seu lead time reportado como completo sem ressalva. Agora entra no gate history_coverage.

Nenhum problema de segurança encontrado (token nunca logado, criptografia reaproveita o padrão da migration 014 sem código novo, rota do cron com a mesma auth já existente, sem IDOR no ?board= da página — tudo já escopado por tenant no layout).

Testes: 382/382 passam nesta rodada (era 324 na versão original desta descrição; a diferença inclui os 8 testes novos destas correções mais o que foi acumulado em main entretanto). tsc --noEmit limpo. npm run lint sem erros novos.

Correções de revisão humana (2026-09-16, segunda rodada)

gabriel-gomes-clickbus e lucastribioliclickbus revisaram a PR e deixaram achados reais — commit 5559c60:

De lucastribioliclickbus (3 comentários inline, todos no caminho de escrita):

  1. sync.ts:427 — history_available marcado por intenção, não por resultado. markHistoryAvailable marcava todo item tentado nesta rodada como disponível, mesmo quando fetchStatusHistory não trouxe timeline de volta (conteúdo deletado/inacessível no meio do sync não deixa entrada em eventsByContentId). Extraída a classificação para classifyHistoryResults (pura, testada), que só marca disponível o que de fato voltou.
  2. board-flow.ts:245 — flowEfficiency contava zero para item sem transições. activeHours = 0 para um item sem visitas é ausência de dado, não uma medição — mas leadTimeHours ainda podia resolver via fallback de closedAt, produzindo um flowEfficiency: 0 real onde a Regra 1 do próprio arquivo pede null. Corrigido para exigir ao menos uma visita real.
  3. client.ts:276 — conteúdo inacessível virava DRAFT_ISSUE. content: null (issue/PR deletado, transferido, ou fora do escopo do token) caía no mesmo fallback de um draft de verdade, e o gate history_coverage dizia "são itens draft" para itens que não são drafts. Novo tipo UNKNOWN, distinto de DRAFT_ISSUE, propagado pelo client, pelos tipos, pela migration 023 (CHECK constraint) e pela mensagem do gate.

De gabriel-gomes-clickbus (5 achados, nenhum bloqueante):

Os três últimos não bloqueiam a corretude do que está aqui e resolver os três juntos inflaria ainda mais uma PR já grande — ficam rastreados para entrar depois deste merge.

Boas Práticas a Seguir

  • Token com escopo mínimo (read:project), cifrado em repouso no padrão da migration 014
  • Nenhuma escrita no board — integração read-only
  • Falha de sync nunca derruba o cron (syncOrganization não lança; grava last_error)
  • Sync incremental: só relê timeline de item cujo updatedAt mudou
  • Paginação explícita nos reads internos (evita o cap silencioso de max-rows do PostgREST)

Checklist

  • Testes unitários — 401 no total no branch (era 324 na versão original desta descrição); os novos desta rodada cobrem os 3 bugs do Lucas + client.ts/needsHistoryFetch pedidos pelo Gabriel
  • Os 8 casos de borda da spec cobertos por fixture
  • npx tsc --noEmit limpo
  • npm run lint sem erros e sem warnings nos arquivos novos
  • npx vitest run — 401 passed (29 arquivos)
  • npm run build verde, incluindo a rota /[tenant]/flow
  • Sem secrets no código
  • Logs sem dados sensíveis
  • Validado contra board real via scripts/board-flow-dryrun.ts — 198 itens, 754 eventos, ~14 requests, 17s
  • Rebaseado em main (2026-09-16, duas vezes — v1.7.0 e depois v1.8.0) — sem conflitos, suíte inteira revalidada em cada
  • Revisão de código linha a linha (Claude Sonnet 5) — ver "Correções de revisão" acima
  • Comentários de gabriel-gomes-clickbus e lucastribioliclickbus respondidos — 3 bugs corrigidos, 2 gaps de teste fechados, 3 achados de escala rastreados em [FEAT] UI para conectar um board GitHub Projects V2 #253/[DEBT] Cron sync-integrations sem isolamento de tempo por integração #254/[DEBT] computeCfd recalculado a cada request SSR em /flow, sem cache #255
  • Migration 023 aplicada em staging — ainda pendente. Nem o CI (Platform (Next.js) só roda lint/tsc/vitest/build com env placeholder, sem Postgres real) nem este ambiente de revisão têm acesso a um Postgres real para validar isso; precisa de alguém com acesso a staging/Supabase antes do merge.
  • Sync + upsert exercitados contra Postgres real — mesmo motivo acima (exige Docker/Supabase local, indisponível nos dois ambientes que já passaram por isto)

Validação contra board real

scripts/board-flow-dryrun.ts lê um board real pelo client real e roda gates e métricas reais, sem tocar banco. Rodado num board de 198 itens / 754 eventos (~14 requests GraphQL, 17s) — e achou três defeitos que os testes unitários não pegariam:

  1. test/teste isolados eram falso positivo. Flagravam 3 itens legítimos ("Permitir teste de cenários de rebooking") e zero placeholders — 100% de erro naquele caminho. Num board de engenharia, testar é o trabalho. Marcadores ambíguos agora exigem vida curta corroborando.
  2. Import em massa não é scaffolding. 91 dos 198 itens foram criados em lote e fechados minutos depois — todos trabalho real, importados na criação do board. Mesma distorção, remédio diferente: placeholder você apaga, import você exclui do cálculo de duração. Virou gate próprio.
  3. Coluna renomeada era contada duas vezes. "Ready for Deploy" e "Ready for deploy" apareciam como duas fases (n=11 e n=24 em vez de n=35). Agora a chave é o nome normalizado.

Também: a coluna terminal saiu do tempo-por-fase — Done liderava com mediana de 39 dias, que é idade desde a entrega, não fluxo.

O que ainda não foi validado: o caminho de escrita (migration + sync + upsert). Exige Docker/Supabase local, indisponível em todas as máquinas que já passaram por este PR até agora.

Autoria Assistida

  • Agente: Claude Code (modelo: Claude Opus 5, 1M context, implementação original; Claude Sonnet 5, revisão de código e correções em 2026-09-16, em duas rodadas — a segunda respondendo aos comentários de gabriel-gomes-clickbus e lucastribioliclickbus)
  • Escopo da assistência: investigação da API (sondas GraphQL de validação), desenho da arquitetura, implementação completa (migration, client, sync, portões, métricas, testes, docs), o dry-run contra board real com as três correções que ele revelou, uma revisão de código linha a linha própria, e depois a correção dos 3 bugs de caminho de escrita achados pela revisão humana de lucastribioliclickbus mais a cobertura de teste pedida por gabriel-gomes-clickbus.
  • Revisão humana: parcialmente concluída. lucastribioliclickbus revisou a PR inteira e deixou 3 comentários inline, todos corrigidos e respondidos. gabriel-gomes-clickbus deixou 5 achados de escala; 2 foram corrigidos (cobertura de teste) e 3 viraram issues ([FEAT] UI para conectar um board GitHub Projects V2 #253, [DEBT] Cron sync-integrations sem isolamento de tempo por integração #254, [DEBT] computeCfd recalculado a cada request SSR em /flow, sem cache #255) por serem escopo maior que este PR. Falta: confirmação do autor original (renatoguimaraescb) sobre a decisão de manter a UI nesta PR (ver "Por que a UI entra nesta PR") — uma chamada de escopo que cabe a quem definiu o escopo original, não à revisão automatizada.
  • Issue de rastreamento: [FEAT] Board flow analysis from GitHub Projects V2 #252 (aberta retroativamente — este PR não tinha uma issue correspondente, o que a Issue Tracking rule do CLAUDE.md pede; a issue formaliza o que já estava decidido e implementado aqui, no mesmo espírito do ADR retroativo que o PR [DEBT] CLAUDE.md out of date: Stage 3 opened in May; "external integrations" non-goal never amended #238 fez para a integração Datadog).

Status

Os 3 bugs de caminho de escrita achados pela revisão humana estão corrigidos e respondidos. Falta: confirmação do autor original sobre a decisão de escopo da UI, e aplicar a migration em staging — os dois itens não marcados no checklist seguem bloqueando o merge.

@renatoguimaraescb renatoguimaraescb added the ai-assisted PR gerado total ou parcialmente com auxílio de agente de IA (RFC 0028 §2.2.7) label Aug 23, 2026
@vercel

vercel Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
clickbus-iris Ready Ready Preview Sep 16, 2026 9:11pm UTC

Request Review

@lucastribioliclickbus lucastribioliclickbus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Li a PR inteira. A decisão de arquitetura (timeline events em vez de snapshot + diff) está certa e bem defendida, os gates de qualidade são o ponto mais forte, e a doc em docs/integrations/github-projects.md é excelente.

Deixei 3 comentários inline, todos no caminho de escrita — que é exatamente o que os dois checkboxes abertos dizem que ainda não rodou contra Postgres. Os três corrompem números que a tela mostra como confiáveis:

  1. sync.ts:427 — history_available marcado por intenção de buscar, não por resultado (e diverge do dry-run que validou o board real).
  2. board-flow.ts:245 — flowEfficiency conta zero para item sem transições, contra a regra 1 do próprio arquivo.
  3. client.ts:276 — conteúdo inacessível vira DRAFT_ISSUE, e o gate reporta a causa errada ao usuário.

Tenho mais uma dúzia de achados de severidade média/baixa (paginação por OFFSET sem ordenação determinística, ausência de AbortSignal.timeout que o client Datadog tem, client.ts sem testes, entre outros) — te passo por fora para não poluir a thread, e você decide o que entra nesta PR e o que vira issue.

Comment thread platform/lib/integrations/github-projects/sync.ts
Comment thread platform/lib/queries/board-flow.ts
Comment thread platform/lib/integrations/github-projects/client.ts Outdated
@gabriel-gomes-clickbus

Copy link
Copy Markdown

Vou deixar uns achados aqui e vc vê se faz sentido.
Não são bloqueantes, mas talvez sejam importantes para escala.

  • ❌ Não existe caminho para conectar um board.
    settings/integrations/page.tsx:27 só lista PROVIDERS = [{ id: "datadog" }], e não há rota de API para gravar credenciais/config.boards de github_projects. O commit mais recente (cbaefca) tornou /flow alcançável via nav para todo tenant, mas todo tenant vai cair permanentemente no estado "não configurado" — não há forma de sair dele sem inserir uma linha manualmente no banco.
  • 🟠 platform/src/app/api/cron/sync-integrations/route.ts:63-92 — o novo provider foi anexado ao mesmo loop sequencial de uma única invocação com maxDuration=300 (linha 14), compartilhado com todo o sync do Datadog. Sem isolamento por integração: um board grande em primeiro sync (o dry-run do próprio PR levou ~17s para 198 itens) pode consumir boa parte do orçamento de 5min e atrasar/matar o sync de organizações Datadog que vierem depois na mesma lista, sem retry isolado.
  • 🟡 platform/lib/queries/board-flow.ts:538-569 (computeCfd) — O(semanas × itens × visitas), recalculado a cada request SSR de /flow (sem cache/ISR em page.tsx). Em um board com histórico longo e muitos itens, isso roda inteiro a cada carregamento de página.
  • 🟡 platform/lib/integrations/github-projects/client.ts — 558 linhas de parsing (mapeamento de campos, filtro por project.id no timeline, paginação/truncamento) sem nenhum teste. São funções puras (parseItem, readField, parseEvents), testáveis sem I/O — diferente do resto do sync.
  • 🟡 platform/lib/integrations/github-projects/sync.ts:299 — needsHistoryFetch (a lógica pura que sustenta a promessa central do design — "custo diário proporcional à atividade do board") não tem teste. Só readBoardConfig é testado no arquivo. Isso é consistente com a convenção já existente em datadog-sync.test.ts (que também só testa helpers puros), então não é uma regressão introduzida por este PR — mas é a lógica mais arriscada do sync e continua sem cobertura.

renatoguimaraescb and others added 6 commits September 16, 2026 17:57
Adds an ingestion + metrics module for GitHub Projects boards: lead time,
time per column, throughput, WIP aging, CFD and bottleneck signals. The
engine deliberately measures PR-open-to-merge and says nothing about the
queue in front of it; board data is the missing denominator. If AI
shortens coding but total lead time does not move, the constraint is
outside the code — that is the question this makes answerable.

No snapshot collector, and that is the design decision worth reviewing.
Projects V2 exposes ProjectV2ItemStatusChangedEvent on the content's
timeline, carrying createdAt, previousStatus, status and wasAutomated.
Transition history is therefore readable retroactively at second
precision on the first sync — no accumulation period, no +/-24h detection
window, no blind spot for two moves inside one interval. Verified against
a live board before writing any code.

Structure follows the Datadog integration: a client, a syncOrganization
that never throws, idempotent upserts keyed by the provider's own node
ids, and a slot in the existing 04:00 UTC cron. Metrics live in
lib/queries as pure functions, unit-tested like cycle-time-flow.ts.

Quality gates run before any metric is trusted. Un-gated board data
produced a median lead time of a fraction of a day on a real board —
flattering and false, caused by setup cards created and closed minutes
apart. Six gates report severity, the measured value, affected items and
the impact on the reading; metrics still compute, but never without the
caveat.

Nothing encodes a particular workflow. Column names are free text, mapped
to lifecycle buckets by per-board config first and generic EN/PT name
heuristics second; unmatched columns are reported, never silently treated
as not-done. Fixtures are fictional.

Honesty rules that shaped the code:
- lead time falls back transitions -> closedAt -> updatedAt, and anything
  past the first rung is labelled approximate; updatedAt is never used to
  invent a lead time for work still in flight
- P95 withheld below 20 observations, everything but the median below 10
- re-entering a column accumulates both visits instead of overwriting
- removal from the board is an exit, never a completion
- drafts have no timeline on the API, so they are excluded from duration
  metrics rather than counted as zero, with coverage reported
- assignee concentration describes the board, never a person: no login is
  returned, only the share (Principle #2)

Little's Law is returned beside observed lead time, not instead of it — a
large divergence points at phantom WIP or a mis-mapped terminal column.

No UI in this change: the collector and the gates are the foundation, and
metrics over unvalidated data are worth nothing. Dashboard follows once
the numbers are validated against a real board.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y-run

Adds scripts/board-flow-dryrun.ts — reads a real board through the real
client and runs the real gates and metrics without touching a database.
Ran it against a 198-item board with 754 status events (~14 GraphQL
requests, 17s), which surfaced three defects the unit tests could not.

1. Testing is real work. `synthetic_items` matched test/teste on their
   own and flagged three genuine items ("Permitir teste de cenários de
   rebooking", "Teste não moderado nova UI mobile") while catching zero
   placeholders — a 100% false-positive rate on that path. Unambiguous
   markers (dummy, asdf, lorem) still fire alone; ambiguous words now
   need a short lifetime to corroborate.

2. Import is not scaffolding. 91 of the 198 items were created in
   same-minute batches and closed minutes later — all real work,
   imported when the board was set up. Same distortion as a test card
   (near-zero lead time, throughput spike in one artificial week) but a
   different remedy: you delete a placeholder, you exclude an import
   from duration analysis. Split into its own `mass_import` gate so the
   finding names what actually happened.

3. Renamed columns were counted twice. Per-phase stats keyed on the raw
   name, so "Ready for Deploy" and "Ready for deploy" — one column,
   renamed — split into two rows with a misleadingly small n each
   (11 and 24 instead of 35). Now keyed on the normalized name, labelled
   with the most frequent spelling. Genuinely different names for the
   same stage stay separate; merging those needs explicit statusConfig,
   since guessing would be wrong elsewhere.

Also excludes the terminal column from time-per-phase: an item sits in
Done until archived, so its time there measured age since delivery and
dominated the ranking at a 39-day median.

Regression tests added for each, including the three real titles that
were wrongly flagged.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ntry

The ingestion module had no way in. Adds the route, the read layer it
needed, and the nav entry.

Read layer (lib/queries/board-flow-data.ts) loads items and events and
hands them to the existing pure functions, so every calculation stays
unit-testable without a database. Reads paginate explicitly: PostgREST
caps responses at the project's max-rows, and a board above that would
come back silently truncated — the failure mode behind #121.

The route degrades instead of erroring. A missing schema (migration 023
not applied) and an org with no board both render an unconfigured state,
so the nav entry can ship before the migration lands rather than 500ing
on a deployment that hasn't migrated.

Section order is the spec's and it is deliberate: quality gates before
any number, so the reader knows what the figures can carry before
reading them. Then durations, time per column, WIP aging, throughput,
CFD, stalled items, Little's Law.

Two visualization decisions:

- The CFD groups by lifecycle bucket, not by column. The live board has
  17 columns; stacking that many bands is unreadable, while five buckets
  make accumulation obvious. Needed the resolved bucket per column, so
  summarizeBoard now returns `statusBuckets`.
- Ran the product's categorical ramp through a palette validator. It
  passes colour-vision separation (worst adjacent pair ΔE 18.6, target
  8) but four of five slots fall below 3:1 contrast against the page
  surface. That obligates relief, so every mark is paired with a visible
  label or rendered as a table — identity is never colour alone. Same
  reason the gates lead with an icon and the severity word, not a dot.

Nav entry lives in tenantNavItems, which the sidebar and the mobile
sheet share, so one entry covers both. Translations in en-US and pt-BR;
es-ES falls back to en-US per the existing convention.

Not verified: the page has not been rendered against real data. The
schema is not applied anywhere yet and this machine has no Supabase
credentials, so only the empty and unconfigured states are reachable
locally. Build and types pass; visual confirmation is still owed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion in board flow

Two correctness gaps found in code review before merge:

- resolveRepositoryId matched board items to Iris repositories by bare
  name only, ignoring the owner. A board spanning more than one GitHub
  owner (or two orgs sharing a repo name) could silently link an item
  to the wrong repository. Now matches on remote_url-derived
  "owner/repo" first, same fix the Datadog integration already made
  for the same reason, falling back to bare-name matching only for
  repos with no remote_url on file.
- history_truncated was written by sync but never read: no query
  selected it, no type carried it, no gate surfaced it. An item whose
  timeline exceeded the pagination cap had its lead time reported as
  complete with no caveat. Wired into the history_coverage gate
  alongside history_available.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses lucastribioliclickbus's review of #170 — all three corrupt
numbers the UI shows as reliable:

- sync.ts: markHistoryAvailable marked history_available=true for every
  item *attempted* this run, not just the ones that actually got a
  timeline back. A node coming back null (content deleted mid-sync) or
  without timelineItems left no entry in eventsByContentId, but was
  still marked available. Extracted the classification into a pure,
  now-tested classifyHistoryResults.
- board-flow.ts: flowEfficiency divided activeHours (0 for an item with
  no transitions, by construction) by a lead time that can still
  resolve via the closedAt/updatedAt fallback ladder — reporting a real
  0% instead of "unknown", against the file's own Rule 1. Now null
  unless there is at least one real visit.
- client.ts: content: null (deleted, transferred, or access-revoked
  issue/PR) parsed as DRAFT_ISSUE by toContentType's fallback, so the
  history_coverage gate told the user "these are drafts" for items that
  are not drafts at all. Added a distinct UNKNOWN content type end to
  end (client, types, migration 023's CHECK, the gate's message).

Also adds the test coverage gabriel-gomes-clickbus flagged as missing:
client.ts's parseItem/toContentType (previously zero tests) and
needsHistoryFetch, the pure logic behind the sync's daily-cost
guarantee.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@clickmatos

Copy link
Copy Markdown
Contributor

Obrigado pelos achados — tratei cada um:

🟡 client.ts sem testes: corrigido em 5559c60. Adicionei github-projects-client.test.ts cobrindo parseItem/toContentType (achei um bug real no caminho, ver resposta ao Lucas abaixo).

🟡 needsHistoryFetch sem teste: corrigido em 5559c60. Exportei a função e adicionei os 6 casos (novo item, item conhecido sem histórico, updatedAt mudou, item completo e parado, force, draft excluído de saída).

Os outros 3 (❌ conectar board, 🟠 isolamento do cron, 🟡 cache do CFD) não entraram nesta PR — nenhum bloqueia a corretude do que já está aqui, e resolver os 3 juntos ia inflar ainda mais uma PR que já está grande. Abri issues rastreando cada um, com o achado seu citado:

Se discordar da priorização ou quiser puxar algum desses para dentro desta PR, avisa.

This branch was successfully deployed

1 active deployment
Preview — 5559c600 Deployed Sep 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted PR gerado total ou parcialmente com auxílio de agente de IA (RFC 0028 §2.2.7)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] Board flow analysis from GitHub Projects V2

4 participants