docs: review document for #473

Issue: #473
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-06 12:14:44 +00:00
parent d95255ada2
commit a3396a6c99
+164
View File
@@ -0,0 +1,164 @@
# CODE-REVIEW-473-r1
- **Issue:** #473 — «Перф-регрессия Stage 3 в 6,5–17 раз прошла два раунда ревью:
раунд постфактум на perf-дельту #160 и перф-смок в код-ревью задач отрисовки»
- **Ветка:** `issue/473-iso-perf-witnesses`, **SHA материала:** `d95255ad`
(`git rev-parse HEAD` сверен непосредственно перед выводом вердикта)
- **Этап:** code · **Заход:** r1 (первый заход код-ревью; ревью ТЗ на этом же
issue уже прошло два захода r1(жёлтый)/r2(зелёный) — это отдельный бюджет
циклов, §10.4)
- **Класс изменений:** только B (`test/**`, `scripts/**`,
`.github/workflows/**`) и C (`docs/**`, `PROCESS.md`) — **ни одного файла
класса A**, `src/**` не тронут вовсе
## Скоуп
Диапазон `origin/dev...HEAD`, 16 файлов, +1259/−16. Две части по ТЗ
(`docs/specs/473-iso-perf-witnesses-and-smoke.md`):
1. Четыре мутанта-свидетеля в `scripts/mutation-gate.mjs` на механизмы
перф-дельты #160 (LRU-кэш размещений, кэш по идентичности массива силуэтов,
гард повторного использования при зуме внутрь, AABB-отсечение) плюс гарды в
`test/iso-scene-render.test.mjs` (AC1, AC2).
2. Диффозависимый `performance_smoke` в `.github/workflows/validate.yml`:
новые выходы `perf_iso`/`perf_interaction` job `changes`, вынесенные из
inline-shell в `scripts/classify-changes.mjs`, условные шаги смока с
бюджетами `demo/performance/budgets-isometric-smoke.json` /
`budgets-interaction-smoke.json`, набор профилей в ключе `reuse` (AC3–AC5,
AC8), плюс ручное воспроизведение AC6/AC7 и правка `PROCESS.md` §8.
Продуктовый код (`src/**`) не меняется нигде — подтверждено:
`git diff origin/dev...HEAD -- src/` пуст.
## Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | `npx tsc --noEmit` | чисто, без вывода |
| unit | `npm test` | `tests 2092 / pass 2091 / fail 0 / skipped 1` |
| build | `npm run build` | собран, без ошибок |
| копии бандла | `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | идентичны |
| no-new-any | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | «0 в 0 файлах» (диф не трогает `src/**`) |
| выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «браузерных смоков не выбирается» — диф не трогает `src/**/*.ts`, выбирать нечего |
| workflow YAML | `python3 -c "yaml.safe_load(...)"` | валиден |
| свидетель W1 | `node scripts/mutation-gate.mjs --id=iso-placement-cache-ignores-selected` | чистый прогон ok, мутант «покраснел, поймано 1 из 1» |
| свидетель W2 | `--id=iso-placement-cache-survives-silhouette-change` | поймано 1 из 1 |
| свидетель W3 | `--id=iso-zoom-in-reuses-near-wall-plate` | поймано 1 из 1 |
| свидетель W4 | `--id=iso-aabb-rejects-touching-wall` | поймано 1 из 1 |
| AC3 негативный прогон | вручную снят `if: needs.changes.outputs.perf_iso == 'true'` у шага «Изометрический профиль по диффу», прогнан `node --test --test-name-pattern="перф-смок добавляет профиль ровно при своём выходе changes" test/validate-workflow.test.mjs` | падает (`AssertionError`, ожидаемый `if:` не найден); файл восстановлен, `git status` чист |
| AC5 негативный прогон | вручную убран суффикс `-${{ needs.reuse.outputs.performance_smoke_set }}` из save-ключа `reuse`, прогнан тест «ключ reuse перф-смока различает наборы профилей» | падает; файл восстановлен |
| AC8 негативный прогон | в `scripts/classify-changes.mjs` регэксп `perf_iso` испорчен (`iso-nonexistent-`), прогнан `test/classify-changes.test.mjs` | 2 из 7 тестов падают; файл восстановлен |
| AC4 | `node --test test/performance-budget.test.mjs` | оба новых smoke-бюджета проходят сравнение `hardMaxMs`/`stat`/`longTasks`/`heap`/`cacheEntries` с полными профилями, синтетический отчёт 9870 мс красит `timing.firstStableRenderMs.median` |
| весь релевантный набор одним прогоном | `node --test test/classify-changes.test.mjs test/performance-budget.test.mjs test/validate-workflow.test.mjs test/performance-workflow.test.mjs test/iso-scene-render.test.mjs` | `tests 61 / pass 61 / fail 0` |
**Не прогонял и почему:**
- `npm run golden:verify` — диф не меняет визуал (`src/**` не тронут, эталоны
не тронуты); автор отдельно предупредил, что `dev` уже красный на golden
из-за непересъёмки #471 (`be5eeb41`) — это не имеет отношения к дельте
#473, отдельная проблема вне скоупа этого ревью;
- `python -m pytest tests_backend -q` — диф не трогает `custom_components/**/*.py`;
- `node scripts/check-docs.mjs` — правило требует его при диффе по `src/**`;
здесь `src/**` не тронут, отпечаток скриншотов не может устареть от этого
диффа;
- `node scripts/model-invariants.mjs` — диф не трогает геометрию, ссылки на
неё, `layout`, `marker.space`, `open_spans`;
- полный перф-прогон (`benchmark:large-house` локально) — задача сама
описывает его как «AC6/AC7 доказаны один раз при реализации», числа уже
записаны автором в хендоффе и внутренне согласуются с бюджетами (см. ниже);
повторный прогон стоил бы часы ради проверки арифметики, которую я сверил
чтением;
- полный `scripts/mutation-gate.mjs` без `--id` (все ~40+ мутантов) — задача
добавляет только 4 новых, гонять весь набор ради ревью четырёх — не
соразмерно (§8); проверены именно новые.
Зелёного Validate на SHA `d95255ad` нет (описано в задании), поэтому дешёвые
гейты прогнаны мной лично, см. таблицу выше — все зелёные.
## AC — доказательство и «чем краснеет»
| AC | Заявлено | Чем доказан | Чем краснеет |
|---|---|---|---|
| AC1 | 4 свидетеля в реестре, каждый ловится | `mutation-gate.mjs --id=<witness>`, 4/4 «поймано 1 из 1» (прогнано мной) | сами мутации из таблицы §4 ТЗ; проверено воспроизведением |
| AC2 | envelope без коллизий = точный резолвер до nudge | код-чтение: `resolveIsoOverlayPlacement` (`src/iso-overlays.ts:449-457`) строит `baseFootprint` и решает `nearWallBefore` **до** нужного участка nudge-логики одинаково независимо от того, есть ли стены; при `resolveCollisions:false` `wallSilhouettes` подаётся пустым (`iso-scene-render.ts:849`), значит `nearWallBefore` всегда `false` и nudge не применяется — итоговые границы буквально равны `baseFootprint`, что и есть «точный резолвер до nudge». Существующий тест (строки 391–395, **добавлен исходной перф-дельтой #160, не этой задачей**) подтверждает поведенчески: `nearWallBefore=false`, отдельная запись кэша. **Проверено чтением, не исполнением** — по формулировке спеки «нет однозначной мутации», согласовано ещё на ревью ТЗ (r2) | нет однозначной мутации (заявлено в ТЗ и подтверждено чтением: обе ветки используют один и тот же `baseFootprint`) |
| AC3 | `changes` отдаёт `perf_iso`/`perf_interaction`; смок добавляет профиль ровно при своём выходе | `test/validate-workflow.test.mjs` (контрактный тест на YAML) + мой негативный прогон | снятие `if:` у шага — тест падает (проверено вручную) |
| AC4 | smoke-бюджеты валидны для `--absolute-only`, повторяют `hardMaxMs` полных | `test/performance-budget.test.mjs`, сверка JSON построчно (я перечитал оба файла — потолки идентичны) | несовпадение `hardMaxMs`/`stat`/списка метрик — тест `assert.equal`/`assert.deepEqual` красится напрямую |
| AC5 | ключ `reuse` различает наборы профилей | `test/validate-workflow.test.mjs` + мой негативный прогон (снятие `-performance_smoke_set` из save-ключа) | тест падает (проверено вручную) |
| AC6 | на `de215578` изометрический smoke-бюджет краснеет по первому кадру | ручное воспроизведение автором: артефакт CI-прогона 34013995127 (`isometric-candidate.json`, 7 образцов) прогнан через `benchmark:compare --absolute-only --budgets=...isometric-smoke.json` → 11 отказов, включая `firstStableRenderMs` 9870/3500. Числа (9870, 30545, 12000 для `maxTotalP95Ms`) я сверил построчно с committed `budgets-isometric-smoke.json` — совпадают | команда и результат названы в issue (хендофф), не «verified» без содержания |
| AC7 | текущий `dev`/ветка укладывается в потолки за 3 образца | ручной прогон автора: isometric 29 с (`firstStableRenderMs` 1241/3500, `switchCycle` 1401/8000, `modelReady` 1158/3000), interaction 36 с (`firstStableRenderMs` 2501/3000, `interactionSeries` 2350/3000, `hoverSeries` 200/500) — все числа ниже потолков в committed JSON, сверено | доказательство «в CI этой ветки» технически недостижимо (дифф без продуктовых путей) — верно и подтверждено повторно; ручной способ адекватен |
| AC8 | диффозависимость доказана на функции классификации | `test/classify-changes.test.mjs`, 7 тестов, плюс мой негативный прогон (испорченный regex `perf_iso`) | 2/7 тестов падают при испорченном паттерне (проверено вручную) |
## Что проверено и корректно
- Все 4 патча мутантов (`find`/`replace`) в `scripts/mutation-gate.mjs`
побайтово совпадают с текущим `src/iso-scene-render.ts` /
`src/iso-overlays.ts` — не устарели после ребейза на #471
(`plate`→`footprint`), что подтверждается их успешным прогоном.
- `demo/performance/budgets-isometric-smoke.json` и
`budgets-interaction-smoke.json` — `hardMaxMs`, `stat`, набор метрик,
`longTasks`, `heap.hardMaxGrowthBytes`, `cacheEntries`, `renderedDevices`
идентичны соответствующим `budgets-large-house-*.json` (сверено вручную и
тестом); регрессионные коэффициенты сознательно отсутствуют, как в ТЗ.
- Изменение `.github/workflows/validate.yml`: `classify` пишет через
`scripts/classify-changes.mjs` вместо inline-`has()`, все три fallback-ветки
(`dev`, force-push, пустая база) переведены на `--all`; ключ `reuse`
расширен `performance_smoke_set`, использован согласованно и в
lookup-, и в save-шаге; `needs: changes` у `reuse` и `performance_smoke`
корректны; YAML валиден.
- Регекспы `perf_iso`/`perf_interaction` в `scripts/classify-changes.mjs`
покрывают все существующие файлы `src/iso-*.ts`, `src/live-*.ts`,
`src/render-*.ts`, `houseplan-render-lifecycle.ts`, `houseplan-card.ts`
(проверено по дереву — все плоские, без подкаталогов).
- Трейлеры на всех 9 коммитах диапазона — `Issue: #473`,
`User-Visible: no` — корректны для infra/process-задачи без
пользовательского поведения; ветка называется `issue/473-iso-perf-witnesses`.
- Документация — `PROCESS.md` §8 и `demo/performance/README.md` описывают
ровно то, что реализовано; `docs/specs/README.md` получил строку. Всё в тех
же коммитах, что и код (правило §11 PROCESS.md).
- Никаких файлов класса D (`dist/**`, `custom_components/houseplan/frontend/**`)
в диапазоне коммитов нет — сборка/копия проверены мной локально, не
закоммичены.
- Автор честно зафиксировал независимую проблему (красный golden на `dev` от
#471) вместо того, чтобы скрыть или чинить её здесь — соответствует «скоуп
не расширяется».
## Находки
Нет ни одной находки уровня High или Medium. Диапазон не меняет продуктовый
код, все заявленные механизмы свидетельствования воспроизведены мной лично с
отрицательными прогонами (AC1, AC3, AC5, AC8), AC2/AC6/AC7 разобраны по коду
и по числам и признаны корректными.
Low (не блокирует, снимается записью): хендофф-комментарий группирует AC2 с
AC1 под «свидетели (AC1, AC2)», не проговаривая явно, что AC2 доказывается
чтением, а не мутацией — из-за чего эта явная запись отсутствовала до
настоящего документа. Формально требование §2.7 («либо тест… либо явная
запись „проверено чтением“») выполнено этим документом; отдельного цикла
это не стоит.
## Чего не проверял (и почему — см. таблицу гейтов)
`golden:verify`, `pytest tests_backend`, `check-docs.mjs`,
`model-invariants.mjs`, полный `mutation-gate.mjs` без `--id`, повторный
запуск `benchmark:large-house` локально — все перечислены в таблице «Как
проверялось» с причиной непрогона.
## Вердикт
Зелёный. Очередь на пре-релиз (`S8-merged` после слияния конвейером).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/473-iso-perf-witnesses`, коммит `d95255ada269` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `e510f97d0d84d89d12cfd1d4448c3368bd6a2475`
```
git log --all --format='%H %T' | grep e510f97d0d84
```
- ТЗ `docs/specs/473-iso-perf-witnesses-and-smoke.md`, блоб `5e2068d0dcab862fdf8964a3ec6966f1e73c2431`
```
git log --all --find-object=5e2068d0dcab862fdf8964a3ec6966f1e73c2431 -- docs/specs/473-iso-perf-witnesses-and-smoke.md
```