mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -0,0 +1,264 @@
|
||||
# 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 и
|
||||
производительность. По существу риска в обоих пунктах, скорее всего, нет,
|
||||
и правка — несколько строк текста; возвращаю жёлтым, не красным.
|
||||
Reference in New Issue
Block a user