docs: review document for #745

Issue: #745
User-Visible: no
This commit is contained in:
claude[bot]
2026-10-01 12:30:45 +00:00
parent dc77852e0b
commit d4eea609cf
+193
View File
@@ -0,0 +1,193 @@
# CODE-REVIEW-745-r1
Issue: #745 · Трек: `show` · Заход: r1 · блокирующих циклов 0/2
Материал: `git log --oneline origin/dev..HEAD` / `git diff origin/dev...HEAD`
SHA материала: `ca708d8443aee387895a0547d4a6c52fb95e816f` (сверено `git rev-parse HEAD` перед итогом — совпадает)
Один коммит на ветке `issue/745-space-card-room-keys` поверх `dev` (`7ce3f662`, #742 уже влит).
## Скоуп диффа
```
demo/smoke_space_card.mjs | 153 +++++++++++++++++++++++++-
docs/CHANGELOG.md | 3 +
docs/CHANGELOG.ru.md | 3 +
docs/testing-notes/mutation-browser-guards.md | 5 +-
scripts/mutation-registry.mjs | 13 +++
src/space-render.ts | 12 +-
src/styles/plan.styles.ts | 9 +-
7 files changed, 189 insertions(+), 9 deletions(-)
```
Ровно поверхность из ТЗ: `src/space-render.ts`. Правка — список фигур комнат
карточки пространства (`space-render.ts:534–570`) стал
`keyed(space.id, repeat(shownRooms, (r, index) => r.id || index, (r) => {…}))`
вместо голого `map()`: буквально та же форма, что #742 поставил в
`houseplan-card.ts:10952` для основной карточки (`src/houseplan-card.ts:10950-10952`
сверено построчно). Переход `.room { transition: 0.12s }`
(`plan.styles.ts`) не тронут — только дополнен комментарий про карточку
пространства. Остальные файлы: свидетель (новый раздел существующего смока,
а не новый файл — как и объявлено «принято предположительно»), один мутант и
его запись в `mutation-browser-guards.md`, CHANGELOG RU/EN.
## Как проверялось
Рабочая копия уже стояла на материале (`ca708d84`). Дешёвые гейты (`tsc
--noEmit`, `npm test`, `npm run build`) подтверждены зелёным Validate на
этом SHA ([run 36858825861](https://github.com/Matysh/houseplan-card/actions/runs/36858825861)) —
не перегонял; смоки/golden/мутанты на этом SHA Validate не гонял вовсе
(ветка задачи, пуш не PR/кандидат — политика `validate.yml`: «настоящую
приёмку там делает код-ревью»), поэтому все браузерные проверки ниже я
прогнал сам.
| Гейт | Результат | Примечание |
|---|---|---|
| `npx tsc --noEmit`, `npm run build` | не перегонял отдельно | Validate зелёный на `ca708d84`; `npm run bundle:sync` ниже всё равно прогоняет `build` как часть сборки бандла для смоков — тоже чисто |
| `npm run bundle:sync` (сборка + копия в `demo/srv/assets`) | ✅ | нужно для честного запуска браузерных смоков — `demo/srv/assets` не обновляется автоматически |
| `npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs` | ✅ | тестовая сборка для `test-build/` |
| `npm run gate:small` | ✅ зелёный, 126 с, упало 0 | сборка+typecheck, no-new-any, no-new-private-writes, smoke-select, `npm test` (юниты), bundle-policy --verify, bundle:budget, lint:unused — все шаги `ok` |
| `demo/smoke_space_card.mjs` (новый раздел, AC1–AC2) | ✅ зелёный на правке | прогнан лично на честно собранном бандле; все 9 полей `keyedRooms` совпали с ожидаемыми значениями дословно (см. ниже) |
| Мутант `space-card-rooms-rendered-without-keys` — `node scripts/mutation-gate.mjs --id=space-card-rooms-rendered-without-keys` | ✅ «заявленный тест покраснел на мутанте» | гейт сам применяет патч во временном дереве, собирает и гоняет гард — не доверие авторскому отчёту, а независимое исполнение |
| `node scripts/mutation-gate.mjs --check --changed=origin/dev..HEAD` | ✅ exit 0, `browser guards: 204/200` (WARN, не fail — ориентир, не лимит, #699) | новый id размечен в `mutation-browser-guards.md`, `missingReasons` пуст |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | выполнен | см. решение по строкам ниже |
| `demo/smoke_space_card_bg.mjs` (AC3) | ✅ | |
| `demo/smoke_space_card_decor_capability.mjs` (AC3) | ✅ | |
| `demo/smoke_moon_static.mjs` (AC3) | ✅ | |
| `demo/smoke_room_settings.mjs` (прямое совпадение smoke-select) | ✅ | |
| `demo/smoke_space_switch_transitions.mjs` (companion-свидетель #742, не назван в AC, но делит `plan.styles.ts`) | ✅ | подтверждает, что правка не расшатала прецедент #742 |
| `npm run golden:verify` | не прогонялся | метки `ci:golden` на issue нет; тот же вывод, что и на #742 (`CODE-REVIEW-742-r1.md`: «golden не заказывается») — дефект чисто транзитный (видим только в первом кадре перехода), кадр в покое (стили/разметка/порядок узлов в DOM) не меняется: `keyed`/`repeat` не добавляют обёрточных узлов и сохраняют тот же порядок, что и прежний `map()` |
| `python -m pytest tests_backend` | не прогонялся | дифф не касается `custom_components/**/*.py` |
| `npm run invariants` | не прогонялся | дифф не меняет геометрию комнат и ссылки на неё — только идентичность DOM-узлов списка |
### `smoke-select` — решение по строкам
```
Изменено файлов src/**: 2 · символов проекта на изменённых строках: 2
Матрица: 286 смоков · порог «широкого» символа: больше 57 смоков
Прямое совпадение (1): demo/smoke_room_settings.mjs ← roomFillModeOf
Не учитывались как широкие: setConfig
```
- **Прямое совпадение (1)** — `smoke_room_settings.mjs` прогнан, зелёный.
- **`setConfig` как «широкий»** — не прогонял отдельно. Совпадение идёт не
из продуктового кода диффа (там `setConfig` не менялся), а из тестового
файла: новый раздел смока сам многократно вызывает `el.setConfig(...)` на
карточке пространства. `setConfig` — общий метод HA-карточек, матрица
отметила бы десятки несвязанных смоков; решение осознанное, не ищу
скрытую связь там, где символ пришёл из теста, а не из правки.
### Доказательство негатива (свидетель умеет падать)
Не подменял файл руками — использовал официальный путь: `node
scripts/mutation-gate.mjs --id=space-card-rooms-rendered-without-keys`. Гейт
сам: (1) прогнал чистый смок на правке — `ok`; (2) применил патч мутанта
(внутренний `repeat` заменён на голый `map()`, оставив внешний `keyed`) во
временном дереве, пересобрал бандл и прогнал тот же смок — `ok
space-card-rooms-rendered-without-keys: заявленный тест покраснел на
мутанте». Итог: «поймано 1 из 1». Это ровно AC2 (путь B) — инструмент
подтверждает, что без внутреннего ключа тест красит правильную причину, а
не падает случайно.
Отдельно глазами проверил баланс скобок патча реестра: исходная строка
держит два открытых `(` (`keyed(`, `repeat(`) и один `{`; патч меняет
`repeat(` на `.map(`, то есть тоже даёт два `(` и один `{` — закрывающая
`}));` на `space-render.ts:570` остаётся синтаксически верной и для
мутанта, и для оригинала (структурная проверка `mutation-gate --check`
подтвердила это же: «якорь найден 1 раз», сборка мутанта не падает
компиляцией).
## AC — разбор
| AC | Вердикт | Как доказано |
|---|---|---|
| AC1 (путь A — смена `space` в `setConfig` того же элемента) | ✅ выполнен | Раздел смока: фикстура держит ожидаемую форму (`pathAFixtureHolds: true` — `r1` без заливки/`polygon`, `garden`/`g1` с заливкой/`polygon`), после `setConfig({ space: 'garden' })` и одного кадра: нет `CSSTransition` на `[data-hp="room"]` (`pathANoRoomTransition: []`), ни один старый узел не остался подключён под чужим `data-id` (`pathANoRoomNodeOutlivesTheSwitch: []` — внешний `keyed(space.id,…)` отбрасывает всё поддерево целиком при смене `space`), новый узел `g1` рождается сразу с итоговым `rgb(96, 125, 139) / 0.18` (`pathANewRoomBornInItsFill`, совпадает с `DEFAULT_CUSTOM_FILL` в `src/logic.ts:1301`) |
| AC2 (путь B — событие конфигурации меняет состав комнат; плюс настоящий переход не сломан) | ✅ выполнен, красит мутант `space-card-rooms-rendered-without-keys` | Вставка комнаты первой в `f1.rooms` через `__hpTest.setServerConfig` (путь, которым правка с другого устройства доходит до карточки — то же событие, на которое подписан `onConfigChange` → `_load()` в `space-card.ts`): список вырос (`pathBRoomListGrew: true`), ни один из прежних 4 узлов не подменён (`pathBNoRoomNodeSwapped: []` — внутренний ключ `repeat` по `r.id`), переходов не было (`pathBQuietRooms: []`). Настоящая смена `custom_fill` после этого по-прежнему идёт на том же узле (`pathBRealFillChangeKeepsTheRoomNode: true`) и анимируется (`pathBRealFillChangeAnimates: true`) — это тот самый контроль «ложный фикс `transition:none` не прошёл бы», подтверждён отдельно прогоном `smoke_space_switch_transitions.mjs` (companion #742) |
| AC3 (ничего не сломано, выпуск) | ✅ выполнен | `gate:small` зелёный; `smoke_space_card`, `smoke_space_card_bg`, `smoke_space_card_decor_capability`, `smoke_moon_static` зелёные лично; `smoke-select` учтён построчно; мутант в реестре рядом с #742, пойман своим guard’ом (проверено исполнением, не только `--check`); `mutation-gate --check` зелёный (204/200 — предупреждение по #699, не отказ); CHANGELOG RU/EN в том же коммите, текст дословно совпадает с ТЗ; `golden` не заказан и не нужен (см. таблицу выше) |
Единственное число, которое дифф делает видимым пользователю дважды —
`DEFAULT_CUSTOM_FILL` (`#607d8b` / `0.18`, `src/logic.ts:1301`): оно не
менялось этим диффом, у него один источник (`logic.ts`), и смок читает его
же значение через `getComputedStyle`, а не дублирует константу независимо
(дублирование есть только в самом свидетеле — `FINAL_FILL` в
`smoke_space_card.mjs`, это сверено вручную при разборе AC1, расхождения
нет).
## Находки
Нет. High: 0. Medium: 0.
## Что проверено и корректно
- Форма правки — буквально та же, что #742 поставил в `houseplan-card.ts`:
`keyed(space.id, repeat(shownRooms, (r, index) => r.id || index, …))`;
импорты `keyed`/`repeat` добавлены туда, где их не было (`space-render.ts:10-11`).
- Идентификаторы фикстуры в новом разделе смока (`f1`, `garden`, `r1`–`r4`,
`g1`) — не придуманы: это демонстрационные данные проекта, используемые
уже десятками других смоков (`smoke_iso_floor_switch.mjs`,
`smoke_merge_split.mjs`, `smoke_infinite_canvas.mjs` и др.) и реальные
SVG-подложки `demo/srv/assets/f1.svg`, `garden.svg`.
- Пустой `id` ключуется числовым индексом (`r.id || index`) — тот же приём,
что в #742, по `Map`-семантике `repeat` число никогда не совпадёт со
строковым `id`.
- `.room { transition: 0.12s }` не тронут; AC2-проверка настоящей смены
заливки подтверждает, что это не фикс через `transition: none`.
- `roomShapes` используется ровно один раз (`space-render.ts:964`), встроен
в `<svg>` тем же способом `${roomShapes}`, что и раньше — `keyed`/`repeat`
не добавляют обёрточных DOM-узлов, порядок фигур не меняется.
- `shownRooms` вынесен в переменную и используется один раз — не тронул
остальные независимые фильтры по `space.rooms` (например, `glowBaseShapes`
ниже по файлу).
- Мутант реестра синтаксически корректен для обеих версий (считал баланс
скобок вручную, см. «Доказательство негатива»), guard пойман исполнением,
а не только текстовой проверкой `--check`.
- Трейлеры `Issue: #745`, `User-Visible: yes` на коммите; CHANGELOG RU/EN
правлены в том же коммите, тексты дословно совпадают с текстом ТЗ.
- `no-new-private-writes`: свидетель пишет конфиг только через существующий
`__hpTest.setServerConfig`, как и заявлено отклонением от ТЗ (п. «Путь B
свидетель проверяет через `__hpTest.setServerConfig`, а не записью в
`_snap.config`») — подтверждено прогоном соответствующего шага `gate:small`.
- Рабочая копия репозитория после всех проверок чиста (`npm run
bundle:clean`, `git status --short` пуст, `HEAD` = `ca708d84`).
## Маршрут вердикта (§5)
Все критерии пройдены: сложность и риск низкие (копия уже проверенного на
#742 паттерна), одна поверхность (`src/space-render.ts`), нет миграции
конфига, нет нового UX-контракта (устраняется баг в рамках уже описанного
поведения), нет влияния на производительность/touch-контракт, ожидаемое
поведение зафиксировано тем же правилом #525/#534/#742 и текстом в CHANGELOG.
`route: fix`.
## Чего не проверял
- Полную сверку бандла из трёх независимых копий — не требовалась: Validate
зелёный на этом SHA подтверждает `build`; локальная пересборка (`bundle:sync`
для смоков + `--id` мутанта) прошла ещё дважды без расхождений.
- 285 «слабых»/неотобранных смоков вне прямого совпадения `smoke-select` —
единственная прямая связь (`smoke_room_settings`) прогнана; `setConfig`
как источник совпадения разобран и сознательно не расширен (см. выше).
- `golden:verify`, `pytest tests_backend`, `npm run invariants`,
performance-профили — не требуются по диффу/AC (нет правки Python, нет
правки геометрии комнат, метки `ci:golden` нет, перф не затронут). Решение
по golden совпадает с прецедентом #742 на идентичном паттерне правки.
- Ручная проверка в браузере (глазами) — доказательство построено на
`getComputedStyle`/`getAnimations`, то есть на тех же сигналах, которые
увидел бы человек на первом кадре; отдельно её не повторял.
- Пакетное/ночное ревью, release-review — вне объёма этого раунда (§8:
объём гейтов соразмерен задаче).
---
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/745-space-card-room-keys`, коммит `ca708d8443ae` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `799d5ef74e6afa50d64480013db2799c42b848d0`
```
git log --all --format='%H %T' | grep 799d5ef74e6a
```
- Тело issue: `8c17362760b68e61c8c6b00cda0e18656cacfb312c80e5828034f3cc79599971`
- Вердикт конвейера: `green` · High 0 · маршрут `fix`