mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +00:00
99 lines
22 KiB
Markdown
99 lines
22 KiB
Markdown
# 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
|