Files
houseplan-card/docs/reviews/CODE-REVIEW-331-r1.md
T
claude[bot] 95fba6dbab
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 53s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 1m16s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 1m1s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 17s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 16s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 7m11s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 9m5s
docs: review document for #331
Issue: #331
User-Visible: no
2026-08-28 02:01:06 +00:00

20 KiB
Raw Blame History

CODE-REVIEW-331-r1

Issue: #331 — «Ограничения стыков (#329): пограничная точность даёт ложные отказы и невидимые дубли». Заход: r1 (первый код-ревью-заход для этой задачи; отдельно был полный цикл spec-ревью r1→r3, все находки закрыты, вердикт зелёный). Материал: git log --oneline origin/dev..HEAD, git diff origin/dev...HEAD. SHA материала: 550be251a68b1467940cf296189c7eab31b14181 (сверено git rev-parse HEAD непосредственно перед выводом итога).

Скоуп

Три продуктовых коммита поверх трёх спек-документов:

  • 58f3acbb — шесть нормативных правок §2 спеки, симметрично в src/junction-limits.ts и custom_components/houseplan/junction_limits.py, плюс один catch-блок в src/houseplan-card.ts, i18n-ключ (en+ru), changelog (en+ru), USER-GUIDE (en+ru), docs/specs/329-junction-limits.md дополнен абзацем о точности границ, 4 новых мутанта.
  • 0cdf85d9 — усиление двух мутантов, которые изначально не ловились (честно описано автором в handoff-комментарии).
  • 550be251 — регистрация smoke-link для quantizeKeyCoord/INCIDENT_EPS/ KEY_FACTOR в scripts/smoke-links.mjs.

Изменённых файлов класса A: src/junction-limits.ts, src/houseplan-card.ts, custom_components/houseplan/junction_limits.py, src/i18n/{en,ru}.json — все с трейлером Issue: #331, User-Visible: yes только там, где меняется поведение (первый коммит); остальные два — User-Visible: no (тесты и регистрация в реестре), корректно.

Диапазон не трогает геометрическую модель (рёбра комнат, layout, marker.space, open_spans, записи толщины) — это проверки-валидаторы поверх уже существующей модели wall_segments, читающие сырые координаты. Инварианты модели (npm run invariants) поэтому не гоняю — diff не задевает то, что они проверяют (ключ узла для геометрии, ссылки на неё, ключ записи толщины); задетый здесь ключ — внутренний ключ группировки в валидаторе, не ключ хранения.

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

Гейт Команда Результат
Typecheck npx tsc --noEmit чисто, без вывода
Unit (frontend) npm test 1424 pass / 0 fail / 1 skip (1425 всего)
Backend (узкий) python -m pytest tests_backend/test_junction_limits.py -q 16 passed
Backend (полный) python -m pytest tests_backend -q (после pip install pytest pytest-asyncio pytest-homeassistant-custom-component home-assistant-frontend — в среде их не было) 426 passed, 1 skipped, 1 error — test_ha_upload.py::test_upload_ok, AssertionError на threading._DummyThread/waitpid- в teardown _run_safe_shutdown_loop. Файл не в diff (git diff origin/dev...HEAD -- tests_backend/test_ha_upload.py пуст), сбой воспроизводится и на изолированном прогоне одного файла — это особенность песочницы (наименование потоков в Python 3.12 внутри pytest-homeassistant-custom-component), не регрессия этой задачи. Не блокирует.
Build npm run build собрано; dist/houseplan-card.js побайтово идентичен закоммиченному (git status чист после сборки)
Три копии бандла diff dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js до и после свежей сборки идентичны
check-docs node scripts/check-docs.mjs (diff трогает src/**) «Documentation checks passed (7 files, 10 external links)» — фингерпринт screenshots.json пересчитан и совпадает
smoke-select node scripts/smoke-select.mjs --base origin/dev --head HEAD одна «зарегистрированная связь»: demo/smoke_junction_limits.mjs (символы quantizeKeyCoord, INCIDENT_EPS, KEY_FACTOR) — это и есть AC5-смок, других претендентов нет
Смок AC5 node demo/smoke_junction_limits.mjs (после npm run bundle:sync — стендовая копия бандла не коммитится) OK, все поля включая checkFailureRefusesWrite/checkFailureToastShown — true
Смок (доп., по заявлению автора) node demo/smoke_island_rooms.mjs, node demo/smoke_room_resize.mjs оба OK — держат соседние сценарии барьера #329/#330, которые эта задача не должна была задеть
Бенч #330 npm run benchmark:junction-limits "pass": true, все 5 метрик внутри бюджета без правок бюджетов (tsFullCandidateMs 156.8/400, pyColdValidateMs 2264.8/5000 и т.д.)
Мутанты (свои, точечно) node scripts/mutation-gate.mjs --id=<id> для всех 4 новых id junction-limit-zero-wedge-invisible — 1/1 поймано; junction-limit-key-precision-lost — 1/1; junction-limit-branch-dropped — 1/1; junction-limit-candidate-fail-open — 1/1. Для каждого лично проверено: чистый прогон зелёный, мутация красит именно названный тест
Инварианты модели не гонял diff не меняет геометрическую модель/ключи хранения — см. «Скоуп»
Golden не гонял diff не меняет рендер/геометрию/стили/слои — только вердикты валидатора и один текстовый тост
Полный mutation-gate.mjs без --id случайно запущен один раз при попытке --help; остановлен вручную нашёл несвязанный с этой задачей провал чистого прогона smoke_resize_pointer_real_plan.mjs (падает ДО применения любой мутации). git diff не касается wall-face-graph/resize-pointer кода этой задачи — похоже на особенность песочницы (Chromium/Playwright конкретной среды), а не дефект #331. Не отношу к находкам этого ревью: не смог подтвердить, что это воспроизводится вне данной песочницы, а скоуп задачи эту область не трогает

Разбор по AC (спека docs/specs/331-junction-limit-precision.md, ревизия 3)

  • AC1 (ключи/инцидентность). Доказано автотестом (test/junction-limits.test.mjs «#331 AC1» ×2, tests_backend/test_junction_limits.py::test_331_debris_is_one_node_and_duplicate_is_visible, плюс параметр debris-node в паритет-фикстурах). Тест умеет падать: лично прогнал мутант junction-limit-key-precision-lost, красит именно этот тест. Отдельно проверил чтением: quantizeKeyCoord/_quantize_key_coord математически идентичны (sign*floor(|v|*1e7+0.5)/1e7 в обоих зеркалах, IEEE754 double — операции побитово совпадают в TS и Python), -0 нормализуется явно в обеих реализациях. INCIDENT_EPS поднят синхронно в обоих местах его использования (узел↔узел и узел→стена), как того требует §2.1.
  • AC2 (0°). Доказано автотестом (тот же файл, «#331 AC2», test_331_debris_is_one_node_and_duplicate_is_visible) — тест умеет падать, лично прогнал мутант junction-limit-zero-wedge-invisible. 180°-пара и T-стык остаются чисты — проверено тем же тестом и не задетой параллельно паритет-фикстурой tee.
  • AC3 (итеративность/ветви). Доказано автотестом («#331 AC3» на обеих сторонах, test_331_iterative_run_and_forks, collinear-fork в паритете). Тест умеет падать — лично прогнал junction-limit-branch-dropped, красит именно узел с развилкой (не переполнение). Переполнение на 10 000 атомов проверено прямым вызовом (JS и Python) без исключения — код читал: обход теперь while (frontier.length)/while frontier: с visited-множеством, рекурсии нет ни в одной реализации. 100 развилок — таймаут-ассерт в самом тесте (< 500 ms), зелёный при штатном прогоне npm test.
  • AC4 (дуга). Доказано автотестом («#331 AC4»). Проверено чтением: и в TS, и в Python collinear(candidate, segment) сравнивается с ВНЕШНИМ параметром функции (базой прогона), а не с current/from — накопление поворота по дуге устранено симметрично в обоих зеркалах.
  • AC5 (fail-closed). Доказано смоком (demo/smoke_junction_limits.mjs, секция AC5) — лично прогнал, зелёный; асимметрия (2-й вызов ломает кандидата, 1-й — baseline остаётся fail-open) доказана мутантом junction-limit-candidate-fail-open, лично прогнан, красит смок. Проверено чтением: _junctionLimitLabel строит junction.limit_check_failed из violation.rule, ключ добавлен в en.json/ru.json без плейсхолдеров {actual}/{limit} — числового дублирования нет (правило «одно число — один источник» неприменимо, значения не показываются пользователю).
  • AC6 (except, r2 M-r2-1 — два явных случая). Доказано автотестом test_331_ac6_candidate_bug_raises_previous_bug_falls_back — читал код теста и подтвердил, что он различает стороны: мок подменяет commit_wall_segment_model на всегда бросающий TypeError-подкласс; случай (а) вызывает validate_junction_limits с немигрированным документом-кандидатом → pytest.raises(Boom) — ошибка всплывает честно; случай (б) подсовывает уже смигрированный (model_version >= 9) кандидат, так что мок не задевает сторону кандидата вовсе, а немигрированный previous ловит тот же мок и падает в except Exception, side != "candidate" → тихий фолбэк, исключение не всплывает. Тест умеет падать — если реализация перепутает стороны или уберёт асимметрию, raise либо появится там, где не должен, либо пропадёт там, где обязан. Прочитал вызовы _migrated_spaces: единственные два места (old_counts/предыдущая — без side, по умолчанию "previous"; new_spaces — side="candidate") — совпадает с нормативным текстом §2.6 дословно.
  • AC7 (паритет и регресс). Паритет-набор расширен тремя новыми фикстурами (debris-node, duplicate-wall, collinear-fork), паритет-тест реально гоняет TS через subprocess и сверяет с python — зелёный (python -m pytest tests_backend -q). Полный набор юнитов/смоков #329+#330 зелёный (см. таблицу гейтов), бенч #330 зелёный без правок бюджетов.

Находки

Нет. Ни High, ни Medium, ни Low.

Разбор был направлен на поиск расхождений (паритет TS/Python на плавающей арифметике — источник дефектов #258/#259 в этом же классе задач), скрытого дублирования чисел, тихого пропуска записи и нарушения границ скоупа (#330 бюджеты, #329 инварианты §3) — ни одного не нашёл. Всё найденное в r1–r3 spec-ревью (H1 квантование, M1 порог инцидентности, M2/M-r2-1 асимметрия except, M3 комбинаторика, M4 USER-GUIDE, L1/L2 формулировки) закрыто в тексте спеки и реализация её соблюдает дословно — свёл нормативный текст §2 с кодом построчно, расхождений нет.

Отдельно ценю самокритичность хендоффа: автор сам обнаружил и признал два изначально «беззубых» мутанта (junction-limit-key-precision-lost, junction-limit-branch-dropped) и коммитом 0cdf85d9 усилил тесты и патчи мутантов, а не просто заявил «мутанты ловятся». Я перепроверил оба лично — после усиления оба ловят 1/1.

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

  • Формула квантования ключа узла и её паритет TS↔Python (включая -0 и .5-тик) — прочитано и подтверждено логически идентичным по IEEE754.
  • Порог инцидентности применён синхронно в обоих местах использования (узел↔узел, узел→стена) в обеих реализациях.
  • Итеративный обход по рёбрам с visited — код прочитан построчно в TS и Python, рекурсии не осталось, развилки не теряются, дуга не выдаёт себя за стену (сравнение с базой, не с предыдущим атомом).
  • Fail-closed кандидата и симметричный fail-open baseline — прочитано в коде и доказано смоком + мутантом.
  • Асимметрия except по сторонам в python-зеркале — прочитано построчно и доказано юнитом, различающим стороны.
  • i18n-ключ, changelog (en+ru) и USER-GUIDE (en+ru) — в том же коммите, что поведение; текст USER-GUIDE и текст тоста согласованы по смыслу.
  • Бандл: свежая сборка байт-в-байт совпадает с обеими закоммиченными копиями.
  • Скоуп не расширен: единственная правка в houseplan-card.ts — тот самый catch-блок, никаких «раз уж я здесь».

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

  • Golden-эталоны (npm run golden:verify) — diff не меняет рендер, геометрию, стили или слои, только вердикты валидатора и текст тоста; риска визуальной регрессии не вижу.
  • npm run invariants — diff не трогает геометрическую модель, рёбра комнат, layout, marker.space, open_spans или ключи хранения записей толщины; изменённый ключ — внутренний ключ группировки валидатора, не ключ хранения.
  • Полная матрица demo/smoke_*.mjs — не прогонял всю (194 смока, задача не задевает «всё»); прогнал названный в AC5 (smoke_junction_limits.mjs, подтверждён и smoke-select.mjs как единственная зарегистрированная связь) плюс два соседних по барьеру #329/#330 (smoke_island_rooms.mjs, smoke_room_resize.mjs) — оба зелёные, потому что задача меняет общий для них код валидатора и кэш-барьер.
  • Полный прогон mutation-gate.mjs без --id — дорогой (сотни мутантов); прогнал точечно только 4 новых. Случайный частичный прогон всей матрицы (при опечатке --help) поймал провал smoke_resize_pointer_real_plan.mjs на чистом прогоне, не связанный с этим diff — не оцениваю его как находку этой задачи (см. таблицу гейтов), но фиксирую для истории на случай, если это не воспроизведётся в CI: обнаружено вне скоупа проверки, вне AC #331, относится к резалку(#330-related resize pointer capture — соседняя функциональность), не диагностировано глубже, поскольку это заняло бы время, не относящееся к этому issue.
  • Perf-профили сверх названного бенчем #330 — не отдельно, AC7 их покрывает целиком.

Итог

Все AC (1–7) доказаны автотестом либо смоком, для каждого лично подтверждена способность соответствующего теста падать (через точечный прогон мутанта). Дешёвые гейты — typecheck/test/build/check-docs — зелёные, бенч #330 зелёный без правок бюджетов, паритет TS/Python зелёный на новых граничных классах. Единственная аномалия (test_ha_upload.py в полном backend-прогоне) не связана с diff и воспроизводится как окружение-специфичный сбой teardown, не блокирует.

Вердикт: зелёный.