Перейти к основному содержимому

ADR 0122 — SessionPage.tsx, разделённый на пять механических PR, объявленных заранее

  • Статус: принят
  • Дата: 2026-08-29
  • Ссылается (без правки): ADR 0121 (тот же формат рассуждения, что и здесь, — общий файл вырос за точку, в которой «просто обходить» дешевле, чем провести границу, — но другой формат исполнения: 0121 был коммитом с нулевым diff, этот — пять последовательных PR, потому что файл находится под активным изменением фич, и единый PR либо заморозил бы код, либо вступил бы в гонку со всеми остальными изменениями).

Контекст​

apps/web/src/routes/SessionPage.tsx насчитывает 3 807 строк и 169 KiB — ревизия техдолга в docs/explanation/backlog.md уже содержит для него строку:

Decompose SessionPage.tsx / ProjectSettingsTab.tsx | G | SessionPage.tsx is 169 KiB with 25 test files importing it; ProjectSettingsTab.tsx is 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

Три конкретные издержки делают это чем-то большим, чем жалоба на размер:

  1. От него зависят 25 тестовых файлов (SessionPage.*.test.tsx плюс SessionPage.ponto.test.ts) — любое структурное изменение рискует широким радиусом воздействия, а размер файла затрудняет заранее понять, какие из 25 затронет конкретное изменение.
  2. Файл находится под активным изменением, это не унаследованный код. Его касались 50 коммитов; 15 самых свежих почти все — добавления feat (структурированные вопросы, handoff, полоса активности хода, упорядочение ход/исход из RN-172…), и ни одно из них не было извлечением — это первая попытка провести внутреннюю границу в истории файла. Это исключает переписывание за один раз: заморозка файла ради одного большого PR заблокировала бы все прочие фичи, попадающие в него, или вынудила бы к болезненным повторным rebase.
  3. Набор тестов уже проводит часть границы. До этого PR SessionPage.ordenacao-e-avisos.test.tsx импортирует aberturasDeTurno, turnoDoSeq, afundarDesfechos, ordemDaAcaoNaTimeline и agruparNarracoesDoTurno напрямую по имени из ./SessionPage (await import('./SessionPage'), с деструктуризацией), а SessionPage.ponto.test.ts делает то же с pontoDaSessao — не рендеря компонент, а как простые вызовы функций. Тесты уже решили, что эти пять (плюс agruparNarracoesDoTurno) — независимые единицы; этот ADR возводит существующую границу в реальную границу модуля, а не изобретает новую.

С учётом активного изменения и радиуса воздействия действует постоянное правило CLAUDE.md — «это не рефакторинг мимоходом», — поэтому это отдельная фаза, спланированная как последовательность независимо сливаемых PR, нейтральных по поведению, каждый из которых подтверждается теми же 25 тестовыми файлами, проходящими без правки.

Решение​

Пять PR, выполняемых и сливаемых по одному (никогда параллельно — каждый попадает в dev до начала следующего), в таком порядке:

  1. Чистые помощники timeline/хода → apps/web/src/lib/session-timeline.ts (этот PR). aberturasDeTurno, turnoDoSeq, afundarDesfechos, pontoDaSessao, ordemDaAcaoNaTimeline и общий для них тип TimelineEntry. Все пять подтверждены как чистые — без JSX, без ссылок на styles/CSS module, без замыкания на состояние компонента, — что делает этот срез наименее рискованным и правильным для первого шага: механическое изменение .ts, без чего-либо, что нужно согласовывать с циклом рендера React. agruparNarracoesDoTurno намеренно остаётся в SessionPage.tsx, хотя находится в той же области и тестируется так же: он возвращает JSX (<Disclosure>) и читает три имени классов styles.narracoes* из SessionPage.module.css. Перенос означал бы решение, что lib/ принимает свой первый файл .tsx — сегодня в ней только исходники .ts (есть несколько тестов .tsx, но ноль исходных файлов .tsx), — и этот прецедент принадлежит PR, который действительно посвящён JSX-подкомпонентам (PR 2), а не тому, что должен быть лишь изменением чистых функций.

  2. StorySlide → apps/web/src/routes/StorySlide.tsx. Первый переносимый JSX-подкомпонент и самый простой: единственный листовой компонент (слайд карусели продвижения, RN-148) без потомков, которые тоже нужно переносить. Это PR, который решает вопрос совместного использования SessionPage.module.css, рассмотренный ниже.

  3. StructuredQuestionCard + permiteOutra → apps/web/src/routes/StructuredQuestionCard.tsx. Ещё один самодостаточный JSX-подкомпонент (форма структурированного вопроса из RN-162/RN-171) плюс единственный чистый помощник, от которого он приватно зависит, — переносятся вместе, потому что отделение компонента от единственного помощника, который только он вызывает, добавило бы файл без пользы для какого-либо читателя.

  4. Помощники дерева backlog + ItemDeBacklog + ContextAside → apps/web/src/lib/session-backlog-tree.ts + apps/web/src/routes/ContextAside.tsx. Самый большой срез: чистые помощники сборки дерева (их естественный дом — lib/, то же рассуждение, что в PR 1) рядом с JSX, который их рендерит (ItemDeBacklog, ContextAside — естественный дом в routes/, то же рассуждение, что в PR 2–3). Он идёт после более простых изменений одиночных компонентов именно потому, что это извлечение в два файла и двух видов, и он выигрывает от того, что PR 2–3 уже однажды ответили на вопрос совместного использования CSS.

  5. Флаги готовности → apps/web/src/lib/session-readiness.ts, хук useSessionReadiness. Последний и единственный PR набора, который не является переносом файла: флаги готовности (затворы RN-160/RN-161 на кнопках Arquiteto/PO) вычисляются в строке из состояния и props, живущих в SessionPage.tsx, поэтому их извлечение означает написание нового хука с явным контрактом входа/выхода, а не перемещение существующей отдельной функции или компонента. Он идёт последним, потому что самый рискованный из пяти — граница хука может изменить, когда что-то пересчитывается, даже при неизменной формуле, — и к PR 5 остальные четыре уже уменьшат файл достаточно, чтобы этот риск было проще ревьюить изолированно.

Вопрос совместного использования SessionPage.module.css, решённый один раз​

У SessionPage.module.css сегодня ровно один импортёр — сам SessionPage.tsx (проверено: grep по спецификатору в apps/web/src вне тестовых файлов даёт один результат). Это значит, что PR 2–4 могут заставить свои только что извлечённые соседние файлы (StorySlide.tsx, StructuredQuestionCard.tsx, ContextAside.tsx) импортировать ../routes/SessionPage.module.css напрямую, так же, как сегодня делает SessionPage.tsx, — без разделения CSS, без новой общей таблицы стилей, без риска расхождения двух копий, потому что второй копии никогда не было. Это решение, которое стоит записать один раз, здесь, а не обсуждать заново в трёх отдельных ревью PR.

Явно вне рамок всей работы из пяти PR​

Две вещи остаются нетронутыми этим ADR и пятью PR:

  • Группировка состояния канала хода: turnoViaCanal, statusAgent, pensandoVisivel, atividadeDoTurno. Эти четыре состояния читаются И записываются четырьмя разными обработчиками входа хода (handleSend, handleReadiness, handleArchitectureReadiness, handleAcceptHandoff) и захватываются в замыкание finalizarTurnoDoAgente, который комментарий в коде называет «o ÚNICO lugar que finaliza um turno», — каждый из этих четырёх обработчиков включает turnoViaCanal, и только finalizarTurnoDoAgente его выключает. Это не непрерывное JSX-поддерево и не набор чистых функций; это переплетённый поток управления, и его перенос — это проектное решение о том, где должно жить состояние жизненного цикла хода, а не механическое перемещение. Он получит собственный ADR, позже и с отдельным номером, когда эта работа будет очерчена.
  • apps/web/src/routes/ProjectSettingsTab.tsx (~90 KiB, другой файл, упомянутый в той же строке backlog). Меньше, уже имеет собственный первый шаг в работе (именованные функции внутри файла, ещё не извлечённые в модули), и это отдельное решение о рамках — этот ADR покрывает только SessionPage.tsx.

Последствия​

  • Два человека могут работать в разных углах SessionPage.tsx с меньшим пересечением после того, как пять PR войдут, — та же польза, которую ADR 0121 описал для разделения схемы, в меньшем масштабе (пять извлечённых единиц, а не шестнадцать файлов).
  • Критерий приёмки каждого PR этого набора одинаков: ноль изменений наблюдаемого поведения, доказанный теми же 25 файлами SessionPage.*.test.tsx, проходящими без правки после каждого PR. PR этого набора, которому нужно править один из этих 25 файлов, чтобы пройти, на деле не механический и должен остановиться, а не «чинить» тест.
  • Группировка канала хода и ProjectSettingsTab.tsx остаются ровно такими же спорными, как раньше. Этот ADR не снижает их риск и не утверждает, что снижает, — называние их здесь удерживает срез объявленным, а не молча суженным. Будущий PR, затрагивающий группировку канала хода, нуждается в собственном ADR с новым номером, ссылающемся на этот; он не дополняет этот, по постоянному правилу, что принятый ADR никогда не редактируется.
  • lib/ получает свои первые файлы с типами, соседствующими с JSX, в форме TimelineEntry (у TimelineEntry из PR 1 есть поле ReactNode) не получая своего первого исходного файла .tsx — именно то, что agruparNarracoesDoTurno остаётся в SessionPage.tsx, сохраняет это свойство в PR 1. PR 2 расходует этот прецедент намеренно, один раз, с собственным ревью.
  • SessionPage.tsx уменьшается за пять видимых шагов (3 807 → меньше с каждым PR) вместо одного большого diff — проще ревьюить, но это также означает, что файл проводит время четырёх PR в частично декомпозированном состоянии, что здесь принимается как цена незамораживания активной работы над фичами.

Отвергнутые альтернативы​

  • Один большой PR, переносящий всё сразу. Отвергнуто: файл находится под активным изменением (50 коммитов, последние 15 в основном добавления фич), поэтому единый PR либо заморозит всю остальную работу в SessionPage.tsx до слияния, либо будет накапливать конфликты слияния быстрее, чем его можно отревьюить. Разделение на пять независимо сливаемых PR позволяет обычной работе над фичами продолжаться между ними.
  • Перенести agruparNarracoesDoTurno вместе с остальными пятью в PR 1, раз он живёт в той же области исходника (строки 281–345) и тестируется так же. Отвергнуто: он возвращает JSX и читает styles.narracoes*, поэтому его перенос молча решил бы, что lib/ принимает исходные файлы .tsx, — реальный прецедент, заслуживающий собственного ревью, а не побочный эффект PR, представленного как «изменение чистых функций».
  • Извлечь группировку канала хода как часть этого же плана, раз это другой крупный блок состояния файла. Отвергнуто сразу: это поток управления, сцепленный между четырьмя обработчиками, а не граница, которую уже проводит набор тестов, и комментарий в коде уже называет его единственным местом, которое должно оставаться синхронизированным, — ровно тот тип изменения, который критерий приёмки этого ADR «механическое, доказуемо нейтральное по поведению» не может честно утверждать, что покрывает.