Files
houseplan-card/legacy/reviews/v1.65.0/CODE-REVIEW-201-r1.md
T
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00

16 KiB
Raw Blame History

Код-ревью #201 — r1

  • Issue: #201
  • ТЗ: docs/specs/201-atomic-thickness-lookup.md (ревью ТЗ зелёное: SPEC-REVIEW-201-r1.md)
  • Диапазон: git log --oneline origin/dev..HEAD
    • f7abf14 fix: inherit parent thickness for atomic walls (Issue: #201, User-Visible: yes)
    • 8b8b9ed docs: review document for #201 (Issue: #201, User-Visible: no)
    • 7b759f3 docs: specify atomic wall thickness lookup (Issue: #201, User-Visible: no)
  • Роль: ревьюер кода, свежая сессия без контекста реализации.

Скоуп

Баг: thicknessCmAt() возвращал 0 для атомарного child-сегмента, покрытого более длинной exact wall-записью, из-за чего единственный продуктовый потребитель — thicknessOnClose() в src/open-spans.ts — терял реального соседа и подставлял DRAW_WALL_DEFAULT_CM (15 см) вместо фактических 20/22 см при закрытии виртуальной границы, разделённой третьей комнатой. Скоуп по non-scope ТЗ: только thicknessCmAt() + regression-покрытие; wallIntervals(), cmsForPoly(), рендер, schema, Optimize (#198) не трогаются.

Относится к J4/J6 (docs/SCOPE.md): «Keep the plan true as the home evolves» — Close не должен незаметно подменять сохранённую физическую толщину значением по умолчанию.

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

Дешёвые гейты прогнаны лично, не только по слову автора:

Гейт Команда Результат
Typecheck npx tsc --noEmit зелёный, без вывода
Unit npm test 912/912, fail 0 — совпадает с заявленным
Build npm run build зелёный
Bundle parity sha256sum dist/... custom_components/.../houseplan-card.js demo/srv/assets/houseplan-card.js все три 35ad6b54...a89e — совпадает с хендоффом
Docs gate node scripts/check-docs.mjs --external Documentation checks passed (7 files, 10 external links) — уместен, т.к. коммит трогает docs/images/screenshots.json
Mutation gate node scripts/mutation-gate.mjs --check все записи ok, включая новую atomic-child-thickness-parent-fallback
Targeted smoke node demo/smoke_resize_virtual_thick.mjs (после свежего npm run build + copy) все поля true, включая три новых atomicParentClose*, OK

Тест умеет падать — проверено исполнением, не по слову автора. Временно откатил src/wall-thickness.ts до состояния origin/dev (git apply -R на diff файла), пересобрал test-build (npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs) и прогнал: node --test --test-name-pattern="exact parent|atomic solid children" test/wall-thickness.test.mjs test/open-spans.test.mjs — оба новых теста красные (0 !== 20, 15 !== 22). Затем git checkout -- src/wall-thickness.ts, пересобрал test-build, node --test test/wall-thickness.test.mjs test/open-spans.test.mjs → зелёные (97 тестов, 0 fail), рабочее дерево чистое (git status --short пусто) — эксперимент не оставил следов.

Читал построчно: thicknessCmAt(), новый приватный exactCoveringWall(), entrySpan(), distToSeg(), angleClose(), segAngle(), и параллельно — cmsForPoly() (уже существующий алгоритм «наиболее узкий покрывающий exact span», строки 1081–1099/1107–1140), чтобы убедиться, что новый helper — переиспользование того же контракта, а не новая независимая эвристика. Читал thicknessOnClose()/applyThicknessOnClose() в src/open-spans.ts — diff по этому файлу пуст, что ожидаемо: единственный вызов уже шёл через thicknessCmAt(), фикс на уровне resolver'а автоматически чинит потребителя.

Гейты, которые НЕ прогонял, и почему

  • node demo/smoke_*.mjs (остальные 126 из 127). Diff ограничен одной чистой функцией с одним продуктовым потребителем внутри src/wall-thickness.ts; AC7/AC8 сами называют только smoke_resize_virtual_thick.mjs. Остальные smoke не касаются thicknessCmAt/Close и не входят в затронутые поверхности ТЗ (§4/§9).
  • npm run golden:verify. ТЗ прямо говорит golden не обязателен (§10.2): видимая форма уже численно проверена browser-смоуком, общий wall renderer (wallIntervals()/cmsForPoly()/body geometry) не менялся. Диф docs/images/screenshots.json — только пересчитанный sourceFingerprint под изменившийся src/**, imageSha256 не менялся ни для одного сценария → новой визуальной сцены нет.
  • python -m pytest tests_backend -q. Диапазон не касается custom_components/**/*.py (см. таблицу файлов diff) — backend гейт не относится к этой задаче.
  • performance-профили. Не названы в AC, риск в ТЗ явно закрыт (§11: поиск идёт только при editor Close, линейный проход сравним с существующим lookup, вне render tick) и не затрагивает горячий путь рендера.

Это сужение соразмерно объёму: один pure-helper и один тестовый/smoke файл; полный набор гейтов — предрелизный, а не гейт код-ревью (PROCESS.md §8).

Проверка AC (ТЗ §9)

AC Как доказано Вердикт
AC1 wall-thickness.test.mjs: thicknessCmAt inherits the narrowest exact parent... — прогнан, зелёный; откатом кода воспроизведён красным (0 !== 20). Доказано автотестом, тест умеет падать.
AC2 Тот же тест покрывает coordScale = 1 и 1000 и обратное направление endpoints — прогнан в составе npm test. Доказано автотестом.
AC3 thicknessCmAt exact-parent fallback does not leak from partial or unrelated spans — partial [0..4]→[0..10], parallel offset, perpendicular, malformed (a:['bad',0], вырожденный [0,0]-[0,0]) — все дают 0; прогнан в npm test. Прочитан код: entrySpan() отбрасывает нечисловые/неполные endpoints, spanLen<1e-12 и spanLen+tol<queryLen отсекают частичный/вырожденный случай до входа в distToSeg. Доказано автотестом + чтением guard-условий.
AC4 nested case в том же тесте: два кандидата (10-см родитель и 20-см вложенный [4,0]-[6,0]), прогнан для nested и [...nested].reverse() — оба дают 30 (узкий кандидат). Прочитан tie-break: extra = spanLen-queryLen, при равенстве — лексикографический stable (key+cm+координаты), не зависит от порядка входного массива. Доказано автотестом + чтением.
AC5 Существующие unit на legacy key-only lookup (lookupWall finds an entry...) прошли без изменений в npm test (912/912); entrySpan() возвращает null при отсутствии a/b, поэтому legacy-строки физически не могут попасть в новый fallback. Доказано регрессией + чтением.
AC6 open-spans.test.mjs: новый тест (parent 22 см → 22) плюс уже существующий thicknessOnClose uses neighbour cm else default (без толстого соседа → DRAW_WALL_DEFAULT_CM) — оба зелёные в npm test; новый воспроизведён красным при откате (15 !== 22). Доказано автотестом, тест умеет падать.
AC7 node demo/smoke_resize_virtual_thick.mjs после свежего build+copy — atomicParentClosePreviewInherits, atomicParentClosePersistsNeighbourCm, atomicParentCloseUndo все true, скрипт завершается OK (сценарий явно проверяет отсутствие console/page ошибок — общий паттерн файла, остальные 19 полей смоука тоже true, регрессий не внесено). Доказано исполнением browser smoke.
AC8 Тот же прогон smoke — все 19 ранее существующих полей (realBodyReady…virtualTJunctionMitred) остались true; npm test 912/912 без регрессий вне нового кода. wallIntervals()/cmsForPoly()/render не изменены (diff пуст по этим функциям). Доказано регрессией + чтением diff (не тронуты).
AC9 docs/CHANGELOG.md/docs/CHANGELOG.ru.md (в том же коммите f7abf14, User-Visible: yes), docs/TESTING.md — все три diff проверены построчно, формулировки соответствуют факту фикса. Проверено чтением diff.
AC10 node scripts/mutation-gate.mjs --check — запись atomic-child-thickness-parent-fallback в списке ok; guard-команда мутанта (--test-name-pattern="exact parent|atomic solid children") — именно те тесты, которые я лично воспроизвёл красными при откате фикса. Мутант превращает exact в false ? ... : null, то есть буквально отключает новый fallback. Доказано исполнением + чтением патча мутанта.
AC11 npx tsc --noEmit, npm test, npm run build, node demo/smoke_resize_virtual_thick.mjs — все зелёные лично. Доказано исполнением.

Находки

Нет находок ни High, ни Medium, ни Low.

Отдельно проверено на предмет типичных ошибок такого рефакторинга и не подтвердилось:

  • Дублирование логики без переиспользования. Новый exactCoveringWall() — не копия, а тот же containment-контракт (distToSeg двух endpoints ≤ tol, angleClose, spanLen ≥ queryLen - tol), что и уже работающий блок cmsForPoly() (строки 1081–1099). Общие entrySpan/angleClose/segAngle/ distToSeg не продублированы, а вызваны из обоих мест.
  • Тихая утечка толщины через legacy fallback. Проверено: entrySpan() требует валидных a/b; legacy {key,cm}-строки не имеют a/b и физически не проходят в exactCoveringWall().
  • Ложно-зелёный мутационный гейт. Проверил вручную (не только по выводу --check): без фикса именно эти два новых теста падают, с фиксом — проходят; guard мутанта запускает ровно их.

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

  • Причинно-следственная цепочка дефекта, описанная в ТЗ §3, подтверждена чтением: единственный продуктовый вызов thicknessCmAt() — из thicknessOnClose(); фикс на уровне resolver'а автоматически чинит потребителя без изменений в open-spans.ts (diff файла пуст).
  • Все 11 AC ТЗ имеют явное доказательство и я его воспроизвёл лично там, где это было унит/smoke/mutation-тест; там, где доказательство — «не менялось» я сверил diff'ом.
  • Три копии бандла (dist/, custom_components/houseplan/frontend/, demo/srv/assets/) байт-в-байт идентичны и совпадают с SHA, заявленным в хендоффе.
  • Трейлеры Issue:/User-Visible: на всех трёх коммитах корректны; User-Visible: yes в f7abf14 сопровождён правками обоих changelog в том же коммите.
  • Изменение docs/images/screenshots.json — только sourceFingerprint (пересчитан от изменившегося src/**), imageSha256 не менялся ни в одном сценарии; новой визуальной сцены нет, golden обоснованно не запускался.
  • Non-scope ТЗ выдержан: wallIntervals(), cmsForPoly(), схема, Optimize/#198 не тронуты.

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

  • Полный набор demo/smoke_*.mjs (126 из 127) — не относятся к затронутой поверхности, см. таблицу гейтов выше.
  • npm run golden:verify — не запускал; обоснование — отсутствие изменения imageSha256 и явное «golden не обязателен» в ТЗ §10.2.
  • python -m pytest tests_backend -q — Python-код не затронут.
  • performance-профили — не названы в AC, риск закрыт архитектурно (поиск только на editor Close, вне render tick).
  • Ручное взаимодействие с реальным HA-сервером — вне цикла ревью по процессу; за него отвечает исполненный browser smoke.

Вердикт

Зелёный. Причина дефекта верна, исправление минимально и переиспользует существующий проверенный алгоритм, регрессия доказана падающим до фикса тестом (лично воспроизведено), утечка толщины исключена явным негативным покрытием, потребитель (thicknessOnClose) чинится автоматически без собственных изменений, все AC подтверждены исполнением или чтением с пометкой, документация и changelog в том же коммите, гейты зелёные.