From 4808426369a71c77dd3260a0a9949033628d2be2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:15:05 +0000 Subject: [PATCH] docs: review document for #354 Issue: #354 User-Visible: no --- docs/reviews/CODE-REVIEW-354-r2.md | 167 +++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 docs/reviews/CODE-REVIEW-354-r2.md diff --git a/docs/reviews/CODE-REVIEW-354-r2.md b/docs/reviews/CODE-REVIEW-354-r2.md new file mode 100644 index 00000000..33968f9f --- /dev/null +++ b/docs/reviews/CODE-REVIEW-354-r2.md @@ -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-смок).