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

17 KiB
Raw Permalink Blame History

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 → в задаче