20 KiB
CODE-REVIEW-460-r1
Issue: #460 — «Смок мебели краснеет через раз: у живого пути #451 нет точки синхронизации, аналогичной updateComplete».
Этап: code (PROCESS.md §2.7). Заход r1. Блокирующих циклов израсходовано: 0/4.
Материал: git log --oneline origin/dev..HEAD и git diff origin/dev...HEAD на SHA dfb4efaa1cb4695098a93e7472b7f64aab69fdc3, дерево 37f08510521440b1ac1a1d4e2a5f7a1226c5a344, ветка issue/460-live-editor-settle (detached HEAD).
Это первый заход код-ревью для #460 — предыдущего код-ревью нет, разбор полный. Разбор по дельте (§2.9/§2.10) относится к раунду r2+ этого же этапа и здесь не применяется.
Скоуп проверки
Пять коммитов: ca2a0023 (ТЗ), 570da3dd (документ спек-ревью), 5763c6ae (fix, User-Visible: yes), 5d09e722 (test), dfb4efaa (docs: отпечаток скриншотов). Продуктовый код — только src/live-editor.ts и src/houseplan-editor-runtime.ts; остальное — тесты, три смока-потребителя, мутанты, docs/changelog и сгенерированные бандл-деревья.
Проверялось:
- соответствие AC1…AC4 (
docs/specs/460-live-editor-settlement.md) фактическому диффу и его исполняемое доказательство, а не заявление автора; - корректность модели ревизий/waiters в
live-editor.ts(coalescing, гонки, устаревшие обещания); - что terminal-null для hover-preview действительно уходит в полный Lit-commit, а не в live-путь (AC2), и что это доказано смоком, а не только unit-тестом;
- что оба мутанта AC4 реально ловятся своими гейтами (не «должны ловиться», а поймал сам);
- что
smoke_furniture.mjsдействительно даёт ≥10 подряд зелёных прогонов, а не «должен»; - трейлеры коммитов (
Issue:,User-Visible:) и синхронность правки обоих changelog с fix-коммитом; - отсутствие расширения скоупа за пределы «внутренний контракт + terminal-null routing + smoke-потребители».
Как проверялось
Прочитаны docs/SCOPE.md, AGENTS.md, PROCESS.md, docs/specs/460-live-editor-settlement.md, docs/reviews/SPEC-REVIEW-460-r1.md (материал предыдущего этапа, не этого раунда), docs/FURNITURE.md (граница preview), полностью src/live-editor.ts, диффы src/houseplan-editor-runtime.ts, test/live-editor.test.mjs, трёх смоков, scripts/mutation-gate.mjs, scripts/smoke-links.mjs, scripts/bundle-budget.mjs, docs/DEVELOPMENT.md, docs/CHANGELOG.md/.ru.md.
Сборка/тесты выполнялись мной на этом SHA (Chromium и зависимости уже установлены средой, npm ci не запускал):
npx tsc --noEmit— чисто, без вывода.npm test—tests 1990 / pass 1989 / fail 0 / skipped 1, включая обе новые записи вtest/live-editor.test.mjs.node --test --test-name-pattern="live editor|browser smokes use the explicit" test/live-editor.test.mjs— 2/2 ok (изолированный прогон новых тестов).npm run build(tsc --noEmit && rollup -c) — успешно.node scripts/bundle-sync.mjs— обе копии (custom_components/houseplan/frontend,demo/srv/assets) синхронизированы;git status --porcelainпосле сборки пуст — совпадает с закоммиченными деревьями трёх копий (dist, custom_components, demo/srv).node scripts/bundle-budget.mjs—initial View: 297481 Bв полосе298500±2000, предупреждение про малый запас (2519 Б < 15000) — существующий долг #367, не новый, ceiling пересчитан автором с объяснением в комментарии.node scripts/check-docs.mjs— «Documentation checks passed» (diff трогаетsrc/**, отпечаток уже обновлён коммитомdfb4efaa).node scripts/smoke-select.mjs --base origin/dev --head HEAD— «Зарегистрированная связь (3)»:smoke_furniture.mjs,smoke_decor.mjs,smoke_decor_text.mjs, ровно те три, что названы в AC3; НЕОПРЕДЕЛЁННОСТЕЙ и «прямых совпадений» сверх них нет.node demo/smoke_furniture.mjs,node demo/smoke_decor.mjs,node demo/smoke_decor_text.mjs— все триOK(по одному целевому прогону для decor/decor_text, как требует план автотестов).node demo/smoke_furniture.mjsв цикле — 10/10 последовательных запусковexit=0 / OK(AC3: «не менее 10 последовательных запусков без падений»).node scripts/mutation-gate.mjs --id=live-editor-settlement-runs-ahead-of-paint— «поймано 1 из 1».node scripts/mutation-gate.mjs --id=live-editor-smoke-falls-back-to-double-raf— «поймано 1 из 1».- Ручная регрессионная проверка AC2 (описана ниже) — временный откат правки routing и повторный
npm run build+bundle-sync+node demo/smoke_furniture.mjs, дерево полностью восстановлено (cpиз бэкапа, пересборка),git status --porcelainпосле восстановления пуст.
Не запускал: npm run invariants (диф не трогает геометрию/layout/marker.space/wall-ключи — только счётчики ревизий и RAF-роутинг), python -m pytest tests_backend (диф не трогает custom_components/**/*.py, только сгенерированные фронтенд-бандлы внутри custom_components/houseplan/frontend), npm run golden:verify (не-скоуп прямо исключает изменение геометрии/стилей/слоёв, а сам фикс адресует момент коммита кадра, а не то, что рисуется), полный scripts/mutation-gate.mjs без --id (предрелизный гейт §8, не гейт ревью; два адресных мутанта AC4 прогнаны прицельно), полную матрицу demo/smoke_*.mjs (диф не задевает ничего вне зарегистрированной связи по инструменту выбора).
Находки
Ниже — одно наблюдение уровня Low, не блокирует.
| # | Наблюдение | Где | Severity |
|---|---|---|---|
| L1 | scheduleHouseplanEditor не увеличивает state.raf, если typeof requestAnimationFrame !== 'function' (ветка для сред без RAF). В этом случае whenHouseplanEditorSettled видит !state.raf и резолвится немедленно, даже если requestedRevision > paintedRevision — то есть контракт «Promise не завершается раньше покраски» формально нарушается в среде без RAF. Ветка не нова (существовала до этой задачи как no-op guard) и не достижима ни в одном реальном потребителе: браузерные смоки идут через настоящий Chromium (RAF всегда есть), а юнит-тесты сами подставляют globalThis.requestAnimationFrame перед вызовом routeHouseplanEditorUpdate/scheduleHouseplanEditor. AC1/AC2/AC3 её не задевают ни в одном сценарии. |
src/live-editor.ts:365-373 (scheduleHouseplanEditor), src/live-editor.ts:377-383 (whenHouseplanEditorSettled) |
Low |
Решение по L1: не блокирует и не требует правки в этой задаче — сценарий недостижим текущими потребителями (production-браузер и тесты с fake RAF). Оставляю как наблюдение на будущее, если когда-нибудь появится потребитель без RAF (SSR/headless без polyfill).
Что проверено и признано корректным
- AC1 (явная синхронизация):
whenHouseplanEditorSettled/finishRevision/requestedRevision/paintedRevision/waitersвsrc/live-editor.ts:14-104,364-383реализуют монотонный счётчик ревизий с очередью waiter'ов. Прогнанный тестlive editor settlement waits for paint and releases on commit or dispose(test/live-editor.test.mjs:85-161) с управляемым fake RAF доказывает: (а) промис не резолвится до реального вызова RAF-колбэка (firstDone === falseдоrunFrame()); (б) две смежных смены_cursorPtперед одним RAF используют один и тот же кадр (frames.size === 1) и промис ждёт именно объединённую ревизию; (в)commitHouseplanEditor()завершает промис и отменяет RAF (frames.size === 0после commit); (г)disposeHouseplanEditor()делает то же самое. Я запускал этот тест изолированно — зелёный. - AC2 (очистка terminal hover): до правки
routeHouseplanEditorUpdateмаршрутизировал_furnPreviewInput = null(hover-свойство) в live-путь независимо от значения — значит очистка не проходила черезcommitHouseplanEditor, а значит не снимала уже нарисованный в основном Lit-слое preview. Новая строкаsrc/live-editor.ts:136-137возвращаетfalseдля null-значений hover-свойств, отправляя изменение по обычному циклу Lit →updated()→commitHouseplanEditor()(houseplan-card.ts:4009-4011, вызывается безусловно на каждомupdated()). Unit-тест (test/live-editor.test.mjs:118-121) это доказывает явной ассерцией наrouteHouseplanEditorUpdate(...) === false. Браузерное доказательство —pointerLeaveClearsPreviewвsmoke_furniture.mjs, у меня зелёное. Я сам временно откатил именно эту строку (вернул старое поведение маршрутизации), пересобрал (npm run build+bundle-sync) и перезапустилsmoke_furniture.mjs:pointerLeaveClearsPreviewпокраснел (expected true, got false), после чего я восстановил файл из резервной копии, пересобрал и синхронизировал бандл заново —git status --porcelainпуст, дерево идентично коммиту. Это подтверждает, что смок способен падать именно на этом дефекте, а не проходит «всегда зелёным» независимо от кода. - AC3 (browser-путь без временной лотереи): все три названных смока используют
const settleLive = () => c._editorRuntime?._whenLiveEditorSettled() ?? c.updateComplete;вместо двойного RAF (диффыdemo/smoke_decor.mjs,demo/smoke_decor_text.mjs,demo/smoke_furniture.mjs). Статический тестlive editor browser smokes use the explicit settlement contractпроверяет это регэкспом по исходнику всех трёх файлов._whenLiveEditorSettled(houseplan-editor-runtime.ts:1225-1230) — единственный потребитель внутри карточки, наружу не экспортируется (соответствует принятому предположению «остаётся методом lazy editor runtime»). Подготовка мебельного смока (demo/smoke_furniture.mjs:53-70) ждётResizeObserverнаstageEl(), затем крутитwhile (c._modeTransitionBusy || c._refitRaf || c._pendingRefitSize) await new Promise(requestAnimationFrame)— все три флага реально существуют вhouseplan-card.ts(_modeTransitionBusyгеттер,_refitRaf/_pendingRefitSizeполя), не изобретены. Результат пишется вout.initialModeSettledи проверяется общимcheckAll(res)в конце файла (не «вычислено и забыто»). Я прогналsmoke_furniture.mjs10 раз подряд —10/10 exit=0/OK;smoke_decor.mjsиsmoke_decor_text.mjs— по одному целевому прогону, обаOK, как и требует план автотестов. - AC4 (mutation-защита): оба названных в ТЗ мутанта (
live-editor-settlement-runs-ahead-of-paint,live-editor-smoke-falls-back-to-double-raf) добавлены вscripts/mutation-gate.mjsс прицельнымиguard-командами. Я запустил каждый мутант черезnode scripts/mutation-gate.mjs --id=<mutant>— оба «поймано 1 из 1», то есть ихguard-тест реально краснеет на патче и реально зеленеет на чистом коде. - Единственный источник числа: диф не добавляет и не меняет ни одной пользовательски видимой величины (ширина/высота/подпись/подсветка) — только внутренние счётчики ревизий RAF и маршрутизацию null-присвоений.
test/single-source-numbers.test.mjsв общем прогонеnpm testзелёный, отдельного риска дублирования числа в этой задаче не вижу. - Синхронность релиз-артефактов: коммит
5763c6aeсодержит одновременно фикс,docs/CHANGELOG.md,docs/CHANGELOG.ru.mdиdocs/DEVELOPMENT.md(новая заметка про то, чтоupdateCompleteне барьер для live-editor — закрывает наблюдение L2 из спек-ревью) — трейлерUser-Visible: yesна месте, оба changelog правда в том же коммите, а не в последующем. - Трейлеры: у всех пяти коммитов диапазона есть
Issue: #460;User-Visible: yesтолько у fix-коммита, у остальныхno— согласуется с их содержимым (доки/тесты/спека). - Бандл-бюджет: пересчёт потолка
298 000 → 298 500сопровождён комментарием с точными числами (297 496→297 503 Б) и причиной (новый content-hash ленивого чанка); я перепроверил актуальный gzip (297 481 Б) — попадает в полосу, предупреждение про малый запас — существующий, не новый долг (#367). - Три копии бандла синхронны: пересборка +
bundle-syncна моей машине не оставила diff —custom_components/houseplan/frontend,dist,demo/srv/assetsсодержимое коммита воспроизводится детерминированно. - Смок-выборка:
scripts/smoke-select.mjsназывает ровно три смока из AC3 как «зарегистрированная связь», без «прямых совпадений» или «НЕОПРЕДЕЛЁННОСТЕЙ» сверх них — полная матрицаdemo/smoke_*.mjsне требуется. - Скоуп не расширен: диф не трогает геометрию мебели, магнит, толщину, opacity, конфиг/историю Undo-Redo, публичный API карточки/HA,
live-viewport.ts, touch/pen поведение — совпадает с «Не-скоуп» ТЗ.
Чего не проверял
npm run invariants— диф не задевает геометрию стен/layout/marker.space/open_spans; единственные тронутые структуры — счётчики ревизий и boolean-роутинг в live-editor, инварианты модели неприменимы.python -m pytest tests_backend—custom_components/**/*.pyне менялся, только сгенерированные фронтенд-бандлы внутри той же директории.npm run golden:verify— не-скоуп ТЗ прямо исключает изменение геометрии/стилей/слоёв; фикс меняет момент фиксации кадра, а не его визуальное содержимое.- Полный
node scripts/mutation-gate.mjsбез--id— предрелизный гейт (§8), не гейт код-ревью; два адресных мутанта AC4 прогнаны и проверены прицельно, остальные ~тысячи мутантов не относятся к этому диффу. - Полная матрица
demo/smoke_*.mjs(223 смока) — инструмент выбора называет только три относящихся, широкого совпадения (>44 смоков) нет. - Заявленная в issue частота исходного дефекта (7 паданий из 12 на чистом
dev) — не воспроизводил наdev, взял как контекст проблемы; вместо этого лично подтвердил детерминированность 10/10 на текущем SHA и способность смока падать на целевом дефекте (ручной откат AC2-правки). - Perf-профили — не названы в AC, не затронуты чувствительные к перфу пути (диф не в hot-path рендера, только RAF-роутинг вокруг него).
Вердикт
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0
Материал раунда
- Ветка:
issue/460-live-editor-settle, коммитdfb4efaa1cb4— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
37f08510521440b1ac1a1d4e2a5f7a1226c5a344git log --all --format='%H %T' | grep 37f085105214 - ТЗ
docs/specs/460-live-editor-settlement.md, блоб1b215041caaeeb3b98292ace2517ef11339be5a2git log --all --find-object=1b215041caaeeb3b98292ace2517ef11339be5a2 -- docs/specs/460-live-editor-settlement.md