27 KiB
SPEC-REVIEW-406-r2
- Issue: #406
- ТЗ:
docs/specs/406-beta2-polish.md, веткаissue/406-beta2-polish, ревизия 2, SHA0d5b52d1(HEAD ветки44715dd0добавляет поверх толькоdocs/reviews/SPEC-REVIEW-406-r1.md, самого ТЗ не трогает) - Этап: spec (PROCESS.md §2.4)
- Заход: r2 · блокирующих циклов израсходовано 1 из 4
- Вердикт: жёлтый
Почему разбор полный, а не по дельте
Владелец перебазировал ветку на origin/dev (b04387ce) между r1 и r2.
SHA r1, названный в его собственном документе, — 8248f9e4 — на текущем
дереве не существует (git cat-file -t 8248f9e4 → Not a valid object name): это не «SHA не назван», SHA назван корректно, но ребейз его
переписал. Это ровно случай PROCESS.md §2.10: «ребейз на ушедший вперёд dev
(после ребейза это другой код)» — разбор обязан быть полным. Дополнительное
основание для полноты: ребейз протащил на ветку чужой коммит
(8119c523, issue #409), который меняет ровно тот файл и ровно то место,
которое контракт (д) описывает как открытый дефект — см. находку №1.
Скоуп
Без изменений от r1: пять независимых мелочей полного трека (i18n-мусор +
гейт, роль hp-dialog/hp-confirm, смок HA-ветки, уборка снапшотов area,
сохранение следа приёмки скриншотов).
Как проверялось
Прочитан docs/SCOPE.md (раздел «Partially covered»: «Accessibility:
… no ARIA labelling of the plan» — задача (б)/(в) законно попадает сюда),
PROCESS.md §2.4/§2.9/§2.10/§7.1/§7.2, тело issue #406 и все комментарии
(включая S2-разбор, ссылку на ТЗ, вердикт r1, ответ владельца на r1),
docs/USER-GUIDE.ru.md (терминология подтверждения/разблокировки, строки
672-730, 824-963 — новых формулировок ТЗ не вводит).
Само ТЗ вычитано целиком построчно. Каждое фактическое утверждение перепроверено на текущем дереве (не на SHA r1 — его больше нет), а не принято на слово:
src/i18n/en.json|ru.json|de.json|fr.json— подсчёт ключей и паритет (node -eподсчёт), точечная проверка литерального использования всех 13 заявленных мёртвых ключей и представителей заявленных динамических семейств (furn.cat_*,furn.sym_*,wall_model.reason.*,resize.disabled.*,junction.limit_*,decor.*,_help()/.aria);src/hp-dialog.ts,src/hp-confirm.ts,src/danger-confirm.ts,src/houseplan-card.ts(строки вокруг_lockAction) — роль диалога, устройствоHpConfirmKind, место вызоваwarning-подтверждения;src/device-area-relocation.tsцеликом —resolveDeviceAreaRelocations,applyAreaRelocationResolution,markerAreaSnapshotOf,git logфайла (единственный коммит с #126, после рёбейза не менялся);scripts/docs-accept.mjsцеликом,git log -pпо нему — здесь нашлась находка №1;test/unified-wall-tool-source.test.mjs:32,demo/smoke_free_walls.mjs(ha-dialog-стаб),demo/smoke_esc_dialogs.mjs,demo/smoke_danger_confirm_ branches.mjs,demo/smoke_danger_confirmation.mjs— покрытие веток и прецедент стаба.
Гейты (tsc/test/build) не гонял: git diff --stat origin/dev...HEAD
показывает только два doc-файла (docs/specs/406-beta2-polish.md,
docs/reviews/SPEC-REVIEW-406-r1.md), продуктового кода на ветке ещё нет —
то же основание, что в r1.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
(а) Medium: инвентарь *.help.aria неверен (13 из 19 объявлены мёртвыми, а живы все 19) |
Раздел «(а)» переписан: явно выделен абзац «Важное динамическое семейство, которое удалять нельзя» — все 19 *.help.aria признаны живыми через производную ${key}.aria от литерального _help('*.help'); список мёртвых сокращён до 13 (7 confirm.* + 6 прочих); правило внесено в AC3 явно, а не подразумевается в «прочих семействах» |
docs/specs/406-beta2-polish.md:59-64,217-221. Перепроверено заново на текущем дереве (не наследовано): все 19 *.help.aria-ключей действительно имеют по одному литеральному _help('x.help')-потребителю, 13 заявленных мёртвых ключей — без потребителей ни в одном из 4 словарей |
(б) Medium: AC6/AC7 не решают роль для HpConfirmKind.warning |
Контракт (б) переписан: явная дихотомия снята, оба kind — destructive и warning — объявлены as alertdialog; warning назван явно как разблокировка двери с confirm.unlock_body; AC6 переформулирован на «оба вида» |
docs/specs/406-beta2-polish.md:100-105,227-230. Перепроверено: houseplan-card.ts:13041-13049 — единственный warning-запрос, ссылка точна |
Обе находки r1 закрыты по существу, не косметически. Полный повторный разбор (см. выше) вскрыл в разделе (б) отдельную, ранее не поднимавшуюся проблему феазибилити (находка №2) — она не переоткрывает находку r1, это независимый дефект того же раздела.
Унаследовано из r1
Формально пусто: ребейз на dev обесценил SHA r1, поэтому по правилу §2.10
разбор в этом раунде — полный, а не по дельте, и каждое утверждение ТЗ
перепроверено заново на текущем дереве (см. «Как проверялось»), а не принято
на основании документа r1. Структурная оценка §7.1 (все обязательные разделы
присутствуют, продуктовых вопросов владельцу не вынесено, скоуп/не-скоуп
корректны, docs/SCOPE.md подтверждает легитимность полного трека) не
поменялась между r1 и r2 и не пересчитывалась заново построчно — в остальном
результат r1 в этой части подтверждён независимым прочтением r2, а не
переносится слепо.
Находки
[Medium, в скоупе] №1 — (д) контракт описывает уже исправленный дефект и расходится с тем, что реально попало в дерево ребейзом
Файл: docs/specs/406-beta2-polish.md, раздел «(д) Затирание следа
приёмки» (строки 157-172) и AC12/AC13 (строки 246-251).
Что не так: ТЗ строит контракт (д) на утверждении «scripts/docs- accept.mjs:145 пишет declared: [...decision.replace]» безусловно, и
приводит модельный прогон: «replace=[] → в манифест уйдёт
acceptance.declared = []». На SHA r1 (8248f9e4) это было верно — сам
дефект и являлся предметом п. (д). Но ветка с тех пор перебазирована на
origin/dev, и в dev уже есть коммит 8119c523 («fix: the screenshot
witness floor comes from the set, not from survivors», issue #409, попал в
дерево этим самым ребейзом), который правит именно эту строку:
// scripts/docs-accept.mjs (текущее дерево)
const previous = JSON.parse(readFileSync(resolve(ROOT, 'docs/images/screenshots.json'), 'utf8'))
.acceptance;
const accepted = {
...manifest,
acceptance: decision.replace.length
? { declared: [...decision.replace], witnesses: decision.witnesses.length, floor: decision.floor, ... }
: { ...(previous || {}), lastWriteWasFingerprintOnly: true },
};
Commit message коммита 8119c523 прямым текстом: «Попутно Low из #405:
повторная приёмка неизменённого набора затирала acceptance.declared
пустым списком … Прежний след сохраняется и помечается
lastWriteWasFingerprintOnly.» — то есть ровно дефект (д), заведённый в
#406 как самостоятельный пункт, уже был отмечен как Low в #405 и уже
устранён в #409 до того, как ТЗ #406 дошло до ревизии 2 (ревизия 2 датирована
2026-09-01, тем же днём, что и коммит фикса, но после ребейза на dev,
согласно комментарию владельца «Ветка перебазирована… ТЗ поднято до
revision 2»).
Хуже того — исправление, которое реально попало в дерево, не совпадает
с контрактом (д) в тексте ТЗ. ТЗ требует: «Свежие числа (witnesses,
floor) обновляются, список declared … сохраняется» — то есть на пустом
replace witnesses/floor должны пересчитываться заново. Фактически
смёрженный код делает обратное: при replace.length === 0 он не
пересчитывает witnesses/floor вовсе, а замораживает весь предыдущий
acceptance-блок целиком ({...(previous || {})}) и лишь помечает его
флагом lastWriteWasFingerprintOnly — что по своему обоснованию в коммите
специально сигнализирует «эти числа не свежие, а старые от последней
настоящей проверки».
Сценарий отказа: если реализация будет буквально следовать тексту ТЗ
(«свежие числа witnesses/floor обновляются»), она отменит уже
смёрженное и обоснованное поведение lastWriteWasFingerprintOnly — то есть
пункт (д) этой же задачи внесёт регресс в код, который #409 только что
починил, и уберёт сигнал «эти цифры устарели», ради которого фикс и писался.
Если же реализация просто оставит код как есть (ничего не делая, потому что
контракт уже выполнен фактически), AC12/AC13 будут пройдены случайно, без
теста на функцию main() (docs-accept.mjs) — единственный существующий
тест, test/docs-accept.test.mjs, покрывает только verifyDocsCandidate,
а не запись acceptance в main(); test/docs-acceptance.test.mjs
покрывает только чистую функцию docsAcceptancePlan, тоже без записи файла.
То есть заявленное AC12 «доказательство: тест на функции приёмки — replace: [] оставляет прежний список» ни разу не написано и по этой ревизии ТЗ не
попадёт в план работы как отдельная задача, потому что автор считает пункт
ещё непочиненным.
Ожидаемо: раздел (д) переписан с учётом текущего состояния дерева —
либо признаётся, что дефект уже устранён коммитом 8119c523/#409 и пункт
(д) сокращается до «добавить недостающий тест на запись main() в
docs-accept.mjs, поведение не менять», либо, если владелец сознательно
хочет другое поведение (witnesses/floor обновлять даже на пустом
replace, отказавшись от lastWriteWasFingerprintOnly), это должно быть
явно названо как осознанное расхождение с #409, а не как продолжение
нарратива «дефект ещё жив».
[Medium, в скоупе] №2 — (б) контракт не называет механизм, которым HA-ветка hp-dialog получает alertdialog/aria-describedby, хотя рядом есть свидетельство, что «просто атрибут» не сработает
Файл: docs/specs/406-beta2-polish.md, раздел «(б)» (строки 107-115) и
AC6 (строки 227-230).
Что не так: контракт формулирует требование как факт, а не как
инженерную задачу: «Практически: hp-dialog получает семантический признак
alert/обычный и id описания … Обе ветки — HA и запасная — объявляют роль и
описание одинаково.» Это утверждение о поведении стороннего компонента
ha-dialog, которого нет в этом репозитории (это компонент фронтенда Home
Assistant, подключаемый в рантайме; локально существует только тестовый
стаб, demo/smoke_free_walls.mjs:20-31, — фейковый HTMLElement с
произвольным shadowRoot, написанный этим же проектом, а не поведение
настоящего ha-dialog).
В том же файле, который цитирует контракт, уже есть прецедент именно этой
проблемы: для связывания заголовка (aria-labelledby) HA-ветка не
принимает обычный HTML-атрибут — она использует специальное свойство
ha-dialog, .ariaLabelledBy (src/hp-dialog.ts:449,
.ariaLabelledBy=${this._titleId}), тогда как запасная ветка использует
обычный атрибут aria-labelledby=${this._titleId} (:462). Это разделение
не случайно: ha-dialog рендерит собственную внутреннюю структуру в своём
shadowRoot, и произвольный ARIA-атрибут, поставленный на внешний тег
<ha-dialog>, необязательно долетает до реального узла диалога внутри —
именно поэтому для заголовка потребовалось выделенное свойство, а не
атрибут.
Контракт (б) не называет для alertdialog/aria-describedby никакого
аналога .ariaLabelledBy — ни существующего свойства ha-dialog (в этом
репозитории такого свойства нигде не встречается — grep -rn "ariaDescribedBy|ariaLive|type=.\{0,5\}alert" по src/*.ts пуст), ни
явного признания, что такого свойства может не быть и нужно исследование
на этапе реализации. При этом ни один существующий смок не проверяет
роль/описание на HA-ветке вообще (demo/smoke_esc_dialogs.mjs:34 — только
на запасной dialog; ни в одном файле demo/smoke_*.mjs нет getAttribute ('role') рядом с ha-dialog), то есть у проекта пока нет ни одного
подтверждённого прецедента, что произвольный ARIA-признак на HA-ветке вообще
долетает до реального диалога в настоящем Home Assistant — только
локальный, полностью подконтрольный автору стаб.
Сценарий отказа: реализация ставит role="alertdialog" и
aria-describedby атрибутами прямо на тег <ha-dialog>, локальный смок
(тестирующий собственный же стаб) зеленеет по AC6/AC8, PR проходит ревью —
а в реальном Home Assistant скринридер по-прежнему не объявляет
alertdialog, потому что настоящий ha-dialog эти атрибуты на внешнем
теге игнорирует так же, как проигнорировал бы aria-labelledby, если бы
для него не завели .ariaLabelledBy. Ровно тот класс регрессии, который
локальный тест из-за природы стаба не способен поймать.
Ожидаемо: контракт (б) явно называет способ донести
alert-семантику/описание до реального ha-dialog (существующее свойство,
если оно есть — тогда назвать его, как назван .ariaLabelledBy), либо
явно помечает это как открытый технический вопрос реализации с планом
проверки на реальной Home Assistant (не только на локальном стабе), а не
формулирует как решённый факт «обе ветки объявляют одинаково».
Что проверено и корректно
- (а) Числа сходятся на текущем дереве независимо от r1: 1201 ключ во
всех четырёх словарях (
en/ru/de/fr), все 13 заявленных мёртвых ключей (7confirm.*+marker.display_hint(_icon),history. delete_room,markup.delete,title.markup,history.partition_add) без единого литерального потребителя ни в одном из четырёх словарей; все 19*.help.ariaдействительно производятся из литеральных_help('*.help')на строкеhouseplan-editor-runtime.ts:1194, номер строки точен. Заявленные динамические семейства AC3 (furn.cat_*,furn.sym_*,wall_model.reason.*,resize.disabled.*,junction.limit_*,decor.*) реально существуют как шаблонные литералы вhouseplan-editor-runtime.ts/houseplan-card.ts.52 = 13×4верно. Коллизия сtest/unified-wall-tool-source.test.mjs:32подтверждена дословно на текущей строке. - (б) дихотомия destructive/warning — подтверждена: ровно 7 мест
kind: 'destructive'(все — удаления,houseplan-editor-runtime.tsиhouseplan-onboarding-runtime.ts) и ровно одноkind: 'warning'(houseplan-card.ts:13041-13049, разблокировка,confirm.unlock_body).hp-confirm.tsна текущем дереве действительно не передаётhp-dialogникакого alert-признака — контракт описывает реальный, ещё не закрытый разрыв, только механизм его закрытия для HA-ветки не назван (находка №2). - (в) непокрытая HA-ветка — подтверждена без изменений от r1:
smoke_free_walls.mjs— единственный файл сha-dialog-стабом и глушит_confirmDanger;smoke_danger_confirmation.mjs/smoke_danger_ confirm_branches.mjsсловаha-dialogне содержат. Файлdemo/smoke_ danger_confirm_branches.mjsдействительно существует (создан #402, подтверждено содержимым) — план расширить именно его реалистичен. - (г) утечка снапшота и порядок обрезки — файл
device-area- relocation.tsне менялся с исходного коммита #126 (633cb20e, до ребейза), поэтому не задет им; перечитан целиком заново, а не наследован.resolveDeviceAreaRelocationsдействительно строитdecisionsтолько из цикла поoptions.devices(строка 155) — устройство, отсутствующее в этом списке целиком, не порождает decision и, соответственно,applyAreaRelocationResolution(строка 239) его запись не тронет никогда. Условие!options.authoritative(строка 142) — раннийreturnс пустым результатом, соответствует контракту «не трогать при неавторитетном реестре»..slice(0, MARKER_AREA_SNAPSHOT_LIMIT)(строка 68,MARKER_AREA_SNAPSHOT_LIMIT = 20_000, строка 9) читает по порядку вставки Object.entries — обрезка действительно режет самые свежие записи; разворот правила (AC11) реализуем без переписывания структуры.removeMarkerAreaSnapshots(строки 8326, 8448houseplan-editor- runtime.ts) — номера строк точны. - UX/терминология —
docs/USER-GUIDE.ru.mdуже описывает подтверждение разблокировки (строки 729-730) и удаления (672-673, 824-847) тем же языком, который использует ТЗ; новых терминов не введено.docs/SCOPE.md, раздел «Partially covered»: «Accessibility: … no ARIA labelling of the plan» — пункты (б)/(в) закрывают именно эту декларативно признанную продуктом брешь, скоуп легитимен. - Скоуп/не-скоуп, отсутствие технических вопросов владельцу — как и в r1, границы очерчены явно, пересечения с #403/#405 разведены по разным контрактам одного файла; ни одного технического вопроса не вынесено владельцу.
Чего не проверял
- Гейты (
tsc/test/build,check-docs.mjs) — не гонял: диапазонorigin/dev...HEADсодержит только два.md-файла, продуктового кода ещё нет. То же основание, что в r1. - Не пересчитывал вручную полный список «48 динамических семейств» и
«1188 использовано / 13 не используется» по всем 1201 ключам — точечно
перепроверил категории, названные в тексте (
confirm.*,*.help.aria, единичные ключи, шесть заявленных семейств AC3); этого достаточно, чтобы подтвердить или опровергнуть числа в тексте, исчерпывающий пересчёт — задача самого гейта (AC1), а не ревью ТЗ. - Не проверял (унаследовано из r1 без изменений, дельта его не касается):
останется ли запись
marker_area_snapshotживой, если устройство пропало из_devices, но по тому же id ещё существует маркер — открытый вопрос r1 остаётся открытым, контракт (г) не переформулирован в этой ревизии, граница AC9 та же, что и была. - Не проверял поведение настоящего
ha-dialogв реальной Home Assistant (нет доступа к его исходнику из этого репозитория) — отсюда и находка №2: это ровно то, что нельзя подтвердить чтением этого репозитория, и спецификация обязана либо назвать механизм, либо явно пометить как неисследованное. - Не проверял
scripts/mutation-gate.mjsна предмет технической реализуемости трёх заявленных мутантов — на этапе ТЗ это описание намерения, а не код.
Вердикт
Вердикт: жёлтый · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 2 → в задаче