docs: review document for #385

Issue: #385
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-30 08:24:40 +00:00
parent 223f499e37
commit 2e77fe09a5
+175
View File
@@ -0,0 +1,175 @@
# CODE-REVIEW-385-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/385
- Этап: code (PROCESS.md §2.7)
- Заход: r1 (первый цикл код-ревью для этого issue) · блокирующих циклов израсходовано 0 из 4
- ТЗ: `docs/specs/385-audit-lows.md`, ревизия 2 (принята SPEC-REVIEW-385-r2, зелёный,
`687b2966`)
- Коммиты в диапазоне `origin/dev..HEAD`:
- `353724b2` fix: audit-lows batch from the v1.69.0 audit (#385) — User-Visible: yes, Issue: #385
- `564bf419` build: refresh bundle trees for #385 — User-Visible: no, Issue: #385
## Скоуп
Четыре точечные правки по ТЗ rev2:
- **(а)** `src/houseplan-editor-runtime.ts` — клик по уже выбранному binding-кандидату
(список HA-сущностей, :12305-12313) и повторный выбор уже активного virtual-режима
(:12252-12260) становятся no-op: `valueSource`/`valueSourceTouched` и бейдж не
трогаются, сбрасывается только `bindingOpen`.
- **(б)** `src/devices.ts` — `rewriteMarkerControlReferences` больше не сажает
`value_badge: undefined` / `value_source: undefined` условным спредом.
- **(в)** `scripts/process-gate.mjs` — предикат релизности вынесен в
`isReleaseCommit(subject, one)` (единственная копия выражения, оба дизъюнкта);
`parseRecords` вызывает дорогой `releaseSourceViolationsOf` только когда
`isReleaseCommit` истинен для этого коммита.
- **(г)** `custom_components/houseplan/import_export.py` — параный комментарий у
обоих форматов обезвреживания (`value_badge` полями, `value_source` удалением
ключа) + объединяющий pytest.
Плюс тесты (`test/devices.test.mjs`, `test/process-gate.test.mjs`,
`tests_backend/test_ha_import_export.py`), два новых мутанта
(`scripts/mutation-gate.mjs`), правка `demo/smoke_value_face_source.mjs`,
ченджлоги, обновление трёх копий бандла.
## Как проверялось
Материал — `git log --oneline origin/dev..HEAD` и
`git diff origin/dev...HEAD` построчно по каждому изменённому файлу
(src/devices.ts, src/houseplan-editor-runtime.ts, scripts/process-gate.mjs,
custom_components/houseplan/import_export.py, все тестовые файлы,
scripts/mutation-gate.mjs, ченджлоги). Дополнительно чтением проверено
окружение диффа: полный список мест, где `_valueBadgeForBinding` и
`d.binding =` встречаются в `houseplan-editor-runtime.ts` (grep), чтобы
исключить третий путь выбора binding, который спека просила
перепроверить отдельно (раздел «Риски» ТЗ) — найдено ровно два места
выбора значения (:12257/:12310), оба в диффе; третье найденное вхождение
(:12275-12280, радио «from HA») не присваивает `binding` конкретному
значению и не трогает `valueSource` ни до, ни после диффа — вне контракта (а).
Гейты прогнаны лично на `564bf419` (зелёного Validate на этом SHA не было):
| Гейт | Результат |
|---|---|
| `npx tsc --noEmit` | чисто |
| `npm test` | 1587 tests, 1586 pass, 0 fail, 1 skipped — совпадает с заявленным автором |
| `npm run build` + сверка трёх копий бандла | `git status --short` пуст после билда — дерево уже актуально |
| `node scripts/check-docs.mjs` | "Documentation checks passed (7 files, 10 external links)" |
| `npm run bundle:budget` | initial View 277 979 B / 300 000 B, headroom 22 021 B — совпадает с заявленным (+28 Б от базы) |
| `node scripts/mutation-gate.mjs --id=same-binding-click-resets-source` | поймано 1/1 |
| `node scripts/mutation-gate.mjs --id=release-proof-computed-for-every-commit` | поймано 1/1 |
| `node demo/smoke_value_face_source.mjs` (после `npm run bundle:sync`, локальный `demo/srv/assets` не в git) | все 16 полей `true`, включая `sameBindingKeepsSource`/`sameVirtualKeepsSource` (AC1/AC2) и регресс-ветку `bindingResetToAuto` |
| `python3 -m pytest tests_backend/test_ha_import_export.py -k 385` | 1 passed (AC5) |
| `python3 -m pytest tests_backend/ -q` (полный, т.к. тронут `import_export.py`) | 446 passed, 1 skipped, 1 error — см. «Не проверял/не относится» |
| `node --test test/single-source-numbers.test.mjs` | 3/3 — диф не добавляет новую видимую величину, гейт для полноты |
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`: 2 файла в `src/**`,
1 символ на изменённых строках (`_markerDialog`), матрица 205 смоков, порог
широкого символа 41. Вывод — **НЕОПРЕДЕЛЁННОСТЬ**: только слабая связь по
общему имени `_markerDialog` (33 смока, включая уже прогнанный
`smoke_value_face_source.mjs`). Остальные 32 не прогонялись: связь слабая
(общее имя состояния диалога маркеров, а не конкретно binding-клика),
`smoke_value_face_source.mjs` — прямое совпадение (это тот самый смок,
который автор расширил под AC1/AC2) и уже покрывает сценарий диалога
целиком (открытие/выбор/сохранение/переоткрытие/статическая карточка/tap).
Инварианты модели (`npm run invariants`) не прогонялись — диф не трогает
рёбра комнат, толщину стен, `layout`, `marker.space`, `open_spans`.
`npm run golden:verify` не прогонялся — диф не меняет рендер/геометрию/стили
(логика диалога, отбрасывание ключа, гейт-скрипт, экспорт-нейтрализация).
## Находки
Блокирующих (High) находок нет. Medium в скоупе — нет.
**L1 (Low, не блокирует, снимаю записью).** `scripts/process-gate.mjs:118-121`
и `:166` — предикат релизности (`isReleaseCommit`) действительно один и тот же
и переиспользуется (AC4 выполнен буквально), но вспомогательный `one(name)`
продублирован двумя разными реализациями: в `makeCommit` — через
`all(name)[0] ?? null` на `matchAll(/…/gmi)`, в `parseRecords` — через
`text.match(/…/mi)`. Поведенчески идентичны (обе находят первое
`^Name:\s*(.+)$` без учёта регистра), и это подтверждено новым тестом
(`test/process-gate.test.mjs` — коммит-бета с `Release:`-трейлером и
стабильный релиз по subject дают одинаковую классификацию в обоих вызовах).
Риск чисто будущий: если кто-то поменяет формат трейлера в одном `one`, не
тронув другой, `isReleaseCommit` в `parseRecords` и в `makeCommit` разойдётся
по входу, а не по логике предиката — сам предикат от этого не расходится
(это то, что просило AC4), но результат классификации коммита — да. Не в
скоупе AC4 (там речь про предикат, не про хелпер `one`), масштаб — рефакторинг
не по этой задаче. Оставляю на усмотрение автора.
## Что проверено и корректно
- **(а)** — оба места выбора binding в скоупе, третьего нет (проверено grep +
чтением, см. «Как проверялось»); ранний no-op сохраняет весь драфт кроме
закрытия списка, что буквально соответствует контракту ТЗ. Смок гоняет и
успешный сценарий (сохранение source, предпросмотр, статическая карточка,
реакция на `unavailable`), и обе новые ветки (а), не разрывая старую
регресс-ветку `bindingResetToAuto`.
- **(б)** — условный спред верен: ключ появляется в выходном объекте только
когда исходное значение (`marker.value_badge`/`marker.value_source`) было
задано или было переписано на новый ref; для marker без ключа
`valueBadge`/`valueSource` остаются `undefined` из `: marker.value_badge`
fallback → ключ не создаётся. Тест `test/devices.test.mjs` бьёт по обеим
веткам (голый маркер, маркер с существующим `value_source`).
- **(в)** — семантика гейта не изменена (для нерелизных `null` как и раньше,
`() => null` больше не нужен — сравнение по значению, не по идентичности
функции), дорогая проверка теперь гарантированно вызывается только для тех
же коммитов, для которых `isRelease` истинен, потому что это буквально одна
и та же функция `isReleaseCommit`, вызванная дважды с одним `subject`.
Тест воспроизводит именно контрпример из ТЗ (бета-приёмка с `Release:`
трейлером — второй дизъюнкт).
- **(г)** — асимметрия форматов задокументирована на месте, `dropped_marker_links`
считает оба пути, pytest пришпиливает оба формата одновременно (не по
отдельности, как раньше было бы недостаточно для AC5).
- Ченджлоги (`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`) правлены в том же
коммите `353724b2`, что и код, запись только про (а), соответствует
`User-Visible: yes` и разделу «Release-артефакты» ТЗ. Трейлеры `Issue:` и
`User-Visible:` на месте на обоих коммитах диапазона.
- Три копии бандла (`dist/`, `custom_components/houseplan/frontend/`,
`houseplan-assets.json`×2) синхронны с исходниками — билд с нуля не дал
diff после коммита `564bf419`.
## Чего не проверял
- Полный browser-smoke матрицы (205 файлов) — не прогонял; инструмент дал
только слабую связь по общему имени `_markerDialog`, прямое совпадение
(`smoke_value_face_source.mjs`) прогнано и покрывает сценарий диалога
целиком. Остальные 32 «слабых» смока не трогают binding-клик по названию и
по факту (проверены выборочно по имени — `smoke_binding_picker.mjs`,
`smoke_binding_ui.mjs`, `smoke_dialog_zombie.mjs` про открытие/закрытие
диалогов и выбор HA-сущности, не про повторный клик по уже выбранной);
полный прогон матрицы — предрелизная обязанность, не гейт этого ревью.
- `npm run golden:verify` — диф не меняет рендер/геометрию/стили/слои.
- `npm run invariants` — диф не трогает геометрию модели (рёбра, толщину,
`layout`, `marker.space`, `open_spans`).
- Perf-профили — не названы в AC; (в) — только уменьшение работы гейта, ТЗ
прямо говорит «бенч не требуется».
- `tests_backend/test_ha_upload.py::test_upload_ok` — упал с
`AssertionError` на проверке типа daemon-потока при остановке aiohttp-сервера
(`_run_safe_shutdown_loop` / `threading._DummyThread`). Файл и код upload
не входят в диф этой задачи (диф трогает только `import_export.py` из
бэкенда), само падение — про завершение потока, не про сериализацию
маркеров/экспорт. Отношу к флаку окружения песочницы, а не к регрессии
этой правки; целевой тест AC5 (`test_issue_385_space_export_drops_badge_and_value_face_links_together`)
и весь `test_ha_import_export.py` (запускался в составе полного прогона)
прошли зелёным.
## AC — прослеживаемость
| AC | Доказательство | Статус |
|---|---|---|
| AC1 | `demo/smoke_value_face_source.mjs` → `sameBindingKeepsSource`; регресс `bindingResetToAuto` | прогнано лично, зелёное |
| AC2 | тот же смок → `sameVirtualKeepsSource` (плюс бейдж не проверяется отдельным полем смока, но реализация в (а) обрабатывает бейдж тем же early-return — проверено чтением) | прогнано лично + чтением |
| AC3 | `test/devices.test.mjs` "#385(б) rewrite never plants…" | прогнано в составе `npm test` |
| AC4 | `test/process-gate.test.mjs` "#385(в) the diff proof runs only for release-classified commits…" + мутант `release-proof-computed-for-every-commit` | прогнано лично, мутант пойман 1/1 |
| AC5 | `tests_backend/test_ha_import_export.py::test_issue_385_space_export_drops_badge_and_value_face_links_together` | прогнано лично, passed |
| AC6 | tsc/test/build/check-docs/bundle:budget — см. таблицу гейтов выше | прогнано лично, всё зелёное |
## Вывод
Все шесть AC доказаны автотестами, которые лично прогнаны и (там, где
предусмотрены мутанты) умеют падать. Единственная находка — Low, вне
критического пути, снята записью с обоснованием. Реализация точно
соответствует ТЗ rev2 по всем четырём пунктам, скоуп не расширен и не сужен.
Вердикт: **зелёный**.