From 162c8a3beff3ffffca6c78dc3dd417396fd0df88 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:44:22 +0000 Subject: [PATCH] docs: review document for #391 Issue: #391 User-Visible: no --- docs/reviews/SPEC-REVIEW-391-r1.md | 148 +++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/reviews/SPEC-REVIEW-391-r1.md diff --git a/docs/reviews/SPEC-REVIEW-391-r1.md b/docs/reviews/SPEC-REVIEW-391-r1.md new file mode 100644 index 00000000..c9a56145 --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-391-r1.md @@ -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` в `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, снято решением ревьюера с записью выше, без возврата автору.