16 KiB
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.* и т. п.
Как проверялось
-
Прочитаны
docs/SCOPE.md,AGENTS.md,PROCESS.md(§1–§10, включая §2.4, §2.10, §5, §7.1). -
Прочитано тело issue #502 целиком и оба комментария (S2-анализ, передача на ревью). Нашедшая задачу цепочка — код-ревью #485 r2 — тоже сверена.
-
Прочитан текущий
test/i18n-dead-keys.test.mjsнаdevпострочно — подтверждено, чтоexpressionPatternдляBinaryExpressionс+действительно собирает регулярку без требования точки в статической части, и что'r' + Date.now().toString(36)вsrc/houseplan-editor-runtime.ts(id черновика) даёт ровно^r.+$, как написано в issue. -
Проверено текущее состояние
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 закрыт. -
Эмпирическая проверка выполнимости 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. -
Дешёвые гейты (диф не тронут — 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