mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -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-смок).
|
||||
Reference in New Issue
Block a user