# SPEC-REVIEW-502-r1 Issue: #502 — «i18n-dead-keys: широкий паттерн `r.+` маскирует мёртвые ключи» Этап: spec (PROCESS.md §2.4), лёгкий трек (`small`), ТЗ в теле issue. Заход: r1 · блокирующих циклов израсходовано 0 из 2 (лимит §4 для лёгкого трека). SHA рабочей копии на момент ревью: `72764cff906d8e27996e638ced8b708251e24657`. ## Скоуп Класс B, один файл `test/i18n-dead-keys.test.mjs` (опционально новый `test/helpers/i18n-consumers.mjs`). Продуктового кода, i18n-словарей, UX, миграций задача не касается; `User-Visible: no`. Цель: сузить генератор «динамических потребителей» в гейте мёртвых i18n-ключей так, чтобы паттерн вида `^r.+$` (рождённый из `'r' + Date.now().toString(36)`) не маскировал реальные мёртвые ключи семейства `radar.*`, `room.*`, `run.*` и т. п. ## Как проверялось 1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` (§1–§10, включая §2.4, §2.10, §5, §7.1). 2. Прочитано тело issue #502 целиком и оба комментария (S2-анализ, передача на ревью). Нашедшая задачу цепочка — код-ревью #485 r2 — тоже сверена. 3. Прочитан текущий `test/i18n-dead-keys.test.mjs` на `dev` построчно — подтверждено, что `expressionPattern` для `BinaryExpression` с `+` действительно собирает регулярку без требования точки в статической части, и что `'r' + Date.now().toString(36)` в `src/houseplan-editor-runtime.ts` (id черновика) даёт ровно `^r.+$`, как написано в issue. 4. Проверено текущее состояние `radar.bad_references`: `grep` показал, что ключ уже используется как строковый литерал в `src/radar-setup.ts:334` (`... : message === 'bad_references' ? 'radar.bad_references' : ...`), т.е. дефект из #485 r2, который и породил эту задачу, на `dev` уже устранён (коммит `e9fb255a fix: close radar review gaps`), а #485 закрыт. 5. **Эмпирическая проверка выполнимости AC4** (гейт зелёный на реальном дереве после сужения) — не полагаясь на заявление автора. Собран одноразовый скрипт, реализующий текущий `expressionPattern` один в один плюс критерий контракта п.1 («статическая часть содержит `.`») как фильтр `isKeyShapedPattern`, прогнан на актуальном `src/**` и словарях `src/i18n/en.json` + `src/i18n/support/en.json` (полный листинг результата сохранён в сессии, не коммитился, репозиторий не менялся): ``` OLD unused count: 0 [] NEW unused count: 0 [] ``` Узкий фильтр отбрасывает несколько десятков динамических паттернов (CSS/HTML шаблоны в `src/day-cycle-render.ts`, `src/backdrop-pick.ts`, `src/decor-image-editor.ts`, `src/color.ts`, `src/coincident-partitions.ts` и др. — они никогда не были осмысленными i18n-потребителями, просто случайно совпадали под старым широким критерием), но ни один из них не превращает ни один ключ словаря в мёртвый: результат «0 unused» не меняется. Вывод: AC4 — не догадка, а проверяемо достижимое требование уже на сегодняшнем дереве, при разумной трактовке контракта п.1–2. 6. Дешёвые гейты (диф не тронут — smoke/golden/backend не запускались, они не нужны для ревью ТЗ): - `npx tsc --noEmit` — зелёный (0 ошибок), выполнен на `dev` без изменений. Смысл: подтвердить, что рабочая копия сама по себе в порядке перед разбором ТЗ; продуктовый код в этом раунде не менялся, полный `npm test`/`npm run build` для ревью **спецификации** избыточны (диф — только тело issue). ## Находки Находок High или Medium (в скоупе или вне него) не обнаружено. ### Low — снимается с записью **L1. Раздел «Зависимости» описывает уже не актуальную ситуацию.** Файл: тело issue #502 (раздел «Зависимости»). Текст предполагает, что `radar.bad_references` может быть всё ещё мёртвым на момент реализации и тогда сужённый гейт «покраснеет на нём», требуя ждать слияния #485. Фактически #485 уже закрыт и слит, ключ уже используется в `src/radar-setup.ts:334` — зависимость исполнена ещё до передачи ТЗ на ревью. Раздел не создаёт риска (он написан как условие «если … то», а не как факт) и не блокирует переход в `S5-ready`: реализатору достаточно перечитать текущее состояние `dev` перед стартом, что он обязан сделать в любом случае. Снимаю без правки текста — исправлять формулировку ради контроля версий issue не имеет смысла, реализатор увидит актуальное состояние сам. **L2. Место объявления `DYNAMIC_KEY_FAMILIES` не названо явно.** Контракт п.3 вводит константу `DYNAMIC_KEY_FAMILIES`, «принятый предположительно» блок разносит чистые функции в `test/helpers/i18n-consumers.mjs`, а сам гейт остаётся в `test/i18n-dead-keys.test.mjs` — но не сказано, в каком из двух файлов будет жить сама константа-список семей. Это техническая, не продуктовая деталь (расположение файла, PROCESS §7.1: «место, где стоит гвард» — решает исполнитель), и она не единообразна ни с чем, что требовало бы одного конкретного ответа. Не блокирует; исполнитель решает свободно. ## Проверка контракта на непротиворечивость и проверяемость - **П.1 (точка в статической части)** — проверяемо: AC1 и AC2 дают конкретные примеры входов и ожидаемых `true`/`false`, юнит на синтетическом AST. Убедился (см. «Как проверялось», п.6), что действующая реализация `expressionPattern` уже собирает `source`/`dynamic` в форме, из которой этот фильтр вычисляется механически (строка минус все `.+`-вставки содержит `.`). Однозначно. - **П.2 (открытый хвост допустим, если после точки)** — это уточнение к п.1, не отдельный независимый фильтр; пример `.+\.title` корректно иллюстрирует разрешённый случай, а `^r.+$` — прямое следствие того, что паттерн из п.1 уже отсеян (в нём нет точки вовсе), а не отдельно запрещённый случай «начала с хвоста». Формулировка чуть избыточна («паттерн … невозможен по построению п.1» — верно и прямо сказано в тексте, противоречия нет), Low не завожу. - **П.3 (явный список семей + непустая причина + запись без покрытия — ошибка теста)** — проверяемо AC4, оба условия (непустая причина, покрытие ≥1 ключа) разнесены по разным ветвям мутации в третьем столбце таблицы AC. - **П.4 (реально мёртвые ключи не маскируются, уходят отдельным issue Codex)** — граница со скоупом задачи прочерчена верно: класс A (словари) не затрагивается этим issue, что совпадает с заявленным классом B. Единственный известный на момент ТЗ случай (`radar.bad_references`) уже не актуален (см. L1); эмпирическая проверка (п.5 выше) показывает, что на сегодняшнем дереве других случаев нет — контракт не вступает в противоречие с AC4 «зелёный гейт на реальном дереве». - **П.5 (форма сообщения об ошибке не меняется)** — тривиально проверяемо визуальным сравнением текста в тесте, доказательство не требуется отдельным AC (описательное требование к диагностике, не к защите). Утверждений, поданных как факт о продуктовом поведении без пометки «предположение», не найдено — блок «Принято предположительно» присутствует, покрывает единственное содержательное техническое решение (критерий «похоже на ключ» = наличие точки, и альтернатива, которую отвергли, названа явно). Продуктовых вопросов владельцу в тексте нет и не требуется: гейт не наблюдаем пользователем (персонами J1–J7 из `docs/SCOPE.md` этот тест не встречается вовсе), задача не расширяет и не сужает видимое поведение продукта. ## Что проверено и корректно - Класс изменений (B), `User-Visible: no`, отсутствие миграций/i18n/UX/perf/ touch-влияния — заявлено верно и подтверждено чтением диффа не требуется, т.к. диффа кода ещё нет; сам факт, что единственный целевой файл лежит в `test/**`, подтверждён структурой репозитория. - Формат AC1–AC5: каждый несёт способ доказательства и (кроме AC5, честно помеченного «не защита») мутацию, которой он красне́ет — это ровно требование §2.7 на будущий код-ревью, заранее подготовленное автором, снижает риск возврата на код-ревью. - Откат — одним revert-коммитом, без затронутого продуктового кода; минимально достаточен для класса B. - Трек `small`: все пять критериев §5 действительно выполняются одновременно (одна поверхность — один тестовый файл; ни миграции, ни compatibility-полей; ни нового UX-контракта — потому что UX-контракта нет вовсе; ни perf/touch- влияния). Полный трек не требовался бы ни по одному критерию. - Эмпирическая проверка достижимости AC4 (раздел «Как проверялось», п.5) — сильнее, чем «ревьюер поверил автору»: воспроизведена логика контракта на реальном src-дереве, а не пересказана. ## Чего не проверял - Полный `npm test` / `npm run build` / `npm run bundle:sync` — не нужны на этапе ревью спецификации: продуктового и тестового кода ещё нет, диф — только текст issue. Будут обязательны на код-ревью (§7.2), включая дисциплину «тест умеет падать» по мутациям, которые сам автор уже назвал в таблице AC. - Браузерные смоки, golden, backend pytest, performance-профили — не применимо: задача не трогает `src/**`, `custom_components/**/*.py` и не меняет видимый результат. - Точная реализация `isKeyShapedPattern`/`unusedKeys` как кода — не пишется на этом этапе (ревьюер ТЗ не правит продуктовый/тестовый код); эмпирическая симуляция в разделе «Как проверялось» использована только чтобы проверить ВЫПОЛНИМОСТЬ контракта, а не чтобы заменить будущий код-ревью. ## Вердикт Зелёный. Two Low-находки (L1, L2) сняты с записью выше, без правки текста issue — они не мешают переходу в `S5-ready` и не создают риска для реализации. High и Medium (в скоупе или вне него) не найдено. --- ## Материал раунда - Issue: #502, тело + 2 комментария (S2-анализ и передача на `S4-spec-review`), оба от `Matysh`, 2026-09-09. - ТЗ живёт в теле issue (лёгкий трек), файла в `docs/specs/` нет и не должно быть. - SHA рабочей копии на момент вывода вердикта: `72764cff906d8e27996e638ced8b708251e24657` (сверено непосредственно перед подведением итогов). --- ## Материал раунда - Ветка: `dev`, коммит `` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Якоря снять не удалось: ветки задачи нет, материал читался по `dev`. - Вердикт конвейера: `green` · High 0