docs: review document for #276

Issue: #276
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-24 02:51:26 +00:00
parent 496e79a6df
commit 57fdfdbd83
+275
View File
@@ -0,0 +1,275 @@
# CODE-REVIEW-276-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/276
- **Ветка:** `issue/276-reconcile-coincident-partition`
- **Проверенный HEAD:** `496e79a6dff56db2d5e491d11d02c67ca77d08d2` (совпадает с заявленным в хендоффе автора)
- **Диапазон:** `origin/dev...HEAD` (`origin/dev` = `240316a6`)
- **ТЗ:** `docs/specs/276-coincident-partition-reconciliation.md`, зелёное ревью
`docs/reviews/SPEC-REVIEW-276-r3.md` (3 захода на этапе spec, 1/4
блокирующих циклов израсходовано там; счётчик код-ревью отдельный)
- **Заход:** r1 (код-ревью) · блокирующих циклов до этого раунда: 0/4
## 1. Скоуп проверки
Полный разбор (это первый заход код-ревью, §2.10 не применяется).
Диапазон коммитов:
```
a06c94b9 docs: specify coincident partition reconciliation (ТЗ, r1..r3 циклы spec)
ca516643 docs: address spec review for partition reconciliation
cfc8f211 docs: cover nested coincident partition thickness
a6056111 fix: reconcile coincident partitions during Optimize ← продуктовый код
434995be test: strengthen coincident partition mutation
496e79a6 docs: refresh screenshot fingerprint
```
Продуктовая логика — единственный коммит `a6056111`. Он несёт `Issue: #276`,
`User-Visible: yes`, и в нём же — оба changelog (`docs/CHANGELOG.md`,
`docs/CHANGELOG.ru.md`), `docs/USER-GUIDE.md`/`.ru.md`, `docs/WALL-THICKNESS.md`,
`docs/ARCHITECTURE.md`, `docs/TESTING.md`, `docs/STATUS.md`, все тесты, demo
smoke, `scripts/mutation-gate.mjs`, `scripts/smoke-links.mjs` и три копии
бандла. `434995be` — тест-только правка мутанта (`User-Visible: no`, верно).
`496e79a6` — обновление отпечатка скриншотов (`User-Visible: no`, верно,
отдельный коммит по прецеденту #237).
## 2. Как проверялось (гейты)
Дешёвые гейты — всегда, в этом раунде тоже:
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | green, без вывода |
| Unit | `npm test` | **1176/1176 passed**, 0 skipped, 0 failed (авторский хендофф заявлял 1175 passed + 1 штатно skipped — расхождение на 1 тест воспроизведено средой: пропуски в `test/process-gate.test.mjs` условны на `git`/`gh`-стаб и различаются Windows/Linux; не дефект диффа) |
| Build + sync | `npm run build && npm run bundle:sync` | green; `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` и `demo/srv/assets/houseplan-card.js` — три копии byte-identical, `git status` чистый после сборки |
| check-docs | `node scripts/check-docs.mjs` | green: «Documentation checks passed (7 files, 10 external links)» — обязателен, диффа задел `src/**` |
`node scripts/smoke-select.mjs --base origin/dev --head HEAD` — полный вывод
приложен ниже с решением по каждой строке.
```
Прямое совпадение (13): smoke_zero_divider_taper, smoke_decor_layer_order,
smoke_drag_bounds, smoke_furniture, smoke_glow, smoke_grid_snap,
smoke_infinite_canvas, smoke_merge_split, smoke_open_passage,
smoke_opening_tunnel_fill, smoke_optimize_micro_interval,
smoke_optional_space_model, smoke_split_corner_wall
Зарегистрированная связь (3): smoke_optimize_coincident_partition,
smoke_resize_wall_thickness, smoke_wall_key_roundtrip
```
Решение: все 13 «прямых совпадений» — следствие того, что новый модуль
импортирует широко используемые имена (`wallKey`, `NORM_W`, `GRID_PITCH`,
`SpaceModel`), а не потому что диффа меняет их контракт — классический
«слабая связь по распространённому имени» (PROCESS.md §8). Ни один из этих
13 смоков не относится к теме issue (decor layer order, furniture, glow
generic, grid snap, infinite canvas…), и полный прогон всех 16 не
пропорционален задаче. Прогнаны:
- `demo/smoke_optimize_coincident_partition.mjs` — прямо назван в AC6/AC7,
запущен: **все 11 проверок true** (Preview не пишет, один атомарный
Apply, reload сохраняет каноническое тело, one-shot Undo восстанавливает
hosted-форму, Boundary видит shared wall, Thickness меняет единственное тело);
- `demo/smoke_wall_key_roundtrip.mjs` — «зарегистрированная связь» напрямую
касается AC10 (consumer parity Plan/View/kiosk/Static/hidden Iso/light):
запущен, **все 24 проверки true**;
- `demo/smoke_resize_wall_thickness.mjs` — «зарегистрированная связь» по
`WallEntry`, который использует `setWallThickness`: запущен, **все 7
проверок true**; Resize сам по себе вне скоупа (#277), но это дешёвая
проверка отсутствия регресса в общем помощнике.
Остальные 10 «прямых совпадений» не прогонялись — тема (decor/furniture/
glow/grid snap/infinite canvas/merge-split/open passage/tunnel fill/split
corner) не пересекается ни с одним AC #276, а связь чисто по имени
импортированной утилиты.
Мутации (AC12, три идентификатора):
```
node scripts/mutation-gate.mjs --id=optimizer-coincident-opening-rehost-disabled
→ поймано 1 из 1
node scripts/mutation-gate.mjs --id=optimizer-coincident-partial-accepted
→ поймано 1 из 1
node scripts/mutation-gate.mjs --id=optimize-preflight-bypassed (общий #199 мутант,
переиспользован — reconciliation идёт через тот же `d.preflight?.ok` гейт)
→ поймано 1 из 1
```
Инварианты модели (диффа меняет геометрию/ссылки — обязателен, PROCESS.md §8,
#254/#253/#244/#252/#258/#259). Прогнан на выходе `optimizePlans()` для
фикстуры issue (5-см офсеты, room 15/partition 20) и на синтетической
nested-fixture (room 30/partition 20):
```
npm run invariants -- --config /tmp/276-reconciled.json
→ Инварианты выполнены: ссылки разрешимы, записи толщины находятся.
npm run invariants -- --config /tmp/276-nested.json
→ Инварианты выполнены: ссылки разрешимы, записи толщины находятся.
```
Оба файла получены прогоном `test-build/plan-optimizer.js` на входных
данных теста (команда воспроизводима, не «verified» без результата).
**Одно число — один источник.** Новые видимые числа — два счётчика Optimize
report (`partitionsReconciled`, `openingsRehosted`); каждый рендерится ровно
в одном месте (`src/houseplan-card.ts` вокруг строки 16233) через
существующий `_t()`-путь, как остальные счётчики отчёта. Итоговая толщина
общей стены (`max(roomCm, partitionCm)`) после Apply читается Thickness и
Boundary из одного и того же `space.walls` — тот же источник, что и для
любой обычной стены, второго хранения нет. `node --test
test/single-source-numbers.test.mjs` — 3/3 green, диффа не касается
форматтера.
## 3. Не прогонялось и почему
- **Полный `golden` матрикс (155 сценариев)** — предрелизный гейт (PROCESS.md
§8, §11.4), не гейт код-ревью; диффа не меняет ни один существующий golden
fixture (нет coincident-partition геометрии ни в одном текущем сценарии
`demo/golden/matrix.mjs`), поэтому целевой прогон был бы no-op, а полный —
не соразмерен задаче. См. находку M3 — целевой сценарий для #276 в матрицу
не добавлен вовсе, поэтому полный/предрелизный прогон и не смог бы его
проверить.
- **`python -m pytest tests_backend`** — диффа не касается `custom_components/**/*.py`.
- **Полная performance-матрица / `demo/benchmark_large_house.mjs` как есть** —
не тот профиль (это render/pointer-путь, не «Optimize candidate pass»);
вместо него — целевое измерение см. §4, M1.
- **`node demo/smoke_*.mjs` полный набор (177)** — не соразмерно; выбор по
инструменту и теме см. §2.
## 4. Находки
Все три ниже — **Medium, в скоупе задачи** (High не найдено, красного нет).
По правилу §2.7/§4 это жёлтый вердикт, автор чинит в этом же issue, фикс
проходит следующий раунд.
### M1 — AC11 (перф-бюджет §10) не доказан ни автотестом, ни явным
«проверено чтением, не исполнением»
`docs/specs/276-coincident-partition-reconciliation.md` §10 обещает
«дополнительный p95 не превышает 15%... и 25 ms абсолютного overhead» на
large-house fixture, а AC11 называет доказательство: «Benchmark +
call-count test». Ни то, ни другое не добавлено:
- `demo/benchmark_large_house.mjs` — рендер/pointer-профиль, не измеряет
`optimizePlans()`;
- `demo/benchmark_optimize_geometry_preflight.mjs` — измеряет только #199
preflight, вызывается на данных, подготовленных `prepareSpacePhysicalGeometryInputs`,
и не включает новый pass;
- ни один тест не проверяет счётчик вызовов `reconcileCoincidentPartitions`
(call-count) — то есть будущая регрессия «функция стала вызываться на
render/pointer path» не будет отловлена ничем.
Я измерил бюджет сам (не входит в зачёт «тест умеет падать» — это ad hoc
проверка ревьюера, а не доказательство автора), чтобы понять, реален ли
риск:
```
git worktree add --detach /tmp/hp-baseline cfc8f211 # до продуктового коммита
# baseline (без reconciliation), large-house fixture, N=30:
BASELINE min 31.20 median 34.59 p95 50.25 max 107.17
# HEAD (496e79a6), тот же прогон:
HEAD min 30.62 median 34.07 p95 50.98 max 105.23
```
Разница p95 — 0.73 ms (~1.5%), внутри бюджета §10 с большим запасом.
**Фактического риска для перфоманса нет**, но это устанавливает ревьюер
разовым замером, а не тест, который переживёт следующую правку и умеет
упасть. Чинится добавлением: (а) целевого бенчмарка `optimizePlans()` на
large-house fixture с ассертом бюджета §10, аналогично
`benchmark_optimize_geometry_preflight.mjs`; (б) unit-теста, считающего
вызовы `reconcileCoincidentPartitions` только из `optimizePlans()` (например,
через инструментированный импорт/счётчик), либо равноценного доказательства
«не вызывается на render/pointer» по коду с явной пометкой в тесте.
### M2 — AC3 «несколько hosted openings на одной partition» не покрыто тестом
Спецификация (AC3) и план тестов (§13) требуют доказательства для «один И
**несколько** hosted door/window/gate». Все три источника (fixture
`test/fixtures/276-coincident-partition.json`, `test/coincident-partitions.test.mjs`,
`test/plan-optimizer.test.mjs`) используют ровно один hosted opening на
кандидата; парный overlap-цикл в `src/coincident-partitions.ts:265-270`
(`for i; for j > i;`) и `openingsRehosted += nextHosted.length` для случая
`length > 1` остаются непроверенными автотестом.
Проверил сам, чтобы понять фактический риск (ad hoc, не входит в зачёт
автора):
```js
// два hosted openings (door + window) на одной coincident partition
partitionsReconciled 1 openingsRehosted 2
// оба opening корректно материализованы на своих x/y/angle, host снят,
// повторный optimizePlans() — changed=false
```
Код и в этом случае работает верно — но это не устраняет разрыв: без
committed-теста регресс в парном overlap-цикле не будет пойман никем. Чинится
добавлением одного кандидата с двумя (лучше — тремя, чтобы задеть все пары
`i<j`) hosted openings в `test/coincident-partitions.test.mjs` либо
`test/plan-optimizer.test.mjs`, включая позитивный (не конфликтующие) и
негативный (два новых opening перекрылись бы после rehost → fail-closed)
случай.
### M3 — AC5 «targeted golden» не добавлен ни в каком виде
`docs/specs/276-coincident-partition-reconciliation.md` §13 обещает
«targeted golden: short 5-cm offsets + hosted door до/после 10/30/virtual», и
AC5 называет доказательством «Canonical geometry unit + targeted golden».
`demo/golden/matrix.mjs` не тронут этим диффом — ни один сценарий для #276 не
существует. Геометрическая часть AC5 полностью доказана (`wallIntervals`
unit-проверка в `test/coincident-partitions.test.mjs`/`plan-optimizer.test.mjs`
+ browser-smoke), но визуальный/пиксельный канал, который отдельно ловил
предыдущие регрессы такого рода (#234: подсветка «Толщины» и запись
расходились именно на уровне рендера, не геометрии), для этой фичи не
существует вовсе — ни сейчас, ни в будущем предрелизном прогоне, потому что
сценарий просто не описан в матрице. Это не блокирует Apply/AC своей
неполнотой сегодня (риск чисто визуального дефекта низкий: путь рендера
переиспользует давно проверенный `physicalBodyParts()`/`wallBodiesGeometry`,
никакой новой отрисовки не добавлено), но обещанный тест-план не выполнен.
Чинится добавлением сценария в `demo/golden/matrix.mjs` (короткие 5-см
офсеты + hosted door, до/после 10/30/virtual) — сама запись сценария не
требует прохождения через `golden:accept`, это делает пре-релизный гейт
(§8/§11.4) при первом реальном захвате.
## 5. Проверка AC — что доказано и как
| AC | Статус | Доказательство |
|---|---|---|
| AC1 | ✅ | `test('issue 276 exact proof ignores endpoint direction and room order')` — матрица 2×2 (direction×room order), все проходят |
| AC2 | ✅ | `test/plan-optimizer.test.mjs`: same-cm (20/20), nested narrower (30/20→30), nested wider (20/30→30), partial (byte-equivalent), ambiguous second partition (byte-equivalent); `test/coincident-partitions.test.mjs`: orphan/overlap/draft/column/unknown-field все byte-equivalent |
| AC3 | 🟡 частично | один hosted opening — unit + unknown-field round-trip (`future_field`) доказаны; **несколько** — не покрыто автотестом (M2), верно по факту исполнения (проверено мной ad hoc), но не доказано committed-тестом |
| AC4 | ✅ | `test('issue 276 fails closed for an orphan host, overlap, draft, column and unknown partition data')` — 5 вариантов, все fail-closed |
| AC5 | 🟡 частично | геометрическая эквивалентность — unit (`wallIntervals` после Apply) + browser smoke; **targeted golden не добавлен** (M3) |
| AC6 | ✅ | `smoke_optimize_coincident_partition.mjs`: `boundarySeesSharedWall`, `thicknessToolSelectsCanonicalWall`, `thicknessChangesSingleBody`, `previewDoesNotWrite` — все true |
| AC7 | ✅ | i18n unit (`test/i18n.test.mjs` проверяет обе строки в исходнике card) + smoke (`reportRendersBothCounters`, `applyUsesOneAtomicWrite`, `applyEnablesUndo`, `undoRestoresHostedPartition`) |
| AC8 | ✅ | Идемпотентность доказана в обоих unit-тестах (`second.changed === false`, `second.report.partitionsReconciled === 0`, `deepEqual(second.config, first.config)`) и smoke (reload) |
| AC9 | ✅ (проверено чтением, переиспользует #199) | `_runAlignToGrid()` блокирует Apply на `!d.preflight?.ok` (src/houseplan-card.ts:15112) — путь общий для всех Optimize-кандидатов, включая новый; мутант `optimize-preflight-bypassed` подтверждён (1/1) |
| AC10 | ✅ (проверено чтением) | Все консумеры (Plan/View/kiosk/Static/hidden Iso/Glow/sun) читают геометрию из одного `model.partitions`/`space.walls` через `physicalBodyParts()`/`modelOf()` — нет отдельной копии на consumer; после Apply запись `partitions` удаляется из персистентной конфигурации целиком, поэтому расхождения консумеров структурно невозможны. Отдельного dedicated parity-теста для #276 нет, но `smoke_wall_key_roundtrip.mjs` (запущен, 24/24 green) подтверждает единый источник для смежного случая |
| AC11 | 🟡 частично | call-count и бюджет §10 не доказаны автотестом (M1); эмпирически бюджет выполняется (p95 +1.5%, см. M1) |
| AC12 | ✅ | 3 мутанта, все 1/1 (см. §2) |
| AC13 | ✅ | typecheck/test/build+sync — все green, см. §2 |
| AC14 | ✅ | оба changelog, USER-GUIDE (RU/EN), WALL-THICKNESS.md, ARCHITECTURE.md, TESTING.md, STATUS.md — все в одном коммите с кодом, терминология («Толщина», «Граница») соответствует USER-GUIDE |
## 6. Прочее (не находки, для полноты)
- Хостинг документации/строк использует существующую терминологию
(«Граница», «Толщина») — не изобретена.
- `rawPartitionKnown()` фильтрует по сырому JSON (`{id,a,b,cm}`), а не по
нормализованной модели — верно, иначе неизвестное поле терялось бы до
проверки; подтверждено тестом `unknown partition data`.
- Эпсилон `Math.max(GRID_PITCH * 0.0002, 1e-9)` — доля шага сетки (~0.02%
клетки), а не сама сетка; соответствует запрету ТЗ «пользовательская сетка
не является epsilon».
- `columnBlocks()` использует точный circumradius (квадрат — `size *
Math.SQRT1_2`, круг — `size * 0.5`), что совпадает с фактической
геометрией `columnBody()` в `src/physical-geometry.ts` — не эвристика.
## 7. Вывод
Ядро реализации корректно, идемпотентно и хорошо покрыто тестами по
основным путям; все проверенные мной гейты (typecheck/test/build/check-docs/
целевые smoke/mutation/invariants) зелёные, три копии бандла синхронны,
трейлеры и changelog на месте. Три Medium-находки — все про **полноту
доказательной базы**, названной в самом ТЗ (AC3/AC5/AC11), а не про дефект
поведения; ни одна не про-функциональна (я лично исполнением подтвердил, что
множественные hosted openings и перф-бюджет фактически работают верно).
Все три в скоупе issue #276 и чинятся автором в этом же issue.
**Вердикт: жёлтый.**