Files
houseplan-card/legacy/reviews/v1.65.0/CODE-REVIEW-150-r1.md
T
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00

297 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 — issue #150, cycle r1
Вердикт: **красный** · цикл r1/4 · High: 1 · Medium: 0
Ветка: `issue/150-wall-thickness-transition-fix` · implementation-коммит
[`1975d1a`](https://github.com/Matysh/houseplan-card/commit/1975d1ae3784a0b98cf2f489910370f37417a1c6)
на базе `origin/dev` (`a05aa5d`) · ТЗ:
[`docs/specs/150-wall-thickness-transition.md`](../../specs/150-wall-thickness-transition.md)
(r2, зелёный `SPEC-REVIEW-150-r2`, High: 0 · Medium: 1 → #171, комментарий issue
от 2026-08-18T08:40:34Z).
Примечание по трассировке: чекаут, выданный ревью по умолчанию, стоял на
`origin/issue/150-wall-thickness-transition` (спек-ветка, коммит `003a9b5`),
которая **не содержит** фикса. Продуктовый код лежит в отдельной ветке
`issue/150-wall-thickness-transition-fix` (коммит `1975d1a`), на неё явно
указывает хендофф-комментарий владельца от 2026-08-18T08:49:25Z. Весь разбор
ниже сделан по `git diff origin/dev...origin/issue/150-wall-thickness-transition-fix`.
## Скоуп проверки
Диапазон `git diff origin/dev...HEAD` (13 файлов):
- ядро — `src/wall-thickness.ts` (14 строк: `pointOnSegment`,
`exteriorBoundaryProfile`);
- тесты — `test/wall-thickness.test.mjs` (+172 строки: 3 новых теста, 2 новых
probe-хелпера через `intersection`);
- новый browser smoke — `demo/smoke_wall_thickness_transition.mjs`;
- документация — `docs/ARCHITECTURE.md`, `docs/WALL-THICKNESS.md`,
`docs/TESTING.md`, `docs/specs/README.md`, оба changelog;
- три копии бандла (`dist/`, `custom_components/houseplan/frontend/`,
`demo/srv/assets/`).
Единственный implementation-коммит `1975d1a`: трейлеры `Issue: #150` ·
`User-Visible: yes`; оба changelog обновлены в этом же коммите — требование
выполнено. Ветка называется `issue/150-…`, соответствует диапазону.
`custom_components/**/*.py`, i18n JSON и `src/types.ts` не затронуты —
согласуется с ТЗ §10/§14 (без миграции, без бэкенда, без i18n).
## Как проверялось
Работа велась в отдельном git-worktree на `origin/issue/150-wall-thickness-transition-fix`
(`node_modules` не переустанавливался — симлинк на уже установленный каталог
основного чекаута, `npm ci` не запускался).
Дешёвые гейты (всегда):
- `npx tsc --noEmit` → **зелёный**, без вывода.
- `npm test` → **831/831 green**, совпадает с числом из хендоффа.
- `npm run build` → зелёный; свежая сборка побайтно совпала со всеми тремя
закоммиченными копиями (`sha256 74f35a36…c90f2d` — тот же хэш, что в
хендоффе).
Дисциплина «тест умеет падать» — проверено вручную, а не на слово автора:
временно откатил ровно эти 14 строк `src/wall-thickness.ts` до состояния
`origin/dev` (в рабочей копии, без коммита) и перезапустил `npm test`.
Результат: **828/831**, ровно 3 новых теста краснеют —
```
not ok 792 - production-scale Split keeps the 10 → 0 facade transition at the divider
not ok 793 - production-scale collinear transitions keep both local depths in either direction
not ok 794 - production-scale 45° facade keeps an exact unequal-thickness breakpoint
```
— все остальные 828 тестов, включая существовавшие ранее регрессии по #123/#141,
не задеты. Это подтверждает: тесты не вакуальны и действительно проверяют
именно исправленный механизм, а не что-то попутное. Файл восстановлен обратно
после проверки.
Гейты по необходимости (diff меняет каноническую геометрию рендера/света, и
под задачу заведён именной smoke, названный в AC7):
- `node demo/smoke_wall_thickness_transition.mjs` (AC7 требует
`[smoke + golden]`) — **упал**: 3 из 11 проверок `false`. Разбор — в разделе
«Находки», это блокирующий High.
- Регрессионные smoke смежных задач и поверхностей, которые тот же
`exteriorBoundaryProfile()`/`wallBodiesGeometry()` обслуживает:
`demo/smoke_split_corner_wall.mjs` (#123, corner Split facade) — **зелёный**,
все 13 проверок `true`; `demo/smoke_wall_junctions.mjs` (#141, независимые
стыки) — **зелёный**, все 12 `true`; `demo/smoke_wall_thickness.mjs` — **зелёный**,
все 17 `true`; `demo/smoke_opening_tunnel_fill.mjs` — **зелёный**, все 13
`true`; `demo/smoke_draw_wall_thickness.mjs` — **зелёный**, все 11 `true`.
Полный набор (127 файлов) не прогонялся — диф не задевает openings/сборку/
editor UI за пределами толщины стен, прогон всех непропорционален узкому
геометрическому фиксу.
- `npm run golden:verify` — **не прогонялся**. ТЗ §13 и хендофф автора прямо
относят golden к предрелизному гейту по §11.4/AGENTS.md «Gates»; новых
golden-сценариев в этом диффе нет (они по плану добавляются перед бетой).
Риск в отношении уже существующих golden-сценариев закрыт достаточно другим
способом: диф меняет только внутренний допуск в двух функциях одного файла,
используемых исключительно при коллинеарном разрыве толщины — сценарий,
которого в существующей golden-матрице не было (это и есть суть бага),
так что регресс существующих baseline-кадров маловероятен; кроме того,
зелёные regression-smoke выше покрывают ту же геометрическую функцию на
смежных сценариях.
- `python -m pytest tests_backend` — не прогонялся: Python не тронут ни одним
файлом диффа.
- performance-профили — не прогонялись: изменение — один дополнительный
`Math.sqrt`/деление на уже существующий per-edge проход, не новый
O(n²)/O(n·m) путь; ни ТЗ (AC11), ни диф не дают повода подозревать
регресс, а полный performance-прогон — предрелизный гейт.
## Находки
### [High] Собственный AC7-smoke красный при первом запуске — фикстура не включает `show_borders`, а не дефект геометрии
**Файл:** `demo/smoke_wall_thickness_transition.mjs:44-58` (инициализация
фикстуры перед `_setMode('view')`/static/iso).
**Воспроизведено запуском, не только чтением:**
```
node demo/smoke_wall_thickness_transition.mjs
```
```json
{
"splitDialog": true, "splitCommitted": true, "leftWallSelected": true,
"savedIntervals": true, "planFullDepth": true, "planZeroSideClear": true,
"paperZeroSideClear": true,
"viewMatchesPlan": false,
"lightUsesSteppedMasonry": true,
"staticMatchesPlan": false,
"hiddenIsoMatchesCanonical": false
}
FAILED (3): viewMatchesPlan, staticMatchesPlan, hiddenIsoMatchesCanonical
```
Ровно те три проверки, которые доказывают AC7 (единая геометрия Plan/View/
static/hidden Iso) — красные. Причина не в проекте `src/wall-thickness.ts`:
`_renderWallBodies()` (`src/houseplan-card.ts:10389-10392`) и симметричный код
static/iso-рендера пропускают отрисовку стен целиком, если `disp.showBorders`
ложно, а в режиме `view`/`devices` возвращают пустой `svg`:
```ts
private _renderWallBodies(disp: SpaceDisplay): TemplateResult {
if (this._renderProjection === 'iso') return svg`` as unknown as TemplateResult;
if (disp && !disp.showBorders && (this._mode === 'view' || this._mode === 'devices'))
return svg`` as unknown as TemplateResult;
…
```
Фикстура смока удаляет `walls`/`openings`/`open_spans`/`partitions`/
`room_drafts`/`wall_columns` из клона серверного конфига, но **не** выставляет
`space.settings.show_borders = true`, поэтому `showBorders` остаётся ложным
(дефолт для нового плана) и `[data-hp="wall"]` в режимах `view`/static/iso
попросту отсутствует в DOM — `viewD`/`staticD` пустые, `isoWalls` пустой.
Отдельно проверил через `page.evaluate` (`viewPathExists === false`,
`planD.length === 351`, `viewD.length === 6`) — путь для View не существует
вовсе, сравнение `viewD === planD` ложно тривиально, а не из-за расхождения
геометрии.
Прецедент того же класса задач уже решает эту проблему: соседний
регрессионный smoke `demo/smoke_split_corner_wall.mjs:54` (тоже про
Plan/View/static/iso-паритет фасада после Split, задача #123) явно ставит
```js
space.settings = { ...(space.settings || {}), show_borders: true };
```
перед переключением в View. В новом smoke этой строки нет.
**Подтверждение, что дефект — в фикстуре, а не в продукте:** взял копию
`demo/smoke_wall_thickness_transition.mjs`, добавил ровно ту же строку
(`space.settings = { ...(space.settings || {}), show_borders: true };`) сразу
после удаления полей конфига, пересобрал ничего (бандл не менялся) и
перезапустил — **все 11 проверок стали `true`**, включая
`viewMatchesPlan`/`staticMatchesPlan`/`hiddenIsoMatchesCanonical`, а
`viewD === planD` побайтно совпал. Это независимо подтверждает: сам
геометрический фикс в `src/wall-thickness.ts` работает верно и на
Plan, и на View, и на static, и на hidden Iso — реальный дефект есть только
в тесте, который должен был это доказать.
**Почему это блокирует, а не Low.** AC7 сформулировано как
`[smoke + golden]`; смок — единственное автоматическое доказательство именно
кросс-поверхностного паритета для этой задачи (golden отложен на
предрелизный гейт). Красный смок, дошедший до код-ревью, — ровно тот сценарий,
который `AGENTS.md` («Gates») явно называет дорогим («A red smoke that reaches
the review costs a cycle; run locally it costs a minute», со ссылкой на
прецедент #89) и который должен был отловиться локальным прогоном перед
переводом в `S7-code-review`. Хендофф-комментарий автора прямо говорит:
«по действующему процессу browser smoke не запускался в цикле реализации» —
но AC7 этой же задачи требует смок как доказательство, и в текущем состоянии
это доказательство отсутствует: `docs/WALL-THICKNESS.md` §8 и `docs/TESTING.md`
уже утверждают (со ссылкой на этот smoke), что паритет поверхностей
подтверждён — на актуальном коммите это не так.
**Требуется:** добавить `space.settings = { ...(space.settings || {}),
show_borders: true };` в фикстуру `demo/smoke_wall_thickness_transition.mjs`
(по образцу `smoke_split_corner_wall.mjs:54`) и приложить к следующему циклу
зелёный прогон именно этого smoke. Изменений в `src/wall-thickness.ts` для
этого не требуется — правка ограничена одним тестовым файлом.
## Что проверено и корректно
- **AC1 (data profile fixture, `coordScale = 1000`)** — юнит-тест
`production-scale Split keeps the 10 → 0 facade transition at the divider`
проверяет `top`/`divider` интервалы (`10`/`0`/`10`) именно на
production-масштабе; проходит, и я подтвердил выше, что без фикса именно
этот тест (и два соседних) красный.
- **AC2 (полное сечение 10 см = 5 наружу + 5 внутрь)** — тот же тест,
`assertProbeInside`/`assertProbeOutside` на `half*0.75` и `half+0.2` по обе
стороны centreline; корректно считает `half = wallCmToUnits(10, cellCm,
GRID_PITCH) / 2`.
- **AC3 (точная ступень `10 → 0` на конце разделителя)** — unit-часть
доказана (`assertProbeOutside(… [700, 96] …)`, `assertProbeOutside(…
[700, 104] …)`, точка перехода на `x = 500` найдена в `geometry.geom.flat(2)`
с допуском `1e-7`); golden-часть отложена на предрелизный гейт по плану ТЗ —
не проверялась (см. «Чего не проверял»).
- **AC4 (матрица `0↔10`, `10↔20`, `1↔100`, оба направления, оба winding)** —
тест `production-scale collinear transitions keep both local depths in
either direction` перебирает ровно эту матрицу плюс равные толщины,
сравнивает symmetric-difference площадей между прямым и развёрнутым
winding (`closeTo(…, 0, 1e-7)` в обе стороны) — корректно.
- **AC5 (shared divider — полная толщина, без выступа наружу)** — тот же
тест: `divider.length === 2`, `divider.every(iv => iv.cm === 10)`,
`assertProbeOutside(… divider protrudes outside …)`.
- **AC6 (paper/clean floor/area по локальным depths)** — `assertProbeOutside`/
`assertProbeInside` на `innerContourForRoom` обеих комнат и
`geometry.paperGeom` на нулевой стороне — корректно; отдельно тест проверяет
`JSON.stringify({rooms, walls})` неизменным до/после рендера (AC10).
- **AC7 (Plan/View/static/hidden Iso/свет — единая геометрия)** — **не
доказано смоком как есть** (High выше); проверено независимо мной через
исправленную копию фикстуры и regression-smoke #123/#141 — сам механизм
общей канонической геометрии (одна функция `wallBodiesGeometry`/
`exteriorBoundaryProfile` для всех потребителей, как описано в
`docs/WALL-THICKNESS.md` §2) не менялся диффом за пределами двух
проверенных функций, поэтому паритет поверхностей — тот же самый механизм,
что уже подтверждён #123/#141 smoke, плюс моя ручная проверка.
- **AC8 (openings у transition)** — прямого нового теста на opening рядом с
переходом в этом диффе нет, но `demo/smoke_opening_tunnel_fill.mjs`
(существующий регресс на ту же геометрическую функцию) зелёный; диф не
трогает association/opening-код (`OpeningWallIndex`, opening cut) —
**проверено чтением, не отдельным исполнением новой сцены**: изменённые
строки относятся только к `exteriorBoundaryProfile`/`pointOnSegment`,
которые вызываются до/независимо от opening-cut пути.
- **AC9 (регрессии #123/#141 не открылись заново)** — `demo/smoke_split_corner_wall.mjs`
и `demo/smoke_wall_junctions.mjs` зелёные целиком (см. «Как проверялось»);
соответствующие unit-тесты (`corner Split …`, `linear wall joins …`) входят
в те же 828 неизменных тестов при откате фикса.
- **AC10 (старый конфиг исправляется без миграции)** — юнит-тест явно
сравнивает `JSON.stringify({rooms, walls})` до/после рендера; diff не
добавляет новых полей в `WallEntry`/схему; подтверждено также diff'ом (нет
правок в `src/types.ts`, нет migration-кода).
- **AC11 (кеш/fingerprint, HA tick не перестраивает topology)** —
**проверено чтением, не исполнением**: диф не касается кеш-ключей/
`_cfgEpoch`/`_wallUnionCache`/structural fingerprint — только внутренняя
арифметика `pointOnSegment`/`exteriorBoundaryProfile`, вызываемых из уже
существующего кешированного прохода; поведение кеша не изменено.
- **AC12 (fail-closed при сбое boolean)** — **проверено чтением**: код вокруг
`exteriorBoundaryProfile`/`wallBodiesGeometry`, отвечающий за fallback при
`null`/ошибке union, диффом не тронут.
- **Трейлеры/changelog/бандлы** — `Issue: #150` / `User-Visible: yes` на
единственном коммите, оба changelog обновлены в нём же, три копии бандла
побайтно совпадают со свежей локальной сборкой (см. «Как проверялось»).
- **Документация** — `docs/ARCHITECTURE.md`, `docs/WALL-THICKNESS.md` §2/§8,
`docs/TESTING.md`, `docs/specs/README.md` обновлены consistently с
описанием причины (`t`/`eps` размерность) и списком потребителей; терминология
соответствует уже принятой в `docs/WALL-THICKNESS.md` (`centreline ±½`,
atomic interval, canonical masonry pass) — новых терминов не изобретено.
Единственная неточность — `WALL-THICKNESS.md` §8 и `TESTING.md` уже
утверждают зелёный кросс-поверхностный результат со ссылкой на
`demo/smoke_wall_thickness_transition.mjs`, что на данный момент не так
(см. High); текст документации менять не нужно — после починки фикстуры
утверждение станет верным без правки самой документации.
## Чего не проверял
- **`npm run golden:verify` (полный набор)** — не прогонялся; см.
обоснование в «Как проверялось». Новых golden-сценариев для #150 диф не
добавляет — они по плану ТЗ идут перед бетой.
- **Полный browser smoke-suite (127 файлов)** — не прогонялся; прогнаны
только именной AC7-smoke и 5 регрессионных smoke по тем же геометрическим
функциям (#123, #141, толщина, тоннели проёмов, рисование с толщиной).
- **`python -m pytest tests_backend`** — не прогонялся, Python не тронут.
- **Performance smoke / Full Performance** — не прогонялись; изменение —
O(1) на существующем per-edge проходе, ни ТЗ, ни диф не дают повода
подозревать регресс.
- **Ручное визуальное сравнение в браузере (не headless)** — не выполнялось;
вывод основан на unit/smoke harness и прямом воспроизведении через
скомпилированный `test-build`/собранный бандл.
- **AC8 отдельным исполняемым сценарием** (opening ровно на transition) —
разобран только чтением кода и косвенным regression-smoke; собственного
прогона новой сцены «opening на переходе» не делал.
## Итог
High: 1 (собственный AC7-smoke красный из-за фикстуры, не из-за геометрии —
описано выше, блокирует). Medium: 0.
Вердикт красный: цикл возвращается автору на однострочную правку
`demo/smoke_wall_thickness_transition.mjs` (добавить `show_borders: true` в
фикстуру, по образцу `smoke_split_corner_wall.mjs:54`) и повторный зелёный
прогон именно этого smoke. Правка `src/wall-thickness.ts` не требуется —
геометрический фикс подтверждён как корректный независимо (юнит-тесты,
способные падать без него; regression-smoke #123/#141; ручной прогон
исправленной копии AC7-smoke).