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. Задача уходит в очередь на пре-релиз.