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

15 KiB
Raw Blame History

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