21 KiB
CODE-REVIEW #32 · r1 — единое подтверждение опасных действий
- Issue: https://github.com/Matysh/houseplan-card/issues/32
- Этап: code (PROCESS.md §2.7)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Материал:
git log --oneline origin/dev..HEAD,git diff origin/dev...HEAD - ТЗ:
docs/specs/032-unified-danger-confirmation.md(spec-review зелёный на r2, SHA206bcd1a, документыdocs/reviews/SPEC-REVIEW-32-r{1,2}.md) - Это первый раунд код-ревью — предыдущего кода-раунда нет, разбор полный, разделы «Унаследовано из r0» и «Закрытие r0» не нужны.
Скоуп диффа
origin/dev...HEAD, 57 файлов, ключевые продуктовые:
src/danger-confirm.ts(новый) —HpConfirmController, replace-not-queue, token-based resolve.src/hp-confirm.ts(новый) — presentation поверхhp-dialog.src/houseplan-card.ts— controller-owner, host API_confirmDanger/_cancelDangerConfirm, cancel на смене space/mode/route/disconnect, unlock call site.src/houseplan-editor-runtime.ts,src/houseplan-onboarding-runtime.ts— миграция 7 из 8 call sites (draft whole/segment, marker, plan×2, space×2).src/i18n/{en,ru,de,fr}.json— новые title/body ключи.src/styles/dialogs.styles.ts— стили.danger-confirm-*.test/danger-confirmation.test.mjs(новый), правкиtest/render-device-snapshot.test.mjs,tsconfig.test.json.demo/smoke_danger_confirmation.mjs(новый), правкиdemo/smoke_lock_action.mjs,demo/smoke_lock_invariant.mjs,demo/smoke_saved_plans.mjs.- Документация:
docs/ARCHITECTURE.md,docs/USER-GUIDE.ru.md, оба CHANGELOG, сгенерированное дерево бандла (dist + custom_components + demo/srv).
Основа сверена читением: все 8 call sites §4.1 ТЗ найдены в диффе как
await this._confirmDanger(...) / await this.host._confirmDanger(...);
прямых вызовов confirm( в src/** не осталось (перепроверено rg и
собственным прогоном test/danger-confirmation.test.mjs).
Как проверялось
Зелёного Validate на SHA 7b363caa нет — все гейты ниже прогнаны мной лично
в рабочей копии на этом SHA.
Дешёвые гейты (прогнаны)
| Команда | Результат |
|---|---|
npx tsc --noEmit |
чисто, без вывода |
npm test |
tests 1652 · pass 1651 · fail 0 · skipped 1 |
npm run bundle:sync |
build ok; git status --porcelain после — пусто (три дерева синхронны) |
npm run bundle:budget |
initial View: 285355 B gzip (budget 300000 B, headroom 14645 B) — совпадает с заявкой автора (+1300 B к baseline 284055, ≤3 KiB по AC-13) |
node scripts/check-docs.mjs |
Documentation checks passed (7 files, 10 external links) |
node scripts/no-new-any.mjs --base origin/dev --head HEAD |
Новых any нет (379 добавленных строк в 6 файлах) |
npm run invariants не прогонял: дифф не трогает рёбра/толщину/layout/
marker.space/open_spans — только диалоговый слой и call sites,
геометрия не затронута.
npm run golden:verify не прогонял: новый диалог не входит в golden-матрицу,
и diff не показывает изменения существующего baseline (только служебные
скриншоты документации, см. ниже).
python -m pytest tests_backend -q не прогонял: custom_components/**/*.py
не тронут (только сгенерированное frontend/*).
Performance-профили не прогонял: не названы в AC, чувствительные к перфу пути не затронуты.
Browser smoke — выбор и результат
node scripts/smoke-select.mjs --base origin/dev --head HEAD: матрица 209
смоков, 39 «прямое совпадение», 81 «слабая связь» (остальное неопределённость).
Прямые совпадения в основном объясняются тем, что дифф трогает общие поля
хоста (_mode, _markerDialog, _spaceDialog, _physicalDialog,
_curSpaceCfg, _openingInfo, _commitSpace) в функциях-обёртках
(_setMode, _commitSpace, _editorSecondaryDialogBlocked), а не потому что
эти смоки проверяют подтверждение как таковое.
Прогнал (актуальный контракт диалога — cancel/accept/replace/focus/touch, уже прогнанные автором плюс независимая перепроверка):
smoke_danger_confirmation, smoke_esc_dialogs, smoke_dialog_footer_width,
smoke_toggle_confirmation, smoke_opening_binding, smoke_lock_action,
smoke_lock_invariant, smoke_saved_plans, smoke_warm_dialogs,
smoke_editor_tabs — все 10 зелёные (полный JSON output эквивалентен
заявке автора).
Дополнительно прогнал прямые совпадения на _physicalDialog/_openingInfo,
которых не было в списке автора, потому что ровно они называют мигрированные
call sites (_deleteDraftWhole/_deleteDraftSegment, _lockAction):
smoke_free_walls, smoke_registryless_opening, smoke_inert_openings,
smoke_open_passage, smoke_lazy_editor_chunk (AC-10).
Это вскрыло H1 (ниже): smoke_free_walls и smoke_registryless_opening
падают. Разбирая причину, прошёлся по всем демо-смокам, которые вообще
вызывают один из мигрированных методов
(grep -rl "_deleteMarker\|_deleteSpace()\|_deleteServerPlan\|_deleteDraftWhole\|_deleteDraftSegment\|_deletePhysicalSelection\|_lockAction(" demo/smoke_*.mjs
→ 11 файлов) и отдельно все, что стабили window.confirm
(grep -rl "window\.confirm\s*=" demo/smoke_*.mjs → 6 файлов), чтобы отделить
не задетые (lock-only, partition/column-kind, blocked-путь) от реально
сломанных. Результат — раздел «Находки» ниже.
Остальные 29 прямых совпадений и все 81 слабых не прогонял: по чтению кода
дифф трогает эти поля только через общий _editorSecondaryDialogBlocked/
_cancelDangerConfirm — они не меняют логику, которую эти смоки проверяют
(room dialog, merge/split, tap confirm, vac calibration, палитра мебели и
т.п.), а не потому что «выбор по теме» — их предмет прямо не пересекается ни
с одним из 8 мигрированных call sites (подтверждено grep выше: только
перечисленные 11 файлов вообще вызывают эти методы).
Находки
H1 — миграция ломает существующие смоки: 2 дают неверный вердикт, 3 виснут навсегда
Файлы: demo/smoke_free_walls.mjs, demo/smoke_registryless_opening.mjs,
demo/smoke_binding_picker.mjs, demo/smoke_hidden_flag.mjs,
demo/smoke_optional_space_model.mjs.
Задача заменяет синхронный confirm() на await this._confirmDanger(...),
который резолвится только реальным кликом по кнопке диалога. Дифф обновил под
это ровно 3 существующих смока (smoke_lock_action.mjs,
smoke_lock_invariant.mjs, smoke_saved_plans.mjs — видно в
git diff origin/dev...HEAD --stat), но в дереве есть ещё 5 смоков, которые
вызывают один из тех же восьми call sites и по-старому либо стабят
window.confirm = () => true без взаимодействия с диалогом, либо не ждут
промис вовсе. Воспроизведено исполнением, не догадкой:
$ node demo/smoke_free_walls.mjs
FAILED (2):
- deleteOnDraftRemovesWholeOutline: expected true, got false
- invalidThicknessCreatesNothing: expected true, got false
$ node demo/smoke_registryless_opening.mjs
FAILED (1):
- explicitInfoActionStillWorks: expected true, got false
$ timeout 25 node demo/smoke_binding_picker.mjs
Terminated # процесс не завершается сам, требуется kill
$ timeout 25 node demo/smoke_hidden_flag.mjs
Terminated
$ timeout 25 node demo/smoke_optional_space_model.mjs
Terminated
Причина видна в исходниках смоков:
smoke_free_walls.mjs:13ставитwindow.confirm = () => true;, затем синхронно вызываетc._deletePhysicalSelection()наdraft-выборе (строки 195–197) и тут же читает результат —_deletePhysicalSelectionдляkind: 'draft'делаетthis._deleteDraftWhole(); return;безawait(src/houseplan-editor-runtime.ts:3072), поэтому реальное удаление происходит позже, после клика по несуществующей кнопке — который никогда не случается.smoke_registryless_opening.mjs:89-91тем же способом стабитwindow.confirmи вызываетcard._lockAction(lockId, 'unlock')безawait, читаяserviceCalls.at(-1)сразу же.smoke_binding_picker.mjs:33-34,smoke_hidden_flag.mjs:83-84,96-97,smoke_optional_space_model.mjs:91-93ждут промис (await c._deleteMarker()/await card._deleteSpace()), но промис_confirmDangerрезолвится только явнымhp-confirm-decision, а стабwindow.confirmбольше никем не читается — сцена не диспатчит клик, дизконнект или замену запроса, поэтомуawaitвиснет бесконечно. Три файла проверял отдельно — во всех трёх процесс не завершается,finish()(demo/serve.mjs:35, выставляетprocess.exitCode = 1при провале) вообще не достигается. Это хуже явного провала: в CI такой смок не падает быстро, а занимает слот до тайм-аута раннера.
Не задеты (проверено, не потребовалось чинить): smoke_orphan_space_references.mjs
стабит window.confirm и ждёт _deleteSpace(), но сценарий останавливается на
blocker-проверке до _confirmDanger (deleteExplainsBlockerWithoutConfirmOrWrite),
поэтому не виснет — прогнан, зелёный. smoke_opening_binding.mjs и
smoke_partition_openings.mjs вызывают _lockAction(..., 'lock') (не 'unlock')
и _deletePhysicalSelection() на partition/column-выборе — ни один путь не
проходит через _confirmDanger по контракту §4.1/§7 ТЗ, поэтому оба зелёные
без правок.
Это не «сторонний тест устарел сам по себе» — регресс создан ровно теми
изменениями, которые в скоупе задачи (§4.1 ТЗ, миграция _deleteDraftWhole,
_deleteMarker, _deleteSpace, _lockAction). Три из восьми уже
адаптированных смоков показывают правильный образец правки — кликать по
hp-confirm .danger-confirm-footer .btn... после await c.updateComplete
(см. demo/smoke_lock_action.mjs, demo/smoke_lock_invariant.mjs,
demo/smoke_saved_plans.mjs), тот же приём применим к оставшимся 5 файлам.
До правки полный pre-release smoke-прогон (§8) зависнет на трёх из них.
Серьёзность: High. Блокирует; чинится в этой же задаче (§4.1 её собственный скоуп), issue не заводится.
L1 — шесть старых односложных ключей i18n остались без потребителей
confirm.delete_draft, confirm.delete_draft_segment, confirm.remove_marker,
confirm.delete_space, confirm.unlock, confirm.delete_plan присутствуют во
всех четырёх словарях, но после миграции ни один файл в src/**, test/**,
demo/** их не использует — проверено:
$ grep -rn "confirm\.delete_draft'" src/ test/ demo/ # и так для всех шести — пусто
ТЗ §8 разрешает удаление старых ключей «только если source/test search доказывает отсутствие потребителей» — этот поиск и есть доказательство. AC-11 (парность словарей) не нарушен: возврат чист, находка не о корректности, а об оставленном мёртвом коде.
Серьёзность: Low. Не блокирует; можно поправить в этой же правке (удалить шесть ключей из всех четырёх словарей) либо оставить с запиской — на усмотрение автора.
Что проверено и совпало с кодом
- AC-1/AC-2 — source-contract test (
test/danger-confirmation.test.mjs, тест «all dangerous-action call sites use the shared confirmation contract») считает 0 вызововconfirm(и ровно 8await this(.host)?._confirmDanger(в перечисленных трёх файлах, плюс проверяет все 6 диагностическихkey. Тест умеет падать:nativeCalls.length === 0иsharedCalls.length === 8оба жёстко зафиксированы, любое отклонение красит тест. - AC-3/AC-4/AC-5/AC-6/AC-7/AC-8/AC-9 —
demo/smoke_danger_confirmation.mjsпрогнан лично, зелёный; закрывает cancel-safety по всем 6 классам операций (включая оба lazy runtime для plan/space), replace-семантику, race (staleContextCannotMutate), фокус/трап/320px/локали. - AC-10 —
demo/smoke_lazy_editor_chunk.mjs(не тронут диффом, прогнан лично) остаётся зелёным:onboardingDoesNotLoadEditor: true— controller иhp-confirmдействительно в eager root, не тянут editor chunk. - AC-11 —
npm testвключаетtest/i18n.test.mjs, generic key-set parity (assert.deepEqual(Object.keys(dictionary).sort(), enKeys)) прошёл для всех четырёх словарей. - AC-12 — читением:
_roomDeleteDialog,_partitionDeleteDialog,confirm.erase_decor, backup/optimize диалоги не тронуты диффом (нет их в списке изменённых файлов/функций),_editorSecondaryDialogBlockedполучил добавление_dangerConfirmв общий OR-список блокировки secondary-панели — это тот же паттерн, что уже применён к_tapConfirm/_roomDialogи не меняет их собственную логику.smoke_editor_tabs,smoke_esc_dialogs(оба трогают_markerDialog/_roomDialog/_spaceDialog) зелёные. - AC-13 — см. таблицу гейтов выше, число совпадает с заявкой автора.
- Race-safety §7 — читением всех 7 переписанных call sites
(
_deleteDraftWhole,_deleteDraftSegment,_deleteMarker,_deleteServerPlan×2,_deleteSpace×2): каждый захватывает identity (spaceId/draftId/segment/targetId/plan.url+modified) доawait, затем перепроверяет её после — включая пересчётcollectSpaceMarkerDependenciesиdeletingLastSpaceзаново для_deleteSpace, ровно как требует §7 ТЗ. Дополнительно подтверждено исполнением:staleContextCannotMutateвsmoke_danger_confirmation.mjsменяет_spaceмежду открытием иresolveи проверяет, что геометрия не мутировала. - Unlock revalidation —
_lockAction(src/houseplan-card.ts:12766) перепроверяет_openingEntityAvailable, что_openingInfo— тот же proём/тип/lockid, и что состояние всё ещё'locked', иначе не вызываетhass.callService.test/render-device-snapshot.test.mjsтеперь проверяет тот же порядок статическим разбором продакшен-исходника. - Blocker-проверка раньше confirm (§4.1 таблица сценариев, «пространство»)
— в обеих реализациях
_deleteSpaceблокер-чек и ветка «последнее пространство» стоят строго до_confirmDanger, без изменений порядка относительно dev. - Bundle sync — сгенерированное дерево (
dist/**,custom_components/houseplan/frontend/**,demo/srv/assets/**) идентично тому, что производитnpm run bundle:syncна этом SHA (git statusпустой после локальной сборки). - Трейлеры — все 7 коммитов имеют
Issue: #32;User-Visible: yesстоит ровно у коммитаc91ac74e, и оба CHANGELOG изменены в этом же коммите (git show --stat c91ac74e). - Документация —
docs/ARCHITECTURE.md(новый абзац One modal contract),docs/USER-GUIDE.ru.md(терминология «Отмена», «Esc», диалог называет объект — согласовано с текущим текстом гайда, не изобретено).
Чего не проверял
- 29 из 39 «прямых» и все 81 «слабых» строк
smoke-select— обоснование в разделе «Как проверялось». npm run golden:verify,python -m pytest tests_backend, performance-профили,npm run invariants— обоснование там же (не затронуты диффом/AC).- Ручное браузерное тестирование вне headless-смоков (touch-жесты на реальном
устройстве, экранный ридер) — вне возможностей цикла; доверился
demo/smoke_danger_confirmation.mjsи existingsmoke_esc_dialogs/smoke_dialog_footer_widthкак прокси для focus trap/320px/screen-reader разметки (role/ariaнаследуются отhp-dialog, не проверял вручную скринридером). - Полный прогон всех 209 демо-смоков — не требуется на этапе ревью (PROCESS.md §8, это пред-релизный гейт), но именно поэтому H1 не является «пропустил бы CI» — это ревью и должно было его поймать до пред-релизного гейта, что и произошло.
Вывод
AC-1…AC-13 подтверждены по коду и/или исполнением, кроме побочного эффекта миграции на пять не входящих в §4.1 файлов демо-смоков (H1) — тот же класс регресса, что и в описанном риске ТЗ «синхронный caller продолжит работу до решения», только на testing-инфраструктуре, а не в продакшен-пути. Продуктовый код и текстовый контракт корректны и полностью соответствуют ТЗ; блокирует только H1.
Вердикт: жёлтый. Возврат автору для правки 5 смоков (образец есть в этом же диффе) и по желанию — снятия L1.