Files
houseplan-card/docs/reviews/CODE-REVIEW-276-r2.md
T
2026-08-24 03:13:45 +00:00

223 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-276-r2
- **Issue:** https://github.com/Matysh/houseplan-card/issues/276
- **Ветка:** `issue/276-reconcile-coincident-partition`
- **Проверенный HEAD:** `82fe98bcd3fb76833c27dd888d265fc15583a43b` (совпадает с заявленным в хендоффе автора)
- **Предыдущий раунд:** `docs/reviews/CODE-REVIEW-276-r1.md`, HEAD `496e79a6dff56db2d5e491d11d02c67ca77d08d2`, вердикт жёлтый (High: 0, Medium: 3 — M1/M2/M3)
- **Заход:** r2 (код-ревью) · блокирующих циклов до этого раунда: 1/4
## 1. Скоуп проверки
Разбор по дельте (PROCESS.md §2.9): `git diff 496e79a6..82fe98bc`. Дельта локальна —
только тесты, бенчмарк, golden-фикстура, docs — контракт поведения не меняется,
новая подсистема не затронута, ребейза не было (`origin/dev` в этом диапазоне не
двигался относительно r1). Полный разбор не требуется; уже прочитанное ядро
реализации (`src/coincident-partitions.ts`, интеграция в `optimizePlans()`)
наследуется из r1 без повторного чтения — единственная продуктовая правка
дельты, `src/plan-optimizer.ts`, — это DI-шов для теста (см. §2 ниже), а не
изменение поведения.
Коммиты дельты:
```
a6056111 fix: reconcile coincident partitions during Optimize (унаследован из r1)
434995be test: strengthen coincident partition mutation (унаследован из r1)
496e79a6 docs: refresh screenshot fingerprint (унаследован из r1, HEAD r1)
57fdfdbd docs: review document for #276 ← публикация CODE-REVIEW-276-r1 (инфраструктура)
82fe98bc test: prove coincident partition acceptance ← дельта r2
```
`82fe98bc` несёт `Issue: #276`, `User-Visible: no` — верно: правки только в
`src/plan-optimizer.ts` (DI-шов без изменения поведения по умолчанию),
`test/**`, `demo/benchmark_coincident_partitions.mjs`, `demo/golden/{harness,matrix}.mjs`,
`docs/ARCHITECTURE.md`, `docs/TESTING.md`, `package.json`, плюс штатный
рефреш `docs/images/screenshots.json`/`06-device-editor.png` (отпечаток
считается по всему `src/**`, а `src/plan-optimizer.ts` в дельте изменился —
рефреш обязателен и корректно лёг в тот же коммит). Видимого поведения нет —
`User-Visible: no` верен.
## 2. Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| **M1** — AC11 (перф-бюджет §10, «Benchmark + call-count test») не доказан ни одним committed-тестом | Частично закрыта. Добавлен `demo/benchmark_coincident_partitions.mjs` (`npm run benchmark:coincident-partitions`) и unit-тест `test('issue 276 reconciliation is owned by explicit Optimize and called once per valid space')` в `test/coincident-partitions.test.mjs`. Call-count/source-ownership половина закрыта твёрдо. Бенчмарк-половина закрыта только по букве — сам бенчмарк содержит дефект измерения, см. **новую находку M1-r2** ниже | `test/coincident-partitions.test.mjs:139-172` (call-count+ownership, зелёный, детерминирован); `demo/benchmark_coincident_partitions.mjs` (недетерминирован, см. §4) |
| **M2** — AC3 «несколько hosted door/window/gate» не покрыто автотестом | Закрыта. Добавлены позитивный тест (`door`+`window`+`gate`, 3 непересекающихся openings, атомарный rehost, идемпотентность повторного Optimize) и негативный (`overlap-door`+`overlap-window`, fail-closed, partition и hosts не тронуты) | `test/coincident-partitions.test.mjs:74-121`; прогнано мной: `node --test test/coincident-partitions.test.mjs` — все проходят |
| **M3** — AC5 «targeted golden: 5-см офсеты + hosted door до/после 10/30/virtual» не добавлен | Закрыта на уровне фикстуры/данных (сама визуальная приёмка — предрелизный гейт, как и в r1). Добавлены 4 сценария `coincident-partition-{before,thin,thick,virtual}-dark` в `demo/golden/matrix.mjs` (`GOLDEN_MATRIX_VERSION` 41→42), логика подготовки фикстуры в `demo/golden/harness.mjs`, и тест `test('issue 276 golden captures 5 cm offsets and hosted door before/after 10/30/virtual')` проверяет форму каждого состояния (5-см офсет виден, `partitions`/`host` сняты, толщина 10/30, `open_spans` для virtual) | `demo/golden/matrix.mjs:168-179`; `demo/golden/harness.mjs:70-96`; `test/golden-matrix.test.mjs:258-286`; прогнано: `node --test test/golden-matrix.test.mjs` — зелёный; baseline-изображений для новых ID нет — ожидаемо, приёмка при первом `golden:capture`/`golden:accept` предрелизного гейта |
## 3. Унаследовано из r1 (без повторной проверки)
Источник: `docs/reviews/CODE-REVIEW-276-r1.md`, HEAD `496e79a6`. Дельта не
касается перечисленного ниже кода/поведения, поэтому вывод r1 принят без
повторного исполнения:
- **AC1, AC2, AC4** (exact-match условие, byte-preserved fail-closed варианты,
nested narrower/wider, orphan/overlap/draft/column/unknown-field) —
`src/coincident-partitions.ts` дельтой не тронут.
- **AC6, AC7, AC8** (Preview/atomic Apply/report/Undo/идемпотентность) —
`smoke_optimize_coincident_partition.mjs` не менялся; я перезапустил его в
этом раунде как дешёвую проверку регресса (см. §5) — 11/11 green, вывод
подтверждён, не только унаследован.
- **AC9** (preflight гейтит Apply) — код `_runAlignToGrid()` не тронут;
мутант `optimize-preflight-bypassed` тот же (см. §5, перезапущен).
- **AC10** (consumer parity, единый источник геометрии для всех потребителей)
— рендер-путь дельтой не тронут; вывод r1 «проверено чтением» принят как
есть.
- **AC12, AC13** (мутации, дешёвые гейты) — идентификаторы мутантов и состав
гейтов не изменились; я перезапустил их заново на новом HEAD (не по
доверию, а потому что это дёшево, см. §5) и они дают тот же результат.
- **AC14** (changelog/docs/терминология) — уже закрыт в `a6056111`, дельта
добавляет только `ARCHITECTURE.md`/`TESTING.md`, что не отменяет прежнюю
проверку остальных документов.
- Раздел «Одно число — один источник» (report counters, итоговая толщина) —
дельта не добавляет новых видимых пользователю чисел; вывод r1 принят.
## 4. Как проверялось в этом раунде (гейты)
Дешёвые гейты — все green на HEAD `82fe98bc`:
| Гейт | Результат |
|---|---|
| `npx tsc --noEmit` | green, без вывода |
| `npm test` | **1180/1180 passed**, 0 skipped, 0 failed (у автора в хендоффе — 1179 pass + 1 conditional skip; расхождение на 1 тест — тот же класс среды, что в r1: условный skip в `test/process-gate.test.mjs` на git/gh-стаб отличается по окружению, не дефект диффа) |
| `npm run build && npm run bundle:sync` | green; три копии бандла (`dist/`, `custom_components/houseplan/frontend/`, `demo/srv/assets/`) идентичны, `git status` чист после сборки |
| `node scripts/check-docs.mjs` | green: «Documentation checks passed (7 files, 10 external links)» — обязателен, дельта касается `src/**` (`plan-optimizer.ts`) |
`node scripts/smoke-select.mjs --base 496e79a6 --head 82fe98bc`:
```
Изменено файлов src/**: 1 · символов проекта на изменённых строках: 2
Зарегистрированная связь (1): demo/smoke_optimize_coincident_partition.mjs
← reconcileCoincidentPartitions
```
Единственная связь — сама AC6/AC7 сцена. Прогнана:
```
node demo/smoke_optimize_coincident_partition.mjs → все 11 проверок true, OK
```
Мутации (перезапущены на новом HEAD, не по доверию к r1 — дёшево и делта
трогает `src/plan-optimizer.ts`, где интегрирован вызов):
```
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 → поймано 1 из 1
```
Инварианты модели — дельта не меняет геометрию/ссылки (`coincident-partitions.ts`
не тронут), но перезапущены дёшево на исходной фикстуре issue:
```
node scripts/model-invariants.mjs --config test/fixtures/276-coincident-partition.json
→ Инварианты выполнены: ссылки разрешимы, записи толщины находятся.
```
`golden:verify` (полный, 155+4 сценария) не запускался — предрелизный гейт
(PROCESS.md §8/§11.4), дельта не меняет ни один существующий рендер-путь, а
для 4 новых сценариев ещё нет baseline (ожидаемо, приёмка происходит на
первом `golden:capture`). `python -m pytest tests_backend` не запускался —
дельта не касается `custom_components/**/*.py` (в песочнице ревью к тому же
отсутствует модуль `pytest`, что подтверждено отдельно и не влияет на этот
диапазон).
## 5. Находки
### M1-r2 (Medium, в скоупе, чинится тем же автором) — `benchmark:coincident-partitions` даёт систематически смещённую и нестабильную оценку p95-оверхеда
`demo/benchmark_coincident_partitions.mjs:62-67` считает **два разных**
показателя оверхеда, но использует для gate худший из них:
```js
const overheadSummary = summary(overheadTimes); // p95 ПАРНОЙ разницы на каждой итерации — корректная оценка
...
const measuredOverheadP95 = Math.max(0, candidateSummary.p95 - baselineSummary.p95); // разность ДВУХ НЕЗАВИСИМЫХ p95 — используется в gate
const relativeP95 = measuredOverheadP95 / Math.max(baselineSummary.p95, Number.EPSILON);
const pass = measuredOverheadP95 <= ABSOLUTE_OVERHEAD_MS && relativeP95 <= RELATIVE_OVERHEAD;
```
`overheadSummary` (парная разница `candidateTimes[i] - baselineTimes[i]` на
каждой итерации, корректная статистика «сколько именно эта операция стоит
дороже») вычисляется и даже публикуется в отчёте как `pairedOverhead`, но
**не используется в самом решении pass/fail** — вместо неё gate вычитает
`baselineSummary.p95` из `candidateSummary.p95`, то есть разность двух
отдельно посчитанных квантилей по двум переставленным (interleaved), но не
идентичным по составу выборкам. Это статистически более шумная и смещённая
вниз оценка тем сильнее, чем меньше сам baseline (~30 мс на этой машине для
large-house фикстуры, при бюджете 15%).
Воспроизведение (без единой правки кода, только повторные запуски):
```
$ for i in 1 2 3 4; do npm run benchmark:coincident-partitions; done
RUN 1: gate metric 1.29 ms → pass: true | pairedOverhead.p95 9.22 ms → would-fail (23%)
RUN 2: gate metric 0.52 ms → pass: true | pairedOverhead.p95 5.74 ms → would-fail (16%)
RUN 3: gate metric 6.44 ms → pass: false | pairedOverhead.p95 8.67 ms → would-fail (24%)
RUN 4: gate metric 5.25 ms → pass: true | pairedOverhead.p95 11.25 ms → would-fail (31%)
```
(дополнительно ранее в этой же сессии: ещё 9 запусков дали 3 `pass:false` на
используемой сейчас метрике — т.е. ~⅓ прогонов красные без единой правки
кода). Корректная метрика (`pairedOverhead.p95`), которую сам скрипт уже
считает и печатает, проваливает бюджет §10 **во всех 4** прогонах подряд —
то есть используемый в `pass` показатель систематически даёт более
оптимистичный результат, чем собственная более точная оценка того же
скрипта.
Почему это Medium, а не просто «шумная машина»: сиблинг-бенчмарк той же
кодовой базы, `demo/benchmark_optimize_geometry_preflight.mjs` (#199), уже
решает ровно эту проблему — малый baseline делает относительный процент
неустойчивым к таймер-шуму — явным запасом
`relativeLimit = baseline.p95 * RELATIVE_RATIO + RELATIVE_NOISE_MS`
(строки 73-76 того файла). Новый бенчмарк #276 не переиспользует этот приём
и вдобавок не гейтится по уже вычисленной корректной парной статистике.
Итог: коммит закрывает M1 из r1 по букве («тест добавлен»), но не по духу
(«тест переживёт будущую правку и умеет упасть по существу») — доказательная
база AC11 по-прежнему ненадёжна, только теперь недетерминированно, а не
отсутствует вовсе. Не блокирует (реальный оверхед — единицы мс, см. и
собственное измерение r1: +1.5%), но должен быть починен в этом же issue:
либо гейтить на `overheadSummary.p95` вместо `candidateSummary.p95 -
baselineSummary.p95`, либо добавить абсолютный noise floor по образцу #199.
Продуктовых вопросов владельцу нет — всё техническое, решает автор.
## 6. Проверка AC — что изменилось в этом раунде
| AC | Статус r1 | Статус r2 | Доказательство дельты |
|---|---|---|---|
| AC3 | 🟡 частично (M2) | ✅ | `test('issue 276 rehosts three non-overlapping door/window/gate openings atomically')` + `test('issue 276 fails closed when two hosted openings would overlap after rehost')`, обе зелёные |
| AC5 | 🟡 частично (M3) | ✅ (данные/фикстура; визуальная приёмка остаётся предрелизным гейтом, как и для всех golden) | `demo/golden/matrix.mjs` 4 новых сценария + `test/golden-matrix.test.mjs` проверка формы каждого состояния |
| AC11 | 🟡 частично (M1) | 🟡 частично иначе: call-count/ownership ✅ твёрдо, budget-часть не доказана надёжно (M1-r2) | `test/coincident-partitions.test.mjs:139-172` (calls===2, source ownership) зелёный и детерминирован; `demo/benchmark_coincident_partitions.mjs` недетерминирован, см. §5 |
Остальные AC (1,2,4,6-10,12-14) дельтой не задеты — статус наследуется из r1
без изменений (см. §3).
## 7. Чего не проверял и почему
- Полный `golden:verify`/`golden:capture` — предрелизный гейт, дельта не
меняет существующий рендер-путь; для новых 4 сценариев baseline ещё нет.
- `python -m pytest tests_backend` — дельта не касается
`custom_components/**/*.py`.
- Остальные 176 browser-смоков кроме названного инструментом и уже
прогнанного в r1 набора (`smoke_wall_key_roundtrip`,
`smoke_resize_wall_thickness`) — не перезапускал повторно, они не входят в
«зарегистрированную связь» этой дельты (инструмент в этом раунде вернул
только одну связь, см. §4), а сама delta не трогает их темы.
- Полная performance-матрица `demo/performance/*` — не гейт код-ревью;
таргетный `benchmark:coincident-partitions` разобран отдельно как находка
M1-r2.
## 8. Вывод
Обе предметные находки r1 (M2, M3) закрыты полностью и доказуемо: новые
тесты для нескольких hosted openings и golden-сценарии для 5-см офсетов
существуют, зелёные и способны падать (проверено запуском). Находка M1
закрыта только наполовину — call-count/source-ownership тест твёрдый и
детерминированный, но добавленный бенчмарк содержит собственный дефект
измерения (использует смещённую метрику вместо уже вычисленной корректной)
и эмпирически недетерминирован на ~⅓ прогонов без единой правки кода. Это
новая Medium-находка (M1-r2), в скоупе issue, не блокирует мёрж по
серьёзности, но по правилу §2.7 требует ещё одного раунда правки от автора.
**Вердикт: жёлтый.**