docs: review document for #740

Issue: #740
User-Visible: no
This commit is contained in:
claude[bot]
2026-10-01 18:27:53 +00:00
parent b8f662cdf1
commit 8e4f417f51
+205
View File
@@ -0,0 +1,205 @@
# CODE-REVIEW-740-r1
Issue: #740 · «Производительность: слой лестниц — ≈50 мс на переключение на этаж 1»
Трек: ask (перф, §5) · Этап: code · Заход: r1 · Материал: `1b46c3f42bb8a9df627d237dcee115a162c20dc3`
(два коммита над `origin/dev`: `0225fbff` — К1, `1b46c3f4` — отпечаток скриншотов после ребейза)
## Скоуп
К1 (единственный пункт контракта): все ступени одной лестницы рисуются одним
`<path class="hp-stair-tread">` в слое View и в редакторе плана вместо
3–7 отдельных `<line>`. Строки разметки (контур и `d` ступеней) строятся один
раз на объект геометрии из `cachedStairRenderGeometry` и кэшируются `WeakMap`
(`cachedStairMarkup`, `stairTreadPath`, `src/stairs.ts`). Эффект доказывается
Full Performance (AC4); пиксельная идентичность — golden (AC3, `ci:golden`).
Файлы диффа: `src/stairs.ts`, `src/stairs-view.ts`, `src/stairs-editor.ts`,
`test/stairs.test.mjs`, `demo/smoke_stairs.mjs`, `scripts/mutation-registry.mjs`,
`scripts/smoke-links.mjs`, `docs/STAIRS.md`, `docs/testing-notes/mutation-browser-guards.md`,
`docs/images/screenshots.json` (только отпечаток после ребейза).
## Как проверялось
| Гейт | Статус | Источник |
|---|---|---|
| `npx tsc --noEmit`, `npm test`, `npm run build`, `bundle-policy --verify` | зелёные | Validate на `1b46c3f4`: [36897816678](https://github.com/Matysh/houseplan-card/actions/runs/36897816678) — приняты как дешёвые, не перегонял |
| `npm run build` + `npm run bundle:sync` | зелёные | прогнал сам, для локального запуска смока и golden-диагностики |
| AC1 — `test/stairs.test.mjs` (две находки с `#740`) | прочитан построчно | не перезапускал отдельно — входит в зелёный `npm test` Validate выше |
| AC2 — `node demo/smoke_stairs.mjs` | зелёный, все поля `true`, включая `editorTreadsAreOnePath`, `viewTreadsAreOnePath` | прогнал сам локально |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | одна зарегистрированная связь — `smoke_stairs.mjs` (та же, что в AC2) | прогнал сам |
| AC5 — `node scripts/mutation-gate.mjs --check` | `ok` для `stairs-view-tread-lines`, реестр и `mutation-browser-guards.md` согласованы; общий WARN 207/200 — существующий фон, не этой задачи | прогнал сам |
| AC3 — golden (`ci:golden`) | 4 сцены лестниц (`stairs-flat-normal-light`, `stairs-flat-hover-dark`, `stairs-flat-selected-light`, `stairs-isometric-dark`) **passed** | не локально (см. «Чего не проверял») — проверено по логам CI, см. ниже |
| AC4 — Full Performance | **не подтверждён на материале ревью** | см. находку ниже |
### AC3 отдельно: почему локальный прогон не понадобился
Моя попытка `npm run golden:verify` локально упёрлась в окружение песочницы
(фетч собранного модуля таймаутит ещё до сверки Chromium — то же расхождение
окружения, о котором уже писал автор про 3 PDF-сцены). Вместо повторной
борьбы со средой проверил цепочку переиспользования самого конвейера: job
«Golden-кадры против принятых эталонов» реально выполнился (не был
переиспользован) на прогоне [36888262153](https://github.com/Matysh/houseplan-card/actions/runs/36888262153)
(SHA `5b94c09e`) и по логам все 192 сцены, включая все 4 сцены лестниц,
`passed`. Сам прогон в целом был `failure`, но из-за несвязанного флака
`guard_tail_rejection.mjs` (#776 по словам автора) — job golden внутри него
зелёный независимо. Дальше каждый следующий пуш (`fc083b96`→…→`1b46c3f4`)
переиспользовал этот результат через маркер `reuse-golden-<hash входов>`
(`#208`): лог Validate на `1b46c3f4` прямо называет источник —
`source_sha=5b94c09e0b21701ac0dfcff1a800c5de55ee130d`, «golden не
прогоняется: входы побайтово те же». Это доказательство исполнением, а не
заявлением автора: я прочитал реальный вывод `passed` по каждой из четырёх
сцен на конкретном SHA и прямую связь с материалом ревью по хэшу входов.
AC3 — выполнен.
## Находки
### Medium (в скоупе) — AC4 не доказан на материале ревью: единственный Full Performance прогон стоит на устаревшей базе
**Где:** AC4 таблицы приёмки (issue #740, раздел «Критерии приёмки»); данные
в комментарии автора «Сделано».
**Что не так.** На ветке есть ровно один прогон `performance.yml`:
[36838891001](https://github.com/Matysh/houseplan-card/actions/runs/36838891001),
на SHA `610134c3` (самый первый коммит задачи) против базы `dev` `7ff2b5ae`.
После него ветка трижды ребейзилась на ушедший вперёд `dev` (причины —
красный Validate: устаревший отпечаток скриншотов, конфликт счётчиков
`mutation-browser-guards.md`, флак `guard_tail_rejection.mjs`) и в итоге
слилась с `dev` `0606a366` без конфликтов в коде. Новый Full Performance
после этих ребейзов не запускался — я проверил список прогонов workflow
`performance.yml` для ветки, там только эта одна запись.
Между измеренной базой (`7ff2b5ae`) и фактической (`0606a366`) в `dev`
попали как минимум три коммита, напрямую меняющих ту же цену тёплого
переключения этажа, которую АС4 сравнивает:
- `9324d81b` perf(iso) #739 — убирает двойной рендер-проход в 2.5D с
фоновым изображением, локально ≈50 мс на тёплый заход. Это тот же участок
(тёплое переключение, 2.5D-профили `isometric`/`isometric-stage3`), что
измеряет АС4.
- `fa18ca81` #747 — прямо по тексту коммита использует **в том числе и сам
прогон #740 (`36838891001`)** как один из четырёх калибровочных рядов,
чтобы подтянуть `hardMaxMs` вниз до 1550 мс (2.5D) и тоже назвать новый
потолок. АС4 утверждает «бюджеты и hardMaxMs не меняются» — для базы
`7ff2b5ae` это было верно, для фактической базы слияния `0606a366`
уже нет: потолок к этому моменту другой, и откалиброван отчасти по старым
числам этой же задачи.
- `a0f72280`/`dc77852e` #742/#745 — переключение ключей комнатных фигур по
`space+id`, тоже на пути `switchCycle`.
Риск 4 самого ТЗ предвидел именно это: «AC4 снимается на ветке, приведённой
к актуальному dev» — но после финального приведения к `0606a366` новое
измерение не опубликовано, остались только числа против `7ff2b5ae`.
**Почему это находка, а не придирка к процедуре.** Код К1 между ребейзами не
менялся (конфликты были только в `docs/testing-notes/mutation-browser-guards.md`
и `docs/images/screenshots.json`), поэтому сам факт улучшения, скорее всего,
сохранился. Но АС4 требует не «вероятно», а «медиана `switchCycleMs`
кандидата в `large-house` ниже базы, в `isometric` и `plan-snap` не выше» —
а один из профилей сравнения (`isometric`) напрямую пересекается с
оптимизацией #739, которая уже снизила именно 2.5D-базу, которую АС4 не
переизмерил. Относительный эффект лестниц (−185…−222 мс по старым данным)
мог заметно сократиться на новой, уже ускоренной базе, а подтверждения, что
`isometric`/`isometric-stage3` по-прежнему «не выше базы» на `0606a366`, в
материале нет.
**Чем доказано, что это отсутствует, а не что я придираюсь:** список
прогонов `performance.yml` для ветки (ровно один, на устаревшем SHA);
`git log --ancestry-path 7ff2b5ae..0606a366 -- src/ demo/performance` (пять
перф-значимых коммитов, в т.ч. #739/#747, лежащих между измеренной и
фактической базой); текст коммита `fa18ca81`, прямо называющий прогон
`36838891001` своим калибровочным входом.
**Чем закрывается:** перезапустить
`gh workflow run performance.yml --ref issue/740-stairs-floor-switch -f comparison_ref=0606a366`
(или актуальный на момент повторного раунда merge-base) и опубликовать в
issue числа по всем профилям против него — именно то, что уже требует AC4 и
что предвидел риск 4 самого ТЗ.
Вердикт — жёлтый из-за этой находки; она в скоупе задачи (это её же AC4),
отдельный issue не заводится.
## Что проверено и корректно
- **AC1.** `stairTreadPath` строит `d` как склейку `M a0 a1 L b0 b1` по
`treads` в порядке геометрии — прочитано построчно, совпадает с контрактом
(`src/stairs.ts:345`). `cachedStairMarkup` кэширует по объекту геометрии
через `WeakMap`, без пересборки при повторном вызове — тест
`test/stairs.test.mjs` доказывает это через `===`-идентичность
возвращаемого объекта (а не просто равенство строк), и отдельно показывает
инвалидацию при смене геометрии через `item.angle = 45`. Нулевой случай
(лестница короче 40 см, 0 ступеней → пустая строка) тоже покрыт.
Тест умеет падать: на `dev` символов `cachedStairMarkup`/`stairTreadPath`
нет, импорт не соберётся.
- **AC2.** Разметка: `path.hp-stair-tread` ровно один вместо `line`-ов,
порядок и остальные классы (`outline`, `hit`, `trapezoid`×3/0, `arrow`) не
изменились — прочитано в `src/stairs-view.ts`/`src/stairs-editor.ts` и
подтверждено прогоном `demo/smoke_stairs.mjs` (все поля `true`, в т.ч.
новые `editorTreadsAreOnePath`/`viewTreadsAreOnePath`). У лестницы без
ступеней элемента нет (`markup.treads ? ... : nothing`).
`smoke-select` показывает, что `smoke_stairs.mjs` — единственная и уже
зарегистрированная связь для новых символов; новых непокрытых символов нет.
- **AC3.** Golden для всех 4 сцен лестниц `passed`, проверено исполнением
через цепочку переиспользования CI (см. выше) — не только по словам
автора.
- **AC5.** Мутант `stairs-view-tread-lines` зарегистрирован, откатывает
именно `src/stairs-view.ts` к старым `<line>`, гард —
`smoke_stairs.mjs`, который на такой откат покраснеет
(`editorTreadsAreOnePath`/`viewTreadsAreOnePath`/`count('line.hp-stair-tread')===0`
перестанут выполняться). Запись в `docs/testing-notes/mutation-browser-guards.md`
и `scripts/smoke-links.mjs` на месте. `mutation-gate.mjs --check` — зелёный.
- **Остальной контракт К1.** Порядок DOM-элементов внутри `<g class="hp-stair...">`
не изменился (outline → hit → trapezoid → treads → arrow); класс
`hp-stair-view` по-прежнему различает слои — это использует и сам смок для
независимой проверки каждого слоя. `.hp-stair-tread` в
`src/styles/plan.styles.ts:1645` селектит и `path`, и `line` без правки,
как и требовал контракт. Редактор (`stairs-box.ts`, черновик, ручки) К1 не
трогает — изменений там нет. `src/space-render.ts` и `src/pdf/pdf-scene.ts`
не задеты, что соответствует «не-скоупу» issue.
- **Трейлеры.** Оба коммита несут `Issue: #740` и `User-Visible: no`;
поведение действительно не видно пользователю (markup-рефакторинг под
капотом), changelog не правился — корректно.
- **Числа.** DOM-счётчик «4210 → 2960» в issue и в теле коммита `0225fbff`
совпадает дословно — один источник, не разъехался.
## Чего не проверял
- **Full Performance** сам не запускал — это дорогой гейт (десятки минут,
выделенный workflow с `comparison_ref`); его обязан прогнать автор, см.
находку выше.
- **`npm test` целиком и `npx tsc --noEmit`** отдельно не перегонял — принял
зелёный Validate на точном SHA материала (`1b46c3f4`) как подтверждение
дешёвых гейтов согласно прилагаемой инструкции; `npm run build` всё же
прогнал сам (для локального смока и попытки golden), он тоже зелёный.
- **`golden:verify` локально** — не прогнал до конца (окружение песочницы:
Chromium/фетч бандла ведут себя иначе, чем в CI-профиле). Заменил на
чтение логов CI (см. AC3 выше) — это проверка исполнением на конкретном
SHA, а не доверие к заявлению автора.
- **`npm run invariants`** — не прогонял: диапазон диффа не меняет саму
геометрию (`stairRenderGeometry`, `treads`, `outline` остались прежними
данными), меняется только то, как её точки сериализуются в разметку;
степень риска инвариантов геометрии к этой задаче не относится.
- **`pytest tests_backend`** — не прогонял, диффа в Python нет.
- **Полный `golden:capture`/ручной визуальный осмотр скриншотов** — не делал;
положился на пиксельное сравнение CI, которое строже визуального осмотра.
## Вердикт
Жёлтый. Единственная находка — Medium, в скоупе задачи (AC4 этой же issue):
перезапустить Full Performance против актуального `dev` и опубликовать
числа. Код К1, AC1/AC2/AC3/AC5 проверены и корректны — при зелёном AC4
задача готова к повторному зелёному вердикту без доп. находок по коду.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/740-stairs-floor-switch`, коммит `1b46c3f42bb8` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `14c4fb8c2402beb23220a3ca0e3d4e45f06f5e43`
```
git log --all --format='%H %T' | grep 14c4fb8c2402
```
- Тело issue: `9cfcfc9e8157d5a25961e793ed530de257848fb5d870dee885fbd71103ffb3bd`
- Вердикт конвейера: `yellow` · High 0 · маршрут `fix`
<!-- hp:usage input_tokens=4228 output_tokens=36126 cache_creation_input_tokens=122259 cache_read_input_tokens=4857388 num_turns=65 -->