From aad1eeaa1075d2f841fc7b2e8f0e2a78b133a244 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Tue, 1 Sep 2026 18:14:58 +0300 Subject: [PATCH] docs: revise beta2 polish spec Issue: #406 User-Visible: no --- docs/specs/406-beta2-polish.md | 125 +++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 53 deletions(-) diff --git a/docs/specs/406-beta2-polish.md b/docs/specs/406-beta2-polish.md index eba7c5f7..61bbfb66 100644 --- a/docs/specs/406-beta2-polish.md +++ b/docs/specs/406-beta2-polish.md @@ -4,7 +4,7 @@ - Приоритет: P3, polish; полный трек — четыре несвязанные поверхности (словари i18n, `hp-dialog`, снапшоты переезда area, приёмка скриншотов), критерий «одна поверхность» для `small` не выполняется — прецеденты #385, #400 -- Ревизия: 1 (2026-08-31) +- Ревизия: 2 (2026-09-01), после SPEC-REVIEW-406-r1 ## Сценарий @@ -17,37 +17,37 @@ ## Что человек увидит до и после -**До**: (1) 28 строк в четырёх словарях никому не показываются, но их переводят +**До**: (1) 52 строки в четырёх словарях никому не показываются, но их переводят и поддерживают; (2) скринридер объявляет диалог удаления как обычный `dialog` и не читает текст последствий; (3) HA-ветка диалога не покрыта ни одним смоком — проверяется только запасная; (4) записи снапшота исчезнувших устройств живут вечно; (5) повторная приёмка кадров затирает список «что меняли». **После**: словари содержат только используемое, и это проверяется гейтом; -подтверждение разрушающего действия объявляется как `alertdialog` вместе с -последствиями; смок ходит по обеим веткам диалога; снапшот убирает записи -устройств, которых достоверно нет; след приёмки не теряется. +подтверждение опасного действия — удаления или разблокировки — объявляется как +`alertdialog` вместе с последствиями; смок ходит по обеим веткам диалога; +снапшот убирает записи устройств, которых достоверно нет; след приёмки не +теряется. ## Проблема и контракты по пунктам ### (а) Мёртвые строки в словарях -Сверка ключей `src/i18n/en.json` с литералами в `src/*.ts` (динамические -шаблоны вида `` `furn.cat_${id}` `` учтены — их 48 семейств): +Сверка ключей `src/i18n/en.json` с литералами и производными ключами в +`src/*.ts` (динамические шаблоны вида `` `furn.cat_${id}` `` и механически +производные суффиксы учтены): -| Ключей в словаре | Использовано литералом | Не используется никак | +| Ключей в словаре | Есть потребитель | Не используется никак | |---|---|---| -| 1201 | 937 | **30** | +| 1201 | 1188 | **13** | -Из тридцати семь — предмет issue (`confirm.delete_draft`, +Из тринадцати семь — предмет issue (`confirm.delete_draft`, `confirm.delete_draft_segment`, `confirm.delete_plan`, `confirm.delete_room`, `confirm.delete_space`, `confirm.remove_marker`, `confirm.unlock`): это односоставные строки браузерного `confirm()`, которые #32 заменил парами `…_title` + `…_body`. -Остальные двадцать три — тот же узор в других семействах: +Остальные шесть — тот же узор в других семействах: -- 13 ключей `*.help.aria` (`marker.*`, `space.*`, `gs.*`, `device_inbox.*`) — - задел, не подключённый ни одним потребителем; - `marker.display_hint`, `marker.display_hint_icon` — вытеснены `marker.display_hint_badge/_icon_ripple/_value/_static_icon` (`houseplan-card.ts:404-407`); @@ -56,16 +56,23 @@ - `markup.delete` — вытеснен `markup.delete_room` (`:11877`); - `title.markup`, `history.partition_add` — остатки прежнего инструмента стен. +**Важное динамическое семейство, которое удалять нельзя**: все 19 ключей +`*.help.aria` используются фабрикой `_help('literal.help')`. В +`houseplan-editor-runtime.ts:1194` и onboarding-ветке доступный ключ выводится +как `` `${key}.aria` ``; для каждого из 19 базовых `*.help` есть литеральный +вызов `_help()`. Гейт обязан распознавать эту производную пару так же, как +шаблоны `furn.cat_*`, а не требовать буквального появления полного aria-ключа. + **Контракт**: словарь содержит только то, что кто-то показывает. Проверяется -гейтом, который строит множество литералов и динамических префиксов из `src/` -и объявляет остальное мёртвым. +гейтом, который строит множество литералов, динамических префиксов и +механически производных ключей из `src/` и объявляет остальное мёртвым. **Скоуп сознательно шире тела issue, и вот почему**: гейт нельзя ввести -наполовину. Если он судит весь словарь — он покраснеет на всех тридцати; если +наполовину. Если он судит весь словарь — он покраснеет на всех тринадцати; если только на `confirm.*` — он не поймает следующий такой ключ, а именно за этим его и заводят. Поэтому в задаче удаляются все доказанно мёртвые ключи, а не -только семь. Временный список исключений не заводится: это долг, который никто -не разберёт. +только семь. Живые производные семейства, включая `*.help.aria`, описываются +правилом извлечения, а не временным списком исключений. **Известная коллизия, которую обязана разрешить реализация**: `test/unified-wall-tool-source.test.mjs:32` **требует наличия** @@ -90,15 +97,22 @@ и не объявит предложение, ради которого диалог и показывают («Устройство исчезнет с плана вместе с настройками»). -**Контракт**: диалог, требующий решения о разрушающем действии, объявляется как -`alertdialog`, и его текст последствий связан с диалогом через -`aria-describedby`. Роль **выбирается, а не меняется глобально**: `hp-dialog` -несёт и обычные диалоги (маркер, калибровка пылесоса), для которых `alertdialog` -неверен — эта роль обязывает screen reader прервать чтение. +**Контракт**: любой запрос `hp-confirm` — и `HpConfirmKind.destructive`, и +`HpConfirmKind.warning` — объявляется как `alertdialog`, и его текст последствий +связан с диалогом через `aria-describedby`. `warning` здесь не означает обычное +информационное окно: единственный такой запрос — разблокировка двери +(`houseplan-card.ts:13042-13046`) с последствиями из `confirm.unlock_body`, то +есть санкционированная `SCOPE.md` поверхность опасного действия. -Практически: `hp-dialog` получает признак вида (destructive / обычный), который -проставляет `hp-confirm` для своих запросов; обе ветки — HA и запасная — -объявляют роль одинаково. +Роль **выбирается, а не меняется глобально**: `hp-dialog` несёт и обычные +диалоги (маркер, калибровка пылесоса), для которых `alertdialog` неверен — эта +роль обязывает screen reader прервать чтение. Обычным считается диалог, который +не представлен `hp-confirm`; внутри `hp-confirm` ветки «обычного» вида нет. + +Практически: `hp-dialog` получает семантический признак alert/обычный и id +описания; `hp-confirm` передаёт alert-семантику для обоих своих kind и связывает +с ней `.danger-confirm-body`. Обе ветки — HA и запасная — объявляют роль и +описание одинаково. ### (в) HA-ветка диалога не покрыта @@ -164,8 +178,8 @@ decisions не попадает — его запись `marker_area_snapshot[id `hp-confirm`; смок HA-ветки подтверждения; уборка снапшотов при авторитетном реестре и порядок обрезки; сохранение `acceptance.declared`. -**Не в скоупе**: подключение `*.help.aria` к интерфейсу (если задел нужен — -отдельный issue; строки восстанавливаются из истории); содержимое и оформление +**Не в скоупе**: изменение содержимого или оформления help-подсказок — их 19 +пар `.help` + `.help.aria` уже подключены и сохраняются; содержимое и оформление диалога (#32); переезд area и его отказы (#403); порог свидетелей (#405 — соседняя строка того же файла, другой контракт); рост `known_devices` — тот же класс, но своя поверхность. @@ -173,7 +187,7 @@ decisions не попадает — его запись `marker_area_snapshot[id ## UX Оформление не меняется. Меняется то, что слышит пользователь скринридера: -подтверждение разрушающего действия объявляется как оповещение и читается +подтверждение удаления или разблокировки объявляется как оповещение и читается вместе с последствиями. Терминология берётся из `docs/USER-GUIDE.ru.md`, новых формулировок не вводится. @@ -188,31 +202,35 @@ decisions не попадает — его запись `marker_area_snapshot[id ## i18n -Новых строк нет; тридцать (по числу мёртвых ключей) удаляются из каждого из -четырёх словарей. Паритет сохраняется — удаление синхронное во всех четырёх. +Новых строк нет; тринадцать мёртвых ключей удаляются из каждого из четырёх +словарей — 52 строки суммарно. Все 19 пар `*.help` + `*.help.aria` сохраняются. +Паритет сохраняется — удаление синхронное во всех четырёх словарях. ## Критерии приёмки - **AC1**. Ни один ключ словаря не остаётся без потребителя: гейт строит - множество литералов и динамических префиксов из `src/` и краснеет на любом - ключе, не покрытом ни тем, ни другим. Доказательство: гейт зелёный после - чистки. + множество литералов, динамических префиксов и производных ключей из `src/` и + краснеет на любом ключе, не покрытом этими правилами. Доказательство: гейт + зелёный после чистки. - **AC2**. **Отрицательный прогон обязателен**: возвращённый в словарь мёртвый ключ роняет гейт. Доказательство: мутант через штатный раннер. -- **AC3**. Гейт не обвиняет динамические ключи: `furn.cat_*`, `furn.sym_*`, - `wall_model.reason.*`, `resize.disabled.*`, `junction.limit_*`, `decor.*` и - прочие 48 семейств остаются зелёными. Доказательство: гейт зелёный на текущем - словаре после удаления только доказанно мёртвых. +- **AC3**. Гейт не обвиняет динамические и производные ключи: `furn.cat_*`, + `furn.sym_*`, `wall_model.reason.*`, `resize.disabled.*`, + `junction.limit_*`, `decor.*`, а также все 19 `*.help.aria`, получаемые из + литеральных `_help('*.help')`, остаются зелёными. Доказательство: гейт + зелёный на текущем словаре после удаления ровно 13 доказанно мёртвых ключей. - **AC4**. Коллизия с `test/unified-wall-tool-source.test.mjs` разрешена явно: либо показан потребитель `history.partition_add`, либо ключ удалён вместе со строкой теста. Доказательство: оба гейта зелёные одновременно. - **AC5**. Паритет словарей сохранён: `test/i18n.test.mjs` зелёный без правок его утверждений, в четырёх словарях одинаковый набор ключей. -- **AC6**. Подтверждение разрушающего действия объявляется как `alertdialog`, а - текст последствий связан через `aria-describedby`. Доказательство: смок - читает атрибуты у настоящего диалога. -- **AC7**. Обычные диалоги (маркер, калибровка) остались `dialog`. - Доказательство: тот же смок, вторая проверка. +- **AC6**. Оба вида `hp-confirm` — `destructive` (например, удаление) и + `warning` (разблокировка двери) — объявляются как `alertdialog`, а их текст + последствий связан через `aria-describedby`. Доказательство: смок читает + атрибуты и доступное описание у настоящего диалога для обоих kind. +- **AC7**. Диалоги вне `hp-confirm` (маркер, калибровка) остались `dialog` и не + получают alert-семантику. Доказательство: тот же смок, отдельная проверка + обычного `hp-dialog`. - **AC8**. Инварианты подтверждения проверены на **обеих** ветках: диалог показан, «Отмена» резолвит `false`, начальный фокус на «Отмена», Esc отменяет. Доказательство: смок подтверждений со стабом `ha-dialog` (прецедент @@ -231,7 +249,7 @@ decisions не попадает — его запись `marker_area_snapshot[id - **AC13**. Штатная приёмка изменённого кадра по-прежнему записывает свой список. Доказательство: существующие тесты `test/docs-acceptance.test.mjs` зелёные без правок их утверждений. -- **AC14**. Бюджет initial не растёт: удаление тридцати строк из словарей +- **AC14**. Бюджет initial не растёт: удаление тринадцати ключей из словарей уменьшает его, роль и `aria-describedby` добавляют единицы байт. Доказательство: `npm run bundle:budget` до и после. @@ -245,8 +263,8 @@ decisions не попадает — его запись `marker_area_snapshot[id **Browser smoke** (`demo/smoke_danger_confirm_branches.mjs` — дополнение; файл уже создан #402 и держит свои фикстуры): -2. Роль и `aria-describedby` у подтверждения (AC6), роль у обычного диалога - (AC7). +2. Роль и `aria-describedby` у destructive- и warning-подтверждения (AC6), + роль у обычного диалога вне `hp-confirm` (AC7). 3. Те же инварианты со стабом `ha-dialog` (AC8). **Browser smoke** (`demo/smoke_area_relocation.mjs` — дополнение): @@ -279,15 +297,16 @@ decisions не попадает — его запись `marker_area_snapshot[id автоматически (48 семейств уже покрыты), а сообщение об отказе называет ключ и подсказывает обе законные дороги — использовать или удалить. - **`alertdialog` меняет поведение скринридера сильнее, чем кажется**: он - прерывает текущее чтение. Смягчение: роль назначается только подтверждениям - разрушающих действий, обычные диалоги проверяются отдельным AC. + прерывает текущее чтение. Смягчение: роль назначается только двум видам + `hp-confirm` (`destructive` и `warning`); обычные диалоги вне подтверждения + проверяются отдельным AC. - **Уборка снапшотов может стереть законные записи.** Это ровно тот класс, что чинит #403. Смягчение: уборка только при авторитетном реестре, AC10 проверяет обратный случай напрямую, мутант закреплён. -- **Удаление 13 ключей `*.help.aria` окажется преждевременным**, если задел - планировался к подключению. Смягчение: восстановление из истории тривиально, - а поддерживать непоказываемый перевод в четырёх языках дороже, чем вернуть - строки в тот день, когда для них появится потребитель. +- **Гейт примет производный ключ за мёртвый.** Наиболее опасный текущий пример — + 19 живых `*.help.aria`, полный ключ которых собирается в `_help()`. Смягчение: + производное правило `.help` → `.help.aria` является частью AC3 и проверяется + всем текущим набором, а не ручным исключением отдельных имён. ## Откат @@ -299,6 +318,6 @@ decisions не попадает — его запись `marker_area_snapshot[id ## Release-артефакты - `docs/CHANGELOG.md` / `docs/CHANGELOG.ru.md`: пункт о том, что подтверждение - разрушающего действия объявляется скринридером вместе с последствиями + удаления или разблокировки объявляется скринридером вместе с последствиями (User-Visible: yes). Остальные четыре пункта — внутренние. - Скриншоты не меняются: диалог в статике не открыт.