Files
houseplan-card/docs/reviews/CODE-REVIEW-310-r1.md
T
2026-08-25 20:47:17 +00:00

22 KiB
Raw Blame History

CODE-REVIEW-310-r1

Issue: https://github.com/Matysh/houseplan-card/issues/310 Ветка: issue/310-pair-apex @ 0e6fbfb00b81166bf3d9323fd21dfb86524cd3ed ТЗ: docs/specs/310-pair-apex.md (ревизия 2, SPEC-REVIEW-310-r2 — зелёный) Заход: r1 (первый код-ревью, документов CODE-REVIEW-310-* ранее не существовало)

1. Скоуп диффа

git diff origin/dev...HEAD:

  • src/wall-thickness.ts — новая pairButtEndTrimWedges, парная ветка linearWallJoinPatches без визуального лимита/фаски, drawWallPreviewD вычитает клин из тел сегментов перед юнионом.
  • src/physical-geometry.ts — physicalBodyParts вычитает клинья #310 из тел партиций/драфтов до cutPartitionBody (до разрезов проёмов).
  • scripts/mutation-gate.mjs — 3 новых мутанта (pair-chamfer-returns, butt-end-trim-disabled, butt-end-trim-unbounded).
  • test/wall-thickness.test.mjs — 5 новых issue 310-тестов + 2 переписанных issue 309-теста.
  • docs/WALL-THICKNESS.md, оба CHANGELOG*, docs/images/screenshots.json — документация/трейлеры.
  • demo/golden/baselines/{baselines-index.json,junction-309-spike-dark.png} (отдельный коммит 0e6fbfb0) — принятие единственной изменившейся сцены.

Соответствует заявленному скоупу ТЗ §3 (linearWallJoinPatches, новый торцевой трим и его прокладка в physicalBodyParts/превью/карте, golden junction-309-spike-dark, юниты/мутанты, docs). Продуктовый код не менялся сверх этого; данных/i18n/UX-контролов действительно не задето.

2. Как проверялось

Гейт Команда Результат
Typecheck npx tsc --noEmit чисто (пусто)
Unit npm test 1313 pass / 0 fail / 1 skip (1314 объявлено) — совпадает с хендоффом
Build + бандлы npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js собрано; cmp — совпадение байт в байт; git status после билда чист (закоммиченные копии актуальны)
Docs fingerprint node scripts/check-docs.mjs (обязателен — диф трогает src/**) Documentation checks passed (7 files, 10 external links)
Мутанты #310 node scripts/mutation-gate.mjs --id=pair-chamfer-returns
--id=butt-end-trim-disabled
--id=butt-end-trim-unbounded
все три: «тест покраснел, как обязан» — самостоятельно перепроверено, не со слов автора
Смок-выборка node scripts/smoke-select.mjs --base origin/dev --head HEAD 190 смоков в матрице; вывод — 3 «зарегистрированные связи» (см. §5)
Прогнанные смоки node demo/smoke_{junction_holes,real_plan_masonry,multiwall_junction,wall_junctions,free_walls,wall_thickness}.mjs (после npm run bundle:sync) все OK — самостоятельно перепроверено
Golden npm run golden:verify (диф меняет геометрию рендера, п.8 требует) 129/129 passed, включая обновлённую junction-309-spike-dark-dark и байтово прежние junction-309-step-dark/junction-309-hump-dark
Backend — не запускался: custom_components/**/*.py не тронут диффом
Model invariants — не запускался: диф не трогает модель/конфиг (рёбра комнат, walls, layout, marker.space, open_spans) — только вычисляемая геометрия рендера кладки, что явно оговорено в ТЗ §4
«Одно число — один источник» — неприменимо: диф не добавляет и не меняет ни одной пользовательски видимой величины (только форма кладки)

Не прогонялось и почему

  • demo/smoke_resize_pointer_real_plan.mjs, demo/smoke_resize_wall_thickness.mjs — smoke-select дал по ним «зарегистрированную связь» через closePoint, но это общий геометрический хелпер, используемый по всему wall-thickness.ts/resize-коду; диф не меняет его поведение и не трогает Resize-путь вовсе (только linearWallJoinPatches/pairButtEndTrimWedges/drawWallPreviewD). Решение — не гонять: связь по одному распространённому имени, тема смоков (Resize-указатель) не пересекается с изменением.
  • python -m pytest tests_backend -q — диф не трогает Python.
  • Полная матрица demo/smoke_*.mjs (190 файлов) и performance_smoke — не требуется по объёму задачи (§8 PROCESS.md: полные наборы — предрелизный гейт).

3. AC — доказательства

  1. Остриё (AC1). Код: linearWallJoinPatches (src/wall-thickness.ts:1310-1316) теперь строит [node,pA,hit,pB] без проверки VISUAL_MITRE_LIMIT/chamferApex. Тест linear wall joins bevel an excessive mitre... (test/wall-thickness.test.mjs:2755) обновлён на acute[0].length === 4; тест issue 310 the acute 10/20 pair keeps its full apex... проверяет апекс дальше 1.5·hMax. Мутант pair-chamfer-returns красит именно эту ветку — перепроверено исполнением. Доказано автотестом, умеющим падать.
  2. Зубец срезан (AC2). pairButtEndTrimWedges (src/wall-thickness.ts:1151) строит клин полуплоскостью грани партнёра; physicalBodyParts вычитает его до разрезов проёмов. Юнит issue 310 the butt-end trim is addressed... пробует точку в старой зоне зубца через physicalBodySet(...).geometry — она пуста. Мутант butt-end-trim-disabled красный — перепроверено. Golden junction-309-spike-dark пересчитан и проходит golden:verify. Доказано.
  3. Адресность (AC3). reach = Math.min(2 * self.halfDepth, self.length) (src/wall-thickness.ts). Тест issue 310 the butt-end trim reach is bounded... проверяет, что все вершины клина не дальше 2·halfDepth вдоль оси на почти-параллельной паре; тест «issue 310 the butt-end trim is addressed...» отдельно пробует точку в 200 units от узла — тело стены там цело. Мутант butt-end-trim-unbounded красный — перепроверено. Доказано.
  4. Узлы ≥3 не тронуты (AC4). golden:verify — 129/129, включая junction-309-step-dark/junction-309-hump-dark и 16 старых junction-*-сцен + owner-repro байтово прежние (сам run.mjs сравнивает хэши; расхождение = fail). Доказано golden-гейтом.
  5. Без дыр (AC5). Для узлов ≥3 — прежний junctionContractHoles, не тронут (не в диффе). Для пар — новый тест issue 310 pair grid contract: masonry equals strips plus patch minus wedges (test/wall-thickness.test.mjs:3274), плотная сетка 3·h с шагом h/4 на трёх конфигурациях (owner spike, square-90, near-parallel), сверка с фактическим выходом linearWallBody/linearWallJoinPatches/pairButtEndTrimWedges, near-edge зона пропускается корректно (нестабильный оракул на общей границе). Прогнан лично — ok. Доказано.
  6. 90° равных толщин (AC6). issue 309 a square corner of equal depths keeps its byte-identical mitre (не менялся, зелёный) + новый issue 310 a square pair of equal depths has no butt-end wedge — pairButtEndTrimWedges возвращает [] для равных перпендикулярных стен. Доказано.
  7. Мутанты (AC7). Все три (pair-chamfer-returns, butt-end-trim-disabled, butt-end-trim-unbounded) перепроверены лично — каждый красит целевой тест, гварды не тавтологичны. Доказано.

Раздел «§8 план тестов» ТЗ обещал дополнительно юнит «пара с коротким толстым саппортом (#271+#310)» под риском №3 — см. находку Medium ниже, этот юнит не найден в диффе.

4. Находки

High — недостоверный Baseline-Reviewed в коммите 0e6fbfb0

Коммит test: accept the #310 pair-apex baseline несёт трейлер Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32886626656, но этот прогон физически не может быть источником эталона #310:

  • gh run view 32886626656 → head_branch: issue/309-junction-visual-limit, head_sha: 6c1534f170e501806ff38b73ebfd4f82c87ab730, заголовок «test: pin the golden matrix guard to version 46 (#309)» — это чужая ветка и коммит, который на графе истории предшествует самому первому коммиту #310 (c8d56b6c): git log --oneline 6c1534f1..HEAD показывает все шесть коммитов #310 как потомков этого SHA, то есть в дереве этого прогона кода pairButtEndTrimWedges и безлимитного mitre для пар физически не существовало.
  • У этого прогона в принципе нет артефакта golden-images: gh run view показывает единственный артефакт performance-smoke. По .github/workflows/validate.yml:411-416 шаг Upload golden candidates/diffs грузит golden-images только if: failure() || has_baselines == 'false' — то есть только когда golden упал (новые пиксели не совпали со старым эталоном) либо когда эталонов не было вовсе. Job golden в прогоне 32886626656 завершился ✓ (успех) — артефакт для ревью просто не создавался.
  • Прогон на самой ветке issue/310-pair-apex, где эталон реально мог не совпасть (SHA de0867b0, коммит с кодом трима, до принятия эталона), не существует: gh api "repos/Matysh/houseplan-card/actions/runs?branch=issue/310-pair-apex" возвращает ровно три прогона (c8d56b6c, 8d41651b, 0e6fbfb0) — коммит de0867b0 был запушен в одном пуше вместе с уже принятым 0e6fbfb0, поэтому единственный Validate-прогон на этой ветке стартовал сразу с уже обновлённым эталоном и golden в нём тоже прошёл зелёным без артефакта.

Итог: ни разу не существовало прогона Linux CI, который произвёл бы кандидатные изображения #310 для ревью — то есть эталон junction-309-spike-dark.png был принят не через артефакт CI, как того требует demo/golden/README.md («Future updates must still use the golden-images artifact produced by the Linux CI job as the review set») и PROCESS.md §8/§12/§13 («эталоны golden принимаются только через npm run golden:accept -- --reviewed на полном артефакте Linux CI»), а вставленная ссылка на чужой прогон — это ровно то, что AGENTS.md §«Commits» запрещает явно: «Never invent a review link». Хук commit-msg/validate-commit-provenance.mjs здесь бессилен: validateCommitMessage (scripts/validate-commit-provenance.mjs:53-62) проверяет только, что трейлер Baseline-Reviewed не пуст, содержимое ссылки не проверяется вообще — обойти это механически легко, что и произошло.

Замечу отдельно: сам PNG, судя по всему, визуально корректен — я independently прогнал npm run golden:verify на Linux (текущее окружение) и получил 129/129 passed, то есть текущий закоммиченный эталон согласован с кодом на пиксельном уровне в среде с тем же пиннингом Chromium, что и у CI. Находка не о качестве изображения, а о недостоверности записи о том, как оно было принято — а это именно тот механизм (различие рендеринга шрифтов/окружений между локальной машиной и CI), ради которого правило существует.

Требуется: новым коммитом (не переписывая de0867b0/0e6fbfb0 — история опубликованной ветки не перезаписывается) либо (а) добыть настоящий прогон CI с реальным артефактом golden-images для кода #310 (можно — второй пуш только кода без принятого эталона, дать golden упасть, скачать артефакт, принять --from=..., скорректировать трейлер), либо (б) если это институционально не нужно повторно — как минимум исправить трейлер на честную запись происхождения с указанием, что канонический прогон отсутствовал, и это решение отдельно согласовать с владельцем, поскольку прямо противоречит зафиксированному в demo/golden/README.md контракту.

Medium (в скоупе) — обещанный тест на пересечение #271+#310 отсутствует

ТЗ (ревизия 2, зелёное ревью r2) риск №3: «Двойной трим (#271 + #310) в одном узле — интерференция; покрыть юнитом на коротком толстом саппорте пары», и §8 плана тестов повторяет: «пара с коротким толстым саппортом (#271+#310)». В test/wall-thickness.test.mjs пять тестов issue 310 (строки 3104, 3208, 3243, 3251, 3274) покрывают AC1-AC6, но ни один не соответствует этому описанию; единственный тест с «short thick support» в файле — issue 302 a mitre past the short thick support degrades to a bevel (test/wall-thickness.test.mjs:2976), он про ≥3-лучевой узел (#302/#271) и не про пару.

Структурно риск может быть неопасен: pairButtEndTrimWedges работает только над [...draftSegments, ...partitionSegments] в physicalBodyParts (src/physical-geometry.ts:267-268), то есть только над независимыми партициями/драфтами, а buildMultiWallNodeMap/bevelMultiWallBody (#271) — над структурной картой узлов, которая считает лучи иначе и per WALL-THICKNESS.md относится к узлам ≥3 канонических лучей. Формально это разные тела и разные проходы, так что дословная «интерференция» в одном узле может быть невозможна. Но ТЗ прошло зелёное ревью именно с этим риском и этим пунктом плана, и раз обещанный тест не написан, отсутствие интерференции не доказано, а предположено — то самое расхождение, которое код-ревью обязано поймать. Фикс — либо написать юнит по описанию плана (партиция с коротким плечом у узла, где тело идёт в #271-путь и одновременно участвует в паре), либо явно снять пункт из риска/плана тестов с обоснованием несовместимости путей, задокументированным в ТЗ или в WALL-THICKNESS.md.

5. Что проверено и корректно

  • Полный mitre для пар — без визуального лимита, скол убран из этой ветки, фаска #309 осталась только в узлах ≥3 лучей (VISUAL_MITRE_LIMIT используется в диффе только там, где раньше — код проверен построчно).
  • pairButtEndTrimWedges: фильтрация вырожденных сегментов, T-узлы (pointOnSegmentInterior) корректно исключаются из парной ветки (иначе трёхлучевой узел получил бы парную трактовку), клин строится Сазерленд-Ходжменом по полуплоскости грани партнёра — реализация читаема и соответствует контракту §2.2 ТЗ.
  • Порядок операций в physicalBodyParts: трим применяется до cutPartitionBody (до вырезания проёмов), как и требует ТЗ.
  • drawWallPreviewD индексно совместим с segments — тримит тела до фильтрации null (комментарий в диффе объясняет почему), не путает индексы.
  • Не в скоупе (по ТЗ) — снап-оверлей _drawPreviewJoinPatchD (src/houseplan-card.ts:19944) делит linearWallJoinPatches, но не применяет pairButtEndTrimWedges к ранее сохранённым телам; ТЗ §3 явно относит «снап»/превью-слушатели к не-скоупу («форма кладки, слушателей нет»), так что это не находка.
  • Трейлеры de0867b0: Issue: #310, User-Visible: yes, оба CHANGELOG правятся в этом же коммите — соответствует правилу.
  • Трейлеры 0e6fbfb0: Issue: #310, User-Visible: no, Release: v1.68.0-beta.1 — формально на месте (кроме содержания Baseline-Reviewed, см. High).
  • Дублирование subtractWedgeFromBody (physical-geometry.ts) и clipBodyByWedge (wall-thickness.ts) — почти одинаковая функция «разность многоугольников, взять кольцо большей площади» в двух файлах. Не блокирует (Low, простительно: разные модули/типы входа), но стоило бы вынести в общий хелпер при следующей правке этой области — не поднимаю до Medium, оставляю на суждение автора.

6. Чего не проверял

  • Полную браузерную смок-матрицу (190 файлов) и performance_smoke — избыточно для объёма задачи, это предрелизный гейт.
  • python -m pytest tests_backend — диф не трогает Python.
  • scripts/model-invariants.mjs — диф не меняет модель/конфиг (только вычисляемую геометрию рендера), инварианты неприменимы.
  • Скриншоты документации визуально (глазами) — check-docs.mjs подтвердил отпечаток, содержательного изменения скриншотов, требующего пересъёмки Docs screenshots, диф не подразумевает (не трогает публичные экраны/тексты документации).
  • Точный побайтовый прогон golden:accept заново (не переигрывал принятие эталона — это разрушило бы уже принятое состояние; сверял только текущий результат golden:verify).