diff --git a/docs/reviews/SPEC-REVIEW-371-r2.md b/docs/reviews/SPEC-REVIEW-371-r2.md new file mode 100644 index 00000000..b740ca40 --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-371-r2.md @@ -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).