21 KiB
CODE-REVIEW-271-r1
- Issue: #271 — «Degree-3 узел достраивает короткий луч до 8×H»
- Этап: code (PROCESS.md §2.7)
- Заход: r1 (первый код-ревью цикл; спека прошла ревью зелёным в отдельном r1 —
docs/reviews/SPEC-REVIEW-271-r1.md) - Материал:
git log --oneline origin/dev..HEAD=396b391(fix),77143d4(docs: спек-ревью),482b2be(docs: спека) - Коммит реализации:
396b391a93cc228bd6b779b9985db87e853bfa50, трейлерыIssue: #271/User-Visible: yes, оба changelog в этом же коммите — корректно. - Вердикт: красный (High: 1, Medium: 1 в скоупе)
Скоуп
Диапазон origin/dev...HEAD — один продуктовый файл src/wall-thickness.ts
(добавлено поле supports: MultiWallNodeRaySupport[] на канонический луч
multi-wall узла и ограничение локальной реконструкции реальной длиной
support'а вместо 8×H), плюс тесты (test/wall-thickness.test.mjs,
test/golden-matrix.test.mjs), таргетированный smoke
(demo/smoke_junction_patch_resilience.mjs), golden matrix/harness
(demo/golden/matrix.mjs, demo/golden/harness.mjs), мутация
(scripts/mutation-gate.mjs), реестр smoke-связей (scripts/smoke-links.mjs)
и документация (ARCHITECTURE/WALL-THICKNESS/TESTING/USER-GUIDE×2/CHANGELOG×2 +
docs/images/screenshots.json). Соответствует заявленным «Ожидаемым файлам»
§9 ТЗ.
Привязка к продукту подтверждена: J1 (план не должен показывать
несуществующую архитектуру) и J6 (одна каноническая геометрия для всех
потребителей) — сценарий и симптом (docs/SCOPE.md).
Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Типы | npx tsc --noEmit |
зелёный, без вывода |
| Юнит | npm test |
1163 passed, 0 failed |
| Сборка | npm run build |
зелёный |
| Синхронизация трёх копий бандла | npm run bundle:sync |
зелёный; git status --short после — пусто (байт-в-байт) |
| Документация/скриншот-отпечаток | node scripts/check-docs.mjs |
«Documentation checks passed (7 files, 10 external links)» |
| Целевая мутация | node scripts/mutation-gate.mjs --id=multi-wall-finite-ray-disabled |
поймано 1/1, чистый прогон зелёный |
| Выбор smoke по дельте | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
назвал smoke_junction_patch_resilience.mjs, smoke_multiwall_junction.mjs (обе — «зарегистрированная связь») |
| Smoke #197/#271 | node demo/smoke_junction_patch_resilience.mjs |
OK, все поля true, включая новые *StopsAtFiniteRayEndpoint |
| Smoke #249 | node demo/smoke_multiwall_junction.mjs |
OK |
| Целевой semantic golden (AC6) | node demo/golden/run.mjs --mode=capture --scenario=junction-patch-resilience-plan-dark |
error: golden finite multi-wall ray contract failed |
| Целевой semantic golden (AC6) | node demo/golden/run.mjs --mode=capture --scenario=junction-patch-resilience-view-dark |
error: та же ошибка |
| Независимая проверка чтением | прямой вызов buildMultiWallNodeMap/wallBodiesGeometry из test-build/wall-thickness.js на той же фикстуре |
подтвердил причину ошибки на уровне сырой геометрии (см. находку H-1) |
npm run golden:verify не запускался — инструмент сам это запрещает вне
полной матрицы (policy.mjs: golden verify must run the complete matrix),
что и является предусмотренным prerelease-гейтом (§8/AC9). Но именно поэтому
диагностический --mode=capture --scenario=<id> — единственный способ
исполнить добавленную семантическую проверку AC6 локально, и он был обязателен
для этих двух конкретных сценариев, потому что diff добавляет в них новую
проверку (absentWallProbes) целиком под этот issue.
Находки
[High] AC6 не выполняется: собственная семантическая golden-проверка issue падает на обоих сценариях
Файлы: demo/golden/matrix.mjs:266,271 (значение absentWallProbes[1]),
demo/golden/harness.mjs:582-588 (проверка, которая на нём падает).
Воспроизведение:
node demo/golden/run.mjs --mode=capture --scenario=junction-patch-resilience-plan-dark
# → error junction-patch-resilience-plan-dark
# artifacts/golden/golden-report.json:
# "error": "page.evaluate: Error: golden finite multi-wall ray contract
# failed: junction-patch-resilience-plan-dark"
То же для junction-patch-resilience-view-dark. Точечной проверкой (клонирую
сценарий с одним absentWallProbes элементом за раз) показано, что падает
именно второй пробник — [0.936524285, 0.345833333]; первый
([0.420833333, 0.37625], тот же, что в
smoke_junction_patch_resilience.mjs) действительно пуст.
Причина (подтверждена прямым вызовом продукт-функций на той же фикстуре
test/fixtures/197-junction-patch.json, без браузера):
Узел (887.5, 345.833) (в render units) имеет три луча:
u=[1,0] halfDepth=6.25 length=1.3819039326657503 ← искомый короткий луч из таблицы issue
u=[-1,0] halfDepth=9.1667 length=195.833...
u=[0,-1] halfDepth=12.083 length=295.833...
Это в точности совпадает со строкой из issue/ТЗ:
1 / (887.500, 345.833) | 1.381904 | 96.667 — реализация действительно
ограничивает этот луч 1.3819, а не 96.667. Сам фикс на этом узле работает
правильно.
Но зонд [936.524285, 345.833] находится в 887.5 + 49.02 — то есть внутри
совершенно другого, независимого и полностью легитимного интервала стены:
room_8: a=[888.8819039326658, 345.8333], b=[979.1666666666666, 345.8333],
cm=22, half=9.1666...
Этот интервал начинается сразу там же, где заканчивается короткий луч
(888.88), и тянется до 979.17 — то есть физически это настоящая,
непрерывная (без зазора) кладка room_8, никак не связанная с 8×H-багом.
Прямой point-in-polygon расчёт против wallBodiesGeometry(...).geom
подтверждает: probe1 → true (внутри), probe0 → false (пусто, как и
ожидается).
Значит: между реальным концом короткого луча (1.3819) и следующим
независимым сегментом (888.88) зазора попросту нет — вдоль этого
направления в принципе не существует точки, которая была бы одновременно
«дальше короткого луча» и «пуста», потому что там сразу начинается другая
настоящая стена. §6.3 ТЗ прямо предвидел этот случай («если другой
независимый ray/body законно занимает точку за этим торцом, final union
остаётся заполненным им») — но зонд был выбран без проверки этого условия.
Почему это блокирует: AC6 требует, чтобы «до pixel diff harness
проверял, что два probes за finite endpoint пусты» — с добавленным кодом
это происходит, и один из двух пробников красный. Это не спекуляция и не
пропущенный гейт: гейт исполнялся (дважды, разными методами — Chromium и
прямой вызов), и оба раза воспроизвёл одну и ту же ошибку. Если это попадёт
в предрелизный npm run golden:verify (§8), CI-джоб golden станет красным
на dev до следующей задачи — тот же класс инцидента, что уже стоил
продукту #237. Хендофф-комментарий автора не упоминает запуск этого сценария
вообще (перечислены typecheck/test/bundle:sync/check-docs/mutation-gate/два
smoke) — гейт добавлен в этот же коммит, но не был исполнен ни разу.
Что нужно исправить: заменить absentWallProbes[1] на координату,
которая действительно лежит в пустоте за реальным концом искомого короткого
луча (либо выбрать другой узел/направление для второй иллюстрации из
таблицы issue, для которого такая пустая точка вообще существует — у этого
конкретного направления её нет, зазора между сегментами нет). Требуется
перепрогнать оба golden-сценария до зелёного после правки.
[Medium, в скоупе] AC4 не имеет собственного fixture/smoke с проёмом
ТЗ (§8, AC4) и сама формулировка issue («Обязательные регресс-тесты», п. 3) требуют fixture с коротким перпендикулярным лучом рядом с дверью и targeted browser smoke с реальным SVG/shadow probe, доказывающие: opening slot остаётся пустым, короткий луч не заходит в slot, light/sun occluder не получает лишней площади, а opening tunnel/symbol contract не меняется.
Ни новый unit-тест (test/wall-thickness.test.mjs:840, openings: []), ни
переиспользуемая фикстура test/fixtures/197-junction-patch.json
(openings отсутствует как ключ), ни test/fixtures/249-multiwall-junction.json
не содержат ни одного проёма. Единственная косвенная проверка —
lightAndSunStopAtFiniteRayEndpoint в smoke_junction_patch_resilience.mjs
— проверяет тот же общий occluder-механизм, но не рядом с дверью и без
проверки самого opening slot/tunnel/symbol.
Проверено чтением, не исполнением: bevelMultiWallBody() вызывается в
wallBodiesGeometry() (src/wall-thickness.ts:2686) строго до цикла вырезания
проёмов (src/wall-thickness.ts:2688-2705), который работает как чистое
булево вычитание по openingWallIndex, построенному из walls/wallIntervals
— эта функция в диффе не тронута. Значит вырезание проёма не может вести
себя иначе только потому, что рядом finite ray; фикс равно применяется вне
зависимости от наличия проёма поблизости, и вероятность отдельного,
специфичного для проёма дефекта архитектурно мала. Именно поэтому это
Medium, а не High: живого дефекта не нахожу, но обязательное по своему же ТЗ
регресс-покрытие именно того производственного симптома, который запустил
issue (тёмное пятно у двери на 2-1.png), отсутствует.
В скоупе задачи, чинится в этом же цикле по правилу §2.7 (Medium без High — жёлтый; здесь High уже есть, поэтому общий вердикт красный, но это не меняет маршрут находки — не заводится отдельным issue).
Что проверено и корректно
- AC1 (node map хранит конечную длину, permutation/order-independent) —
доказано автотестом
test/wall-thickness.test.mjs:840(deep-equal supports, reversed/permuted intervals дают одинаковую подпись). Тест умеет падать: мутацияmulti-wall-finite-ray-disabledкрасит именно этот тест-файл (через ту же комбинированную проверку, что и AC2, — см. ниже). - AC2 (короткий луч не достраивается) — доказано автотестом и
независимо — прямым вызовом
buildMultiWallNodeMap/wallBodiesGeometryя воспроизвёл узел(887.5,345.833)и подтвердилlength=1.3819..., а не96.667; probe[0,10]внутри,[0,100]иroomGeom-эквивалент — снаружи (тест). Совпадает с числами из таблицы issue. - AC3 (длинные rays/join #249 не обрезаны) — весь существующий набор
#249/#261/#197зелёный; area-бейзлайн#197-фикстуры пересчитан с явным комментарием, объясняющим уменьшение (124568.27...→124244.27...), величина уменьшения (≈324 ед²) правдоподобна для одного убранного фантомного прямоугольника. - AC5 (все поверхности используют finite result) — доказано smoke:
planStopsAtFiniteRayEndpoint,viewStopsAtFiniteRayEndpoint,kioskStopsAtFiniteRayEndpoint,staticStopsAtFiniteRayEndpoint,hiddenIsoStopsAtFiniteRayEndpoint,lightAndSunStopAtFiniteRayEndpoint,cleanFloorOwnsAreaAfterFiniteEndpoint— всеtrueпри реальном прогоне в Chromium. - AC7 (мутант ловит регрессию) —
node scripts/mutation-gate.mjs --id=multi-wall-finite-ray-disabledподтверждает: чистый прогон зелёный, патчsupportExtent = extentкрасит тест. Мутация задевает ровно ту строку (src/wall-thickness.tsвbevelMultiWallBody), которая реализует контракт §6.1/6.3 ТЗ. - AC8 (приватность/детерминизм)** — проверено чтением: fixture —
переиспользование уже прошедшей ревью анонимизированной
197-junction-patch.json, приватные1.json/2.jsonне коммитятся (в диффе их нет);renderNeverWritesConfigв smoke подтверждает отсутствие мутации config. - Failure isolation §6.5 — проверено чтением:
try/catchвокруг цикла по узлам вbevelMultiWallBodyне тронут,canonicalRays-фильтр (Number.isFinite && > 0/eps) исключает только невалидный ray/node, не весь план. - Compatibility §7 — persisted config/model version не тронуты (diff
ограничен внутренним представлением
MultiWallNodeRay/локальной реконструкцией); нет нового глобальногоO(E²)—canonicalSupportsимO(k²)только по числу дублей на одном направлении одного узла, что и раньше было O(дублей) на дедупликации. - Трейлеры и changelog — один коммит
396b391,Issue: #271,User-Visible: yes, оба changelog внутри того же коммита — соответствует §7 PROCESS. - Документация —
ARCHITECTURE.md/WALL-THICKNESS.md/TESTING.md/обаUSER-GUIDEобновлены точно в терминах реализованного контракта (non-dominated finite(half-depth, length)supports);docs/images/screenshots.json— толькоsourceFingerprintизменился, всеimageSha256совпадают (PNG не тронуты) — подтвержденоcheck-docs.mjsи сверкой diff вручную. - smoke-links —
scripts/smoke-links.mjsдополнен новыми символами (buildMultiWallNodeMap,MultiWallNodeRay,MultiWallNodeRaySupport) с причиной;smoke-selectподтвердил именно эти два smoke как «зарегистрированная связь», оба фактически прогнаны.
Чего не проверял и почему
- Полный
npm run golden:verify— инструмент сам требует полную матрицу (не принимает--scenarioв verify-режиме); это осознанный prerelease-гейт по AC9/§8, не подмена. Вместо этого исполнен диагностический--mode=capture --scenario=<id>для обоих сценариев, добавленных этим диффом — что и вскрыло находку H-1. - Полная smoke-матрица (174 сценария) — не запускал;
smoke-selectназвал только 2 релевантных, оба прогнаны. Diff не расширяет поверхность за пределы multi-wall reconstruction, широких символов (>34 смоков) не затронуто. npm run invariants— не запускал. Diff не трогает хранение записей толщины,layout,marker.space,open_spansили ключи решёточных рёбер — только внутреннее представлениеMultiWallNodeRayи локальную реконструкцию тела в уже существующей структурной фазе, читающую уже разрешённыеWallInterval. Ссылочная целостность конфигурации не может быть задета этим диффом; проверено чтением diff (git diff --statпоказывает единственный продуктовый файлsrc/wall-thickness.ts, без затрагиванияsetWallThickness/хранения ключей).python -m pytest tests_backend— не запускал,custom_components/**/*.pyне тронут (только скомпилированныйfrontend/houseplan-card.js).- Performance-профили — не названы в AC, geometry hot-path не меняет алгоритмической сложности (см. Compatibility выше).
- Полный golden/perf/Linux HA harness — предрелизный гейт по PROCESS §8/AC9 «Release-артефакты» ТЗ, сознательно не гоняется в цикле ревью.
Итог
Реализация верно устраняет корневую причину (потерю длины луча в
buildMultiWallNodeMap/bevelMultiWallBody) и большинство AC подтверждены
автотестами и независимым чтением/пересчётом. Но собственная семантическая
golden-проверка, которую сам диф добавляет для AC6, красная при реальном
исполнении — это High, блокирует. Плюс отсутствует обязательное по ТЗ
door-adjacent регресс-покрытие AC4 — Medium, в скоупе. Оба возвращаются
автору в этом же цикле, отдельный issue не заводится.