Files
houseplan-card/docs/reviews/CODE-REVIEW-423-r1.md
T
2026-09-02 18:53:33 +00:00

96 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-423-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/423
- Этап: код-ревью (PROCESS.md §2.7), заход **r1**, блокирующих циклов израсходовано 0 из 4
- Материал: ветка `issue/423-v170-polish`, HEAD на момент вывода `3576cc2ea7e5f85b4e8d91a45be4d85b7158528b`
- ТЗ: `docs/specs/423-v170-polish.md` (SPEC-REVIEW-423-r1, зелёный)
- Диапазон: `git diff origin/dev...HEAD` (54 файла, включая генерированные `dist/**` и `custom_components/houseplan/frontend/**`)
## Скоуп
Шесть подтверждённых аудитом v1.70.0 дефектов support pipeline:
1. Repair families по `translation_key` вместо `broken_plan_*`.
2. Filename из SHA-256-префикса вместо capability-token.
3. Двухфазная quota-проверка (до store/executor и после `await`).
4. `support_api: 1` вместо строгого равенства версий релиза.
5. Lazy support-словари вне initial View graph.
6. `backdrop_decode` benchmark под общий browser-error guard.
Пункт (е) issue (комментарий `docs-screenshots.yml`) явно исключён — принадлежит #422, файл в диффе не тронут (подтверждено: `git diff` по этому workflow пуст).
## Как проверялось
### Гейты
| Гейт | Статус | Как подтверждён |
|---|---|---|
| `npx tsc --noEmit` / `npm run typecheck` | зелёный | не перегонял: Validate `3576cc2e` → success, job «Фронтенд: типы, юниты, мутанты, синхрон бандла» выполнился заново (не reuse) и прошёл — https://github.com/Matysh/houseplan-card/actions/runs/33668037515 |
| `npm test` | зелёный | тот же job на том же прогоне; численно 1749/1749 (1 skip) заявлено автором, не расходится с зелёным job |
| `npm run build` + `bundle:sync` (сверка копий) | зелёный | тот же job (`синхрон бандла` — часть его имени и шагов) |
| `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | зелёный | перепрогнал сам: «Новых any нет» |
| `npm run bundle:budget` | зелёный | перепроверил вручную числа из `dist/houseplan-assets.json`: `initialViewGzipBytes=289137` < baseline 291046 и < budget 300000; `lazyEditorGzipBytes=154772`; `assertSupportBundleOwnership` подключён в `scripts/bundle-budget.mjs` и вызывается из main-блока, вызываемого `npm run bundle:budget`, который стоит в `validate.yml:460` и `publish-prerelease.yml:86` |
| `node scripts/check-docs.mjs` / job `docs` | зелёный | Validate `3576cc2e`, job «Предполётные проверки: документация, провенанс, процесс» → success (на предыдущем SHA `2038ab91` падал именно на `DOCS: failure`; коммит `3576cc2e` принял 2 скриншота и починил) |
| `python -m pytest tests_backend -q` (Linux HA) | зелёный | не перегонял локально (нет `homeassistant` в этом окружении — тихий скип, доказательством не считается). Проверил по CI: job «Бэкенд: pytest в Home Assistant» на прогоне для `4a7de337` (test: harden support preflight verification) реально выполнился и завершился `success` в 18:32:22 — до дальнейших изменений backend-путей не было, поэтому для `3576cc2e` job корректно **reuse**-нут (ключ `gate-reuse.mjs` включает src/**+demo/golden/**+package-lock и т.д., но backend-ключ отдельный и завязан на измененные backend-пути; после `4a7de337` backend-файлы не менялись) |
| `npm run golden:verify` | зелёный, не по диффу | Validate `3576cc2e` показывает job `golden` как **skipped** (reuse). Проверил происхождение реального прогона: на прогоне `4a7de337` (33667562495) job «Golden-кадры против принятых эталонов» дошёл до конца матрицы — лог показывает `passed` для всех сценариев, включая `support-desktop-empty-light-en`, `support-desktop-preview-dark-en`, `support-phone-validation-light-ru`, `support-phone-success-dark-en`, `support-relay-error-light-en`, `support-tablet-preview-dark-ru` — и **успешно сохранил cache-маркер** `reuse-golden-366e97...` в 18:34:48, после чего run получил `cancelled` только из-за отмены следующим пушем (смоки/golden в этом run отменились позже реального успеха golden). Это не «слепое доверие reuse»: сам прогон матрицы состоялся и был зелёным на коде, идентичном текущему `demo/golden/harness.mjs` (правка `card._haSupportApi = 1` в сценарии `support` вошла именно в этот коммит) |
| `performance_smoke` | зелёный, не по диффу | тот же прогон `4a7de337`: job «Перф-смок: бюджет времени кадра» реально выполнился и завершился success (18:31:37–18:33:15), не отменён |
| Браузерные смоки (полная матрица, 3 шарда) | зелёный, ПО ДИФФУ | Validate `3576cc2e`: все 3 шарда + «Смоки: все шарды зелёные» выполнились заново (не reuse) и прошли — это полный прогон `demo/smoke_*.mjs`, включая `smoke_support_feedback.mjs`, `smoke_help_affordance.mjs`, `smoke_preflight_diagnostics.mjs` |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | выполнил сам | «Прямое совпадение» (9): `smoke_support_feedback`, `smoke_help_affordance`, `smoke_optimize_coordinate_canonicalization`, `smoke_tap_ctx`, `smoke_junction_limits`, `smoke_partition_openings`, `smoke_preflight_diagnostics`, `smoke_room_resize`, `smoke_zero_wall_migration_unblocked`. Первые три из списка совпадают по существу (support-диалог, `_haIntegrationVersion`); остальные — совпадение по `_showToast`/`this.host._config` (не изменённая диффом логика, просто те же геттеры переиспользованы в новых строках `_copySupportText`/`_supportErrorText`). Все 9, как и вся остальная матрица, реально прогнаны в job "Смоки в браузере" на `3576cc2e" (см. строку выше) — отдельный точечный прогон не потребовался |
| `node demo/benchmark_backdrop_decode.mjs --guard-probe` | перепрогнал сам | `EXC Error: houseplan backdrop guard probe` → `FAILED: 1 uncaught exception(s) inside the card` → exit code 1. Тест умеет падать: подтверждено выполнением, не чтением |
| `node --test test/smoke-harness-contract.test.mjs` (контракт AC9) | проверено чтением | Мутационная проверка встроена в сам тест (снимает `watchPage`/`reportPageErrors` и требует красноты) — не потребовало отдельного прогона поверх зелёного `npm test` |
| Инварианты модели (`npm run invariants`) | не прогонял, не требуется | диапазон не трогает геометрию/`layout`/`marker.space`/`open_spans`/толщину стен |
| `python -m pytest tests_backend -q` без HA | не прогонял | заведомо тихо скипнул бы `test_ha_*.py`, доказательством не является (AGENTS.md) |
### Ручное чтение и трассировка
- Прочитан полный диапазон `custom_components/houseplan/websocket_api.py`, `support_transport.py`, `const.py` — построчно сверен с §"Контракт реализации" ТЗ.
- Прочитан полный диапазон `src/houseplan-card.ts`, `src/houseplan-editor-runtime.ts`, `src/support-feedback.ts`, `src/i18n/support.ts`.
- Прочитаны все новые/изменённые backend- и frontend-тесты построчно, а не по именам.
- Проведена архивная проверка происхождения зелёных гейтов в GitHub Actions (`gh api .../jobs`, `gh cache list`) для reuse-джобов backend/golden/performance — см. таблицу выше. Это не входит в обычный объём ревью, но потребовалось, потому что итоговый прогон `3576cc2e` сам по себе не содержит подтверждения backend/golden — я не принял reuse на слово.
## Находки
Пусто. High: 0, Medium: 0, Low: 0.
## Проверено и корректно (по AC)
- **AC1.** `_support_repairs()` (`websocket_api.py:251-267`) агрегирует по `translation_key` через `_SUPPORT_REPAIR_FAMILY = ^[a-z][a-z0-9_]{0,63}$`, пропускает foreign domain, `None` и невалидные ключи. `test_support_repairs_aggregate_safe_translation_key_families` конструирует ровно эти шесть случаев (два `broken_plan`, одно новое семейство, unsafe-ключ, missing-ключ, foreign domain) и проверяет через `json.dumps`, что raw id/placeholders/unsafe-ключ физически отсутствуют в сериализованном результате. Тест умеет падать: откат к `startswith("broken_plan_")` не подхватит `future_repair`. Доказано автотестом.
- **AC2.** Backend (`support_transport.py:58-60`) и frontend (`houseplan-editor-runtime.ts:9308-9310`) оба берут `sha256[:12]`/`sha256.slice(0,12)` из одного и того же значения `preview["sha256"]`, вычисленного один раз в `_build_snapshot` и не пересчитываемого отдельно нигде (проверено: `attachment_sha256=preview.get("sha256")` в `ws_support_submit`, `websocket_api.py:2333`) — источник числа один, ветвления сравнения "видно дважды" нет. `filename_token` полностью удалён из `support_transport.py` и всех вызывающих мест (grep по репозиторию — пусто). Backend-тест проверяет точное имя `houseplan-support-0123456789ab.json` и отсутствие token-префикса. Доказано транспортным тестом + frontend smoke (`downloadExact` в `smoke_support_feedback.mjs`).
- **AC3/AC4.** Прочитан порядок в `ws_support_preview` (`websocket_api.py:2164-2233`): preflight-проверка стоит до `write_lock`/`store.async_load`/`executor_job`; финальная проверка — после `await`, до удаления старого token; удаление старого token — синхронный участок без `await` между проверкой и записью. Три специально сконструированных backend-теста доказывают это исполнением, а не декларацией: `test_support_preview_quota_rejects_before_store_load_or_executor` мон키патчит `async_load`/`async_add_executor_job` так, что их вызов кидает `AssertionError`, и подтверждает, что при исчерпанной квоте они не вызываются вовсе; `test_support_preview_failed_refresh_keeps_previous_token` роняет `build_support_package` и проверяет, что старый token не исчез; `test_support_preview_final_quota_check_closes_executor_race` реальным `threading.Barrier` синхронизирует два конкурентных WS-запроса внутри executor-колбэка и проверяет, что ровно один получает `support_rate_limited`, а итоговый набор превью не превышает лимит. Все три теста по конструкции обязаны падать при откате к прежнему порядку (задача явно этого требует, и логика совпадает с планом отрицательной проверки ТЗ, пункт 3). Доказано backend-тестами.
- **AC5.** `SUPPORT_API_VERSION = 1` (`const.py`) добавлен в `ws_config_get` как top-level `support_api`, включая ветку с `fields`-проекцией (`websocket_api.py:1264`, обе ветки видел в диффе и в тесте). Backend-тест проверяет оба пути (`resp["result"]` и `projected["result"]`) и явно — что поле отсутствует внутри `config`. Frontend: `_adoptConfigCapabilities` (`houseplan-card.ts:4279-4288`) принимает только `Number.isSafeInteger`, иначе сбрасывает в `null`; вызывается на всех четырёх путях получения `config/get` (полная загрузка, structural adopt, periodic reload, config-reload) — проверил каждый вызов построчно. Доказано backend-тестом + чтением всех вызовов на фронтенде.
- **AC6.** Единый предикат `supportApiCompatible` (`support-feedback.ts:6-8`, `value === 1`) используется во всех трёх местах — `_buildSupportPreview`, `_submitSupport`, `_renderSupportDialog` (grep подтвердил отсутствие остатков старого `_haIntegrationVersion === CARD_VERSION`). Unit-тест `supportApiCompatible` перебирает `undefined, null, 0, 2, '1', 1.0, true, {}, NaN` — все должны быть несовместимы, только `1` совместим. `smoke_support_feedback.mjs` добавил явный сценарий mixed-release (`_haIntegrationVersion='mixed-release', support_api=1` → форма видна) и повторно проверил old-backend fail-closed через `_haSupportApi=null`. Доказано unit-тестом + smoke.
- **AC7.** `src/i18n/support/{en,ru,de,fr}.json` содержат по 42 ключа (тест явно фиксирует число), `en.json`/`ru.json`/etc. в основном словаре оставляют только `support.title`. `i18n.test.mjs` добавил тест на равенство key-set и плейсхолдеров всех четырёх support-словарей; `i18n-dead-keys.test.mjs` теперь сканирует объединённый словарь (core + support). `bundle-assets.test.mjs` — юнит-тест на `assertSupportBundleOwnership` с мутационной проверкой (marker в initial graph → падает; initial gzip не улучшился → падает). Доказано i18n- и bundle-тестами.
- **AC8.** `dist/houseplan-assets.json`: `initialViewGzipBytes=289137` (< 291046 baseline, budget не менялся — 300000). Перепроверил числа лично, не только со слов автора. `assertSupportBundleOwnership` — новый вызов в `bundle-budget.mjs`, подключён в CI (`validate.yml:460`, `publish-prerelease.yml:86`). Доказано build-манифестом + подключённым тестом.
- **AC9.** Лично прогнал `node demo/benchmark_backdrop_decode.mjs --guard-probe` — код возврата 1, ожидаемое сообщение о необработанном исключении. Обычный режим (`cases`) не тронут кроме обёртки `watchPage`/`reportPageErrors`. Контрактный тест `smoke-harness-contract.test.mjs` идёт дальше формального совпадения по имени: у него есть встроенная негативная проверка (снять `watchPage` или заменить `reportPageErrors()` на `false` → контракт обязан покраснеть) — то есть тест доказанно умеет падать, а не просто присутствует. Доказано исполнением + контрактным тестом.
- **AC10.** `git diff` по `.github/workflows/docs-screenshots.yml` пуст. Доказано diff'ом напрямую.
- **AC11.** Typecheck/unit/build — см. таблицу гейтов. Runtime-схемы (`CONFIG_SCHEMA`, `LAYOUT_SCHEMA`) в диффе не тронуты — `support_api` не проходит через них, добавляется в ответ отдельно после `_layout_metadata`/config-объекта. Проверено чтением.
- **AC12.** `docs/USER-GUIDE.md`/`.ru.md` переформулированы на «доступна при совместимом API», без упоминания SHA-префикса как публичного обещания. Оба `docs/CHANGELOG.md`/`.ru.md` отредактированы в том же коммите (`2038ab91`, `User-Visible: yes`) — трейлеры и оба changelog проверены `git show` напрямую. Доказано чтением diff + code review.
## Что не проверял (и почему)
- **Полный ручной прогон `python -m pytest tests_backend -q` на этой машине** — здесь нет `homeassistant`, тихий скип не был бы доказательством. Заменено проверкой происхождения реального Linux-CI-прогона на предыдущем SHA той же ветки (см. таблицу).
- **`npm run golden:verify` и `performance_smoke` вручную** — ТЗ прямо говорит, что видимый DOM/layout не меняется (только текст `support.update_required`, который не входит в golden-покрытие по разметке). Вместо повторного прогона проверил, что зелёный результат в reuse-цепочке реален (не «зелёная галка по недоразумению») — см. лог golden-job с `passed` по всем `support-*` сценариям.
- **8 из 9 «слабых»/совпавших по `_showToast`/`_config` смоков** (`smoke_junction_limits`, `smoke_partition_openings`, `smoke_room_resize`, `smoke_zero_wall_migration_unblocked`, `smoke_tap_ctx`, `smoke_optimize_coordinate_canonicalization`) и 18 позиций из «слабой связи» — отдельно не гонял: они совпали по названию геттера, который диапазон не меняет по смыслу (`_showToast`/`this.host._config` использованы как есть, без изменения контракта). Все они входят в полную матрицу, которая реально прогналась на точном SHA `3576cc2e` (3 шарда + сводный job зелёные), так что фактическое покрытие есть — просто не через отдельный целевой запуск.
- **Инварианты модели (`npm run invariants`)** — диапазон не касается геометрии рёбер, толщины стен, `layout`, `marker.space`, `open_spans`; не применимо.
- **Реальный HA `IssueEntry.translation_key`** — не проверял исходники `homeassistant-core` напрямую (модуль недоступен локально); опирался на (а) существующее использование `translation_key=` в `custom_components/houseplan/repairs.py:51` того же пакета для создания `broken_plan`-репаров, (б) зелёный Linux backend CI на этом же коде. Оснований сомневаться нет, но это не то же самое, что чтение исходников HA.
## Вердикт
Зелёный. Все 12 AC доказаны автотестами, которые проверены на способность падать (чтением конструкции или прогоном), либо прямым чтением кода с фиксацией «проверено чтением». Находок нет.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/423-v170-polish`, коммит `3576cc2ea7e5` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `856c13aff5985cd40bf743c00137911b055fd955`
```
git log --all --format='%H %T' | grep 856c13aff598
```
- ТЗ `docs/specs/423-v170-polish.md`, блоб `3a0e7b78a9f8a01f3f54e4a6182dba04badecfae`
```
git log --all --find-object=3a0e7b78a9f8a01f3f54e4a6182dba04badecfae -- docs/specs/423-v170-polish.md
```