20 KiB
SPEC-REVIEW-442-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/442
- ТЗ:
docs/specs/442-marker-write-rollback.md - Этап:
S4-spec-review, заход r1, блокирующих циклов израсходовано 0 из 4 - Ревьюер: Claude (роль «ревьюер ТЗ», отдельная сессия от автора)
- Материал ревью: ТЗ на коммите
a8426268db90666920801f9e09554aa81343ceba(веткаissue/442-marker-write-rollback), тело issue #442 и все его комментарии на момент разбора (2026-09-03)
Скоуп разбора
Первый заход — разбор полный: продуктовая рамка (docs/SCOPE.md), процесс
(PROCESS.md §2.4/§7.1, AGENTS.md), тело issue #442 и вся цепочка
комментариев (аналитика → вопросы владельцу → решение владельца → готовое
ТЗ), связанные issue #439 (guarded rollback общих настроек) и #441 (атомарный
route CRUD), канонические docs/CONFIG-COMPATIBILITY.md и docs/VACUUM.md,
терминология docs/USER-GUIDE.ru.md. Отдельно верифицированы факты о текущем
коде, на которых ТЗ строит контракт: src/serialized-write-queue.ts,
src/houseplan-editor-runtime.ts, src/editors/vacuum-maps-section.ts,
custom_components/houseplan/validation.py, наличие названных тестов/смоков.
Как проверялось
- Прочитаны
docs/SCOPE.md,PROCESS.md(§1–§10),AGENTS.mdполностью. - Прочитаны тело issue #442 и все 4 комментария (аналитика с оценками 8/10 польза · 9/10 разработка · 5/10 сложность · 5/10 риск · P2 · full track; пачка вопросов Q1/Q2 с default-вариантами; решение владельца, принявшее оба default; финальный комментарий со ссылкой на готовое ТЗ).
- Прочитан весь текст
docs/specs/442-marker-write-rollback.md(379 строк). - Прочитаны связанные issue #439 и #441 — оба уже приняты и описывают ровно тот прецедент (guarded rollback, атомарный route writer), на который ТЗ #442 ссылается как на эталон и явно не дублирует.
- Прочитан
docs/CONFIG-COMPATIBILITY.mdцеликом — не найдено противоречий между заявленным «revision + content fingerprint» guard'ом и уже задокументированным контрактом ревизий (#340), геометрии (#282/#306/#224) и маршрутов пылесоса (#162). - Прочитан
docs/VACUUM.mdцеликом — раздел «Calibration» действительно не документирует порядок toast/подтверждение относительно ответа сервера, то есть заявленный в ТЗ release-артефакт (обновлениеVACUUM.md) не дублирует уже существующий текст, а закрывает реальный пробел. - Проверена терминология
docs/USER-GUIDE.ru.md: «Редактор устройств», «Карты и этажи», «ручная подгонка», «Применить» — используемые в ТЗ термины (кроме одного, см. Low ниже) не расходятся с интерфейсом. - Делегирована фактологическая проверка кода отдельному агенту (Explore) по 9 конкретным пунктам — см. «Верификация фактов» ниже. Каждый пункт перепроверен по конкретным строкам файла, не на веру.
- Самостоятельно прочитан
src/serialized-write-queue.tsцеликом (60 строк) — независимо подтверждён guard:host._cfgRev !== attempt.revision || (current !== attempt.attempted && fingerprint(current) !== attempt.attemptedFingerprint), то есть ревизия и контент-фингерпринт одновременно, включая ветку одного and того же object identity — именно то, что ТЗ требует мутационно проверить в AC2. - Самостоятельно найден
_writeChain/expected_rev(src/houseplan-card.ts:7662-7730,src/houseplan-editor-runtime.ts:1175и далее) — сериализация запросов, на которую ссылается контракт §2, существует и используется.
Верификация фактов ТЗ против текущего кода (HEAD a8426268)
| # | Утверждение ТЗ/issue | Результат |
|---|---|---|
| 1 | _saveMarker() мутирует cfg.markers до await _saveConfigNow(), catch только снимает busy и показывает toast |
Подтверждено — src/houseplan-editor-runtime.ts:8270-8285 мутация, :8330 await, catch :8359-8366 без восстановления _serverCfg и без rebuild devices |
| 2 | (из тела issue, не из ТЗ) готовый guarded rollback применён «ровно к одному пути — общим настройкам (_updateDecorStyle)» |
Неточность в тексте issue. Пара optimisticAttempt/rollbackOptimistic действительно встречается 3 раза (импорт + одна пара вызовов), но пара живёт в _saveSettingsDialog (:10231-10275, вызовы на :10260/:10271), а не в _updateDecorStyle (:10204) — та не содержит optimistic-логики вовсе и делегирует в _persistDecorStyle → обычный debounced _saveConfig(). Счётчик «18 вызовов _saveConfig()» корректен |
| 3 | _vacSaveMatrix() мутирует in-place, коллеры показывают success toast и закрывают UI до подтверждения записи сервером |
Подтверждено — мутация m.vacuum, _maybeRebuildDevices(), невawait-нутый debounced _saveConfig(); toast.autocal_done/toast.cal_done показываются сразу после синхронного (boolean) возврата _vacSaveMatrix, без ожидания сервера |
| 4 | Route CRUD в vacuum-maps-section.ts уже использует optimisticAttempt/rollbackOptimistic (эталон #441) |
Подтверждено — импорт :21, вызовы :105/:116 |
| 5 | Guard сравнивает revision и content fingerprint; есть _writeChain/expected_rev |
Подтверждено самостоятельно (см. «Как проверялось» п.9-10) |
| 6 | Четыре семантических валидатора маркера существуют под этими именами | Подтверждено ровно с этими именами (кроме generic «проверка controls», которую ТЗ и не называет точным именем) — validate_marker_value_badges (:811), validate_marker_vacuum_routes (:886), validate_marker_light_entities (:930), validate_marker_controls (:972, цикл — :1049) в custom_components/houseplan/validation.py |
| 7 | Названные в плане тестирования файлы существуют | Подтверждено — все 6: test/serialized-write-queue.test.mjs, demo/smoke_vacuum_route_draft.mjs, demo/smoke_vacuum_firstuse.mjs, demo/smoke_vacuum_multifloor.mjs, demo/smoke_vacuum.mjs, demo/smoke_dialog_zombie.mjs |
| 8 | docs/VACUUM.md ещё не документирует reject/success UX калибровки |
Подтверждено — раздел «Calibration» молчит именно об этом |
| 9 | scripts/mutation-gate.mjs существует |
Подтверждено |
Итог верификации: единственное расхождение с кодом (пункт 2) находится в
теле issue, не в тексте ТЗ — сам документ docs/specs/442-...md нигде не
называет _updateDecorStyle и не строит контракт на этом имени; он ссылается
на механизм обобщённо («общий writer», «уже готовый optimisticAttempt/
rollbackOptimistic»), что после проверки оказалось верным независимо от
того, в какой именно функции этот механизм физически живёт. Так что дефект
не проникает в исполнимость ТЗ — см. Low ниже, почему он всё равно стоит
записи.
Соответствие §7.1
Все обязательные разделы присутствуют: Сценарий · Что человек увидит до и после · Подтверждённая проблема · Скоуп/Не-скоуп · Контракт поведения (6 подпунктов) · Данные и совместимость · Touch/клавиатура/доступность · Ошибки и крайние случаи · AC1–AC9 с доказательством (unit/smoke/test double у каждого) · План тестирования · Карта реализации · Риски и rollback · Release-артефакты · Принятые предположения.
Владелец отвечал только на продуктовые вопросы (Q1 — какие marker-пути входят в атомизацию; Q2 — что делать с calibration UI при отказе), оба с default-вариантом и явным принятием; технические решения (immutable candidate, где именно живёт guard, разбиение на pure-модуль) автор резервирует себе явным блоком «Принятые предположения» в конце — ровно так, как требует процесс.
Находки
Low-1. Неточное имя функции в теле issue (не в ТЗ)
Issue #442 в разделе «Проблема» называет уже реализованный guarded-rollback
путь _updateDecorStyle; фактически это _saveSettingsDialog
(src/houseplan-editor-runtime.ts:10231). _updateDecorStyle — соседняя
функция без optimistic-логики. Ошибка не искажает контракт ТЗ (там имя
функции не упоминается вовсе, только «общий writer» и сам механизм), поэтому
не блокирует и не требует правки ТЗ. Фиксирую как заметку для реализации:
если разработчик будет ориентироваться на текст issue при поиске «уже
готового пути», неверное имя может стоить нескольких минут поиска не там —
не более того.
Решение ревьюера: снимается без правки, запись сделана для истории раунда.
Low-2. Явного раздела «i18n» нет — контент есть, но не под заголовком
§7.1 перечисляет i18n отдельным обязательным разделом. В ТЗ #442 подтверждение
«новых строк нет» разнесено по двум местам («Новых targets, жестов и строк
i18n нет» в разделе Touch; «новых текстов ошибок... не добавляется» в
Не-скоуп) без отдельного заголовка. По содержанию требование выполнено
(эксплицитно и дважды непротиворечиво), поэтому не блокирует переход в
S5-ready — DoR требует «i18n: ключи en+ru перечислены», а перечислять
здесь нечего, что и сказано явно. Отмечаю как стилистическую придирку, не
находку, которая должна кого-то останавливать.
Решение ревьюера: снимается без правки.
Что проверено и корректно
- Продуктовая рамка. Задача закрывает J6 (
docs/SCOPE.md: «Keep the plan true as the home evolves» — «View не должен показывать конфигурацию, которую сервер отверг»), сценарий и «что человек увидит» — оба заполнены без терминов реализации первым предложением, персона (Home admin) и поверхность (Device editor + calibration overlay) названы. - Скоуп/не-скоуп зеркалят принятые owner defaults Q1/Q2 дословно, без расширения и без урезания против решения владельца.
- Технические утверждения о текущем коде — 8 из 9 проверенных фактов подтверждены чтением реальных строк; единственное расхождение (Low-1) не влияет на сам контракт ТЗ.
- Контракт поведения опирается на реально существующий примитив
(
optimisticAttempt/rollbackOptimisticс двойным guard'ом ревизия+ фингерпринт, уже обкатанным в #441 и в_saveSettingsDialog), а не изобретает новый механизм с нуля — минимизирует риск. - AC1–AC9 каждый указывает способ доказательства (unit / browser smoke / test double) и достаточно конкретен для написания теста без дополнительных решений: например, AC2 явно называет два обязательных мутанта (unconditional rollback; пропуск fingerprint для same identity), предвосхищая требование код-ревью §2.7 о «таблице чем краснеет». Формулировки допускают ровно одну трактовку каждая — не нашёл AC, который можно закрыть двумя разными реализациями с разным видимым поведением.
- Регрессионный периметр (AC8, таблица «Ошибки и крайние случаи», строка «route CRUD #441 reject/retry») явно защищает уже принятый атомарный route writer от повторной реализации или порчи — соответствует правилу «не подменять существующий механизм».
- Данные и совместимость: явно «миграции нет», persisted schema не
меняется, что соответствует характеру задачи (только client-side
транзакционность) и не противоречит ни одному разделу
CONFIG-COMPATIBILITY.md. - Touch/доступность: busy — настоящий disabled, а не только визуальный
индикатор; фокус остаётся в открытом диалоге — соответствует
TOUCH-SUPPORT.md(редакторы desktop-first, но контракт не деградирует специально под touch). - Release-артефакты называют оба changelog,
CONFIG-COMPATIBILITY.md,VACUUM.md(подтверждено выше, что там действительно есть пробел для заполнения), условие для docs-скриншотов и явное «golden не нужен, если подходящей канонической поверхности нет» — не оставляет открытых пунктов DoR. - Догадки, выданные за решения: не найдено. Каждое место, где решение не следует напрямую из существующего документа, либо помечено как default-ответ владельца (Q1/Q2), либо вынесено в явный блок «Принятые предположения» в конце ТЗ, что ревьюер вправе (и не находит повода) оспорить.
Чего не проверял
- Не запускал
npx tsc --noEmit/npm test/npm run build— на этапе ревью ТЗ кода ещё нет (это этапspec, неcode), гейты §8 к этому документу неприменимы. - Не проверял мутационные сценарии AC2 практически (мутант ещё не написан — это работа код-ревью, не спек-ревью); отметил только, что защитный примитив уже существует и уже используется в проде (#441).
- Не проверял браузерные смоки исполнением — только их наличие как файлов (см. таблицу верификации, п.7). Их фактическое покрытие AC — задача реализации и код-ревью.
- Не проверял
docs/specs/README.mdна предмет обязательной строки в таблице issue↔ТЗ — это релиз-артефакт автора ТЗ, не блокирует ревью.
Вердикт
Зелёный. High: 0, Medium: 0. Обе Low-находки сняты без правки текста ТЗ (записаны выше для истории раунда, не блокируют). ТЗ полно по §7.1, каждый AC однозначен и снабжён способом доказательства, продуктовые вопросы решены владельцем до написания ТЗ, а технические утверждения о существующем коде подтверждены независимым чтением, а не приняты на веру.
Материал раунда
- Ветка:
issue/442-marker-write-rollback - SHA ТЗ:
a8426268db90666920801f9e09554aa81343ceba - Путь ТЗ:
docs/specs/442-marker-write-rollback.md - Найти дерево:
git log --all --format='%H %T' | grep <дерево HEAD> - Найти блоб ТЗ:
git log --all --find-object=<блоб> -- docs/specs/442-marker-write-rollback.md
Материал раунда
- Ветка:
issue/442-marker-write-rollback, коммитa8426268db90— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
5d3fbe71add69e722a9f6d87b1ce04dcea586729git log --all --format='%H %T' | grep 5d3fbe71add6 - ТЗ
docs/specs/442-marker-write-rollback.md, блобa9dcca2ff269a1c309387902875a4937efbd4ebfgit log --all --find-object=a9dcca2ff269a1c309387902875a4937efbd4ebf -- docs/specs/442-marker-write-rollback.md