22 KiB
SPEC-REVIEW-592-r1 — «Вынести четыре диалога настроек из houseplan-editor-runtime.ts в модули (шаг 0 эпика #591)»
Issue: #592
Этап: spec (полный трек — задача не несёт метку small; критерий §5 «одна поверхность» нарушен явно: перемещаются четыре разных диалога, не один)
Заход: r1 (первый; разделы «Унаследовано из r0» и «Закрытие раунда r0» не нужны — §2.10 применяется со второго захода)
Вердикт
Жёлтый. High: 0. Medium в скоупе: 1 (возвращается автору в этой же задаче). Medium вне скоупа: 0. Low: 1 (снята с записью, см. ниже).
Скоуп разбора
Полный разбор: тело issue #592 (раздел ## ТЗ целиком, включая «Аналитика» перед ним), связанный эпик #591 — для проверки, что шаг 0 действительно готовит почву для описанного там редизайна, а не для чего-то ещё, docs/SCOPE.md, AGENTS.md, PROCESS.md (жизненный цикл, классы файлов, §7.1, §5 лёгкий трек). Поскольку почти все содержательные утверждения ТЗ — проверяемые факты о текущем состоянии репозитория, а не предположения о будущем поведении, — разбор включает и сам код:
src/houseplan-editor-runtime.ts— фактический размер файла и границы всех четырёх методов (_renderMarkerDialog,_renderSpaceDialog,_renderSettingsDialog,_renderRoomDialog);src/houseplan-card.ts— фактический размер;test/core-file-budget.test.mjs— текущие потолки CAPS и способ измерения (split('\n').length);src/editors/radar-section.ts,src/editors/vacuum-maps-section.ts— заявленный прецедент (функция(host) => TemplateResult, отсутствие своего@state()), и кто их импортирует;scripts/mutation-registry.mjs— число патчей сfile: 'src/houseplan-editor-runtime.ts'и реализацияrelocateEditorPatch(направление переноса анкеров);test/mutation-gate.test.mjs— тест «каждый якорь патча встречается ровно один раз», названный доказательством AC6;test/bundle-assets.test.mjs,scripts/bundle-budget.mjs— реальность измерения ленивого графа для AC5 (тест разбирает фактический вывод Rollup-чанков, а не заявление);demo/smoke_general_settings.mjs,smoke_room_settings.mjs,smoke_device_preview_parity.mjs,smoke_color_picker_consumers.mjs,smoke_static_icon.mjs,smoke_device_inbox.mjs,smoke_room_temperature_thresholds.mjs— существование файлов (AC1);demo/golden/baselines/*.pngиdemo/golden/matrix.mjs,demo/golden/harness.mjs— существование всех 12 эталонов, названных в AC2, и то, какой код каждый из них реально рендерит.
Продуктовых вопросов владельцу в тексте нет: задача не имеет видимых изменений (К1), сценарий и «что человек увидит до/после» корректно формулируют это как продуктовый факт («он не должен заметить ничего»), а не как отсутствие анализа.
Как проверялось
- Размеры ядер.
wc -lдаёт 14096/13728, но реальная мера ratchet —split('\n').length(файл оканчивается переводом строки, что даёт на 1 больше): фактически 14097 и 13729 против потолков 14100/13732, то есть запас 3, а не 4, как написано в шапке «Аналитика». Прогнал самtest/core-file-budget.test.mjs— 7/7 зелёных, ratchet сейчас не нарушен ни в одну сторону. Расхождение 4→3 не влияет ни на один AC (ни один AC не ссылается на число «4»), это находка Low, разобрана ниже. - Границы методов совпадают побайтово с ТЗ:
_renderMarkerDialogначинается на:12884и заканчивается перед следующим методом на:13610(727 строк = 13610-12884+1);_renderSpaceDialog:13611;_renderSettingsDialog:10337;_renderRoomDialog:13984-:14097(114 строк, файл на этом заканчивается). Все четыре диапазона и объёмы в таблице «Аналитика» подтверждены чтением, не выдуманы. - Прецедент
radar-section.ts/vacuum-maps-section.ts: оба экспортируютexport function render*Section(...), ни один не имеет@state(); импортируются исключительно изhouseplan-editor-runtime.ts(grepне нашёл других импортёров) — форма экспорта и граница состояния, заявленные в «Принято предположительно», совпадают с реально существующим паттерном, а не изобретены. - «В реестре 33 патча» (К5) —
grep -c "file: 'src/houseplan-editor-runtime.ts'"вscripts/mutation-registry.mjsдаёт ровно 33.relocateEditorPatch(:20-…) действительно переносит якоря только в направленииsrc/houseplan-card.ts → src/houseplan-editor-runtime.ts(условие на входе:patch.file !== 'src/houseplan-card.ts'→ возврат патча как есть) — заявление «покрывает только направление „карта → редактор“, и на переезд из редактора дальше не рассчитан» точное. - AC6 доказательство существует буквально:
test/mutation-gate.test.mjs:25—test('every mutant patch anchors exactly once in the current source', …). - AC5 доказательство не декларативное:
test/bundle-assets.test.mjsразбирает реальный манифест Rollup-чанков (buildBundleManifest,assertOwnBundleTopology), аscripts/bundle-budget.mjsсчитает фактический gzip initial View — оба способны обнаружить, если новый модуль случайно попадёт в нележачий (initial) чанк. - AC1: все семь названных смоков существуют файлами в
demo/. Отдельно проверил, чтоsmoke_device_inbox.mjs— не случайный выбор: он открывает_markerDialogпри конвертации записи инбокса в устройство (c._markerDialog && !c._deviceInboxна:88), то есть реально задевает код, который переезжает. - AC2 — построчная сверка списка из 12 эталонов с
demo/golden/matrix.mjs/harness.mjs: каждый существует и рендерит именно один из четырёх переезжающих диалогов (device,room-temperature,general-color,general-help,space-room-color,toggle-entityчерезdeviceс другим маркером). Отдельно перебрал все значенияdialog:вmatrix.mjs(14 уникальных), чтобы исключить пропуск:decor-color,backup-*,optimize-*,device-inbox,supportрендерятся другими методами (_renderDeviceInboxна:12329, вне всех четырёх диапазонов) и корректно не входят в список. Но нашёл один, который входит в диапазон и не назван — см. находку М1. - Классы риска §2.6 — все шесть разобраны явно, с обоснованием неприменимости там, где она заявлена (async, данные/права, геометрия — действительно не имеют отношения к чисто синтаксическому переносу разметки).
- Обязательные разделы §7.1 — присутствуют все, включая оба продуктовых (сценарий, «что человек увидит до/после»), и раздел «Принято предположительно» корректно ограничен техническими решениями (раскладка файлов, форма экспорта, величина опускания потолка), которые ревьюер вправе оспорить и не оспаривает — они совпадают с уже принятым в проекте прецедентом.
Обязательные разделы §7.1 — комплектность
Присутствуют все: сценарий · что человек увидит до/после · проблема · скоуп/не-скоуп · контракт поведения (К1–К5) · UX (явное «нет») · модель данных и миграция (явное «нет») · i18n (явное «нет») · AC1–AC7 с указанием доказательства и «чем краснеет» · план автотестов · риски · откат · release-артефакты. Раздел «Принято предположительно» — на месте. Touch-контракт отдельной строкой не выделен, но покрыт по существу в разборе класса риска host/input («обработчики переносятся вместе с разметкой, порядок навешивания сохраняется») — для задачи без единого визуального или интерактивного изменения этого достаточно, отдельной находки не завожу.
Находки
Medium (в скоупе задачи — возвращается автору без отдельного issue)
М1. AC2 не называет один из golden-эталонов, который рендерит код именно внутри переносимого блока _renderMarkerDialog, и тем самым не покрывает его пиксельной проверкой.
Внутри диапазона _renderMarkerDialog (:12884-:13610) есть ветка d.display === 'icon_ripple' (:13517-:13529), рисующая hp-color-opacity для цвета ripple-эффекта (marker.activity_color). Единственный golden-сценарий, который вообще включает эту ветку — device-ripple-color-popover-mobile-ru (demo/golden/matrix.mjs:1084; харнесс demo/golden/harness.mjs:1993-2011 явно ставит card._markerDialog = { ...card._markerDialog, display: 'icon_ripple' } и кликает по триггеру hp-color-opacity, помеченному marker.activity_color). Ни один из трёх названных в AC2 device-dialog-* эталонов (device-dialog-desktop-en, -de, device-dialog-mobile-ru) не переключает display на icon_ripple — все трое используют устройство golden-light-two без такого оверрайда, то есть рендерят маркер в другом режиме отображения, и колонка .colorrow.ripple-colorrow в них не появляется вовсе.
device-ripple-color-popover-mobile-ru существует как эталон (demo/golden/baselines/device-ripple-color-popover-mobile-ru.png), но не входит в список AC2.
Чем это красное на практике. Реализация может честно свести все 12 названных в AC2 эталонов к нулевому расхождению — то есть формально выполнить AC2 «на зелёный» — и при этом сломать именно ripple-цветовую строку при переносе (например, порядок навешивания @click на hp-color-opacity внутри .colorrow.ripple-colorrow, или привязку this.host._markerDialog в замыкании при копировании метода в новый модуль). К1 («ни один id/class/data-* не меняется») в этой части останется недоказанным, а не доказанным нулевым расхождением, как заявляет ТЗ.
Почему Medium, а не High. Не блокирует старт работы и не требует пересмотра контракта — правка ограничивается добавлением одной строки device-ripple-color-popover-mobile-ru в список эталонов AC2 (эталон уже существует, golden:verify уже умеет его прогонять).
Что нужно на правку: добавить device-ripple-color-popover-mobile-ru в список эталонов AC2.
Low
L1. Шапка «Аналитика» называет запас обоих ядер «4», фактический запас (по мере, которую использует сам ratchet, split('\n').length) — 3 для обоих файлов. wc -l (которым, по всей видимости, считал автор) даёт на единицу меньше из-за завершающего перевода строки в конце файла. Расхождение не влияет ни на один AC — ни один AC не ссылается на число 4 — и не меняет вывод «оба ядра стоят в нескольких строках от потолка». Снимаю без правки, фиксирую записью здесь.
Что проверено и корректно
- AC1–AC7 однозначны, у каждого назван способ доказательства и «чем краснеет» отдельной колонкой, включая защитный AC3 (сам ratchet-тест краснеет и на росте, и на незафиксированном падении) и AC6 (существующий тест на уникальность якоря).
- К1–К5 — каждое утверждение о текущем состоянии кода (границы методов, число мутационных патчей, направление
relocateEditorPatch, прецедентradar-section.ts/vacuum-maps-section.ts) подтверждено чтением, а не выдумано; расхождение с фактическим состоянием найдено только в шапке «Аналитика» (L1, не в самом ТЗ). - Скоуп/не-скоуп точно называет пять затрагиваемых путей и явно исключает
houseplan-card.ts, i18n, сами контролы и содержание редизайна — совпадает с реальной картиной: код четырёх диалогов целиком лежит в одном файле, потребители контролов (hp-color-opacity,hp-help) не меняются. - Риск №1 (молчаливая правка под видом переноса) и риск №2 (якоря мутантов) — реалистичны и корректно адресованы AC2/AC6; риск №3 (соблазн раздробить 727-строчный диалог устройства на подсекции) — разумное явное ограничение скоупа этим шагом.
- Раздел «Принято предположительно» ограничен техническими решениями (раскладка файлов, форма экспорта функции, величина опускания потолка) и не подменяет ни одного продуктового решения; величина потолка прямо привязана к фактическому выносу, а не к произвольному запасу «на будущее» — соответствует духу ratchet-теста (
test/core-file-budget.test.mjs, комментарий «потолок, который вычисляется от текущего размера, потолком не является», здесь не нарушается, поскольку опускается он, а не GPT-число). - Trailers/release-артефакты:
User-Visible: noкорректно, changelog не требуется — задача не меняет ничего наблюдаемого пользователем.
Чего не проверял
- Не проходил построчно по всем ≈1300 строкам четырёх методов в поиске других скрытых веток по образцу ripple (
display === '…'), кроме уже найденной; не исключаю, что есть другие аналогично «спрятанные» состояния без golden-покрытия за пределами уже указанной в М1. Ограничился одним конкретным найденным гэпом и общей сверкой всех 14 уникальных значенийdialog:вmatrix.mjsпротив четырёх диапазонов — этого достаточно, чтобы находка была не гипотетической, но не является исчерпывающим построчным аудитом каждой условной ветки. - Не сверял состав
smoke_general_settings.mjs/smoke_room_settings.mjs/smoke_color_picker_consumers.mjsпострочно с содержимым_renderSettingsDialog/_renderRoomDialogна предмет аналогичных пропущенных состояний внутри самих смоков (только для AC1, а не для AC2) — при отсутствии диффаsmoke-select.mjsнеприменим, а ручная построчная сверка семи смоков против трёх методов на этапе ТЗ избыточна: она относится к моменту, когда появится реальный перенос кода, и будет проверяться на код-ревью тем же способом, каким я нашёл М1. - Не запускал
npx tsc --noEmit,npm test,npm run build,golden:verify,bundle:budget— на этапе ревью ТЗ код ещё не менялся, эти гейты не относятся к предмету этого раунда (продуктового кода нет,test/core-file-budget.test.mjsпрогнал только чтобы проверить фактическое число запаса ядер для L1, это не гейт задачи). - Не читал полностью эпик #591 (спецификацию редизайна из вложенного архива) — использовал только тело issue #591, достаточное чтобы подтвердить, что шаг 0 — реальная подготовка к реальному следующему шагу, а не работа в никуда.
Материал раунда
- Issue #592, тело на момент разбора (raw, полученное
gh issue view 592 --json body); SHA-256 нормализованного тела вписывается конвейером публикации в блок якорей, здесь не дублируется. - Заход r1, циклов ревью ТЗ израсходовано 0 из 4 (полный трек, лимит циклов — 4 по §4; текущий жёлтый вердикт израсходует цикл 1/4 после публикации).
- Код сверялся на рабочей копии репозитория в состоянии
HEADна момент разбора (задача ещё не начата, продуктовый код не менялся под неё — все ссылки на строки относятся к текущему состояниюsrc/houseplan-editor-runtime.ts, а не к материалу будущего code-review).
Материал раунда
- Ветка:
dev, коммит01fe7350791e— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
0054c6622b79256b64b455ed786b46270fec9208git log --all --format='%H %T' | grep 0054c6622b79 - Тело issue:
9d6ae9888877555f98da1dc0314a77f06d543792a5108b5e9494f17debf309ee - Вердикт конвейера:
yellow· High 0