36 KiB
CODE-REVIEW-462-r1
Issue: https://github.com/Matysh/houseplan-card/issues/462
Этап: код-ревью (PROCESS.md §2.7), заход r1, блокирующих циклов израсходовано 0/4
Ревьюер: Claude (роль «ревьюер кода», отдельная от роли «разработчик» — Codex)
Материал: git log --oneline origin/dev..HEAD (16 коммитов, все с трейлером Issue: #462) и
git diff origin/dev...HEAD на SHA daccaf7ea54a01e13d4150cbd588893ad90a4672 — этот SHA совпадает
с текущим HEAD в момент вывода вердикта (проверено git rev-parse HEAD непосредственно перед
подведением итогов, §2.7 «вердикт привязан к SHA»).
ТЗ: docs/specs/462-card-resource-registration.md, ревью ТЗ зелёное на r3
(docs/reviews/SPEC-REVIEW-462-r3.md), AC1–AC13.
1. Скоуп диффа
74 файла, +4739/−450 (единственный User-Visible: yes коммит fcf0ce80, остальные 15 —
User-Visible: no, тесты/документация/сгенерированные бандлы). Функционально:
- Backend: новый модуль
custom_components/houseplan/frontend_registration.py(659 строк) — типизированный outcome регистрации Lovelace-ресурса, lifecycle retry на 1 секунду, одноразовоеpersistent_notification, честный System Health;__init__.pyиsystem_health.pyпереведены на него;manifest.jsonполучилafter_dependencies: ["lovelace"]. - Frontend: новый чистый контроллер
src/version-recovery.ts(331 строк, без DOM/Lit/HA) + адаптерsrc/version-recovery-card.ts(223 строки) + интеграция вsrc/houseplan-card.ts(89 строк диффа: баннер в_renderRoot, единая точка_openMoreInfo,_syncVersionRecoveryна всех жизненных циклах). - Документация: README/USER-GUIDE EN+RU — развилка Storage/YAML
resource_mode/legacymode: yaml; статический parser вscripts/check-docs.mjs; оба changelog. - Тесты: 24 backend-теста (
tests_backend/test_ha_frontend_registration.py), 3 новых frontend-unit-файла,demo/smoke_version_recovery.mjs(405 строк), 3 golden-сценария, 14 новых мутантов вscripts/mutation-gate.mjs.
Продуктовое поведение подтверждено проверкой на реальном исходнике HA (см. §5) — задача не меняет раздачу бандла/кэш-бастинг, только надёжность регистрации и явную диагностику.
2. Как проверялось — таблица гейтов
| Гейт | Прогнан | Результат |
|---|---|---|
npx tsc --noEmit, npm test, npm run build + сверка 3 копий бандла |
Нет, переиспользован | Validate зелёный на точном HEAD daccaf7e: https://github.com/Matysh/houseplan-card/actions/runs/33969641826 |
node scripts/check-docs.mjs (diff трогает src/**) |
Нет напрямую, переиспользован | Часть того же зелёного Validate (job docs) |
npm run golden:verify (диффа рендера AC12) |
Нет, переиспользован | Часть зелёного Validate (job golden); 3 новых baseline приняты коммитом daccaf7e с трейлерами Release: v1.72.0-beta.5 + Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33969189246 |
python -m pytest tests_backend -q |
Нет, переиспользован | Часть зелёного Validate (job backend); локально pytest/homeassistant не установлены в этой ревью-сессии (см. §6) |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
Да | 98 «прямых совпадений» — разбор ниже (§4.3), большинство ложные |
node demo/smoke_version_recovery.mjs |
Да (через мутанта, чистый прогон) | зелёный — ok чистый прогон: node demo/smoke_version_recovery.mjs |
node scripts/mutation-gate.mjs --id=<14 мутантов #462> |
Да, 10 из 14 | 10/10 покраснели корректно (см. таблицу §5); 4 backend-мутанта не прогнаны локально (нет pytest/homeassistant в этой ревью-песочнице — дорогой гейт, разрешено читать вместо повторного прогона, §2.7) |
Чтение исходника HA (homeassistant==2024.6.0, минимально поддерживаемая версия) |
Да | скачан pip download homeassistant==2024.6.0, подтверждено наличие ConfigEntry.async_create_background_task(hass, target, name, eager_start=True) — закрывает открытый вопрос по AC3 (см. §5) |
Инварианты модели (npm run invariants) |
Не прогнан | diff не трогает геометрию/layout/marker.space/open_spans — неприменимо |
| Performance-профили | Не прогнан | не названы в AC; единственное влияние на перф — бюджет бандла, уже пересчитан и задокументирован автором в scripts/bundle-budget.mjs (см. §7) |
3. Находки
High: 0. Medium: 0 (в скоупе задачи и вне его). Low: 3, ни один не блокирует, оставлены с записью ниже.
| # | Файл:строка | Что | Severity | Решение |
|---|---|---|---|---|
| L1 | src/version-recovery-card.ts:176 |
viewOnly включает host._config?.kiosk === true, хотя VersionRecoveryController._reconcile() (src/version-recovery.ts:242) уже сам не вызывает isVersionReloadSafe() вне kiosk-ветки. Задвоение безопасное (belt-and-suspenders), но при будущем рефакторинге кто-то может решить, что viewOnly — единственный источник kiosk-гейта, и убрать внешнюю проверку по ошибке |
Low | Оставлено с записью; не влияет на корректность текущего кода |
| L2 | src/styles/base.styles.ts (правила .version-recovery*, :host(houseplan-card)) |
Стили лежат в общем baseStyles, который импортирует и houseplan-space-card (src/space-card.ts:9,932). Селектор :host(houseplan-card) корректно не матчится на space-card, поэтому визуальной утечки нет — это чистый вопрос лишних байт в бандле space-card |
Low | Оставлено; test/version-recovery-card-contract.test.mjs уже фиксирует границу селектора |
| L3 | custom_components/houseplan/frontend_registration.py:537 |
entry.async_on_unload(state.cancel) регистрируется при каждом вызове async_setup_frontend_registration, включая гипотетический повторный вызов без промежуточного unload — список on-unload callbacks мог бы расти. cancel() идемпотентен, и повторный вызов достижим только при нарушении контракта HA (setup без unload между вызовами), что не воспроизводится ни в одном тесте и не считается реальным путём в текущем HA lifecycle |
Low | Оставлено с записью; не воспроизводится, не блокирует |
Ни один из этих трёх пунктов не меняет вывод AC и не варьирует поведение, наблюдаемое пользователем — вердикт по ним: правится не будет, оставлены как документированные наблюдения (решение ревьюера, §2.7 «Low либо правится, либо снимается с записью»).
Отдельно отмечаю (не находка, наблюдение по §2.4/2.6 «скоуп не расширяется»): коммит 65d38b24
(fix: keep continuity cache read-only until load, Issue: #462, User-Visible: no) добавляет
guard !this._loadOk в _seedHiddenDevices и _syncAreaRelocations
(src/houseplan-card.ts:3958,5120-5121) — защита от того, что continuity-cache-кадр,
нарисованный до завершения авторитетной загрузки config+layout, мог бы вызвать деструктивную
запись (перезаписать серверную фильтрацию или удалить валидный snapshot area-relocation) поверх
устаревшего состояния. Это тот же инвариант _loadOk, от которого зависит новый предикат
initialFrameSettled (version-recovery-card.ts:155), поэтому чинить его в рамках этой же задачи
оправдано — это не отдельная попутная правка, а условие корректности самого AC8. Пользовательски
видимый эффект (защита от редкой потери непримечательного состояния при холодном старте) реален,
но обнаружен как превентивное упрочнение, а не как воспроизведённый дефект; User-Visible: no
для него — решение автора на грани, но не искажение фактов и не блокирует.
4. AC-по-AC разбор
4.1 Backend (AC2, AC3, AC4, AC5, AC11)
Читал custom_components/houseplan/frontend_registration.py целиком (659 строк) плюс
tests_backend/test_ha_frontend_registration.py (24 теста) и system_health.py/__init__.py
диффы. Параллельно второй читатель (независимый субагент) прошёл тот же материал —
расхождений в выводах нет.
- AC2 (честный outcome).
async_register_lovelace_resource(строки 191–265) различает все 7 состояний; existing-запись обновляется без дубля (_async_remove_duplicate_resources, 164–189, тестtest_registration_keeps_one_canonical_resource_and_removes_duplicates:114-130); отсутствующий файл никогда не даёт успешный loader (async_setup_frontend_registration:518-535, 562-564, тестtest_ha_setup.py::test_missing_frontend_bundle_does_not_skip_backend_setup:54-83проверяетresource_status == "not_attempted",loader == "none"). Держится. - AC3 (retry без двойного loader).
_schedule_retry(475–504) всегда ставит фиксированную секундную задержку черезasync_call_laterвнутри колбэкаasync_at_started, даже когда HA уже running (в этом случаеasync_at_startedвызывает колбэк синхронно, ноasync_call_laterвсё равно откладывает retry) — подтверждено и чтением, и тестомtest_already_running_setup_still_uses_the_fixed_one_second_delay:338-390. Отмена start-listener/timer/retry-task привязана кentry.async_on_unload(state.cancel)(537),cancel()(92–105) корректно останавливает все три; все три окна отмены (до старта, во время задержки, во время задачи) явно проверены вtest_retry_lifecycle_handles_are_cancelled:456-551тремя «поколениями» entry. Удаляется только собственный versioned URL setup'а (_owned_fallback_urls, 130–137 +_remove_owned_fallback_url, 304–327), чужой/пользовательский URL никогда не трогается (test_preexisting_exact_fallback_is_used_but_never_claimed_or_removed: 670-720). Повторный вызов_schedule_retryиз одного-единственного места (591) — «второй retry не наступает» верно по построению, а не только по тесту. Держится. - AC4 (одноразовое notification). Флаг пишется в
entry.dataтолько после того, какpersistent_notification.async_createвернулся без исключения (_async_create_reload_notice: 350-433), гонка «fallback-путь vs retry-завершение» серилизованаstate._notice_lock(360); тестtest_reload_notice_is_localized_and_persisted_once:723-770дважды вызывает функцию и проверяет ровно одно создание. Missing file/static failure не расходуют флаг (365–372,pending_frontend). Uninstall dismiss-ит best-effort (631–634). Держится. - AC5 (System Health).
system_health.py:37-75кладёт все восемь полей §10 поверх существующей статистики плана; параметризованный тестtest_system_health_reports_frontend_registration_matrix:1150-1335покрывает missing/success/pending/yaml/exception и отдельно проверяет, что старые поля (config_rev,spaces, …) не исчезли (1314–1335). Держится. - AC11 (downgrade/compatibility).
websocket_api.py, отдающийintegration_version, этим диффом не тронут (нет в списке изменённых файлов); существующийtest_ha_websocket.pyне модифицирован.test_ha_setup.pyполучил один новый тест, остальные не изменены. Держится (проверено чтением, не повторным прогоном полного backend-набора — см. §6).
Разрешённая неопределённость. Backend-субагент отметил открытый вопрос: доступен ли
ConfigEntry.async_create_background_task на минимально поддерживаемой HA 2024.6? Я скачал
homeassistant==2024.6.0 (pip download) и прочитал homeassistant/config_entries.py из wheel —
метод существует с сигнатурой (hass, target, name, eager_start=True), ровно как используется в
frontend_registration.py:494-497. Вопрос закрыт, разрыва по минимальной версии нет.
after_dependencies: ["lovelace"] (manifest.json:4-6) — техническая избыточность поверх уже
существующей жёсткой цепочки houseplan → frontend → lovelace (проверено: dependencies в
манифесте houseplan уже содержит frontend, а frontend's собственный манифест в
HA 2026.8.3 содержит lovelace как hard dependency) — это defense-in-depth, не фикс
несуществующего разрыва, и явно назван так в ТЗ §22 п.3 (не выдан за факт).
4.2 Frontend (AC6–AC10, AC12)
Читал src/version-recovery.ts и src/version-recovery-card.ts целиком, диффы
src/houseplan-card.ts/base.styles.ts/i18n, три тестовых файла и
demo/smoke_version_recovery.mjs полностью. Второй независимый читатель (субагент) прошёл тот же
материал параллельно — расхождений нет.
- AC6 (точное сравнение).
normalizeRuntimeVersion/compareRuntimeVersions(version-recovery.ts:69-83) трактуют не-строку/пустую/whitespace-строку какunknown, что стирает устаревший mismatch, а не оставляет его (мутантversion-recovery-treats-unknown-as-mismatchпрогнан лично мной — покраснел). Матрица §11.1 реализована в_reconcile()(233–260) построчно. Держится. - AC7 (обычный режим никогда не reload сам). Вне kiosk
_reconcile()(242–247) никогда не вызывает_armTimer(); единственный путь кreload()—renderVersionBanner'sevent.isTrusted && controller.hasCurrentMismatchNotice(version-recovery-card.ts:195-197). Мутантversion-recovery-auto-reloads-ordinary-viewпрогнан лично мной — покраснел. Держится. - AC8 (kiosk safety). Все 10 полей
isVersionReloadSafe(version-recovery.ts:99-110) замаплены на конкретное состояние карточки вcardVersionReloadSafetySnapshot(version-recovery-card.ts:151-186).surfacesIdleпереиспользует существующий_editorSecondaryDialogBlocked(houseplan-card.ts:9017-9027, покрывает_dangerConfirm,_importDialog,_backupImportDialog,_kioskDialogи ещё 15 диалогов) плюс отдельно_partitionDeleteDialog/_roomDeleteDialog/_backdropGuard/_vacFit._openMoreInfo(houseplan-card.ts:5603-5619) — единственная точка входа для всех вызовов (проверено: все 5 call site — pointer/_ctxDevice, keyboard через_clickDevice, vacuum-иконка, info-card кнопки — сведены к этой функции), продлевающая_cyclePausedUntilбезусловно приthis._kiosk, независимо от модальности вызова._tick()(278–306) перечитываетhooks.safety()заново на каждом 250-мс тике — не залипающий снапшот, TOCTOU-окна нет (проверка иclaimReloadTargetпроисходят синхронно в одном тике). Четыре мутанта (version-recovery-ignores-editor-state,-ignores-dialog-state,-ignores-pending-config-write,-ignores-interaction-pause) прогнаны лично мной — все покраснели. Держится. - AC9 (защита от reload-loop).
attemptedTarget/claimReloadTarget(version-recovery.ts:117-147) читают и пишут один и тот же ключsessionStorage; target durable записан ДО вызоваreload()(293–298, подтверждено мутантомversion-recovery-marks-target-after-reload— покраснел лично у меня); сохранённый target уважается новым инстансом (мутантversion-recovery-ignores-stored-target— покраснел лично у меня); недоступныйsessionStorage— fail-safe в баннер (attemptedTarget/claimReloadTargetcatch-блоки, 121-127/138-147). Держится. - AC10 (граница full/space).
test/version-recovery-card-contract.test.mjs:16-31статически подтверждает отсутствие импорта version-recovery вsrc/space-card.ts(grep подтвердил 0 совпадений); браузерно —demo/smoke_version_recovery.mjs:349-362(staticOwnsNoController: !('_versionRecovery' in staticCard)). Держится. - AC12 (View/touch/a11y/визуальная стабильность).
ordinaryStageDoesNotMove(smoke) — баннер не двигает stage bbox;role=status/aria-live=polite/aria-atomic=trueбез повторного объявления благодаря early-return в_showBanner(version-recovery.ts:308-310); кнопка ≥44×44 CSS px (buttonMin44в smoke);prefers-reduced-motionотключает анимацию (_hideBanner, 320-330 +base.styles.ts). Подавление тоста #353 — точное условиеinfo.terminal && this._versionRecovery.hasCurrentMismatchNotice(houseplan-card.ts:717,747), не более широкое: non-terminal и terminal-без-баннера всегда показывают тост (проверено и unit-веткой, и smoke-сценариямиlazyToastWithBanner/terminalToastWithoutBanner). Три golden-сценария (desktop/touch/kiosk after-attempt) приняты вdaccaf7eсо ссылкой на прогон CI. Держится.
4.3 Документация (AC1) и артефакты сборки (AC13)
scripts/check-docs.mjs (диф +72 строки) статическим парсером требует ровно два снепшота
(resource_mode: yaml современный + помеченный legacy mode: yaml), запрещает плоский
top-level resources:, и требует оба hard-reload shortcut во всех четырёх документах — сверил
построчно с README.md/README.ru.md/USER-GUIDE.{md,ru.md}, текст идентичен ожидаемому контракту.
Мутант resource-docs-flatten-current-yaml прогнан лично мной — покраснел. Держится.
AC13: три бандл-дерева (dist, custom_components/.../frontend, демо-стенд) синхронны — это
часть зелёного Validate на daccaf7e, отдельно не перепроверял. i18n: en/ru/de/fr на фронтенде
(src/i18n/*.json) и en/ru/de/fr на бэкенде (strings.json+translations/*.json) — сверил
построчно, ключи присутствуют во всех восьми файлах без исключений.
5. Таблица «чем краснеет» (защитные AC, §2.7)
| AC | Чем доказан | Чем краснеет | Прогнал сам? |
|---|---|---|---|
| AC1 (docs contract) | node scripts/check-docs.mjs |
мутант resource-docs-flatten-current-yaml (плоский resources: вместо вложенного) |
Да — покраснел |
| AC3 (retry без delay) | pytest -k retry_waits_one_second |
мутант frontend-registration-retries-without-delay (снята фиксированная секундная задержка) |
Нет (нет pytest/HA локально) — подтверждено чтением assertion timer["delay"] == 1.0 дважды независимо (я + субагент) |
| AC3 (retry вообще не наступает) | тот же тест | мутант frontend-registration-skips-retry (убран вызов _schedule_retry) |
Нет — чтением: assertion "callback" in started |
| AC3 (unload-race) | pytest -k retry_lifecycle_handles_are_cancelled |
мутант frontend-registration-is-not-unload-bound (убран entry.async_on_unload) |
Нет — чтением: assertion len(before_start_entry.unload_callbacks) == 1 |
| AC4 (флаг персистится) | pytest -k reload_notice_is_localized_and_persisted_once |
мутант frontend-reload-notice-forgets-persisted-flag |
Нет — чтением: assertion len(created) == 1 после второго вызова |
| AC6 (unknown не путается с mismatch) | node --test --test-name-pattern="malformed values stay unknown" test/version-recovery.test.mjs |
мутант version-recovery-treats-unknown-as-mismatch |
Да — покраснел |
| AC6 (config/get владеет capability-adoption до соседних отказов) | node demo/smoke_version_recovery.mjs |
мутант version-recovery-delays-config-capability-adoption |
Да — покраснел (бандл пересобран, полный дорогой прогон) |
| AC7 (обычный режим не reload) | node --test --test-name-pattern="ordinary mode always" test/version-recovery.test.mjs |
мутант version-recovery-auto-reloads-ordinary-view |
Да — покраснел |
| AC8 (editor guard) | test/version-recovery-card.test.mjs (card adapter maps editor state) |
мутант version-recovery-ignores-editor-state |
Да — покраснел |
| AC8 (dialog guard) | тот же файл (maps blocking surfaces) |
мутант version-recovery-ignores-dialog-state |
Да — покраснел |
| AC8 (config-write guard) | тот же файл (maps config writes) |
мутант version-recovery-ignores-pending-config-write |
Да — покраснел |
| AC8 (interaction-pause guard) | тот же файл (maps interaction pause) |
мутант version-recovery-ignores-interaction-pause |
Да — покраснел |
| AC9 (target записан до reload) | test/version-recovery.test.mjs (marks the exact target before one reload) |
мутант version-recovery-marks-target-after-reload |
Да — покраснел |
| AC9 (сохранённый target уважается) | тот же файл (same target is once per tab) |
мутант version-recovery-ignores-stored-target |
Да — покраснел |
10 из 14 мутантов прогнаны лично мной командой node scripts/mutation-gate.mjs --id=<id> —
каждый показал ok <id>: тест покраснел, как обязан. 4 backend-мутанта относятся к дорогому
гейту (pytest/homeassistant, недоступны в этой ревью-песочнице) — для них по правилу §2.7
достаточно чтения: я лично прочитал целевые assertion'ы и подтвердил точное попадание в
мутированную строку (см. §4.1); независимый второй читатель (субагент) пришёл к тем же выводам
по тем же строкам.
6. Одно число — один источник
Версии, показанные в баннере (version_mismatch.frontend/.backend), и версия, используемая
для ключа sessionStorage-guard'а, читаются из одного вызова:
compareRuntimeVersions(normalized.frontendVersion, normalized.backendVersion)
(version-recovery.ts:215-222), где normalized.backendVersion — результат
normalizeRuntimeVersion(capabilities.integration_version) из того же adoptCardConfigCapabilities
(version-recovery-card.ts:52), что и адаптирует capability. Второго независимого пути получения
integration_version в новом коде нет — старое присвоение _haIntegrationVersion внутри
_reloadLayoutOnly() (houseplan-card.ts:4914-4917 до диффа читало resp?.integration_version
из ответа houseplan/layout/get) удалено этим диффом (дифф -4879,8 +4914,6 вычёркивает
строки this._haIntegrationVersion = typeof resp?.integration_version === 'string' ? ...),
то есть ранее существовавший второй источник (из layout/get) закрыт, а не оставлен рядом с
новым (config/get через adoptCardConfigCapabilities). Один источник подтверждён.
7. Что проверено и признано корректным
- Все 13 AC ТЗ разобраны по коду (см. §4), для защитных — таблицей «чем краснеет» (§5).
- Idempotency повторного
setup/reload/unload— тестамиtest_repeated_setup_updates_once_ then_reuses_canonical_resource,test_fallback_survives_unload_reload_and_registry_ reconciliation, тремя «поколениями» вtest_retry_lifecycle_handles_are_cancelled. _safe_error(frontend_registration.py:108-110) — толькоphase:ExceptionClassName, ни сообщения, ни пути, ни стектрейса; проверено по всем вызовам (async_register_lovelace_ resource,_add_fallback,_remove_owned_fallback_url,async_setup_frontend_registration).- Отсутствующий bundle не валит setup (
test_missing_frontend_bundle_does_not_skip_backend_ setup), sweep/repairs продолжают исполняться. - Uninstall убирает все записи с базовым URL, но никогда — чужой/пользовательский URL
(
test_uninstall_removes_all_base_url_entries_fallback_and_notice,test_preexisting_exact_fallback_is_used_but_never_claimed_or_removed). single_config_entry: trueв манифесте (не менялся этим диффом) исключает многократную конкурентную регистрацию из разных entries — предпосылка «второй retry не наступает» не имеет обходного пути через второй entry.- Бюджет бандла (
scripts/bundle-budget.mjs) пересчитан и задокументирован: потолок 298 500→301 000, бюджет 300 000→301 066, измеренный факт 300 090 Б gzip (+2 587 Б за eager version-recovery controller), 976 Б запаса;test/bundle-assets.test.mjsбольше не хардкодит число, читает константу. - golden: 3 новых сценария (
version-mismatch-{desktop-dark-en,touch-light-ru, kiosk-attempted-dark-de}) приняты на полном Linux CI-артефакте (Baseline-Reviewedссылка в трейлере),demo/golden/harness.mjsдо снятия кадра явно бросает, если целевая версия баннера/sessionStorageне совпала со сценарием — фейл-фаст на рассинхрон, а не тихий кадр. - i18n en/ru/de/fr полны на обеих сторонах (frontend
src/i18n/*.json, backendstrings.json+translations/*.json),test/i18n.test.mjsучитывает новый allowed-equal key. - Оба changelog обновлены в том же коммите, что и поведение (
fcf0ce80,User-Visible: yes). - Все 16 коммитов диапазона несут
Issue: #462; единственныйUser-Visible: yesкоммит содержит правки в обоих changelog.
8. Чего не проверял и почему
- Полный локальный прогон
tsc/npm test/npm run build/golden:verify/pytest tests_backend— не прогонял: они дешёвые по регламенту, но у меня уже есть зелёный Validate ровно на этом SHA (daccaf7e), и правило «дешёвые гейты уже подтверждены» разрешает их не перегонять в этом раунде. - 4 backend-мутанта (
frontend-registration-*,frontend-reload-notice-*) — не прогнаны: в этой ревью-песочнице нетpytest/homeassistant(.venv-backendне создаётся вне cloud-agent startup script, см. AGENTS.md), а мутация-гейт для них — дорогой (пересборка + pytest). По §2.7 для дорогого backend-гейта разрешено читать assertion вместо повторного прогона — сделано дважды независимо (я и субагент), см. §5. - 97 из 98 «прямых совпадений»
smoke-select.mjs— не прогонял. Инструмент матчит по общим символам (_mode,_zoom,_stageEl,_config, …), но подавляющее большинство совпадений — побочный эффект того, чтоVersionRecoveryCardPort(version-recovery-card.ts:71-136) объявляет ~40 УЖЕ существующих полей карточки как readonly-типы для предикатов безопасности, не меняя их семантику ни в одной строке. Ни один из этих смоков (decor/furniture/kiosk/resize/…) не проверяет код, который этот дифф действительно меняет — реальное изменение поведения ограничено_openMoreInfo,_renderRoot,_syncVersionRecoveryи новыми модулями. Прогон всех 98 был бы предрелизным набором, а не гейтом ревью (PROCESS.md §8). Единственный по-настоящему целевой смок —smoke_version_recovery.mjs— прогнан (см. §2). npm run invariants— не прогонял: diff не трогает геометрию,layout,marker.space,open_spansни в одной строке.- Performance-профили — не прогонял: не названы в AC; единственное влияние на перф — бюджет бандла, который уже пересчитан и обоснован автором с измеренным фактом (см. §7).
- Ручное тестирование в браузере — не проводилось (в этом процессе его нет вообще, код-ревью заменяет его, PROCESS.md §2.7).
9. Вывод
Все 13 AC либо доказаны автотестом, чью способность падать я лично подтвердил (10 мутантов из 14 для #462 прогнаны лично, плюс независимая пара глаз на оставшихся 4), либо разобраны по коду с явной пометкой «проверено чтением» там, где повторный прогон дорогого гейта не входит в объём ревью. Три Low-находки не меняют вывод ни по одному AC и оставлены с записью. High и Medium находок нет — ни в скоупе, ни вне его.
Вердикт: зелёный.
Материал раунда
- Ветка:
issue/462-card-resource-registration, коммитdaccaf7ea54a— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
0caccec714620782910b4f0c0fc60ddc0f769561git log --all --format='%H %T' | grep 0caccec71462 - ТЗ
docs/specs/462-card-resource-registration.md, блобd3620eab48950255aa91240cd70ec3ed1edfd756git log --all --find-object=d3620eab48950255aa91240cd70ec3ed1edfd756 -- docs/specs/462-card-resource-registration.md