Files
houseplan-card/docs/reviews/CODE-REVIEW-314-r1.md
T
2026-08-26 07:09:22 +00:00

252 lines
21 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-314-r2
Issue: [#314](https://github.com/Matysh/houseplan-card/issues/314) — Model v8: рисование комнат
отклоняется, draft IDs теряются, возможны ложные перегородки.
Этап: code (PROCESS.md §2.7).
Ветка: `issue/314-v8-draft-write-regression`. Материал ревью: диапазон
`origin/dev...HEAD`, вершина `64e9bf8f6b7dca3be5431613875e287b2cecc7be`
(`git rev-parse HEAD`, сверено непосредственно перед подведением итогов).
## Поправка нумерации захода
Вводная к этой сессии называла заход **r1** с бюджетом **0/4**. Это не
соответствует фактическому состоянию issue: в репозитории уже лежит
`docs/reviews/CODE-REVIEW-314-r1.md` (закоммичен `069c4867`) — это реальный
завершённый прогон код-ревью с **жёлтым** вердиктом, опубликованным
комментарием issue в `2026-08-26T06:43:32Z`:
> Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 1 → в задаче
Автор ответил хендофф-комментарием `2026-08-26T06:59:45Z` («Code review r1 —
M1 исправлено» → «Возвращаю в S7 для code review r2») с точным SHA `64e9bf8`.
Жёлтый вердикт расходует бюджет циклов (§4; зелёный — нет, #227), поэтому
фактическое состояние — **заход r2, блокирующих циклов израсходовано 1/4**, а
не 0/4. Действую по фактическому состоянию, а не по (неверной) вводной:
дальше в этом документе — разбор по дельте (PROCESS.md §2.9), с разделами
«Закрытие раунда r1» и «Унаследовано из r1». SHA предыдущего вердикта назван
в самом комментарии (`ce18012f`, подтверждён и в шапке
`CODE-REVIEW-314-r1.md`) — находки тут нет.
## Скоуп разбора
1. Вердикт r1 и его SHA найдены в issue (см. выше): жёлтый, `ce18012f`,
Medium M1 — не хватает обязательного по ТЗ (AC8) исполняемого теста на
плотной обезличенной fixture с прогоном `npm run invariants`.
2. Дельта: `git diff ce18012f..64e9bf8f` — 8 файлов, +452/-20:
`demo/fixtures/v8-draft-regression.mjs` (новый, 86 строк), правки
`demo/smoke_v8_draft_write.mjs` (+83/-3), `docs/TESTING.md` (+7),
`docs/specs/314-v8-draft-write-regression.md` (+7, AC8), плюс перегенерированные
`dist/houseplan-card.js` и `custom_components/houseplan/frontend/houseplan-card.js`
(байт-в-байт синхронизированы между собой и с текущим `npm run build`,
проверено `sha256sum` — совпадают), `docs/images/screenshots.json`
(только `sourceFingerprint`, все 10 `imageSha256` не изменились), и
собственно `docs/reviews/CODE-REVIEW-314-r1.md` (публикация предыдущего
ревью — не продуктовый код).
3. Дельта не трогает ни одного файла `src/**/*.ts`, ни
`custom_components/houseplan/*.py`, ни `tests_backend/**` — она
полностью в тестовом/фикстурном/документационном слое. Это подтверждено
`git diff ce18012f..64e9bf8f --stat` (список выше) и независимо
`node scripts/smoke-select.mjs --base ce18012f --head 64e9bf8f`, который
прямо сообщил: «Исполняемого frontend-диффа нет (src/**/*.ts не тронут)».
4. Дельта локальна: не ребейз на ушедший вперёд `dev` (это тот же топ ветки,
`origin/dev` не двигался между раундами — `git merge-base` совпадает),
не меняет контракт поведения (контракт §5–§6 спеки не правился, только
добавлен абзац AC8 с указанием, где лежит доказательство), не задевает
новую подсистему. Полный повторный разбор не требуется: проверяется
закрытие M1 и всё, до чего дельта дотягивается — а именно AC8 целиком (обе
его половины: «не удаляет» и «не плодит новые violations») и общие
гейты, которые дёшевы и гоняются каждый раунд независимо от размера
дельты (§2.10).
## Как проверялось
### Дешёвые гейты — прогнаны заново на актуальном HEAD
| Команда | Результат |
|---|---|
| `npx tsc --noEmit` | зелёный, без вывода |
| `npm test` | 1336 passed, 1 skipped, 0 failed — совпадает с числом из r1 |
| `npm run build` | успешно; `sha256sum dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` — идентичные хеши |
| `npm run bundle:sync` | зелёный, `git status --porcelain` после — пусто (бандл уже был синхронизирован автором) |
| `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» — обязателен, полный `origin/dev...HEAD` трогает `src/**` (правки унаследованы из r1) |
### Смок AC8 — прогнан лично, не со слов автора
`node demo/smoke_v8_draft_write.mjs` → **16/16 checks, OK** (~11 c с учётом
запуска браузера и двух реальных прогонов `npm run invariants` внутри). Пять
новых проверок из дельты все `true`:
`denseFixtureHasOwnerReportShape`, `denseFixtureRoomSurvivesReload`,
`denseFixturePreservesIndependentGeometry`,
`denseFixtureKnownDebtIsExplicit`, `denseFixtureAddsNoInvariantViolations`.
Отдельно проверил вручную, что CLI действительно вычисляет то, что смок
утверждает, а не что смок просто доверяет своей же обёртке:
```
npm run invariants -- --config <fixture as JSON> --json
```
на выходе ровно один violation — `hidden_obstacles/unusable_draft`,
`owner: "space-314-dense:draft-314-known-debt"` — байт-в-байт то, что
проверяет `denseFixtureKnownDebtIsExplicit`. Это реальный вызов канонического
`scripts/model-invariants.mjs` (`spawnSync('npm', ['run', 'invariants', ...])`
в самом смоке — не мок, не заглушка), совпадает с текстом AC10 п.15 и
формулировкой M1 «нужен прогон `npm run invariants` до/после».
Тест умеет падать не декларативно, а по конструкции: `denseFixtureAddsNoInvariantViolations`
сравнивает строгое JSON-равенство сигнатур находок до/после через
`checkAll()` (`demo/serve.mjs`), которая при любом `false` выставляет
`process.exitCode = 1` — появление хотя бы одной новой находки после
закрытия комнаты немедленно красит смок.
`smoke-select.mjs` по дельте `ce18012f..64e9bf8f` не выбрал ни одного
дополнительного браузерного смока (выбирать нечего — дельта не в `src/**`),
что корректно: сама дельта — это и есть новый/расширенный смок, его я и
прогнал напрямую.
### Мутационные гейты #314 — оба перепроверены
```
node scripts/mutation-gate.mjs --id=v8-draft-sanitation-shifts-segment-identity
→ поймано 1 из 1
node scripts/mutation-gate.mjs --id=v8-rejected-physical-write-keeps-optimistic-draft
→ поймано 1 из 1
```
Второй мутант гоняет «чистый прогон»
`npm run bundle:sync && node demo/smoke_v8_draft_write.mjs` — значит baseline
уже включает пять новых dense-fixture проверок и остаётся зелёным на чистом
коде, а мутация ловится (тест краснеет) с грязным — обе половины дисциплины
подтверждены.
### Бэкенд, инварианты проекта, golden — унаследованы из r1 без повторного прогона
Дельта не трогает `custom_components/houseplan/*.py`, `tests_backend/**`,
`src/**` — доказательства r1 для AC1–AC7, AC9 не могли быть задеты этой
правкой. `npm run invariants -- --config <export>` на отдельном экспорте
владельца не прогонялся отдельно — предмет именно этой правки уже покрыт
реальным вызовом того же CLI внутри `smoke_v8_draft_write.mjs` (см. выше).
`golden:verify` не прогонялся повторно: `imageSha256` во всех 10 сценариях
`screenshots.json` не изменились между r1 и r2 (сверено `git diff` по файлу),
предмета для повторной проверки нет.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **M1** — ТЗ требует для AC8 исполняемый тест на минимизированной обезличенной fixture формы отчёта владельца (13 комнат/44 wall_segments/24 partitions/1 unusable draft) с прогоном `npm run invariants` до/после; в дифе r1 такого теста не было, только generic-проверка на одном объекте | Добавлена fixture `demo/fixtures/v8-draft-regression.mjs`, воспроизводящая ровно заявленную форму (13/44/24/1, проверено `wallSegments.length !== 44` guard внутри самой fixture и явными счётчиками в новых assertion). `smoke_v8_draft_write.mjs` загружает её, рисует и закрывает новую комнату реальным editor-путём, дважды по-настоящему вызывает `npm run invariants -- --config <snapshot> --json` (до/после) и сравнивает сигнатуры находок строгим равенством | `demo/fixtures/v8-draft-regression.mjs` (весь файл); `demo/smoke_v8_draft_write.mjs` строки с `runInvariants`/`denseFixtureKnownDebtIsExplicit`/`denseFixtureAddsNoInvariantViolations`; лично воспроизведено прогоном смока (16/16) и отдельным ручным вызовом CLI на той же fixture (см. «Как проверялось») |
Единственная Medium-находка r1 закрыта буквальным исполняемым тестом, а не
формальной отпиской: я независимо перезапустил и смок, и сам CLI на той же
fixture и получил тот же результат, который проверяет assertion.
## Унаследовано из r1
Без повторной проверки в r2 принято по документу
`docs/reviews/CODE-REVIEW-314-r1.md` (SHA вердикта `ce18012fb9f3c2addc957f19360a1beb9dbf96f7`) —
дельта r1→r2 не трогает ни один из затронутых файлов:
- **AC1** (ID переживают sanitation) — `test/wall-segment-model.test.mjs` +
backend schema fixture, прогнаны в r1, код `_dropLegacySegments()` не
менялся.
- **AC2** (Undo сохраняет lineage) — `smoke_v8_draft_write.mjs:successfulWriteHasStableIds`,
прогнан повторно в составе моего перезапуска смока (16/16), логика `_undoPoint()` не менялась.
- **AC3** (валидные independent writes не stale) — backend parametrized
tests, прогнаны в r1 (`test_current_v8_independent_geometry_does_not_require_contour_catalog_change`
и др.), `validation.py` дельтой не тронут.
- **AC4** (настоящий stale writer остаётся закрыт) — backend negative tests,
прогнаны в r1 (13/13 в `test_wall_segment_model.py`), файл не менялся.
- **AC5** (комната сохраняется end-to-end) — `smoke_v8_draft_write.mjs`
основной сценарий, лично подтверждено падение на pre-fix `dev@98a0a24` в
r1 (worktree-эксперимент), продуктовый код `_writeConfig()` не менялся.
- **AC6/AC7** (rollback rejected write, серийная очередь F1/F2) —
`rejectionRollsBackSynchronously`/`successQueueRetainsF2` и построчный разбор
`_rollbackRejectedPhysicalWrites()`/`_pendingPhysicalWrites` в r1, код не
менялся.
- **AC9** (совместимость/View/touch) — `imageSha256` не изменились (проверено
и в r2), backend compatibility tests из r1 не затронуты дельтой.
- **AC10** (документация и локальный гейт) — RU/EN changelog обновлены в
коммите `2347e8df` (`User-Visible: yes`) ещё в r1; в r2 дополнительно
обновлён `docs/TESTING.md` (сам предмет этого раунда) — см. выше.
- **Трейлеры и changelog** — `2347e8df` (`User-Visible: yes`) правит оба
changelog в том же коммите, ‑ подтверждено в r1 и не переоткрывалось. Новый
коммит раунда `64e9bf8f` — `User-Visible: no`, поведения не меняет
(тест/фикстура/документация), changelog ему не нужен — проверил трейлеры
напрямую (`git show -s --format=%B 64e9bf8f`).
## Находки
Ни одной High или Medium-находки не обнаружено. Одно наблюдение без статуса
находки:
Мой прогон `npm test` дал **1336 passed, 1 skipped** (дважды подряд,
детерминированно) — тем же числом, что и в r1. Хендофф-комментарий автора
после исправления M1 заявляет «1335 passed, 2 skipped». Разница — не
регрессия: оба пропускаемых теста в проекте условны и зависят от окружения
(`test/process-gate.test.mjs` — доступность `git`/`gh`-стаба;
`test/resize-optimize.test.mjs` — наличие приватной fixture #281), значит
дополнительный skip у автора — вопрос локального окружения, а не кода. Не
завожу как Low: 0 failed в обоих случаях, число и причина расхождения
объяснимы существующими условными `t.skip()` в этих же двух файлах, которые
дельтой не тронуты.
## Проверка AC — сводно (только то, что задела дельта)
| AC | Доказательство | Статус |
|---|---|---|
| AC8 (данные владельца не чистятся молча, включая «не плодит новые invariant violations» на реалистичной плотности) | `demo/fixtures/v8-draft-regression.mjs` + `smoke_v8_draft_write.mjs` (`denseFixture*` группа, 5/5 checks); лично перезапущен смок и отдельно CLI `npm run invariants` на той же fixture | **подтверждён полностью** (в r1 была подтверждена только первая половина чтением, вторая — предмет M1) |
Остальные девять AC — «Унаследовано из r1» выше.
## Что проверено чтением, не исполнением
- `demo/fixtures/v8-draft-regression.mjs`: корректность формы (13 комнат ×
4 стены с одним разрезанным верхним ребром у первых четырёх = 44
catalogue walls) проверена подсчётом по коду генератора и внутренним
guard'ом `if (wallSegments.length !== 44) throw`, который сам себя
проверяет при каждом запуске смока — не только чтением, но и исполнением
(смок прогнан).
- Диагональные `partitions` в fixture (`b: point(x + 8, y + 2)`, не
axis-aligned) — прочитано и подтверждено исполнением: реальный вызов
`npm run invariants` на этой fixture не даёт никаких находок про них
(единственная находка — заранее известный `unusable_draft`), значит для
целей этого теста (fake-WS смок, не backend-schema round-trip) форма
геометрична допустима.
## Чего не проверял
- `tests_backend/**`, `python -m pytest` — не прогонял повторно: дельта
r1→r2 не касается `custom_components/houseplan/*.py` ни одной строкой;
результат r1 (206 passed, 1 skipped, 1 не связанный с #314 fail из-за
отсутствия `pytest-asyncio` в песочнице) наследуется без изменений.
- `npm run invariants -- --config <реальный экспорт владельца>` отдельной
командой — не запускал: сырой экспорт владельца недоступен в этой сессии
и не должен коммититься (см. спека §3); тот же CLI на той же по форме
синтетической fixture уже реально исполнен дважды (мной и внутри смока).
- `npm run golden:verify` — не прогонялся: `imageSha256` неизменны между r1
и r2, предмета для проверки нет (AC9 явно требует «не меняют визуальный
результат»).
- Полный набор `demo/smoke_*.mjs` — не прогонялся: дельта не в `src/**`,
`smoke-select.mjs` по дельте `ce18012f..64e9bf8f` не выбрал ни одного
дополнительного файла; смок, который и есть предмет этого раунда, прогнан
напрямую.
- `tests_backend/test_ha_*.py` — недоступны без
`pytest-homeassistant-custom-component`, канон для них — Linux CI;
дельта их не касается.
- Performance-профили — не названы в AC, дельта не в горячем пути рендера
(только тестовый код).
## Итог
High: 0. Medium: 0. Единственная Medium-находка r1 (M1 — отсутствие
обязательного по ТЗ исполняемого теста AC8 на плотной fixture с реальным
прогоном `npm run invariants`) закрыта буквально: новая fixture нужной формы,
расширенный смок, лично перезапущенный и подтверждённый независимым ручным
вызовом того же CLI. Все десять AC подтверждены (девять унаследованы из
зелёной части r1, AC8 переподтверждён полностью в этом раунде). Дешёвые
гейты (`tsc`, `test`, `build`+bundle-sync, `check-docs`) зелёные на
актуальном HEAD, оба мутанта #314 пойманы.
Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0