docs: review document for #406

Issue: #406
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-01 16:32:25 +00:00
parent 269d828dd4
commit 77a22efc78
+233
View File
@@ -0,0 +1,233 @@
# CODE-REVIEW-406-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/406
- Этап: code (PROCESS.md §2.7)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4 (перед этим раундом)
- Ветка: `issue/406-beta2-polish`
- Коммит на ревью: `fe385cbf9d5a0fc11f0bcfacbe4ba03e01167949`
- ТЗ: `docs/specs/406-beta2-polish.md`, revision 4, зелёный `SPEC-REVIEW-406-r4`
- База сравнения: `origin/dev` = `b04387ce914aa8559fb3bc446f99a5751f67f32c`
## Скоуп
Четыре независимые правки из ТЗ:
(а) удаление 13 мёртвых ключей i18n + AST-гейт `test/i18n-dead-keys.test.mjs`;
(б) `role="alertdialog"` + `aria-describedby` для `hp-confirm` (`destructive` и
`warning`), обычные диалоги остаются на HA-ветке (`src/hp-dialog.ts`,
`src/hp-confirm.ts`);
(в) смок `demo/smoke_danger_confirm_branches.mjs` покрывает и HA-, и
нативную ветку;
(г) уборка `marker_area_snapshot` для исчезнувших устройств только при
авторитетном реестре + разворот правила усечения по лимиту
(`src/device-area-relocation.ts`).
Материал ревью — `git log --oneline origin/dev..HEAD` (9 коммитов, из них 5
`docs: review document for #406`/`docs: resolve...`/`docs: revise...` — все
имеют отношение к ТЗ, не к коду) и `git diff origin/dev...HEAD` (42 файла:
продуктовый код, тесты, смоки, мутанты, i18n, бандлы, changelog).
## Как проверялось
Ручного тестирования в цикле нет — правильность работы устанавливается
чтением кода, автотестами и браузерными смоками.
**Прогнано лично на `fe385cbf` (зелёного Validate на этом SHA нет):**
| Гейт | Результат |
|---|---|
| `npx tsc --noEmit` | green, без вывода |
| `npm test` | 1706 passed, 0 failed, 1 skipped — совпадает с заявкой автора |
| `npm run build` | green, `dist` собран за 16.3s |
| `node scripts/bundle-sync.mjs --check` (после build) | green, три копии бандла синхронны |
| `npm run bundle:budget` | initial 287320 B / budget 300000 B, headroom 12680 B (>0 → AC12 выполнен); есть предупреждение о низком запасе — это фон #367, не регрессия этой задачи |
| `node scripts/mutation-gate.mjs --check` | все мутанты, включая три новых (`i18n-dead-key-returns`, `confirm-dialog-loses-alertdialog`, `area-snapshot-cleanup-ignores-authority`), — `ok` |
| `node scripts/check-docs.mjs` | **RED** — см. находку ниже |
| `node demo/smoke_danger_confirm_branches.mjs` | green, все 24 поля `true` (AC6–AC8) |
| `node demo/smoke_area_relocation.mjs` | green, все 20 полей `true`, включая `authoritativeOrphanRemoved`/`nonAuthoritativeOrphanPreserved` (AC9–AC10) |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «НЕОПРЕДЕЛЁННОСТЬ»: изменённых символов проекта — 6 (`MARKER_AREA_SNAPSHOT_LIMIT`, `MarkerAreaBinding`, `_useHaDialog`, `_usesHaDialog`, `validBinding`, `validText`), матрица — 212 смоков, ни один не назван связанным доказуемо |
**Не прогонял и почему:**
- Полная матрица `demo/smoke_*.mjs` (212 файлов) — диапазон затрагивает
только `hp-dialog`/`hp-confirm` и `device-area-relocation`, у обоих есть
прямо названные в AC6–AC10 смоки, которые я прогнал. Символы из вывода
`smoke-select` — внутренние помощники (`_usesHaDialog`, `validBinding`,
`MARKER_AREA_SNAPSHOT_LIMIT`), уже покрытые юнитами
`test/device-area-relocation.test.mjs` построчно (проверено чтением: тест
на переполнение лимита обращается к `MARKER_AREA_SNAPSHOT_LIMIT` напрямую).
Полный прогон матрицы — предрелизная обязанность (§8), не гейт этого раунда.
- `npm run golden:verify` — прочитал `demo/golden/matrix.mjs`: ни один
сценарий не открывает destructive/warning `hp-confirm` (есть только
`device-dialog-*`, `decor-color`, `optimize-preflight-dialog` — другие
диалоги). Видимый рендер, который затрагивает диапазон, в голден-матрице не
зафиксирован — прогон не даст сигнала по этой задаче.
- `npm run invariants -- --config …` — диапазон не трогает рёбра комнат,
записи толщины, `layout`-формат, `marker.space` или `open_spans`;
`device-area-relocation.ts` меняет только момент уборки записи снапшота и
порядок среза при переполнении, не геометрию. Не применимо.
- `python -m pytest tests_backend -q` — `custom_components/**/*.py` в диффе
нет (только скомпилированные frontend-бандлы).
- Перфоманс-профили — не названы в AC, чувствительные к перфу пути не
тронуты.
## Находки
### Medium (в скоупе, чинится в этой же задаче) — стал устаревшим отпечаток скриншотов документации
`node scripts/check-docs.mjs` красный на `fe385cbf`:
```
ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs
```
Воспроизведение (после `npm run build`, который уже входит в проверки
автора):
```
$ node scripts/check-docs.mjs
ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs
```
Причина механическая и ожидаемая: `visualFingerprint(ROOT)`
(`scripts/source-fingerprint.mjs`) считается по всему `src/**`, а диапазон
меняет `src/hp-dialog.ts`, `src/hp-confirm.ts`, `src/device-area-relocation.ts`
и все четыре словаря `src/i18n/*.json` — то есть отпечаток обязан был
измениться, и `docs/images/screenshots.json` обязан был обновиться вместе с
ним, независимо от того, меняются ли реально показанные пиксели. Я
дополнительно прогнал `node demo/docs/capture.mjs` во временной копии рабочей
дерева и подтвердил: все 10 сценариев (`view-desktop`, `view-touch`,
`space-create`, `room-contour-close`, `plan-context-tray`, `device-editor`,
`device-display-preview`, `background-editor`, `room-card`, `device-info`) —
из `demo/docs/screenshots.mjs`, ни один не открывает destructive/warning
`hp-confirm`, так что заявление автора «скриншоты не меняются» по существу,
вероятно, верно (файлы отличались побайтово в моей проверке, но это из-за
отсутствующего в песочнице `oxipng`, не из-за иного содержимого кадра — само
сообщение инструмента `oxipng не найден: кадры пишутся как есть, без
перепаковки` называет причину; я откатил эту пробную капчу перед
продолжением ревью). Но идентичность пикселей не отменяет обязательности
шага: `docs/images/screenshots.json` в диффе не тронут вообще
(`git diff origin/dev...HEAD -- docs/images/` — пусто), а `check-docs.mjs`
проверяет именно факт совпадения записанного отпечатка с текущим деревом, не
факт визуального совпадения.
Это ровно тот класс регрессии, который уже дважды оставлял `dev` с красным
job `docs` до следующей задачи (#230, #234, #237): гейт красный на коммите,
который автор передаёт на ревью, и останется красным после мержа, если его
не прогнать перед мержем.
**Ожидаемо**: `npm run build && node demo/docs/capture.mjs && npm run
docs:accept` (или эквивалент), обновлённые `docs/images/screenshots.json` и
(если байты правда меняются вне зависимости от oxipng) PNG — в том же PR.
## Что проверено и корректно
- **AC1–AC3** (мёртвые ключи, гейт, законные динамические/производные
семейства). `test/i18n-dead-keys.test.mjs` строит множество через реальный
TS AST (`ts.createSourceFile` по каждому файлу `src/**/*.ts`): литералы,
шаблонные выражения с горизонтом `+`/`` ` ``, и производное правило
`_help('x.help')` → `x.help.aria`. Второй тест в файле жёстко фиксирует
`derivedHelpAria.size === 19` — то есть падает, если производных пар станет
больше или меньше 19, а не просто «не меньше». `src/i18n/en.json` до/после:
13 ключей удалены (`history.partition_add`, `confirm.delete_draft`,
`confirm.delete_draft_segment`, `title.markup`, `history.delete_room`,
`markup.delete`, `marker.display_hint`, `marker.display_hint_icon`,
`confirm.delete_room`, `confirm.remove_marker`, `confirm.delete_space`,
`confirm.unlock`, `confirm.delete_plan`) — совпадает построчно со списком
ТЗ. `npm test` включает оба теста файла и прошёл.
- **AC4** (коллизия с `test/unified-wall-tool-source.test.mjs`). Строка
`'history.partition_add'` убрана из проверяемого списка теста тем же
коммитом, что и ключ из словарей — оба гейта зелёные одновременно
(проверено прогоном `npm test`).
- **AC5** (паритет словарей). Диф по `de.json`/`fr.json`/`ru.json` даёт ту же
тринадцатку удалённых строк, что и `en.json` (сверил `git diff` по каждому
файлу построчно) — синхронно, 52 строки суммарно. `test/i18n.test.mjs`
входит в `npm test` и прошёл.
- **AC6/AC7** (роль и `aria-describedby`). `src/hp-confirm.ts`: оба `kind`
(`destructive` и `warning`) получают `.alert=${true}` безусловно —
дихотомии по виду нет, что и требовал r1/r2 SPEC-REVIEW. `src/hp-dialog.ts`
добавляет `alert`/`describedBy` как реактивные свойства,
`_usesHaDialog()` возвращает `false`, когда `alert` истинен, — то есть
alert-подтверждение принудительно идёт в нативную ветку независимо от
`customElements.get('ha-dialog')`; нативный `<dialog>` получает
`role=${this.alert ? 'alertdialog' : 'dialog'}` и
`aria-describedby=${this.describedBy || nothing}`. Проверил по всему `src/`
(`grep -n '\.alert='`), что `.alert` выставляет только `hp-confirm.ts` —
ни один из ~30 других вызовов `<hp-dialog>` (маркер, калибровка,
бэкдроп, импорт/экспорт, kiosk, слияние комнат и т.д.) не трогает
`alert`/`describedBy`, то есть остаются обычным `role="dialog"` на
HA-ветке. `demo/smoke_danger_confirm_branches.mjs` — green, поля
`noHaDestructiveIsDescribedAlert`/`noHaWarningIsDescribedAlert`/
`haDestructiveStaysNativeAlert`/`haWarningStaysNativeAlert` истинны;
последняя пара доказывает именно то, что было провалом в r2 SPEC-REVIEW —
alert не уходит в зарегистрированный `ha-dialog`. Отдельный `getByRole`
на реальном accessibility-дереве Chromium (`realAccessibilityTreeIncludesConsequence`)
подтверждает, что текст последствий действительно долетает до дерева
доступности, а не только до DOM-атрибута.
- **AC8** (инварианты подтверждения в обоих окружениях). Тот же смок:
фокус на «Отмена» (`noHa*FocusesCancel`/`ha*FocusesCancel`), Esc → `false`
(`noHaWarningEscapeResolvesFalse`/`haWarningEscapeResolvesFalse`), клик по
«Отмена» → `false` в обоих окружениях — все `true`.
- **AC9/AC10** (уборка снапшота только при авторитетном реестре).
`resolveDeviceAreaRelocations` строит `liveIds`/`liveBindings` из
`options.devices` и помечает `removeSnapshot: true` для записей снапшота,
чей id и binding не совпадают ни с одним живым устройством/маркером, — но
делает это только после `if (!options.authoritative) return ...` в начале
функции, то есть цикл недостижим при неавторитетном реестре. Проверил
обоих вызывающих в продукте: `houseplan-card.ts:5054/5166` передаёт
`devices: this._devices` — полный список, только что пересобранный
`buildDevices(...)` из актуального `registry`/`markers`/`settings`, а не
отфильтрованное подмножество; `_syncAreaRelocations` (запись) вызывается
только при `this._haRegistry.authoritative`. Читающие проекции
(`space-render.ts:283`, `space-card.ts:455`) используют только
`.relocateIds` и никогда не вызывают `applyAreaRelocationResolution` — то
есть read-only карточка не может стереть запись сама
(`staticMadeNoWrites: true` в смоке это подтверждает). Юниты
`test/device-area-relocation.test.mjs` («authoritative registry removes
orphan snapshots but preserves live marker ids» / «non-authoritative
registry preserves orphan snapshots») и смок
`demo/smoke_area_relocation.mjs` (`authoritativeOrphanRemoved` /
`nonAuthoritativeOrphanPreserved`) оба зелёные. Мутант
`area-snapshot-cleanup-ignores-authority` (снимает условие `authoritative`)
ловится именно этим юнитом — проверил прогоном `mutation-gate.mjs --check`.
- **AC11** (усечение снапшота сохраняет последние записи). Один символ
правки: `.slice(0, MARKER_AREA_SNAPSHOT_LIMIT)` →
`.slice(-MARKER_AREA_SNAPSHOT_LIMIT)`. Новый юнит-тест строит вход
`LIMIT + 2` записей и проверяет, что первые две (`entry-0`, `entry-1`)
исчезли, а последняя (`entry-${LIMIT+1}`) осталась, — тест умеет падать:
на старом `.slice(0, LIMIT)` он утверждал бы обратное и провалился бы.
- **AC12** (бюджет initial). `npm run bundle:budget` → 287320 B при бюджете
300000 B, положительный запас — не растёт (совпадает с ожиданием ТЗ:
удаление 52 строк уменьшает бюджет, атрибуты добавляют единицы байт).
- **Трейлеры и changelog**. Коммит `fe385cbf`: `Issue: #406`,
`User-Visible: yes`. `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` получили по
одному пункту про переход на нативную оболочку и `alertdialog` — в том же
коммите, что и код (проверено `git show --stat fe385cbf`).
- **Одно число — один источник**. Диапазон не добавляет и не меняет ни одной
видимой пользователю величины (роль/aria — не число; лимит `20_000` не
показывается пользователю ни в каком UI) — раздел неприменим по существу,
а не пропущен. `test/single-source-numbers.test.mjs` входит в `npm test` и
прошёл.
## Чего не проверял
- Полную матрицу `demo/smoke_*.mjs` (212 файлов) — обоснование в разделе
«Как проверялось».
- `npm run golden:verify` — ни один голден-сценарий не открывает
затронутый диалог; обоснование там же.
- `npm run invariants` и `python -m pytest tests_backend` — диапазон их не
задевает (геометрия/толщина/`layout` не тронуты; `.py`-файлов в диффе нет).
- Перфоманс-профили — не названы в AC, не тронуты.
- Реальный визуальный вид нативного диалога в живом Home Assistant (смена
HA-хрома на нативную оболочку, которую ТЗ прямо называет видимым
компромиссом) — только по коду и accessibility-дереву Chromium; ручного
просмотра в настоящем HA в этом цикле нет и не может быть.
## Вердикт
Единственная находка — Medium, в скоупе задачи (устаревший отпечаток
`docs/images/screenshots.json` после правок в `src/**`), чинится тем же
шагом, что автор уже применяет в релизном процессе. Все 12 AC доказаны
тестом или смоком, который я лично прогнал и который умеет падать (мутанты
подтверждают это для трёх ключевых контрактов). High-находок нет.
**Жёлтый.**