docs: review document for #354

Issue: #354
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-28 15:15:05 +00:00
parent c1356fae18
commit 4808426369
+167
View File
@@ -0,0 +1,167 @@
# CODE-REVIEW-354-r2
- **Issue:** https://github.com/Matysh/houseplan-card/issues/354
- **Этап:** код-ревью (PROCESS.md §2.7)
- **Заход:** r2 · блокирующих циклов израсходовано 1 из 2 (лёгкий трек, лимит 2)
- **SHA материала:** `c1356fae1845d04264ed79628c93d3f1598c449f` (`git rev-parse HEAD`)
- **SHA предыдущего раунда (r1):** `1397afefd5564abf76cb6ed87b9f5eaee074361b`
(не назван в r1-вердикте — см. документ CODE-REVIEW-354-r1.md, шапка)
- **Ветка:** `issue/354-language-runtime`
- **Вердикт: зелёный**
## Скоуп
Разбор по дельте (PROCESS.md §2.9): единственный новый коммит с момента
r1 — `c1356fae` («test: prove the failure delivery itself, not only the
unsubscription (#354 r1-M1)»), `git diff 1397afef..c1356fae`. Коммит написан
явно как правка находки r1-M1 (плюс попутно взята r1-L1) — продуктовый
контракт К1/К2/К3 не переоткрывается, ребейза на ушедший вперёд `dev` не
было (`1397afef` — прямой родитель `c1356fae`), новая подсистема не
задета. Дельта локальна — полный разбор не требуется.
Файлы дельты: `src/i18n/registry.ts` (13 строк), `test/i18n-runtime.test.mjs`
(23 строки), `scripts/mutation-gate.mjs` (новый мутант, 19 строк),
`docs/USER-GUIDE.md`/`docs/USER-GUIDE.ru.md` (по 1 предложению),
синхронизированные копии бандла (`dist/**`, `custom_components/houseplan/
frontend/**`) и `docs/images/screenshots.json` + `docs/images/06-device-editor.png`
(перегенерированы `check-docs` из-за смены `sourceFingerprint`, т.к.
`src/i18n/registry.ts` входит в `src/**`).
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **M1** (Medium) — юнит `subscribeLanguageLoadFailures` не проводил код через реальный fan-out, ловил только «отписка молчит» | Fan-out вынесен в экспортируемую `notifyLanguageLoadFailures(code)` (`src/i18n/registry.ts:58-64`); юнит теперь вызывает её напрямую и проверяет доставку двум живым подписчикам, молчание одного после отписки и полную тишину после отписки обоих | `test/i18n-runtime.test.mjs:173-191` (тело теста, см. «Как проверялось»); новый мутант `locale-failure-delivery-cut` в `scripts/mutation-gate.mjs:659-668` воспроизводит именно ту правку, которой r1-ревьюер поймал дыру, и красит юнит — прогнан мной лично, 1/1 |
| **L1** (Low) — `docs/USER-GUIDE.ru.md` не упоминал новый тост при отказе локали | Абзац о немецкой локали в обоих гайдах дополнен предложением о тосте | `docs/USER-GUIDE.md:149-153`, `docs/USER-GUIDE.ru.md:155-158` (см. диф `git diff 1397afef..c1356fae -- docs/USER-GUIDE.md docs/USER-GUIDE.ru.md`) |
Обе находки закрыты по существу, не текстом коммит-сообщения — проверено
чтением диффа и повторным прогоном нового мутанта.
## Унаследовано из r1
Код-контракты, которых дельта не касается, приняты из
`docs/reviews/CODE-REVIEW-354-r1.md` (SHA `1397afef`) без повторного
разбора, кроме точечных перепроверок ниже, которые дельта затронула
косвенно (общий прогон тестов/мутантов/смока и один точечный grep):
- **AC1/К1** (`LANGUAGE_RUNTIME` — экземпляр `LanguageRuntime`, рукописный
дубль удалён) — сама конструкция `new LanguageRuntime(...)` в дельте
переписана только в части четвёртого аргумента (инлайн-стрелка →
именованная функция), логика и итоговое поведение не изменились.
Перепроверено попутно: мутант `language-runtime-handwritten-duplicate`
прогнан мной повторно на `HEAD` — 1/1 поймана.
- **AC3/К3** (контракт-юнит + мутант на возврат дубля) — не в дельте,
перепроверено попутно тем же прогоном, зелёный.
- **К2-периметр** (только `houseplan-card.ts` подписывается и показывает
тост; `space-card.ts`, `editor.ts`, `space-editor.ts` — без
тост-инфраструктуры) — файлы вне дельты, не тронуты. Перепроверено
точечно: `grep -rn "notifyLanguageLoadFailures" src/` — единственное
использование внутри `registry.ts` (конструктор рантайма), периметр не
расширился.
- **i18n-ключ `toast.locale_load_failed`** и его тексты en/ru/de — вне
дельты, не тронуты; паритет-тест ключей локалей входит в общий прогон
`npm test`, прошёл в его составе.
- **Трейлеры и changelog за поведенческую часть** — уже приняты в r1 для
`1397afef`. У `c1356fae` — `Issue: #354`, `User-Visible: no`, что верно:
дельта не создаёт новое пользовательское поведение (тост уже был
User-Visible в `1397afef`), только выносит внутреннюю функцию для
тестируемости и правит документацию/тестовую инфраструктуру. Changelog
в `c1356fae` не тронут — и не должен быть.
## Как проверялось
Зелёного `Validate` на SHA `c1356fae` не найдено — все гейты прогнаны мной лично.
| Гейт | Результат | Команда |
|---|---|---|
| typecheck | зелёный, без вывода | `npx tsc --noEmit` |
| unit-тесты (полный набор) | зелёный, 1487 pass / 0 fail / 1 skip (тот же skip, что и в r1, не связан с #354) | `npm test` |
| build + сверка 3 копий бандла | зелёный, `git status --porcelain` пуст после `npm run build && npm run bundle:sync` | `npm run build && npm run bundle:sync` |
| docs-гейт (скриншоты/фингерпринт) | зелёный, «Documentation checks passed (7 files, 10 external links)» — фингерпринт уже пересчитан автором в самом коммите | `node scripts/check-docs.mjs` |
| bundle:budget | зелёный, справочно, 257212 B < 282000 B | `npm run bundle:budget` |
| provenance (трейлеры) | зелёный | `node scripts/validate-commit-provenance.mjs` |
| мутант М1-фикса `locale-failure-delivery-cut` | поймана 1/1, «тест покраснел, как обязан» | `node scripts/mutation-gate.mjs --id=locale-failure-delivery-cut` |
| мутант К1/К3 `language-runtime-handwritten-duplicate` (наследуется, перепроверен попутно) | поймана 1/1 | `node scripts/mutation-gate.mjs --id=language-runtime-handwritten-duplicate` |
| мутант К2 `locale-failure-toast-dropped` (наследуется, перепроверен попутно) | поймана 1/1 | `node scripts/mutation-gate.mjs --id=locale-failure-toast-dropped` |
| `demo/smoke_german_locale.mjs` (полный прогон) | зелёный, все 9 проверок `true`, включая `failureShowsToast` | `node demo/smoke_german_locale.mjs` |
| `node scripts/smoke-select.mjs --base 1397afef --head HEAD` | см. ниже | — |
**smoke-select:** дельта касается 1 файла `src/**`, 2 изменённых символа
проекта (`languageLoadFailureListeners`, `notifyLanguageLoadFailures`) — оба
не встречаются ни в одном из 198 смоков, инструмент вернул
**НЕОПРЕДЕЛЁННОСТЬ**. Решение: новый браузерный смок не нужен. Оба символа —
чисто внутренняя разводка (замыкание вынесено в именованную функцию для
юнит-тестируемости), наблюдаемое поведение через них не меняется — оно уже
доказано `demo/smoke_german_locale.mjs` (`failureShowsToast`), который
является зарегистрированной связью для контракта К2 и прогнан целиком
выше, зелёный.
## Находки
Нет. Обе находки r1 закрыты по существу (см. «Закрытие раунда r1»), новых
дефектов дельта не вносит.
## Что проверено и корректно
- **AC4, часть 2 (юнит на `subscribeLanguageLoadFailures`)** — ранее
единственный незакрытый пункт АС4. Текст ТЗ требует «подписка получает
код, отписка работает»: новый тест (`test/i18n-runtime.test.mjs:173-191`)
вызывает `notifyLanguageLoadFailures('de')` и проверяет, что оба живых
подписчика получают код (`['de', '2:de']`), что отписанный слушатель
замолкает, а второй продолжает получать (`['de', '2:de', '2:de']`), и что
после отписки обоих доставок больше нет. Проверено автотестом — сам
прогнал `node --test --test-name-pattern="#354" test/i18n-runtime.test.mjs`
внутри `mutation-gate` (гейт печатает «чистый прогон» перед мутацией) —
зелёный. Adversarial-часть: мутант `locale-failure-delivery-cut` заменяет
ровно ту строку `for (const listener of languageLoadFailureListeners)
listener(code);`, которой в r1 я вручную ломал доставку и юниты этого не
замечали — теперь красит юнит (1/1, воспроизведено мной запуском
`node scripts/mutation-gate.mjs --id=locale-failure-delivery-cut`).
Дисциплина «тест умеет падать» соблюдена не на словах автора, а моим
собственным прогоном.
- **Контракт-юнит теперь пиннит и саму разводку**: новая ассерция в
`test/i18n-runtime.test.mjs:146-150` матчит источник на
`new LanguageRuntime(LANGUAGE_REGISTRY, BUILD_FINGERPRINT, console.warn,
notifyLanguageLoadFailures,)` — если кто-то вернёт инлайн-стрелку без
фан-аута наружу, юнит покраснеет раньше мутационного гейта. Проверено
чтением и совпадением с текущим `src/i18n/registry.ts:73-79`.
- **L1-фикс не переизобретает формулировку**: оба абзаца
(`docs/USER-GUIDE.md:149-153`, `docs/USER-GUIDE.ru.md:155-158`) описывают
тот же сценарий («обе попытки не удались») и добавляют одно предложение
про тост, не меняя остального текста — проверено построчным диффом.
- **User-Visible: no обоснован по существу**, не только по трейлеру: дельта
не меняет ни один пользовательский артефакт (текст тоста, ключи локалей,
условия его появления) — только внутреннее имя функции и тестовую/
документационную обвязку. Changelog оставлен нетронутым правомерно.
- **Генерируемые копии бандла**: после `npm run build && npm run
bundle:sync` рабочее дерево чистое — три копии синхронны с
закоммиченным состоянием на `HEAD`.
- **Регенерация docs-фингерпринта корректна**: `sourceFingerprint` в
`docs/images/screenshots.json` пересчитан автором в том же коммите
(потому что `src/i18n/registry.ts` входит в покрытие отпечатка) и
`check-docs.mjs` подтверждает его актуальность — не унаследованный
зелёный статус из r1, а новый, пересчитанный под `c1356fae`.
- **«Одно число — один источник»**: дельта не добавляет и не меняет ни
одну пользователю видимую величину (тот же текст тоста, что и в r1) —
правило не применимо.
## Чего не проверял
- `npm run golden:verify` — дельта не меняет геометрию/рендер/стили/слои
(внутренняя разводка i18n + два предложения в докс); решение, не
пропуск.
- `python -m pytest tests_backend` — ни один `custom_components/**/*.py` не
тронут ни в r1, ни в этой дельте.
- `npm run invariants` — геометрия, `layout`, `marker.space`, `open_spans`
не затронуты.
- Performance-профили — не названы в AC, чувствительные к перфу пути не
задеты.
- Новый браузерный смок под `languageLoadFailureListeners`/
`notifyLanguageLoadFailures` — не создавался; см. обоснование в разделе
«Как проверялось» (smoke-select → НЕОПРЕДЕЛЁННОСТЬ, поведение уже
доказано существующим `demo/smoke_german_locale.mjs`).
- 5 из 6 «прямых совпадений» `smoke-select` по символу `_showToast` из
r1 — вне дельты этого раунда, не перепрогонялись повторно (тот же метод
и там не менялся).
- Ручной просмотр карточки в живом браузере HA — только автоматические
прогоны (unit, mutation-gate, Playwright-смок).