23 KiB
CODE-REVIEW-614-r1
Issue: #614 — «Диалоги настроек: единая политика пустого/невалидного поля,
тосты _saveMarker → ошибки под полем, снимок baseline забывается до валидации».
Материал: bc6b243f56362ba89f157b891955b71a004d8e81 (two commits on
issue/614-dialog-validation-baseline: 1a6b12af — основная реализация,
User-Visible: yes, оба changelog в том же коммите; bc6b243f — точечный
тип-фикс no-new-any, User-Visible: no).
Заход: r1 — первый фактический код-ревью этой задачи. Предыдущая попытка
поставить S7-code-review на 1a6b12af не дошла до модели: гейт-Validate с
мутантами упал до ревью (см. комментарий от 2026-09-23T12:43:43Z), цикл не
израсходован. Разделов «Закрытие раунда r0» и «Унаследовано из r0» поэтому
нет — это не сокращённый повторный проход, это первый разбор всего диффа.
Трек: полный (аналитика: сложность 6, риск 6, P2). Спецревью прошло зелёным
2026-09-23T12:02:30Z, документ docs/reviews/SPEC-REVIEW-614-r1.md, Q1/Q2
закрыты владельцем как Default/Default.
Скоуп проверки
Полный разбор диффа origin/dev...HEAD (86 файлов, класс A: 13 файлов
src/**; класс B: 4 смока, scripts/bundle-budget.mjs,
scripts/mutation-registry.mjs, tsconfig.test.json, 2 unit-теста; класс C:
оба changelog, USER-GUIDE.ru.md; класс D: dist/** и зеркала — не
проверяю содержательно, только что сборка сошлась).
Читал построчно: dialog-baseline.ts, general-form-state.ts,
general-settings-dialog.ts, space-form-state.ts, space-form.ts,
marker-form-state.ts, marker-dialog.ts, houseplan-editor-runtime.ts
(_saveMarker, _saveSpaceDialog, _saveSettingsDialog,
_openSpaceDialog, _openSettingsDialog), houseplan-card.ts
(_warmDialogState, revive-switch, _settingsDialog type), card-runtime.ts
(WarmDialog), houseplan-onboarding-runtime.ts, grid-scale.ts,
space-dialog.ts (strictNumber, тип SpaceDialogState), i18n
{en,ru,de,fr}.json × 2 файла, оба changelog, USER-GUIDE.ru.md,
scripts/mutation-registry.mjs (шесть новых мутантов #614),
scripts/bundle-budget.mjs (два новых потолка).
Как проверялось
Дешёвые гейты уже подтверждены на этом SHA: Validate
run 35863114963
завершился success (typecheck, test, build+сверка бандла,
no-new-any). Не перегонял их заново по правилу «один зелёный прогон на SHA
не повторяется» — вместо этого потратил время ревью на код и на то, что
Validate не гарантирует.
Сам прогнал:
node scripts/smoke-select.mjs --base origin/dev --head HEAD— 27 прямых совпадений из 263, все по общеупотребимым символам (_showToast,_mode,cellCm,_settings,_saveMarker,_settingsDialog); ниже порог «широкого» символа (52) не превышен ни по одному имени. Из прямых совпадений отобрал по существу задачи четыре смока, которые сам диф и переписал под AC1–AC4 — остальные совпадения либо про соседний слой (smoke_grid_scale_invariance,smoke_radar_*,smoke_partition_openingsи т.п. зацеплены только заcellCm/_showToast— общие символы, не контракт этой задачи), либо про warm-механику вне четырёх baseline-aware форм. Слабые совпадения (53) не разбирал построчно — все по одному общему имени, разрешение инструмента прямо говорит, что это не обязанность прогонять.npm run bundle:sync(пересобрал —demo/srv/assetsне закоммичен, #255) →node demo/smoke_general_settings_form.mjs,smoke_space_settings_form.mjs,smoke_device_settings_form.mjs,smoke_warm_dialogs.mjs— все четыре: OK, все ключи-ассерты (включая новые:invalidGlowKeepsRawAndBlocksOldValue,northOutOfRangeBlocksSave,invalidScaleKeepsRawAndBlocksOldValue,twoInvalidTempsKeepRawAndCountSeparately,virtualNameIsAnInlineProblem,runWithoutTargetIsInline,badgeWithoutSourceIsInline,saveAttemptDoesNotForgetBaseline,eSettingsDirtyAfter,fRoomDirtyAfterи т.д.) —true.
npm test— 2838 тестов, 2837 pass, 1 skipped (не связан с #614), 0 fail.npm run bundle:budget— оба новых потолка (INITIAL_VIEW_GZIP_CEILING292000,LAZY_EDITOR_GZIP_CEILING245400) сходятся с фактической сборкой (291016 Б / 244340 Б), заявленные в комментариях замеры (~291041/~244356) им не противоречат. Предупреждение про запас < 15000 Б — существующий долг (#367/#474), не новый и не по вине этой задачи.node scripts/check-docs.mjs— красный по отпечатку скриншотов; ожидаемо для любой правкиsrc/**(см. ниже, это не гейт ревью).
Не гонял: npx tsc --noEmit / отдельный npm run build (уже green на этом
SHA), golden:verify, pytest tests_backend, geometry/invariants
performance — обоснование каждого пропуска в разделе «Чего не проверял».
Находки
M1 (Medium, в скоупе) — AC1/AC2/AC3 требуют unit-тест, а он не написан
ТЗ (принятое зелёным спецревью) для каждого из AC1–AC4 в явном виде называет двойное доказательство: «unit + browser smoke». Браузерные смоки дописаны честно и содержательно (см. прогон выше). Юнит-тестов на саму логику черновика — ни одного:
test/dialog-baseline.test.mjs(новый) тестирует только перенос baseline между хостами (dialogBaseline/restoreDialogBaseline/stableKey) — это закрывает юнит-часть AC4.test/space-dialog.test.mjsполучил один новый тест наstrictNumber— но это существовавшая до задачи чистая функция, не новая логика черновика.generalProblems(general-form-state.ts:35), новая веткаspaceDialogProblemsдляcellCm/tempMin/tempMax(space-form-state.ts:279), иmarkerProblemsс тремя новыми полями (marker-form-state.ts:47) — ни одна не импортирована ни однимtest/*.mjs:grep -rl "generalProblems\|markerProblems\|spaceDialogProblems" test/*.mjsничего не находит.- Причина технически проста и указывает на пропуск, а не на решение:
tsconfig.test.jsonкомпилируетdialog-baseline.tsдля тестов (строка добавлена этим же диффом), но неgeneral-form-state.ts,space-form-state.tsиmarker-form-state.ts— юнит-тест для них физически не может быть собран без такого же добавления, которое для этих трёх файлов не сделано.
Это именно тот код, ради которого заведена задача (M3/M4 из аудита): контракт
«сырой текст не подменяется старым числом» и три инлайн-ошибки устройства.
Он проверен интеграционно (браузер, реальный DOM) и мутационно
(scripts/mutation-registry.mjs добавляет 6 новых мутантов на этот самый
код с смоками как guard), но не на уровне чистой функции, как того явно
требует принятое ТЗ. Мутационный guard ловит конкретную регрессию, которую
описал автор мутанта, а не проверяет функцию по всей области определения
(границы 0/359, -0, запятая как разделитель, пробелы, NaN-подобный текст,
min === max и т.п. — часть этих случаев в смоках не встречается вовсе).
Воспроизведение: grep -rl "generalProblems\|markerProblems\|spaceDialogProblems" test/*.mjs
— пусто. grep -n "general-form-state\|space-form-state\|marker-form-state" tsconfig.test.json
— пусто.
Почему Medium, не High: поведение реально работает — смоки и мутационные guard'ы это подтверждают построчно и я перепроверил их сам (все зелёные на этом SHA). Ничего не сломано и не потеряно для пользователя. Но принятое ТЗ называет юнит-тест частью критерия приёмки трёх из четырёх AC, и его объективно нет — это дыра в контракте задачи, не гипотетический риск.
Что нужно: добавить general-form-state.ts, space-form-state.ts,
marker-form-state.ts в tsconfig.test.json (по образцу уже добавленного
dialog-baseline.ts) и юнит-тесты на generalProblems, spaceDialogProblems
(ветки scale/temp/north) и markerProblems (три новых поля), минимум
по границам контракта: пусто/пробелы/запятая/мусор/выход за диапазон/валидное
значение.
Находка в скоупе задачи (оба файла-кандидата уже изменены этим диффом, тестовая инфраструктура для них уже начата этим же диффом для соседнего модуля) → жёлтый вердикт, доработка в этой же задаче, отдельный issue не заводится (owner's decision #202).
Что проверено и корректно
- AC1 (общие настройки).
generalProblemsчитаетglowRadiusInput/northDegInputнапрямую (не «последнее валидное число»); typed-значение обновляется только при успешном разборе (general-settings-dialog.ts:99-106,112-131).TRANSIENTвgeneral-form-state.ts:11исключает оба raw-поля из dirty-ключа — другой текст того же числа не создаёт ложного изменения, а само по себе изменение raw без изменения typed не разблокирует Save в обход неизменившегося dirty-ключа (согласуется с контрактом «Сырые поля исключаются из semantic dirty-key»). Reset-to-defaults синхронно обновляет оба представления (general-settings-dialog.ts:136-142). СмокinvalidGlowKeepsRawAndBlocksOldValue/northOutOfRangeBlocksSaveподтверждают на реальном DOM. - AC2 (пространство).
spaceDialogProblemsпересчитываетcellCmстрого изcellCmInputс учётом imperial-конверсии черезgridCellFieldToCm, границы вынесены в единый источникGRID_CELL_CM_MIN/MAX(grid-scale.ts:9-10), на который теперь ссылаются иspace-form.ts(CELL_CM_MIN/MAXреэкспорт), иspaceDialogProblems— один источник для одного числа.tempMin/tempMaxпроверяются раздельно, ошибка диапазона привязана к max, как того требует ТЗ (space-form-state.ts:298-306). North-инпут блокирует Save только пока выбран custom-режим; переключение на «как в общих» одним действием сбрасывает иnorthDeg, иnorthDegInput(space-form.ts:429-432,453-454)._saveSpaceDialog/_saveSpaceDialog(обычный редактор и онбординг) перепроверяютspaceDialogProblemsперед записью — невалидный raw физически не может попасть в конфиг (houseplan-editor-runtime.ts:8472,houseplan-onboarding-runtime.ts:695). Проверил построчно, что итоговая запись вsp.settings/sp.cell_cm(houseplan-editor-runtime.ts:8541-8574) берёт только typed-поля (d.northDeg,d.tempMin,d.tempMax,d.cellCm) — ни один*Inputв конфиг не попадает (AC5). - AC3 (устройство). Три предусловия
_saveMarker(virtual_name,run_target,value_badge_source) продублированы вmarkerProblems(marker-form-state.ts:231-242) в порядке полей формы, привязаны кerror/aria-invalidпод соответствующим полем (marker-dialog.ts:171-198). Старые тостыtoast.virtual_name_required/toast.run_target_required/toast.value_badge_source_requiredудалены из всех четырёх локалей без расхождения (en/ru/de/fr идентичны),_saveMarkerсохранил раннийreturnбез тоста — это внутренняя защита от программного вызова, как и требует ТЗ, а не основной UX.forgetMarkerBaselineперенесён из обработчика клика внутрь_saveMarkerи вызывается только послеconfigAccepted/успешных побочных эффектов, непосредственно перед_closeMarkerDialog()(houseplan-editor-runtime.ts:8125-8126) — ранний выход или исключение вcatch(строки 8127+) его не трогает, baseline переживает отказ записи.requestCloseвmarker-dialog.ts:172,181тоже явно забывает baseline только при закрытии без изменений или при подтверждённом discard — ровно условие «Cancel без confirm при отсутствии изменений» из ТЗ. - AC4 (warm revive).
WarmDialog.baselineпереносится для всех четырёх baseline-aware форм одинаково — через общий хелперat()в_warmDialogState()(houseplan-card.ts:3579-3608), который вызываетwarmDialogBaseline(this, kind)независимо от вида диалога. Восстановление (houseplan-card.ts:3709,restoreWarmDialogBaseline) происходит синхронно сразу после того, как switch восстановил сам черновик диалога и доrequestUpdate()— оба объекта (draft, baseline) оказываются на новом хосте в одном синхронном участке кода, что и требует ТЗ («атомарно до вычисления dirty-state»).warmBaselineKindограничивает перенос ровно четырьмя видами (space/marker/settings/room), остальные виды warm-диалогов (opening,decorTextи т.д.) не получают baseline-семантики — согласуется с «для остальных видов warm-диалогов поведение не меняется». Прогналsmoke_warm_dialogs.mjsцеликом (включая новые блоки E/F проsettingsиroom) — все ассертыtrue. - AC5 (совместимость). Ни один
*Input-ключ (glowRadiusInput,northDegInput,cellCmInput,tempMinInput,tempMaxInput) не сериализуется вServerConfig; baseline живёт в модульномWeakMap(dialog-baseline.ts:14), не вlocalStorage— grep поlocalStorageв затронутых файлах не находит ничего нового рядом с warm-механикой. Формат валидного сохранения (sp.settings.north_deg,temp_min/temp_max,cell_cm,marker.glow_radius_cm) не изменился относительноdev. - i18n. Пять новых ключей (
space.error_scale,space.error_temp_value,marker.error_virtual_name,marker.error_run_target,marker.error_value_badge_source) добавлены во все четыре локали без расхождений; три отмерших тоста синхронно удалены из всех четырёх. Тексты EN/RU человекочитаемы, DE/FR — паритетны по смыслу. - Трейлеры/changelog.
1a6b12af—Issue: #614,User-Visible: yes, оба changelog в этом же коммите, формулировки соответствуютUSER-GUIDE.ru.md(термин «Проверить N полей» использован как есть, не изобретён заново).bc6b243f— точечный тип-фикс,User-Visible: no, changelog не тронут — верно, изменение не поведенческое. docs/USER-GUIDE.ru.md. Формулировки про пустое поле-как-наследование сужены до «только там, где это прямо написано в подсказке» и добавлен абзац про warm revive — оба соответствуют принятому ТЗ, ничего не выдумано.
Чего не проверял и почему
npx tsc --noEmit/ отдельныйnpm run buildвнеbundle:sync— уже green на этом SHA (run 35863114963);bundle:sync, который я запустил ради смоков, транзитивно прогнал иtsc --noEmit, иrollup -cбез ошибок — повторное подтверждение получено бесплатно, отдельно не считаю.golden:verify— diff не трогает канвас/геометрию/слои; открытые диалоги настроек не входят ни в один сценарийdemo/golden/*.mjs(demo/golden/harness.mjsтрогает_markerDialogтолько для установкиdisplay: 'icon_ripple'в несвязанном сценарии, не для сравнения формы). Видимый результат, который меняет эта задача, — содержимое диалогов, не снимки плана.python -m pytest tests_backend— diff не касается ни одногоcustom_components/**/*.py.npm run invariants/ geometry parity — diff не трогает рёбра комнат, толщину стен,layoutилиopen_spans; единственная геометрическая величина в диффе (cellCm/CELL_CM_MIN/MAX) — это масштаб отображения формы, а не геометрия модели, и её пределы не изменились (0.1…1000, перенесены в общий источник без изменения значений).- Полный
smoke-набор (263 файла) — не запускал по решению изsmoke-select: ни одно совпадение не перешагнуло порог «широкого» символа, совпадения — по общеупотребимым именам, а не по контракту задачи. Это предрелизная обязанность, не обязанность этого ревью. check-docs --screenshots— красный ожидаемо (любая правкаsrc/**делает отпечаток скриншотов устаревшим); это ошибка только на релизном кандидате, не гейт ревью.- Performance-профили — не названы в AC, diff не касается путей, к которым они привязаны.
Итог
Единственная находка — Medium, в скоупе задачи (пропущенный юнит-уровень доказательства для AC1–AC3, явно требуемого принятым ТЗ). Функциональность работает: перепроверил лично все заявленные смоки и мутационные guard'ы на этом SHA, юнит-сьют зелёный (2837/2838, 1 skip не по теме), бандл-бюджет сходится. High-находок нет.
Материал раунда
- Ветка:
issue/614-dialog-validation-baseline, коммитbc6b243f5636— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
a808be3f921a36d3943cc93fbc1d8a70ce3c79afgit log --all --format='%H %T' | grep a808be3f921a - Тело issue:
3b8a95fd43a037cc6c1f3207c6f5b242ef566b28bde385f5e12b4a225d938221 - Вердикт конвейера:
yellow· High 0