15 KiB
SPEC-REVIEW-248-r1
- Issue: #248 — «Оптимизировать» не идемпотентна после записи и reload
- ТЗ:
docs/specs/248-optimize-idempotence.md, веткаissue/248-optimize-idempotence, коммит4d73d031a477c06c5b28366bd9e3883ebec421f1 - Этап: spec (PROCESS.md §2.4), заход r1, трек обычный (не
small) - Вердикт: зелёный
Скоуп ревью
Первый заход — разбор полный, разделов «Закрытие раунда» и «Унаследовано» нет (PROCESS.md §2.10 применяется со второго цикла).
Проверено по порядку из инструкции:
docs/SCOPE.md— сценарий закрывает J6 («Keep the plan true as the home evolves»); действие administrative/desktop-first, View/kiosk не задеты.AGENTS.md,PROCESS.md§7.1, §2.4, §2.5 — обязательные разделы ТЗ, классы файлов, критерии DoR.- Тело issue #248 и оба комментария (аналитика владельца, хендофф автора ТЗ).
docs/USER-GUIDE.ru.md— раздел «Что делает оптимизация» (строки 1360–1409), терминология «сдвинуто/устранён шум/повторный Optimize».- Канонический документ подсистемы —
docs/CANVAS.md§9.5, плюсdocs/CONFIG-COMPATIBILITY.mdраздел «Canonical geometry on write (#224)».
Как проверялось
Ревью состязательное: ТЗ читалось без устных пояснений автора, каждое
техническое утверждение ТЗ (§3 «Подтверждённая причина», §6 «Контракт
поведения», §13 «Принятые предположения») сверялось с актуальным кодом на
HEAD, а не принималось на слово.
Прочитан код:
src/coordinate-canonicalization.ts(полностью) — allowlist и формулаcanonicalizeNumber(floor(|v|·10⁹+0.5)/10⁹,COORDINATE_DECIMALS = 9).src/plan-optimizer.ts(полностью) —optimizePlans():changedвычисляется какJSON.stringify(config) !== original, гдеoriginal = JSON.stringify(configIn), и нигде не пропускается черезcanonicalizeConfigGeometry/canonicalizeLayoutGeometry. Это подтверждает корневую причину ТЗ буквально: pure-результатsnapN()(шаг1/240, двоичное число) и то, что вернётconfig/getпосле девятизнакового round-trip storage-контракта #224, — два разных JSON-представления одного и того же логического значения.src/align-grid.ts(основная часть) —snapN(),GRID_STEP_N,EPS, перечень grid-bound/wall-bound элементов (room poly/rect, room_drafts, partitions, wall_columns, decor, openings) — совпадает с матрицей AC2.custom_components/houseplan/websocket_api.py:1554-1666(ws_plan_optimize) —pending["config"]/pending["layout"]канонизируются явно (canonicalize_ config_geometry(msg["config"])), а фактическая запись идёт черезasync_save_config_state()/async_save_layout_state().custom_components/houseplan/store.py:150-229— оба метода канонизируют геометрию внутри себя (layout_store_payload→canonicalize_layout_ geometry,async_save_config_state→canonicalize_config_geometry) независимо от того, что передал вызывающий код.custom_components/houseplan/__init__.py:184(startup finisher,optimize_pending) — путь recovery, на который ссылается AC3, существует.test/plan-optimizer.test.mjs,test/align-grid.test.mjs,test/fixtures/coordinate-canonicalization.json+test/coordinate-canonicalization.test.mjs+tests_backend/test_coordinate_canonicalization.py— подтверждают, что паттерн «одна JSON-фикстура, читаемая Node и Python независимо, без вызова одного рантайма из другого» (§13.2 ТЗ) уже используется в проекте для ровно той же канонизации, а не изобретается заново.
Вывод по существу: исходная гипотеза issue («сырая запись против
канонизированного pending») в актуальном dev не подтверждается — ровно как
и написал автор в аналитике и в ТЗ §3. Обе фактические записи
(async_save_config_state, async_save_layout_state) канонизируют геометрию
сами, независимо от вызывающего кода. Настоящая причина —
несовместимость двух корректных по отдельности числовых контрактов (шаг
сетки 1/240 не представим точной десятичной дробью, storage округляет её
до 9 знаков), и именно она воспроизводится по коду. ТЗ не выдаёт догадку за
факт: раздел §13 явно маркирует технические решения как «принято
предположительно», а корневая причина подтверждена чтением, а не заявлена.
Разделы §7.1 — проверка полноты
Все обязательные разделы присутствуют и не пусты: сценарий (§1) · что человек
увидит до/после (§2) · проблема/причина (§3) · scope/non-scope (§4–5) ·
контракт поведения (§6) · UX/i18n/touch (§7) · модель данных и совместимость
(§8) · AC1…AC6 с доказательством (§9) · план автотестов (§10) · риски/perf/
security/rollback (§11) · release-артефакты (§12) · принятые технические
предположения (§13). Продуктовых вопросов владельцу нет — по инструкции
такое допустимо, когда ожидаемое поведение уже зафиксировано (здесь —
docs/CANVAS.md §9.5 и текущий текст USER-GUIDE.ru.md, см. ниже).
AC — однозначность и способ доказательства
| AC | Однозначен? | Доказательство названо и выполнимо? |
|---|---|---|
| AC1 | Да — «changed:false, нулевые счётчики, deep-equal» проверяемо программно | unit + mutation guard на «убрать финальную канонизацию» — файл test/plan-optimizer.test.mjs существует, фикстуры названы |
| AC2 | Да — конкретная матрица geometry-типов и cell_cm |
unit parameterized + существующий test/align-grid.test.mjs + mutant «вернуть сырой 1/240» |
| AC3 | Да — exact-equal pending/final/recovery pair | backend pytest, startup finisher (__init__.py:184) реально существует |
| AC4 | Да, включая явную оговорку «без Node subprocess из pytest» (§13.2) | общая fixture по прецеденту coordinate-canonicalization.json; парность фикстур — новый, но понятный guard |
| AC5 | Частично составной (browser smoke + чтение кода Undo lifecycle), но каждая половина названа отдельно | targeted smoke с mocked WS + explicit «проверено чтением» для Undo — соответствует §18 PROCESS.md |
| AC6 | Да, стандартная формулировка | typecheck/unit/build + bundle parity; golden/smoke/perf — предрелizный гейт по правилу «полные наборы — не гейт ревью» |
Ни один AC не содержит скрытого домысла о поведении, которого нет в
канонических документах: контракт §6.1–6.4 — прямое следствие уже
существующего docs/CANVAS.md §9.5 («every pass is idempotent») и
docs/CONFIG-COMPATIBILITY.md («A repeated canonical Save … is a no-op»),
только явно распространённое на write/reload boundary, которого раньше не
было в явном виде.
Находки
Блокирующих (High) находок нет. Находок Medium в скоупе или вне скоупа нет.
Low — не блокирует, оставлено на усмотрение автора
- §12 ТЗ формулирует обновление
docs/USER-GUIDE.ru.mdтак, будто факт идемпотентности сейчас не документирован, а строка 1380 текущегоUSER-GUIDE.ru.mdуже утверждает «Повторный Optimize над результатом ничего не предлагает» — то есть документ уже обещает то поведение, которое чинит эта задача, только без явного упоминания границы reload/server-event. Не искажает контракт и не создаёт риска для AC — при реализации это, вероятно, точечное уточнение одной фразы, а не новый раздел. Снимается автором по факту правки; фиксирую, чтобы это не создало неверного впечатления при код-ревью, что документация была неверна и её было нужно переписывать заново.
Что проверено и корректно
- Корневая причина (§3 ТЗ) подтверждена чтением actual
dev, а не унаследована из первоначальной (опровергнутой) гипотезы issue. - Контракт §6 не противоречит
docs/CANVAS.md§9.5 иdocs/CONFIG-COMPATIBILITY.md— расширяет их на write/reload boundary. - Non-scope (§5) корректно исключает изменение шага сетки, точности канонизации #224, миграций и визуальных изменений — задача не расширяет скоуп бага в рефакторинг.
- Тестовый план (§10, AC1–AC4) ссылается на реально существующие файлы
(
test/plan-optimizer.test.mjs,test/align-grid.test.mjs,test/fixtures/coordinate-canonicalization.json) и на реально существующий прецедент общей Node/Python-фикстуры, а не на гипотетический паттерн. - Backend-путь (
ws_plan_optimize,async_save_config_state,async_save_layout_state, startup finisher) существует ровно там, где ТЗ на него ссылается, с теми же именами функций. - Release-артефакты (§12), rollback (§11) и i18n/touch (§7) — не создают
новых миграций, флагов или UI-контрактов; согласуется с оценкой сложности
4/10 и
User-Visible: yesдля будущего implementation-коммита. - Роли и трейлеры: коммит
4d73d03несётIssue: #248иUser-Visible: no(корректно — это документация ТЗ, не поведение); автор ТЗ и ревьюер — разные модели (Codex/Claude), правило PROCESS.md §6 соблюдено.
Чего не проверял
- Не запускал никаких гейтов (
typecheck/test/build) — на этапе spec-review предмет проверки текст ТЗ и его соответствие коду/документам, а не implementation; кода поведения ещё нет (класс A файлов в диффе этого коммита нет — толькоdocs/specs/**). - Не проверял
custom_components/houseplan/validation.py:600(_dedupe_open_spans), упомянутый в исходном issue как альтернативная гипотеза (нормализация проёмов на записи) — ТЗ явно отводит эту гипотезу (§3: «предложенная в исходном описании причина... не подтверждается») и не включает её в scope; сама эта альтернатива не стала предметом контракта, поэтому её код не требовался для проверки полноты ТЗ. - Не оценивал производительность реализации (нет кода) — риск-таблица §11
ТЗ содержит явную оценку (
O(n)copy-on-write), достаточную для DoR. - Не проверял i18n-файлы построчно — ТЗ утверждает «новых строк нет», что
проверяемо тривиально на этапе код-ревью через diff
src/i18n/*.json.
Итог
ТЗ выполнимо, каждый AC проверяем и снабжён способом доказательства, корневая причина подтверждена по коду, а не по доверию к автору или к первоначальной (опровергнутой) гипотезе issue. Скоуп не расширяется и не сужается относительно реального дефекта. Единственная находка — Low, не блокирует переход в «Готово к разработке».
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0