# SPEC-REVIEW-74-r1 - **Issue:** https://github.com/Matysh/houseplan-card/issues/74 - **Этап:** ревью ТЗ (PROCESS.md §2.4) - **Материал:** `docs/specs/074-device-position-undo.md` на SHA `a3c305e6052c001d3ea39a190ed6b528f56e2df5` (ветка `issue/74-device-position-undo`, комментарий владельца от 2026-08-30 фиксирует этот SHA как перебазированную и расширенную редакцию ТЗ) - **Заход:** r1 (первое ревью документа; прежних вердиктов по этому ТЗ в issue нет — комментарии от 2026-08-14/08-15/08-30 это аналитика и акт написания ТЗ, не ревью) - **Вердикт:** зелёный - **High:** 0 · **Medium:** 0 · **Low:** 2 (обе сняты ревьюером с записью ниже) ## Скоуп разбора Первый заход — полный разбор: сам документ ТЗ, тело issue #74 и все три комментария (аналитика 2026-08-14, публикация ТЗ 2026-08-15, актуализация на `dev` 2026-08-30), `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` §2.4/§7.1, `docs/UX-MODES.md`, `docs/TOUCH-SUPPORT.md`, и заявленное в ТЗ текущее поведение `dev` (раздел 2 «Актуальность на 2026-08-30») сверено построчно с кодом. ## Как проверялось Ревью ТЗ не запускает гейты (это не код-ревью), но обязано поймать догадку, выданную за факт. Документ №074 необычно детален и содержит развёрнутый раздел «Актуальность» с явными утверждениями о текущем поведении `dev` — каждое из них можно фальсифицировать чтением кода, поэтому все они прочитаны и сверены: 1. `CommandStack` существует и используется только для geometry/decor, отдельной истории позиций устройств нет — подтверждено: `src/command-stack.ts:15-55`, `src/houseplan-card.ts:1704` (`_geometryHistory = new CommandStack<...>(50)`), отдельной по-девайсной истории в коде нет. 2. Device toolbar не содержит Undo/Redo — подтверждено: `_renderDevicesBar()` (`src/houseplan-editor-runtime.ts:11743-11768`) не рендерит такие кнопки, в отличие от `_renderMarkupBar()` (`:11722-11729`). 3. Keyboard handler выходит до истории в `devices` — подтверждено: `src/houseplan-card.ts:2816` `if (!this._markup) return;` до диспетчеризации undo/redo (`:2824-2856`), а `_markup` — это `mode === 'plan'` (`:1699-1701`); `devices` не попадает ни в decor-ветку, ни в `_markup`. 4. `_pointerMove()` вызывает `_savePos()` и планирует persist на каждом кадре — подтверждено: `src/houseplan-card.ts:6381-6405` вызывает `_savePos` безусловно; `_savePos` (`:5001-5021`) кладёт id в `_dirtyPos` и вызывает `_persistLayout()` (debounce, `:4769`) при каждом вызове. 5. `pointercancel` направлен в тот же `_pointerUp`, что и обычный `pointerup` — подтверждено: `src/houseplan-card.ts:11806-11807`, оба события биндятся на `this._pointerUp(e, d)`; отдельного abort-пути для устройств сегодня нет. 6. Backend уже предоставляет точечные `houseplan/layout/update` и `houseplan/layout/delete` без изменения схемы — подтверждено: `custom_components/houseplan/websocket_api.py:147-148,622,1077`, уже вызываются из фронтенда (`houseplan-card.ts:4782`, `houseplan-editor-runtime.ts:8308,8369`). 7. `_sentPos` (pending-authority карта, §9) существует — подтверждено: `src/houseplan-card.ts:4767,4739,4780,4785`. 8. Механизм различения own echo / внешнего изменения по revision/content, на переиспользование которого рассчитывает §10, существует и именно content-based, не timeout — подтверждено: `_adoptStructuralResponses` (`houseplan-card.ts:4033-4053`) сравнивает `contentFingerprint(...)` и чистит `_geometryHistory` только при реальном расхождении, с явным комментарием в коде «A reconnect echo with identical content deliberately does not [clear]». 9. Кросс-пространственная история (§9 «команда другого пространства сначала переключает карточку на `spaceId`») — не новый паттерн: `_applyGeometryState` (`houseplan-editor-runtime.ts:2344-2348`) уже переключает `host._space` перед применением geometry-команды, а `_commitSpace`/`_slideTo` (`houseplan-card.ts:1443-1463`) никогда не чистят `_geometryHistory` — существующий стек уже глобальный, не по-пространственный. ТЗ описывает продолжение существующего контракта, а не изобретает новый. 10. `rl_*` (room-label) действительно имеют отдельный от устройств путь сохранения — подтверждено (`houseplan-card.ts:11913,11941,12123`, `houseplan-editor-runtime.ts:10706-10770`), корректно исключены из §7/§10. 11. Guards перед `pointerdown`, на которые ссылается §8.1 — подтверждено: `houseplan-card.ts:6356` (mode guard), `:6373` (`ha_disabled` guard) до фиксации `_drag` (`:6374-6376`). 12. `docs/TOUCH-SUPPORT.md` и `docs/UX-MODES.md` сверены на противоречия с §1 и §9/§12: противоречий не найдено, «не сохранять случайную позицию при системной отмене жеста» — прямое следствие уже существующего требования `TOUCH-SUPPORT.md:78-79` («not... saving unintended geometry merely because a pinch, pointer cancellation or second touch was misread as a click»). 13. Формат интерполяции `history.device_move` с именем устройства технически совместим с существующим паттерном `{name}` в `history.undo_named` / `redo_named` (`src/i18n/en.json:105-110`), хотя это первый ключ, где сам `{name}` — динамически подставленное (не статическое) значение; отмечаю как новый, но не проблемный шаг того же механизма. 14. Числа языков/ключей: en/ru/de/fr параллельны (по 37 `history.*` ключей в каждом), подтверждает выполнимость плана и18n на 4 языка из §16. Отдельно проверено требование §7.1 «сценарий + что человек увидит» (раздел 1 документа: персона — администратор HA, поверхность — Device editor, desktop-first; до/после — одной фразой без терминов реализации, за вычетом одного слова «layout», которое уже вошло в пользовательскую терминологию проекта через `docs/USER-GUIDE.ru.md`) и соответствие Core user job (J6 «Keep the plan true as the home evolves», `docs/SCOPE.md:48`) — подтверждено как владельцем в аналитике (2026-08-14), так и по существу. ## Находки ### Low-1 — неверные пути трёх скриптов в §14 «Регрессия и gates» `docs/specs/074-device-position-undo.md:285-287` называет `python scripts/check_docs.py`, `node scripts/no_new_any.mjs`, `node scripts/smoke_select.mjs`. Фактические имена в репозитории: `scripts/check-docs.mjs` (Node, не Python), `scripts/no-new-any.mjs`, `scripts/smoke-select.mjs` (дефис, не подчёркивание) — проверено `ls scripts/`. Как написано, ни одна из трёх команд не выполнится. **Воспроизведение:** `ls scripts/check_docs.py` → `No such file or directory`; реальный файл — `scripts/check-docs.mjs`. Аналогично для двух других. **Решение ревьюера:** снимаю как Low без возврата в цикл — не влияет на выполнимость самого AC-контракта (доказательства AC1-AC14 не зависят от этих трёх строк), тривиально исправляется при выходе в реализацию простой заменой имён на актуальные. Автору стоит поправить эти три строки перед `S5-ready`, но отдельного цикла ревью это не требует. ### Low-2 — нет явного блока «принято предположительно, поменять свободно» и явной строки про bundle-бюджет PROCESS.md §7.1 требует технические решения автора, не наблюдаемые пользователем, фиксировать явным блоком в конце ТЗ. Документ принимает несколько таких решений по ходу текста (имя нового модуля `src/device-position-history.ts` помечено «рекомендуемо», отдельный `CommandStack(50)` вместо переиспользования geometry-стека, форма интерфейса `DevicePlacement`), но не сводит их в отдельный итоговый блок. Также §12 называет производительность (O(1), снижение нагрузки на `pointermove`), но не отдельной строкой — влияние на `bundle:budget` (256000 B gzip, `AGENTS.md:334`) от нового модуля + 2 кнопок + 2 ключей i18n×4 языка. **Решение ревьюера:** снимаю как Low. Технические решения по существу разумны, обоснованы рядом (риски §17 прямо привязаны к каждому), и реальный прирост бюджета от pure-модуля на пару функций и двух иконок пренебрежимо мал по сравнению с порогом 256000 B — нет оснований ожидать, что бюджет пострадает. Формальное отсутствие сводного блока не создаёт продуктовой неоднозначности и не блокирует DoR по существу. Других находок нет. High и Medium не обнаружены — ни в скоупе, ни вне его. ## Что проверено и признано корректным - Все технические утверждения §2 «Актуальность на 2026-08-30» о поведении текущего `dev` — фактически точны (детали выше, пп. 1-11). Раздел не содержит выданной за факт догадки: каждое утверждение фальсифицируемо и выдержало проверку. - Продуктовая рамка (§1): персона, поверхность, момент и видимый результат названы, соответствуют J6 из `docs/SCOPE.md` и позиции «редакторы admin-only, desktop-first» — противоречий с `UX-MODES.md` / `TOUCH-SUPPORT.md` не найдено. - Не-скоуп (§4) и строгая граница истории (§11) описывают ровно то, что оставлено вне задачи, без незаявленных побочных эффектов на geometry/decor историю: reuse-механизмы (own-echo/content fingerprint, кросс-space переключение) — это переиспользование уже существующего в коде контракта, а не новое допущение, которое стоило бы адресовать владельцу. - AC1-AC14 (§13) — каждый снабжён способом доказательства (`unit` / `browser smoke` / `golden` / `source guard`), формулировки однозначны, ни один не описывает выдуманное поведение. - Откат (§17, последний абзац) и миграция (везде явно «не требуется», формат layout не меняется) — присутствуют и не голословны, поскольку backend API уже существует без изменений (см. п. 6 выше). - Открытых продуктовых вопросов владельцу нет; предыдущий комментарий владельца (2026-08-14) подтверждает это независимо («Вопросы: нет»), и разбор ТЗ не нашёл скрытого продуктового вопроса, который следовало бы поднять вместо этого. - i18n: новые ключи (`history.device_move`, `history.device_stale`) не коллизируют с существующими (проверено grep по всем четырём `src/i18n/*.json`), план покрывает en/ru/de/fr, что соответствует фактическому состоянию проекта (по 37 `history.*` ключей на язык уже сегодня). ## Чего не проверял - Не запускал `npm run typecheck`/`npm test`/`npm run build` — на этапе ревью ТЗ кода ещё нет (ветка на SHA `a3c305e6` не содержит изменений в `src/**` или `test/**` для этой задачи, только сам файл спецификации), гейты реализации нечего гонять. - Не проверял golden-инфраструктуру построчно за пределами подтверждения, что существующий baseline `device-editor` toolbar-сценария (`geometry-devices-editor-*.png`) уже существует как объект для будущего diff — сам скриншот не перегенерировал. Это не требуется на этапе ТЗ. - Не оценивал производительность драга количественно (нет кода для профилирования); раздел §12 принят по правдоподобию инженерного рассуждения (O(1) snapshot/apply, устранение persist на каждый `pointermove` объективно снижает, а не повышает нагрузку), не по измерению. - Не сверял с точностью до символа все 14 AC на предмет полноты матрицы edge-cases сверх выборочной проверки логической согласованности §8-§11 — это стандартный предмет код-ревью (`тест умеет падать`), а не ревью ТЗ. ## Итог Документ ТЗ #074 методологически образцовый для полного трека: раздел «Актуальность» не содержит ни одной догадки, выданной за факт — все 11+ проверенных технических утверждений о `dev` подтвердились построчным чтением кода; продуктовая рамка и Core user job названы и совпадают с `docs/SCOPE.md`; AC полны, пронумерованы и каждый снабжён способом доказательства; риски и откат заполнены содержательно, а не для галочки. Единственные две находки — Low, обе не блокируют DoR и сняты этим ревью с запиской для автора поправить имена скриптов перед стартом реализации. **Вердикт: зелёный.** Переход в «Готово к разработке» (`S5-ready`).