Files
houseplan-card/docs/reviews/CODE-REVIEW-354-r2.md
T
2026-08-28 15:15:05 +00:00

168 lines
15 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-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-смок).