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

22 KiB
Raw Blame History

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), сценарий и «что человек увидит до/после» корректно формулируют это как продуктовый факт («он не должен заметить ничего»), а не как отсутствие анализа.

Как проверялось

  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).

Материал раунда

  • Ветка: dev, коммит 01fe7350791e — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 0054c6622b79256b64b455ed786b46270fec9208
    git log --all --format='%H %T' | grep 0054c6622b79
    
  • Тело issue: 9d6ae9888877555f98da1dc0314a77f06d543792a5108b5e9494f17debf309ee
  • Вердикт конвейера: yellow · High 0