Skip to content

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

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

feat(platform): board flow analysis from GitHub Projects V2#170
renatoguimaraescb wants to merge 4 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 na plataforma.

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.

Sem UI nesta PR. Coletor + portões são a fundação; métrica sobre dado não validado não vale nada. Dashboard vem depois de validar os números contra um board real.

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.

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

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

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 — 57 novos (board-flow, board-quality, github-projects-sync)
  • 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 — 324 passed (27 arquivos)
  • npm run build verde
  • 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
  • Migration 023 aplicada em staging — pendente, precisa rodar antes do merge
  • Sync + upsert exercitados contra Postgres (exige Docker/Supabase local — não validado ainda)

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 na máquina onde isto rodou.

Autoria Assistida

  • Agente: Claude Code (modelo: Claude Opus 5, 1M context)
  • 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), e o dry-run contra board real com as três correções que ele revelou.
  • Revisão humana: pendente — spec original, decisões de escopo (timeline events em vez de snapshot, platform-first, tratamento de drafts) e requisito de agnosticismo definidos pelo autor; código ainda não revisado linha a linha.

Status

🚧 Work in Progress — aberto como Draft.

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>
@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 (UTC)
clickbus-iris Ready Ready Preview Aug 24, 2026 6:05pm

Request Review

renatoguimaraescb and others added 2 commits August 24, 2026 13:07
…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>

@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:427history_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:245flowEfficiency 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
@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.

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.

3 participants