17 KiB
SPEC-REVIEW-406-r1
- Issue: #406
- ТЗ:
docs/specs/406-beta2-polish.md, веткаissue/406-beta2-polish, SHA8248f9e4 - Этап: 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на SHA8248f9e4.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 → в задаче