23 KiB
Code review — issue #150, cycle r1
Вердикт: красный · цикл r1/4 · High: 1 · Medium: 0
Ветка: issue/150-wall-thickness-transition-fix · implementation-коммит
1975d1a
на базе origin/dev (a05aa5d) · ТЗ:
docs/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, независимые стыки) — зелёный, все 12true;demo/smoke_wall_thickness.mjs— зелёный, все 17true;demo/smoke_opening_tunnel_fill.mjs— зелёный, все 13true;demo/smoke_draw_wall_thickness.mjs— зелёный, все 11true. Полный набор (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
{
"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:
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) явно ставит
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).