mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -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, снято решением ревьюера с записью выше, без возврата автору.
|
||||
Reference in New Issue
Block a user