25 KiB
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 «никогда не удалять данные пользователя по
догадке» — обе находки именно про это: ручная позиция маркера — данные,
введённые пользователем руками.
Как проверялось
- Прочитаны целиком
docs/SCOPE.md,AGENTS.md,PROCESS.md(§1, §2.4, §2.5, §4, §5, §6, §7.1, §7.2, §8), тело issue #403 и все восемь комментариев (включая последовательность из шести неудачных запусков конвейера — учтено при подсчёте бюджета циклов, ни один из них вердикта не дал). - Прочитан весь текст ТЗ
docs/specs/403-area-relocation-safety.md. - Каждое фактическое утверждение ТЗ о коде сверено построчно с деревом на
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 до финального успешного прогона (комментарии аналитики) — воспроизведение не голословно, ошибка автора зафиксирована и исправлена явно.
- Проверено соответствие терминологии
docs/USER-GUIDE.ru.md: раздел «Устройства» (:757-771) документирует именно тот успешный путь, который AC2 требует сохранить («старая позиция удаляется… появляется красная отметка внимания»), и раздел «История редактора» (:260,:808-809) подтверждает существующий контракт Undo (50 команд, best effort на touch) — спецификация не вводит новых терминов и не противоречит гайду. - Проверено
docs/TOUCH-SUPPORT.mdи DoR-чек-лист §2.5 на обязательные пункты «влияние на touch» и «влияние на производительность» — см. находку H1. - Проверены обязательные разделы §7.1 — присутствуют все (сценарий · что человек увидит до/после · проблема и контракт по каждому пункту · скоуп/ не-скоуп · UX · модель данных и миграция · i18n · AC1–AC7 с доказательством · план автотестов · риски · откат · release-артефакты).
- Проверено существование инструментов, на которые ссылается план тестов:
scripts/mutation-gate.mjsесть, ни одного мутантаarea-relocation-*в нём пока нет (согласуется с ТЗ — это новые мутанты);demo/smoke_area_relocation.mjsсуществует и уже умеет мокать отказhouseplan/config/set(rejectKettleRelocation, строки 19/262/279) — план AC1/AC4 технически реализуем на существующей инфраструктуре смоков, не является фантазией. - Гейты кода (
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 и
производительность. По существу риска в обоих пунктах, скорее всего, нет,
и правка — несколько строк текста; возвращаю жёлтым, не красным.