Files
houseplan-card/docs/reviews/SPEC-REVIEW-536-r1.md
T
2026-09-12 03:40:45 +00:00

248 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC-REVIEW-536-r1
**Issue:** #536 — «Плашка несовпадения версий не гарантирует свой снос: `disconnect()` обнуляет `_banner` без `hooks.changed()`»
**Этап:** S4-spec-review (ревью ТЗ, PROCESS.md §2.4)
**Трек:** лёгкий (`small`), лимит циклов ревью ТЗ — 2
**Заход:** r1 · блокирующих циклов израсходовано 0 из 2
**Ревьюер:** Claude (Sonnet 5), роль «ревьюер ТЗ»
**Автор ТЗ:** Claude (класс A по прямому указанию владельца, отмечено в теле issue)
## Материал раунда
- Тело issue #536, раздел `## ТЗ` (Продуктовая рамка, Контракт К1–К3, таблица
AC1–AC3, Риски, Затронутые файлы, Откат), плюс предшествующий ему разбор
(«Симптом», «Где именно», «Замер», «Насколько это видно пользователю»).
- SHA256 тела issue, как получено через `gh issue view 536 --json body -q .body`:
`42fc4ceae09539fe52bd339ae55bb4812367a7a996e4a417165481c259b028af`
- Единственный комментарий issue («Аналитика и ТЗ — на ревью», S1→S4 одним ходом).
- Код на момент ревью: `dev` @ `05a2c68da23b890de049d8507dd41c1ba2c9910e` (рабочая
копия чиста, HEAD == origin/dev). Читался только для проверки выполнимости и
однозначности ТЗ — продуктовый код не менялся, правки нет ни одной.
## Скоуп ревью
Обязательные для лёгкого трека разделы ТЗ (§5: проблема · контракт · AC1…ACn с
доказательством · откат, плюс два продуктовых раздела §7.1), однозначность и
проверяемость AC1–AC3, наличие «чем краснеет» для защитных AC (§2.7), отсутствие
невыделенных догадок о поведении, соответствие текущей реализации
`src/version-recovery.ts` и `src/version-recovery-card.ts`, корректность
классификации лёгкого трека и `User-Visible: no`.
## Как проверялось
1. Прочитан `docs/SCOPE.md` — задача не вводит новую функциональность и не
расширяет ни один Core user job; это исправление надёжности уже существующего
механизма (плашка версии — служебное уведомление, привязанное к J6 «Keep the
plan true» лишь косвенно). Конфликтов со скоупом нет.
2. Прочитан `AGENTS.md` (классы файлов, лёгкий трек — умолчание) и `PROCESS.md`
§1, §2.2–2.4 (лёгкий трек одним ходом S1→S4, что законно и явно объяснено
автором в комментарии), §4 (бюджет циклов, лёгкий трек — 2), §5 (критерии
лёгкого трека), §7.1 (обязательные разделы), §7.2 (формат вердикта).
3. Прочитан текущий `src/version-recovery.ts` целиком. Симптом подтверждён
построчно: `disconnect()` (строки 199–206) действительно обнуляет `_banner`
без вызова `this.hooks.changed()`, а `_hideBanner()` (строки 320–330) уже
пуст к моменту вызова после `disconnect()` и молча возвращается. Найденное в
issue «Замер» (последовательность `changed=1` четыре раза подряд, включая
`connect()` в конце) воспроизводится по коду вручную, а не принимается на
слово.
4. Прослежен вручную предложенный контракт К1–К3 на конкретной
последовательности из AC1 («несовпадение → disconnect → версии сошлись →
connect»), с учётом того, что плашка уже видна до начала последовательности
(это следует из «Замера» в issue, а не додумано): `_reconcile()` при
несовпадении зовёт `_showBanner` → `changed` #1; правленный `disconnect()`
при непустой `_banner` зовёт `changed` #2 и обнуляет поле; последующий
`update()` к состоянию `equal` вызывает `_reconcile()`, но `_connected` уже
`false`, и `_hideBanner()` рано выходит (поле уже `null`) — без нового
`changed`; `connect()` вызывает `_reconcile()` при `relation.kind !== 'mismatch'`
— снова ранний выход `_hideBanner()`, без нового `changed`. Итог: ровно два
вызова `changed`, плашка пуста — совпадает с формулировкой AC1 дословно.
5. Отдельно проверено АC2 логически: путь `disconnect()` без `_banner` не входит
в изменённую ветку (`if (this._banner) { … }`), лишнего `changed()` там нет ни
до, ни после правки — это тривиальное следствие формы правки, а не отдельный
риск.
6. Прочитан `src/version-recovery-card.ts` (строки 147–160) — `hooks.changed`
действительно равен `() => { host.requestUpdate(); }`, как и заявлено в ТЗ.
7. Проверена состоятельность риска №1 («`requestUpdate()` из
`disconnectedCallback` — путь не новый»), а не принята на слово: в
`src/houseplan-card.ts` переопределение `requestUpdate()` (строки 680–694) не
делает исключения для отключённого состояния — оно безусловно вызывает
`super.requestUpdate(...)`. В `disconnectedCallback()` самой карточки (строки
2743–2751) `this._versionRecovery.disconnect()` стоит **первой** строкой,
перед `_clearRoomFocus(true)` и `_cancelDangerConfirm()`, а `super.disconnectedCallback()`
зовётся в конце метода — стандартный для этого файла паттерн (тот же порядок
в `space-card.ts`, `hp-dialog.ts`, `hp-help.ts`, `hp-color-opacity.ts`).
Довод автора подтверждён чтением, а не верой в комментарий issue.
8. Прочитан `test/version-recovery.test.mjs` (строки 1–71) — инфраструктура для
AC1/AC2 уже существует: `harness()` собирает `events.push({ kind: 'changed' })`
при каждом вызове `hooks.changed`, так что подсчёт «два вызова, а не один» —
не гипотетический план, а тривиальное расширение уже работающего каркаса.
9. Проверено `scripts/mutation-gate.mjs` на предмет коллизии имени: существующие
мутанты для этого файла (`version-recovery-treats-unknown-as-mismatch`,
`version-recovery-auto-reloads-ordinary-view` и другие, строки 890–970) не
пересекаются с предложенным `version-banner-disconnect-silent` — новое имя
свободно.
10. Проверено `docs/USER-GUIDE.ru.md` и весь `docs/` на упоминания плашки
версии/`version-recovery` — терминология нигде не зафиксирована как
пользовательская (это внутренний механизм восстановления, не описанный в
гайде), поэтому вопрос словаря интерфейса не встаёт.
11. Оценена корректность `User-Visible: no`: в худшем случае непочиненный дефект
даёт как раз наблюдаемый артефакт (зависшая плашка) — это отмечено
автором честно, а не скрыто. Довод «сегодня корректность держится на
98%-совпадении, а не на контракте» и «в браузерной пробе всегда чинится
посторонней перерисовкой» проверяемы только через существующий смок
панели — на этапе ревью ТЗ смок не гонялся (кода ещё нет), поэтому запись
ниже помечена как решение ревьюера по существу, а не как перепроверенный
факт.
Гейты (typecheck/test/build/golden/backend) не гонялись — это этап ревью ТЗ,
продуктовый код ещё не написан; §2.4 их не требует.
## Разбор по существу
### Обязательные разделы (лёгкий трек, §5 + два продуктовых раздела §7.1)
| Раздел | Есть? | Где |
|---|---|---|
| Сценарий (персона/поверхность/момент) | ✅ | «Продуктовая рамка»: «любая персона, кто видит плашку» — оправданно широко, плашка не персонифицирована ни в одном документе |
| Что человек увидит до/после | ⚠️ формально | «До/После» присутствует, но использует термин реализации «контроллер решил её снять» — см. Low №1 |
| Проблема | ✅ | «Симптом» + «Где именно» + «Замер» до раздела `## ТЗ` |
| Контракт поведения | ✅ | К1–К3, дословно привязан к текущему коду |
| AC1…ACn с доказательством и «чем краснеет» | ✅ | Таблица AC1–AC3, у каждого юнит-тест и мутант/контрпроба |
| Риски | ✅ | 2 пункта, оба по существу (см. проверку риска №1 выше) |
| Откат | ✅ | «Ревёрт одной строки» — соответствует реальному размеру правки |
| Скоуп/не-скоуп | ⚠️ неявно | К2 «Больше ничего не меняется» плюс список затронутых файлов пинует скоуп к одному модулю, но отдельного заголовка «не-скоуп» нет — см. Low №2 |
| Миграция/i18n/perf/touch: явное «нет» | ⚠️ подразумевается | Не выписано отдельными строками, следует из перечисления критериев трека — см. Low №2 |
### Техническая состоятельность К1–К3 и AC1–AC3
Разобрано построчно в шагах 3–7 «Как проверялось» выше. Контракт реализуем
буквально: правка — оборачивание существующего `if (this._banner) this._banner = null;`
в блок, добавляющий `this.hooks.changed()` до или после обнуления. АС1
арифметически подтверждён вручную (два вызова `changed`, а не один или три);
АС2 — тривиальное следствие формы правки; АС3 — регресс на неизменённых тестах,
корректный выбор для «ничего больше не меняется».
Риск №1 (безопасность `requestUpdate()` из `disconnectedCallback`) — не догадка
автора, а подтверждённый чтением существующий паттерн в этом же файле
(`houseplan-card.ts:2743`), что снимает главный источник тревоги: не откроет
ли фикс новый путь падения при демонтаже карточки. Риск корректно передаёт
дальнейшую проверку («существующий смок панели и юниты карточки») ревью кода —
это стадия, где это можно доказать исполнением, а не чтением.
### Классификация трека и `User-Visible: no`
Оба вопроса автор прямо адресовал ревьюеру, а не владельцу — это правильная
маршрутизация: ни один из них не о том, что видит или делает персона, оба
решаются встроенным сюда разбором.
- **Лёгкий трек.** Все критерии §5 выполнены одновременно и ни один не нарушен:
одна поверхность (`src/version-recovery.ts`), сложность и риск — реально 2 (это
подтверждает и объём настоящего разбора: вся логика прослеживается вручную за
один проход), миграции конфига нет, нового UX-контракта нет (внешнее поведение
не меняется ни в одном уже покрытом тестами сценарии — AC3 это фиксирует),
влияния на touch/perf нет. **Подтверждаю классификацию.**
- **`User-Visible: no`.** Довод автора состоятелен: сегодня корректность снятия
плашки после реаттача держится на том, что в реальном приложении между
disconnect и следующим кадром почти всегда происходит посторонняя
перерисовка (`_maybeRebuildDevices` и другие), а не на гарантии контракта.
Правка не меняет ни одного уже наблюдавшегося поведения (AC3), а закрывает
единственный путь, который по признанию самого автора не удалось пронаблюдать
вживую. Записывать в changelog нечего показать пользователю — там нет «было
Х, стало Y» с наблюдаемой разницей. **Подтверждаю `User-Visible: no`.**
### Проверка «не выдана ли догадка за решение»
- «Замысел прежней редакции сохраняется дословно» (обоснование, зачем `_banner`
вообще обнуляется в `disconnect()`) — не догадка, а прямое чтение
существующего кода и комментария к нему; К2 явно фиксирует это как то, что
не трогается.
- Утверждение «в браузерной пробе не видно — 11 посторонних перерисовок» —
заявлено как результат конкретного замера, не как общее свойство системы;
автор честно помечает, что корректность «держится на совпадении», а не
утверждает, что дефекта нет вообще.
- Риск №1 подан как риск с указанием, чем закрывается (существующий путь плюс
смок/юниты), не как решённый факт — и подтверждён теперь ещё и чтением
ревьюера.
Непомеченных догадок о поведении, которого нет ни в одном документе, не найдено.
## Находки
Блокирующих (High) находок нет. Находок Medium — в скоупе или вне скоупа — нет.
### Low (сняты решением ревьюера, с записью, без возврата на цикл)
1. **«До/После» использует термин реализации.** Формулировка «уходит тогда,
когда контроллер решил её снять» называет внутреннюю сущность
(`VersionRecoveryController`) вместо чисто наблюдаемой фразы. По содержанию
вреда нет: правка не меняет наблюдаемого поведения ни в одном
протестированном сценарии (сама пара «До/После» и заявляет «внешне то же
самое»), поэтому смысловой ошибки, которая исказила бы приёмку, здесь нет.
Снимаю без возврата — переписывать одну фразу ради буквы правила не стоит
цикла ревью на лёгком треке (лимит 2).
2. **Не-скоуп, миграция/i18n/perf/touch не выписаны отдельными строками.** Всё
это выводится однозначно: список «Затронутые файлы» называет ровно один
модуль плюс тест и реестр мутантов, а обоснование лёгкого трека в конце ТЗ
перечисляет «нет миграции, нет UX-контракта, нет влияния на перф и touch» —
содержательно то же самое, что отдельные строки DoR-чек-листа (§2.5)
потребовали бы дословно. Снимаю с записью: автор или разработчик могут
дописать явные строки при переводе в `S5-ready` без нового цикла ревью ТЗ —
аналогично прецеденту SPEC-REVIEW-535-r1, Low №2.
## Что проверено и корректно
- Симптом воспроизведён чтением текущего `src/version-recovery.ts`: `disconnect()`
обнуляет `_banner` без `hooks.changed()`, `_hideBanner()` после этого — пустой
ранний выход. Соответствует «Замеру» в issue.
- К1–К3 реализуемы буквально текущим кодом; АС1 арифметически проверен вручную
(ровно два вызова `changed`, плашка пуста в конце последовательности); АС2 и
АС3 — корректные, проверяемые утверждения с названным способом доказательства
и «чем краснеет».
- Риск №1 (безопасность `requestUpdate()` из `disconnectedCallback`) подтверждён
независимым чтением `houseplan-card.ts` (переопределение `requestUpdate()` не
делает исключения для отключённого состояния; вызов `_versionRecovery.disconnect()`
стоит первой строкой `disconnectedCallback()`, `super.disconnectedCallback()` —
последней, как и в остальных компонентах этого репозитория).
- Тестовая инфраструктура для AC1/AC2 уже существует (`harness()` считает
события `changed`) — план автотестов не вымышленный.
- Имя предлагаемого мутанта (`version-banner-disconnect-silent`) не пересекается
с существующим реестром `scripts/mutation-gate.mjs`.
- Классификация лёгкого трека и `User-Visible: no` — оба вопроса, адресованные
ревьюеру автором, разобраны по существу и подтверждены, решение принято здесь,
а не вынесено владельцу (это не продуктовый вопрос «что видит/делает персона»).
- Непомеченных догадок о недокументированном поведении не найдено.
- Терминология плашки версий нигде не зафиксирована в `docs/USER-GUIDE.ru.md`,
поэтому вопрос словаря интерфейса не встаёт.
## Чего не проверял
- Гейты (`typecheck`/`test`/`build`/golden/backend) — не требуются на этапе
ревью ТЗ (§2.4), кода ещё нет.
- Реальный прогон юнит-теста и мутанта `version-banner-disconnect-silent` — они
ещё не написаны; это предмет код-ревью.
- Существующий смок панели и юниты карточки на предмет «отсоединённый рендер не
падает» (риск №1) — упомянуты автором как свидетель, но не перезапускались
мной; это стадия код-ревью, где можно доказать исполнением.
- Реальное поведение в браузере (11 посторонних перерисовок и т.п.) — измерение
автора, не переизмерял.
## Вердикт
Вердикт: зелёный · заход r1 · блокирующих циклов 0/2 · High: 0 · Medium: 0 → в задаче
Issue #536 переходит в `S5-ready`.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `05a2c68da23b` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `d334191242147a177483837271e9462955b4807e`
```
git log --all --format='%H %T' | grep d33419124214
```
- Тело issue: `fdc4c4f20bae1574a65b8c8bac06c43f4ecdab6d3394149baea09c34bec75cc4`
- Вердикт конвейера: `green` · High 0