docs: review document for #391

Issue: #391
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-30 16:44:22 +00:00
parent a47ec97a04
commit 162c8a3bef
+148
View File
@@ -0,0 +1,148 @@
# SPEC-REVIEW-391-r1
Issue: #391 · Этап: spec (S4-spec-review) · Трек: `small` (лёгкий) · Заход: r1 · блокирующих циклов ревью ТЗ израсходовано 0 из 2 (лимит лёгкого трека — 2)
## Скоуп
Тех.долг: заменить `as any` на 33 вызовах `this.host._t(...)` в
`src/houseplan-editor-runtime.ts` на снятие каста (для литеральных ключей) или
узкий `as I18nKey` (для вычисляемых ключей), в трёх семействах —
`device_inbox.*`, `marker.*`, `gs.preflight_reason_*`. Выделено из код-ревью
#390. Изменение невидимо пользователю (`User-Visible: no`), не меняет ключи,
переводы, разметку, поведение и не расширяет тип `I18nKey`/`_t`.
Кода ещё нет: ветки `issue/391-*` не существует (`git branch -a` пуст),
проверяется только текст ТЗ в теле issue против фактического состояния
`src/houseplan-editor-runtime.ts` на `origin/dev` = `a47ec97a`.
## Как проверялось
Ревью ТЗ на лёгком треке не гоняет билд-гейты (кода нет, гонять нечего) —
основной метод: чтение `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md`, тела issue
#391 и его единственного комментария (аналитика владельца), затем построчная
сверка каждого технического утверждения ТЗ с реальным кодом:
- пересчитал все `as any` рядом с `_t(...)` в файле поимённо (`awk` по токену
`as any` на строках, содержащих `device_inbox`/`marker.`/`gs.preflight`,
считая по несколько совпадений на строку, а не по строкам) — получил ровно
**26 device_inbox + 6 marker + 1 gs = 33**, что совпадает с числом из тела
issue и из комментария аналитика. Первый проход давал 23/5/1=29 и 25/5/1=31 —
ошибка была в собственном regexp (пропускал вторую строку многострочного
`_t(...)`, строку `const emptyKey = ... as any` без вызова `_t` на той же
строке, и `${…} as any : ${…} as any` — два каста в одном вызове на
строке 12055). После исправления regexp числа сошлись;
- для каждого класса вычисляемых ключей проверил, действительно ли источник —
закрытый union, как утверждает ТЗ:
- `device_inbox.tab_${tab}` / `device_inbox.empty_${dialog.tab}` — `tab`/`dialog.tab`
типа `DeviceInboxCategory = 'on_plan'|'available'|'hidden'|'readd'`
(`src/device-inbox.ts:12`), словарь содержит все 4×2 производных ключа —
сходится, каст снимается на `as I18nKey` без потери типобезопасности;
- `device_inbox.status_${row.status.kind}` — ветка после отсечения `'active'`
в тернаре, оставшиеся значения `ha_disabled|orphaned|unverified`, все три
ключа в словаре есть (`src/i18n/en.json:500-502`);
- `gs.preflight_reason_${failure.reason}` — `OptimizeGeometryFailureReason`
закрытый union (`src/plan-geometry-preflight.ts:71`), ключи в словаре
подтверждены (`src/i18n/en.json:885-891`);
- `marker.value_badge_attr_${source.attribute}` (строка 12411) —
единственный проверенный случай, где источник **не** закрытый union:
`ValueBadgeSource['attribute']` типизирован как обычный `string`
(`src/types.ts:103-106`, `VALUE_BADGE_ATTRIBUTES: Record<string, readonly
string[]>` в `src/device-value-badge.ts:9`). Но это именно тот случай,
который ТЗ прямо описывает как «компилятор не выводит ключ сам» — узкий
`as I18nKey` здесь необходим и ожидаем самим текстом ТЗ, а не
недосмотром;
- `marker.toggle_effect_${value.replace('-', '_')}` и аналоги (12257, 12294) —
`String.prototype.replace` всегда возвращает `string` независимо от
входа, литеральный union теряется — тоже честный случай для `as I18nKey`,
ТЗ это покрывает;
- проверил, что литеральные ключи (device_inbox.title/button/search/…,
device_inbox.saved, marker.value_badge_lqi и т.д.) действительно есть в
`src/i18n/en.json` без модификации — 17 выборочно проверенных ключей все
найдены;
- отдельно нашёл в файле соседний, но не входящий в скоуп паттерн: строки
11927–11968 используют `as never` (не `as any`) на других ключах семейства
`device_inbox.filters_*`/`device_inbox.empty_*`-соседях. Это не предмет
задачи (ТЗ и правило 3 явно запрещают *добавлять* новые `as never`, но не
требуют убирать существующие), не находка;
- проверил прецедент: предшественник #390 (та же пара автор/ревьюер, тот же
файл, тот же лёгкий трек) был принят и слит без явного раздела «откат» в
теле issue — ориентир для оценки, насколько строго требовать этот раздел
здесь (см. находку L1 ниже);
- проверил самооценку трека: сложность 1/3, одна поверхность
(`houseplan-editor-runtime.ts`), нет миграции конфига, нового UX-контракта,
влияния на perf/touch — подтверждается тем, что диапазон правки — только
снятие/сужение приведения типов на уже существующих строках, без изменения
веток исполнения.
Гейты (`typecheck`/`test`/`build`) не прогонялись сознательно: на этом этапе
нет диффа кода, который они могли бы проверить — они относятся к этапу
код-ревью после реализации, не к ревью ТЗ.
## Находки
### L1 (Low) — нет явного раздела «откат»
Шаблон лёгкого трека (`PROCESS.md` §5) требует «проблема · контракт ·
AC1…ACn с доказательством · **откат**». В теле issue #391 такого раздела нет
(есть «Контекст/Проблема», «Цель», «Скоуп», «Правила реализации», «Критерии
приёмки», «Проверка», «Оценка и трек», «DoD» — отката среди них нет).
Снимаю без правки: откат здесь тривиален и не требует отдельного решения —
`User-Visible: no`, миграции нет, единственный коммит с трейлерами
`Issue: #391`/`User-Visible: no`, откат = `git revert` этого коммита без
побочных эффектов. Тот же паттерн (без явного «откат») был принят у
предшественника #390 с тем же профилем риска. Дальнейшего требования к
автору нет — только запись в этом документе.
Находок High или Medium, блокирующих или требующих правки, не найдено.
## Что проверено и корректно
- **Числа в ТЗ точны**: 26 device_inbox + 6 marker + 1 gs = 33 `as any` на
вызовах `_t`, проверено построчным пересчётом, не поверено на слово автора.
- **Технические предположения не являются недоказанной догадкой**: утверждение
«динамические ключи собираются из закрытых union» проверено по каждому
вычисляемому ключу; там, где это неверно (`source.attribute: string`,
`.replace()` результат), ТЗ само относит случай к «узкий `as I18nKey`, если
компилятор не выводит ключ сам» — то есть не выдаёт неполную догадку за
универсальный факт, оговаривает исключение заранее.
- **Скоуп замкнут и не пересекается с продуктовым поведением**: словари,
переводы, `_t`/`I18nKey`, Device Inbox/marker/preflight UX явно исключены;
соседние `as any` (decor.*, vac.*, run.*, tap.*, fill.*, room.*, markup.*)
тоже явно вне скоупа и не тронуты правилами реализации.
- **AC проверяемы и у каждого понятен способ доказательства**: AC1–AC4 —
чтением кода/diff (описано в разделе «Проверка»: grep на `as any`, сверка
ключей/веток/строк), AC5–AC7 — именованные команды (`npm run typecheck`,
`npm test`, `npm run build`+`bundle:sync`+`bundle:budget`+`inventory`).
- **Классификация `small`/сложность 1/3 обоснована**, а не заявлена без
проверки: единственная поверхность, механическая типизация, ни один пункт
критериев лёгкого трека не нарушается.
- **Соответствие docs/SCOPE.md**: задача не добавляет и не меняет
пользовательское поведение, поэтому не обязана закрывать строку Core user
jobs — это внутренняя типобезопасность, тот же класс, что принятый
предшественник #390.
- **Гейт #342 (`no-new-any`) корректно учтён**: правило 5 ТЗ верно описывает
механику гейта (судит только добавленные/изменённые строки диапазона) —
соответствует `PROCESS.md` §8 дословно.
## Чего не проверял
- Не запускал `npx tsc --noEmit`/`npm test`/`npm run build` — на этом этапе
нет кода для проверки (ветка `issue/391-*` не создана). Это будет предметом
код-ревью после реализации, а не спек-ревью.
- Не проверял il8n-ключи `ru.json` на предмет паритета для затронутых
семейств — вне скоупа задачи (ключи не меняются) и не влияет на оценку ТЗ.
- Не выполнял линию `npm run inventory`/`bundle:budget` — неприменимо без
кода.
- Не проверял все 33 места по отдельности до последней строки итогового кода
(это будет делать код-ревью с реальным diff'ом); проверил представительную
выборку и все нетривиальные (не факт «закрытый union») случаи, которых
оказалось 2 из 33 (`marker.value_badge_attr_*`, `marker.toggle_*_${value...}`
и `marker.toggle_none_${reason...}` через `.replaceAll`) — оба уже покрыты
формулировкой ТЗ.
## Вердикт
Зелёный. ТЗ выполнимо, проверяемо, не содержит недоказанных догадок,
самооценка трека верна. Единственное отклонение от шаблона (нет раздела
«откат») — Low, снято решением ревьюера с записью выше, без возврата автору.