mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 21:01:21 +00:00
265 lines
25 KiB
Markdown
265 lines
25 KiB
Markdown
# SPEC-REVIEW-403-r1
|
||
|
||
- Issue: https://github.com/Matysh/houseplan-card/issues/403
|
||
- Артефакт ТЗ: `docs/specs/403-area-relocation-safety.md` (полный трек, класс A;
|
||
без метки `small`/`trivial`)
|
||
- Материал: SHA `1f9d9014` (ветка `issue/403-area-relocation-safety`,
|
||
коммит «docs: specify area relocation safety (#403)»)
|
||
- Заход: r1 · лимит циклов ревью ТЗ для полного трека — 4 (§4), израсходовано
|
||
до этого раунда — 0 (пять предыдущих запусков конвейера падали до первого
|
||
обращения к модели — `is_error: true`, `modelUsage: {}` — вердикта не было
|
||
ни разу, бюджет не тратили, см. комментарии issue)
|
||
- Ревьюер: Claude (роль «ревьюер ТЗ»), независимая сессия, без устных
|
||
пояснений автора
|
||
- Первый раунд — разбор полный, раздела «дельта» нет (§2.10 применяется
|
||
начиная со второго захода)
|
||
|
||
## Скоуп ревью
|
||
|
||
Bug P1, класс A, полный трек (issue не помечен `small`): две находки
|
||
свежего кода #126 на одной поверхности (`src/houseplan-card.ts`,
|
||
`_syncAreaRelocations` и обработка отказов):
|
||
|
||
- **C2 (High из аудита)** — отказ `houseplan/config/set` во время переезда
|
||
area оставляет `layout` уже удалённым (удаление успело пройти раньше) и
|
||
не восстанавливает позицию и не помечает устройство как требующее
|
||
внимания — ручная расстановка маркера теряется молча, самовоспроизводяще
|
||
(снапшот откатывается на старую area → следующий authoritative-проход
|
||
снова решает `relocate`).
|
||
- **M1 (Medium из аудита)** — переезд area **любого** устройства чистит
|
||
**весь** стек Undo позиций (`_devicePositionHistory.clear()`), включая
|
||
записи устройств, которых переезд не касался, и без уведомления (в
|
||
отличие от соседнего класса очистки истории — `history.device_stale`).
|
||
|
||
Задача ложится на J6 `docs/SCOPE.md` («Keep the plan true as the home
|
||
evolves» — оптимистичная блокировка, ручная расстановка маркеров) и на
|
||
стоящее правило SCOPE.md «никогда не удалять данные пользователя по
|
||
догадке» — обе находки именно про это: ручная позиция маркера — данные,
|
||
введённые пользователем руками.
|
||
|
||
## Как проверялось
|
||
|
||
1. Прочитаны целиком `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` (§1, §2.4,
|
||
§2.5, §4, §5, §6, §7.1, §7.2, §8), тело issue #403 и все восемь
|
||
комментариев (включая последовательность из шести неудачных запусков
|
||
конвейера — учтено при подсчёте бюджета циклов, ни один из них вердикта
|
||
не дал).
|
||
2. Прочитан весь текст ТЗ `docs/specs/403-area-relocation-safety.md`.
|
||
3. Каждое фактическое утверждение ТЗ о коде сверено построчно с деревом на
|
||
`1f9d9014`:
|
||
- `_maybeRebuildDevices`/`_syncAreaRelocations`
|
||
(`src/houseplan-card.ts:5016-5254`) прочитаны целиком;
|
||
- подтверждено: ветка отказа **удаления** layout восстанавливает позицию
|
||
(`applyDevicePlacement(before)`, фактическая строка `:5184`, ТЗ называет
|
||
`:5183` — расхождение в одну строку, не искажает факт);
|
||
- подтверждено: ветка отказа **записи конфига** (`catch` на `:5223-5248`,
|
||
ТЗ называет тот же диапазон точно) восстанавливает только
|
||
`marker_area_snapshot`/`new_device_ids` в памяти (если фингерпринт не
|
||
разошёлся) и **не восстанавливает** удалённую позицию — заявление ТЗ
|
||
подтверждено буквально, строка в строку;
|
||
- подтверждён механизм самовоспроизведения: `resolveDeviceAreaRelocations`
|
||
(`src/device-area-relocation.ts:181-188`) решает `relocate = true`
|
||
ровно когда `previous.area !== area`; после откатa снапшота на старую
|
||
area это условие снова истинно на следующем authoritative-проходе —
|
||
цикл, описанный в ТЗ, воспроизводится логикой резолвера, а не является
|
||
догадкой;
|
||
- подтверждена находка M1: `_devicePositionHistory.clear()`
|
||
(`src/houseplan-card.ts:5062-5065` — ТЗ называет `:5056-5058`,
|
||
расхождение в 6 строк, см. находку L1) стоит под условием «есть хоть
|
||
одно переезжающее устройство», без фильтра по `deviceId`;
|
||
- `CommandStack` (`src/command-stack.ts`) не имеет метода выборочного
|
||
удаления — подтверждено, что контракт AC5/AC6 («снимается история
|
||
переехавших, а не весь стек») требует нового метода, но это техническая
|
||
деталь реализации, а не пробел ТЗ (стек типизирован по
|
||
`NamedCommand<DevicePositionState>`, `deviceId` уже есть в каждой
|
||
записи — технически осуществимо без изменения формата данных);
|
||
- `toast.pos_save_failed`/`toast.cfg_save_failed`/`toast.conflict`/
|
||
`history.device_stale` — все четыре ключа существуют в `src/i18n/ru.json`
|
||
(и en/de/fr) — заявления ТЗ о «существующей метке» и «существующем
|
||
уведомлении» подтверждены, не придуманы;
|
||
- `registryFollowingBinding`/формат `binding` (`device-area-relocation.ts:95-107`)
|
||
— подтверждён формат `${bindingKind}:${bindingRef}`, ровно то, что автор
|
||
сам называет причиной трёх неудачных попыток воспроизведения C2 до
|
||
финального успешного прогона (комментарии аналитики) — воспроизведение
|
||
не голословно, ошибка автора зафиксирована и исправлена явно.
|
||
4. Проверено соответствие терминологии `docs/USER-GUIDE.ru.md`: раздел
|
||
«Устройства» (`:757-771`) документирует именно тот успешный путь, который
|
||
AC2 требует сохранить («старая позиция удаляется… появляется красная
|
||
отметка внимания»), и раздел «История редактора» (`:260`, `:808-809`)
|
||
подтверждает существующий контракт Undo (50 команд, best effort на touch) —
|
||
спецификация не вводит новых терминов и не противоречит гайду.
|
||
5. Проверено `docs/TOUCH-SUPPORT.md` и DoR-чек-лист §2.5 на обязательные
|
||
пункты «влияние на touch» и «влияние на производительность» — см. находку
|
||
H1.
|
||
6. Проверены обязательные разделы §7.1 — присутствуют все (сценарий · что
|
||
человек увидит до/после · проблема и контракт по каждому пункту · скоуп/
|
||
не-скоуп · UX · модель данных и миграция · i18n · AC1–AC7 с доказательством ·
|
||
план автотестов · риски · откат · release-артефакты).
|
||
7. Проверено существование инструментов, на которые ссылается план тестов:
|
||
`scripts/mutation-gate.mjs` есть, ни одного мутанта `area-relocation-*` в
|
||
нём пока нет (согласуется с ТЗ — это новые мутанты); `demo/smoke_area_relocation.mjs`
|
||
существует и уже умеет мокать отказ `houseplan/config/set`
|
||
(`rejectKettleRelocation`, строки 19/262/279) — план AC1/AC4 технически
|
||
реализуем на существующей инфраструктуре смоков, не является фантазией.
|
||
8. Гейты кода (`tsc`, `test`, `build`) не гонялись: на этапе ТЗ продуктового
|
||
диффа нет (класс C — только `docs/specs/**`), гонять их не над чем.
|
||
|
||
## Находки
|
||
|
||
### H1 (High, блокирует, в скоупе). ТЗ не называет два обязательных пункта DoR — влияние на touch/kiosk и на производительность
|
||
|
||
**Где**: весь файл `docs/specs/403-area-relocation-safety.md` — ни разу не
|
||
упоминает touch, kiosk, `TOUCH-SUPPORT.md`, производительность, перф или
|
||
бюджет (`grep -i "touch|kiosk|перф|производительн|performance"` по файлу —
|
||
ноль совпадений).
|
||
|
||
**Почему это находка, а не формальность**. PROCESS.md §2.5 перечисляет
|
||
обязательные пункты «Готово к разработке» и требует по каждому явную
|
||
запись, а не молчание: «влияние на производительность и бюджеты названо
|
||
(**или явно «нет»**)» и «влияние на touch по `docs/TOUCH-SUPPORT.md`
|
||
(**View и киоск — блокирующие**)». Оба пункта — из списка, помеченного
|
||
«Все пункты обязательны», и: «Если хоть один пункт не выполнен — статус не
|
||
«Готово к разработке», как бы ни хотелось начать». Ни один из двух пунктов
|
||
в ТЗ не назван — ни утвердительно, ни отрицательно.
|
||
|
||
Это не абстрактная бумажная претензия: у задачи есть настоящая View/kiosk
|
||
грань. AC2 фиксирует видимый на любом клиенте (включая киоск-планшет — J1
|
||
`docs/SCOPE.md`, «Show the whole home … device states») эффект успешного
|
||
переезда — маркер оказывается в новой комнате с отметкой внимания; это
|
||
именно то поведение, что описано в `docs/USER-GUIDE.ru.md:761-767`.
|
||
Симметрично, дефект C2 в необработанном виде **тоже виден на киоске** —
|
||
маркер молча возвращается в центр комнаты. То есть эта ветка кода реально
|
||
затрагивает View/kiosk-наблюдаемое поведение, а не только редакторский
|
||
слой, и именно поэтому DoR требует явного заявления, а не тишины.
|
||
AC5/AC6 (сужение очистки Undo) относятся к редактору устройств, для
|
||
которого touch уже документирован как best effort (`USER-GUIDE.ru.md:260`,
|
||
`:808-809`) — но это тоже должно быть **названо**, а не молчаливо
|
||
унаследовано: без явной строки нельзя отличить «автор сверился с
|
||
TOUCH-SUPPORT.md и решил, что контракт не меняется» от «автор не думал про
|
||
touch вовсе» (тот же аргумент, которым в SPEC-REVIEW-402-r1 было обосновано
|
||
идентичное H1-заключение для #402 — прецедент этого же ревьюера на этом же
|
||
проекте).
|
||
|
||
Фактическая оценка по существу (для экономии цикла): последствий, скорее
|
||
всего, нет ни для touch, ни для перфа. Обе правки — (1) порядок операций в
|
||
одном async-методе `_syncAreaRelocations` плюс восстановление/повторная
|
||
попытка записи при отказе, (2) точечный фильтр по `deviceId` в
|
||
уже существующей структуре истории на 50 записей. Ни жесты, ни рендер, ни
|
||
DOM, ни сетевые вызовы сверх уже выполняемых не меняются. Но это вывод
|
||
ревьюера, а не факт, зафиксированный автором в ТЗ, — фиксировать обязан
|
||
автор.
|
||
|
||
**Требуемая правка** (несколько строк текста, не кода): добавить в ТЗ,
|
||
например —
|
||
- `Touch: не затронут — правка меняет порядок серверной записи
|
||
(_syncAreaRelocations) и фильтр очистки Undo-стека по deviceId, не
|
||
касается drag/tap-жестов, рендера или DOM; наследует существующий
|
||
контракт Undo/Redo (best effort на touch, USER-GUIDE §10). View/kiosk:
|
||
наблюдаемый эффект (AC1/AC2) — позиционный, не входной, контракта View на
|
||
touch не меняет.`
|
||
- `Производительность: нет — правка не добавляет новых циклов, подписок или
|
||
сетевых вызовов сверх уже выполняемых `_syncAreaRelocations`/`_writeConfig`;
|
||
фильтр истории работает на существующем массиве максимум 50 записей.`
|
||
|
||
### L1 (Low, снимается с записью). Номер строк для сниппета M1 отстал от кода на SHA `1f9d9014`
|
||
|
||
**Где**: `docs/specs/403-area-relocation-safety.md`, раздел «(2) M1»:
|
||
«`src/houseplan-card.ts:5056-5058`».
|
||
|
||
**Проверено чтением**: на `1f9d9014` этот диапазон (`:5056-5058`) — три
|
||
строки середины вызова `resolveDeviceAreaRelocations` (`model:`, `layout:`,
|
||
`snapshot:` — параметры объекта опций), не имеющие отношения к M1. Сам
|
||
процитированный в ТЗ сниппет (`this._areaRelocationIds = new
|
||
Set(...); if (...) { this._cancelDeviceDrag(); this._devicePositionHistory.clear();`)
|
||
дословно совпадает с кодом, но находится на строках `:5062-5065`.
|
||
Содержание находки верно и не пострадало (сверено выше, в «Как
|
||
проверялось», п.3), только адрес неточен — вероятно, из-за смещения при
|
||
правках между тем, когда аналитика собирала цитату, и фиксацией ТЗ.
|
||
|
||
**Решение ревьюера**: не блокирует, не создаёт отдельного цикла. Снимаю с
|
||
записью — исправить номера строк можно попутно при правке по H1 (тот же
|
||
файл будет открыт на редактирование), отдельного возврата ради одной этой
|
||
находки не требуется.
|
||
|
||
## Что проверено и признано корректным
|
||
|
||
- **Диагноз C2 точен и воспроизводим**: ветка отказа удаления восстанавливает
|
||
layout (`:5184`), ветка отказа записи конфига — нет (`:5223-5248`); порядок
|
||
«delete-first» (`:5147` комментарий «Layout deletion is deliberately
|
||
completed before provenance advances») подтверждён и корректно процитирован.
|
||
Самовоспроизводящийся цикл (снапшот откатывается → резолвер снова решает
|
||
`relocate`) подтверждён логикой `resolveDeviceAreaRelocations`
|
||
(`device-area-relocation.ts:181-188`), а не выдан за факт без опоры на код.
|
||
- **Диагноз M1 точен**: `clear()` действительно безусловен по всему набору
|
||
переезжающих устройств, `history.device_stale` действительно существует как
|
||
образец уже принятого в проекте паттерна уведомления об очистке истории.
|
||
- **Два допустимых исхода C2 (запись первой / восстановление при отказе)
|
||
сформулированы как решаемая ревьюером/автором техническая развилка**, а не
|
||
как догадка, выданная за факт — с явным критерием выбора (AC3, свойство
|
||
delete-first) и явной эскалацией в §"Риски", если восстановление тоже
|
||
отказывает («потеря неизбежна» → AC1 формулируется как «позиция ИЛИ
|
||
метка», не «позиция всегда»). Это корректное использование блока
|
||
«принято предположительно» из §7.1 AGENTS.md для чисто технических решений.
|
||
- **AC1–AC7 однозначны и указывают способ доказательства** (браузерный смок,
|
||
для AC7 — уже существующий `demo/smoke_area_relocation.mjs`). Способ
|
||
реалистичен: существующий смок #126 уже умеет мокать отказ `config/set`
|
||
(`rejectKettleRelocation`), новый смок под #403 — органичное расширение
|
||
того же приёма, не изобретение с нуля.
|
||
- **Скоуп/не-скоуп корректен и не пересекается** с #126 (критерии переезда,
|
||
формат снапшота — не трогаются), #74/#397 (механика Undo как таковая — не
|
||
трогается, трогается только объём очистки), #406 «г» (гигиена снапшотов
|
||
исчезнувших устройств — не относится к этой находке).
|
||
- **Соответствие `docs/SCOPE.md`**: закрывает J6 (оптимистичная блокировка,
|
||
ручная расстановка маркеров) и защищает от нарушения стоящего правила
|
||
«никогда не удалять пользовательские данные по инференсу» — задача не
|
||
расширяет продукт, а чинит потерю уже введённых пользователем данных.
|
||
- **i18n-раздел корректно условен**: если решение обходится существующей
|
||
меткой внимания — новых строк нет; если потребуется отдельное уведомление —
|
||
явно предписано добавить ключ во все четыре словаря. Не оставляет открытого
|
||
вопроса без явного правила на оба исхода.
|
||
- **Откат и release-артефакты адекватны масштабу**: обе правки локальны
|
||
(один метод, один фильтр), changelog User-Visible: yes корректно назван
|
||
(пользователь увидит, что расстановка переживает отказ записи).
|
||
- **Метки issue** (`bug`, `P1`, `S4-spec-review`, без `small`/`trivial`)
|
||
согласуются с заявленным в ТЗ полным треком — задание корректно определило
|
||
входной трек.
|
||
- **Инфраструктурная переписка issue** (шесть провалившихся прогонов
|
||
конвейера, `is_error`/`modelUsage: {}`) не образует циклов ревью:
|
||
вердикта не было ни в одном из них, бюджет §4 остаётся 0 до этого раунда.
|
||
|
||
## Чего не проверял
|
||
|
||
- **Гейты кода** (`npx tsc --noEmit`, `npm test`, `npm run build`,
|
||
`check-docs`, смоки, инварианты модели) — не гонялись: на этапе ТЗ
|
||
продуктового кода нет, диффа для гейтов не существует (диапазон
|
||
`origin/dev..HEAD` состоит только из `docs/specs/403-area-relocation-safety.md`).
|
||
Это предмет код-ревью после реализации.
|
||
- **`scripts/mutation-gate.mjs` / `demo/smoke_area_relocation.mjs` —
|
||
запуск**: не запускал ни то, ни другое; только убедился построчным чтением,
|
||
что оба файла существуют, а механизм мока отказа WS (`rejectKettleRelocation`)
|
||
в существующем смоке технически совместим с планом автотестов ТЗ (AC1/AC4).
|
||
- **Реальный браузерный повтор репродукции C2/M1** — на этапе ТЗ кода
|
||
нет; воспроизведение, описанное автором аналитики (лог WS-вызовов,
|
||
состояние `layoutHasDevice`/`snapshotAreaNow`/`attention`/`undoAvailable`),
|
||
принято на основании сверки с логикой резолвера и обработчика отказа в
|
||
текущем дереве (см. «Как проверялось», п.3), а не путём собственного
|
||
запуска браузера — независимая браузерная перепроверка будет предметом
|
||
код-ревью, когда появится смок.
|
||
- **`CommandStack`-реализация выборочного удаления** — не проектировал и не
|
||
требовал конкретного API; отметил только, что текущий тип данных
|
||
(`NamedCommand<DevicePositionState>` с `deviceId` в каждой записи) делает
|
||
контракт AC5/AC6 технически осуществимым, выбор метода — за реализацией.
|
||
- **Таблицу `docs/specs/README.md`** — строка для #403 в неё не добавлена;
|
||
это известный, не относящийся к этой задаче долг (§7.3 п.1 PROCESS.md),
|
||
не поднимаю отдельной находкой.
|
||
|
||
## Вывод
|
||
|
||
Диагноз и контракт по обеим находкам аудита (C2, M1) точны, построчно
|
||
сверены с кодом на `1f9d9014` и не содержат догадок, выданных за факт; AC1–
|
||
AC7 однозначны, доказуемы и реалистичны на существующей тестовой
|
||
инфраструктуре. Единственная блокирующая находка — процедурная (H1):
|
||
ТЗ не называет обязательные по DoR §2.5 пункты про touch/kiosk и
|
||
производительность. По существу риска в обоих пунктах, скорее всего, нет,
|
||
и правка — несколько строк текста; возвращаю жёлтым, не красным.
|