Files
houseplan-card/docs/reviews/CODE-REVIEW-330-r3.md
T
2026-08-28 00:49:56 +00:00

227 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-330-r3
Issue: #330 — «Ограничения стыков (#329) блокируют event loop HA и дёргают ресайз на больших планах»
Этап: code (PROCESS.md §2.7)
Заход: r3 · блокирующих циклов израсходовано 2 из 4 (до этого раунда)
Ветка: `issue/330-junction-limits-performance`, HEAD ревью — `04bb54ae`
## 0. Разбор по дельте — почему и от какого SHA
Предыдущий вердикт (code-review r2, 2026-08-28T00:30:25Z) назван на HEAD
`85636a65`. Проверено, что этот SHA — прямой предок текущего HEAD и что
между ними ребейза не было:
```
$ git merge-base --is-ancestor 85636a65 HEAD && echo ancestor
ancestor
$ git log --oneline 85636a65..HEAD
04bb54ae fix: the baseline cache keys on geometry, and the smoke tells all three worlds apart (#330 r2-M1)
294d7047 docs: review document for #330
```
Два коммита, оба ожидаемые: `294d7047` — публикация документа r2 (не
код), `04bb54ae` — правка автора на единственную находку r2 (M1). Дельта
локальна: один файл продуктового кода, один смок, один юнит-тест, один
мутант-гвард. Контракт поведения не менялся, новая подсистема не задета,
объём дельты много меньше исходной задачи. Условия §2.10/§7.2 для полного
разбора не выполнены — разбираю по дельте `git diff 85636a65..HEAD`.
## 1. Скоуп дельты
`git diff 85636a65..HEAD --stat`:
```
.../houseplan/frontend/houseplan-card.js | 4 +-
demo/smoke_junction_limits.mjs | 56 ++++-
dist/houseplan-card.js | 4 +-
docs/reviews/CODE-REVIEW-330-r2.md | 257 +++++++++++++
scripts/mutation-gate.mjs | 9 +-
src/houseplan-card.ts | 25 +-
test/junction-limits.test.mjs | 6 +-
7 files changed, 336 insertions(+), 25 deletions(-)
```
Только r2-M1 (АС4 «поведенческая часть кэша не различает рабочий кэш от
удалённого»):
- `src/houseplan-card.ts:7501-7553` (`_junctionLimitsIntroduced`) — ключ
кэша `_junctionBaselineCache` заменён с `(epoch, spaceId)` на
`(spaceId, fingerprint)`, где `fingerprint = spacePhysicalGeometryFingerprint(previousSpace)`
— существующая, уже применяемая в этом же файле утилита (строки 7212,
7569-7570, 7676, 7751-7752, 7800, 16031), а не новый механизм.
- `demo/smoke_junction_limits.mjs` — счётчик теперь считает ТОЛЬКО вызовы
`_junctionLimitViolations` с документом `=== card._serverCfg` (вычисления
baseline, не кандидата), плюс второй жест ресайза с коммитом между ними,
чтобы отличить «кэш никогда не инвалидируется» от «кэш инвалидируется
честно».
- `test/junction-limits.test.mjs` — regex источника обновлён под новое имя
полей (`cached.fingerprint === fingerprint`, `spacePhysicalGeometryFingerprint(previousSpace)`).
- `scripts/mutation-gate.mjs` — guard мутанта `junction-limit-baseline-cache-stale`
теперь запускает и смок (поведенческая часть), и юнит (исходник), было —
только юнит.
- `dist/**`/frontend-копия — механический пересчёт бандла тем же коммитом.
## 2. Закрытие раунда r2
| Находка r2 | Чем закрыта | Где это видно |
|---|---|---|
| M1 (в скоупе) — заявленная «поведенческая половина» AC4 не различала рабочий и удалённый кэш (11 против 12 вызовов тонули в шуме); реальный баг вскрылся тем же разбором: кэш ключевался на `_cfgEpoch`, который тикает на каждом принятом превью-оверлее без изменения хранимого документа, поэтому baseline пересчитывался ~4 раза за жест, а не 1 | Ключ кэша заменён на identity документа + `spacePhysicalGeometryFingerprint` его пространства — содержание, не счётчик; превью-оверлей fingerprint не трогает, in-place structural commit меняет геометрию и инвалидирует честно. Смок считает отдельно вычисления baseline (документ `===_serverCfg`) через два жеста с коммитом между ними: ожидание строго 1, затем строго 2 | `src/houseplan-card.ts:7518-7553`; `demo/smoke_junction_limits.mjs:186-254`; я применил ровно мутант `junction-limit-baseline-cache-stale` (условие кэша обратно на `cached.spaceId === spaceId` без fingerprint) и прогнал сам смок — `resizeBaselineRecomputedAfterCommit: expected true, got false` (см. §3); без мутанта смок и `mutation-gate.mjs --id=junction-limit-baseline-cache-stale` — оба зелёные (см. §3) |
Находка закрыта по существу и подтверждена исполнением, не только чтением.
## 3. Как проверялось (дельта, r3)
Зелёного Validate на `04bb54ae` не найдено — гоняю дешёвые гейты сам, плюс
`check-docs` (диф трогает `src/**`) и целевой смок/мутант по дельте.
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | чисто, без вывода |
| Юниты | `npm test` | `# tests 1420 / pass 1419 / fail 0 / skipped 1` |
| Сборка + идентичность 3 копий бандла | `npm run build && npm run bundle:sync`, затем `cmp` попарно всех трёх файлов | все три `cmp` — молча, идентичны; `git status --short` после сборки чист |
| Докс-гейт (диф трогает `src/**`) | `node scripts/check-docs.mjs` | **`exit 1`** — `ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs`. Проверил напрямую: записанный `sourceFingerprint` в `docs/images/screenshots.json` — `2a5ae6c1…`, реальный `visualFingerprint()` на этом дереве — `dbac4a84…`. Последний коммит, трогавший `screenshots.json`, — `85636a65` (ДО фикса); `04bb54ae` поменял `src/houseplan-card.ts` и не пересчитал фингерпринт — см. находку H1 |
| Целевой смок AC4 | `node demo/smoke_junction_limits.mjs` | `OK` (после свежей сборки и `bundle:sync`) |
| Тест умеет падать (AC4/мутант) | вручную заменил условие кэша в `src/houseplan-card.ts` на мутант `junction-limit-baseline-cache-stale` (`if (cached && cached.spaceId === spaceId)`, без fingerprint), пересобрал бандл, прогнал смок | `FAILED (1): resizeBaselineRecomputedAfterCommit: expected true, got false` — мутант ловится; источник восстановлен, пересобран, смок снова `OK`, `git status --short` пуст |
| Тот же мутант через гвард | `node scripts/mutation-gate.mjs --id=junction-limit-baseline-cache-stale` | `ok junction-limit-baseline-cache-stale: тест покраснел, как обязан`; «поймано 1 из 1» |
| Выборка смоков по дельте | `node scripts/smoke-select.mjs --base 85636a65 --head HEAD` | 1 файл `src/**` изменён, 3 символа на изменённых строках. Одна **зарегистрированная связь**: `demo/smoke_wall_union_isolation.mjs` ← `spacePhysicalGeometryFingerprint`. Прогнан отдельно (см. ниже) |
| `smoke_wall_union_isolation.mjs` (по связи из smoke-select) | `node demo/smoke_wall_union_isolation.mjs` | `OK`, все 11 полей `true` |
| Перф-контракт §5/AC7 (не трогается дельтой, но это перф-задача — перепроверил) | `npm run benchmark:junction-limits` | `pass: true`, `tsFullCandidateMs: 152` (бюджет 400, запас 2.6×) |
### Отдельная проверка: не добавила ли дельта скрытую перф-регрессию
`_junctionLimitsIntroduced` теперь считает `spacePhysicalGeometryFingerprint`
на КАЖДЫЙ вызов (и при попадании в кэш, и при промахе) — раньше сравнение
было целочисленным O(1). Измерил стоимость самой функции на фикстуре §5
(576 атомов, 12×12) отдельно от бенчей (ни `benchmark_junction_limits.mjs`,
ни `benchmark_safe_resize.mjs` не проходят через этот метод класса — оба
вызывают чистые функции/`resize.js` напрямую, это отмечено ещё в r1/r2):
```
$ node --input-type=module -e '... spacePhysicalGeometryFingerprint(space) × 200 ...'
avg ms per call: 1.71
```
~1.7 мс на вызов на самой крупной фикстуре задачи — на два порядка меньше
бюджета кадра и на два порядка меньше стоимости, которую §4.4 устраняет
(до фикса пересчёт baseline стоил сотни мс — 4.2 с на полный набор до §4.7).
Не блокирует, отмечаю как проверенное наблюдение: этот путь остаётся не
покрыт числовым перф-бюджетом ни в одном бенче — не находка этой задачи
(оба бенча так же не покрывали его в r1/r2), но стоит иметь в виду, если
§330 продолжится следующим срезом.
## 4. Находки
### H1 — докс-гейт красный на HEAD ревью (в скоупе, блокирует)
`node scripts/check-docs.mjs` завершается `exit 1`:
```
ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs
```
Коммит `04bb54ae` меняет `src/houseplan-card.ts` (комментарий + логика
кэша), но не пересчитывает `docs/images/screenshots.json`. Отпечаток
считается по всему `src/**` (PROCESS.md §8), поэтому любая правка
фронтенда делает его устаревшим механически, без вариантов трактовки —
ровно класс дефекта, который уже стоил `dev` красного джоба `docs` на
#230/#234 (найдено только на следующей задаче, #237). Сама задача #330
уже дважды проходила через этот гейт (r1-H1 в code-review, тот же класс)
и знает цену пропуска. UI не изменился (изменение сугубо внутреннее,
кэш-логика), поэтому PNG, вероятно, останутся байт-в-байт те же — но
запись `sourceFingerprint` обязана быть пересчитана командой `npm run
build && node demo/docs/capture.mjs` и принята `npm run docs:accept
-- --reviewed --from=<артефакт>` тем же правилом, что и раньше в этой
задаче; самому пересобирать и коммитить в рабочей копии не в праве
(ревьюер не правит код).
**Что нужно поправить:** пересчитать фингерпринт/скриншоты и закоммитить
вместе (или отдельным коммитом класса C поверх), убедиться, что
`check-docs` зелёный на итоговом SHA.
Находок Medium/Low, не закрытых в тексте, нет.
## 5. Унаследовано из r2 (без повторной проверки)
- **Архитектура срезов §4.1–§4.7** (executor-цепочка, rev-кэш `previous`,
byNode-индекс П3, bucket-решётка П4, §4.6 «v9 как есть», общий проход
§4.7) — признана верной по коду и по вердиктам в code-review r2
(`docs/reviews/CODE-REVIEW-330-r2.md`, HEAD `85636a65`, разделы 3 и 7).
Дельта r3 их не касается ни одной строкой (см. §1) — принимаю без
повторного чтения.
- **AC1 (event loop/executor)** — HA-харнесс тест
`test_330_config_set_validators_run_in_the_executor`, прогнан исполнением
в r2 (`pytest -k 330` → `1 passed`, документ r2 §5/§6). Дельта r3 не
трогает `custom_components/**`.
- **AC2/AC3/AC5 (линейный П3/П4, rev-кэш бэкенда, эквивалентность §4.6)** —
подтверждены исполнением в r2 (паритет-тесты TS↔Python, границы ячеек,
`pytest tests_backend -q` 423 passed). Ни один из файлов бэкенда не в
дельте r3.
- **AC7/перф-контракт §5, включение в `validate.yml`** — подтверждено в r2
чтением воркфлоу (`validate.yml:543-547`) и исполнением
(`pass:true`, запас 2.65×). Я перепрогнал `benchmark:junction-limits`
заново в этом раунде (см. §3) — тот же результат в пределах шума
раннера (152 мс против 150.7 мс в r2), воркфлоу-файл не в дельте, не
перечитывал.
- **golden:verify** — не гонял, как и в r2: дельта не трогает рендер,
геометрию, стили, слои, только ключ кэша и подсчёт вызовов в смоке.
- **Полная матрица `demo/smoke_*.mjs` (194 шт.)** и **полный
`mutation-gate.mjs`** — не гонял целиком, как и в r2: это предрелизные
гейты (PROCESS.md §8), не гейт ревью. По дельте прогнал точечно —
`smoke_junction_limits.mjs` (прямое совпадение) и
`smoke_wall_union_isolation.mjs` (зарегистрированная связь по
`spacePhysicalGeometryFingerprint`, см. §3) — плюс единственный
релевантный дельте мутант через `--id=`.
- **`pytest tests_backend`** — не гонял; дельта не содержит ни одного
файла `.py` (см. `git diff --name-only 85636a65..HEAD`).
- **`npm run invariants -- --config <...>`** отдельной командой — не
гонял; дельта не вводит новую геометрию/фикстуру, только ключ кэша по
уже существующему `spacePhysicalGeometryFingerprint`; `npm test`
прогоняет инварианты модели на существующих моделях проекта и прошёл
(см. §3).
- **Одно число — один источник** — неприменимо: дельта не вводит и не
меняет ни одной пользователю видимой величины (внутренний счётчик
вызовов в смоке и внутренний кэш-ключ невидимы пользователю).
- **CHANGELOG** — не требуется: коммит `04bb54ae` несёт `User-Visible: no`
(внутренняя правка кэша, поведение для пользователя не меняется);
трейлеры коммита корректны (`Issue: #330`, `User-Visible: no`).
## 6. Что проверено и корректно (эта дельта)
- Новый ключ кэша (`spaceId + fingerprint`) читается из `previousConfig`,
вычисляется безопасно через `try/catch` с пустой строкой на ошибке;
пустой fingerprint явно исключён из условия попадания в кэш И из записи
в кэш (`&& fingerprint`) — сбой вычисления fingerprint отключает
кэширование для этого вызова, а не выдаёт ложное попадание.
Прочитано построчно (`src/houseplan-card.ts:7526-7553`).
- `spacePhysicalGeometryFingerprint` — не новый механизм, а уже
используемый в этом же файле барьер физической геометрии (6 других
мест), корректность и покрытие которого не в скоупе этой находки.
- Смок теперь физически различает три мира (рабочий кэш = 1 затем 2,
вечный кэш = 1 затем 1, отключённый кэш = ~10 затем ~20) — проверено
и мутантом (см. §3), и обычным прогоном.
- Трейлеры коммита `04bb54ae` — `Issue: #330`, `User-Visible: no`,
корректны для внутренней правки.
## 7. Чего не проверял и почему
- `pytest tests_backend` — дельта не трогает `.py`, наследую r2 (§5).
- `golden:verify` — дельта не трогает визуал, наследую r2 (§5).
- Полная матрица смоков (194) и полный `mutation-gate.mjs` (все мутанты) —
предрелизные гейты, не гейт ревью; прогнал точечно по выводу
`smoke-select.mjs` плюс единственный релевантный мутант.
- Перф-бюджет самой `spacePhysicalGeometryFingerprint` внутри
`_junctionLimitsIntroduced` — не заведён отдельным числом ни в одном
бенче (см. §3, «отдельная проверка»); измерил вручную (1.7 мс/вызов на
576-атомной фикстуре) вместо гона несуществующего гейта — не блокирует,
записано как наблюдение.
- Ручное тестирование в браузере — вне цикла ревью по регламенту;
браузерный смок прогнан, это и есть замена.
## 8. Итог
High: 1 (докс-гейт красный на этом SHA — механический пропуск
`check-docs` после правки `src/**`, в скоупе, чинится в этой же задаче).
Medium: 0. Единственная находка r2 (M1) закрыта по существу и
подтверждена мутантом лично. Архитектурная часть решения (§4.1–§4.7)
унаследована из r2 без повторной проверки — дельта её не касается.