mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
21 KiB
21 KiB
CODE-REVIEW-378-r1
- Issue: #378 — «Значение + состояние»: выбор источника значения, как у бейджа
- Этап: код-ревью (PROCESS.md §2.7)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4 до этого вердикта
- SHA материала:
552b78a134026f084593d5c76dcb55cba50088cd(git rev-parse HEADсверен непосредственно перед подведением итогов) - Диапазон:
origin/dev...HEAD=6e1d9364..552b78a1(a851c8f9ТЗ,ad9c6349ревью ТЗ r1 — зелёный,591f8f6aреализация,552b78a1docs screenshots после rebase) - Спецификация:
docs/specs/378-value-face-source.md, ревью ТЗ зелёное (docs/reviews/SPEC-REVIEW-378-r1.md) - Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 1 → в задаче
Скоуп
Диапазон полный (первый заход код-ревью, дельты предыдущего раунда нет — предыдущий вердикт того же issue относился к этапу ТЗ, не к коду). Проверены все 10 AC ТЗ:
src/device-value-badge.ts— общийresolveValueSource/formatter/failure-код,valueSourceWriteFields.src/device-presentation.ts— explicit-веткаresolveValue(), приоритет virtual,sourceSignature,fallbackReason/valueFullText.src/houseplan-editor-runtime.ts— селектор «Источник значения» в диалоге, draft/preview, reset на смену binding, rebind marker id.src/devices.ts,src/houseplan-card.ts,src/types.ts— поле конфига,rewriteMarkerControlReferences.custom_components/houseplan/validation.py— общаяvalidate_source(), lossless-схема, delta-validation, marker-ref target/is_light.custom_components/houseplan/import_export.py— full/space export/import, drop/remap/virtualize,_transfer_dropped_marker_linksmaximum.scripts/model-invariants.mjs,scripts/smoke-links.mjs— новый инвариант ссылки и регистрация смока.demo/smoke_value_face_source.mjs— целевой browser smoke.- Тесты:
test/device-presentation.test.mjs,test/device-presentation-policy.test.mjs,test/devices.test.mjs,test/native-select-contract.test.mjs,test/fixtures/device-presentation-decisions.mjs,tests_backend/test_validation.py,tests_backend/test_ha_import_export.py. - Документация:
docs/ARCHITECTURE.md,docs/CONFIG-COMPATIBILITY.md,docs/DEVICE-PRESENTATION.md,docs/USER-GUIDE.md/.ru.md,docs/CHANGELOG.md/.ru.md, i18n en/ru/de/fr,docs/specs/README.md.
Как проверялось
Дешёвые гейты уже подтверждены зелёным Validate на этом же SHA 552b78a1
(https://github.com/Matysh/houseplan-card/actions/runs/33270791009) —
npx tsc --noEmit, npm test (1574 passed / 1 skipped, включая
model-invariants.test.mjs на всех моделях проекта), npm run build,
backend pytest (143 passed), no-new-any, bundle budget/sync не
перегонялись повторно ревьюером.
| Гейт | Статус | Как |
|---|---|---|
npx tsc --noEmit, npm test, npm run build, backend pytest, no-new-any |
не гонял повторно | зелёный Validate на точном SHA 552b78a1 (ссылка выше); код с тех пор не менялся |
node scripts/check-docs.mjs |
прогнал | Documentation checks passed (7 files, 10 external links) |
node scripts/bundle-sync.mjs + сверка трёх копий бандла |
прогнал | git status --short пуст после синка — dist/, custom_components/.../frontend и рабочая копия совпадают побайтово |
node scripts/bundle-budget.mjs |
прогнал | 277948 B initial View при лимите 300000 B (что коррелирует с заявленными автором 277599 B; малая разница — среда/oxipng, бюджет не нарушен) |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
прогнал | прямое совпадение: smoke_cover_tap.mjs, smoke_value_face_source.mjs; зарегистрированная связь: smoke_cold_view_toggle.mjs; 31 слабая связь по общему имени _markerDialog — не прогонялись, см. «Чего не проверял» |
demo/smoke_value_face_source.mjs |
прогнал, зелёный, проверил, что умеет падать | мутация text: text ?? '—' → 'RAW' в device-value-badge.ts дала 5 честных провалов (preview42, plan42, static42, unavailableDash, recovered55); откат мутации восстановил зелёный прогон и чистое дерево |
demo/smoke_cover_tap.mjs (прямое совпадение) |
прогнал | все ключи true |
demo/smoke_cold_view_toggle.mjs (зарегистрированная связь: resolvedLightSources) |
прогнал | все ключи true |
demo/smoke_device_preview_parity.mjs (слабая связь, но прямо про preview, который правит диф) |
прогнал | все ключи true |
npm run golden:verify (диф трогает рендер лица значения) |
прогнал | 78/78 сценариев passed, включая device-value-badge-positions-dark и все device-dialog-* |
npm run invariants -- --config <…> на конкретном конфиге |
не гонял отдельно | checkReferences() для marker.value_source уже покрыт model-invariants.test.mjs, часть зелёного npm test на всех моделях проекта |
python -m pytest tests_backend -q |
не гонял повторно | входит в зелёный Validate; изменения только в validation.py/import_export.py, оба покрыты новыми тестами, прочитанными построчно |
Находки
Medium — AC2 не доказан golden-сценарием, как того явно требует принятое ТЗ
- Файл:
docs/specs/378-value-face-source.md(AC2, «План автотестов», «Release-артефакты») vs фактический диапазонdemo/golden/** - Summary: ТЗ трижды явно требует golden-доказательство для видимого
результата явного источника (
cover.current_position = 42→42 %): в AC2 («unit + smoke + golden»), в плане автотестов («Golden-сценарий с cover 42 % и выбранным source принимается только через штатный reviewed Linux artifact») и в разделе Release-артефактов. Ни один файлdemo/golden/**не тронут:git diff origin/dev...HEAD --stat -- demo/goldenпуст, вdemo/golden/matrix.mjsнет ни одного маркера сvalue_source. Автор сам зафиксировал это в handoff-комментарии: «Локальный golden на Windows дал неканонические renderer-различия, baseline не менялся и не принимался», но сценарий не был добавлен вовсе (даже без принятия эталона) — сравнивать боту было нечего, а не только «нечего принять». - Failure scenario: будущий рефакторинг рендера
.valtext/.valonly(шрифт, обрезка, цвет, позиционирование внутри капсулы) сможет сломать именно новый визуальный контракт «явный источник →42 %вместо иконки» и пройти незамеченным:npm run golden:verifyв этом PR прогнал 78 сценариев зелёным, но ни один из них не рендерит маркер сvalue_source, поэтому регресс такого рода этот гейт органически не ловит. Browser-smoke (demo/smoke_value_face_source.mjs) проверяет толькоtextContent('42 %'как строку), а не визуальную раскладку — разного рода поломки вёрстки капсулы через него не видны. - Что делает находку Medium, а не High: функциональная корректность самого значения (форматирование, dash, восстановление, паритет рендереров, rebind, import/export) доказана unit+backend+smoke кодом, который умеет падать (см. таблицу выше) — рабочего дефекта в текущем поведении нет. Пробел — только в будущей защите от визуальной регрессии, для которой ТЗ явно назвало метод доказательства, а имплементация его не предоставила. Находка в скоупе задачи (тот же файл ТЗ, тот же issue), поэтому по правилу #202 отдельный issue не заводится: правится в этой же ветке.
- Что нужно: добавить сценарий в
demo/golden/matrix.mjs(маркер cover сvalue_source: {kind: 'entity_attribute', entity_id: …, attribute: 'current_position'}, ожидаемое42 %) и провести штатный Linux-цикл capture/accept черезnpm run golden:accept -- --reviewedна CI — именно так же, как в этом PR уже был принятdocs screenshotsартефакт (https://github.com/Matysh/houseplan-card/actions/runs/33270679280). Автор физически не может принять корректный baseline с Windows — это не повод пропустить шаг, а повод завести его через CI, как и было сделано для скриншотов документации в этой же ветке.
Low — index-таблица docs/specs/README.md нарушает сортировку по номеру issue
- Файл:
docs/specs/README.md:113 - Summary: новая строка
#378вставлена между#90и#94, хотя вся остальная таблица строго отсортирована по возрастанию номера issue (…, #90, #94, #101, #107, #113, …). Чисто косметическая непоследовательность, функционально ни на что не влияет (ссылка и путь верны). - Решение ревьюера: снимается без правки — не блокирует и не входит в условия DoD; при следующей правке этого файла можно переставить строку.
Что проверено и корректно
- AC1 (список/сохранение).
valueBadgeCandidates()используется как единственный источник кандидатов и для value badge, и для нового селектора;valueSourceWriteFields()— тот же паттернtouched/originalHas/original, что и уvalueBadgeWriteFields()(auto = отсутствие поля). Доказано unit (test/device-presentation.test.mjs:920+«persistence keeps untouched data») и smoke (candidatePresent,draftSourceExact,savedExact,reopenedExact,cancelKeptSource— всеtrue, прогнано лично). - AC2 (результат на всех рендерерах).
resolveDevicePresentation()— единственная функция, которую вызывают preview, полный план иhouseplan-space-card;sourceKey/text/fullTextформируются один раз вresolveValueSource(). Smoke подтвердил побитовое совпадениеpreview42,plan42,static42,actionUnchanged(лично прогнано, зелёное). Golden proof отсутствует — см. находку Medium выше. - AC3 (паритет formatter).
resolveValueSource()— общая функция для badge и face; unit-тест «explicit cover position uses the exact value-badge formatter» сравниваетresult.valueBadge.text === result.valueTextи сверяет источник черезvalueBadgeCandidates()за тем же ключом. - AC4 (legacy compatibility). Явный источник читается только если
d.marker?.value_sourcetruthy; auto-ветка (resolveValue()безexplicitSource) байт-в-байт совпадает с прежним кодом путём climate/temp/ hum/ambiguous/no-state. Тест «explicit unavailable source… without auto fallback» отдельно прогоняетlegacy-маркер без поля и получаетvalueText: null(не dash) — подтверждает, что явная и auto ветки не смешиваются. F09–F12 фикстурыdevice-presentation-policy.test.mjsне тронуты (только добавлен F18),npm testзелёный на этом SHA. - AC5 (fail explicit).
resolveValueSource():failureвыставляется ровно тогда, когда локальная переменнаяtextосталасьnull; финальная сборкаtext: text ?? '—',fullText: text ?? unavailableTextгарантирует их взаимоисключение — dash и диагностический код всегда идут вместе, а не вместо друг друга. Убедился мутацией (см. таблицу гейтов): без dash-веточки тест валится по пяти полям.fallbackReasonбольше не гасится наличием dash-текста (убрано условие!valueText) — специально для explicit-ветки, где текст'—'неnull, но диагностика должна остаться видимой; для auto-ветки это изменение поведенчески нейтрально, тамtextиfallbackвсегда взаимоисключающи и без этого условия. - AC6 (независимость).
value_virtual-проверка (if (d.virtual) return) стоит раньше чтенияvalue_source— приоритет сохранён кодом, а не только комментарием.lqiTextподавляется и приvalueBadge?.isLqi, и теперь приvalue.source?.kind === 'derived_lqi'— покрыто unit-тестом «derived sources share the plan graph and suppress duplicate LQI». Duplicate-hint в диалоге теперь сравниваетbadgeSourceKey === innerValueSourceKey, гдеinnerValueSourceKeyберётся из единогоpreviewPresentation.valueSource.sourceKeyвместо ручной реконструкции — то же наблюдаемое поведение, один источник строки. Touch/pointer путь не тронут структурно (_clickDeviceне менялся, только текст.valtext) — проверено чтением, не исполнением отдельного touch-смока; логика тапа общая для обоих устройств ввода. - AC7 (конфиг и ссылки). Backend:
validate_source()— общая функция дляvalue_badge.sourceиvalue_source, коды ошибок отличаются префиксом, проверено построчно; delta-validation честно различает changed/unchanged через_matching_previous_marker(rename-tolerant, тест «marker reference and id rename are delta-safe»). Import/export:_drop_invalid_import_marker_links,_repair_target_space_refs,build_space_merge,create_export— все четыре точки reference seam обновлены параллельно уже существующим дляvalue_badge; счётчикMAX_MARKERS * (MAX_CONTROLS + 2)корректно увеличен на 1 (было+1для одного возможного badge-дропа на маркер, теперь+2для badge и source). Frontend rebind:rewriteMarkerControlReferences()и_saveMarker()id-rename оба переписываютvalue_source.ref, тестdevices.test.mjsэто подтверждает. - AC8 (preview/Cancel/binding). Смена binding (оба места: virtual-radio и
выбор HA-сущности) добавляет
valueSource: null, valueSourceTouched: true— единственная точка сброса, найдена и прочитана в обоих местах. SmokebindingResetToAutoиcancelKeptSourceподтверждают оба направления (сброс на смену binding, сохранение при незасейвленной смене source). - AC9 (локализация/документация). i18n: 5 новых ключей + обновление
marker.display_hint_valueприсутствуют идентично в en/ru/de/fr (сверено построчно диффом). Документация:ARCHITECTURE.md,CONFIG-COMPATIBILITY.md,DEVICE-PRESENTATION.md(новая строка F18 с корректной ссылкой наdevice-presentation-policy-value/presentation-row-contract, обе строки существуют как реальные тестовые id),USER-GUIDE.md/.ru.md, оба CHANGELOG — в одном коммите с поведением (591f8f6a), трейлерUser-Visible: yesна месте. Docs screenshots пересчитаны после rebase на точном SHA552b78a1отдельным commit-only-docs коммитом сUser-Visible: no— процессуально корректно. - AC10 (гейты и бюджет). См. таблицу гейтов выше — все обязательные зелёные (частично по ссылке на Validate этого SHA, частично лично прогнаны).
- Трейлеры и процесс.
Issue: #378на каждом коммите класса A/B/C,User-Visibleрасставлен верно, CHANGELOG в том же коммите, что поведение, branchissue/378-value-face-source, rebase-конфликт (только сгенерированный бандл) разрешён и пересобран без orphan chunks — подтверждено побайтовой сверкойbundle-syncвыше.
Чего не проверял
- Не прогонял 31 слабую связь
smoke-selectс общим именем_markerDialog(полный список — в выводе инструмента выше): диалог устройства используют почти все смоки этого файла не по существу дифа, специфичной дляvalue_sourceлогики в них нет. Решение — не прогонять, риск низкий. - Не гонял
npx tsc --noEmit,npm test,npm run build,python -m pytest tests_backend -qповторно — зелёный Validate на точном SHA552b78a1уже это доказал, код с тех пор не менялся. - Не выполнял ручное тестирование в браузере (вне процесса, п. «оно вообще работает» закрыт код-ревью + автотестами + собственноручно прогнанными smoke/golden выше).
- Не проверял visual regression на конкретном golden-сценарии с явным
value_source— он не существует (сама находка Medium). - Полный performance-профиль не гонял: AC/риски не называют влияние на перф, а изменение — O(1) поиск по уже кэшированному графу; принято по чтению кода.