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.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
Три конкретные издержки делают это чем-то большим, чем жалоба на размер:
- От него зависят 25 тестовых файлов (
SessionPage.*.test.tsxплюсSessionPage.ponto.test.ts) — любое структурное изменение рискует широким радиусом воздействия, а размер файла затрудняет заранее понять, какие из 25 затронет конкретное изменение. - Файл находится под активным изменением, это не унаследованный код. Его
касались 50 коммитов; 15 самых свежих почти все — добавления
feat(структурированные вопросы, handoff, полоса активности хода, упорядочение ход/исход из RN-172…), и ни одно из них не было извлечением — это первая попытка провести внутреннюю границу в истории файла. Это исключает переписывание за один раз: заморозка файла ради одного большого PR заблокировала бы все прочие фичи, попадающие в него, или вынудила бы к болезненным повторным rebase. - Набор тестов уже проводит часть границы. До этого 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 до начала следующего), в таком порядке:
-
Чистые помощники 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), а не тому, что должен быть лишь изменением чистых функций. -
StorySlide→apps/web/src/routes/StorySlide.tsx. Первый переносимый JSX-подкомпонент и самый простой: единственный листовой компонент (слайд карусели продвижения, RN-148) без потомков, которые тоже нужно переносить. Это PR, который решает вопрос совместного использованияSessionPage.module.css, рассмотренный ниже. -
StructuredQuestionCard+permiteOutra→apps/web/src/routes/StructuredQuestionCard.tsx. Ещё один самодостаточный JSX-подкомпонент (форма структурированного вопроса из RN-162/RN-171) плюс единственный чистый помощник, от которого он приватно зависит, — переносятся вместе, потому что отделение компонента от единственного помощника, который только он вызывает, добавило бы файл без пользы для какого-либо читателя. -
Помощники дерева 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. -
Флаги готовности →
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 «механическое, доказуемо нейтральное по поведению» не может честно утверждать, что покрывает.