Files
houseplan-card/docs/reviews/CODE-REVIEW-306-r2.md
T
claude[bot] 26ad7651fb
Validate / docs (push) Failing after 35s
Validate / process-workflow-sync (push) Successful in 1m7s
Validate / provenance (push) Successful in 1m29s
Validate / process-gate (push) Successful in 1m27s
Validate / changes (push) Successful in 1m15s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / frontend (push) Skipped
Validate / reuse (push) Successful in 1m10s
Validate / smoke (1) (push) Skipped
Validate / smoke (2) (push) Skipped
Validate / smoke (3) (push) Skipped
Validate / smoke_done (push) Skipped
Validate / golden (push) Skipped
Validate / performance_smoke (push) Skipped
Validate / backend (push) Skipped
docs: review document for #306
Issue: #306
User-Visible: no
2026-08-26 11:14:32 +00:00

187 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-306-r2
- **Issue:** [#306](https://github.com/Matysh/houseplan-card/issues/306) — нулевые стены вместо виртуальных границ
- **Этап:** code (PROCESS.md §2.7)
- **Заход:** r2 · блокирующих циклов израсходовано 1/4 (r1 — жёлтый, M1+M2 в скоупе, L1 на суждение автора; зелёный вердикт цикла не образует, §4/#227)
- **Ветка / SHA материала:** `issue/306-zero-thickness-walls` @ `9134d862667e907f877830207d78916697bf59f4` (`git status` чист, HEAD совпадает с SHA, названным автором в комментарии от 2026-08-26T11:07)
- **Предыдущий раунд:** `docs/reviews/CODE-REVIEW-306-r1.md`, вердикт жёлтый, материал `a66fb7ef53906bed33c6f92369b14e8e398be3bd`
## 1. Скоуп раунда — дельта, не задача целиком
Разбор ведётся по §2.10: r1 закрыл полный первый заход код-ревью (91 файл,
+4210/-2989), r2 — правки по трём находкам r1, ничего больше.
`a66fb7ef53906bed33c6f92369b14e8e398be3bd` — ancestor текущего HEAD (rebase не
происходил, `merge-base HEAD origin/dev == origin/dev`, ветка уже содержит весь
`dev`). Дельта `git diff a66fb7ef..HEAD` — 13 файлов, +406/-49:
```
custom_components/houseplan/frontend/houseplan-card.js | 13 +- (D, сгенерировано)
dist/houseplan-card.js | 13 +- (D, сгенерировано)
docs/CHANGELOG.md | 5 +-
docs/CHANGELOG.ru.md | 5 +-
docs/images/09-device-info.png | bin (D, сгенерировано)
docs/images/screenshots.json | 24 +-
docs/reviews/CODE-REVIEW-306-r1.md |290 + (документ этого же ревью)
src/houseplan-card.ts | 48 +-
src/i18n/en.json | 7 +-
src/i18n/ru.json | 7 +-
src/plan-optimizer.ts | 10 +-
test/i18n.test.mjs | 29 +-
test/plan-optimizer.test.mjs | 4 +-
```
Дельта локальна: не меняет контракт поведения (единая семантика `cm:0`, миграция,
Resize/Undo, backend-схема не тронуты), не задевает новую подсистему, не сопоставима
по объёму с задачей (13 файлов против 91, +406/-49 против +4210/-2989). Условия
«разбор остаётся полным» (ребейз на ушедший `dev`, смена контракта, новая
подсистема, сопоставимый объём) не выполнены — разбор по дельте, §2.10 п.4.
## 2. Как проверялось
### 2.1 Гейты — что прогнано лично
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | green, exit 0 |
| Unit | `npm test` | 1346 tests: 1345 passed, 1 skipped, 0 failed |
| Build + сверка бандлов | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js`, затем `npm run bundle:sync` | green; все три копии (`dist/`, `custom_components/houseplan/frontend/`, `demo/srv/assets/`) byte-identical, SHA-256 `189ad1c164d68494ef92fef48d50690e3c79d4b2fd46778cdd44900c30b7f16c` — совпадает с хешем, названным автором в хендоффе 2026-08-26T11:07 |
| Docs fingerprint | `node scripts/check-docs.mjs` | green: «Documentation checks passed (7 files, 10 external links)» — обязателен, дельта трогает `src/**` (i18n-строки, toast-wiring) |
| Смок-выборка по дельте | `node scripts/smoke-select.mjs --base a66fb7ef53906bed33c6f92369b14e8e398be3bd --head HEAD` | 15 «прямое совпадение» (`_showToast`, `_drawWallCm`, `NORM_W`); вывод и решение по каждой строке — §2.3 |
| Целевой браузерный смок (лично, Chromium песочницы) | `node demo/smoke_zero_walls.mjs` (после `npm run bundle:sync`) | green, все ключи `true`, включая `openingBlocksZero` (проверяет ровно путь `toast.zero_wall_opening_conflict`, переименованный этой правкой) |
### 2.2 Что НЕ прогонялось и почему
- **`node scripts/model-invariants.mjs`** — не прогонялся. Дельта не трогает
геометрию/ссылки на неё: изменение в `plan-optimizer.ts` — чисто счётный
рефакторинг (`legacyZeroContourLines(...)` вынесен в переменную и посчитана её
длина, тело цикла и его эффект на `config`/`layout` не изменились, проверено
построчным чтением диффа). `src/houseplan-card.ts` меняет только выбор ключа
i18n для существующих веток `_showToast`, ни одна точка записи геометрии не
задета. Гейт был green на предыдущем полном заходе (r1, `a66fb7ef`) и делта его
не касается.
- **`npm run golden:verify` / golden-эталоны** — не прогонялся и не принимался.
Дельта не меняет рендер, геометрию, стили или слои — только текст toast/Optimize
и одну ре-акцептированную документационную PNG (см. ниже), в `demo/golden/baselines/**`
изменений нет (подтверждено `git diff --stat` дельты).
- **`python -m pytest tests_backend -q`** — не прогонялся. Дельта не трогает
`custom_components/**/*.py` (подтверждено `git diff --stat`).
- **Полный набор `demo/smoke_*.mjs` (191 файл)** — не прогонялся, дельта не
задевает всё: точечная правка i18n-текста и Optimize-репортинга, не новый
контракт поведения. Полный набор — предрелизный гейт (§8), не гейт ревью.
- **14 из 15 «прямых совпадений» смок-выборки не прогнаны отдельно** — проверены
чтением на релевантность (детали §2.3), кроме уже прогнанного `smoke_zero_walls`.
- **Performance-профиль (`benchmark:wall-model`)** — не прогонялся: дельта не
трогает горячий путь геометрии/кэшей, в AC13/AC17 не названа.
### 2.3 Разбор смок-выборки по дельте
`smoke-select.mjs` вернул только категорию «прямое совпадение» (15 файлов, порог
широкого символа — 39 смоков, ни один совпавший символ его не достиг). Проверка
каждой строки чтением содержимого смока на предмет реальной, а не по имени,
связи с изменёнными строками:
| Смок | Символ-триггер | Решение |
|---|---|---|
| `smoke_zero_walls.mjs` | не в списке (совпадение вне выборки) | **прогнан** — прямо проверяет `openingBlocksZero` через `toast.zero_wall_opening_conflict` |
| `smoke_partition_openings.mjs` | `_showToast` | не прогнан — файл не содержит ссылок на `zero_wall_*`/`cmRaw`, `_showToast` matched на несвязанной строке (grep пуст на релевантных паттернах) |
| `smoke_room_resize.mjs` | `_showToast` | не прогнан — тот же результат: нет ссылок на изменённые ветки |
| `smoke_help_affordance.mjs` | `_showToast` | не прогнан — про подсказки интерфейса, не про zero-wall toasts |
| `smoke_optimize_coordinate_canonicalization.mjs` | `_showToast` | не прогнан — файл не проверяет `spansMerged`/`legacyZeroWallsMigrated`/`gs.optimize_changes` (grep пуст) |
| `smoke_draw_wall_thickness.mjs` | `_drawWallCm` | не прогнан — единственная проверка `_drawWallCm === 15`, положительная толщина, не задевает ветку `=== 0` |
| `smoke_unified_wall_tool.mjs`, `smoke_wall_chain_merge.mjs`, `smoke_wall_face_overlap.mjs` | `_drawWallCm` | не прогнаны — нет проверок ambiguous-ветки или `=== 0` (grep пуст) |
| `smoke_wall_chain_thickness.mjs` | `_drawWallCm` | не прогнан — проверяет `=== 30`, не относится к дельте |
| `smoke_active_chain_ink.mjs`, `smoke_drag_bounds.mjs`, `smoke_grid_snap.mjs`, `smoke_infinite_canvas.mjs`, `smoke_junction_holes.mjs`, `smoke_wallthick_standalone.mjs` | `NORM_W` | не прогнаны — `NORM_W` совпал вне контекста дельты (модульная константа, используется в сотнях не связанных мест) |
Ни один из непрогнанных смоков не содержит ассерта на изменённые строки — совпадения
по имени символа, не по поведению. Слабых связей (категория «НЕОПРЕДЕЛЁННОСТЬ») в
выдаче не было вовсе.
## 3. Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| **M1** (Medium, в скоупе) — Optimize называл `cm:0` «виртуальными» (`gs.optimize_changes`), смешанный счётчик `spansMerged` вместо раздельных «legacy virtual spans migrated» / «zero-wall atoms merged» (§8.4 ТЗ) | En/ru-текст `gs.optimize_changes` заменён на «merged zero-thickness wall fragments» / «объединено отрезков стен нулевой толщины»; добавлен отдельный ключ `gs.zero_walls_migrated` и поле `OptimizeReport.legacyZeroWallsMigrated`, посчитанное отдельно от `spansMerged` в цикле миграции; в разметке добавлена отдельная строка `${r.legacyZeroWallsMigrated ? html...}` рядом со строкой `spansMerged` | `src/i18n/en.json:820-821`, `src/i18n/ru.json:820-821`, `src/plan-optimizer.ts:60-61,485,516-518,701`, `src/houseplan-card.ts:16937-16939`; unit `test/i18n.test.mjs:53-77` (проверяет буквальный текст + `cardSource` ссылку), `test/plan-optimizer.test.mjs:600` (`assert.equal(first.report.legacyZeroWallsMigrated, 1)` — тест умеет падать: сборка без нового поля/счётчика провалит оба ассерта) |
| **M2** (Medium, в скоупе) — changelog не предупреждал о backup перед миграцией model v9, хотя §12.2 ТЗ прямо требует | В оба changelog добавлено предложение «Export a backup before the first structural save, import or **Optimize plans** that writes model v9: downgrade to an older version does not understand the new wall model, and restoring that backup is the supported way back» / рус. эквивалент | `docs/CHANGELOG.md:10-15`, `docs/CHANGELOG.ru.md:17-22`, оба в одном коммите `f15bdc67` (`User-Visible: yes`, оба changelog в том же коммите — трейлер-требование выполнено) |
| **L1** (Low, на суждение автора) — 4 i18n-ключа §15 ТЗ (`toast.zero_wall_opening_conflict`, `toast.zero_wall_ambiguous`, `toast.zero_wall_migration_blocked`, `gs.zero_walls_migrated`) переиспользовали обобщённые ключи вместо выделенных | Все четыре ключа заведены отдельно в en/ru и **подключены** в местах вызова: `toast.zero_wall_opening_conflict` заменил `toast.zero_wall_opening` в трёх местах блокировки проёма; `toast.zero_wall_ambiguous` подключён веткой `this._drawWallCm === 0 ? 'toast.zero_wall_ambiguous' : 'toast.wall_repair_ambiguous'` в обоих местах ambiguous-репейра стены; `toast.zero_wall_migration_blocked` подключён через новый хелпер `_showWallModelMigrationBlocked`, который сам выбирает специализированный или общий ключ по `_hasLegacyZeroWallFields()`; `gs.zero_walls_migrated` — см. M1 | `src/houseplan-card.ts:7350-7368` (хелперы), `:8143-8146,8221-8224` (ambiguous), `:8349,11721,11761` (opening conflict), `:7439,7571,15679` (migration blocked через хелпер); unit `test/i18n.test.mjs:72-91` — новый тест `issue 306 zero-wall failures have dedicated symmetric copy`, проверяет и текст, и присутствие имени ключа в собранном `cardSource` |
Все три находки закрыты по коду, не по заявлению автора: правки прочитаны построчно,
`_drawWallCm === 0` подтверждена как геттер активной толщины инструмента «Стены»
(`src/houseplan-card.ts:6700`, используется тем же паттерном в соседней проверке
`cmRaw === 0` строкой ниже), рефакторинг `plan-optimizer.ts` не меняет побочных
эффектов миграции (тело цикла идентично, добавлено только присваивание счётчика).
## 4. Новые находки
Нет. High: 0, Medium: 0, Low: 0.
Дельта — точечная (текст toast/changelog + разделение счётчика Optimize), риск
регрессии низкий: изменённые ветки — либо чистое добавление счётчика без влияния
на существующий эффект (M1), либо документация (M2), либо выбор строки по уже
существующему и повсеместно используемому в этом же файле предикату `=== 0` (L1).
`smoke_zero_walls` — единственный смок, реально проверяющий один из трёх
изменённых путей (`toast.zero_wall_opening_conflict`), зелёный.
## 5. Что проверено и корректно
- Trailers коммитов дельты: `f15bdc67` — `Issue: #306`, `User-Visible: yes`, оба
changelog в том же коммите; `db4e0ef9` — документ ревью, `User-Visible: no`;
`9134d862` — приёмка скриншотов, `Source:` на прогон CI, `User-Visible: no`. Все
три корректны по `PROCESS.md` §10.2/AGENTS.md.
- **Одно число — один источник**: `gs.zero_walls_migrated` показывает
`r.legacyZeroWallsMigrated`, `gs.optimize_changes` показывает `r.spansMerged` —
это два разных по смыслу счётчика (legacy-конвертация и слияние атомов),
каждый вычисляется и отображается ровно один раз, дублирования одной и той же
величины нет.
- Приёмка скриншотов (`docs/images/screenshots.json`, `09-device-info.png`)
ссылается на реальный прогон CI (`Source:` в коммите), проведена после смены
`sourceFingerprint`, вызванной правками `src/**`; `check-docs.mjs` подтверждает
соответствие текущему `src/**`. Единственное изменённое изображение —
`09-device-info.png`, что совпадает с заявлением автора («17 пикселей,
delta=1»); остальные девять кадров сохранили прежний `imageSha256` при новом
`sourceSha256` — ожидаемо для чисто-текстовых изменений, не меняющих их вёрстку.
- AC13 (Optimize показывает counts) и AC18 (документация/changelog описывают
backup) — единственные AC, доказательство которых дельта задевает; оба
подтверждены заново по коду и тестам выше (§3).
## 6. Чего не проверял
- Полный HA backend harness (`python -m pytest tests_backend`, полный) — дельта
не трогает Python; итоговый гейт — CI job `backend` на точном SHA.
- `golden:verify` — дельта не меняет визуал; в `demo/golden/baselines/**`
изменений нет.
- 14 из 15 смоков с «прямым совпадением» символа — решение по каждому в §2.3,
ни один не содержит ассерта на изменённые строки.
- Полный набор `demo/smoke_*.mjs` и `performance_smoke` — предрелизный гейт,
не гейт этого раунда.
- Ручное исполнение приложения в браузере — не выполнялось; доказательство
идёт через `smoke_zero_walls.mjs` (реально исполняется в Chromium песочницы)
и чтение кода для остального.
## 7. Унаследовано из r1
Без повторной проверки в этом раунде принято по `docs/reviews/CODE-REVIEW-306-r1.md`
(материал `a66fb7ef53906bed33c6f92369b14e8e398be3bd`):
- полный разбор AC1–AC9, AC10 (кроме переименованного ключа — закрытие показано
в §3 этого документа), AC11, AC12, AC14–AC18 (кроме changelog-строки — §3);
- проверка backend-схемы (`validation.py`, `wall_segment_model.py`) чтением +
автором названный прогон «pure backend — 206 passed, 1 skipped»;
- единая семантика `cm:0` без `zero_kind`/compatibility-маркера (AC3, grep по
всему r1-диффу);
- Glow/солнце через общий resolver (AC5–AC7, `src/zero-walls.ts`);
- защита проёмов от потери на уровне backend+frontend+смок (AC10, часть про
сам guard, не про текст toast);
- touch-контракт, i18n-полнота (кроме 4 ключей §15, разобранных в §3),
release-артефакты (кроме changelog-строки backup);
- model-invariants и golden — green на `a66fb7ef` (полный первый заход).
## Вывод
Все три находки r1 (M1, M2, L1) закрыты по коду, не по заявлению автора. Новых
находок нет. Дешёвые гейты (typecheck, test, build+bundle, check-docs — дельта
трогает `src/**`) green лично. Целевой смок, реально проверяющий изменённый путь,
green. Задача готова к очереди на пре-релиз.
**Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0**