docs: review document for #371

Issue: #371
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-29 10:13:09 +00:00
parent dd591a6c77
commit eadd69644c
+148
View File
@@ -0,0 +1,148 @@
# SPEC-REVIEW-371-r2
Issue: [#371 — Add French localization files](https://github.com/Matysh/houseplan-card/issues/371)
Трек: `small` (лёгкий). Заход r2, блокирующих циклов израсходовано 1 из 2 (израсходован
на жёлтый вердикт r1; зелёный вердикт цикла не тратит — см. §4 PROCESS.md и правило #227).
Ревьюер: Claude (роль «ревьюер ТЗ»), автор ревизии: Codex (комментарий-ревизия от
2026-08-29T10:08:21Z).
Материал: тело issue #371 на момент разбора (текущая версия, с примечанием «Ревизия 2
(по SPEC-REVIEW-371-r1)» в конце тела) — без файла в `docs/specs/`, соответствует метке `small`.
## Скоуп раунда
Это ревью округа **r2** — по правилам §2.9 объём разбора идёт по дельте, а не заново.
Дельта r1→r2 — это правка тела issue, объявленная автором в комментарии-ревизии и в
примечании внизу тела:
1. **К2** дополнен абзацем «Плюс (r1-M1)»: французская ветка в `entryFallbackPlugin`
(#353) и расширение smoke `entry_stale` третьим локальным прогоном.
2. **AC1** дополнен абзацем «Плюс (r1-M2)»: французские аналоги двух German-персональных
тестов из `test/i18n.test.mjs` — глоссарий продуктовых терминов и скан на артефакты
не-перевода (кириллица + allow-list явных совпадений с английским).
Больше нигде текст issue не менялся: К1, К3, К4, AC2–AC5, откат, трейлер `User-Visible`
и продуктовая классификация трека — та же строка символов, что была на r1. Дельта
локальна (правка двух абзацев ради двух точечных технических пробелов), новой
подсистемы не задевает, контракт поведения не меняет объёма — полный разбор с нуля не
требуется по критерию §2.9.
## Как проверялось
1. Найден вердикт r1 (жёлтый, 0 High / 2 Medium в скоупе — M1, M2) и текст документа
`docs/reviews/SPEC-REVIEW-371-r1.md`, зафиксировавший SHA сверки кода — `914bb4ed`
(dev на момент r1).
2. Получено текущее тело issue #371 (`gh issue view 371 --json body,comments`) и сверено
построчно с текстом, процитированным в `SPEC-REVIEW-371-r1.md`, чтобы выделить именно
дельту, а не пересказ автора о ней.
3. Для M1 — прочитан `scripts/bundle-manifest.mjs:192-224` (`entryFallbackPlugin`) на
текущем `dev` (`dd591a6c`, код в этом файле не менялся со времени r1 — issue #371 ещё
не имеет кода в репозитории). Подтверждено: ternary строк 208-212 действительно
обрывается на `ru`/`de` и молча уходит в английский текст для всех остальных языков —
ровно та дыра, которую r1 назвал. Новый абзац К2 закрывает её точным описанием ветки
(`l.startsWith('fr')` перед финальным `else`) с готовым французским текстом в том же
стиле, что немецкий и русский варианты (тот же формат: «что случилось — что делать
(Ctrl+F5)»).
4. Для M1 (smoke) — прочитан `demo/smoke_entry_stale.mjs` целиком. Подтверждено: текущий
смок гоняет **только** `en-US`/`ru-RU` (де-локаль в этом конкретном смоке не
проверяется вовсе, несмотря на то что у неё есть своя ветка в коде) — формулировка
ревизии «третья локаль в существующем цикле en/ru» точна, а не приблизительна.
5. Для M2 — прочитан `test/i18n.test.mjs:327-370`, оба German-персональных теста
(`German catalog keeps the product glossary...`, `German values equal to English are
explicitly reviewed`). Подтверждено: это точечные assert’ы конкретных строк плюс
регэксп-скан на кириллицу/сентинелы и allow-list точных совпадений с `en.json`, ни один
не читает `LANGUAGE_REGISTRY`. Новый абзац AC1 обещает French-аналоги той же
конструкции (глоссарий ключевых терминов + запрет кириллицы + allow-list совпадений,
список которого «собирается по факту словаря» — так же, как собирался немецкий
allow-list по факту немецкого словаря, а не заранее).
6. Проверено, что остальные разделы (К1, К3, К4, AC2-AC5, откат, `User-Visible`) не
изменились с r1 — значит, находки r1 вне M1/M2 (L1, L2) и весь блок «что проверено и
корректно» из r1 переносятся без повторной проверки (см. «Унаследовано из r1» ниже).
7. Гейты (`tsc`/`test`/`build`/`bundle:budget`/`check-docs`) не запускались — этап `spec`,
кода French в репозитории по-прежнему нет (`ls src/i18n/` не изменился с r1: только
`de.json/de.ts/en.json/ru.json/registry.ts/language-runtime.ts`), гнать нечего — то же
основание, что было в r1.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **M1** — `entryFallbackPlugin` хардкодит `ru`/`de` в тексте плашки устаревшего entry (#353), French останется без ветки и молча получит английский текст; ни один AC это не покрывает | К2 дополнен точной веткой `l.startsWith('fr')` с готовым французским текстом в формате существующих веток + smoke `entry_stale` явно расширяется fr-прогоном как третьей локалью | Тело issue, абзац К2, предложение «Плюс (r1-M1): entryFallbackPlugin (#353) несёт локализованную плашку… добавляется французская ветка… и smoke entry_stale расширяется fr-прогоном (третья локаль в существующем цикле en/ru)» |
| **M2** — AC1 заявляет «существующий тестовый механизм расширяется на fr» как единый факт, но два German-персональных теста (глоссарий + скан на артефакты) не зациклены по реестру и не расширяются автоматически | AC1 дополнен явным обязательством завести French-аналоги обоих тестов: глоссарий продуктовых терминов + скан на кириллицу/артефакты не-перевода с allow-list | Тело issue, абзац AC1, предложение «Плюс (r1-M2) — аналоги двух German-персональных тестов: (а) французский глоссарий… (б) скан на артефакты не-перевода…» |
| L1 (Low, снята ревьюером в r1 без правки) | Не требовала правки текста — снята с записью в r1 | `SPEC-REVIEW-371-r1.md`, раздел L1 |
| L2 (Low, снята ревьюером в r1 без правки) | Не требовала правки текста — снята с записью в r1 | `SPEC-REVIEW-371-r1.md`, раздел L2 |
Обе Medium-находки закрыты содержательно, не декларативно: формулировки ревизии
указывают конкретную ветку кода (с точным местом вставки и текстом), конкретный смок и
конкретную тестовую конструкцию, а не общие слова «учтём» / «поправим». Технической
проверкой (сверка с реальным кодом `bundle-manifest.mjs` и `i18n.test.mjs`, п. 3-5 выше)
подтверждено, что оба обещания реализуемы буквально так, как написаны, и действительно
закрывают тот пробел, который был назван в r1.
## Унаследовано из r1
Без повторной проверки в этом раунде принято всё, что не входит в дельту r1→r2 —
документ `docs/reviews/SPEC-REVIEW-371-r1.md`, сверка на SHA `914bb4ed` (dev на момент
r1; код с тех пор не менялся, issue ещё не реализована):
- Классификация трека `small` по пяти критериям §5 (единственная точка расширения —
реестр локалей #62, спроектированный под этот случай; одна поверхность; нет миграций
конфига; нет нового UX-контракта; нет влияния на perf/touch) — раздел «Что проверено и
корректно» r1.
- К1 (файлы, арифметика вклада 1026+121−21=1126, backend-паритет 6/6).
- К2 в заявленной на r1 части (`registry.ts`: `loadFrench`/токен/запись — точная копия
`loadGerman`; `bundle-manifest.mjs`: `_role`, `localeRoots`, `DE_RETRY_ASSET_TOKEN` →
парный FR-токен и regex `(de|fr)-`).
- К3 (авто-выбор по BCP-47: `resolveLanguageCode` уже обобщён, юнит `de-DE/de-AT/de-CH`
доказывает механизм без доп. кода для `fr-FR/fr-CA/fr-BE/fr-CH`).
- К4 (доки: абзац auto-языка в USER-GUIDE строится по существующему шаблону; changelog).
- AC2-AC5 в исходной формулировке r1 (юнит реестра, смок AC3 как лёгкий клон
`smoke_german_locale`, бюджет AC4, мутационный тест AC5).
- Продуктовая рамка: локализация не входит буквальной строкой в J1-J7 `docs/SCOPE.md`, но
прецедент #62/#348 уже прошёл этот вопрос — инфраструктура спроектирована под
добавление языков, German принят на ней с P1, открывать вопрос заново на четвёртом
языке того же пайплайна избыточно (перепроверено: `docs/SCOPE.md` по-прежнему не
содержит строки про локализацию как отдельный job, см. п. «Как проверялось» этого
документа — J1-J7 сверены заново, т.к. это дешёвая проверка, но вывод не изменился).
- Вывод «догадок, выданных за решение, не найдено» — дельта r1→r2 тоже проверена на этот
критерий заново (см. ниже) и вывод тот же.
## Проверка дельты на новые находки
Прочитаны оба добавленных абзаца ещё раз с вопросом «не появилась ли новая находка
вместе с закрытием старой»:
- Порядок веток в ternary (`ru → de → fr → en`) ревизия не оговаривает явно, но это не
требует решения: проверка идёт по несовпадающим префиксам (`ru`/`de`/`fr`), порядок
веток не влияет на результат — не находка, а деталь реализации без развилки.
- Ревизия не переносит в French-тест конкретные «сентинел»-маркеры German-теста
(`ZXQPH`/`QXZ`/`⟦HP`, `test/i18n.test.mjs:335`) — это не пробел: маркеры проверены,
нигде в кодовой базе не используются как общий контракт (`grep` по `.mjs/.ts/.js` вне
`node_modules` даёт единственное вхождение — в самом German-тесте), это специфичный
для истории German-словаря артефакт, а не воспроизводимая для French проверка.
Формулировка ревизии «скан на артефакты не-перевода: кириллица… запрещена» покрывает
содержательно значимую часть (реальный риск для контрибьютора, часть рабочего процесса
которого проходит на русском) без копирования частности одного раунда.
- Новый текст не вводит противоречий с K1/K3/K4/AC2-AC5 и не расширяет скоуп задачи
(не добавляет пятый язык, не трогает `LanguageRuntime`, не меняет видимое поведение
сверх «French — четвёртый пункт в существующем пикере»).
Новых находок в дельте не обнаружено.
## Вердикт
Зелёный: 0 High, 0 Medium. Обе Medium-находки r1 закрыты содержательно и проверяемо;
новых находок дельта не породила. Классификация трека, продуктовая рамка и остальной
контракт наследуются из r1 без изменений. ТЗ готово к переходу на код-ревью.
## Чего не проверял
- Не открывал приложенный контрибьютором zip-архив и не проверял содержимое French
словарей — файлов ещё нет в репозитории (то же основание, что в r1).
- Не запускал `npm run typecheck`/`test`/`build`/`bundle:budget`/`check-docs` — класс A
не тронут, кода этой задачи в репозитории ещё нет; фиксировать зелёный прогон не на
чем на этапе `spec`.
- Не оценивал лингвистическое качество французских строк ни в присланном словаре, ни в
новых французских фразах ревизии (текст плашки `entryFallbackPlugin`) — вне
компетенции ревью ТЗ; owner явно оставил финальную вычитку носителю языка постфактум
(комментарий от 2026-08-29T09:58:31Z).