Files
houseplan-card/docs/reviews/CODE-REVIEW-302-r2.md
T
2026-08-25 17:34:20 +03:00

247 lines
23 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-302-r2
Issue: #302 · Этап: code · Заход: r2 · блокирующих циклов израсходовано 1/4 до
этого захода
## Скоуп раунда
r1 (SHA `fa1112e7`) закончился жёлтым: High 0, Medium 2 (M1 — доку
`docs/WALL-THICKNESS.md` не переписали под решение №5; M2 — гвард мутанта
`junction-fans-disabled` инфраструктурно сломан, «поймано» не было проверено
исполнением). Автор хендоффнул исправление на вершине `a5ea578`
(комментарий issue 2026-08-25T14:00:05Z), затем конвейер перед ревью
ребейзнул ветку на свежий `dev` — вершина стала `42e02396`.
**Проверка ребейза (§7.2).** `git diff a5ea578..42e02396 --stat` даёт ровно
один файл: `docs/reviews/SPEC-REVIEW-304-r1.md` (+128, чужой issue). Дерево
кода/тестов/доки #302 идентично байт-в-байт. Это не «другой код» — единственный
привнесённый ребейзом файл не пересекается ни с одним путём этой задачи и не
проходит фильтр `frontend`-триггера CI (подтверждено логом job `changes`,
CI-прогон 32856428594). Разбор веду **по дельте** r1→r2: `git diff
fa1112e7..HEAD` — четыре файла по существу (`docs/WALL-THICKNESS.md`,
`scripts/mutation-gate.mjs`, `src/wall-thickness.ts`, плюс пересборка бандла и
`docs/images/screenshots.json`), plus два review-документа. Расширил разбор за
пределы формальной дельты в одном месте (см. находку M3) — при чтении
`src/wall-thickness.ts` полностью для оценки M2/Low вокруг него нашёл
расхождение AC6 с реализацией; код этого места не менялся между r1 и r2, но
находка целиком в скоупе задачи, а не в скоупе только этой правки, поэтому
привожу её отдельно, а не молчу.
## Как проверялось
Гейты «всегда» (исполнением, на HEAD `42e02396`):
| гейт | команда | результат |
|---|---|---|
| типы | `npx tsc --noEmit` | чисто |
| юниты | `npm test` | 1303 pass / 1 skip / 0 fail |
| сборка | `npm run build` + `diff dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | rollup ok; `git status` после билда чист (свежий `dist` побайтно совпал с закоммиченным) — три копии (пересобранная, `dist/`, `custom_components/`) идентичны |
| доки | `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» |
| инварианты | `npm run invariants -- --config test/fixtures/302-junction-artifacts.json` | «ссылки разрешимы, записи толщины находятся» |
Мутанты, названные M2/Low (исполнением, штатным харнессом
`scripts/mutation-gate.mjs --id=...`):
| id | результат |
|---|---|
| `junction-fans-disabled` | поймано 1 из 1 |
| `junction-fan-ignores-thick-length` | поймано 1 из 1 |
| `junction-reflex-outer-mitre-missing` | поймано 1 из 1 |
| `junction-fan-limit-back-to-249` | поймано 1 из 1 |
| `junction-detector-blind` | поймано 1 из 1 |
| `junction-pieces-unbounded` | поймано 1 из 1 |
| `partition-merge-ignores-junction` | поймано 1 из 1 |
| `junction-checks-room-vertices-only` | поймано 1 из 1 |
`junction-supports-not-restored` и `multi-wall-paper-full-origin-cut` из
реестра удалены этим диффом — проверено, что их find-паттерны (обращения к
удалённым `paperWithNodeCorners`/`[...corners.supports, ...corners.fans]`)
больше не существуют в файле: мутанты стали неприменимы к текущему коду, снятие
не притворное.
Смоки/golden: `smoke-select.mjs` для дельты `fa1112e7..HEAD` даёт прямые
совпадения `smoke_junction_holes`, `smoke_decor`, `smoke_grid_scale_invariance`,
`smoke_real_plan_masonry`, `smoke_space_scale_defaults` и пять
«зарегистрированных связей» (`junction_patch_resilience`, `multiwall_junction`,
`multiwall_strip_containment`, `resize_pointer_real_plan`,
`resize_wall_thickness`). Локально браузерные проверки (`golden:verify`,
`demo/smoke_*.mjs`) **не выполнились**: `page.route` не перехватывает
динамический `import()` в этом окружении — `Failed to fetch dynamically
imported module: http://demo.local/assets/houseplan-card.js`. Проверил, что
это ограничение песочницы, а не регрессия: та же ошибка воспроизводится
байт-в-байт на чистом `origin/dev` (worktree, символическая ссылка на
`node_modules`) с давно существующим `demo/smoke_align_guides.mjs`, который
дифф не касается.
Вместо локального прогона поднял CI-прогоны по `gh run view`:
- `32855773018` (headSha `30b4bbf6`, кодово идентичен `a5ea578`/`42e02396` по
`src/**`/`test/**`/`scripts/**` — единственная разница дальше по цепочке это
фикс отпечатка скриншотов) — `frontend`, `golden`, все три шарда `smoke`,
`performance_smoke` зелёные; красным был только `docs` (протухший отпечаток
скриншотов — это и есть то, что чинит следующий коммит `42e02396`, дока
«refresh the screenshot fingerprint»);
- `32856428594` (headSha `a5ea578`) — `docs` зелёный (фингерпринт поправлен),
`frontend`/`golden`/`smoke` пропущены самим `changes`-джобом: между
`30b4bbf6` и `a5ea578` изменился только `docs/images/screenshots.json`, под
фронтенд-триггер не попадает;
- `32856829923` (headSha `42e02396`, текущий HEAD) — красный один
`process-gate` на несуществующем в раннере before-SHA (артефакт force-push
после ребейза на dev, автор описывает его же во всех трёх последних
хендоффах); `frontend`/`docs`/`provenance` зелёные, `golden`/`smoke`
пропущены — тем же основанием (после `a5ea578` изменился только чужой
`SPEC-REVIEW-304-r1.md`).
Собранные вместе, эти три прогона покрывают полный набор гейтов на
кодово-идентичном дереве, включая golden и все смоки — считаю это equivalent
доказательству «оно работает», раз локальный браузерный прогон недоступен.
## Закрытие раунда r1
| находка r1 | чем закрыта | где видно |
|---|---|---|
| **M1** — `docs/WALL-THICKNESS.md` §3 описывал отменённое решение «фаска #249 сохраняется» | Абзац переписан под решение №5 целиком: полный mitre в секторе, `bevelMultiWallBody` — только адресный трим для #271, `bevelMultiWallPaper` убрана | `docs/WALL-THICKNESS.md:185-206` (коммит `72a5992f`); прочитано и сверено построчно с текущим кодом `junctionNodeGeometry`/`bevelMultiWallBody` — соответствует |
| **M2** — гвард `junction-fans-disabled` был `node demo/smoke_junction_holes.mjs` без сборки `test-build`, падал `ERR_MODULE_NOT_FOUND` на чистом дереве | Guard переведён на `npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test --test-name-pattern="issue 302" ...`; дополнительно (второй виток, коммит `af33c49e`) сама проверка переведена с самоссылочного контракт-проб-детектора на независимый юнит формы `issue 302 a T node covers every sector with a fan or a mitre` (`test/wall-thickness.test.mjs:2930-2940`) | `scripts/mutation-gate.mjs:2174-2190`; исполнено лично — `node scripts/mutation-gate.mjs --id=junction-fans-disabled` → «поймано 1 из 1» |
| Low — `bevelMultiWallPaper` мёртвый код | Функция удалена целиком (`src/wall-thickness.ts`, было `:2955-2994`); следом измерением снята и re-union саппорт-квадов в теле (`31ba14e0`), плюс два осиротевших мутанта из реестра | `git diff fa1112e7..HEAD -- src/wall-thickness.ts` — оба блока отсутствуют; live-подтверждение — тест `issue 302 the owner repro is hole-free end to end` (`test/wall-thickness.test.mjs:3026-3054`) гоняет упрощённый пайплайн на реальной фикстуре репро и не находит дыр |
## Находки
### M3 (Medium, в скоупе задачи) — AC6 не выполнен: узловая механика не «безоперационна по difference», спека это не признаёт
**Файл:** `src/wall-thickness.ts:3535-3555` (вызов), `src/wall-thickness.ts:2856-2953`
(тело `bevelMultiWallBody`); спека `docs/specs/302-junction-node-material.md:64-73`
(§4, решение №5), `:129-131` (§8.2), `:196-198` (AC6).
**Что заявлено.** Решение владельца №5 (принято в день ребейза, зафиксировано
в спеке и в теле коммита `79e86b0a`): «фаска #249 демонтируется **целиком**...
слой `bevelMultiWallBody`/`bevelMultiWallPaper` **удаляется**». §8.2 повторяет
это как формальный контракт: «**В узловой механике нет ни одной операции
`difference`** (вычитающий слой фаски демонтирован): дыра между полосами
невозможна **по построению**». AC6 — то же самое как проверяемый критерий.
**Что в коде.** `bevelMultiWallBody` (старая реализация фаски #249, с
`difference()` на строках 2889, 2898, 2922, 2924) не удалена: она вызывается
на подмножестве узлов, отфильтрованном `needsTrim` (короткий толстый саппорт,
#271) — `src/wall-thickness.ts:3542-3554`. Это не мёртвый код: комментарий на
`fixture #197` (`test/wall-thickness.test.mjs:2236-2240`) прямо говорит, что
тест полагается на этот трим («#271 removes only the area that the old
node-wide 8H rectangles invented»), и площадь фикстуры (`closeTo(...,
124535.20808099362, ...)`) посчитана с его участием.
**Почему это не придирка к формулировке.** Весь смысл переработки #302 —
уйти от вычитающих ремонтных слоёв, потому что именно они были источником
рецидивирующих дыр (это буквально сюжет issue: 29 коммитов «point fixes»,
которые не сходились). «Дыра невозможна по построению» — это утверждение
именно про отсутствие `difference`; для узлов, где реально срабатывает
`needsTrim`, инвариант «hole-free» держится не построением, а корректностью
унаследованного вычитающего кода — той же категории кода, что и раньше ломался.
Спека и коммит основного решения (`79e86b0a`) заявляют «удаляется целиком», но
имплементация — тот же коммит! — сохраняет узкий трим и честно объясняет
зачем в комментарии кода. То есть автор знал про исключение в момент
написания контрактной формулировки, но не отразил его ни в §4/§8.2/AC6 спеки.
`docs/WALL-THICKNESS.md` (живой документ подсистемы) это исключение описывает
верно — именно потому что M1 в этом самом раунде его туда вписал; спека
(контракт, по которому пишутся AC) осталась с абсолютной, невыполненной
формулировкой.
Отдельно: ни один этап ревью до сих пор AC6 не подтверждал. Решение №5
поменяло текст спеки уже ПОСЛЕ обоих раундов SPEC-REVIEW (они зелёные на
версии «фаска #249 сохраняется» — SHA `3b19111a`); AC6 в его текущей редакции
никогда не проходил ни одного ревью. В вердикте r1 (комментарий issue
2026-08-25T13:29:36Z) явно перечислены как проверенные AC1, AC2, AC4, AC5,
AC7, AC9 — AC6 в списке демонстративно нет, то есть r1 либо не проверял его,
либо проверял и не сообщил результат.
**Воспроизведение.** `grep -n "difference(" src/wall-thickness.ts` в диапазоне
2856-2953 (`bevelMultiWallBody`) — 4 вызова; трассировка вызова от
`wallBodiesGeometry` (`:3542-3554`) до этой функции безусловна при
`trimNodes.length > 0`. `needsTrim` реально истинен минимум для узла фикстуры
#197 (по прямому указанию комментария теста, задача #271).
**Серьёзность и что чинить.** Не блокирует релиз функционально — вся
визуальная/golden матрица подтверждает, что заявленный владельцем результат
(без вырезов/рожков, полный mitre) достигнут, и трим — узкий, адресный,
дисциплинированно закомментированный код, а не регресс. Но AC — формальный
критерий приёмки этой задачи, и он не выполнен в буквальном прочтении. Дёшево
чинится без правки кода: привести спеку в соответствие с уже написанной (в
этом же раунде) правдой `docs/WALL-THICKNESS.md` — явно назвать исключение
#271 в §4.5/§8.2 и переформулировать AC6 («узловая механика аддитивна для
всех узлов, кроме адресного #271-трима, унаследованного из #249 и суженного
до вырожденного случая»), либо, если владелец сочтёт исключение
неприемлемым, вернуть задачу на технический разбор устранения самого трима.
Это решение продуктовое/архитектурное, не моё — фиксирую находку, не
предписываю какой из двух путей выбрать.
## Что проверено и корректно
- M1, M2, Low из r1 закрыты по существу, не только по заявлению — см. таблицу
выше; для M2 лично прогнал мутант и получил «поймано 1 из 1», а не поверил
тексту коммита (текст коммита `72a5992f` сам признаёт, что автор один раз
уже ошибочно заявил «проверено исполнением», не проверив).
- Удаление `bevelMultiWallPaper`/`paperWithNodeCorners`/двух мутантов —
полное, без осиротевших ссылок; подтверждено `grep` по обеим удалённым
сигнатурам и живым тестом на реальном пайплайне (`the owner repro is
hole-free end to end`).
- `docs/WALL-THICKNESS.md` после M1 фактически точен для того путя, который
описывает M3 (адресный трим назван прямо) — то есть подсистемная дока не
расходится с кодом, расходится только сама спека issue.
- Трейлеры всех четырёх коммитов дельты (`72a5992f`, `af33c49e`, `31ba14e0`,
`42e02396`) содержат `Issue: #302` и `User-Visible: no` — корректно: эти
четыре коммита не меняют видимое поведение относительно того, что уже было
выпущено с `User-Visible: yes` в `79e86b0a`/`a5d30467` (там же в том же
коммите обновлены оба CHANGELOG — проверено `git show 79e86b0a --
docs/CHANGELOG.md docs/CHANGELOG.ru.md`).
- tsc/test/build/check-docs/invariants — все чисто, гейты не помечены как
условно пройденные.
- Мутационное покрытие узловой механики (8 мутантов, включая переписанный
M2) — самопроверено исполнением, не только чтением реестра.
## Чего не проверял и почему
- **`npm run golden:verify` и `demo/smoke_*.mjs` — не выполнил локально.**
Окружение ревью не даёт Chromium перехватить динамический `import()`
(воспроизведено то же самое на чистом `origin/dev` с чужим смоком —
ограничение песочницы, не регрессия дельты). Компенсировано тремя
CI-прогонами на кодово-идентичном дереве (см. «Как проверялось»): golden
126/126 и все три шарда смоков зелёные на `30b4bbf6`; `docs`-джоб зелёный на
`a5ea578`/`42e02396` после фикса отпечатка.
- **Полный golden-набор и весь `demo/smoke_*.mjs` (не только выбранные
smoke-select) не гонял вручную** — избыточно: `smoke-select.mjs` дал узкий
список, а CI уже прогнал полный набор (все три шарда — это весь
`demo/smoke_*.mjs`, не подмножество) на этом дереве.
- **Продуктовую визуальную приёмку 16 новых junction-сцен не пересматривал
глазами** — это решение владельца (комментарии issue 2026-08-25 11:40 и
12:42), не предмет код-ревью; полагаюсь на golden pixel-diff.
- **AC3, AC8** (пиксельное совпадение непричастных сцен, перф-бюджет) —
унаследованы из r1/из CI (`performance_smoke` зелёный в `32855773018`), не
передельфрено отдельно: дельта r1→r2 не трогает рендер-путь и перф-профиль.
## Унаследовано из r1
Документ r1: `docs/reviews/CODE-REVIEW-302-r1.md` (комментарий issue
2026-08-25T13:29:36Z), проверен на SHA `fa1112e7`.
Принято без повторной проверки, так как дельта r1→r2 не касается этого кода:
- геометрия веера/mitre/bevel-хорды/рефлекса (`junctionNodeGeometry`) —
«прочитана построчно и соответствует контракту §8» (кроме уточнения по
AC6 выше — то новая находка этого раунда, не подтверждение r1);
- AC1, AC2, AC4, AC5, AC7, AC9 — сочтены выполненными и проверенными
исполнением в r1; код, который их обеспечивает, не менялся между `fa1112e7`
и `HEAD` (сверено `git diff fa1112e7..HEAD -- src/wall-thickness.ts` —
единственные правки вне M1/M2/Low перечислены в разделе «Закрытие раунда
r1» и не затрагивают mitre/bevel/reflex ветки);
- продуктовая рамка (SCOPE.md, J1/J2) и полнота DoR спеки — не пересматривал,
дельта не продуктовая;
- решение владельца №5 (полный mitre, отказ от фаски #249 как видимого
эффекта) как таковое не оспариваю — это принятое продуктовое решение;
находка M3 — про честность формальной формулировки AC6, а не про само
решение.
## Вывод
Мultimedia M1/M2/Low закрыты добросовестно и проверены исполнением, а не на
слово. Обнаружена одна новая находка в скоупе (M3): AC6 этой же задачи не
выполнен буквально, и это никогда не проходило ревью (решение №5 переписало
спеку уже после двух зелёных SPEC-REVIEW). Возвращаю жёлтым для приведения
спеки в соответствие с уже написанной верной докой подсистемы либо для
явного решения владельца по трим-исключению.