21 KiB
CODE-REVIEW-314-r2
Issue: #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) — находки тут нет.
Скоуп разбора
- Вердикт r1 и его SHA найдены в issue (см. выше): жёлтый,
ce18012f, Medium M1 — не хватает обязательного по ТЗ (AC8) исполняемого теста на плотной обезличенной fixture с прогономnpm run invariants. - Дельта:
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, все 10imageSha256не изменились), и собственноdocs/reviews/CODE-REVIEW-314-r1.md(публикация предыдущего ревью — не продуктовый код). - Дельта не трогает ни одного файла
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 не тронут)». - Дельта локальна: не ребейз на ушедший вперёд
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-fixdev@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