Files
houseplan-card/docs/reviews/SPEC-REVIEW-406-r1.md
T
2026-09-01 16:32:25 +00:00

199 lines
17 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-406-r1
- Issue: #406
- ТЗ: `docs/specs/406-beta2-polish.md`, ветка `issue/406-beta2-polish`, SHA `8248f9e4`
- Этап: spec (PROCESS.md §2.4)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Вердикт: **жёлтый**
## Скоуп
Пять независимых мелочей (полный трек, критерий «одна поверхность» для `small`
не выполняется — согласен, обоснование корректно и подкреплено прецедентами
#385/#400):
- (а) мёртвые ключи `confirm.*` и смежных семейств в 4 словарях + гейт;
- (б) роль `alertdialog`/`aria-describedby` в `hp-dialog`/`hp-confirm`;
- (в) смок HA-ветки диалога подтверждения;
- (г) уборка `marker_area_snapshot` для исчезнувших устройств + порядок
обрезки при лимите;
- (д) сохранение `acceptance.declared` при пустой замене в `docs-accept.mjs`.
## Как проверялось
Ревью аналитическое (этап spec, кода ещё нет). Читал тело issue #406, оба
комментария владельца (S2-разбор, ссылка на ТЗ), сам файл ТЗ на SHA `8248f9e4`,
`PROCESS.md` (§2.4, §7.1, §7.2), `docs/SCOPE.md`. Затем **перепроверил
фактические утверждения ТЗ по текущему коду** (`git archive` того же SHA,
`src/i18n/en.json`, `src/hp-dialog.ts`, `src/hp-confirm.ts`,
`src/danger-confirm.ts`, `src/device-area-relocation.ts`,
`scripts/docs-acceptance.mjs`, `demo/smoke_*`), поскольку ТЗ строит контракт
(а) на конкретных числах и списке ключей, а разошедшееся число здесь — не
стилистика, а входные данные для гейта, который эта же задача пишет.
Обязательные разделы §7.1 присутствуют все: сценарий · что человек увидит до/
после · проблема+контракт (по каждому из пяти пунктов) · скоуп/не-скоуп · UX ·
модель данных и миграция · i18n · AC1…AC14 с доказательством · план автотестов
· риски · откат · release-артефакты.
## Находки
### [Medium, в скоупе] (а) Инвентарь мёртвых ключей `*.help.aria` неверен — все 19, а не 6 из 19, используются
**Файл**: `docs/specs/406-beta2-polish.md`, раздел «(а) Мёртвые строки в
словарях», строка со списком «13 ключей `*.help.aria`».
**Что не так**: ТЗ утверждает, что из ключей семейства `*.help.aria` 13 (из 19
в словаре) — «задел, не подключённый ни одним потребителем». Проверка кода на
том же SHA это опровергает: **все 19** ключей `*.help.aria` в `en.json`
используются — не литералом, а через паттерн `` `${key}.aria` `` в
`_help(key)` (`houseplan-editor-runtime.ts:1194`: `const ariaKey =
\`${key}.aria\``). Для каждого из 19 базовых `*.help` ключей нашёлся ровно один
литеральный вызов `this._help('xxx.help')` в `houseplan-editor-runtime.ts`
и/или `houseplan-onboarding-runtime.ts` (`space.cell_cm.help`,
`space.zero_wall_style.help`, `space.fill_mode.help`,
`device_inbox.show_hidden.help`, `gs.glow_radius.help`, `marker.controls.help`,
`marker.glow_radius.help`, `marker.value_badge.help`,
`marker.value_badge_source.help`, `marker.value_badge_position.help`,
`marker.value_source.help`, `marker.light_role.help`,
`marker.light_entity.help`, `marker.toggle_entity.help`,
`marker.glow_mode.help`, `gs.bg_mode.help`, `gs.north.help`,
`space.bg_mode.help`, `space.north.help`) — то есть ровно тот самый паттерн
«динамический суффикс от литерального ключа», который ТЗ признаёт законным
классом (`furn.cat_${id}` и «48 семейств»), но почему-то не признал для этого
конкретного случая.
Отдельно: список «Остальные двадцать три» в этом же разделе перечисляет только
19 позиций (13 `.help.aria` + `marker.display_hint` + `marker.display_hint_icon`
+ `history.delete_room` + `markup.delete` + `title.markup` +
`history.partition_add` = 19), а не 23 — четыре ключа из заявленных 23 нигде не
названы.
**Сценарий отказа**: если реализация будет опираться на нарратив ТЗ (список
«30 доказанно мёртвых ключей», явно включающий 13 `.help.aria`) вместо
самостоятельной перепроверки, из словарей будут удалены 13 реально
показываемых строк подсказки-для-скринридера в панели настроек — то есть
задача, соседний пункт которой (б) *чинит* доступность для скринридера,
пунктом (а) её же и *поломает*. Если же реализация полагается только на
механический гейт (AC1) — риск переносится на сам гейт: правило распознавания
«динамических семейств» (AC3) не называет паттерн `${key}.help → ${key}.aria`
явно (назван только пример `furn.cat_${id}`), и создатель гейта имеет
основания не покрыть его — а именно это и произошло у автора ТЗ при ручном
аудите.
**Ожидаемо**: пересчитать список действительно мёртвых ключей раздела (а)
(проверено выше: 6 из 19 `confirm.*`-ключей подтверждаются, `marker.
display_hint*`, `history.delete_room`, `markup.delete`, `title.markup`,
`history.partition_add` подтверждаются — эти 12 факт-чекнуты и мертвы; ни один
`*.help.aria` мёртвым не подтверждён при ручной проверке) и явно включить
правило «литеральный вызов `_help('x.help')` делает живыми оба ключа, `x.help`
и `x.help.aria`» в контракт AC3 как отдельно названное семейство, а не
подразумевать его в «и прочие 48».
### [Medium, в скоупе] (б) AC6/AC7 не решают роль для `HpConfirmKind.warning` (диалог разблокировки)
**Файл**: `docs/specs/406-beta2-polish.md`, раздел «(б) Роль диалога и текст
последствий», AC6/AC7.
**Что не так**: `hp-confirm` — общий компонент для двух видов запросов,
`kind: 'destructive'` (используется 7 раз, все — удаления) и `kind: 'warning'`
(используется ровно один раз — подтверждение разблокировки двери,
`houseplan-card.ts:13001-13006`, с собственным текстом последствий
`confirm.unlock_body`). Контракт ТЗ формулирует дихотомию «destructive →
alertdialog» / «обычный → dialog», а в качестве примера «обычных» диалогов
называет только диалоги, вообще не проходящие через `hp-confirm` (маркер,
калибровка пылесоса). Про `kind: 'warning'` контракт молчит: буквальное
прочтение AC6 («Подтверждение **разрушающего** действия объявляется как
alertdialog») позволяет реализовать признак как `destructive =
request.kind === 'destructive'` и оставить диалог разблокировки на
`role="dialog"` без `aria-describedby` — то есть ровно тот же дефект, ради
починки которого заведён пункт (б), сохранится на диалоге, который
`docs/SCOPE.md` называет единственной санкционированной поверхностью опасного
действия (лок/разлок).
**Сценарий отказа**: AC6 и AC7 оба зелёные (пройдены буквально по тексту), но
пользователь со скринридером по-прежнему не услышит «Устройство разблокируется»
при нажатии Unlock — потому что это `warning`, а не `destructive`, и контракт
не сказал, что с ним делать.
**Ожидаемо**: явное решение в контракте (б) — получает ли `kind: 'warning'`
тот же `alertdialog` + `aria-describedby`, что и `destructive` (агент вправе
решить сам, это не видимая пользователю форма, а объявляемая роль ARIA, но
решение должно быть явным и внесено в AC6/AC7, а не оставлено читателю
угадывать по бинарной формулировке), и соответствующий пункт в AC8/плане
автотестов, если ветка `warning` должна тоже проверяться смоком.
## Что проверено и корректно
- **Числа по словарю в целом**: 1201 ключ в `en.json` — подтверждено точным
подсчётом. 7 ключей `confirm.*` (delete_draft, delete_draft_segment,
delete_plan, delete_room, delete_space, remove_marker, unlock) — подтверждено
отсутствием любого литерального использования в `src/*.ts` на SHA
`8248f9e4`. `marker.display_hint`, `marker.display_hint_icon`,
`history.delete_room`, `markup.delete`, `title.markup`,
`history.partition_add` — подтверждено тем же способом, действительно мертвы.
- **(а) коллизия с `test/unified-wall-tool-source.test.mjs`** — реальна и
корректно описана как открытая: тест требует `history.partition_add`, ключ
в `src/` не используется; ТЗ явно требует разрешить конфликт, а не тихо
оставить.
- **(в) HA-ветка не покрыта** — подтверждено: `smoke_free_walls.mjs` —
единственный смок, определяющий `ha-dialog`, и он стабит `_confirmDanger`,
так что сам диалог не рисуется; в `smoke_danger_confirmation.mjs` и
`smoke_danger_confirm_branches.mjs` слова `ha-dialog` нет. Прецедент стаба
(`smoke_free_walls.mjs:20-22`), на который ссылается AC8, реален и рабочий.
- **(г) утечка снапшота** — подтверждено чтением `device-area-relocation.ts`:
`resolveDeviceAreaRelocations` строит `decisions` только по
`options.devices`; устройство, отсутствующее в этом списке целиком, никогда
не попадёт ни в `decisions`, ни, соответственно, в `removeSnapshot` — запись
`marker_area_snapshot[id]` переживёт его исчезновение бессрочно.
`removeMarkerAreaSnapshots` действительно вызывается только из ручного
редактирования/удаления маркера (`houseplan-editor-runtime.ts:8326,8448`) —
номера строк точны. Термин «авторитетный реестр» — не изобретение ТЗ, это
существующее в коде понятие (`device-area-relocation.ts:50,133,142`,
`ha-binding-status.ts`), контракт AC10 (не трогать при неавторитетном
реестре) корректно продолжает уже действующий ранний `return` в
`resolveDeviceAreaRelocations` при `!authoritative`.
`.slice(0, MARKER_AREA_SNAPSHOT_LIMIT)` при чтении — подтверждено
(`device-area-relocation.ts:64-75`), обрезка по вставке действительно режет
новые записи, а не старые; контракт на переворот правила (AC11) реализуем.
- **(д) `acceptance.declared`** — подтверждено чтением
`scripts/docs-acceptance.mjs` и `scripts/docs-accept.mjs:141-145`:
`docsAcceptancePlan` при пустом `declared` не отказывает (не тот класс
ошибок, что «объявлены, но не изменились»), и текущий код действительно
безусловно пишет `declared: [...decision.replace]` — при `replace: []` это
стирает предыдущий список. AC12/AC13 реализуемы без правки существующих
тестовых утверждений — `docsAcceptancePlan` не меняется, меняется только
сборка `accepted.acceptance` в `docs-accept.mjs`.
- **Скоуп/не-скоуп** — границы названы явно и по делу (например, отказ от
подключения `*.help.aria`-задела к интерфейсу — не в этой задаче), пересечение
с #403/#405 разведено по разным контрактам того же файла.
- **Продуктовые вопросы владельцу** — ни одного технического вопроса не
вынесено владельцу; UX-раздел корректно ссылается на терминологию
`USER-GUIDE.ru.md` вместо изобретения новой.
- Release-артефакты: изменение User-Visible (скринридер объявляет
подтверждение как alertdialog) корректно привязано к обоим CHANGELOG,
остальные четыре пункта верно помечены внутренними.
## Чего не проверял
- Не пересчитывал вручную полный список из «48 динамических семейств» и
итоговые «937 использовано / 30 не используется» построчно — точечно
перепроверил категории, названные в тексте (`confirm.*`, `*.help.aria`,
единичные ключи), этого достаточно, чтобы найти расхождение; исчерпывающий
пересчёт всех 1201 ключей — задача самого гейта (AC1), не ревью ТЗ.
- Не проверял, останется ли запись `marker_area_snapshot` живой, если
устройство пропало из `_devices`, но по тому же id всё ещё существует
маркер (контракт (г) говорит «пока жив маркер ИЛИ устройство», AC9
формулирует доказательство только через отсутствие в `_devices`, без явной
проверки маркера) — не смог дёшево установить, входят ли такие маркеры в
`options.devices` по построению; если нет — AC9 в текущей формулировке может
требовать удаления записи, которую контракт просит сохранить. Не поднимаю
до отдельной находки: не смог воспроизвести конкретный сценарий отказа за
разумное время, это кандидат на внимание в код-ревью, а не блокер ТЗ.
- Гейты (`tsc`/`test`/`build`) не гонял — на этапе spec кода ещё нет, дерево
ветки состоит из одного doc-коммита.
## Вердикт
Вердикт: жёлтый · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 2 → в задаче