Files
houseplan-card/docs/reviews/SPEC-REVIEW-502-r1.md
T
2026-09-09 19:35:30 +00:00

185 lines
16 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.
# 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`
(сверено непосредственно перед подведением итогов).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Якоря снять не удалось: ветки задачи нет, материал читался по `dev`.
- Вердикт конвейера: `green` · High 0