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