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