docs: review document for #391

Issue: #391
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-30 17:13:01 +00:00
parent 31cb2827bf
commit 2518abd961
+189
View File
@@ -0,0 +1,189 @@
# CODE-REVIEW-391-r1
Issue: #391 «refactor: type editor i18n keys»
Этап: code (S7-code-review)
Заход: r1 (первый код-ревью раунд; спек-ревью прошло зелёным на r1 отдельно)
Ветка: `issue/391-i18n-key-casts`
SHA под ревью: `81a4278aa4236cec656e473e3ff3953a05c6c5d1` (пайплайн привёл ветку к `dev`
поверх коммита `f7f088fe`; после ребейза это другой код — разбор ниже полный).
Материал: `git diff origin/dev...HEAD` относительно локально закэшированного
`origin/dev` = `eb5aa2a0f8dea5514ee2fee2dbd609457dd0fb4d`.
## Скоуп
Класс A: `src/houseplan-editor-runtime.ts` — снятие `as any` с 33 вызовов
`this.host._t(...)` в трёх семействах ключей (`device_inbox.*` — 26,
`marker.*` — 6, `gs.preflight_reason_*` — 1) и замена вычисляемых ключей на
узкий `as I18nKey`, без изменения словарей, текстов, разметки и ветвления.
Класс B: `scripts/mutation-gate.mjs` — перенос mutation-anchor #295
(`preflight-reason-lost-in-dialog`) на новый файл/сигнатуру после того, как
lazy-load рефакторинг увёл вызов из `houseplan-card.ts` в
`houseplan-editor-runtime.ts` (без изменения `guard`/`because`).
Класс D: `dist/**`, `custom_components/houseplan/frontend/**` — пересборка.
`User-Visible: no`, трейлер `Issue: #391` на месте — оба корректны для
чисто типового рефакторинга без пользовательского эффекта; изменений в
`docs/CHANGELOG*.md` нет и не требуется.
## Как проверялось
Зелёного Validate на этом SHA не найдено, поэтому гейты прогнаны вручную на
чистом дереве этого коммита (Node 22, зависимости уже установлены):
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | pass, без вывода |
| Unit/integration | `npm test` | 1644 pass / 0 fail / 1 skipped |
| No-new-any (#342) | `git diff origin/dev...HEAD -- src/houseplan-editor-runtime.ts \| node scripts/no-new-any.mjs --diff -` | «Проверено добавленных строк: 32 в 1 файл(ах). Новых any нет.» |
| Mutation-gate анchor (#295) | `node --test test/mutation-gate.test.mjs` | 10/10 pass |
| Build | `npm run build` | pass |
| Bundle sync | `npm run bundle:sync` | pass; `git status --porcelain` после пересборки — пусто (обе generated-копии побайтово совпадают с закоммиченными) |
| Bundle budget | `npm run bundle:budget` | initial View 283100 B / budget 300000 B; lazy editor 143903 B; lazy locale 46996 B — совпадает с числами автора |
| Inventory | `npm run inventory` | Node 1645 (было 1643 у автора; расхождение — от несвязанного коммита `eb5aa2a0`, легшего поверх после отправки на ревью, не дефект этой задачи) |
| Docs fingerprint | `node scripts/check-docs.mjs` | **FAIL** — см. находку M1 |
Diff не трогает геометрию/модель (нет правок `wall-thickness.ts`, `layout`,
`marker.space`, `open_spans`, узлов/рёбер) — `npm run invariants` не
запускался, это осознанный пропуск, а не мелочь.
Diff не трогает `custom_components/**/*.py` — `pytest tests_backend` не
запускался.
`golden:verify` не запускался: изменение не может повлиять на рендер —
это доказано не только заявлением автора, а тем, что все правки состоят
из удаления TypeScript-каста (`as any`/`as I18nKey`), который стирается на
этапе компиляции без остатка; визуальных/строковых/ветвящихся изменений в
diff нет.
### Браузерные смоки
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`:
```
Изменено файлов src/**: 1 · символов проекта на изменённых строках: 4
Прямое совпадение (7): _showToast →
smoke_help_affordance, smoke_junction_limits,
smoke_optimize_coordinate_canonicalization, smoke_partition_openings,
smoke_room_resize, smoke_tap_ctx, smoke_zero_wall_migration_unblocked
```
Символ `_showToast` попал в выборку механически: он встречается на той же
строке, что и переименованный `_t('device_inbox.saved' as any)` →
`_t('device_inbox.saved')`, хотя сам вызов `_showToast` не менялся. Решение:
прогнать все 7 — дёшево, а инструмент прямо предупреждает не полагаться на
тематический отбор (регресс #234 был пойман смоком, чьё название не
подсказывало связь). Все 7 — `OK`. Дополнительно прогнан
`node demo/smoke_preflight_diagnostics.mjs` — это ровно guard-команда
mutation-anchor `preflight-reason-lost-in-dialog`, который правит diff;
`OK`. Итого 8 браузерных смоков, все зелёные. Полный набор (208 смоков) не
запускался — задача не задевает ничего, кроме уже перечисленного, это
предрелизная обязанность, а не гейт код-ревью.
## Находки
### M1 (Medium, в скоупе — правится в этой же задаче) — стал протухшим отпечаток скриншотов документации
`node scripts/check-docs.mjs` на SHA `81a4278a` падает:
```
ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs
```
Воспроизведение и причина, а не голословно: `visualFingerprint()`
(`scripts/source-fingerprint.mjs`) хэширует весь `src/**` плюс несколько
build-input файлов. Коммит правит `src/houseplan-editor-runtime.ts`, поэтому
отпечаток обязан был измениться — и изменился (проверено прямым вызовом:
на этом SHA `visualFingerprint('.')` = `74378a15…`, а
`docs/images/screenshots.json.sourceFingerprint`, оставшийся из `dev`, —
`d27cbb20…`; для контроля тот же расчёт на чистом клоне `origin/dev`
(`eb5aa2a0`, без диффа этой задачи) даёт согласованные `d27cbb20…` с обеих
сторон — несовпадение появляется ровно с этим коммитом, а не раньше).
Коммит не содержит правок `docs/images/**`, то есть шаг
`npm run build && node demo/docs/capture.mjs` не выполнялся.
Это не гипотетическая придирка: `docs` — один из перечисленных gate-джобов
Validate (AGENTS.md), и ровно этот пропуск уже дважды оставлял `dev` с
красным `docs` до отдельной последующей задачи (#230, #234 → почищено в
#237). Смысловых изменений отпечаток не несёт (правки — чистое удаление
типовых кастов, кадры почти наверняка выйдут пиксель-в-пиксель прежними),
но манифест обязан отражать актуальный `sourceFingerprint`, иначе `docs`
red на `dev` сразу после мержа.
Почему Medium, а не High: не портит пользовательское поведение, не ломает
контракт, чинится механически. Почему в скоупе этой задачи, а не отдельный
issue: находка — прямое следствие ровно этого diff (правка src/** этой
задачи сделала отпечаток невалидным), а не постороннего дефекта соседней
подсистемы — правило «Medium вне скоупа → отдельный issue» сюда не
применяется.
Что нужно для закрытия: `npm run build && node demo/docs/capture.mjs`,
проверить, что кадры не изменились (`imageSha256` в манифесте), закоммитить
обновлённый `docs/images/screenshots.json` (и PNG, если оптимизатор дал
другие байты) в той же ветке с трейлерами `Issue: #391` / `User-Visible: no`
— это не пользовательская документация, а служебный манифест, видимое
поведение не меняется.
## Что проверено и корректно
- Пересчитаны все 33 целевых вызова построчно (`grep` по
`_t(.*as any` до/после диффа): 26 `device_inbox.*` + 6 `marker.*` + 1
`gs.preflight_reason_*` — все убраны; ни один каст той же строки за
пределами трёх заявленных семейств не тронут (проверено: `tap.*`,
`fill.*`, `decor.*`, `junction.*`, `vac.*`, `run.*` и generic `message`/
`hintKey`/`labelKey` остаются с `as any` — корректно, они вне скоупа).
- Условный `readd`/`add` (строка ~12054) переписан на литеральный union без
каста на каждой ветке, как требует правило реализации №5 — подтверждено
диффом.
- `I18nKey` был импортирован и `_t` уже строго типизирован до этой правки
(строки 245, 1104 в текущем файле) — задача действительно восстанавливает
существующий контракт, а не создаёт новый.
- Ни `as never`, ни `as unknown as`, ни новых `any`, ни расширения
сигнатуры `_t`/`I18nKey` в diff нет — подтверждено чтением полного diff и
`no-new-any.mjs`.
- Ключи, тексты и ветвление визуально не изменились — единственные строки
diff это удаление/сужение каста; проверено чтением, не исполнением, для
всех 33 мест плюс подтверждено 8 зелёными браузерными смоками и `npm
test`, включая i18n parity/policy-тесты в общем прогоне.
- Mutation-anchor `preflight-reason-lost-in-dialog` (#295) актуализирован
корректно: `find`-строка патча совпадает буквально со строкой 9769
текущего файла, `guard`/`because` не изменены, `node --test
test/mutation-gate.test.mjs` зелёный (уникальность/применимость мутантов
проверяется этим набором), `node demo/smoke_preflight_diagnostics.mjs` —
`OK`.
- Generated bundle (класс D) синхронизирован и совпадает побайтово: после
`npm run build && npm run bundle:sync` `git status` пуст.
- Трейлеры коммита корректны: `Issue: #391`, `User-Visible: no`; changelog
оправданно не тронут.
- Ремарка спек-ревью про два «нечистых» вычисляемых ключа
(`marker.value_badge_attr_${source.attribute}`, где `attribute: string`,
и `.replace()/.replaceAll()`, теряющие литеральный тип) — не новый риск:
тот же профиль (произвольная строка, приведённая к `I18nKey`) был и до
правки под `as any`; ТЗ сознательно разрешает это как единственно
доступный вариант, когда компилятор сам не выводит ключ.
## Чего не проверял и почему
- `npm run invariants` — diff не касается геометрии/модели (нет правок
толщины стен, `layout`, `marker.space`, `open_spans`, рёбер решётки).
- `python -m pytest tests_backend` — diff не касается
`custom_components/**/*.py`.
- `npm run golden:verify` — правка стирается на этапе компиляции
(`as any`/`as I18nKey` не производят рантайм-кода), рендер не может
измениться; смоки подтверждают отсутствие видимых эффектов дешевле, чем
полный golden-прогон.
- Полный набор из 208 браузерных смоков — не оправдано ни AC, ни объёмом
diff (одна поверхность, 33 каста в одном файле); прогнаны все 7
«прямых совпадений» инструмента подбора плюс guard-смок мутанта — этого
достаточно для типовой правки такого размера.
- Performance-профили — не названы в AC, diff не касается чувствительных к
перфу путей.
- Ручное тестирование в браузере (UI) не проводилось — не требуется:
изменение не может изменить скомпилированную семантику, что подтверждено
выше техническим аргументом (стирание типовых кастов), а не только словом
автора.
## Вердикт
Единственная находка (M1) — Medium, в скоупе задачи, чинится в ней же:
`npm run build && node demo/docs/capture.mjs`, проверка, что кадры не
изменились, коммит обновлённого манифеста с теми же трейлерами. High
находок нет. Все AC из тела issue выполнены и подтверждены либо тестом,
либо построчным чтением с указанием, что именно проверено. Вердикт —
жёлтый.