Files
houseplan-card/docs/reviews/SPEC-REVIEW-74-r1.md
T
2026-08-30 13:16:27 +00:00

17 KiB
Raw Blame History

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