16 KiB
CODE-REVIEW — issue #419, заход r1
- SHA:
567a45ab(веткаissue/419-area-snapshot-roster-guard) - Диапазон:
origin/dev..HEAD(5 коммитов) - Этап: code (PROCESS.md §2.7) · заход r1 · блокирующих циклов израсходовано 0 из 4
- Вердикт: зелёный · High: 0 · Medium: 0
Скоуп
ТЗ docs/specs/419-area-snapshot-roster-guard.md (принято зелёным SPEC-REVIEW-419-r1,
High 0 / Medium 0). Задача: resolveDeviceAreaRelocations() считала отфильтрованную
презентационную проекцию devices доказательством исчезновения binding из HA и стирала
marker_area_snapshot при пустом/усечённом ростере. Контракт: отделить destructive
orphan-cleanup от presentation-роутинга, ввести positive evidence (полный Device/Entity
Registry, exact live state, живой marker), запретить cleanup при пустом namespace,
требовать два различных непустых authoritative revisions перед удалением, один
confirmation refresh, runtime-only кандидаты.
Job из docs/SCOPE.md: J6 «Keep the plan true as the home evolves» — сохранность persisted lifecycle-метаданных при живом плане. Смежная линия — «never delete a user's file on an inference» (тот же принцип: слабая улика не повод для необратимого удаления), применённая здесь не к файлам, а к Area provenance.
Диапазон изменений: src/device-area-relocation.ts, src/houseplan-card.ts,
test/device-area-relocation.test.mjs, demo/smoke_area_relocation.mjs (новый),
scripts/mutation-gate.mjs, docs/CONFIG-COMPATIBILITY.md, docs/FILTERING.md,
docs/TESTING.md, docs/CHANGELOG.md/.ru.md, синхронные bundles. Геометрия, layout
edges, marker.space, open_spans, wall-thickness записи и Python backend не тронуты —
инварианты модели и pytest не запускались обоснованно (см. «Чего не проверял»).
Как проверялось
Материал — git log --oneline origin/dev..HEAD и git diff origin/dev...HEAD, плюс
чтение полного resolveAreaSnapshotCleanup/resolveDeviceAreaRelocations и вызывающего
кода в houseplan-card.ts, а также ha-binding-status.ts (семантика revision,
authoritative, дебаунс scheduleReload) — читал, чтобы доказать AC9 (никакого
reload-loop на state tick, ровно один confirmation refresh), а не поверил описанию.
Гейты прогнаны самостоятельно на 567a45ab (зелёного Validate на этом SHA не найдено):
| Гейт | Результат |
|---|---|
npx tsc --noEmit |
green, без вывода |
npm test |
1743 tests, 1742 pass, 0 fail, 1 skip |
npm run bundle:sync (build + 3 копии) |
green; git status после сборки чист — bundles уже синхронны с коммитом |
node scripts/check-docs.mjs |
green: «Documentation checks passed (7 files, 10 external links)» |
node scripts/mutation-gate.mjs --check |
341/341 ok, 0 FAIL (якоря патчей не устарели) |
node scripts/mutation-gate.mjs --id=area-snapshot-cleanup-* (все 4 новых/обновлённых мутанта, реальный прогон, не только anchor-check) |
все 4: тест покраснел, как обязан, поймано 1 из 1 каждый |
node demo/smoke_area_relocation.mjs |
все 26 полей true, включая новые #419-сценарии (recovered/confirmed/empty probes) |
node demo/smoke_readonly_cold_start.mjs (прямое совпадение по _canEdit) |
все поля true |
node demo/smoke_binding_picker.mjs (прямое совпадение по _markers) |
все поля true |
node scripts/no-new-any.mjs --base origin/dev --head HEAD |
«130 строк в 2 файлах, новых any нет» |
node scripts/bundle-budget.mjs |
green, initial View 291046 B / бюджет 300000 B, запас 8954 Б (известный долг #367, не регрессия этой задачи) |
node scripts/process-gate.mjs --issues |
«гейт пройден, предупреждений 0» |
git diff --stat -- custom_components/**/*.py |
пусто — pytest backend обоснованно не прогонялся |
Все цифры совпали с тем, что автор заявил в комментарии от 16:27:33Z — включая
smoke-select вывод (10 прямых/17 слабых совпадений, оба прогнанных прямых совпадения
подтверждены).
node scripts/smoke-select.mjs --base origin/dev --head HEAD (прогнан повторно):
10 прямых совпадений (NORM_W×7, Marker, _markers, _canEdit), 17 слабых
(все — _model). Решение по каждому: NORM_W/Marker — не поведенческая правка (тип
и уже существующий параметр coordinateScale не меняются по значению), эти 8 смоков не
гоняю; _markers→smoke_binding_picker, _canEdit→smoke_readonly_cold_start — оба
прогнаны выше, зелёные. _model-слабая связь (17 смоков) — диффом не задета (модель
пространств/комнат не читается новым кодом), не гоняю. demo/smoke_area_relocation.mjs
не входит в вывод инструмента, так как это новый файл — прогнан отдельно как основной
AC-носитель (AC1,2,5–7,9–11).
Инварианты модели (npm run invariants) не запускались: diff не трогает рёбра комнат,
записи толщины, layout, marker.space, open_spans — только lifecycle-решение по
marker_area_snapshot (Record binding+area, формат не изменён).
Находки
Нет. High: 0, Medium: 0, Low: 0 (без сравнения с предыдущим раундом — это заход r1 code, делить не с чем).
Что проверено и корректно (по AC)
- AC1/AC12 —
resolveAreaSnapshotCleanupстроитexistsизmarkerBindings,markerIds,deviceIds/entityIds/liveEntityIds, никогда изdevices[]. Юнит «orphan cleanup uses full registry evidence instead of the presentation roster» и мутантarea-snapshot-cleanup-forgets-registry-evidence(реально прогнан, покраснел). - AC2/AC12 — пустой
deviceIds/entityIdsблокирует confirmation (namespaceNonEmptyпроверяется раздельно по kind). Юнит «empty registry namespaces never confirm...» и мутантarea-snapshot-cleanup-trusts-empty-namespace(прогнан, покраснел). СмокemptyRegistryPreservesSnapshot+emptyRegistryDoesNotReloadLoop(deviceRegistryCalls === 1) — подтверждено исполнением. - AC3 —
liveEntityIdsстроится изoptions.liveStatesбез фильтра поstate, так чтоon/unavailable/unknownодинаково считаются доказательством. Параметризован в юните «exact live entity state preserves registry-less snapshot provenance». - AC4 —
markerBindings/markerIdsсобираются сif (marker.removed) continue;— tombstone не даёт положительного доказательства. Юнит «a live saved marker preserves matching id or binding but a tombstone does not». - AC5/AC6/AC7 — состояние
candidates: Map<binding, revision>двигается по строгой машине (undefined → отметить + запросить refresh; та же revision → no-op; другая revision → confirm removeIds; появление binding →candidates.delete). Три юнита покрывают все переходы plюс мутантarea-snapshot-cleanup-trusts-first-absence(прогнан, покраснел). Смок:confirmationRefreshRunsOnce(recoveredProbe: deviceRegistryCalls===2, cleanupWrites===0) доказывает «первое отсутствие не удаляет, восстановление снимает кандидат» на реальном production bundle, не только на чистой функции. - AC8 — «limited frames and runtime reset do not confirm an earlier absence»: смена
authoritative:falseне продвигает кандидата; свежийMap()(эмуляция remount) снова стартует с первого отсутствия. В карточке_areaSnapshotCleanupCandidates.clear()вызывается в_ensureHaRegistryAuthority()при смене connection — то же место, где уже сбрасывались_haBindingCacheKey/_haRegistryRev. - AC9 —
revisionвha-binding-status.tsинкрементируется только вfinallyблокаloadFullRegistries(то есть на завершение полного registry-фетча), не на state tick — прочитано и проверено чтением, чтоhaRegistryBuildSignature/_maybeRebuildDevicesне создают новых кандидатов на каждый рендер без изменения revision.needsConfirmationRefreshустанавливается только в веткеfirstRevision === undefined, то есть ровно один раз на binding до следующей смены revision. СмокconfirmationRefreshRunsOnceиemptyRegistryDoesNotReloadLoopдоказывают счётчиком WS-вызовов, не только по количеству полей результата. - AC10 — явный rebind/delete/explicit-placement идёт старым однопроходным путём в
resolveDeviceAreaRelocations(if (!binding) { if (previous) removeSnapshot: true }), который читаетregistryFollowingBinding(device)по актуальному marker/binding, а не затрагивается двойным подтверждением. Проверено чтением и существующими юнитами/смоком (explicitOverrideExcluded,compositeGroupExcluded, вся матрица #126/#403/#406 вsmoke_area_relocation.mjsзелёная). - AC11 —
confirmedProbe(rejectFirstCleanup) в смоке: первыйconfig/setс уменьшённымmarker_area_snapshotсинтетически падает,cleanupWrites >= 2доказывает повтор после отказа; неподтверждённый (recoveredProbe) кандидат даётcleanupWrites === 0— подтверждённое решение не может утечь в write batch раньше срока. - AC12 — 3 новых + 1 обновлённый мутант в
scripts/mutation-gate.mjs, все проверены полным прогоном (--id=...), а не только anchor-check: каждый покраснел на именно том юните, что указан вguard. - AC13 — tsc/test/build зелёные (см. таблицу),
git statusпослеbundle:syncпуст (три копии синхронны),no-new-any— 0 новых. - AC14 —
docs/CONFIG-COMPATIBILITY.md,docs/FILTERING.md,docs/TESTING.md, оба CHANGELOG обновлены в одном коммите489e289fвместе с кодом; терминология («Area provenance», «registry-following») согласована с существующим текстом этих же файлов, новых пользовательских терминов не вводится (UI не меняется —docs/USER-GUIDE.ru.mdтрогать не требовалось и не тронут).check-docsзелёный; commit567a45abотдельно обновляет только screenshot fingerprint со ссылкой на канонический CI run и подтверждением на независимомdev-прогоне, что дельта инфраструктурная (runner image), а не визуальная.
Трейлеры Issue:/User-Visible: на месте во всех 5 коммитах; коммит с User-Visible: yes
(489e289f) содержит правки обоих CHANGELOG в себе же (process-gate.mjs --issues зелёный).
Один источник числа
Диффом не добавлено и не изменено ни одной пользовательски видимой величины (UI не меняется, спека прямо это фиксирует). Раздел неприменим.
Чего не проверял
npm run golden:verifyи performance-профили — не прогонял: diff не меняет рендер, геометрию или чувствительный к перфу путь (только registry/lifecycle-решение до input/render веток); AC и риски задачи явно относят их к предрелизным гейтам.npm run invariants— не прогонял: diff не касается рёбер комнат, записей толщины,layout,marker.space,open_spans.python -m pytest tests_backend -q— не прогонял: ни одинcustom_components/**/*.pyне в диффе.- Остальные 8 «прямых совпадений»
smoke-select(NORM_W×7,Marker) и все 17 «слабых» (_model) — не гонял: символы задеты только как неизменённый тип/константа, диффом не меняется их пользовательский путь (см. раздел «Как проверялось»). - Полный
demo/smoke_*.mjsнабор целиком — не гонял: задача не задевает все подсистемы, это предрелизный гейт (PROCESS.md §8). - Ручное тестирование в браузере HA — не проводилось (не часть цикла ревью); достоверность обеспечена browser smoke на production bundle плюс unit/mutation.
Заключение
ТЗ выполнено полностью и доказуемо: каждый AC либо подтверждён юнит-тестом, либо browser
smoke на production bundle с точными счётчиками сетевых вызовов, либо явной записью
«проверено чтением» с указанием конкретных строк (ha-binding-status.ts revision
semantics). Мутанты реально прогоняются и падают на удаление доказательства, а не только
проходят anchor-check. Гейты, прогнанные самостоятельно, совпали с заявленными автором
результатами один в один. Находок нет — вердикт зелёный.
Материал раунда
- Ветка:
issue/419-area-snapshot-roster-guard, коммит567a45ab6bf5— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
58de96f55f1d82cb5695cf151b597b09edfc2505git log --all --format='%H %T' | grep 58de96f55f1d - ТЗ
docs/specs/419-area-snapshot-roster-guard.md, блобeb5a7a352d742be601c7c695d27256f9af9bc0adgit log --all --find-object=eb5a7a352d742be601c7c695d27256f9af9bc0ad -- docs/specs/419-area-snapshot-roster-guard.md