mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
275 lines
26 KiB
Markdown
275 lines
26 KiB
Markdown
# CODE-REVIEW-302-r1
|
||
|
||
Issue: [#302](https://github.com/Matysh/houseplan-card/issues/302) — «Артефакты на стыках стен: дыры в union-геометрии. Переработка механизма стыков + полноценный сет скриншот-тестов»
|
||
Спек: `docs/specs/302-junction-node-material.md` (зелёное ревью ТЗ, `SPEC-REVIEW-302-r1`/`r2`)
|
||
Ветка: `issue/302-junction-node-material`, вершина `fa1112e7` (после ребейза на `origin/dev` = `2143e888`, включает #265/#301/#303)
|
||
Заход: r1 · блокирующих циклов израсходовано 0 из 4 (этап код-ревью первый; предыдущая попытка `S7-code-review` не состоялась — упёрлась в конфликт ребейза до чтения кода, цикл не расходовался согласно комментарию владельца от 12:43)
|
||
Класс изменения: A (продукт) + B (гейты/тесты) + C (документация)
|
||
|
||
## Скоуп
|
||
|
||
Полная переработка механики материала в узлах стен (degree-3+, три и более
|
||
сходящихся луча): старая смешанная схема «аддитивные патчи + вычитающий
|
||
`bevelMultiWallBody`-бевел (#249) с protection-union» заменяется на решение
|
||
владельца №5 — **полный mitre по умолчанию** (веер `junctionNodeGeometry`,
|
||
mitre в пределах `MITRE_LIMIT`, bevel-хорда за ним, рефлексные сектора
|
||
замыкаются обратным mitre или хордой), плюс формальный детектор дыр
|
||
`junctionContractHoles` и сет из 16 крупноплановых golden-сцен + сцена-репро
|
||
владельца. Старый вычитающий слой `bevelMultiWallBody` не демонтирован
|
||
целиком, а сужен до адресного латерального трима для вырожденных
|
||
короткосаппортных узлов (#271); `bevelMultiWallPaper` из пути бумаги убран
|
||
полностью.
|
||
|
||
Диапазон: `git diff origin/dev...HEAD` — 47 файлов, ядро в
|
||
`src/wall-thickness.ts` (+351/-… строк), тесты (`test/wall-thickness.test.mjs`,
|
||
новая фикстура `test/fixtures/302-junction-artifacts.json`), новый смок
|
||
`demo/smoke_junction_holes.mjs`, обновлённый `demo/smoke_multiwall_junction.mjs`
|
||
и `demo/smoke_grid_scale_invariance.mjs`, 7 новых мутантов
|
||
`scripts/mutation-gate.mjs`, 16 новых + 2 изменённых golden-эталона,
|
||
`docs/WALL-THICKNESS.md`, оба CHANGELOG.
|
||
|
||
Это первый содержательный проход код-ревью по этой задаче (первая попытка
|
||
`S7-code-review` вернулась конвейером до чтения кода из-за конфликта
|
||
ребейза — цикл не расходован, документ по дельте не нужен), поэтому разбор
|
||
полный, а не по дельте (§2.10 не применяется).
|
||
|
||
## Как проверялось
|
||
|
||
Материал — коммиты `git log --oneline origin/dev..HEAD` (14 штук) и
|
||
`git diff origin/dev...HEAD`. Выполнено на этом дереве (SHA `fa1112e7745e0ba0`),
|
||
не на слово автора.
|
||
|
||
| Гейт | Команда | Результат |
|
||
|---|---|---|
|
||
| typecheck | `npx tsc --noEmit` | чисто |
|
||
| unit | `npm test` | 1303 pass / 1 skip / 0 fail (1304 объявлено) |
|
||
| build + сверка бандла | `npm run build && cmp dist/… custom_components/…` | идентичны; `npm run bundle:sync` не изменил рабочее дерево (git status чист) |
|
||
| docs screenshots fingerprint | `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» — обязателен, диф трогает `src/**` |
|
||
| инварианты модели (диф трогает геометрию узлов/`walls`) | `npm run invariants -- --config <репро #302, обёрнутое {space:{...}}>` | «Инварианты выполнены: ссылки разрешимы, записи толщины находятся» |
|
||
| golden | `npm run golden:verify` | **126/126 passed**, локально, целиком (совпадает с заявкой автора) |
|
||
| смоки — прямые совпадения `smoke-select.mjs --base origin/dev --head HEAD` | `smoke_junction_holes`, `smoke_real_plan_masonry`, `smoke_grid_scale_invariance` | все OK |
|
||
| смоки — AC9 плюс «зарегистрированная связь» | `smoke_multiwall_junction`, `smoke_wall_junctions`, `smoke_junction_patch_resilience`, `smoke_render_perf` | все OK |
|
||
| мутанты #302 (выборочно, `--id=`) | `junction-fan-limit-back-to-249`, `junction-detector-blind`, `junction-pieces-unbounded`, `junction-supports-not-restored` | 4/4 корректно ловят поломку («поймано 1 из 1» / «поймано 0 из 1» для guard без мутации, как обязано) |
|
||
| мутант `junction-fans-disabled` | `node scripts/mutation-gate.mjs --id=junction-fans-disabled` | **упал на чистом прогоне** — см. находку M1 |
|
||
| process-gate офлайн | `node scripts/process-gate.mjs` | «гейт пройден, предупреждений 0» |
|
||
| CI на вершине | `gh run view` на прогонах, указанных автором | подтверждено см. ниже |
|
||
|
||
Не прогонялось и почему:
|
||
- `python -m pytest tests_backend` — диф не трогает `custom_components/**/*.py` (не нужен по правилу гейтов);
|
||
- полный `demo/smoke_*.mjs` (189 файлов) — задача не «задевает всё», выборка
|
||
дана `smoke-select.mjs` (5 прямых + 2 «зарегистрированная связь») плюс AC9;
|
||
расширил её `smoke_render_perf` (AC8) вручную;
|
||
- `npm run mutants` целиком (все ~90 мутантов проекта) — предрелизный гейт
|
||
(§8), не гейт ревью; из новых семи прогнал четыре показательных плюс
|
||
специально диагностировал сломанный;
|
||
- полный performance-бенчмарк (`npm run benchmark:*`) — дорогой,
|
||
предрелизный; вместо повторного прогона проверил, что CI job
|
||
`performance_smoke` реально выполнился и был зелёным на `b95c55d3` (см. ниже),
|
||
а не «reuse»-пропущен.
|
||
|
||
**CI, названный автором.** Прогон [32850091121](https://github.com/Matysh/houseplan-card/actions/runs/32850091121)
|
||
на `b95c55d3` (родитель финального docs-коммита): `frontend`, `golden`,
|
||
`performance_smoke`, `smoke` (все три шарда), `docs`, `provenance` —
|
||
`success`; `process-gate` — `failure` (ожидаемо: before-SHA от форс-пуша,
|
||
самовылечился следующим пушем, как и описал автор). Прогон
|
||
[32850748475](https://github.com/Matysh/houseplan-card/actions/runs/32850748475)
|
||
на финальном `fa1112e7` — `success` целиком; тяжёлые job'ы там `skipped`
|
||
через легитимный `reuse` (доки — единственный дифф этого коммита, не
|
||
затрагивающий их отпечаток). Вместе оба прогона покрывают полный набор на
|
||
проверяемом дереве, как и заявил автор.
|
||
|
||
## AC — разбор
|
||
|
||
| AC | Статус | Доказательство |
|
||
|---|---|---|
|
||
| AC1 (детектор: 0 дыр на всём сете) | ✅ | golden verify 126/126 включает все 16 сцен + репро; `smoke_junction_holes` (прямое исполнение) `noContractHoles: true`; юнит-тест «the owner repro is hole-free end to end» зелёный |
|
||
| AC2 (репро владельца: 0 дыр) | ✅ | тот же смок + golden `junction-owner-repro-dark` passed |
|
||
| AC3 (несвязанные сцены — побайтно; junction-сцены — легально изменены) | ✅ | `baselines-index.json`: только 16 новых + 2 изменённых (`safe-resize-handles-clamp-*`, названо автором как узловая вершина ромба) хэша; остальные 108 не тронуты — подтверждено чтением индекса |
|
||
| AC4 (57°, 50/70 — сплошная кладка) | ✅ | юнит «the 57° mixed-thickness pair takes the full mitre (decision #5)» проверяет ровно эту геометрию (углы 45°/102.3°/332.2°, half 4.861/3.472) |
|
||
| AC5 (виртуальный луч не порождает кладку) | ✅ (чтением) | `junctionNodeGeometry`: `rays = node.rays.filter(ray.halfDepth > 0)` — нулевой/виртуальный луч исключён из веерного обхода до сортировки по азимуту, соседи по азимуту становятся его соседями автоматически; golden `junction-t-virtual-arm-dark`/`junction-x-virtual-through-dark` в сете |
|
||
| AC6 (узловая механика не содержит `difference`) | ⚠️ частично — см. находку M1/M2 | Буквально неверно: `bevelMultiWallBody` (внутри — `difference`) по-прежнему вызывается для узлов с вырожденно-коротким толстым саппортом (`corePhase = 'multi-wall-trim'`, `wall-thickness.ts:3600-3613`). Регресс-мутант, названный в спеке для этого AC («лимит веера 1.25·h»), проверен и ловится (`junction-fan-limit-back-to-249`). Мутант, названный в спеке для «веера не строятся вовсе», сломан на инфраструктурном уровне — см. M2 |
|
||
| AC7 (оба рендерера — один вызов) | ✅ (чтением) | `wallBodiesUnionPath` (единственная точка, `wall-thickness.ts:3714`) вызывается и из `houseplan-card.ts:11692`, и из `space-render.ts:432`; архитектура не менялась, только тело функции |
|
||
| AC8 (перф в бюджете) | ✅ с оговоркой | `smoke_render_perf` OK; CI `performance_smoke` реально выполнился и зелёный на `b95c55d3` (не reuse-пропуск, проверено по списку job'ов). Явного числового «замера large-house» в хендоффе нет, спека требовала это отдельной строкой — не блокирует (Low, механизм проверки существует и сработал) |
|
||
| AC9 (существующие смоки стыков зелёные) | ✅ | `wall_junctions`, `junction_patch_resilience`, `multiwall_junction`, `real_plan_masonry` — все прогнаны лично, OK |
|
||
|
||
## Находки
|
||
|
||
### Medium (в скоупе) — M1: `docs/WALL-THICKNESS.md` не переписан под решение №5, хотя спека требовала это явно
|
||
|
||
Файл: `docs/WALL-THICKNESS.md:188-196`.
|
||
|
||
Абзац «**Junction nodes (#302).**» по-прежнему гласит: *«A degree-3+ node
|
||
keeps the approved #249 chamfer (`bevelMultiWallBody`, bounded by the node's
|
||
join limit `MULTI_WALL_JOIN_LIMIT × halfDepth`), and AFTER it the node
|
||
additively gets back what a chamfer must never eat…»* — это описание
|
||
**отменённого** утреннего решения «сохранить фаску #249», а не итогового
|
||
решения №5 («узлы смыкаются полным mitre… фаска #249 демонтируется целиком»,
|
||
`docs/specs/302-junction-node-material.md:65-73`). Фактическое поведение (по
|
||
чтению `wall-thickness.ts:3591-3634`) обратное описанному в доке: фаска
|
||
(`bevelMultiWallBody`) теперь **опциональна и включается только** для узлов
|
||
с вырожденно-коротким толстым саппортом (`needsTrim`), а не «для каждого
|
||
degree-3+ узла» как написано; для всех остальных узлов работает чистый
|
||
аддитивный веер, что и есть содержательное отличие этой задачи.
|
||
|
||
Коммит `e8338663` («feat: full mitre at every node — the #249 chamfer
|
||
retires», `User-Visible: yes`, решение №5) не тронул `docs/WALL-THICKNESS.md`
|
||
вовсе (`git show --stat e8338663` не содержит этот файл). Единственный
|
||
последующий докс-коммит `fa1112e7` добавил только раздел «Junction tooling»
|
||
(список тестов), не исправив основной абзац.
|
||
|
||
Нарушает:
|
||
- `PROCESS.md` правило 11: «Документация — в том же коммите, что поведение.
|
||
Отдельным «допишу потом» коммитом документация не бывает» — коммит,
|
||
сменивший контракт (`e8338663`, User-Visible: yes), не обновил канонический
|
||
документ подсистемы;
|
||
- собственное требование спеки, §15: «`docs/WALL-THICKNESS.md` §3 и §9
|
||
переписываются под новую механику» — раздел §9 (Independent partitions)
|
||
тоже не упоминает #302 вовсе, что ожидаемо (узел независимых партиций не
|
||
трогается), но §3 обязан был обновиться и не обновился.
|
||
|
||
Почему это находка, а не педантизм: этот файл — канонический документ
|
||
подсистемы, который явно предписано читать перед любой работой над стенами
|
||
(`AGENTS.md`, сам этот ревью начиналось с его чтения). Ложное описание
|
||
«узел всегда получает старую фаску» уведёт следующего агента/автора по
|
||
неверному следу при следующей задаче на стыках.
|
||
|
||
Воспроизведение: `git diff origin/dev...HEAD -- docs/WALL-THICKNESS.md` —
|
||
всего 24 добавленные строки, из которых 15 — новый раздел про тестирование;
|
||
абзац §3 про механику узла не менялся с коммита `9e68d641` (12:33), то есть
|
||
раньше решения №5 (15:13).
|
||
|
||
Фикс: переписать абзац `docs/WALL-THICKNESS.md:188-196` под фактический
|
||
контракт — веер по умолчанию (mitre/bevel-хорда, рефлекс), `bevelMultiWallBody`
|
||
только как адресный трим для короткосаппортных узлов, `bevelMultiWallPaper`
|
||
убран из бумаги целиком.
|
||
|
||
### Medium (в скоупе) — M2: мутант `junction-fans-disabled` не работает, вопреки заявлению «краснота проверена исполнением»
|
||
|
||
Файл: `scripts/mutation-gate.mjs:2213-2225`.
|
||
|
||
```
|
||
guard: 'node demo/smoke_junction_holes.mjs',
|
||
```
|
||
|
||
`demo/smoke_junction_holes.mjs` — единственный смок в проекте, импортирующий
|
||
напрямую из `../test-build/wall-thickness.js` (`grep -rl "from '../test-build/"
|
||
demo/smoke_*.mjs` находит только его), поэтому его guard обязан собрать
|
||
`test-build/` первым (как это делают все соседние #302-мутанты:
|
||
`npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && …`).
|
||
У этого мутанта такого префикса нет.
|
||
|
||
Харнесс сам решает, нужен ли `test-build`, эвристикой
|
||
`guardNeedsTestBuild()` (`scripts/mutation-gate.mjs:2920-2922`):
|
||
`/(^|[\s&|;])node --test\b/.test(guard) && !guard.includes('tsconfig.test.json')`
|
||
— она матчит только guard'ы вида `node --test …`, а не «браузерный» смок,
|
||
который тем не менее тоже читает `test-build/`. Результат — воспроизведено
|
||
исполнением:
|
||
|
||
```
|
||
$ node scripts/mutation-gate.mjs --id=junction-fans-disabled
|
||
FAIL чистый прогон: node demo/smoke_junction_holes.mjs красный без мутанта
|
||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/tmp/hp-mutant-uvmoTK/test-build/wall-thickness.js' …
|
||
```
|
||
|
||
Гейт падает уже на «чистом» прогоне (до применения мутации) — это
|
||
означает, что мутант не проверял АБСОЛЮТНО ничего ни разу с момента
|
||
создания: ни в момент написания, ни в любом гипотетическом прогоне харнесса
|
||
он не мог напечатать «поймано 1 из 1». Это прямо противоречит явному
|
||
заявлению в теле коммита `e8338663`: *«мутанты переякорены, краснота каждого
|
||
проверена исполнением»* и в хендоффе issue: *«восемь мутантов — краснота
|
||
каждого проверена исполнением штатным харнесом»*.
|
||
|
||
Мутант закрывает первую строку таблицы §14 спеки — `node-fan-disabled`
|
||
(«веера не строятся вовсе» → «детектор на сете») — самый фундаментальный
|
||
регресс-класс новой механики. Сейчас его прикрывает не он, а косвенно
|
||
`junction-supports-not-restored` (другой мутант, который частично
|
||
пересекается по эффекту — тоже режет вклад аддитивных кусков — и этот
|
||
действительно ловится, «поймано 0 из 1» без мутации, «FAIL» ожидаемо на
|
||
чистом прогоне... то есть сам по себе он в порядке). Но заявленное покрытие
|
||
«веера отключены полностью» не проверено никем.
|
||
|
||
Не является блокирующим (High), потому что: (а) это дефект тестовой
|
||
инфраструктуры, а не продуктового кода — сама геометрия работает и покрыта
|
||
множеством других зелёных проверок (детектор, 126/126 golden, все AC9-смоки);
|
||
(б) `mutation-gate` — предрелизный, не блокирующий Validate гейт (§8,
|
||
`.github/workflows/mutation-gate.yml` — по расписанию/`workflow_dispatch`,
|
||
не на каждый push), поэтому CI branch protection этим не введён в заблуждение
|
||
формально. Но это Medium в скоупе: спека прямо обещала эту защиту (§14),
|
||
коммит прямо заявил, что она проверена, и оба заявления не соответствуют
|
||
действительности.
|
||
|
||
Фикс: добавить в `guard` этого мутанта тот же префикс сборки, что у соседей —
|
||
`'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node demo/smoke_junction_holes.mjs'`
|
||
— и повторно прогнать `--id=junction-fans-disabled`, чтобы подтвердить
|
||
«поймано 1 из 1» на самом деле.
|
||
|
||
### Low — L1: `bevelMultiWallPaper` осталась мёртвым кодом
|
||
|
||
Файл: `src/wall-thickness.ts:2955-2984`.
|
||
|
||
Функция `bevelMultiWallPaper` (вычитающий бевел для контура бумаги) не имеет
|
||
вызовов нигде в дереве (`grep -rn "bevelMultiWallPaper" src/*.ts test/*.mjs
|
||
demo/*.mjs` находит только её собственное объявление) — коммит `e8338663`
|
||
корректно убрал единственный вызов (`paperGeom`/`floorFootprintGeometry`
|
||
теперь используют `paperWithNodeCorners`), но само тело функции осталось.
|
||
Не экспортирована, поведения не меняет, `tsc --noEmit` её не флагует
|
||
(в проекте нет `noUnusedLocals` для верхнеуровневых функций). Снимаю с
|
||
записью — правьте свободно вместе с M1, либо отдельной строкой, решение за
|
||
автором: это тривиальная уборка, не тянет на отдельный цикл сама по себе.
|
||
|
||
## Что проверено и корректно
|
||
|
||
- Основной контракт (веер `junctionNodeGeometry`, границы mitre/bevel,
|
||
фасадный клип `junctionNodeBound`, спец-случаи виртуального луча,
|
||
коллинеарной пары, degree-1, колонны) прочитан построчно и соответствует
|
||
§8.2-8.3 спеки; тесты `test/wall-thickness.test.mjs` («issue 302 …», 6 новых
|
||
тестов) целятся именно в эти границы, включая регресс на 1.25·h-лимит
|
||
(старый #249) и на вырожденный реверс-mitre (Y-60 из отчёта владельца).
|
||
- Формальный детектор `junctionContractHoles` не «слеп»: юнит-тест кормит
|
||
ему заведомо дырявое тело (2 из 3 полос) и требует красноты — прошёл;
|
||
отдельно мутант `junction-detector-blind` подтверждает то же на живом
|
||
харнессе.
|
||
- Golden-набор реально расширяет покрытие матрицы §13 почти полностью:
|
||
L отсутствует как отдельная сцена, но обоснованно — `buildMultiWallNodeMap`
|
||
строит узлы только для `rays.length >= 3` (`wall-thickness.ts:1939`),
|
||
обычный двухлучевой угол этим механизмом вообще не обрабатывается (это
|
||
зона старого, не тронутого этой задачей `outsetContour`/`insetContour`
|
||
mitre); включение «L» в таблицу §13 спеки было избыточным пожеланием, а не
|
||
пропущенным требованием — не в счёт находок.
|
||
- Трейлеры: оба `User-Visible: yes`-коммита (`9afc410d`, `e8338663`) несут
|
||
правки в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том же коммите;
|
||
формулировка финальной записи («the old junction chamfers are gone»)
|
||
описывает видимый результат (визуально фаска исчезла — трим невидим,
|
||
работает только на вырожденных геометриях) и не расходится с рендером.
|
||
- Golden-коммиты несут `Release:`/`Baseline-Reviewed:` на реальные зелёные
|
||
прогоны CI (проверено — оба run ID существуют и относятся к веткам этой
|
||
задачи).
|
||
- `process-gate.mjs` офлайн — 0 предупреждений; ветка полностью содержит
|
||
`origin/dev` (ребейз произведён, конфликтов нет).
|
||
- AC3 количественно: индекс эталонов содержит ровно 16 новых + 2 изменённых
|
||
хэша из 126 (сверено чтением `baselines-index.json`, не поверил на слово).
|
||
|
||
## Чего не проверял
|
||
|
||
- Полный `demo/smoke_*.mjs` (189 файлов) и полный `npm run mutants`
|
||
(~90 мутантов) — предрелизные гейты, задача не задевает всё; выборка
|
||
обоснована в таблице выше.
|
||
- Локальный полный `python -m pytest tests_backend` — диф не трогает Python.
|
||
- Числовой перф-профиль large-house вручную не переснимал; доверился
|
||
зелёному CI job `performance_smoke` на `b95c55d3` (подтверждено — не reuse).
|
||
- Не проверял три «зарегистрированные связи» смока `smoke_decor.mjs` и
|
||
`smoke_space_scale_defaults.mjs` (символ `cellCm` — широко используемый,
|
||
слабая связь по инструменту) — доверился их независимости от узловой
|
||
механики по чтению кода (`cellCm` там используется вне контекста
|
||
multi-wall-узлов).
|
||
|
||
## Вердикт
|
||
|
||
High: 0 · Medium: 2 (обе в скоупе) → возврат автору, без нового issue (#202).
|
||
|
||
Оба Medium дешёво чинятся: M1 — переписать один абзац
|
||
`docs/WALL-THICKNESS.md:188-196` под фактический контракт; M2 — добавить
|
||
build-префикс в guard одного мутанта и подтвердить «поймано 1 из 1»
|
||
исполнением. Ни одна находка не требует правки продуктовой геометрии —
|
||
сам механизм (AC1-AC5, AC7, AC9) проверен исполнением и корректен.
|