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

213 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. Задача уходит в очередь на пре-релиз.