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