Files
houseplan-card/docs/reviews/CODE-REVIEW-43-r2.md
T
2026-09-02 00:05:37 +00:00

207 lines
23 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-43-r2
- Issue: [#43](https://github.com/Matysh/houseplan-card/issues/43) — Диалог помощи и обратной связи с обезличенным support report
- Ветка: `issue/43-help-feedback`
- SHA ревью: `0e0ca3d4dc38851555a825b9481817642d8647a9`
- Заход: r2 (второй код-ревью) · блокирующих циклов израсходовано **1/4** (r1 был жёлтым — потратил цикл; зелёный вердикт цикла не образует, §4/#227)
- Вердикт: **зелёный** · High: 0 · Medium: 0
## 1. Скоуп раунда
Это второй заход, разбор — по дельте (§2.10 PROCESS.md). Предыдущий вердикт
(r1, жёлтый) получен на SHA `1ba5363038055b3f3442b62a761161d05c6d03c7`
(документ `docs/reviews/CODE-REVIEW-43-r1.md`, SHA явно назван в самом
документе). Дельта:
```
git diff 1ba53630..0e0ca3d4
```
3 коммита сверх r1: `ffddc383` (fix: satisfy validation gates), `40d63080`
(docs: review document — публикация r1, генерирует конвейер, не автор),
`0e0ca3d4` (test: cover help and feedback browser flows).
Продуктовый код изменён только в двух местах, и оба — рефакторинг типов без
изменения поведения:
- `src/houseplan-editor-runtime.ts` (+28/−17): `response: any` → `response: unknown`
с явным сужением через промежуточный `payload`-объект в `_buildSupportPreview`
и `_submitSupport`. Логика чтения полей (`text`, `size`, `token`, `sha256`,
`report_id`, …) идентична строка в строку — только тип входа сузился.
- `src/support-feedback.ts` (+4/−3): `options.language as any` → typed
`readonly string[]`, `(value as any).code` → `(value as {code?:unknown}).code`.
Тоже чистое сужение типа, не логики.
`custom_components/houseplan/**/*.py` в дельте не тронут вообще (проверено
`git diff --stat 1ba53630..HEAD -- custom_components/` — только сгенерированные
`frontend/**`). Единственная правка бэкенда — 158 новых строк тестов в
`tests_backend/test_support_package.py`, сам `support_package.py` не менялся.
Остальное — тесты и инфраструктура: новый `demo/smoke_support_feedback.mjs`
(270 строк), 6 golden-сцен в `demo/golden/matrix.mjs` + ветка `dialog === 'support'`
в `demo/golden/harness.mjs`, обновлённый `demo/smoke_general_settings.mjs`,
новый мутант в `scripts/mutation-gate.mjs`, обновлённый `docs/images/screenshots.json`
(отпечаток скриншотов).
Дельта локальна: контракт поведения не менялся, новая подсистема не задета,
объём (≈390 строк не считая бандла) на порядок меньше исходной задачи
(+7023/−411 в r1). Полный повторный разбор не требуется — §2.10.
## 2. Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **M1.** `check-docs` красный — устаревший `sourceFingerprint` скриншотов | `docs/images/screenshots.json` обновлён (`sourceFingerprint`/`sourceSha256` во всех сценах: `7faa6c2e…` → `7e950337…`) коммитом `ffddc383` | Лично прогнал `node scripts/check-docs.mjs` на HEAD `0e0ca3d4` → `Documentation checks passed (7 files, 10 external links)` |
| **M2.** Ни одного браузерного смока/golden-сцены для диалога Help & Feedback | Новый `demo/smoke_support_feedback.mjs` (режимы view/plan/devices/decor, kiosk, read-only, EN/RU, старый backend, validation/focus, preview/SHA/download exact bytes, success/429/timeout/unknown, retry с тем же idempotency key, 320×760 и 760×320, touch-target 44px) + 6 golden-сцен в `matrix.mjs` (`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`) + ветка `dialog === 'support'` в `demo/golden/harness.mjs`, оба коммитом `0e0ca3d4` | Лично прогнал `node demo/smoke_support_feedback.mjs` на свежем бандле — все 18 полей `true`, `OK`. Лично прогнал `node scripts/mutation-gate.mjs --id=support-timeout-claims-success` — `чистый прогон` + `тест покраснел, как обязан`, `поймано 1 из 1`: смок реально умеет падать на мутанте §14.4 (ложный success на timeout). Лично прогнал `node demo/golden/run.mjs --mode=capture --scenario=<каждая из 6>` — все 6 строятся без ошибки (`missing-baseline`, не `error`; baseline умышленно не принят до предрелизного визуального ревью, это по процессу, не пропуск). `test/golden-matrix.test.mjs` получил новый тест «issue 43 support dialog has the six reviewed responsive states», зелёный в `npm test` |
| **M3.** `no-new-any` красный — 4 новых `any` без обоснования | Все 4 типизированы (см. §1) коммитом `ffddc383` | Лично прогнал `node scripts/no-new-any.mjs --base origin/dev --head HEAD` на HEAD `0e0ca3d4` → `Новых any нет` |
Все три Medium закрыты предметно — не косметика вокруг находки, а именно то,
что было названо (актуальный отпечаток вместо любого другого фикса; смок и
golden именно для нового диалога, а не для соседней фичи; типизация именно тех
четырёх строк).
## 3. Унаследовано из r1
Без повторной проверки в этом раунде — код на этих участках дельта не
затронула, полный разбор см. `docs/reviews/CODE-REVIEW-43-r1.md`
(SHA `1ba53630`):
- Allowlist-проекция `support_package.py`: 8 `_project_*`-функций строят новый
`dict`, не мутируя исходник (§7.3). Сам файл в дельте не менялся —
дельта только добавила тесты поверх него (см. §5).
- Авторизация и владение токеном на всех трёх WS-командах
(`_check_write`/`may_write`, сверка `preview.get("owner")`).
- Транспорт: фиксированный HTTPS-хост, без редиректов, таймауты 5/20с, статус
мапится на закрытый список кодов до чтения тела.
- Relay `scripts/support-relay/**`: не менялся дельтой, 36/36 тестов,
инварианты против подмены `X-Forwarded-For` (живой пентест владельца при
ревью ТЗ r3-r5).
- «Одно число — один источник» для preview/download/submit (§2.6 r1) —
цепочка байт/хеш не менялась.
- Деградация на старом backend (`compatible = _haIntegrationVersion === CARD_VERSION`).
- AC15 (нулевое влияние на `diagnostics.py`/`import_export.py`/`validation.py`).
- Трейлеры и changelog коммита `1e2e0fa6` (Issue/User-Visible, оба changelog
в одном коммите) — не пересматривались, дельта changelog не трогает.
- Low-находки r1 (L1 единицы Caddyfile, L2 порядок проверки CPU/лимита, L3
`_support_repairs()` без sentinel-теста, L4 неиспользуемые i18n-ключи
`backup.error.support_*`) — сняты решением ревьюера r1 с записью, дельта их
не касается, повторно не поднимаю.
- Ограничение проверки бэкенда: полный HA-харнесс (`test_ha_websocket.py` и
т.д.) в r1 не поднимался (нет закреплённой версии Python 3.14/phcc в
песочнице ревьюера) — то же ограничение действует и здесь, оно не связано с
дельтой (AC9/AC10 остаются «проверено чтением», не автотестом с моей
стороны, как и в r1).
## 4. Как проверялось в этом раунде
Дешёвые гейты гоняются в каждом раунде (§2.10); прогнаны лично на HEAD
`0e0ca3d4`, зелёного Validate CI на этом SHA нет — обязанность ревьюера (см.
задание раунда).
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | зелёный, 0 ошибок |
| Юнит/интеграционные | `npm test` | 1725 тестов: 1724 passed, 1 skipped, 0 failed — совпадает с числом из хендоффа автора |
| Сборка + сверка бандла | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | идентичны |
| Три копии бандла | `npm run bundle:sync` | `git status` чист после — `dist`, `custom_components/.../frontend`, `demo/srv/assets` синхронны |
| Docs-контракт (диапазон меняет `src/**`) | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 10 external links)` — M1 закрыт |
| Новый `any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | `Новых any нет` — M3 закрыт |
| process-gate | `node scripts/process-gate.mjs --range origin/dev..HEAD --issues` | «гейт пройден, предупреждений 0», 18 коммитов |
| Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 6 изменённых файлов `src/**`, 37 «прямое совпадение»; `demo/smoke_support_feedback.mjs` назван прямым совпадением по символам `_closeSupportDialog, _config, _downloadSupportPreview, _editorRuntime, _haIntegrationVersion, _submitSupport` — ровно то, что и требовалось запустить |
| Целевой смок (новый, M2) | `node demo/smoke_support_feedback.mjs` | все 18 проверок `true`, `OK` |
| Целевой смок (изменённый, About moved out) | `node demo/smoke_general_settings.mjs` | все проверки `true`, `OK`, включая новую `aboutMovedOut: true` |
| Мутационный гейт нового смока | `node scripts/mutation-gate.mjs --id=support-timeout-claims-success` | `ok чистый прогон` + `ok тест покраснел, как обязан` → `поймано 1 из 1` — смок доказуемо умеет падать (дисциплина §2.7/§18) |
| Golden-сцены (существование/сборка) | `node demo/golden/run.mjs --mode=capture --scenario=<id>` для всех 6 новых сцен | все 6 строятся без ошибки (`missing-baseline`, не `error`) |
| Бэкенд-тесты дельты | `pip install pytest voluptuous` (без пина, как в r1) → `python -m pytest tests_backend/test_support_package.py -q` | `14 passed` (10 старых + 4 новых: reject non-mapping, rich-projection sentinel, projection helpers fail-closed, size-limit-after-projection) |
## 5. Разбор по AC, чьё доказательство дельта задевает
Обозначения как в r1: **PASS/тест** — автотест, умеющий падать (см. §4);
**PASS/чтение** — проверено чтением; **∅** — не переоценивался, см. §3.
| AC | Было (r1) | Стало (r2) | Почему изменилось |
|---|---|---|---|
| AC1 | PASS/чтение | **PASS/тест** | `smoke_support_feedback.mjs`: порядок кнопок (`afterSettings` = `.support-button` идёт после `mdi:cog-outline`), неизменность режима/zoom/выбора при открытии для всех 4 режимов, `kioskHidden`, `readonlyAbsent` — все `true` |
| AC2 | PASS/тест+чтение | PASS/тест+чтение (усилено) | Логика не менялась (тест `test/support-feedback.test.mjs` не в дельте); новый смок добавляет DOM-уровень: `aboutAndEnglishGuide`/`russianGuide`/`oldBackendDegrades` — `true`. `smoke_general_settings.mjs` подтверждает `aboutMovedOut: true` — блок не задвоен |
| AC3 | PASS/тест | PASS/тест (усилено) | `freshDefaults`, `freshAfterSuccess` — новый черновик пуст при каждом открытии/после успеха, чекбокс не сохраняется, теперь и на уровне браузера, не только unit |
| AC4 | PASS/тест | PASS/тест (усилено) | `openIsLocal`: открытие диалога не увеличивает счётчик `houseplan/support/*`-вызовов до включения чекбокса |
| AC5, AC9, AC10 | PASS/тест(+чтение) | ∅ (логика не менялась) | Единственная правка смежного кода — сужение `any → unknown` в `_buildSupportPreview`/`_submitSupport`; путь чтения полей (`payload.text`, `payload.token`, …) идентичен построчно старому `response?.text` и т.д. — проверил диффом, поведение при `response` не-объектом (`undefined`) то же самое (`{}` фоллбэк даёт те же `''`/`NaN`, что и `?.`) |
| AC6, AC7, AC8 | PASS/тест | ∅, но с дополнительным покрытием | `support_package.py` не менялся; `tests_backend/test_support_package.py` получил 4 новых теста, включая структурный `test_rich_plan_projection_preserves_safe_structure_and_drops_unknown_values` (сеет `poly`/`walls`/`markers`/`decor` с частично некорректными формами и секретными полями, проверяет точный спроецированный результат) — прогнал сам, 14/14 зелёных. Это усиление доказательства, не обязательное по находкам r1, но не создаёт риска: тесты только добавляют строгости |
| AC13 | PASS/чтение | **PASS/тест** | Смок покрывает success/`support_rate_limited`/`support_unavailable` (timeout)/`unknown_command`, ретрай с тем же `idempotencyKey` (4 запроса с одним и тем же ключом), сохранение черновика при ошибке. Мутационный гейт `support-timeout-claims-success` подтверждает, что смок красный на реальной регрессии — не просто зелёный тест-пустышка |
| AC14 | PASS/чтение, UNVERIFIED по заявленному способу | **PASS/тест** (touch/layout), golden-candidate (не baseline) | `touchTarget` (≥44×44), `noHorizontalOverflow`, `phonePortrait`/`phoneLandscape` (320×760, 760×320 без клиппинга) — реальный браузерный рендер, не чтение TS. 6 golden-сцен покрывают desktop/phone/tablet × light/dark по именам состояний из §14.2 ТЗ; baseline умышленно не принят (процесс требует полного Linux CI артефакта для приёмки — это предрелизный шаг, не код-ревью) |
| AC11, AC12/12a, AC15, AC16, AC17 | PASS | ∅ | Relay, backend-модули diagnostics/import_export/validation, i18n-независимость и lazy-импорт дельтой не задеты — код идентичен r1 |
## 6. Находки этого раунда
Ничего блокирующего. Одна наблюдение уровня Low, снимаю с записью, не
блокирует:
**L5 (новая, Low).** Старый `demo/smoke_general_settings.mjs` проверял в
реальном DOM точные атрибуты About-ссылок (`href`, `target=_blank`,
`rel=noopener`) и точный текст версии (`Houseplan Card v${BUNDLE_VERSION}`) —
эта проверка выпала при переносе About в новый диалог: замена,
`aboutAndEnglishGuide` в `smoke_support_feedback.mjs`, проверяет только
количество `.aboutlink` (`=== 2`) и суффикс `href` guide-ссылки, не точные
`href`/`target`/`rel` GitHub/Telegram-ссылок и не текст версии. Не блокирую:
разметка статична (`src/houseplan-editor-runtime.ts:9387-9397`, атрибуты
захардкожены в шаблоне), риск молчаливого дрейфа без сопутствующей правки кода
практически нулевой, а `test/support-feedback.test.mjs` (не в этой дельте)
уже проверяет текстом, что `gs.about_version` не задвоен. Стоит вернуть
точную проверку атрибутов при следующей правке этого смока, отдельного issue
не требует.
## 7. Что проверено и корректно (в дополнение к §3)
- Три Medium из r1 закрыты предметно, не декларативно — по каждой лично
воспроизвёл зелёный результат на HEAD этого раунда (см. таблицу §2 и §4).
- Новый смок доказуемо способен упасть: мутационный гейт `support-timeout-claims-success`
красит именно `demo/smoke_support_feedback.mjs`, а не абстрактный юнит —
снимает риск «зелёный тест ничего не проверяет», который прямо назван в
документации самого `mutation-gate.mjs`.
- Рефакторинг типов (`any` → `unknown`/сужение) не меняет поведение: логика
чтения полей идентична, `npm test` (1724/1724 незменившихся зелёных) и
целевые смоки подтверждают эквивалентность.
- Три копии бандла синхронны, `git status` чист после `bundle:sync` —
класс D не расходится с классом A.
- Трейлеры всех трёх коммитов дельты корректны (`Issue: #43`,
`User-Visible: no` — оправданно, видимого поведения дельта не меняет,
changelog не тронут).
- `process-gate --issues` зелёный, 0 предупреждений на 18 коммитах диапазона.
## 8. Чего не проверял и почему
- **Полный HA-харнесс** (`test_ha_websocket.py` целиком) — то же ограничение
песочницы, что и в r1 (нет закреплённой Python 3.14/`pytest-homeassistant-custom-component==0.13.357`);
дельта не меняет ни один файл, который этот харнесс покрывал бы иначе, чем
уже покрыто в r1. AC9/AC10 остаются на «проверено чтением».
- **`npm run invariants`** — диапазон не меняет геометрию/`layout`/толщину
стен ни в r1, ни в дельте; неприменимо, не пропуск.
- **`golden:verify`/приёмка эталонов** — 6 новых сцен собираются без ошибки,
но baseline не принимался: это предрелизный шаг (§8 PROCESS.md,
`npm run golden:accept -- --reviewed` только по полному Linux CI
артефакту), не обязанность код-ревью. Локальный `golden:verify` на всей
матрице не гонял — большинство существующих сцен в этой песочнице дают
`different` уже на `origin/dev` (другой Chromium/шрифты), это фон, не
регрессия дельты, и полный прогон — предрелизная, а не ревью-обязанность.
- **Полный набор `demo/smoke_*.mjs`** (213 файлов) — не оправдано: дельта не
меняет ничего, что затрагивало бы несвязанные поверхности; `smoke-select.mjs`
подтверждает совпадения только на общих символах для остальных 36 файлов
(типовой фон, тот же вывод, что и в r1).
- **Relay `scripts/support-relay/**`** — не в дельте, не перепрочитывал
повторно (r1 уже перечитал этот код заново как часть первого код-ревью, а
не принял на веру со спек-ревью).
## 9. Итог
Все три Medium-находки r1 закрыты предметно, каждая — воспроизведённым
зелёным результатом на HEAD этого раунда, а не заявлением автора. Дельта не
меняет продуктовую логику (только сужение типов и добавление тестов/смоков/
golden-сцен), поэтому полный повторный разбор всех 17 AC не требовался —
переоценены только AC1/AC2/AC3/AC4/AC13/AC14, чьё доказательство дельта
затронула, остальные унаследованы из r1 без изменений в коде, который их
доказывает. Новый браузерный смок дополнительно подтверждён мутационным
гейтом — это не пустой зелёный тест. High-находок нет, Medium-находок нет,
одно Low-наблюдение снято с записью в этом же документе.
Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0