mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
@@ -0,0 +1,171 @@
|
||||
# SPEC-REVIEW-248-r1
|
||||
|
||||
- Issue: [#248](https://github.com/Matysh/houseplan-card/issues/248) — «Оптимизировать» не идемпотентна после записи и reload
|
||||
- ТЗ: `docs/specs/248-optimize-idempotence.md`, ветка `issue/248-optimize-idempotence`, коммит `4d73d031a477c06c5b28366bd9e3883ebec421f1`
|
||||
- Этап: spec (PROCESS.md §2.4), заход r1, трек обычный (не `small`)
|
||||
- Вердикт: **зелёный**
|
||||
|
||||
## Скоуп ревью
|
||||
|
||||
Первый заход — разбор полный, разделов «Закрытие раунда» и «Унаследовано» нет
|
||||
(PROCESS.md §2.10 применяется со второго цикла).
|
||||
|
||||
Проверено по порядку из инструкции:
|
||||
|
||||
1. `docs/SCOPE.md` — сценарий закрывает J6 («Keep the plan true as the home
|
||||
evolves»); действие administrative/desktop-first, View/kiosk не задеты.
|
||||
2. `AGENTS.md`, `PROCESS.md` §7.1, §2.4, §2.5 — обязательные разделы ТЗ,
|
||||
классы файлов, критерии DoR.
|
||||
3. Тело issue #248 и оба комментария (аналитика владельца, хендофф автора ТЗ).
|
||||
4. `docs/USER-GUIDE.ru.md` — раздел «Что делает оптимизация» (строки
|
||||
1360–1409), терминология «сдвинуто/устранён шум/повторный Optimize».
|
||||
5. Канонический документ подсистемы — `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 — не блокирует, оставлено на усмотрение автора
|
||||
|
||||
1. **§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**
|
||||
Reference in New Issue
Block a user