Files
houseplan-card/docs/reviews/SPEC-REVIEW-592-r1.md
T
2026-09-18 15:55:17 +00:00

99 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC-REVIEW-592-r1 — «Вынести четыре диалога настроек из houseplan-editor-runtime.ts в модули (шаг 0 эпика #591)»
Issue: [#592](https://github.com/Matysh/houseplan-card/issues/592)
Этап: spec (полный трек — задача не несёт метку `small`; критерий §5 «одна поверхность» нарушен явно: перемещаются четыре разных диалога, не один)
Заход: r1 (первый; разделы «Унаследовано из r0» и «Закрытие раунда r0» не нужны — §2.10 применяется со второго захода)
## Вердикт
**Жёлтый.** High: 0. Medium в скоупе: 1 (возвращается автору в этой же задаче). Medium вне скоупа: 0. Low: 1 (снята с записью, см. ниже).
## Скоуп разбора
Полный разбор: тело issue #592 (раздел `## ТЗ` целиком, включая «Аналитика» перед ним), связанный эпик [#591](https://github.com/Matysh/houseplan-card/issues/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), сценарий и «что человек увидит до/после» корректно формулируют это как продуктовый факт («он не должен заметить ничего»), а не как отсутствие анализа.
## Как проверялось
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, разобрана ниже.
2. Границы методов совпадают побайтово с ТЗ: `_renderMarkerDialog` начинается на `:12884` и заканчивается перед следующим методом на `:13610` (727 строк = 13610-12884+1); `_renderSpaceDialog` `:13611`; `_renderSettingsDialog` `:10337`; `_renderRoomDialog` `:13984`-`:14097` (114 строк, файл на этом заканчивается). Все четыре диапазона и объёмы в таблице «Аналитика» подтверждены чтением, не выдуманы.
3. Прецедент `radar-section.ts`/`vacuum-maps-section.ts`: оба экспортируют `export function render*Section(...)`, ни один не имеет `@state()`; импортируются исключительно из `houseplan-editor-runtime.ts` (`grep` не нашёл других импортёров) — форма экспорта и граница состояния, заявленные в «Принято предположительно», совпадают с реально существующим паттерном, а не изобретены.
4. «В реестре 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'` → возврат патча как есть) — заявление «покрывает только направление „карта → редактор“, и на переезд из редактора дальше не рассчитан» точное.
5. AC6 доказательство существует буквально: `test/mutation-gate.test.mjs:25` — `test('every mutant patch anchors exactly once in the current source', …)`.
6. AC5 доказательство не декларативное: `test/bundle-assets.test.mjs` разбирает реальный манифест Rollup-чанков (`buildBundleManifest`, `assertOwnBundleTopology`), а `scripts/bundle-budget.mjs` считает фактический gzip initial View — оба способны обнаружить, если новый модуль случайно попадёт в нележачий (initial) чанк.
7. AC1: все семь названных смоков существуют файлами в `demo/`. Отдельно проверил, что `smoke_device_inbox.mjs` — не случайный выбор: он открывает `_markerDialog` при конвертации записи инбокса в устройство (`c._markerDialog && !c._deviceInbox` на `:88`), то есть реально задевает код, который переезжает.
8. 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.
9. Классы риска §2.6 — все шесть разобраны явно, с обоснованием неприменимости там, где она заявлена (async, данные/права, геометрия — действительно не имеют отношения к чисто синтаксическому переносу разметки).
10. Обязательные разделы §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).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `01fe7350791e` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `0054c6622b79256b64b455ed786b46270fec9208`
```
git log --all --format='%H %T' | grep 0054c6622b79
```
- Тело issue: `9d6ae9888877555f98da1dc0314a77f06d543792a5108b5e9494f17debf309ee`
- Вердикт конвейера: `yellow` · High 0