Files
houseplan-card/docs/reviews/CODE-REVIEW-291-r2.md
T
claude[bot] a4ef0e2088
Validate / docs (push) Successful in 23s
Validate / process-workflow-sync (push) Successful in 39s
Validate / provenance (push) Successful in 46s
Validate / process-gate (push) Failing after 44s
Validate / changes (push) Successful in 34s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / frontend (push) Skipped
Validate / reuse (push) Successful in 46s
Validate / smoke (1) (push) Skipped
Validate / smoke (2) (push) Skipped
Validate / smoke (3) (push) Skipped
Validate / smoke_done (push) Skipped
Validate / golden (push) Skipped
Validate / performance_smoke (push) Skipped
Validate / backend (push) Skipped
docs: review document for #291
Issue: #291
User-Visible: no
2026-08-24 14:34:43 +00:00

239 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-291-r2
- **Issue:** #291 «Барьер записи координат: убрать шум как явление»
- **Этап:** код-ревью (PROCESS.md §2.7), **заход r2**
- **Диапазон:** `52c74301..3c754729` (merge-base с `origin/dev` .. HEAD ветки
`issue/291-lattice-coordinate-barrier`), 12 коммитов, 38 файлов, +2696/-327
- **ТЗ:** `docs/specs/291-lattice-coordinate-write-barrier.md`, зелёное ревью
`SPEC-REVIEW-291-r3.md`
- **Ревьюер:** свежая сессия, без контекста реализации; артефакты читаны из
ветки и через `gh`, не со слов автора
## Почему это ПОЛНЫЙ разбор, а не разбор по дельте
Вердикт r1 (`CODE-REVIEW-291-r1.md`) получен на диапазоне
`10fa0dc7..81d2a801`. Между r1 и этим заходом ветка была **ребейзнута на
ушедший вперёд `dev`** после слияния #290 (комментарий автора 14:17:38 UTC:
«Ребейз на актуальный `dev` после слияния #290 завершён»). Проверено прямо:
```
$ git cat-file -e 81d2a801^{commit}
fatal: Not a valid object name 81d2a801^{commit}
$ git cat-file -e 7de1f44^{commit} # коммит с фиксами M1/M2 из r1
fatal: Not a valid object name 7de1f44^{commit}
```
Оба SHA, на которые ссылался r1 (диапазон ревью и коммит с фиксами),
переписаны и не существуют в дереве. Это буквально случай PROCESS.md §2.10 /
AGENTS.md §7.2: «после ребейза на ушедший вперёд `dev` это другой код» —
разбор по дельте здесь неприменим, весь диф пройден заново.
## Скоуп
Единый барьер записи, приводящий координаты, лежащие ближе `1e-4` шага сетки
к узлу `k/240`, к точному double этого узла — во frontend и backend, в layout
и config, с отдельным отчётом Optimize и защитой от обхода. Не входит: смена
схемы хранения, #288/#289/#290, union-алгоритм #278, снап авторской off-grid
геометрии.
## Как проверялось
Гейты по PROCESS.md §8, соразмерно задаче (диапазон трогает `src/**`,
`custom_components/**/*.py`, геометрию и ссылки на неё):
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | зелёный |
| Unit | `npm test` | 1242 passed, 1 skipped, 0 failed (было 1232 на r1 — рост за счёт `test/coordinate-write-barrier-guard.test.mjs` и правок M1/M2) |
| Build + bundle parity | `npm run build && npm run bundle:sync` + `cmp` трёх копий | `dist/`, `custom_components/houseplan/frontend/`, `demo/srv/assets/` побайтово идентичны |
| Docs fingerprint | `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» — обязателен, diff трогает `src/**` |
| Model invariants | `node scripts/model-invariants.mjs --config test/fixtures/real-plan-first-floor.json --lattice` (raw fixture) | 66.15% шума (127/192), худший `8.00e-8` шага — воспроизводит измеренную причину issue буквально; после барьера (через `npm test`, см. ниже) — 0 |
| Model invariants (после барьера) | `npm test` → `test('барьер очищает клоны реальных планов, не переписывая source fixtures (#291)')` | зелёный: `profile.noise===0` на клонах обоих реальных планов, `checkWallKeys`/`checkMixedRoleRecords`=0, raw-файл байт-в-байт неизменен |
| Mutation gate (структурная проверка) | `node scripts/mutation-gate.mjs --check` | все патчи, включая 6 lattice-мутантов, применяются к текущему коду ровно один раз |
| Mutation gate (полный прогон, все 6 lattice-мутантов) | `--id=lattice-round-truncates`, `--id=frontend-writes-raw-coords`, `--id=lattice-noise-threshold-too-small`, `--id=lattice-noise-threshold-too-large`, `--id=lattice-layout-allowlist-omitted`, `--id=lattice-unknown-fields-recursive` | все 6: мутант применён → именованный тест **покраснел**, гейт зафиксировал ловлю (`python-lattice-round-truncates` не прогнан — backend, см. «Не проверял») |
| Performance (AC11) | `node demo/benchmark_coordinate_write_barrier.mjs` | ratio 0.608 (лимит 1.2), независимый прогон |
| Smoke — выбор | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 3 «прямых совпадения» (`cellCm`) + 1 «зарегистрированная связь» (`repairSpaceReferences`) — те же, что на r1 |
| Smoke — по AC4/AC7 | `smoke_lattice_write_barrier`, `smoke_optimize_coordinate_canonicalization` | оба **OK**, все под-проверки true |
| Smoke — композиционное доказательство AC4 (M1) | `smoke_wall_chain_thickness`, `smoke_room_resize`, `smoke_opening_preview`, `smoke_free_walls`, `smoke_decor`, `smoke_drag_bounds` | все 6 **OK** — реальные production controller paths (wall chain, resize, opening/partition/column, decor, marker/label drag) |
| Smoke — по smoke-select | `smoke_grid_scale_invariance`, `smoke_space_scale_defaults`, `smoke_orphan_space_references` | все **OK** |
| Backend | `python -m pytest tests_backend -q` | **не выполнено** — среда без `pytest`/`homeassistant` (см. «Не проверял»); backend-`.py` не менялся ни одним коммитом после `4ccf4a3c` (см. ниже), значит это тот же код, что был зелёным в CI |
| Golden | не прогонялся | сознательное решение, см. «Не проверял» |
| Process gate | `node scripts/process-gate.mjs --github-range --issues` локально на HEAD | «гейт пройден, предупреждений 0» |
| Process gate (CI, точный SHA) | `gh run view` на run `32737681761` (HEAD `3c754729`) | `success`; `docs`/`process-gate`/`provenance` реально выполнены и зелёные |
Вывод `smoke-select.mjs` (идентичен r1 — диф по `src/**` не расширился сверх
трёх изменённых файлов):
```
Изменено файлов src/**: 3 · символов проекта на изменённых строках: 26
Прямое совпадение (3): smoke_decor.mjs ← cellCm; smoke_grid_scale_invariance.mjs ← cellCm;
smoke_space_scale_defaults.mjs ← cellCm
Зарегистрированная связь (1): smoke_orphan_space_references.mjs ← repairSpaceReferences
```
Итого прогнано лично 11 браузерных смоков (2 по AC + 6 по композиции AC4 + 3
по smoke-select), все зелёные.
### CI на финальном SHA — что реально проверено автоматикой, а что нет
Проверил независимо от слов автора, через `gh run view --json jobs`:
- `3c754729` (текущий HEAD): Validate `success`, но `changes`-job отфильтровал
диапазон последнего пуша (только `docs/images/screenshots.json`) —
`frontend`/`backend`/`smoke`/`golden`/`performance_smoke` **skipped**, не
«зелёные», а не запускались вовсе;
- `81d2a801` (пуш со скриншотами, SHA которого больше не существует после
ребейза): та же картина — все браузерные/backend job'ы skipped;
- `9a42c85` (SHA пре-ребейз реализации, тоже переписан): здесь
`frontend`/`hacs`/`hassfest`/`backend` реально **success** до отмены
прогона следующим пушем; `golden`/`smoke`/`performance_smoke` — `cancelled`
(штатное поведение `concurrency.cancel-in-progress`), а не «зелёные».
Вывод: ни один CI-прогон в истории этой задачи (ни до, ни после ребейза) не
довёл `golden`/`smoke`/`performance_smoke` до завершения. Это не искажение со
стороны автора (в хендоффах формулировка точная — «Linux backend … зелёный»,
без утверждений про golden/smoke), но практическая проверка ложится на
код-ревью, что и сделано выше (11 смоков лично, бенчмарк лично, invariants
лично). Backend — единственный пробел, не покрытый ни CI на актуальном коде,
ни локальным исполнением (см. «Не проверял»), закрыт чтением и тем, что файл
не менялся с последнего зелёного `backend`-прогона.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **M1** (Medium) — AC4 не доказан ни автотестом, ни явной записью: `smoke_lattice_write_barrier` инъецировал шум напрямую в `_serverCfg`/`_layout`, минуя контроллеры | ТЗ переписан (AC4, коммит `3f633888`): явно зафиксирована **композиционная** схема доказательства — 6 production-bundle смоков реальных контроллеров + executable writer-inventory guard + `smoke_lattice_write_barrier` на уровне общей границы. `docs/TESTING.md` получил два новых пункта с той же формулировкой и списком `[auto: …]`. Комментарий в самом смоке (`smoke_lattice_write_barrier.mjs:96-99`) прямо называет разделение ответственности | `docs/specs/291-lattice-coordinate-write-barrier.md` §AC4 (текст «Поведение доказывается композицией, а не вторым mega-smoke»); `docs/TESTING.md` два новых пункта `[auto: ...]`; прогнал лично все 6 controller-смоков + writer-inventory тест — все зелёные (таблица выше) |
| **M2** (Medium) — `docs/USER-GUIDE.ru.md` не обновлён при `User-Visible: yes`, хотя `docs/USER-GUIDE.md` (EN) получил абзац | RU-раздел 19 получил абзац с тем же контрактом: общий счётчик, максимум с тремя значащими цифрами, scientific notation ниже 0.001 см, только затронутые пространства, неизменность off-grid значений | `git diff origin/dev...HEAD -- docs/USER-GUIDE.ru.md` — непустой, абзац на месте (см. «Что проверено» ниже за точный текст) |
| **L1** (Low) — хендофф цитировал `cancelled` прогон как «зелёный» для backend-job без упоминания отмены | Не требовал правки (наблюдение о точности цитирования, не о коде); не блокирует, статус не менялся | — |
Обе Medium-находки закрыты по существу и проверены построчно (диффы выше,
не заявление автора), не сняты игнорированием. Third `M2`-related risk —
не перепутана ли RU-формулировка с устаревшей — проверена: абзац называет
`3.30e-5 cm`-подобную scientific-notation политику и «только затронутые
пространства», то есть терминологию именно #291, а не старый общий текст
раздела 19 (написанный до ветки задачи).
## Унаследовано из r1
Поскольку разбор в этом заходе полный (см. «Почему это полный разбор»
выше), формально «наследовать без проверки» нечего — весь диф пройден заново
этим документом. Единственное, что взято как факт без повторной проверки:
**зелёный вердикт ревью ТЗ** (`SPEC-REVIEW-291-r3.md`, SHA `4ec43ffd`+ и
`23fb41d4`+, три захода, зелёное на r3) — ревью кода не пересматривает
продуктовые решения владельца, зафиксированные на этапе ТЗ (Q1 про
детализацию Optimize-отчёта, threshold `1e-4`, allow-list полей).
## AC — построчно (повторная проверка, полный разбор)
| AC | Доказательство | Вердикт |
|---|---|---|
| AC1 Идемпотентность/точные биты | `test/coordinate-canonicalization.test.mjs`: 4801 узел, идемпотентность, `-0`→`+0`, `0.06`/`0.2875` не снапятся, граница `1e-4` — в `npm test` (зелёный) | ✅ доказано автотестом, выполнено |
| AC2 Shared allow-list parity | Общая fixture, deep-equal TS/Python; TS-часть выполнена (`npm test`); backend-часть проверена чтением (файл не менялся с последнего зелёного backend CI) | ✅ доказано автотестом (TS) + чтением (backend) |
| AC3 Живой профиль после migration = 0 | `test/model-invariants.test.mjs`: real-plan clone → noise=0, `checkWallKeys`/`checkMixedRoleRecords`=0, raw byte-equal — зелёный в `npm test`; плюс мой независимый CLI-прогон на сыром real-plan-first-floor.json (66.15% шума, воспроизводит измерение issue) | ✅ доказано автотестом + исполнением |
| AC4 Произвольная editing session не возвращает noise | Композиционное доказательство теперь явно задокументировано (закрытие M1 выше) и лично прогнано целиком: 6 production controller smokes + writer-inventory guard test + `smoke_lattice_write_barrier` (граница) — все зелёные | ✅ доказано композицией автотестов, все компоненты лично исполнены |
| AC5 Write barrier нельзя обойти | `test/coordinate-write-barrier-guard.test.mjs` (инвентарь всех frontend-writer'ов + backend Store boundary); мутанты `lattice-round-truncates`/`frontend-writes-raw-coords` лично прогнаны — красятся | ✅ доказано автотестом, выполнено |
| AC6 Source fixtures остаются шумными | Тест в `npm test`: raw-файл байт-в-байт равен до/после, `profile.noise>=100` пин остаётся | ✅ доказано автотестом, выполнено |
| AC7 Optimize report и transaction | `smoke_optimize_coordinate_canonicalization` (лично, зелёный, 13/13); порядок операций в `plan-optimizer.ts` проверен чтением: `lattice`-отчёт измеряется один раз **до** мутаций (строка 427), финальный safety-pass канонизации (строки 649-650) явно прокомментирован как «internal work must not leak into the user-visible report» и гейтится тем же `changed`-флагом, что и остальные счётчики — не создаёт второго источника числа | ✅ доказано автотестом + smoke + чтением, отдельно проверено на предмет «одно число — один источник» после интеграции с #290 (near-axis repair/повторный align) |
| AC8 Compatibility paths | Существующие тесты не тронуты диффом сверх заявленного; no schema/version bump | ✅ проверено чтением + существующими тестами |
| AC9 Polyclip/#278 regression | `npm test` включает все 5 тестов `#278 …`, зелёные (видел в хвосте прогона: `#278 anonymized regression…`, `#278 component set is deterministic…`, `#278 Optimize and model-invariants use the same strict structural result`, `#278 physical fingerprint…`, `#278 production source routes physical writers…`) | ✅ доказано автотестом, выполнено |
| AC10 Мутанты | 6 lattice-мутантов — все лично прогнаны и красятся (таблица гейтов); backend-мутант не прогнан (среда, см. «Не проверял») | ✅ доказано исполнением (frontend), backend — чтением |
| AC11 Performance | `demo/benchmark_coordinate_write_barrier.mjs`, ratio 0.608 (я, независимый прогон), лимит 1.2 | ✅ доказано бенчмарком, выполнено |
| AC12 Локальные гейты | typecheck/test/build/bundle-parity/check-docs — выполнены и зелёные (таблица выше); backend — не выполнен (среда) | ✅/⚠️ частично, backend — см. «Не проверял» |
## Находки
Находок уровня High или Medium в этом заходе **нет**. Обе находки r1 закрыты
(таблица выше), нового Medium/High разбор не вскрыл.
### Low — не блокируют
- **L2** (унаследовано из r1/спек-ревью, не относится к коду): формулировка
тела issue «Стадия 1 из ADR #282» технически неточна (по ADR #282 «Стадия
1» — stable wall ids, явно вынесенные в «Не входит» этого же ТЗ). Не
влияет на код-ревью, отмечен ещё на этапе ТЗ, не блокирует.
- **L4** (новое наблюдение этого захода) — ни один CI-прогон в истории ветки
не довёл `golden`/`smoke`/`performance_smoke` до завершения (см. раздел про
CI выше): либо `changes`-path-filter не включил их (докозаписи пуши),
либо следующий пуш отменил незавершённый прогон. Не дефект процесса
(штатное поведение `concurrency.cancel-in-progress` и путевого фильтра,
задокументированное в r1), но фактическую нагрузку по этим трём категориям
целиком несёт код-ревью — что и сделано (11 смоков + бенчмарк лично). Не
блокирует, фиксирую как честное описание происхождения зелёных цифр.
## Что проверено и корректно
- **Ребейз не сломал заявленный порядок операций.** `plan-optimizer.ts:427-432`
измеряет lattice-отчёт и применяет барьер **до** `alignAllToGrid`/
`repairNearAxisRoomWalls` (новый шаг #290); финальный safety-pass в
`canonicalizeConfigGeometryInPlace`/`canonicalizeLayoutGeometryInPlace`
(строки 649-650) идёт **после** второго align-прохода для выпрямленных
стен и явно прокомментирован как «internal work… must not leak into the
user-visible report» — то есть отчёт lattice не задваивается и не
занижается интеграцией с #290. Автор описал это словами в хендоффе
(«canonicalization до Align → near-axis repair → повторный Align →
финальная canonicalization»); я проверил это построчно по коду, а не
поверил формулировке.
- **«Одно число — один источник» для нового placeholder `{cm}`.**
`formatLatticeShiftCm` (использован в диалоге Optimize) и
`latticeReport()`/бенчмарк используют одну и ту же функцию форматирования
с тремя значащими цифрами; не смешивается с обычной политикой округления
`moved/maxShiftCm` grid-align (разные ключи i18n, разные поля отчёта).
- **Backend не менялся с последнего зелёного backend-CI.**
`git log -- 'custom_components/houseplan/*.py'` в диапазоне задачи
показывает единственный коммит `4ccf4a3c` — тот же контент, что был
протестирован в run `32732620838` (`backend: success`, до отмены прогона
следующим пушем). Ни один последующий коммит backend-файлы не трогал —
чтение актуального кода эквивалентно чтению того, что уже прошло CI.
- **i18n.** `gs.optimize_lattice_summary`/`gs.optimize_lattice_space`
присутствуют в `en.json`/`ru.json` буквально как в ТЗ §7.1; проверил
оба файла напрямую.
- **Трейлеры.** Все 12 коммитов диапазона несут `Issue: #291` и
`User-Visible: yes|no`; единственный `User-Visible: yes` (`4ccf4a3c`)
правит оба `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` в одном коммите
(проверил дифф обоих файлов напрямую).
- **Process gate.** И локальный прогон, и CI на точном финальном SHA
(`3c754729`, run `32737681761`) зелёные.
- Гвард `scripts/coordinate-write-barrier-guard.mjs` — структурная проверка
количества writer call-site'ов (1 config/set, 2 layout/update, 1
localStorage, 1 Optimize write во frontend; central Store writer'ы в
backend) читана целиком: любой новый writer, обходящий единую границу,
меняет счётчик occurrences и красит `checkCoordinateWriteBarriers`.
## Чего не проверял и почему
- **Backend pytest** (`tests_backend/test_coordinate_canonicalization.py`):
среда ревью не имеет `pytest`/`homeassistant`
(`.venv-backend` отсутствует, это известное ограничение не-облачного
окружения, AGENTS.md). Заменил чтением плюс фактом, что backend-`.py` не
менялся с последнего зелёного backend CI-прогона (`32732620838`, до
отмены следующим пушем) — см. «Что проверено».
- **`npm run golden:verify`**: не прогонял. Тест
`'модели проекта не несут шума решётки (#282)'` (зелёный в `npm test`)
доказывает, что golden-фикстуры не содержат lattice-шума — значит новые
условные строки Optimize-диалога (`${r.latticeCoordinatesCanonicalized ? …}`)
не рендерятся в golden-сценариях и визуального расхождения не должно быть.
Golden — предрелизный гейт (PROCESS.md §8), не гейт код-ревью.
- **Полный набор из 183 браузерных смоков**: не прогонял, кроме выбранных 11
(2 AC + 6 композиция AC4 + 3 smoke-select) — по правилу §8 «полные наборы —
гейт предрелиза».
- **Полный HA-харнесс / WSL**: недоступен в среде ревью.
- Не проверял отдельно совместимость с #288/#289/#290 сверх того, что их
регрессионные тесты (в т.ч. `#278 …`, `#290 near-axis audit…`) прошли в
`npm test` без изменений в диффе, не относящихся к #291.
## Итог
High-находок нет. Обе Medium-находки r1 (M1 — композиционное доказательство
AC4 не было названо явно; M2 — `docs/USER-GUIDE.ru.md` не обновлён) закрыты
по существу и проверены построчно, не заявлением автора. Разбор в этом
заходе полный, а не по дельте, потому что ребейз на ушедший вперёд `dev`
переписал оба SHA, на которые ссылался r1 (диапазон ревью и коммит с
фиксами) — код на ветке буквально другой. Ядро реализации — канонизация,
единый непроходимый write barrier, backend-зеркало, идемпотентность,
защита от обхода, производительность, интеграция с новым near-axis/align
пайплайном #290 — проверено заново и работает, включая независимое
исполнение 11 браузерных смоков, 6 мутантов, бенчмарка и invariants на
реальных данных.
**Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0**