20 KiB
SPEC-REVIEW-266-r1
Issue: #266 — «Рефакторинг 3/5: расщепить styles.ts»
ТЗ: docs/specs/266-split-styles.md (коммит 4c93f28e, ветка issue/266-split-styles)
Этап: spec (PROCESS.md §2.4) · заход r1 · блокирующих циклов израсходовано 0/4
Вердикт: жёлтый
Скоуп ревью
Класс изменения — A (src/**), но задача помечена как чистый refactor
(tech-debt, User-Visible: no): перенос правил из монолитного src/styles.ts
(3690 строк, один экспорт cardStyles) в пять файлов src/styles/*.styles.ts
по поверхностям + сборщик. Продуктового поведения нет и не должно появиться —
главный заявленный инвариант ТЗ: «пиксели не меняются».
Ревью велось состязательно: без переписки с автором, по тексту ТЗ, телу issue
и фактическому состоянию кода/гейтов на dev.
Как проверялось
- Прочитаны
docs/SCOPE.md,PROCESS.md,AGENTS.md(полностью, включая §7.1, §8, §10.2–10.4). - Прочитано тело issue #266 и комментарий аналитики/автора (единственные два комментария, ни одного цикла ревью до этого не было — подтверждает r1).
- Сверены с кодом фактические числа из ТЗ:
wc -l src/styles.ts→ 3690 строк — совпадает с текстом ТЗ (§0).node -e "import('./demo/golden/matrix.mjs').then(m=>console.log(m.GOLDEN_SCENARIOS.length))"→ 129 сцен — совпадает с AC3.- Потребители
cardStyles:houseplan-card.ts,hp-device-preview.ts,space-card.ts;editorSecondaryStylesподключается вторым элементом массива вhouseplan-card.ts:21973— совпадает с §1.2. - Грубая (regex-based) бакетизация правил по кластерам из §1.1 показала явный
перекос:
plan≈ 1300–1400 строк против заявленного лимита ≤ 1200 (AC2) — см. находку L2 ниже, оставлено как наблюдение, не как блокирующее. - Найдены и прочитаны все 18
@mediaблоковsrc/styles.ts(8×prefers-reduced-motion, 2×forced-colors, 2×prefers-color-scheme, 4×max-width, плюс дубли) и 11@keyframes— см. находку M1. - Проверено покрытие golden этих состояний:
demo/golden/harness.mjs:49иdemo/golden/run.mjs:607— golden всегда эмулируетreducedMotion:'reduce'и не эмулируетforced-colorsвовсе. - Проверено покрытие smoke:
demo/smoke_plan_snap_overlay.mjs:354-367(forcedColors:'active') иdemo/smoke_preloader.mjs:120-121(reducedMotion:'reduce', фаза «house does not pulse»);grepпоtest/*.mjsнаforced-colors/prefers-reduced-motion— толькоtest/color-picker.test.mjs(не относится к затронутым блокам) иtest/isometric-contract.test.mjs(проверяет только наличие класса в разметке, не CSS-блокforced-colorsнаsrc/styles.ts:594-601). - Проверена классификация файла
scripts/dev/styles-diff.mjsпротив таблицы классов PROCESS.md §1 — см. находку L1. docs/ARCHITECTURE.md:17содержит дерево сeditor-secondary.styles.ts, новый каталогsrc/styles/туда естественно вписывается — скоуп «раздел ARCHITECTURE.md о структуре стилей» реалистичен, конфликтов с существующимsrc/styles/нет (каталога пока не существует).
- Технических тяжёлых гейтов (typecheck/test/build/golden) не гонял: на этапе
ревью ТЗ кода изменения ещё нет (диф — один файл
docs/specs/266-*.md), гонять их не по чему. Это соответствует масштабу этапа spec, не пропуск.
Находки
M1 — Medium (в скоупе) — заявленный «объективный критерий» (golden) не видит два реальных CSS-состояния, которые ровно и переносятся этим рефакторингом
src/styles.ts содержит 8 блоков @media (prefers-reduced-motion: reduce) и 2
блока @media (forced-colors: active) (строки 594–601 — .iso-* тени
изометрии, 1922–1930 — .plan-snap-line/.hidden-wall-line). §1.3.1 ТЗ
называет npm run golden:verify «главным и объективным критерием» того, что
пиксели не изменились ни на одном слайсе.
Фактически:
demo/golden/harness.mjs:49иdemo/golden/run.mjs:607жёстко фиксируютreducedMotion: 'reduce'на каждом golden-снимке и никогда не эмулируютforced-colors. Значит: (а) состояние «motion не ограничен» (обычный пользователь без предпочтения) вообще никогда не рендерится golden'ом — если слайс случайно утащит правило из-под@media (prefers-reduced-motion: reduce)наружу или, наоборот, спрятает обычное правило внутрь этого блока, картинка под форсированнымreduceне изменится, и golden останется зелёным; (б)forced-colors: activeне проверяется golden'ом вообще, ни в одном из 129 сцен.- Единственный другой заявленный инструмент проверки — сверочный скрипт
§1.3.5/AC6 — сравнивает «нормализованное множество правил (селектор → текст
объявлений)» без упоминания контекста вложенности. Правило, переехавшее из
одного
@media-блока в другой (или потерявшее обёртку@media/@supportsпри копировании), даст тот же «селектор → текст объявлений», то есть инструмент тоже не заметит подмену. - Существующие browser-smoke закрывают это лишь частично:
smoke_plan_snap_overlay.mjsпроверяет именноforced-colorsдля.plan-snap-line/.hidden-wall-line(тот самый блок на 1922 строке),smoke_preloader.mjsпроверяет, чтоhp-boot-pulseне проигрывается приreducedMotion:'reduce'— но не проверяет обратное (что анимация ЕСТЬ без этого предпочтения), и не затрагивает остальные 7prefers-reduced-motion-блоков. Блокforced-colorsдля.iso-*теней (594–601) не покрыт вообще ничем — ни golden, ни smoke, ни unit (test/isometric-contract.test.mjs:25проверяет только наличие класса в разметке, не содержимое CSS-блока). - План тестов ТЗ (§7) не называет ни один browser-smoke вообще, хотя AGENTS.md («по необходимости... браузерные смоки») и PROCESS.md §8 требуют явно перечислить относящиеся к диффу смоки и решение по каждому.
Сценарий отказа: на слайсе plan (4-й по плану) автор переносит
.plan-snap-line/.hidden-wall-line в plan.styles.ts, но обёртка
@media (forced-colors: active) { … } теряется или переезжает в другой файл
без содержимого. В Windows High Contrast снап-оверлей становится нечитаемым
(регрессия ровно того класса, что уже случался — #234 про пиксельные
рассинхроны, найденные не тем гейтом, который должен был их найти).
golden:verify зелёный (forced-colors не эмулируется), сверочный инструмент
зелёный (текст правила не изменился, только его обёртка), и только если
кто-то вручную вспомнит прогнать smoke_plan_snap_overlay.mjs, дефект
всплывёт — а по плану §7 прогонять его не обязаны.
Почему в скоупе, не блокер целиком: сама конструкция рефакторинга не порочна, и исправление дёшево и целиком укладывается в уже запланированный инструментарий:
- Расширить ключ нормализации сверочного инструмента (§1.3.5/AC6) до полного
пути вложенности: селектор + весь стек охватывающих
@media/@supports/@keyframes-условий, а не только «селектор → объявления». - Явно назвать в §7 два существующих смока
(
demo/smoke_plan_snap_overlay.mjs,demo/smoke_preloader.mjs) как обязательные к прогону на слайсахplan/base, раз они трогают именно переносимыеforced-colors/prefers-reduced-motionправила. - Добавить недостающую проверку (unit или расширение существующего смока) для
блока
.iso-*forced-colors(594–601) до его переноса, либо явно зафиксировать «проверено чтением» с указанием ревьюеру, что делать при переносе этого конкретного блока.
Без этого исправления AC3 («golden 129/129 — доказательство пиксельной неизменности») фактически доказывает меньше, чем заявлено в §1.3.1, а именно для тех самых мест, где перенос селекторов между файлами наиболее рискован.
L1 — Low — неверная классификация вспомогательного скрипта
§1.3.4 (примечание к AC6) называет scripts/dev/styles-diff.mjs «класс C»
(документация). По таблице PROCESS.md §1 путь scripts/** целиком относится к
классу B («Гейты и инструменты»), не к C. Практического следствия нет —
файл уже привязан к issue #266 при любой классификации, — но формулировка в
самом ТЗ противоречит канону. Правится одним словом при следующей правке ТЗ;
не считаю нужным делать отдельный цикл только за это, фиксирую с записью.
L2 — Low (наблюдение, не требует правки ТЗ) — лимит «≤ 1200 строк» на файл (AC2) может не выполниться для plan.styles.ts
Грубая эвристическая бакетизация правил src/styles.ts по кластерам §1.1
(regex по началу правила, накопление до следующего распознанного селектора)
дала: plan ≈ 1382, devices ≈ 980, dialogs ≈ 865, chrome ≈ 407, base —
заведомо недооценён эвристикой. Это неточный инструмент (не учитывает все
переключения кластеров внутри @media), но сигнал достаточно сильный:
поверхность plan (сцена, стены, снап, декор-слой, компас, измерения,
.room*, .bdframe/.dtframe) — самая большая и самая связная, и её слайс
идёт четвёртым, то есть после того как более мелкие поверхности уже вычищены.
Если по факту plan.styles.ts выйдет за 1200 строк, AC2 не докажется буквально
— это будет находка код-ревью, а не крах архитектуры ТЗ. Оставляю как
наблюдение: автор может заранее заложить одно из двух — либо явно
предусмотреть в ТЗ право одного файла (plan) на больший потолок с
обоснованием, либо принять как есть и решить вопрос на слайсе 4. Не
блокирую ревью этим пунктом.
Что проверено и корректно
- Все семь обязательных продуктовых/технических подразделов §7.1
присутствуют в той или иной форме: сценарий (§0), контракт (§1), скоуп/не-
скоуп (§2), UX/данные/i18n/touch — единой строкой (§3, адекватно для
чистого CSS-рефакторинга с нулевым видимым эффектом), риски (§4),
release-артефакты (§5), AC с доказательством (§6), план тестов (§7), откат
(§8). Формулировка «что человек увидит» — «рендер обязан быть пиксельно
идентичен» — честный ответ для задачи без пользовательского эффекта, не
замаскированная техническая работа под видом продуктовой; прецедент такого
же лаконичного оформления для чистого рефакторинга есть в
docs/specs/034-frontend-decomposition.md(родительский зонтик, этап 5 которого — этот issue). - Числа в ТЗ (3690 строк, 129 golden-сцен, состав потребителей
cardStyles, порядок подключенияeditorSecondaryStyles) сверены с кодом дерева на момент ревью и совпадают буквально — никакой догадки, выданной за факт, не обнаружено. - Инвариант «без дубликатов» (§1.3.2) и «фиксированный порядок склейки» (§1.3.3) — оба однозначно проверяемые юнитом, доказательство названо.
- AC1 (
styles.ts≤ 40 строк, только импорты+экспорт) реалистичен и легко проверяем ревью кода. - Порядок слайсов (chrome → dialogs → devices → plan → base) обоснован в теле issue и ТЗ («от изолированной поверхности к связной») и подтверждён наблюдением: у тулбаров/чекбоксов кластеров существенно меньше пересечений с остальными зонами, чем у сцены плана.
- Риск «скрытая зависимость каскада» (§4.1) назван прямо и с механизмом
устранения (перенос конфликтующего правила в
baseна прежнюю относительную позицию) — это корректно закрывает случай межзонного конфликта specificity внутри одного и того же слайса; отдельно не закрывает M1 (это про типы состояний, не про порядок). - Release-артефакты:
docs/ARCHITECTURE.md— реальный кандидат на правку (дерево модулей уже документируетeditor-secondary.styles.tsпо такому же шаблону), CHANGELOG обоснованно не трогается (User-Visible: no, инвариант «пиксели не меняются» делает это законным, а не обходом правила). - Технический вопрос (порядок промежуточной склейки на слайсах 1–4, до того как все пять файлов существуют) не эскалирован владельцу и решён ревью: относится к «всё, чего пользователь не наблюдает» (PROCESS.md §7.1), автор и ревьюер вправе договориться сами. Договорённость: относительный порядок внутри промежуточного массива на каждом слайсе сохраняет исходную позицию ещё не вынесенных правил, что и так неявно требуется инвариантом 1 и ловится golden на каждом шаге (кроме состояний из M1).
Чего не проверял
- Тяжёлые гейты (
npm test,npm run build,golden:verify,model-invariants, browser-smoke целиком) — не гонял: на этапе ревью ТЗ продуктового кода ещё нет, гонять их не по чему; это не пропуск, а соразмерность этапу (PROCESS.md §8). - Точный построчный состав будущих пяти файлов — не пересчитывал вручную построчно, кроме грубой эвристической оценки для L2; финальное распределение проверяется код-ревью по факту переноса.
- Мутационный гейт (
scripts/mutation-gate.mjs) — существует в проекте и запускается стабильным релизом, не гейтом ревью; ТЗ корректно ссылается на него в §7 как на «якоря вне styles.ts», не требуя отдельного прогона здесь.
Унаследовано из предыдущих раундов
Не применимо — это первый заход (r1), предыдущих раундов нет.
Вердикт: жёлтый · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 1 → в задаче