Files
houseplan-card/docs/reviews/CODE-REVIEW-371-r2.md
T
2026-08-29 10:52:27 +00:00

19 KiB

CODE-REVIEW-371-r2

Issue: #371 — Add French localization files Заход: r2 · блокирующих циклов израсходовано 0 из 2 (зелёный вердикт цикла не образует, #227) Материал ревью: 1cd84dd0 (HEAD ветки issue/371-french-locale на момент разбора; git merge-base origin/dev HEAD == origin/dev, т.е. ветка полностью содержит dev, дальнейший ребейз не требуется) Ревьюер: Claude (код-ревью, свежая сессия, без контекста реализации)

Почему разбор полный, а не по дельте

r1 код-ревью было зелёным (High: 0 · Medium: 0, документ docs/reviews/CODE-REVIEW-371-r1.md, материал — SHA 3c19ba18, названный не в самом вердикте, а в следующем комментарии автора; отсутствие SHA в тексте вердикта — процессное наблюдение, см. ниже). Слияние этого зелёного вердикта не удалось: пока шло ревью, dev продвинулся на 6 коммитов, и конвейер вернул задачу в S6-in-progress не как правку по находке, а как чистый ребейз (комментарий от 2026-08-29T10:35:47Z). Автор перебазировал ветку на origin/dev, разрешив конфликты в бандл-деревьях (bundle:sync), changelog’ах (union) и скриншотах (demo/docs/capture.mjs), и вернул S7-code-review.

По PROCESS.md §7.2/§10.4 это ровно тот случай, когда «разбор остаётся полным»: ребейз на ушедший вперёд dev — другой код, а не локальная дельта. Поэтому этот документ — не сокращённый разбор по дельте r1→r2, а полный независимый код-ревью текущего дерева, как будто это первый заход. Разделы «Закрытие раунда r1» и «Унаследовано из r1» ниже присутствуют по формату r2+, но содержательно короткие: r1 не оставил ни одной находки для закрытия, и ничего не принято без собственной перепроверки в этом раунде.

Скоуп диффа

git diff origin/dev...HEAD — 45 файлов, класс A: src/i18n/fr.json (новый, 1128 ключей), src/i18n/fr.ts (новый, клон de.ts), src/i18n/registry.ts (+loadFrench, +FRENCH_RETRY_ASSET, +запись реестра), custom_components/houseplan/translations/fr.json (новый). Класс B: scripts/bundle-manifest.mjs (locale-role/localeRoots/retry-guard/ entryFallbackPlugin обобщены на fr), scripts/mutation-gate.mjs (новый мутант french-locale-wrong-dictionary), scripts/smoke-links.mjs (smoke_french_locale.mjs добавлен в существующую запись локалей), demo/smoke_french_locale.mjs (новый), demo/smoke_entry_stale.mjs (fr-прогон), test/i18n.test.mjs (+резолвер fr-профилей, +2 French-персональных теста), test/bundle-assets.test.mjs (fr-ветка retry-guard теста). Класс C: docs/USER-GUIDE(.ru).md, docs/CHANGELOG(.ru).md, docs/images/* + screenshots.json (пересъёмка отпечатка после ребейза). Класс D: dist/**, custom_components/houseplan/frontend/** — байт-в-байт пересобраны.

Геометрия, layout, marker.space, open_spans, толщина стен — не затронуты. Python-код (custom_components/houseplan/**/*.py) — не затронут, только новый JSON перевода. Видимый рендер/геометрия/стили карточки — не меняются (добавляется только выбираемый язык интерфейса).

Гейты — прогнаны лично на SHA 1cd84dd0

Гейт Команда Результат
typecheck npx tsc --noEmit зелёный, без вывода
unit npm test 1546 tests, 1545 pass, 0 fail, 1 skipped — совпадает с заявленным автором
build + бандл npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js сборка зелёная, cmp без вывода (побайтовое совпадение)
bundle:budget npm run bundle:budget initial 273 927 / 300 000 Б gzip, lazy locale 45 200 Б — совпадает с хендоффом
check-docs node scripts/check-docs.mjs (обязателен — diff трогает src/**) «Documentation checks passed (7 files, 10 external links)»
no-new-any node scripts/no-new-any.mjs --base origin/dev --head HEAD «Новых any нет» (15 добавленных строк в 2 файлах)
smoke-select node scripts/smoke-select.mjs --base origin/dev --head HEAD НЕОПРЕДЕЛЁННОСТЬ по FRENCH_RETRY_ASSET, LazyLanguageModule, loadFrench — разбор ниже
смок AC3 (прямое совпадение) node demo/smoke_french_locale.mjs (после npm run bundle:sync — первый прогон без свежего стенда падал по стухшему бандлу, ожидаемо) все 5 проверок true, OK
смок (зарегистрированная связь) node demo/smoke_entry_stale.mjs OK, включая frenchMessageVisible
смок (регресс на общем de/fr коде) node demo/smoke_german_locale.mjs OK, все 9 проверок true
мутант AC5 node scripts/mutation-gate.mjs --id=french-locale-wrong-dictionary (целевой прогон одного мутанта, не полный дорогой набор) «поймано 1 из 1»: чистый прогон смока зелёный, мутант красит smoke_french_locale.mjs, как обязан
inventory npm run inventory 1546 unit / 213 pure-backend / 150 HA-harness / 203 browser-smoke — счётчики не переписывались вручную

Разбор smoke-select: инструмент не находит FRENCH_RETRY_ASSET, LazyLanguageModule, loadFrench ни в одном смоке текстовым совпадением. Это не новый пробел: German-аналоги (GERMAN_RETRY_ASSET, loadGerman) точно так же не перечислены в scripts/smoke-links.mjs — там регистрируются символы рантайм-уровня (LanguageRuntime, dictionaryFor, ensureLanguage), а не внутренние функции-загрузчики реестра. Автор корректно расширил ровно ту же запись, добавив smoke_french_locale.mjs вторым смоком (диф scripts/smoke-links.mjs), — это «зарегистрированная связь» в терминологии инструмента, и она уже покрывает loadFrench/FRENCH_RETRY_ASSET косвенно: я прогнал smoke_french_locale.mjs напрямую (см. таблицу) и он проверяет именно этот путь (fr-чанк грузится ровно раз, _t отдаёт французский текст). Не гейт, а решение ревьюера засчитать связь достаточной без правки реестра.

Не прогонялось и почему:

  • python -m pytest tests_backend -q — ни один .py-файл не тронут, только custom_components/houseplan/translations/fr.json (не код);
  • node scripts/model-invariants.mjs — геометрия/layout/толщина стен/ marker.space/open_spans не затронуты этим диффом;
  • npm run golden:verify — видимый рендер карточки не меняется (новый язык — не новый визуал; геометрия и стили не тронуты);
  • performance-профили — не названы в AC, чувствительные к перфу пути (canvas, layout) не тронуты;
  • полный ls demo/smoke_*.mjs (203 файла) — диапазон диффа локален (i18n/manifest/registry), прогон всей матрицы это предрелизный гейт (§8), не гейт ревью; выбраны прямое совпадение + зарегистрированная связь + один смежный смок вручную (регресс на общем German/French коде манифеста), как и в r1.

Одно число — один источник. Дифф не добавляет и не меняет ни одной пользовательской величины (только текстовые строки интерфейса и служебные токены сборки) — проверка неприменима, новых чисел для сверки нет.

Проверка AC (по тексту ТЗ ревизии 2, комментарий S3→S4)

  • AC1 (паритет словаря + French-персональные тесты): проверено независимым скриптом, не доверяя заявлению автора — en.json/fr.json: 1128/1128 ключей, 0 пустых, 0 расхождений плейсхолдеров, порядок ключей идентичен. test/i18n.test.mjs содержит французский глоссарий (btn.save, space.header, opening.passage и др.) и скан на кириллицу/сентинелы + allow-list из 22 омографов — прогнан npm test, зелёный. Точечно перепроверил 5 записей allow-list (color_picker.saturation, gs.unit_m, wallthick.unit_cm, furn.cat_bidet, run.script) — все действительно идентичны en (единицы измерения/бренд-термины), не забытый перевод. Доказано автотестом, тест умеет падать (мутант ниже красит именно этот контракт на уровне смока, юнит-паритет красится удалением ключа — проверено чтением структуры теста, не отдельным прогоном мутации паритета).
  • AC2 (реестр): test/i18n.test.mjs содержит resolveLanguageCode для fr-FR/fr-CA/fr_BE/fr-CH, langOf для fr-CA — прогнано npm test, зелёное. LANGUAGE_REGISTRY — ровно en/ru/de/fr (прочитано в src/i18n/registry.ts). Доказано автотестом.
  • AC3 (смок реальным бандлом): demo/smoke_french_locale.mjs прогнан лично на свежем bundle:sync — fr-CA → французский, btn.save→Enregistrer, lang-атрибут fr, fr-чанк запрошен ровно 1 раз, initial-граф без fr, lazy-граф с fr. Доказано автотестом, прогнан лично.
  • AC4 (бюджет): npm run bundle:budget — fr в lazyLocaleFiles, initial 273 927 Б (в пределах 300 000, +0,5 КБ к строке дифф-хендоффа автора — различие в пределах шума пересборки). Доказано прогоном.
  • AC5 (мутант): french-locale-wrong-dictionary в scripts/mutation-gate.mjs (замена import('./fr') на возврат английского словаря при живом импорте — вторая версия мутанта после того, как первая ломала саму сборку, а не смок, что зафиксировано в коммитах 7f3e67bc/ c7dfd712). Прогнан лично целевым --id=french-locale-wrong-dictionary: «поймано 1 из 1», тест умеет падать — не голословное заявление автора.

entryFallbackPlugin (М1 из ревью ТЗ r1) и French-персональные тесты (М2 из ревью ТЗ r1) — обе спек-находки закрыты в коде, проверено чтением (scripts/bundle-manifest.mjs:224-225 — французская ветка ternary с текстом «House Plan a été mis à jour…», плюс fr-прогон в demo/smoke_entry_stale.mjs, прогнан лично, зелёный).

Закрытие раунда r1

r1 (код-этап) — зелёный вердикт, High: 0, Medium: 0. Находок, которые требовалось бы закрывать в r2, у r1 не было; таблица «находка → чем закрыта» пуста по существу. Единственное процессное наблюдение к r1: вердикт-комментарий не назвал SHA материала (см. ниже) — это не находка по коду, а пробел в оформлении вердикта, не влияющий на итог, поскольку код перепроверен заново в этом раунде целиком.

Унаследовано из r1

Ничего не принято без собственной перепроверки. Причина — не сокращение объёма по §2.10 (обычная делегированная дельта), а прямое указание процесса: ребейз на ушедший вперёд dev делает материал другим кодом (§7.2), поэтому весь список AC, все гейты и все находки ревью ТЗ перепроверены заново в этом документе (таблица гейтов и раздел AC выше), а не сославшись на docs/reviews/CODE-REVIEW-371-r1.md (SHA 3c19ba18, предребейзный, самим вердиктом не назван — восстановлен по комментарию автора от 2026-08-29T10:35:47Z «Материал ревью — 3c19ba18»).

Находки

Нет находок High. Нет находок Medium — ни в скоупе, ни вне его.

Low (процессное наблюдение, не по коду задачи): вердикт код-ревью r1 не назвал SHA материала прямо в тексте комментария (требование §2.10/#312 — «SHA в вердикте не назван — это находка»). SHA восстановим по соседнему комментарию автора того же раунда, поэтому не блокирует и не искажает вывод: полный повторный разбор в r2 из-за ребейза делает вопрос неактуальным для самой задачи #371. Снимаю без правки — адресовать в механике публикации вердиктов, а не в этой задаче (чужой скоуп: инфраструктура ревью, не код French-локали).

Что проверено и корректно

  • Полный паритет словарей (фронт 1128/1128, бэкенд 6/6), без пустых значений, без расхождений плейсхолдеров, порядок ключей = en.json — перепроверено независимым скриптом, не только словами автора.
  • Обобщение German-механики на French в bundle-manifest.mjs (_role: 'locale', localeRoots, retry-guard 1/1/1/1, entryFallbackPlugin) — построчно сверено с рабочим de-эквивалентом, консистентно.
  • src/i18n/fr.ts — точный клон de.ts (сверено побайтово текстом).
  • Автоматический выбор French по профилю HA (fr, fr-FR, fr-CA, fr-BE, fr-CH) — юнитами, зелёные.
  • demo/smoke_french_locale.mjs, smoke_entry_stale.mjs (fr-ветка), smoke_german_locale.mjs (регресс на общем коде) — прогнаны лично, зелёные.
  • Мутант AC5 — прогнан лично целевым --id, красит смок, как обязан.
  • Оба changelog (en+ru) и USER-GUIDE(.ru).md правлены в том же feature-коммите (89fa8ca5, User-Visible: yes), терминология («Français», «lazy dictionaries») согласована с существующим German-текстом.
  • Трейлеры на всех коммитах диапазона (Issue: #371, ровно один User-Visible: yes|no) — прочитаны git log --format, соответствуют классам изменений.
  • demo/srv/assets/** не закоммичен (соответствует #255); dist/** и custom_components/houseplan/frontend/** побайтово совпадают со свежей локальной сборкой (cmp, без вывода).
  • Ребейз выполнен корректно: git merge-base origin/dev HEAD == origin/dev, конфликтов для слияния больше нет.

Что не проверялось

  • Лингвистическое качество перевода — вне код-ревью; носитель (@OUARZA) проверит 121 добавленный автором ключ постфактум по решению владельца, зафиксированному в этом же issue (комментарий 2026-08-29T10:26:18Z).
  • pytest tests_backend, model-invariants, golden:verify, performance — не запускались, обоснование см. в таблице гейтов выше (диапазоны диффа их не задевают).
  • Полная матрица smoke_*.mjs (203 файла) — не предмет код-ревью, это предрелизный гейт.
  • Дорогой полный прогон mutation-gate.mjs без --id (все мутанты проекта) — не гейт код-ревью; для AC5 достаточен целевой прогон, выполнен.

Вердикт

Зелёный. High: 0, Medium: 0. Задача уходит в очередь на пре-релиз.