20 KiB
CODE-REVIEW-608-r1
- Issue: #608 «Числовые поля со слайдером (rangeLine) клампят каждый символ»
- Этап: code (PROCESS.md §2.7)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Материал:
8a4ecdea632e776e8b17df3ee1690a0beb8af8c3(рабочая копия уже на нём); один коммит,git diff origin/dev...HEAD.
Скоуп
Один коммит правит общий хелпер rangeLine/unitInput в
src/editors/form-kit.ts: числовое поле больше не коммитит на каждый input,
а копит сырую строку в DOM и атомарно применяется на change/blur —
кламп → привязка к шагу → нормализация floating point; незавершённый слайдер
побеждает над незавершённым текстом. Ни один из трёх диалогов-потребителей
(room-settings-dialog.ts, space-form.ts, marker-dialog.ts) не менялся —
фикс проходит через общий контракт и автоматически покрывает все 7 реальных
вызовов rangeLine(. Плюс: новый browser smoke
demo/smoke_range_line_draft.mjs, доработка smoke_dialog_config_parity.mjs
(хелпер input() теперь шлёт и change), мутант
range-line-clamps-every-keystroke, обновлены docs/TESTING.md, оба
changelog, docs/images/screenshots.json (только fingerprint), потолок
lazy-editor бандла и весь дублирующий bundle-tree (dist/**,
custom_components/houseplan/frontend/**).
Как проверялось
Материал не исполнялся вручную ранее (Validate на этом SHA прогнал только
typecheck/unit/build/docs; смоки на обычном push не входят в heavy —
.github/workflows/validate.yml включает их лишь для PR/кандидата/кнопки,
см. changes.heavy). Поэтому смоки, выбранные по диффу, прогнаны
самостоятельно, а не приняты на слово:
- Прочитан весь диф
src/editors/form-kit.ts:unitInput(новый необязательныйonChange,@change-биндинг черезnothing, когдаonChangeне передан),stepPrecision,committedRangeLineValue(экспортируемая чистая функция),rangeLine(onInput стал no-op, коммит перенесён вonChange). - Проверено, что все 7 вызовов
rangeLine((grep -n "rangeLine(" src/editors/*.ts) идут через один и тот же хелпер и ни один диалог-потребитель не тронут — AC4 «прежние min/max/step/unit и aria-контракт не изменены» доказывается тем, что эти файлы вообще не в диффе. - Проверено, что слайдер (
_rangeInputвhouseplan-editor-runtime.ts:10077) вызывает продуктовыйonInputна каждыйinput/change; поскольку во время набора текста продуктовое состояние не меняется (no-oponInput), Lit-диффинг у.value=${value}пропускает запись в DOM, пока состояние не меняется — ровно поэтомуlive()из первоначальной «Причины» в issue не понадобился: авторы выбрали не «принудительно перечитывать DOM», а «не трогать состояние, пока не подтверждено», что тоже закрывает риск §14. Как только слайдер меняет состояние, ре-рендер расходится с прежним закэшированным значением, и Lit перезаписывает DOM — это и даёт «слайдер побеждает». - Арифметика
committedRangeLineValueпроверена вручную для примеров ТЗ §6.7:120(шаг 5, on-step) → 120;123→ clamp 123 →(123-50)/5=14.6→Math.round=15 →50+75=125(округление половины вверх — не наш случай, ноMath.roundв JS всегда округляет.5к+∞, что и требует контракт);999→ clamp 300; пустая строка/NaN/step<=0→null→ восстановление прежнего значения.stepPrecisionпроверена для0.5→1,0.1→1,5→0 — совпадает с фактическими шагами всех 7 вызовов (0.1, 0.5, 1, 5). - Проверено, что расширение хелпера
input()вsmoke_dialog_config_parity.mjs(теперь шлётchangeвдобавок кinput) не задевает другие поля того же файла:#room-temp-min,#gs-glow-radius,#marker-glow-radius,#gs-north— это обычныеunitInputбезonChange(raw-string-паттерн_roomTempMin/glowRadius), лишнее событиеchangeдля них без слушателя и безвредно;#room-name-scale,#space-card-font,#marker-glow-brightness,#marker-angle— это как разrangeLine-поля, и именно для них новыйchangeобязателен, иначе коммит вообще не произойдёт. - Инварианты модели (
npm run invariants) не запускал: диф не касается комнат/рёбер/toilet записей толщины/layout/marker.space/open_spans— геометрия не тронута.
Гейты — что прогнано
| Гейт | Статус | Как |
|---|---|---|
npx tsc --noEmit, npm test, npm run build (со сверкой бандла) |
не перегонял | Уже зелёные на этом же SHA 8a4ecdea в CI Validate (ссылка в задании); код с тех пор не менялся |
npm run build (локально, для смоков) |
прогнал | зелёный, tsc --noEmit && rollup -c |
node scripts/bundle-sync.mjs + node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend |
прогнал | «verified 20 bundle assets» — совпадает |
node scripts/bundle-budget.mjs |
прогнал | lazy editor 243597 B gzip, потолок 244400 B — совпадает с числами автора; отдельное предупреждение про initial View headroom (10232 Б) — фоновый, не относится к этому диффу (не трогает initial View) |
node scripts/check-docs.mjs |
прогнал | «Documentation checks passed (7 files, 12 external links)» — фингерпринт docs/images/screenshots.json сошёлся, скриншоты не нужно перегонять |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
прогнал | см. ниже |
node demo/smoke_range_line_draft.mjs |
прогнал сам | все 15 проверок true, OK |
node demo/smoke_dialog_config_parity.mjs |
прогнал сам | все 8 проверок true, OK |
node scripts/mutation-gate.mjs --id=range-line-clamps-every-keystroke |
прогнал сам | «поймано 1 из 1»: чистый прогон зелёный, мутант красит smoke_range_line_draft.mjs |
npm run invariants |
не прогонял | диф не касается геометрии/толщины/layout — неприменимо |
npm run golden:verify |
не прогонял | визуальный результат не меняется (ТЗ §16, подтверждено чтением: правка не трогает геометрию/стили/слои) |
python -m pytest tests_backend -q |
не прогонял | custom_components/**/*.py не тронут (диф там — только синхронный frontend/** бандл) |
| performance-профили | не прогонял | не названы в AC, перф-чувствительные пути не тронуты (только DOM-события формы) |
node scripts/smoke-select.mjs --base origin/dev --head HEAD вывод:
Изменено файлов src/**: 1 · символов проекта на изменённых строках: 3
Матрица: 262 смоков · порог «широкого» символа: больше 52 смоков
Зарегистрированная связь (2):
demo/smoke_dialog_config_parity.mjs ← committedRangeLineValue
demo/smoke_range_line_draft.mjs ← committedRangeLineValue
Оба — «зарегистрированная связь», без «прямого совпадения» и без «НЕОПРЕДЕЛЁННОСТИ». Оба уже названы AC1–AC3 и оба прогнаны (см. таблицу выше); других смоков инструмент не предложил, полный прогон всех 262 не требуется — диф локален к одному общему хелперу с уже названными потребителями.
Находки
M1. Русский changelog: сломанное предложение (Medium, в скоупе — правится в этой же задаче)
Файл: docs/CHANGELOG.ru.md:11-15
- Числа рядом со слайдерами в настройках комнаты, пространства и устройства
теперь можно вводить посимвольно без скачков к границам: значение
применяется после выхода из поля,
ввода, привязывается к шагу шкалы и остаётся синхронным с ползунком
([#608](https://github.com/Matysh/houseplan-card/issues/608)).
Собранное предложение: «значение применяется после выхода из поля, ввода,
привязывается к шагу шкалы и остаётся синхронным с ползунком» — не
парсится: висящее «ввода,» без опоры (сравнение с git show origin/dev:docs/CHANGELOG.ru.md
подтверждает, что до этого коммита записи с такой структурой не было — это
не унаследованный дефект, а внесённый этим диффом). Английская версия той же
записи (docs/CHANGELOG.md:6-9) читается нормально: «the value is committed
on leaving the field, snaps to the slider step and stays synchronized with
the slider» — рассинхрон именно в русском переводе, похоже на артефакт
редактирования (вероятно, было «после выхода из поля или завершения ввода»,
и середину вырезали не полностью).
Почему Medium, а не Low: это не опечатка в переменной части (единица
измерения, число), а искажение смысла в обязательном release-артефакте
(§16 ТЗ прямо требует записи в docs/CHANGELOG.ru.md и docs/CHANGELOG.md в
том же коммите; трейлер User-Visible: yes подтверждает, что текст
пользовательский). Пользователь, читающий русскую версию changelog, не
поймёт, что изменилось.
Почему не блокирует High-циклом: правка сугубо текстовая, в скоупе задачи (файл входит в диф этой же задачи), без риска для функциональности или других AC. Per PROCESS.md — Medium в скоупе чинится в этой же задаче, блокирующий цикл не тратится, только жёлтый вердикт.
Как воспроизвести: открыть docs/CHANGELOG.ru.md на 8a4ecdea, строки
11–15.
Что проверено и корректно
- AC1 (room name scale, посимвольный ввод): подтверждено собственным
прогоном
smoke_range_line_draft.mjs—roomPartialTextStaysVisible,roomPartialDoesNotWriteDraft,roomPartialDoesNotMoveSlider,roomPartialDoesNotDirtyForm,roomCommitWrites120Once,roomCommitSynchronizesFieldAndSlider,emptyCommitRestoresConfirmedValue,emptyCommitDoesNotChangeDraft,emptyCommitKeepsDirtyState— всеtrue. - AC2 (space card font, off-step commit):
spacePartialDoesNotWriteDraft,spacePartialTextStaysVisible,offStepCommitSnapsTo125,offStepCommitSynchronizesFieldAndSlider— всеtrue;123→125подтверждено и по коду (см. арифметика в разделе «Как проверялось»), и по смоку. - AC3 (слайдер побеждает над незавершённым текстом; parity зелёный):
sliderWinsOverPartialText,sliderSynchronizesField—true;smoke_dialog_config_parity.mjsзелёный собственным прогоном (8/8). - AC4 (все семь вызовов через общий helper, контракт полей не менялся):
доказано чтением — ни один из трёх диалогов-потребителей не в диффе;
grep -n "rangeLine(" src/editors/*.tsдаёт ровно те же 7 вызовов, что и на этапе spec-review. - AC5 (мутант красит новый smoke): собственный прогон
mutation-gate.mjs --id=range-line-clamps-every-keystroke— «поймано 1 из 1». Проверено также, что мутация меняет именно строкуonInput: () => undefined,на прежний клампающий обработчик (find/replaceвscripts/mutation-registry.mjsдословно совпадают с реальной строкой кода). - AC6 (typecheck/unit/build зелёные, бандл синхронен): подтверждено
ссылкой на зелёный Validate этого же SHA плюс собственным
npm run buildиbundle-tree/bundle-budget(числа совпадают с комментарием автора: 243597 B / потолок 244400 B). - Контракт «пустой/невалидный commit не меняет draft» (§6.4) проверен и
по коду (
committedRangeLineValueвозвращаетnullдля пустой строки,NaN,step<=0), и по смоку. - Округление «на равном расстоянии — вверх» (§6.3):
Math.roundв JS всегда округляет.5к+∞; во всех 7 реальных диапазоновmin≥0, так что побочных эффектов у отрицательных чисел нет. - «Одно число — один источник»: число рангового поля вычисляется один раз
(
committedRangeLineValue) и одинаково уходит в поле (input.value =) и в продуктовый draft (onInput(next), откуда переиспользуется в.valueслайдера при следующем рендере) — раздвоения источника не внесено;test/single-source-numbers.test.mjsне касается этих полей (там только форматтеры длины/площади с единицами «м»/«см»/«м²»), и это ожидаемо не про rangeLine. - Расширение
smoke_dialog_config_parity.mjs(доп.changeв хелпереinput()) не задевает не-rangeLine поля того же файла — они не подписаны наonChange, лишнее событие без слушателя (детали в «Как проверялось», п.5). - Трейлеры коммита:
Issue: #608,User-Visible: yes— оба на месте; оба changelog обновлены в этом же коммите, как требует §16 приUser-Visible: yes. - Скоуп не расширен: диапазоны/шаги/единицы/расположение контролов не менялись; отдельная кнопка подтверждения и новые сообщения об ошибках не добавлены — «Не входит» (§5 ТЗ) не нарушено.
Чего не проверял и почему
- Полный набор
demo/smoke_*.mjs(262 файла) — не требуется: диф локален к одному общему хелперу,smoke-select.mjsназывает ровно два смока, «прямых» символов кроме них не всплыло; оба прогнаны лично. npm run golden:verify— визуальная геометрия/стили/слои не меняются (ТЗ §16 прямо это утверждает, подтверждено чтением диффа: изменения — только JS-обработчики событий формы).npm run invariants— диф не касается комнат/рёбер/записей толщины/layout/marker.space/open_spans.python -m pytest tests_backend -q—custom_components/**/*.pyне в диффе (только синхронныйfrontend/**бандл).- Performance-профили — не названы в AC, чувствительные к перфу пути не тронуты.
npx tsc --noEmit/npm testкак отдельные команды — не перегонял отдельно; полагаюсь на зелёный Validate этого SHA (ссылка в задании) плюс собственныйnpm run build, который уже включаетtsc --noEmit.
Вывод
Реализация верно закрывает контракт ТЗ: коммит атомарен, слайдер побеждает,
off-step значения привязываются к шагу, пустой/невалидный ввод откатывается,
все 7 вызовов идут через общий helper без правок диалогов. Автотесты умеют
падать (мутация подтверждена лично), гейты по объёму диффа адекватны задаче.
Единственная находка — сломанное предложение в docs/CHANGELOG.ru.md,
Medium, в скоупе, требует правки текста перед мержем; High-находок нет,
блокирующий цикл не расходуется.
Материал раунда
- Ветка:
issue/608-range-line-draft-input, коммит8a4ecdea632e— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
ad2bcb990bfb8c4143ea90fcdbf1af7515e2cbbdgit log --all --format='%H %T' | grep ad2bcb990bfb - Тело issue:
518c09d725e03b4ca93b0be8a8b6d61805af4e27b25679ddc71a5237c333055e - Вердикт конвейера:
yellow· High 0