Files
houseplan-card/docs/reviews/CODE-REVIEW-290-r2.md
T
2026-08-24 16:30:43 +03:00

265 lines
22 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-290-r2
- **Issue:** https://github.com/Matysh/houseplan-card/issues/290
- **Ветка:** `issue/290-near-axis-authoring-repair`
- **Диапазон:** `origin/dev` (`f472af88`) → HEAD (`db77470d`)
- **ТЗ:** `docs/specs/290-near-axis-authoring-and-repair.md`, ревью ТЗ зелёное на r2 (`docs/reviews/SPEC-REVIEW-290-r2.md`)
- **Заход:** r2 (код-ревью), блокирующих циклов 1/4
- **Вердикт:** зелёный · High: 0 · Medium: 0
## 0. Примечание о трассируемости r1 (методология, не находка против автора)
Документ `docs/reviews/CODE-REVIEW-290-r1.md` называет диапазон
`origin/dev (523190d8) → HEAD (5aae905a)`. Ни один из этих двух SHA не
резолвится в текущей истории: `523190d8` — реальный коммит, но он на 10
коммитов позади текущего `origin/dev` (`f472af88`); `5aae905a` не существует
вовсе. Также сам вердикт-комментарий в issue не называет SHA явно (только
документ). Причина, установленная сравнением `git diff 523190d8..f472af88`:
между r1 и этим заходом issue #289 (независимая задача, тоже трогающая
`src/resize.ts`) была домержена в `dev`, и ветка #290 была перебазирована на
новый `dev` — отсюда новые SHA всех коммитов (коммиттер-даты всех коммитов
диапазона сгруппированы в окне 15:51–16:01, тогда как авторские даты
разбросаны по всему дню — прямой след rebase).
Это ровно случай «ребейз на ушедший вперёд dev — после ребейза это другой
код» (PROCESS.md §7.2). Поэтому раздел `src/resize.ts` разобран заново
полностью, а не по дельте между коммитами: я лично воспроизвёл поведение
дефекта H1 r1 на **старом** `resize.ts` (восстановленном из `43264eda`) и
на **новом** и убедился, что вывод не зависит от того, какой SHA был назван
в r1 — код, который дошёл до меня, доказан напрямую (§3.1). #289 не тронул
`src/near-axis.ts`, `src/houseplan-card.ts`, `src/plan-optimizer.ts`,
`src/wall-thickness.ts` (проверено: `git diff 523190d8..f472af88 --stat`) —
для них дельта-разбор корректен, см. «Унаследовано из r1».
## 1. Скоуп диффа этого раунда
Между состоянием, которое **содержательно** проверял r1 (коммит `43264eda`,
последний коммит с полным фиче-контентом до раунда) и текущим HEAD, вне
`docs/reviews/**`, изменилось:
```
custom_components/houseplan/frontend/houseplan-card.js | 299 +++++++++----------
dist/houseplan-card.js | 299 +++++++++----------
docs/images/09-device-info.png | Bin 146160 -> 146170 bytes
docs/images/screenshots.json | 24 ++++++++++------
scripts/model-invariants.mjs | 7 +-
scripts/mutation-gate.mjs | 36 +++
src/resize.ts | 9 +-
test/model-invariants.test.mjs | 4 +
test/near-axis.test.mjs | 28 ++
test/resize.test.mjs | 24 ++
```
Это в точности три находки r1 (High H1, Medium M1, Medium M2) плюс
пересборка `dist`/frontend-копии (класс D, следствие правки `resize.ts`) и
пересъёмка одного docs-скриншота (`device-info`, class C/D — фингерпринт
источника изменился у всех 10 сцен, но `imageSha256` реально сдвинулся
только у этой одной; проверил — панель `device-info` не касается
resize/near-axis, это шум капчура, `check-docs.mjs` принял). `src/near-axis.ts`,
`src/houseplan-card.ts`, `src/plan-optimizer.ts`, `src/wall-thickness.ts`,
все demo/i18n/docs-файлы фичи в этом раунде не менялись.
## 2. Как проверялось — гейты
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | `npx tsc --noEmit` | зелёный |
| unit | `npm test` | 1232 passed, 1 skipped, 0 failed (было 1226/1/0 на r1 — прирост от новых тестов H1/M1/M2) |
| build + bundle parity | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | идентичны, `git status` чист после сборки |
| docs fingerprint | `node scripts/check-docs.mjs` (обязателен: диф трогает `src/**`) | «Documentation checks passed (7 files, 10 external links)» |
| invariants (обязателен: диф трогает geometry/resize) | `npm run invariants -- --config test/fixtures/real-plan-second-floor.json --near-axis` и `--config test/fixtures/real-plan-first-floor.json --near-axis` | буквальный CLI-вызов теперь работает **без обёртки** (закрывает вторичную придирку M2 из r1): `real-second-floor: 1` / `0` почти-осевых стен; полный прогон без `--near-axis` — «Инварианты выполнены», exit 0 на обоих |
| mutation-gate (3 новых мутанта AC9) | `node scripts/mutation-gate.mjs --id=near-axis-shared-owner-repair-partial` / `--id=near-axis-shared-owner-double-counted` / `--id=near-axis-optimize-confirmation-bypassed` | все три: «тест покраснел, как обязан», «поймано 1 из 1» |
| mutation-gate реестр | `node scripts/mutation-gate.mjs --check` | без `FAIL`, все якоря патчей валидны (реестр не устарел) |
| smoke-выборка | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | см. §2.1 |
| golden | не запускал | диф не добавляет/не меняет golden-сценарии; H1-фикс — это validation-логика (`validateSafeResize`), не рендер; ни один golden baseline на near-axis/resize-геометрии не завязан |
| performance | не запускал | AC не называет количественный бюджет для этой правки; фикс сужает существующую проверку (меньше работы на итерацию, не больше) |
| backend pytest | не запускал | диф не трогает `custom_components/**/*.py` |
### 2.1 Смок-выборка
`node scripts/smoke-select.mjs --base origin/dev --head HEAD` (по всему
диффу issue, не только по дельте раунда — дельта слишком мала для
собственной осмысленной выборки):
- **Прямое совпадение (3):** `smoke_decor_layer_order.mjs`,
`smoke_room_resize.mjs`, `smoke_zero_divider_taper.mjs` (`roomPoly`/
`samePoint`). Прогнал все три на текущем HEAD (после `npm run
bundle:sync`, до этого демо не поднималось — не хватало
`demo/srv/assets/houseplan-card.js`, ожидаемо, класс D не коммитится):
зелёные, `OK`.
- **Зарегистрированная связь (1):** `smoke_near_axis_optimize.mjs` — прогнан
как guard мутанта `near-axis-optimize-confirmation-bypassed`, зелёный.
- **Слабая связь (19, общий символ `_path`):** не прогонял в этом раунде.
Дельта не трогает `houseplan-card.ts`/authoring-путь (уже проверено r1 на
идентичном по содержанию коммите `43264eda`, включая специально
`smoke_wall_junctions.mjs` по прецеденту #234 — `wall-thickness.ts` в этом
раунде не менялся). Наследую решение r1 не гонять остальные 17.
## 3. Находки
Новых находок нет.
### 3.1 Проверка закрытия High H1 (не декларативно — воспроизведением на обоих состояниях кода)
**Файл:** `src/resize.ts:1012-1023`
Старый код (был на момент r1, восстановлен из `43264eda` для сравнения,
временно, без коммита — `git status` чист до и после):
```ts
for (let index = 0; index < next.length; index++) {
if (classifyNearAxisSegment(next[index], next[(index + 1) % next.length])) return false;
}
```
Новый код (`615c0744`):
```ts
const n = original.poly.length;
const prev = (edge - 1 + n) % n;
const involvedEdges = [prev, edge, (edge + 1) % n];
for (const index of involvedEdges) {
if (classifyNearAxisSegment(next[index], next[(index + 1) % n])) return false;
}
```
Прогнал напрямую (не через demo, через `test-build/resize.js`) на фикстуре
`test/fixtures/279-near-orthogonal-junction.json` (та самая геометрия, что
в теле issue и в `real-plan-second-floor.json`):
| Handle | Старый код | Новый код |
|---|---|---|
| `north-west`, edge 3 (осевое ребро, не смежное с near-axis edge 1) | `invalid-geometry` | `partial-shared` |
| `south-west`, edge 1 (осевое ребро, не смежное с near-axis edge 3) | `invalid-geometry` | `enabled: true` |
Оба ребра под старым кодом валились с `invalid-geometry` — это и есть H1:
near-axis проверка была не привязана к сдвигаемому ребру и валила весь
полигон. Под новым кодом `south-west,1` включается штатно — прямое
доказательство, что over-broad проверка снята. `north-west,3` теперь
блокируется, но по **другой, не связанной с #290 причине**: `resolveSafeResize`
после исходной near-axis-проверки (`d=0`) идёт дальше и пробует реальные шаги
`±step`; для этого ребра оба шага ломают `sideOwnershipPreserved` — это ветка
`resolveSafeResize` (`src/resize.ts:923-925`), унаследованная от #289
(«не превращать shared стену в outer»), и она не зависит от near-axis
классификатора вообще. Убедился математически: сдвигаемые вершины — это
ровно `edge` и `edge+1`; рёбра, инцидентные им — ровно `{prev, edge,
edge+1}`; других рёбер, которые могли бы измениться от драга, не существует
— значит `involvedEdges` полон и корректен, недостача исключена.
Тест `test/resize.test.mjs:308+` (`#290 a remote near-axis edge does not
disable an unrelated exact resize handle`, дважды правленный —
`615c0744` → `6ded0f01` reconcile) фиксирует ровно эту таблицу: явно
проверяет, что `north-west,3` даёт `partial-shared` (с комментарием
«#289 still blocks...», не «near-axis»), и что `south-west,1` — реальный
успешный кейс. Формулировка АС3 («Outer и exact-shared safe drags сохраняют
moving/side edges exact-axis») совпадает с новым скоупом `involvedEdges`
дословно — не шире и не уже.
**Вывод:** H1 закрыт по существу, не только тестом. `git diff 615c0744..6ded0f01`
(«test: reconcile near-axis resize proof») — не заметание находки, а
самокоррекция автора: первая версия теста в `615c0744` утверждала `enabled:
true` для `north-west,3`, что было бы неверно (реально `partial-shared`);
следующий коммит поймал это и добавил правильный негативный кейс вместо
удаления проверки.
### 3.2 Проверка закрытия Medium M1 (mutation-gate)
`scripts/mutation-gate.mjs` теперь регистрирует все 6 категорий AC9. Три
новых (`near-axis-shared-owner-repair-partial`,
`near-axis-shared-owner-double-counted`, `near-axis-optimize-confirmation-bypassed`)
прогнаны индивидуально (`--id=...`), все убиты целевым тестом/смоком (§2,
таблица гейтов). `--check` не находит устаревших якорей ни у одного из 20+
мутантов реестра.
### 3.3 Проверка закрытия Medium M2 (AC10 автотест)
`test/near-axis.test.mjs:106-132` — новый тест
`#290 Optimize reduces the tracked real-plan near-axis profile and stays
idempotent` вызывает `nearAxisProfile`/`optimizePlans` на **обеих** реальных
фикстурах (`real-plan-first-floor.json`, `real-plan-second-floor.json`):
проверяет строгое `after.total <= before.total`, точное равенство
`after.total === before.total - wallsStraightened`, нулевой `wallsStraightened`
на втором проходе (идемпотентность) и явно `before.total === 1`,
`repaired.report.wallsStraightened === 1`, `after.total === 0` для
second-floor — это ровно факт, который r1 проверял вручную. `npm test`
прогнал — тест зелёный, участвует в общем прогоне (не отдельный ad-hoc
скрипт). Убедился, что тест умеет падать: без правки `readModel` (M2 внутри
M1-коммита также чинит парсинг `{note, space}` в
`scripts/model-invariants.mjs`) он не понадобился бы для самого near-axis
теста (тот использует `nearAxisProfile` напрямую на `{spaces:[space]}`,
собранном вручную в тесте, не через CLI) — а вот CLI-путь `npm run
invariants -- --config <fixture>` теперь тоже работает буквально, что и
было второй, менее блокирующей претензией M2.
## 4. Проверено и корректно (в этом раунде)
- Все три находки r1 (H1, M1, M2) закрыты по существу, не только
декларативно — см. §3.1–3.3.
- `git status` чист на всём протяжении проверки: build, bundle:sync,
mutation-gate (использует временные worktree, не трогает основное дерево),
временная подмена `src/resize.ts` для сравнения — всё восстановлено.
- Коммитные трейлеры всего диапазона `origin/dev..HEAD` (9 коммитов):
`Issue: #290` на каждом; `User-Visible: yes` только на `43264eda`
(единственный коммит с видимым поведением — Optimize報report/UI), и в нём
же правки `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` (проверено ещё в r1,
перепроверил сам факт присутствия обоих файлов в файловом списке того
коммита). Три новых коммита раунда (`615c0744`, `6ded0f01`, `db77470d`) —
все `User-Visible: no`, changelog не требуется и не тронут.
- «Одно число, один источник»: в дельте раунда новых видимых пользователю
чисел не появилось (правки — валидационная логика, тесты, CLI-парсинг);
`test/single-source-numbers.test.mjs` прошёл в общем прогоне.
## 5. Унаследовано из r1 (без повторной проверки в этом раунде)
Документ: `docs/reviews/CODE-REVIEW-290-r1.md`. SHA, названный в том
документе (`5aae905a`), не резолвится (см. §0) — как эквивалент указываю
`43264eda`, который содержательно идентичен по всем файлам, перечисленным
ниже (не тронут ни #289-rebase, ни коммитами этого раунда — проверено
`git diff 523190d8..f472af88 --stat` и `git diff 43264eda..HEAD --stat`).
- **AC1** (boundary matrix threshold/inclusive) — `src/near-axis.ts`,
`test/near-axis.test.mjs` boundary-тест; r1 подтвердил мутантами
`near-axis-threshold-weakened`/`near-axis-inclusive-boundary-disabled`.
- **AC2** (Walls authoring не сохраняет `316×1`) — `src/houseplan-card.ts`,
`demo/smoke_plan_drawing_repairs.mjs`; r1 прогнал смок и мутант
`near-axis-authoring-snap-bypassed`.
- **AC4/AC5/AC6/AC7** (Optimize чинит `316×1`, сохраняет related geometry,
multi-space/cell_cm) — `src/plan-optimizer.ts`,
`test/near-axis.test.mjs`, `test/plan-optimizer.test.mjs`; r1 проверил
чтением и ручным прогоном для multi-space части.
- **AC8** (UI/Undo/revision, atomic write) — `demo/smoke_near_axis_optimize.mjs`;
r1 прогнал все 7 сценариев смока.
- **Документация** (CANVAS/RESIZE/WALL-THICKNESS/ARCHITECTURE/
CONFIG-COMPATIBILITY/USER-GUIDE(.ru)/TESTING/CHANGELOG(.ru)) — не
тронута в этом раунде, терминология уже сверена r1.
- **Low L1** (AC1 source guard не механизирован, grep-гейта нет) — снят с
записью в r1, дельта этого раунда его не касается, риск тот же.
## 6. Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| **High H1** — near-axis проверка валит весь полигон комнаты, а не moving/side edges | `src/resize.ts:1018-1023` сужен до `involvedEdges = [prev, edge, edge+1]` | `615c0744` (`fix: scope near-axis resize validation`); воспроизведено мной на старом/новом коде в §3.1; тест `test/resize.test.mjs:308+`, дважды доведённый до правильного утверждения в `6ded0f01` |
| **Medium M1** — 3 из 6 обязательных мутантов AC9 отсутствуют | добавлены `near-axis-shared-owner-repair-partial`, `near-axis-shared-owner-double-counted`, `near-axis-optimize-confirmation-bypassed` | `scripts/mutation-gate.mjs:964-999` (`615c0744`); все три прогнаны индивидуально, см. §3.2 |
| **Medium M2** — AC10 не покрыт автотестом на реальных планах | новый тест на обеих `real-plan-*.json` фикстурах + читатель `readModel` принимает `{note, space}` без обёртки | `test/near-axis.test.mjs:106-132`, `scripts/model-invariants.mjs:519-524` (`615c0744`); прогнано, см. §3.3 |
| Low L1 (source guard не механизирован) | не в скоупе этого раунда, снят с записью r1 | — (унаследовано, см. §5) |
## 7. Чего не проверял
- **Полный Linux CI Validate на точном SHA HEAD** — не переоткрывал `gh`-прогон
отдельно; полагаюсь на локальный повтор дешёвых гейтов + целевые смоки
+ мутанты, как и r1.
- **Полный набор `demo/smoke_*.mjs`** (182 файла) — прогнал только прямые
совпадения и зарегистрированную связь из `smoke-select`; слабую связь
(19 файлов на `_path`) не прогонял в этом раунде — дельта не касается
`houseplan-card.ts`/authoring-пути, где эти смоки актуальны; полный набор —
предрелизный гейт (PROCESS.md §8).
- **`npm run golden:verify` и performance-профили** — не запускал; правка
сужает существующую проверку в `validateSafeResize` (валидационная логика,
не рендер, не добавляет работы), в AC нет количественного бюджета для
этой части.
- **`python -m pytest tests_backend`** — не запускал; диф не трогает Python.
- **Touch/pinch/pointercancel** — дельта раунда не трогает pointer-разбор
вообще (`resize.ts` validation logic не завязана на источник события);
унаследовано из r1, который проверил это для authoring-пути.