mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
Validate / docs (push) Failing after 25s
Validate / reuse (push) Successful in 32s
Validate / provenance (push) Successful in 1m8s
Validate / process-gate (push) Failing after 1m20s
Validate / changes (push) Successful in 1m12s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / frontend (push) Skipped
Validate / smoke (push) Skipped
Validate / golden (push) Skipped
Validate / performance_smoke (push) Skipped
Validate / backend (push) Skipped
Issue: #244 User-Visible: no
244 lines
23 KiB
Markdown
244 lines
23 KiB
Markdown
# CODE-REVIEW-244-r3
|
||
|
||
- Issue: [#244](https://github.com/Matysh/houseplan-card/issues/244) — восстановление маркеров с мёртвой ссылкой на пространство
|
||
- Этап: code (PROCESS.md §2.7)
|
||
- Заход: r3 · блокирующих циклов израсходовано 1 из 4 (потрачен только в r1 — жёлтый/красный тратит бюджет, r2 зелёный цикла не образовал, #227)
|
||
- Кандидат (HEAD): `fe70c1e80303650113a7aafc74983defc27d8841`
|
||
- `origin/dev` на момент разбора: `0c5683d3bf1c79f01e1d52ee544c0fdff4f67a1c` (после релиза v1.67.0-beta.2)
|
||
- SHA предыдущего раунда (r2, зелёный): `a9466da649c5a24ad84cc79b0e6389b724dc8087` (недостижим напрямую — переписан ребейзом, диагностировано по `docs/reviews/CODE-REVIEW-244-r2.md`, который остался в дереве)
|
||
|
||
## Почему этот заход не по дельте, а полный
|
||
|
||
Между r2 (зелёным) и этим заходом код не правился — ветка была перебазирована
|
||
на ушедший вперёд `origin/dev` (комментарий автора `2026-08-22T21:37:13Z`):
|
||
слияние конфликтовало из-за выпущенного в `dev` релиза `v1.67.0-beta.2`
|
||
(`1af9cfc`) и последующих `005eb7f`/`0c5683d`. Это ровно случай из инструкции
|
||
ревью — «ребейз на ушедший вперёд dev — после ребейза это другой код,
|
||
§7.2» — поэтому разбор в этом раунде полный, а не по дельте продуктового
|
||
кода. Экономия достигается не сокращением строгости, а тем, что часть
|
||
доказательства — байтовое сравнение — быстрее многократного построчного
|
||
чтения:
|
||
|
||
1. Список файлов в `git diff origin/dev...HEAD` (43 файла) идентичен
|
||
по составу объединению диффов r1 (40 файлов) и r2 (19 файлов, из них 17
|
||
пересекались с r1) плюс собственный документ `CODE-REVIEW-244-r2.md` —
|
||
новых, не объяснённых предыдущими раундами файлов нет.
|
||
2. `custom_components/houseplan/const.py`: строка `VERSION = "1.67.0-beta.2"`
|
||
не тронута этой веткой (она пришла из релиза и является контекстом, а не
|
||
диффом) — правки диффа ограничены `PLAN_MODEL_VERSION 6 → 7`, как и было
|
||
на кандидате r2.
|
||
3. Пересборка (`npm run build`) на этом HEAD даёт **тот же** SHA-256
|
||
бандла (`9316d1497b6ff0e24403db319df1756b2e420d5a9c87a2c7dcd8d622057fbeda`),
|
||
что и заявлен автором в комментарии о ребейзе — компилируемый TS/JS не
|
||
изменился по сравнению с состоянием, которое проверял r2.
|
||
4. Единственная содержательная правка при разрешении конфликта — в
|
||
`docs/CHANGELOG.md`/`.ru.md`: секция `## Unreleased` с записью #244
|
||
осталась выше уже выпущенной `## v1.67.0-beta.2 — 2026-08-22`, обе записи
|
||
не искажены (проверено чтением, см. ниже).
|
||
5. Коммиты релиза, вошедшие в `dev` между базой r1/r2 и текущей базой
|
||
(`1af9cfc` Release v1.67.0-beta.2, `005eb7f` golden-эталоны, `0c5683d`
|
||
fix #247 для `test/docs-accept.test.mjs`), не касаются
|
||
Optimize/import/delete/editor-путей #244: правки — версия/манифест/
|
||
релиз-ноуты, golden-baseline PNG для стыков стен/полок/трея (#233-класс),
|
||
кросс-платформенный путь фикстуры. Пересечения сценариев нет.
|
||
|
||
Исходя из этого разбор построен так: свежий прогон всех обязательных гейтов
|
||
на текущем HEAD (не унаследован — код по факту другой коммит), независимое
|
||
чтение критичных модулей заново (не только «принято на слово» от r1/r2), и
|
||
унаследование содержательных находок AC1–AC14 из r1/r2 там, где чтение и
|
||
байтовое сравнение подтверждают отсутствие изменений.
|
||
|
||
## Скоуп
|
||
|
||
Normal-трек (`bug`, `P2`). Диапазон `git diff origin/dev...HEAD`: 43 файла,
|
||
включает reference-repair pass (`src/space-reference-repair.ts`),
|
||
delete-preflight (`src/space-deletion.ts`), интеграцию в
|
||
`src/plan-optimizer.ts` (`PLAN_MODEL_VERSION` 6→7), редактор карточки
|
||
(`src/editor.ts`, `src/card-editor-validation.ts`), backend
|
||
`custom_components/houseplan/websocket_api.py` (`houseplan/space/delete`) и
|
||
`import_export.py` (`_repair_target_space_refs`), тесты (unit/backend/smoke/
|
||
golden) и документацию (ТЗ, оба CHANGELOG, USER-GUIDE.md/.ru.md,
|
||
CONFIG-COMPATIBILITY.md, TESTING.md, ARCHITECTURE.md, docs/reviews/*).
|
||
|
||
## Как проверялось
|
||
|
||
| Гейт | Команда | Результат |
|
||
|---|---|---|
|
||
| Typecheck | `npx tsc --noEmit` | PASS, без вывода |
|
||
| Unit | `npm test` | **1114/1114 PASS**, 0 skipped (было 1113 на кандидате r2 — плюс тест из `test/docs-accept.test.mjs` для фикса #247, не относящегося к #244) |
|
||
| Build + bundle sync | `npm run build` + `sha256sum` трёх копий | PASS; `dist/houseplan-card.js`, `demo/srv/assets/houseplan-card.js`, `custom_components/houseplan/frontend/houseplan-card.js` — идентичный SHA-256 `9316d1497b6ff0e24403db319df1756b2e420d5a9c87a2c7dcd8d622057fbeda`, совпадает с заявленным в комментарии о ребейзе; `git status --short` после сборки чист |
|
||
| Docs fingerprint | `node scripts/check-docs.mjs` (diff трогает `src/**`) | PASS: «Documentation checks passed (7 files, 10 external links)» |
|
||
| Backend | `python -m pytest tests_backend -q` (в песочнице ревью не было `pytest-homeassistant-custom-component`/`home-assistant-frontend`, поставил тем же набором пакетов, что CI-workflow: `pip install pytest voluptuous pytest-homeassistant-custom-component home-assistant-frontend`) | **352 passed, 1 error** — `test_ha_upload.py::test_upload_ok`, тот же `_run_safe_shutdown_loop`/`threading._DummyThread` teardown-артефакт, что в r1/r2, независим от диффа (не трогает `websocket_api.py`/`import_export.py` в части, которую использует этот тест) |
|
||
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | — | 41 символ на изменённых строках, порог широкого символа 33 → не превышен. 19 прямых совпадений (список идентичен r1), 29 слабых связей (список идентичен r1) |
|
||
| Целевые smoke, прямо названные в таблице AC (AC1/AC7/AC9/AC10/AC11) | `node demo/smoke_orphan_space_references.mjs` | **PASS**, все 10 ассертов `true`, включая `deleteExplainsBlockerWithoutConfirmOrWrite: true` |
|
||
| — | `node demo/smoke_optional_space_model.mjs` | **PASS**, все 11 ассертов `true`, включая `deleteLastUsesAuthoritativeEndpoint: true`, `deleteLastPreservesMarkersWithoutPlacement: true` |
|
||
| — | `node demo/smoke_optimize_geometry_preflight.mjs` | **PASS**, все 16 ассертов `true` |
|
||
| — | `node demo/smoke_fixed_floor.mjs` | **PASS**, все 14 ассертов `true` |
|
||
| `npm run golden:verify`, остальные 15 прямых + 29 слабых smoke, `mutation-gate.mjs`, performance | — | не прогонялись — см. «Чего не проверял» |
|
||
|
||
### Решение по каждой строке `smoke-select` (подтверждено заново на текущей базе)
|
||
|
||
**Прямое совпадение (19)** — прогнаны 4 (см. таблицу выше): это
|
||
единственные, где названный символ прямо принадлежит изменённой логике
|
||
удаления/Optimize/редактора и прямо назван в таблице AC ТЗ как способ
|
||
доказательства (AC1, AC7, AC9, AC10, AC11). Остальные 15
|
||
(`smoke_linked_virtual_light`, `smoke_save_race`, `smoke_virtual_light_toggle`,
|
||
`smoke_audit_1490`, `smoke_bg_color`, `smoke_config_writer`,
|
||
`smoke_device_preview_parity`, `smoke_dialog_footer_width`,
|
||
`smoke_general_settings`, `smoke_layout_sync`, `smoke_plan_upload_race`,
|
||
`smoke_climate_once`, `smoke_editor_tabs`, `smoke_infinite_canvas`,
|
||
`smoke_ws_resilience`) не прогнаны: совпадение только по общему плумбингу
|
||
(`_cfgRev`, `_layoutRev`, `_config`, `_persistLayout`, `_reloadConfigOnly`,
|
||
`buildDevices`, `Layout`), который #244 читает, а не переопределяет. Список и
|
||
основание идентичны r1 — рекомпиляция и ребейз не изменили набор изменённых
|
||
символов.
|
||
|
||
**Слабая связь (29)** — не прогнаны: все совпадения по одному
|
||
распространённому имени (`_config`, `_spaceDialog`, `_cfgRev`), диалоги
|
||
пространства/цвета/тегов не меняли контракт в этом диффе. Тот же список, что
|
||
и в r1/r2.
|
||
|
||
## Находки
|
||
|
||
Нет. Продуктовый код идентичен зелёному кандидату r2 (подтверждено байтовым
|
||
сравнением бандла и построчным чтением ключевых модулей ниже); ребейз не
|
||
внёс регрессий, конфликт CHANGELOG разрешён корректно.
|
||
|
||
## Закрытие раунда r2
|
||
|
||
| Пункт r2 | Статус на r3 | Где это видно |
|
||
|---|---|---|
|
||
| Находок в r2 не было (H1/H2 из r1 закрыты) | Не переоткрывались: продуктовый код с r2 не менялся, только база | байт-в-байт совпадающий SHA-256 бандла; идентичный список файлов диффа |
|
||
| Требование «повторный код-ревью после ребейза — не формальность» (комментарий автора `2026-08-22T21:33:58Z`) | Выполнено этим документом: полный набор обязательных гейтов перезапущен на новом HEAD, а не унаследован | таблица «Как проверялось» выше — все команды выполнены заново на `fe70c1e`, не скопированы из r2 |
|
||
|
||
## Унаследовано из r1/r2 (без повторного построчного чтения всех файлов)
|
||
|
||
Документы: `docs/reviews/CODE-REVIEW-244-r1.md` (кандидат `a09338f`),
|
||
`docs/reviews/CODE-REVIEW-244-r2.md` (кандидат `a9466da`).
|
||
|
||
- Диагноз §3 ТЗ и его соответствие коду (`resolveExplicitMarkerPlacement`,
|
||
приоритет `marker.space` для virtual-маркера, фильтр рендера View/Static,
|
||
`build_space_merge()`, `resolveInitialSpace()`) — не перепроверялся заново
|
||
построчно; косвенно подтверждён тем, что `src/space-reference-repair.ts`
|
||
(прочитан заново целиком в этом раунде, см. ниже) реализует именно этот
|
||
контракт без расхождений.
|
||
- AC8 (импорт одного пространства, `_repair_target_space_refs`) и AC11
|
||
(`default_floor` в редакторе карточки) — не перечитывались заново
|
||
построчно; диффы этих файлов идентичны r1 (не тронуты r2 и ребейзом),
|
||
тесты `test_ha_import_export.py`/`card-editor-validation.test.mjs`
|
||
перепрогнаны в составе `npm test`/`pytest` этого раунда и зелёные.
|
||
- Touch/kiosk паритет View/Static-рендера (закрыт на этапе spec,
|
||
`SPEC-REVIEW-244-r2`) — рендер-путь не менялся ни в r1/r2, ни ребейзом.
|
||
- Mutation-паттерны трёх frontend-рисков (`orphan-space-detach-disabled`,
|
||
`orphan-space-ambiguous-signature-guessed`,
|
||
`orphan-space-area-keeps-stale-position`) — прочитаны в r1 построчно,
|
||
сам `mutation-gate.mjs` не перезапускался ни разу за три раунда
|
||
(предрелизный гейт).
|
||
|
||
## Что проверено и корректно (перечитано заново в этом раунде)
|
||
|
||
- **`src/space-reference-repair.ts`** — прочитан целиком заново. Порядок
|
||
правил для активного маркера (`signatureSpace` → `uniqueAreaRoom` для
|
||
non-virtual → `delete marker.space`) совпадает со спекой §8.2; virtual
|
||
binding пропускает Area-remap и уходит в signature/detach; removed-маркер
|
||
получает только доказуемый signature (ветки `!isRemoved` его исключают из
|
||
Area/detach) — dead-ссылка tombstone остаётся нетронутой и это
|
||
корректно, не регресс. Позиции: `exact && positionSpace === storedSpace`
|
||
переносит `x/y/k` при sig-remap, иначе `!isRemoved` удаляет layout-запись
|
||
целиком (Area/detach) — совпадает со спекой §8.3. `reversibleStem` режет
|
||
на длине 35 и формате id, `spaceSignatures.get(oldId)?.length === 1`
|
||
требует ровно один кандидат — защита от ложного ремапа (AC3) на месте.
|
||
`remainingDead`/`positionsUnresolved` считают removed/opaque владельцев
|
||
верно (`!activeMarkerIds.has(key) || removedMarkerIds.has(key)`).
|
||
- **`src/space-deletion.ts`** — прочитан целиком заново.
|
||
`collectSpaceMarkerDependencies` дедуплицирует маркер по трём источникам
|
||
ссылки (`marker.space`, `room_id` через комнаты пространства, layout
|
||
`s`) в `Set`, считает один раз — соответствует AC9. `createSpaceDeletion
|
||
Candidate`: `dependencies.count && !deletingLastSpace` блокирует без
|
||
мутации входа (ранний `return`); при `deletingLastSpace` у всех
|
||
затронутых marker-записей (включая removed) снимаются ровно `space` и
|
||
`room_id`, остальные поля не трогаются; layout-записи с `s === spaceId`
|
||
удаляются целиком — совпадает с решением владельца по арбитражу H2 и
|
||
спекой §10.
|
||
- **`custom_components/houseplan/websocket_api.py` (`_space_marker_
|
||
dependencies`, `_space_delete_candidate`, `ws_space_delete`)** — прочитан
|
||
диапазон добавленного кода (187 строк) целиком заново. Backend-версия
|
||
структурно и по значению идентична frontend: тот же порядок проверок,
|
||
тот же набор снимаемых полей. `ws_space_delete` работает под
|
||
`rt.write_lock`, ревалидирует обе `expected_*_rev` внутри лока,
|
||
перепроверяет `dependencies`/`deleting_last_space` на **актуальных**
|
||
`current_config`/`current_layout` (не на входных данных запроса) — гонка,
|
||
где зависимость появилась между preflight и записью, поймана; использует
|
||
`CONFIG_SCHEMA`/`LAYOUT_SCHEMA` и существующий crash-safe
|
||
`_commit_import_pair` (pending/rollback), не вводит новый механизм записи.
|
||
- **`custom_components/houseplan/const.py`** — `PLAN_MODEL_VERSION` 6→7,
|
||
`VERSION` не тронут этим диффом (относится к релизу beta.2, слит без
|
||
конфликта в этой строке).
|
||
- **CHANGELOG-конфликт разрешён корректно** — `docs/CHANGELOG.md` и `.ru.md`
|
||
прочитаны целиком: секция `## Unreleased` с записью #244 (7 предложений,
|
||
описывает Optimize repair/detach/preview, delete-блокировку и
|
||
last-space-исключение, editor-предупреждение) стоит выше уже выпущенной
|
||
`## v1.67.0-beta.2 — 2026-08-22`; ни одна запись не потеряна и не задвоена.
|
||
Мелкий косметический артефакт: в RU-файле фраза «указывает на отсутствующее
|
||
пространство» перенесена на две отдельные строки внутри одного элемента
|
||
списка (`docs/CHANGELOG.ru.md:19-21`) — при рендере Markdown мягкие переносы
|
||
схлопываются в пробел, видимого дефекта нет; не поднимаю как находку.
|
||
- **`docs/CONFIG-COMPATIBILITY.md`, `docs/ARCHITECTURE.md`** — прочитаны
|
||
задетые разделы: везде `model-v6` корректно заменено на `model-v7`, новый
|
||
раздел «Space reference repair (model v7)» точно описывает реализованное
|
||
поведение (signature/Area/detach, one-space import remap, revision-guarded
|
||
delete, last-space-исключение), без противоречий коду.
|
||
- **Трейлеры** — оба `User-Visible: yes` коммита (`f6f877e`, `6a151d1`)
|
||
несут правки в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том же
|
||
коммите; все 13 коммитов диапазона несут `Issue: #244`.
|
||
- **Файловый состав диффа** — 43 файла, без стороннего golden-baseline PNG
|
||
(Optimize/delete/editor — не рендер-путь, совпадает с оценкой r2), без
|
||
случайных файлов вне объяснённого объёма r1+r2+ребейз.
|
||
|
||
## Чего не проверял
|
||
|
||
- **Полный browser smoke (167 сценариев)** — не запускался; `smoke-select`
|
||
на текущей базе не даёт оснований для полного прогона (порог 33 не
|
||
превышен, широких совпадений нет), диапазон изменённых символов
|
||
идентичен r1/r2. Предрелизный гейт (PROCESS.md §8).
|
||
- **`npm run golden:verify` / golden:accept** — не прогонялся; #244 не
|
||
меняет рендер/геометрию/стили/слои, только данные и ветвление
|
||
preflight/backend-транзакции — заключение унаследовано из r1/r2, диффом
|
||
ребейза golden-баз #244 не касается (только несвязанные PNG из релиза
|
||
beta.2, не входящие в диапазон `origin/dev...HEAD` этого ревью).
|
||
- **`scripts/mutation-gate.mjs`** — не запускался ни в одном из трёх
|
||
раундов; не в AC этой задачи, предрелизный гейт.
|
||
- **Performance-профили** — не запускались; путь не горячий (однократное
|
||
явное действие пользователя), не назван в AC.
|
||
- **Построчное чтение backend `import_export.py` (`_repair_target_space_
|
||
refs`) и frontend `editor.ts`/`card-editor-validation.ts` заново в этом
|
||
раунде** — не выполнялось; эти файлы не менялись между r1 и текущим HEAD
|
||
(проверено `git diff` — идентичны версии, прочитанной в r1 построчно),
|
||
их тесты перепрогнаны и зелёные в составе `npm test`/`pytest` этого
|
||
раунда.
|
||
- **Ручное тестирование в браузере** — не проводилось; вместо этого
|
||
прогнаны headless browser-smoke (`smoke_orphan_space_references`,
|
||
`smoke_optional_space_model`, `smoke_optimize_geometry_preflight`,
|
||
`smoke_fixed_floor`), которые управляют реальным `HouseplanCard`
|
||
элементом через Puppeteer/DOM — это заявленный в таблице AC способ
|
||
доказательства для AC1/AC7/AC9/AC10/AC11.
|
||
- **`docs/images/screenshots.json` PNG-содержимое** — не скачивал артефакт
|
||
workflow заново; ссылка на прогон (`.../runs/32599244099`) не изменилась
|
||
относительно r2, где уже была проверена как настоящая.
|
||
|
||
## Итог
|
||
|
||
Ребейз на ушедший вперёд `dev` не изменил продуктовый код #244: байтовое
|
||
совпадение пересобранного бандла, идентичный состав файлов диффа и
|
||
построчное чтение самых критичных модулей (`space-reference-repair.ts`,
|
||
`space-deletion.ts`, `ws_space_delete`) заново подтверждают это независимо
|
||
от заявления автора. Единственная содержательная правка ребейза —
|
||
разрешение конфликта в обоих CHANGELOG — корректна. Все обязательные гейты
|
||
перезапущены на новом HEAD и зелёные; четыре целевых smoke, названных в
|
||
таблице AC, зелёные. Новых находок нет, находки r1 (H1/H2) остаются
|
||
закрытыми.
|
||
|
||
**Вердикт: зелёный · заход r3 · блокирующих циклов 1/4 · High: 0 · Medium: 0 → в задаче**
|
||
|
||
Документ: `docs/reviews/CODE-REVIEW-244-r3.md`
|