Files
houseplan-card/docs/reviews/CODE-REVIEW-521-r2.md
T
2026-09-10 20:48:53 +00:00

275 lines
27 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-521-r2
Issue: #521 «fix: alignment guides follow the live gesture again»
Материал ревью: `9a3af808d95684b32df8f102da4211f3f800703c` (ветка
`issue/521-align-guides-live`). Диапазон делты этого раунда —
`0715321fdc69a2955d7850f3f384c1a0fdc25311..9a3af808` (материал r1 → материал
r2): один коммит `deebcfb0` (публикация `CODE-REVIEW-521-r1.md` конвейером,
0 изменений кода) и один продуктовый/тестовый коммит `9a3af808` «fix: hide the
settled guides copy for the whole gesture, and prove it».
Заход: r2 · блокирующих циклов израсходовано 1 из 4 (r1 — жёлтый).
## Скоуп
Единственная находка r1 (Medium, в скоупе) — AC5 заявлял доказательство
(«снятие подавления осевой копии слоя даёт 2 группы `.alignguides` — смок
красный»), которого прежний свидетель не давал: осевая копия
`.hp-editor-only-layer` в сценарии смока всегда была пуста, потому что смок ни
разу не форсировал осевую отрисовку посреди самого жеста. Дельта r1→r2 —
исправление ровно этого: переписанный сценарий `devices` в
`demo/smoke_align_guides.mjs` форсирует настоящую осевую отрисовку
(`c._hdrH += 0.01`) в середине перетаскивания, проверяет, что она
действительно произошла, что владение слоем перешло к осевшей сцене с
правильной (живой) точкой, что видимая группа ровно одна на каждом шаге
передачи, и что следующее движение возвращает владение живому художнику; плюс
новый мутант `live-editor-keeps-the-settled-guides-visible` в
`scripts/mutation-gate.mjs`, откатывающий подавление обратно в ветку `plan`
(как было до задачи).
Продуктовый код (`src/live-editor.ts`) в этой дельте не изменился по существу:
единственная правка — комментарий, объясняющий измеренный механизм передачи
слоя (было общее «плановый режим уже это делал», стало «владение чередуется
само, `_commitLiveEditor()` … измерено: две `.alignline` …»). Логика
`makeTransparent(...)` вне ветки `plan` осталась той же, что была принята и
проверена в r1 — сама защита не менялась, менялось только доказательство её
работы.
Дельта локальна по критериям §2.10: не ребейз (единственный промежуточный
коммит — публикация документа предыдущего раунда), не смена контракта
поведения (контракт п.3/AC5 переформулированы под тот же механизм, а не
заменены другим), не новая подсистема, объём (17 строк комментариев в
`live-editor.ts`, 16 строк нового мутанта, ~90 строк в свидетеле) далеко не
сопоставим с исходной задачей. Полный повторный разбор всей задачи не
требуется — по AC1–AC4, AC6–AC9 переверяю только то, что могла задеть дельта
(сам свидетель), остальное наследую из r1.
### Отдельная находка: тело issue правилось после зелёного ревью ТЗ (#517)
Хеш нормализованного тела issue на момент зелёного `SPEC-REVIEW-521-r3.md`
(«Материал раунда», конец документа) — `1e7f8d192f340e80e878aec0d6de383c982263254b408f8fc928f383130f5684`.
Текущий хеш (пересчитан дважды независимо: `gh issue view --json body` и
`gh api repos/.../issues/521 -q .body`, оба дают одинаковый нормализованный
текст) — `d117679d25abeab7ebfad3ed7902a8222625fc2ad43d0396b56d6fdacc12b5d3`.
Хеши расходятся — тело менялось после зелёного ревью ТЗ. Дельту GitHub не
хранит, поэтому проверяю не разницу, а текущий текст целиком (см. «Что
проверено» ниже).
Что именно изменилось, восстанавливается по признанию автора в комментарии
«Исправление по код-ревью r1 → заход r2»: «ТЗ: пункт контракта 3 и AC5
переписаны под измеренный механизм». Прочитал текущий пункт 3 «Контракта» и
строку AC5 в теле issue — они дословно описывают именно тот механизм
(чередование владения через `_commitLiveEditor()`, измеренные «две
`.alignline`, осевая отстаёт на шаг»), который выяснился при разборе r1 и
теперь доказан переписанным свидетелем. Текст не расходится с кодом и тестами
этого раунда — я сверил это построчно (см. «Что проверено»), а не принял на
слово автора.
**Оценка:** это находка процесса — правка раздела `## ТЗ`, уже прошедшего
зелёное ревью, должна по идее возвращаться на новый цикл `S4-spec-review`, а
не вноситься по ходу код-ревью. Но по существу правка не меняет ни персону, ни
сценарий, ни объём видимого изменения, ни один AC-номер — она уточняет
формулировку контракта и доказательства AC5 под механизм, который сам же
код-ревью r1 и вскрыл, и делает это прозрачно (объяснено в комментарии, а не
тихой правкой). Отдельного цикла ревью ТЗ эта поправка не заслуживает: именно
для такого случая существует делегирование «технический спор решает ревью
кода» (`PROCESS.md` §7.1) — здесь спора и нет, есть согласие с находкой r1,
выраженное в тексте. Классифицирую как **Low**, снимаю решением ревьюера:
текст ТЗ проверен целиком и корректен, действие не требуется.
## Как проверялось
Валидация на материале раунда (`9a3af808`) зелёная
(https://github.com/Matysh/houseplan-card/actions/runs/34526718586), включая
шесть параллельных джобов «Мутанты по диффу» — я прочитал логи всех шести
шардов напрямую (`gh run view --job <id> --log`), а не поверил статусу
«success» на слово:
| Гейт | Прогнал | Результат |
|---|---|---|
| Validate на `9a3af808` (`tsc`, `test`, `build`+bundle-sync, `no-new-any`) | нет, зачтено по зелёному прогону CI (см. ссылку) | success |
| Логи 6 шардов «Мутанты по диффу» в этом прогоне | да, прочитаны построчно | все 5 мутантов задачи пойманы: `live-editor-devices-drops-align-guides` (шард 5/6), `live-editor-decor-drops-align-guides` (4/6), `live-editor-plan-drops-align-guides` (1/6), `align-point-reads-frozen-snapshot` (2/6), **`live-editor-keeps-the-settled-guides-visible` (6/6)** — новый мутант этого раунда, «тест покраснел, как обязан» |
| `npm run bundle:sync` (пересборка + сверка `dist`/`custom_components/.../frontend`/`demo/srv/assets`) | да, локально (нужно было поднять `demo/srv/assets`, он не коммитится) | чисто, `git status` не показал diff после сборки — включает `tsc --noEmit` |
| `node demo/smoke_align_guides.mjs` (полный сценарий, все 34 поля) | да | все `true`, `OK` |
| `node scripts/mutation-gate.mjs --id=live-editor-keeps-the-settled-guides-visible` | да, независимо от CI, в рабочем дереве материала | «поймано 1 из 1» |
| Ещё 4 мутанта задачи по одному (`--id=…`) | да, все 4 | все «поймано 1 из 1» |
| `node --test test/smoke-harness-contract.test.mjs` (AC8) | да | 7/7 |
| `node scripts/check-docs.mjs` | да | зелёный, 7 файлов, 12 внешних ссылок |
| `node scripts/smoke-select.mjs --base 0715321f --head HEAD` (выборка по дельте раунда, не по всей задаче) | да | 1 прямое совпадение — `demo/smoke_align_guides.mjs` (символ `_alignPoint`); других смоков дельта не касается |
| `node demo/smoke_isometric_live_touch.mjs`, `node demo/smoke_touch_tips.mjs` | да (продуктовый код не менялся между r1/r2, но риск ТЗ по touch не был явно закрыт в r1 — закрываю здесь) | оба зелёные, все поля `true` |
| `cmp dist/houseplan-card.js custom_components/.../houseplan-card.js`, то же для `houseplan-panel.js` и `houseplan-assets.json` | да | побайтово идентичны |
| `git show -s --format=full 9a3af808` (трейлеры) | да | `Issue: #521`, `User-Visible: no` — верно: дельта не меняет видимого поведения (сама правка уже была `User-Visible: yes` в `53585b45`, эта дельта — доказательство и комментарий) |
| Хеш тела issue (`issueBodyDigest` из `scripts/review-doc-guard.mjs`, два независимых способа получения текста) | да | не совпадает с зафиксированным в зелёном `SPEC-REVIEW-521-r3.md` — находка выше |
Не прогонял: `golden:verify` (дельта раунда не меняет визуал — только
комментарий, тестовый файл и конфиг мутанта; видимый рендер не тронут),
`pytest tests_backend` (Python не тронут), полный набор из 239 смоков (не
обосновано ни диффом, ни `smoke-select`), performance-профили (см. ниже),
WSL/HA-харнесс.
**Перф (AC9).** Не перезамерял. Диф `src/live-editor.ts` между материалом r1 и
материалом r2 — только комментарии (проверил построчно: `if`/`makeTransparent`
вызовы в `paintHouseplanEditor` и `editorTemplate` идентичны байт в байт по
существу, разница — только текст комментариев). Комментарии не выполняются,
поэтому число операций в кадре жеста не изменилось ни на одну со времён r1, где
я независимо (не на слово автора) прогнал `benchmark:large-house-interaction`
+ `benchmark:compare` с собственной базой на `fa7ac02c` и получил зелёный отчёт
без регрессии (см. `docs/reviews/CODE-REVIEW-521-r1.md`, таблица гейтов). Этот
вывод наследуется без повторного прогона.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| Medium: AC5 заявлял «снятие подавления даёт 2 группы `.alignguides` — смок красный», но смок никогда не форсировал осевую отрисовку посреди жеста, поэтому осевая копия всегда была пуста и мутация не проверялась | Сценарий `devices` смока форсирует настоящую осевую отрисовку через `c._hdrH += 0.01` (свойство вне `liveProperties`/`hoverProperties`/`gestureProperties` — проверил по `src/live-editor.ts:80-86`, оно не маршрутизируется в живой путь ни при каком состоянии жеста), проверяет, что рендер действительно случился (`handoverHappened`, счётчик `willUpdate`), что слой перешёл сцене с правильной живой точкой (`handoverGivesTheLayerBackToTheSettledScene`, `handoverGuideStaysOnTheLivePoint`), что видимая группа ровно одна (`handoverKeepsExactlyOneGuide`), и что следующее движение возвращает владение живому художнику (`nextMoveTakesTheLayerBack`). Новый мутант `live-editor-keeps-the-settled-guides-visible` откатывает `makeTransparent(...)` обратно в ветку `plan` — смок красный именно на `nextMoveTakesTheLayerBack` | `demo/smoke_align_guides.mjs:162-197`; собственный прогон смока — все поля `true`; собственный прогон `mutation-gate.mjs --id=live-editor-keeps-the-settled-guides-visible` — «поймано 1 из 1»; то же самое независимо подтверждено логом шарда 6/6 в Validate на `9a3af808` |
| (побочно устранённая слабость свидетеля, не отдельная находка r1) счёт направляющих по числу узлов в DOM вместо видимых — гашение делает копию прозрачной, а не удаляет | `visible(selector)` фильтрует по `layer.style.opacity !== '0'` вместо сырого `querySelectorAll`; проверил, что `makeTransparent` (`src/live-editor.ts:157-163`) действительно ставит `element.style.opacity = '0'` строкой, а `restore()` снимает через `removeProperty('opacity')` | `demo/smoke_align_guides.mjs:27-36`, `src/live-editor.ts:157-163`, `:213-220` |
Находка закрыта по существу: не «сделали AC5 неопровержимым переписыванием
слов», а заставили смок действительно воспроизвести сценарий (внешний осевой
рендер посреди активного жеста), который единственный делает защиту
наблюдаемой, и подтвердили мутацией, что без защиты сценарий ловится.
## Унаследовано из r1
Без повторной проверки в этом раунде приняты выводы
`docs/reviews/CODE-REVIEW-521-r1.md` (материал:
`0715321fdc69a2955d7850f3f384c1a0fdc25311`), поскольку дельта r1→r2 их не
задевает:
- диагноз и причинность регресса (`c0d61ca3`/#451, оба слома — слой и
замороженная точка) — не менялись, дельта их не касается;
- AC1 (направляющая в трёх живых жестах), AC2 (живая точка), AC3, AC4 —
свидетель для `decor`/`plan`-сценариев не менялся вовсе в этой дельте
(правки смока — только в сценарии `devices`, между строками 162 и 197);
мутанты `live-editor-devices-drops-align-guides`,
`live-editor-decor-drops-align-guides`, `live-editor-plan-drops-align-guides`,
`align-point-reads-frozen-snapshot` перепрогнаны мной заново тем не менее
(дёшево, один и тот же смок теперь длиннее) — все 4 по-прежнему «поймано 1 из
1»;
- AC6 (один расчёт кандидатов на кадр) — код и проверка не менялись в этой
дельте, наследуется техническое заключение r1 («проверено чтением», без
отдельного мутанта — осталось так же, это не регрессия дельты);
- AC7 (старые гарантии, #400) — не менялся код и не менялся тот участок
смока;
- AC8 (свидетель не фабрикует состояние) — тест-линт `test/smoke-harness-contract.test.mjs`
не менялся; перепрогнал (7/7) на новом более длинном смоке — линт по-прежнему
проверяет отсутствие `_deviceDrag =`/`_decorDraft =`, новые строки (`c._hdrH
= …`, `c._layout = …` уже был в старой версии) под запрет не подпадают;
- AC9 (перф) — см. «Как проверялось» выше, база `fa7ac02c` и зелёный отчёт r1
наследуются без повторного прогона, так как дельта не трогает исполняемый
код;
- бюджет `core-file-budget` и мягкость `_renderAlignGuides` на карточке
(`this._editorRuntime?._renderAlignGuides() ?? nothing`) — не менялись,
проверено в r1 чтением;
- трейлеры и changelog коммита `53585b45` — не менялись, `User-Visible: yes`
подтверждён в r1.
Технический раздел «Чего не проверял» из r1 (golden, pytest, полная матрица
смоков, WSL/HA-харнесс) остаётся в силе тем же образом — дельта раунда их не
задевает и не расширяет их необходимость.
## Находки
### Low-1 (снимается, см. выше) — тело issue правилось после зелёного ревью ТЗ без нового цикла `S4-spec-review`
См. раздел «Отдельная находка» в «Скоупе». Текст пункта 3 «Контракта» и AC5
переписан автором вместе с исправлением по код-ревью r1, без возврата в
`S3-spec`/`S4-spec-review`. Проверено: новая формулировка точно описывает
реализованный и теперь доказанный механизм, персона/сценарий/объём/номера AC
не изменились, изменение раскрыто в комментарии автора. Действия не требую;
записываю, чтобы хеш в следующем зелёном документе (материал раунда ниже)
зафиксировал новое значение и цепочка не молчала.
Других находок нет.
## Что проверено и корректно
- **Механизм передачи владения слоем воспроизведён чтением независимо от
комментария автора.** `updated()` (`houseplan-card.ts:4157`) на каждом
осевом рендере зовёт `_commitLiveEditor()` →
`commitHouseplanEditor` (`live-editor.ts:439-454`): отменяет
незавершённый `raf`, вызывает `restore(state)` (снимает `opacity`/`visibility`,
выставленные `makeTransparent`/`dim`/`hide`) и очищает живой корень
(`render(nothing, target)`). Значит после любого settled-рендера слой физически
переходит к осевшей сцене чистым, без «памяти» о прежнем подавлении.
- **`_hdrH` гарантированно не маршрутизируется в живой путь.** Прочитал
`liveProperties`/`hoverProperties`/`gestureProperties`
(`live-editor.ts:80-86`) и `routeHouseplanEditorUpdate`
(`live-editor.ts:120-145`) — `_hdrH` не входит ни в один набор, значит
`namedLive` ложно и осевой рендер происходит независимо от активного жеста.
Форсирование смоком (`c._hdrH += 0.01`) действительно моделирует внешний
settled-рендер (приход `hass`, ресайз), а не подглядывает во внутреннее
состояние жеста.
- **`_alignPoint` — геттер, а не хранимое состояние**
(`houseplan-card.ts:12879`), поэтому и живой, и осевой путь при вызове
`_renderAlignGuides()` во время самого settled-рендера читают одну и ту же
живую точку — вот почему «отставание на шаг» проявляется только НА
СЛЕДУЮЩЕМ живом кадре после хендовера, а не в момент самого хендовера; смок
проверяет обе фазы раздельно (`handoverGuideStaysOnTheLivePoint`, затем
`nextMoveTakesTheLayerBack`), и это совпадает с объяснением автора.
- **Мутант `live-editor-keeps-the-settled-guides-visible` патчит именно тот
код, который сейчас в файле** — сверил `find`-строку патча
(`scripts/mutation-gate.mjs`) с текущим `paintHouseplanEditor`
(`live-editor.ts:357-360`) посимвольно, совпадает; применимость и результат
подтверждены собственным прогоном и логом CI.
- **AC8 не сломан новыми строками смока.** `c._hdrH = …`, `c._layout = …` (уже
было раньше), `c.requestUpdate()` не запрещены тест-линтом — запрещены
только присваивания `_deviceDrag =`/`_decorDraft =`; перепрогнал линт, 7/7.
- **Touch-контракт не задет** (риск ТЗ явно требовал: «существующие
touch-смоки обязаны остаться зелёными») — `smoke_isometric_live_touch.mjs` и
`smoke_touch_tips.mjs` зелёные на материале раунда; в r1 эти два смока не
были явно названы прогнанными, закрываю пробел здесь, а не переношу его
дальше.
- **«Одно число — один источник» не применимо.** Направляющая — линия и точка,
не число; пользователю в этом диффе не показывается ни одна величина
дважды.
- **Бандл и трейлеры.** `bundle:sync` пересобрал дерево с нуля, `dist`/
`custom_components/.../frontend`/`demo/srv/assets` побайтово совпали с
закоммиченными файлами; `git show -s --format=full 9a3af808` — трейлеры
`Issue: #521`/`User-Visible: no` корректны, changelog не тронут и не должен
быть (дельта раунда не меняет видимое поведение сверх уже опубликованного в
`53585b45`).
## Чего не проверял
- `npm run golden:verify` — дельта раунда не меняет визуал (комментарий +
тестовый файл + конфиг мутанта), в r1 видимый рендер тоже не менялся кроме
уже принятого identical-скриншота.
- `python -m pytest tests_backend` — Python не тронут ни в одном коммите
задачи.
- Полная матрица из 239 смоков — не обоснована ни диффом раунда, ни
`smoke-select.mjs` (одно прямое совпадение); полный набор — предрелизный
гейт.
- Не перезамерял `benchmark:large-house-interaction` — обосновано выше
(дельта раунда не исполняемый код).
- Не запрашивал у владельца дополнительного цикла `S4-spec-review` по поводу
правки ТЗ после зелёного вердикта r3 — see Low-1: решил вопрос по существу
сам, поскольку это чисто техническая правка формулировки под уже найденный в
код-ревью механизм, а не продуктовый вопрос из числа тех, что задаются
владельцу (§7.1).
## Вердикт
Единственная находка предыдущего раунда закрыта по существу и подтверждена
воспроизведением (собственный прогон смока и мутанта плюс независимое чтение
логов CI). Новых блокирующих находок нет; единственная новая находка —
процедурная (Low), снята решением ревьюера с записью выше. High: 0. Задача
готова к очереди на пре-релиз.
Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0 → в задаче
Документ: docs/reviews/CODE-REVIEW-521-r2.md
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/521-align-guides-live`, коммит `9a3af808d956` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `20fbfc5c7f020a361c04f252900511c2e383ceba`
```
git log --all --format='%H %T' | grep 20fbfc5c7f02
```
- Тело issue: `d117679d25abeab7ebfad3ed7902a8222625fc2ad43d0396b56d6fdacc12b5d3`
- Вердикт конвейера: `green` · High 0