ADR 0122 — SessionPage.tsx dividido em cinco PRs mecânicos, declarados de antemão
- Status: aceito
- Data: 2026-08-29
- Referencia (sem editar): ADR 0121 (o mesmo formato de raciocínio deste — um arquivo compartilhado cresceu além do ponto em que "só contornar" sai mais barato que traçar uma fronteira —, mas outro formato de execução: o 0121 foi um commit de diff zero, este são cinco PRs em sequência, porque o arquivo está sob mudança ativa de features e um PR único congelaria o código ou disputaria corrida com toda outra mudança).
Contexto
apps/web/src/routes/SessionPage.tsx tem 3 807 linhas e 169 KiB — a revisão
de dívida técnica de docs/explanation/backlog.md já tem uma linha para ele:
Decompose
SessionPage.tsx/ProjectSettingsTab.tsx| G |SessionPage.tsxis 169 KiB with 25 test files importing it;ProjectSettingsTab.tsxis 90 KiB | already informally called a "disputed file" in historico-de-fases.md, but never promoted to a tracked debt item; per the standing rule this is its own phase, never a drive-by refactor
Três custos concretos fazem disso mais que uma queixa de tamanho:
- 25 arquivos de teste dependem dele (
SessionPage.*.test.tsxmaisSessionPage.ponto.test.ts) — qualquer mudança estrutural arrisca um raio de impacto largo, e o tamanho do arquivo torna difícil saber de antemão quais dos 25 uma dada mudança pode tocar. - O arquivo está sob mudança ativa, não é código legado. 50 commits o
tocaram; os 15 mais recentes são quase todos acréscimos
feat(perguntas estruturadas, handoff, a faixa de atividade do turno, a ordenação turno/desfecho da RN-172…), e nenhum deles foi uma extração — esta é a primeira tentativa de traçar uma fronteira interna na história do arquivo. Isso descarta uma reescrita de uma vez: congelar o arquivo para um PR grande bloquearia toda outra feature que caísse nele ou forçaria rebases dolorosos e repetidos. - A suíte de testes já traça parte da fronteira. Antes deste PR,
SessionPage.ordenacao-e-avisos.test.tsximportaaberturasDeTurno,turnoDoSeq,afundarDesfechos,ordemDaAcaoNaTimelineeagruparNarracoesDoTurnodireto pelo nome de./SessionPage(await import('./SessionPage'), desestruturado), eSessionPage.ponto.test.tsfaz o mesmo compontoDaSessao— não renderizando o componente, mas como chamadas de função simples. Os testes já tinham decidido que essas cinco (maisagruparNarracoesDoTurno) são unidades independentes; este ADR promove essa fronteira existente a uma fronteira real de módulo, em vez de inventar uma nova.
Dados a mudança ativa e o raio de impacto, vale a regra permanente do
CLAUDE.md — "não é refatoração de passagem" —, então isto é uma fase
própria, planejada como uma sequência de PRs mergeáveis de forma
independente e neutros em comportamento, cada um evidenciado pelos mesmos 25
arquivos de teste passando sem edição.
Decisão
Cinco PRs, executados e mergeados um de cada vez (nunca em paralelo — cada um
chega à dev antes de o próximo começar), nesta ordem:
-
Helpers puros de timeline/turno →
apps/web/src/lib/session-timeline.ts(este PR).aberturasDeTurno,turnoDoSeq,afundarDesfechos,pontoDaSessao,ordemDaAcaoNaTimelinee o tipoTimelineEntryque eles compartilham. Os cinco foram confirmados puros — sem JSX, sem referência astyles/CSS module, sem closure sobre estado do componente —, o que faz desta a fatia de menor risco e a certa para ir primeiro: uma mudança mecânica de.ts, sem nada a conciliar com o ciclo de render do React.agruparNarracoesDoTurnofica emSessionPage.tsxde propósito, embora esteja na mesma região e seja testado do mesmo jeito: ele devolve JSX (<Disclosure>) e lê três nomes de classestyles.narracoes*deSessionPage.module.css. Movê-lo significaria decidir quelib/aceita seu primeiro arquivo.tsx— hoje ela só tem fontes.ts(existem alguns testes.tsx, mas zero arquivos fonte.tsx) —, e esse precedente pertence a um PR que de fato trate de subcomponentes JSX (PR 2), não a um que deveria ser só uma mudança de funções puras. -
StorySlide→apps/web/src/routes/StorySlide.tsx. O primeiro subcomponente JSX a mudar, e o mais simples: um único componente folha (o slide do carrossel de promoção, RN-148) sem filhos que também precisem mudar. É o PR que decide a questão do compartilhamento deSessionPage.module.css, tratada abaixo. -
StructuredQuestionCard+permiteOutra→apps/web/src/routes/StructuredQuestionCard.tsx. Outro subcomponente JSX autocontido (o formulário de pergunta estruturada da RN-162/RN-171) mais o único helper puro de que ele depende em privado — movidos juntos porque separar um componente do único helper que só ele chama acrescentaria um arquivo sem benefício para leitor nenhum. -
Helpers da árvore do backlog +
ItemDeBacklog+ContextAside→apps/web/src/lib/session-backlog-tree.ts+apps/web/src/routes/ContextAside.tsx. A maior fatia: helpers puros de montagem da árvore (a casa natural deles élib/, mesmo raciocínio do PR 1) ao lado do JSX que os renderiza (ItemDeBacklog,ContextAside— casa natural emroutes/, mesmo raciocínio dos PRs 2–3). Vem depois das mudanças mais simples de componente único justamente por ser uma extração de dois arquivos e de dois tipos, e ganha com os PRs 2–3 já terem respondido uma vez a questão do compartilhamento de CSS. -
Flags de prontidão →
apps/web/src/lib/session-readiness.ts, um hookuseSessionReadiness. O último, e o único PR do conjunto que não é uma mudança de arquivo: as flags de prontidão (os portões da RN-160/RN-161 nos botões do Arquiteto/PO) são calculadas em linha a partir de estado e props que moram emSessionPage.tsx, então extraí-las significa escrever um hook novo com contrato explícito de entrada/saída, não realocar uma função ou componente avulso existente. Vem por último porque é o mais arriscado dos cinco — uma fronteira de hook pode mudar quando algo é recalculado mesmo com a fórmula intacta —, e no PR 5 os outros quatro já terão encolhido o arquivo o bastante para esse risco ser mais fácil de revisar isolado.
A questão do compartilhamento de SessionPage.module.css, respondida uma vez
SessionPage.module.css tem hoje exatamente um importador — o próprio
SessionPage.tsx (verificado: um grep pelo especificador em apps/web/src
fora dos arquivos de teste devolve um resultado). Isso quer dizer que os
PRs 2–4 podem fazer seus arquivos irmãos recém-extraídos (StorySlide.tsx,
StructuredQuestionCard.tsx, ContextAside.tsx) importarem
../routes/SessionPage.module.css direto, do mesmo jeito que
SessionPage.tsx faz hoje — sem dividir o CSS, sem folha de estilo
compartilhada nova, sem risco de as duas cópias divergirem, porque nunca
houve uma segunda cópia. É uma decisão que vale escrever uma vez, aqui, em
vez de rediscuti-la em três revisões de PR separadas.
Explicitamente fora do escopo de todo o esforço de cinco PRs
Duas coisas ficam intocadas por este ADR e pelos cinco PRs:
- O agrupamento de estado do canal de turno:
turnoViaCanal,statusAgent,pensandoVisivel,atividadeDoTurno. Esses quatro estados são lidos E escritos por quatro handlers diferentes de entrada de turno (handleSend,handleReadiness,handleArchitectureReadiness,handleAcceptHandoff) e capturados em closure porfinalizarTurnoDoAgente, que o comentário no código chama de "o ÚNICO lugar que finaliza um turno" — cada um desses quatro handlers ligaturnoViaCanal, e sófinalizarTurnoDoAgenteo desliga. Não é uma subárvore JSX contígua nem um conjunto de funções puras; é fluxo de controle entrelaçado, e movê-lo é uma decisão de desenho sobre onde o estado do ciclo de vida do turno deve morar, não uma realocação mecânica. Ele ganha um ADR próprio, posterior e com número separado, quando esse trabalho for delimitado. apps/web/src/routes/ProjectSettingsTab.tsx(~90 KiB, o outro arquivo citado na mesma linha do backlog). Menor, já tem um primeiro passo próprio em andamento (funções nomeadas dentro do arquivo, ainda não extraídas para módulos), e é uma decisão de escopo separada — este ADR cobre sóSessionPage.tsx.
Consequências
- Duas pessoas podem trabalhar em cantos diferentes de
SessionPage.tsxcom menos sobreposição depois que os cinco PRs entrarem — o mesmo benefício que o ADR 0121 descreveu para a divisão do schema, em escala menor (cinco unidades extraídas, não dezesseis arquivos). - O critério de aceite de todo PR deste conjunto é o mesmo: zero mudança de
comportamento observável, provado pelos mesmos 25 arquivos
SessionPage.*.test.tsxpassando sem edição depois de cada PR. Um PR deste conjunto que precise editar um desses 25 arquivos para passar não é, de fato, mecânico, e deve parar em vez de "consertar" o teste. - O agrupamento do canal de turno e
ProjectSettingsTab.tsxcontinuam exatamente tão disputados quanto antes. Este ADR não reduz o risco deles e não afirma reduzir — nomeá-los aqui é o que mantém o corte declarado em vez de estreitado em silêncio. Um PR futuro que toque o agrupamento do canal de turno precisa de um ADR próprio, com número novo, que referencie este; ele não emenda este, pela regra permanente de que ADR aceito nunca é editado. lib/ganha seus primeiros arquivos com tipos vizinhos de JSX no formato deTimelineEntry(oTimelineEntrydo PR 1 tem um campoReactNode) sem ganhar seu primeiro arquivo fonte.tsx— éagruparNarracoesDoTurnoficar emSessionPage.tsxque preserva essa propriedade no PR 1. O PR 2 é o que gasta esse precedente, de propósito, uma vez, com revisão própria.SessionPage.tsxencolhe em cinco passos visíveis (3 807 → menor a cada PR) em vez de um diff grande — mais fácil de revisar, mas também quer dizer que o arquivo passa o tempo de quatro PRs num estado parcialmente decomposto, o que aqui se aceita como o custo de não congelar o trabalho ativo de features.
Alternativas descartadas
- Um PR grande movendo tudo de uma vez. Recusado: o arquivo está sob
mudança ativa (50 commits, os 15 últimos na maioria acréscimos de feature),
então um PR único ou congela todo outro trabalho em
SessionPage.tsxaté ser mergeado ou acumula conflitos de merge mais rápido do que pode ser revisado. Dividir em cinco PRs mergeáveis de forma independente deixa o trabalho normal de features continuar entre eles. - Mover
agruparNarracoesDoTurnojunto com os outros cinco no PR 1, já que ele mora na mesma região do fonte (linhas 281–345) e é testado do mesmo jeito. Recusado: ele devolve JSX e lêstyles.narracoes*, então movê-lo decidiria em silêncio quelib/aceita arquivos fonte.tsx— um precedente real que merece revisão própria, não um efeito colateral de um PR apresentado como "mudança de função pura". - Extrair o agrupamento do canal de turno como parte deste mesmo plano, já que é o outro grande bloco de estado do arquivo. Recusado de saída: é fluxo de controle acoplado entre quatro handlers, não uma fronteira que a suíte de testes já traça, e o comentário no código já o nomeia como o único lugar que precisa ficar sincronizado — exatamente o tipo de mudança que o critério de aceite "mecânico, comprovadamente neutro em comportamento" deste ADR não pode afirmar com honestidade que cobre.