Files
houseplan-card/docs/reviews/SPEC-REVIEW-391-r1.md
T
2026-08-30 16:44:22 +00:00

149 lines
13 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-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, снято решением ревьюера с записью выше, без возврата автору.