22 KiB
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 — доказательства
- Остриё (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красит именно эту ветку — перепроверено исполнением. Доказано автотестом, умеющим падать. - Зубец срезан (AC2).
pairButtEndTrimWedges(src/wall-thickness.ts:1151) строит клин полуплоскостью грани партнёра;physicalBodyPartsвычитает его до разрезов проёмов. Юнитissue 310 the butt-end trim is addressed...пробует точку в старой зоне зубца черезphysicalBodySet(...).geometry— она пуста. Мутантbutt-end-trim-disabledкрасный — перепроверено. Goldenjunction-309-spike-darkпересчитан и проходитgolden:verify. Доказано. - Адресность (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красный — перепроверено. Доказано. - Узлы ≥3 не тронуты (AC4).
golden:verify— 129/129, включаяjunction-309-step-dark/junction-309-hump-darkи 16 старыхjunction-*-сцен + owner-repro байтово прежние (самrun.mjsсравнивает хэши; расхождение = fail). Доказано golden-гейтом. - Без дыр (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. Доказано. - 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возвращает[]для равных перпендикулярных стен. Доказано. - Мутанты (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упал (новые пиксели не совпали со старым эталоном) либо когда эталонов не было вовсе. Jobgoldenв прогоне32886626656завершился✓(успех) — артефакт для ревью просто не создавался. - Прогон на самой ветке
issue/310-pair-apex, где эталон реально мог не совпасть (SHAde0867b0, коммит с кодом трима, до принятия эталона), не существует: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).