23 KiB
CODE-REVIEW #477 · заход r1
- Issue: #477 «Оптимизировать планы» не должна требоваться повторно
- Этап: code (PROCESS.md §2.7)
- SHA:
04f4cd10(HEAD), диапазонorigin/dev..HEAD= 9 коммитов (4693d97d…04f4cd10), из них продуктовыйa979699a(fix:,User-Visible: yes), остальные — тест-таргетинг и документация - Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Ребейз: ветка приведена к
devконвейером (+1 коммитdevповерхdfc4ce60→04f4cd10); разбор ниже — полный, не по дельте - ТЗ:
docs/specs/477-editor-writer-fixed-point.md, ревью ТЗ зелёное (r1, см.docs/reviews/SPEC-REVIEW-477-r1.mdи комментарий issue от 2026-09-06 16:51)
Скоуп
Диапазон git diff origin/dev...HEAD — 63 файла, из них продуктовый код:
src/writer-fixed-point.ts (новый), src/room-reference-transaction.ts
(новый), правки src/houseplan-editor-runtime.ts, src/draft-live-commit.ts,
src/coincident-partitions.ts, src/align-grid.ts, src/houseplan-card.ts.
Плюс новые/изменённые тесты (test/writer-fixed-point.test.mjs,
test/align-grid.test.mjs, фикстура test/fixtures/477-writer-fixed-point-writers.json),
смоки (demo/smoke_writer_fixed_point.mjs новый, demo/smoke_wall_chain_merge.mjs
изменён), бенчмарк-харнесс, scripts/mutation-gate.mjs (+9 новых
mutation-свидетелей), scripts/smoke-links.mjs, документация (ARCHITECTURE,
CANVAS, STATUS, TESTING, USER-GUIDE ru/en, CHANGELOG ru/en, specs/README) и
синхронизированный бандл (три копии).
Соответствует ТЗ §5.1 пункт за пунктом: seed-bounded merge/reconciliation на
finish цепочки, единый finish barrier для всех штатных выходов, history-aware
Undo/Redo без скрытого шага, room-reference transaction для Delete/Merge,
исключение furniture/image из alignAllToGrid, исполняемая матрица writers,
перф-свидетель terminal-click/finish, обновлённая документация.
Как проверялось
Зелёного Validate на 04f4cd10 не найдено — прогнал гейты сам.
Дешёвые гейты (обязательные):
npx tsc --noEmit— чисто, без вывода.npm test— 2156 pass / 0 fail / 1 skip (не 46/40, как заявил автор в комментарии — это был targeted подмножество; полный прогон зелёный).npm run buildиnpm run bundle:sync— сборка чистая, working tree после синка не изменился (git status --porcelainпуст) → три копии бандла (dist,custom_components/.../frontend,demo/srv/assets) совпадают.node scripts/check-docs.mjs— «Documentation checks passed (7 files, 12 external links)» (diff трогаетsrc/**, гейт обязателен и пройден).
Инварианты модели (diff трогает partitions, openings,
marker.vacuum.segment_map — обязателен по инструкции):
npm testуже гоняет их на всех моделях проекта — зелёный.node scripts/model-invariants.mjs --config test/fixtures/optimize-storage-roundtrip.json --json→violations: [].- то же для
test/fixtures/477-writer-fixed-point-writers.json→violations: [].
Мутационный гейт:
node scripts/mutation-gate.mjs --check→ 530/530 ok, exit 0, включая 9 новыхwriter-*свидетелей (writer-finish-reset-bypasses-finalizer,writer-finish-adopts-unsafe-candidate,writer-history-skips-finished-chain-normalization,writer-room-delete-keeps-direct-reference,writer-room-delete-keeps-vacuum-reference,writer-align-snaps-free-decor-transform,writer-terminal-click-runs-finish-reconciliation,writer-finish-skips-seed-merge,writer-finish-skips-coincident-reconciliation) — дисциплина «тест умеет падать» проверена самим прогоном гейта: guard реально краснеет при мутации и гейт репортитok, то есть проверяемый код ловит регресс.
Смоки — выбраны по выводу node scripts/smoke-select.mjs --base origin/dev --head HEAD
(228 смоков всего; изменено 7 файлов src/**, 81 символ на изменённых
строках). Прогнал прямые совпадения и одну зарегистрированную связь,
относящиеся к затронутым путям (wall-chain finish, room reference, terminal
click, room acceptance/reconcile):
| Смок | Связь | Результат |
|---|---|---|
smoke_writer_fixed_point.mjs |
прямое (новый, специально под #477) | OK, все 12 полей true |
smoke_wall_chain_merge.mjs |
прямое (_finishWallChain, изменён в diff) |
OK, все 9 полей true |
smoke_wall_draw_click.mjs |
прямое (_activeWallChainId, _path…) |
OK, все 8 полей true (rejected-state atomicity) |
smoke_unified_wall_tool.mjs |
прямое | OK, все 16 полей true, включая acceptedRoomIsOptimizeFixedPoint |
smoke_room_resize.mjs |
прямое (_checkSpacePhysicalGeometry, изменена сигнатура) |
OK |
smoke_optimize_coincident_partition.mjs |
зарегистрированная (reconcileCoincidentPartitions, #276) |
OK, все 10 полей true |
smoke_v8_draft_write.mjs |
прямое (_activeWallChainPartitionIds, _pendingPhysicalWrites) |
OK, все 8 полей true |
Не прогонял остальные ~46 прямых/слабых совпадений (smoke_room_autoclose,
smoke_editor_tabs, smoke_free_walls, zigbee/topology/decor смоки и т.д.):
совпадения там — по общим геттерам состояния (_curSpaceCfg, _path,
_saveConfig), которые не входят в изменённый контракт: правки не трогают
zigbee, decor-персист, resize-контроллер или furniture-палитру напрямую;
выбор ограничен темой diff, а не полным списком «прямых» имён. smoke_edit_walk
(зарегистрированная связь, «дефекты рождаются в редактировании») не
прогонялся отдельно — покрытие того же класса рисков дают
smoke_wall_chain_merge+smoke_wall_draw_click+smoke_writer_fixed_point.
Перформанс (AC9, §11 ТЗ — обязателен, путь #461 задет):
npm run benchmark:wall-draw-click — pass: true, structural: true,
timingPass: true. Ключевые числа (сам прогнал, не со слов автора):
click median 7 ms / max 10.7 ms (бюджет 150/250), remote median 11.4 ms
(бюджет 30.5), finish 4.7 ms (бюджет 250), remote-finish (удвоенные
unrelated rooms/partitions, remoteFixture.positiveSegments = 2×base) 9.5 ms
(бюджет 4.7×1.5+20=27.1) — это ровно требование §11 «удвоение unrelated
rooms/partitions не должно более чем в 1.5 раза увеличивать median finish
плюс 20 ms». Структурные счётчики: terminal click — fullSpacePhysicalChecks:0,
configWrites = 1 на клик, history = 1 на клик; finish — один
localPhysicalChecks, один configWrites, history: 0,
wallFinishBarriers: 1 — подтверждает «один bounded transaction, без
дополнительной history-команды».
Golden: НЕ прогонял npm run golden:verify (222 сценария, полный
предрелизный гейт). Обоснование по чтению кода и матрицы, а не по заявлению
автора: (1) merge коллинеарных partitions и reconcile — lossless-операции над
идентичной абсолютной геометрией (те же a/b, тот же cm), рендер стен не
зависит от числа записей partitions с одинаковым итоговым контуром; (2) два
единственных golden-сценария, упоминающих Optimize
(optimize-preflight-dialog-*, optimize-orphan-references-dark-en),
подставляют синтетический card._alignDialog = {...} вручную
(demo/golden/harness.mjs:1582-1610), а не вызывают реальный
alignAllToGrid/optimizePlans на fixture — furniture-exemption в
align-grid.ts не может повлиять на их пиксели; (3) ни один сценарий матрицы
не проходит через _finishWallChain/Delete-Merge room. Риск ложного
пропуска низкий, но это явное решение, а не тихий пропуск.
Explicit-skip (по diff и AC не требуются):
python -m pytest tests_backend—custom_components/**/*.pyне тронут.- Полный набор
demo/smoke_*.mjs(228 шт.) — задача не задевает всё (zigbee/vacuum/isometric/decor-палитра и т.д. вне скоупа).
Находки
Находок уровня High или Medium в скоупе задачи не обнаружено.
Мелкое наблюдение (не находка, не блокирует): в комментарии реализации автор
написал «расширенный регрессионный набор — 40/40» вместо честного полного
npm test; сам прогнал полный набор — 2156/0/1, разночтений с продуктовым
кодом не выявлено. Указываю для полноты картины, замечание не по существу
кода.
Что проверено и корректно
- AC1 (finished chain fixed point).
test/writer-fixed-point.test.mjsпрогоняет 2- и 3-сегментные коллинеарные цепочки черезfinalizeWallChainSpace→optimizePlans(...).changed === false; смокsmoke_wall_chain_mergeподтверждает то же через реальный_markupClick/_finishWallChainв собранном бандле (straightRunMergedAtFinish: true). - AC2 (coincident chain fixed point). unit-тест на фикстуре
276-coincident-partition.json:partitionsReconciled: 1,openingsRehosted: 1, абсолютная геометрия проёма (x,length,contact,future_field) сохранена, следующий Optimize — no-op; смокsmoke_optimize_coincident_partitionподтверждает атомарность Apply/reload/Undo. Прочитано и проверено: seed-scope действительно ограничивает reconciliation только выжившими ID цепочки (survivingSeedIdsвwriter-fixed-point.ts:47-58), несвязанные швы не трогаются (тест «seed scope merges the touched chain but leaves unrelated historical seams alone»). - AC3 (fail-closed atomicity). Юнит-тест «unsafe coincidence is
fail-closed and byte-equivalent» — 3 варианта (unknown field, column
conflict, orphan opening host) не дают частичной записи,
report.config.spaces[0]побайтово равенbefore. На уровне транзакции —commitWallChainFinishGeometry(draft-live-commit.ts) строит candidate на клоне, проверяетwallModelOffGridValueCount+_checkSpacePhysicalGeometry_junctionLimitsIntroducedдоadoptWallSegmentModelCandidateInPlace; при отказе_finalizeWallChainPartitions(houseplan-editor-runtime.ts:1481+) восстанавливает_path/_activeWallChainId/_wallChainRedo/_closingWallCmиз локальногоsession-снапшота. Мутацияwriter-finish-adopts-unsafe-candidateкрасит guard, если проверку убрать — свидетель умеет падать (гонял gate,ok). Смокsmoke_wall_draw_click:rejectedStateRestored,rejectedSessionRestored,rejectedAddsNoHistory,rejectedQueuesNoWrite— всеtrue.
- AC4 (history continuity).
_canonicalWallChainHistoryState(houseplan-editor-runtime.ts:1976-2016) прогоняет lossless finalizer на snapshot только если цепочка уже не активна (this.host._activeWallChainIdпроверен первым), иначе возвращает snapshot буквально — соответствует §7 ТЗ («пока цепочка активна,_applyGeometryStateприменяет snapshot буквально»). Смокsmoke_writer_fixed_point:finishedChainUndoOptimizeNoop,finishedChainUndoHasOnePartition,finishedChainRedoOptimizeNoop,finishedChainRedoHasOnePartition— всеtrue, т.е. Undo/Redo после finish не возвращает optimizer debt и не плодит лишний partition.finishAddsNoHistory: true— история не получает скрытый шаг. Мутацияwriter-history-skips-finished-chain-normalizationподтверждена красной. - AC5 (all finish owners). Source-contract тест
«routes every explicit chain finish through one finalizer» проверяет:
_finishWallChainвызывает_finalizeWallChainPartitions; Reset-кнопка (btn.reset) в контекст-трее теперь идёт через_finishWallChain(), а не напрямую через_cancelPath()(проверил диф —_cancelPathне удаляет уже принятые стены, только сессионное состояние, так что смена поведения кнопки корректна и соответствует §6.4);_applyWallFaceBatchпри «не создавать ни одну предложенную комнату» и_keepClosedAsPartitionsтоже проведены через_finalizeWallChainPartitions;_slideTo,_onHashChange,Escape(вhouseplan-card.ts) вызывают_finishWallChain; тест явно проверяет ОТСУТСТВИЕ вызова финализатора из_markupClick(terminal-click путь) — прочитал_stagePointerCancel: pointer cancel/pinch не вызывают_finishWallChain/_finalizeWallChainPartitions, соответствует «pointer cancel, pinch и второй touch сами по себе не заканчивают цепочку». - AC6 (room reference transaction).
room-reference-transaction.tsпрочитан целиком:rewriteMarkerRoomReferencesменяет только точное совпадениеroom_id/значенийvacuum.segment_map, сканирует markers всех пространств (глобальная уникальность room ID, как того требует §8.1), пустойsegment_map/vacuumудаляется канонически только если больше ничего не осталось.restoreMarkerRoomReferencesвосстанавливает только захваченные поля, не трогая более поздние правки прочих полей — юнит-тест явно это проверяет («unrelated later marker fields survive Undo»). В runtime_confirmRoomDelete/_commitMergeвызывают rewrite до геометрической мутации и сохраняютbefore.roomReferencesдля отмены;_recordGeometryделает симметричныйafter-снэпшот. Смокsmoke_writer_fixed_point:deleteClearsExactReferences,undoRestoresGeometryAndReferences,redoReappliesGeometryAndReferences,unrelatedMarkerFieldsSurvive— всеtrue. Две мутации (writer-room-delete-keeps-direct-reference,writer-room-delete-keeps-vacuum-reference) подтверждены. - AC7 (continuous transforms stay canonical).
align-grid.ts:kind === 'furniture' || kind === 'image'пропускается до какого-либо изменения (строка добавлена перед инкрементомtotal/moved, то есть транформ вообще не участвует в отчёте — соответствует ТЗ «не увеличиваетmoved,coordsCanonicalized... из-за их transform»). Юнит-тест: byte-equivalent furniture/image (включаяfuture_field, флаги отражения),report.total === 1(только ordinary decor учтён), обычныйrectпо-прежнему двигается. Мутацияwriter-align-snaps-free-decor-transformподтверждена. - AC8 (writer coverage manifest).
test/fixtures/477-writer-fixed-point-writers.json(27 строк, уникальные owners) + source-contract тест, который AST-парситhouseplan-editor-runtime.ts(черезtypescript), собирает все методы, вызывающие_commitPhysicalGeometry/_recordGeometry, и падает, если найденный owner отсутствует в манифесте — реальный «краснеет при добавлении нового writer» механизм, не декларативный список. - AC9 (perf). См. «Как проверялось» выше — прогнал сам, оба структурных и оба тайминговых критерия зелёные, включая seeded-scaling budget.
- AC10 (compatibility/docs).
model_version/схема не менялись (grep по diff — новых persisted полей нет;wallChainSeedIds/roomReferencesобъявлены вSpaceGeometryStateс пометкой «Session-only» и не попадают вServerConfig, что и требуется §12). i18n:toast.geometry_unsafe— уже существующий ключ (использовался в 9 др. местах runtime до этого диффа), новых строк не добавлено. USER-GUIDE.md/.ru.md, CANVAS.md, ARCHITECTURE.md, STATUS.md, TESTING.md обновлены синхронно и по существу совпадают с кодом (сверил формулировки построчно с реализацией, а не только с фактом правки файла).docs/specs/README.mdполучил строку на #477. - Трейлеры. Все 9 коммитов несут
Issue: #477;User-Visible: yesтолько на продуктовомa979699a, который включает оба changelog (docs/CHANGELOG.md,docs/CHANGELOG.ru.md) в одном коммите — соответствует требованию. - Одно число — один источник. Диф не добавляет и не меняет ни одной новой
пользовательски видимой величины (Optimize-отчёт — существующий механизм,
его числа как и раньше берутся из одного источника,
alignAllToGrid/optimizePlans; никакого нового дублирующего представления числа в UI не введено).test/single-source-numbers.test.mjsпрошёл в составеnpm test.
Чего не проверял и почему
npm run golden:verify— не прогонял (обоснование выше: код-чтение показывает, что изменённые пути lossless и не пересекаются с двумя Optimize-related golden-сценариями, которые используют синтетический_alignDialog, а не реальный расчёт). Если ревьюер/владелец сочтёт риск неприемлемым, это единственный неисполненный гейт из «по необходимости».python -m pytest tests_backend— не запускал,custom_components/**/*.pyне тронут диффом.- Полная матрица
demo/smoke_*.mjs(228 файлов) — не запускал; задача не задевает весь редактор (zigbee-топология, isometric, decor-палитра и т.п. вне скоупа и вне выборки инструмента). - Остальные ~46 «прямых»/«слабых» совпадений из вывода
smoke-select.mjs(smoke_room_autoclose,smoke_editor_tabs,smoke_free_walls,smoke_wall_junctions, decor/save-race/zigbee смоки и т.д.) — не запускал: совпадения там по широко переиспользуемым геттерам состояния (_curSpaceCfg,_path,_saveConfig), не по изменённому контракту; прогнанные 7 смоков покрывают все реально изменённые точки входа (wall-chain finish, terminal click, room acceptance/reconcile, room reference transaction). - Ручного тестирования в браузере вне смоков не проводил (в этом цикле его и не предполагается — «Ручного тестирования в цикле нет»).
Вердикт
Зелёный. Все AC доказаны исполняемыми свидетелями (unit + smoke + mutation + source-contract + benchmark), гейты прогнаны лично и по существу, документация и трейлеры в порядке, High/Medium-находок в скоупе нет.
Материал раунда
- Ветка:
issue/477-optimizer-fixed-point, коммитdfc4ce60033f— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
5ea87e1947f3f9ed13fefda60629a7aecc427bf8git log --all --format='%H %T' | grep 5ea87e1947f3 - ТЗ
docs/specs/477-editor-writer-fixed-point.md, блобa07a7248e008229e6e865ef793b213fdb524d5e4git log --all --find-object=a07a7248e008229e6e865ef793b213fdb524d5e4 -- docs/specs/477-editor-writer-fixed-point.md