mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
@@ -0,0 +1,211 @@
|
||||
# Код-ревью — issue #210, цикл r1
|
||||
|
||||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/210
|
||||
- **ТЗ:** `docs/specs/210-fixed-floor-card.md` (зелёное ревью, r2,
|
||||
`docs/reviews/SPEC-REVIEW-210-r2.md`)
|
||||
- **Диапазон:** `origin/dev..HEAD`, единственный продуктовый коммит
|
||||
`6063eea` («fix: pin card instances to configured spaces»), трейлеры
|
||||
`Issue: #210` / `User-Visible: yes` на месте.
|
||||
- **Трек:** обычный (не `small`/`trivial`) — лимит циклов 4.
|
||||
- **Вердикт: зелёный · цикл r1/4 · High: 0 · Medium: 0**
|
||||
|
||||
## Скоуп ревью
|
||||
|
||||
Проверялся диапазон `git diff origin/dev...HEAD` против принятого ТЗ:
|
||||
`src/types.ts`, `src/initial-load.ts`, `src/houseplan-card.ts`, `src/editor.ts`,
|
||||
`src/styles.ts`, `src/i18n/{en,ru}.json`, тесты
|
||||
(`test/fixed-floor-contract.test.mjs`, обновления
|
||||
`test/initial-load.test.mjs`, `test/optional-space-model-contract.test.mjs`),
|
||||
новый смок `demo/smoke_fixed_floor.mjs`, `scripts/mutation-gate.mjs`, три
|
||||
tracked bundle-копии и documentation-артефакты (`docs/ARCHITECTURE.md`,
|
||||
`docs/TESTING.md`, `docs/USER-GUIDE{,.ru}.md`, оба changelog,
|
||||
`docs/specs/README.md`, `docs/images/screenshots.json`). Backend
|
||||
(`custom_components/**/*.py`) и golden-baselines не тронуты — вне скоупа ТЗ,
|
||||
подтверждено `git diff --stat`.
|
||||
|
||||
## Как проверялось
|
||||
|
||||
Сессия свежая, без контекста ревью ТЗ (§6 PROCESS.md). Прочитаны
|
||||
`docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` целиком в этой сессии, тело issue
|
||||
#210 и все комментарии (включая рекомендацию по имплементации и хендофф
|
||||
автора), ТЗ `210-fixed-floor-card.md` целиком и оба документа ревью ТЗ.
|
||||
|
||||
Код читался построчно по каждой зоне, названной в ТЗ §11
|
||||
(«Ожидаемые зоны изменений»), а не только по диффу:
|
||||
|
||||
1. **Pure resolver** (`src/initial-load.ts`, `resolveFixedFloor`) — проверена
|
||||
каждая ветка: absent → нет свойства; строка → пустая невалидна, иначе
|
||||
точное совпадение по `spaceIds`, без coercion; число → `Number.isFinite`,
|
||||
`Number.isInteger`, `>= 0`, индекс в диапазоне; любой другой `typeof` →
|
||||
`invalid-type`. Побайтово сверено, что `resolveInitialSpace` (legacy) не
|
||||
изменена ни на строку — из диффа `src/initial-load.ts` удалена только одна
|
||||
строка (сигнатура типа `InitialSpaceSource`, аддитивно расширенная), тело
|
||||
легаси-функции нетронуто.
|
||||
2. **Единая mutation boundary** (`src/houseplan-card.ts`,
|
||||
`_canCommitSpace`/`_commitSpace`) — грепом `this\._space\s*=(?!=)`
|
||||
подтверждено ровно одно вхождение прямого присваивания во всём файле, и
|
||||
оно лежит внутри самого `_commitSpace`. Все 14 остальных мест, где раньше
|
||||
стояло `this._space = …` (слайд, hash, warm-viewport, `setConfig`,
|
||||
`default_floor`, создание/удаление пространства, backup-import, geometry
|
||||
drag, marker-dialog переходы), переведены на `_commitSpace`/`_slideTo` с
|
||||
проверкой возврата там, где переход мог быть отклонён.
|
||||
`test/fixed-floor-contract.test.mjs` держит этот инвариант структурным
|
||||
тестом по исходнику — деградация до regex-проверки признана достаточной,
|
||||
т.к. независимо от неё это же перепроверено прогоном мутанта (см. ниже).
|
||||
3. **Приоритет и pending-состояние** — `_fixedFloorState()` до получения
|
||||
авторитетного ответа сервера (`this._loadOk`) переводит valid/out-of-range
|
||||
numeric index и valid/unknown-id string в `pending`, а не даёт немедленный
|
||||
`valid`/`invalid`: числовой индекс не может быть принят из непроверенного
|
||||
кэша (порядок пространств мог измениться), а `unknown-id` от кэша не должен
|
||||
стать терминальной ошибкой раньше свежей загрузки — ровно то, что требует
|
||||
ТЗ §7 и таблица рисков §14. Прочитано разбором, эмпирически не воспроизведено
|
||||
(см. «Чего не проверял»).
|
||||
4. **GUI editor** (`src/editor.ts`) — синтетический токен
|
||||
`__houseplan_yaml_floor_index__:<n>` для числового YAML-значения:
|
||||
подтверждено, что `_formData` подставляет токен как текущее значение
|
||||
селекта, `_schema` включает его как единственный дополнительный вариант, а
|
||||
`_valueChanged` преобразует `''` → `delete config.floor` и токен обратно в
|
||||
исходное число. Обратное преобразование опирается на контракт HA
|
||||
`ha-selector-select` («select эмитит ровно `value` выбранной опции») — это
|
||||
тот же admitted-риск, что спек-ревью r2 явно оставило коду-ревью
|
||||
(«не тестировалось эмпирически»); в демо-стенде `ha-form`/`ha-selector`
|
||||
не подключены ни в одном существующем редакторном смоке
|
||||
(`grep` по `demo/*.mjs` не нашёл ни одного использования этих тегов кроме
|
||||
объявления в `src/editor.ts`), так что это ограничение стенда, а не
|
||||
упущение задачи — оценка риска ниже.
|
||||
5. **View/kiosk gating** — `navigationSpaces`, `.tabadd`, `_swipeZone`,
|
||||
`.kioskdots`, `_syncCycleTimer`/`_cycleTick` — каждое условие читает
|
||||
`this._hasFixedFloor` симметрично на всех точках входа (рендер вкладок,
|
||||
свайп-зона, доты, install/re-sync таймера кружки).
|
||||
6. **Compatibility** — `default_floor` путь (`if (!this._hasFixedFloor &&
|
||||
config.default_floor) this._commitSpace(config.default_floor, true)`) и
|
||||
`_savedNav`/`_saveNav` (`if (this._hasFixedFloor) return null/return;`)
|
||||
читаны построчно: отсутствие `floor` не меняет ни один из легаси-путей.
|
||||
7. Прочитаны все затронутые документы: `docs/ARCHITECTURE.md`,
|
||||
`docs/TESTING.md`, `docs/USER-GUIDE.md`/`.ru.md`, оба changelog,
|
||||
`docs/specs/README.md` — термины совпадают с ТЗ и друг с другом (Initial
|
||||
space / Стартовое пространство; Fixed space / Закреплённое пространство).
|
||||
|
||||
## Гейты — что прогнано и с каким результатом
|
||||
|
||||
| Гейт | Команда | Результат |
|
||||
|---|---|---|
|
||||
| Typecheck | `npx tsc --noEmit` | OK, без вывода |
|
||||
| Unit | `npm test` | `# tests 944 / pass 944 / fail 0` |
|
||||
| Build + bundle sync | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js` | OK, обе копии побайтово совпадают с `dist/` |
|
||||
| Целевой смок (новый) | `node demo/smoke_fixed_floor.mjs` | OK, 14/14 фактов true |
|
||||
| Регрессия навигации | `node demo/smoke_nav_persist.mjs` | OK, 7/7 фактов true |
|
||||
| Регрессия kiosk | `node demo/smoke_kiosk.mjs` | OK, 12/12 фактов true |
|
||||
| Blast-radius: warm remount (диф трогает `_warmAdopt`/`_warmVpArmed`) | `node demo/smoke_warm_remount.mjs` | OK, 14/14 фактов true |
|
||||
| Blast-radius: пустое пространство (диф трогает тот же `_space=''` cleanup) | `node demo/smoke_optional_space_model.mjs` | OK, 9/9 фактов true |
|
||||
| Docs-гейт | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 10 external links)` |
|
||||
| Whitespace | `git diff origin/dev...HEAD --check` | чисто, без вывода |
|
||||
| **Дисциплина «тест умеет падать»** для нового guard-мутанта | `node scripts/mutation-gate.mjs --id=fixed-floor-transition-guard-bypassed` | `ok чистый прогон` + `ok fixed-floor-transition-guard-bypassed: тест покраснел, как обязан` — независимо подтверждено ревьюером, не только со слов автора |
|
||||
|
||||
**Не прогонялось и почему:**
|
||||
|
||||
- Полный набор из 127 браузерных смоков — диф не задевает весь продукт;
|
||||
прогнаны названные в плане тестирования ТЗ (`smoke_fixed_floor`,
|
||||
`smoke_nav_persist`, `smoke_kiosk`) плюс два дополнительных смока по местам,
|
||||
которые диф трогает напрямую, но план тестирования не называл (warm remount,
|
||||
optional-space cleanup). Остальные touch/kiosk-смоки
|
||||
(`smoke_kiosk_pan_lock`, `smoke_touch_tips`, `smoke_isometric_live_touch`)
|
||||
не запускались: диф не меняет их код напрямую, а свайп/dots/cycle-путь,
|
||||
который он трогает, уже покрыт зелёным `smoke_kiosk.mjs`
|
||||
(`swipeSwitches`, `noSwipeZoomed`, `dblTapResets`, `iconScaleWorks`).
|
||||
- `npm run golden:verify` — диф не меняет визуальную геометрию/раскладку;
|
||||
ошибочный и invalid-state рендерятся как обычный `ha-card` с DOM/computed
|
||||
assertions в смоке, что и обещало ТЗ §13 («отдельный visual golden не
|
||||
нужен»).
|
||||
- `python -m pytest tests_backend -q` — ни один файл
|
||||
`custom_components/houseplan/**/*.py` не тронут (`git diff --stat`
|
||||
подтверждает).
|
||||
- Performance-профили — не названы в AC и не затронуты (pure O(число
|
||||
пространств) резолвер, без per-frame/per-marker работы, ТЗ §14).
|
||||
- Полный mutation-gate (`node scripts/mutation-gate.mjs` без `--id`) — дорогой
|
||||
прогон всего реестра, это предрелизный гейт (§8 PROCESS.md); прогнан только
|
||||
новый мутант задачи, который и требовалось доказать.
|
||||
|
||||
## Находки
|
||||
|
||||
Ни одной High, ни одной Medium, ни одной Low после независимого чтения кода и
|
||||
прогона гейтов.
|
||||
|
||||
## Проверка по AC1…AC12
|
||||
|
||||
| AC | Как доказан | Вердикт |
|
||||
|---|---|---|
|
||||
| AC1 | `resolveFixedFloor` unit-тест (`test/initial-load.test.mjs`, «distinguishes absence…») + `smoke_fixed_floor.mjs` `idWinsEveryInitialSource` (валидный ID выигрывает при одновременно выставленных hash и saved nav на другой этаж). | Автотест, тест умеет падать (сам pure-резолвер тривиально ломается изменением порядка веток — проверено чтением условий). |
|
||||
| AC2 | Unit-таблица невалидных числовых кейсов + `spaceIds: ['1', 'other'], floor: '1'` → `id: '1', source: 'id'` (строка не превращается в индекс) + `smoke_fixed_floor.mjs` `indexUsesServerOrder`. | Автотест. |
|
||||
| AC3 | Unit (`empty-id`, `unknown-id`, `non-finite-index` и т.д.) + `smoke_fixed_floor.mjs` `invalidFailsVisibly` (data-attr `reason=unknown-id`, `role="alert"`, нет `.stage`). Stale-cache-ветка (`pending`) проверена чтением `_fixedFloorState`, не исполнением — см. «Чего не проверял». | Автотест + частично чтением. |
|
||||
| AC4 | `smoke_fixed_floor.mjs` держит одновременно смонтированными `fixedId` (ID) и `fixedIndex` (index) плюс позже `ordinary` при общем `houseplan_card_nav_v1` — `legacyCardStillRestoresNav` подтверждает, что соседняя обычная карточка не пострадала. | Автотест. |
|
||||
| AC5 | `fixedDoesNotWriteNav` (после `_saveNav()` на fixed-карточке общий ключ остаётся тем, что был до неё) + структурный тест `test/fixed-floor-contract.test.mjs` (`_savedNav`/`_saveNav` гварды по regex) + `legacyCardStillRestoresNav`. | Автотест. |
|
||||
| AC6 | `tabAndTransitionBlocked` (`_commitSpace(ids[1])` возвращает `false`), `hashIgnored`, `fixedKioskCycleDisabled`, `fixedKioskSwipeDisabled` + грепом подтверждено единственное прямое присваивание `_space` во всём файле (структурная гарантия, не только тест). | Автотест + чтением (инвентаризация присваиваний). |
|
||||
| AC7 | `onlyFixedTab` (ровно один таб, без `.tabadd`), `fixedKioskDotsHidden`. Права gear/editors не менялись (диф не трогает `_canEdit`) — проверено чтением: рендер вкладок и gear используют тот же `_spaceModel()`/`_canEdit`, что и раньше. | Автотест + чтением. |
|
||||
| AC8 | `editorPreservesYamlIndex`, `editorClearDeletesKey` + структурный тест на точные строки преобразования токена. Реальное поведение `ha-selector-select` при клике в браузере не воспроизведено — деманд стенда, см. выше. | Автотест (метод) + чтением контракта HA-селектора. |
|
||||
| AC9 | Чтением: `setConfig` сбрасывает `_hashApplied`/`_navApplied`/`_warmVpArmed` при `fixedChanged` и безусловно перевызывает `_adoptInitialSpace`; оба сетевых обработчика (`_adoptStructuralResponses` + WS update) вызывают `_adoptInitialSpace(this._model, true)` после каждого принятого server config, что переоценивает `floor` заново и переводит карточку в invalid при удалённом ID. `smoke_fixed_floor.mjs` `remountStillFixed` подтверждает воспроизведение после технического remount, но не «удалить пространство через gear своего же экземпляра и получить invalid по live-ответу сервера» — это разобрано чтением, не исполнением. | Автотест (частично) + явно «проверено чтением, не исполнением». |
|
||||
| AC10 | `smoke_kiosk.mjs` (не тронутый `iconScaleWorks`, `dblTapResets`, `noSwipeZoomed` и т.д. — регрессии нет) + `smoke_nav_persist.mjs`/`smoke_warm_remount.mjs`/`smoke_optional_space_model.mjs` зелёные на blast-radius, который диф трогает напрямую. Touch-специфичные смоки не перезапускались — их код не тронут. | Автотест. |
|
||||
| AC11 | `git diff --stat` подтверждает правки в обоих `docs/CHANGELOG*.md` со ссылкой на #210 в том же коммите, `docs/USER-GUIDE{,.ru}.md` объясняют `floor` vs `default_floor`, ID vs index, invalid-поведение; `node scripts/check-docs.mjs` зелёный. | Автотест (provenance/docs check) + чтением текста. |
|
||||
| AC12 | `typecheck`/`test`/`build`/named smokes зелёные (таблица выше); `node scripts/mutation-gate.mjs --id=fixed-floor-transition-guard-bypassed` независимо подтверждает, что чистый бандл проходит, а мутированный (guard инвертирован) красит `smoke_fixed_floor.mjs` — «умеет падать» доказано ревьюером, не только записью автора. | Автотест, независимо перепрогнан. |
|
||||
|
||||
## Проверено и признано корректным
|
||||
|
||||
- Единственная mutation boundary реально единственная (грепом, не на слово
|
||||
автора): весь легаси-код навигации (`_slideTo`, hash-listener,
|
||||
`setConfig`/`default_floor`, warm-viewport adopt, создание/удаление
|
||||
пространства, geometry-drag commit, backup-import, marker-dialog
|
||||
cross-space переходы) переведён на `_commitSpace`/`_canCommitSpace`.
|
||||
- `pending`-состояние корректно закрывает риск «кэш на миг покажет чужой
|
||||
индексированный этаж» из ТЗ §14: числовой индекс и cache-only `unknown-id`
|
||||
не финализируются до `this._loadOk`; остальные типы невалидности (пустая
|
||||
строка, дробное/отрицательное/бесконечное число, посторонний тип) не зависят
|
||||
от модели и репортятся немедленно.
|
||||
- Легаси-путь (нет `floor`) не тронут ни строкой в `resolveInitialSpace`;
|
||||
расширение union-типа `InitialSpaceSource` аддитивно.
|
||||
- GUI: удаление ключа при очистке и сохранение YAML-индекса при несвязанном
|
||||
редактировании реализованы так, как описывало исправление M1 в ТЗ; риск,
|
||||
прямо оставленный код-ревью спек-ревьюером, разобран и признан низким по
|
||||
причине, названной выше (харнесс не подключает `ha-form` вообще ни для
|
||||
одного существующего редакторного поля — это не новый риск задачи).
|
||||
- Терминология синхронизирована: «Initial space / Стартовое пространство» и
|
||||
«Fixed space / Закреплённое пространство» одинаковы в GUI-строках, RU/EN
|
||||
guide и changelog.
|
||||
- Коммит один, трейлеры `Issue: #210` / `User-Visible: yes` на месте, оба
|
||||
changelog в нём же; сгенерированные копии бандла побайтово идентичны.
|
||||
|
||||
## Чего не проверял
|
||||
|
||||
- **Реальное поведение `ha-selector-select` в браузере** при явном клике по
|
||||
пустому варианту — демо-стенд не подключает `ha-form`/`ha-selector` ни для
|
||||
одного существующего поля редактора (grep по `demo/*.mjs` не нашёл ни одного
|
||||
прецедента), поэтому и `smoke_fixed_floor.mjs` вызывает `_valueChanged`
|
||||
напрямую с сконструированным `CustomEvent`. Это ограничение стенда, не
|
||||
этой задачи; риск оценён как низкий по стандартному контракту HA select
|
||||
(эмитит ровно `value` выбранной опции).
|
||||
- **`pending`-рендер** (`data-fixed-floor-state="pending"`) не пойман ни одним
|
||||
тестом — существующий смок ждёт `await wait(450)` и `updateComplete` перед
|
||||
проверками, к этому моменту `_loadOk` уже true. Логика разобрана чтением
|
||||
(см. выше) и признана корректной, но живой DOM с `role="status"` в состоянии
|
||||
загрузки эмпирически не воспроизведён.
|
||||
- **AC9, сценарий «удалить закреплённое пространство через его же gear и
|
||||
дождаться invalid по live server response»** — разобран чтением
|
||||
(`_adoptInitialSpace` вызывается после каждого принятого конфига), не
|
||||
исполнением: ни unit, ни browser-тест не воспроизводит именно эту
|
||||
последовательность (delete → save → WS update → invalid render).
|
||||
- Полный набор 127 смоков, `golden:verify`, `performance_smoke`,
|
||||
`pytest tests_backend` — не запускались; обоснование в таблице гейтов выше,
|
||||
ни один из этих гейтов не относится к тронутым файлам или AC.
|
||||
- Ручного тестирования в браузере HA (не demo-стенде) не было — вне
|
||||
процесса на этой стадии.
|
||||
|
||||
## Итог
|
||||
|
||||
ТЗ выполнено без сужения и без расширения скоупа. Единая mutation boundary
|
||||
подтверждена структурно (один `this._space =` во всём файле) и мутационным
|
||||
прогоном, а не только описанием автора. Компатибильность легаси-пути
|
||||
подтверждена побайтовой неизменностью `resolveInitialSpace`. Два риска,
|
||||
явно вынесенных на код-ревью спек-ревьюером (`ha-selector` clear-контракт и
|
||||
stale-cache pending-ветка), разобраны чтением и признаны низкими без находок,
|
||||
требующих правки. Новых High/Medium/Low не найдено.
|
||||
|
||||
**Вердикт: зелёный · цикл r1/4 · High: 0 · Medium: 0**
|
||||
Reference in New Issue
Block a user