31 KiB
SPEC-REVIEW-660-r2
- Issue: https://github.com/Matysh/houseplan-card/issues/660
- Этап:
S4-spec-review(ревью ТЗ, PROCESS.md §2.4) - Трек: полный (не пересматривался в r2, обоснование не менялось — см. «Унаследовано из r1»)
- Материал: тело issue #660 в редакции r2 (
updated_at: 2026-09-26T07:25:23Z,bodyдлиной 16919 символов) — исходная постановка владельца + раздел## ТЗ, добавленный автором после SPEC-REVIEW-660-r1 (комментарий «ТЗ переработано после SPEC-REVIEW-660-r1»,2026-09-26T07:25:21Z); таймлайн меток: вердикт r1 (07:22:39) →S4-spec-reviewснята,S3-specвыставлена (07:23:18) → комментарий автора о переработке (07:25:21) →S3-specснята,S4-spec-reviewвыставлена (07:25:23). Отдельного событияeditedAPI timeline не отдаёт (не отдавал и для r1) — тело сверено напрямую: в r1 раздела## ТЗне было ни строки (см. SPEC-REVIEW-660-r1), сейчас он присутствует целиком. - Заход: r2 · блокирующих циклов израсходовано 1 из 4 (полный трек, лимит 4)
- Роль: ревьюер ТЗ (не автор)
Скоуп ревью
Задача не изменилась с r1: полировка шапки главной панели после #647 — (1)
inline-кнопки сводной панели остаются активными во всех трёх редакторах на
ширинах >480 px; (2) расстояние между блоком редакторов и блоком зума уменьшается
вдвое; (3) резерв под крестик закрытия переезжает внутрь общей подложки блока
редакторов; (4) Esc получает единый терминальный выход в View из Plan и Devices.
Проверялось: закрыты ли три находки SPEC-REVIEW-660-r1 (High-1 — отсутствие
раздела ## ТЗ; High-2 — противоречие с docs/UX-MODES.md; Medium-1 — отсутствие
допуска/целевых значений расстояния); полнота обязательных разделов §7.1 нового
текста; однозначность и проверяемость каждого AC вместе со способом доказательства
(§2.5 DoR); техническая реализуемость новых пунктов Скоупа по коду «как есть»,
а не только по формулировке; согласие с docs/SCOPE.md и каноном подсистемы.
Дельта не локальна. r1 установил, что раздел ## ТЗ отсутствовал целиком; r2
дописывает его впервые полностью — объём новой дельты сопоставим с самой задачей
(PROCESS.md §2.10: «разбор остаётся полным, если... объём сопоставим с задачей»).
Поэтому ниже — полный разбор нового раздела ## ТЗ, а не только сверка трёх
находок r1 построчно (хотя она тоже сделана — см. «Закрытие раунда r1»).
Как проверялось
- Прочитаны PROCESS.md §2.4, §2.5, §2.10, §4, §7.1, §7.2,
docs/process/REVIEWER.mdцеликом. - Прочитано текущее тело issue #660 целиком (
gh issue view 660 --json body, доступ к MCPget_issue/get_issue_commentsбыл запрещён средой — использованghCLI напрямую) и все 4 комментария (gh issue view 660 --comments): «Аналитика», ответ владельца Q1/Q2, вердикт r1, финальный комментарий автора «ТЗ переработано после SPEC-REVIEW-660-r1». - Сверена история меток issue (
gh api .../timeline) — порядок событий red-вердикт →S3-spec→ комментарий автора →S4-spec-reviewподтверждён (см. «Материал» выше). - Прочитан
docs/reviews/SPEC-REVIEW-660-r1.mdцеликом — три находки (High-1, High-2, Medium-1) сверены построчно с новым текстом (раздел «Закрытие раунда r1» ниже). - Прочитан
docs/UX-MODES.mdцеликом (строки 1-140) — сверены обе цитаты, которые High-2 называл противоречащими (57-61, 116-125), и новый текст AC10 / Скоуп п.7, который их должен заменить. - Прочитан код, который новый раздел
## ТЗописывает как основу реализации:src/houseplan-card.ts:10805-10824— текущая разметка.modes/.editor-close-slot(сосед.modes, не потомок) /.spacer/.zoomctl;src/houseplan-card.ts:2825-2995(_onKey) — три веткиEsc(Decor уже терминальна, Devices и Plan — нет), подтверждает feasibility Скоуп п.6 и AC7/AC8;src/summary-panel-runtime-loaded.ts:190-286(renderControls,menuItems) — подтверждает, что раздел.summary-controlуже скрывается CSS-ом, а не JS-гвардом по ширине (см. находку ниже — это единственное техническое место, где новый текст ТЗ разошёлся с реальной архитектурой);src/header-menu.tsцеликом — составheaderMenuItems(), подтверждает Q2;src/houseplan-editor-runtime.ts:916-946(_setMode) — повторный клик по активному пункту меню редактора не переключает в View (if (this.host._mode === mode) { ...; return; }) — проверка того, что на телефоне единственный путь выйти из Plan/Devices без физической клавишиEsc— крестик в шапке;src/styles/chrome.styles.ts:150-262, 280-282— все media-запросы, участвующие в шапке (max-width: 480px,max-width: 720px), включая правило.head > .zoomctl, .head > .editor-close-slot { flex: none }.
- Прочитан
docs/USER-GUIDE.ru.md:247-290(«Шапка на телефоне», «Сводная панель») иdocs/USER-GUIDE.md:225-260(тот же раздел на английском) — Release-артефакты ТЗ требуют явно проверить эти файлы на предмет описаний старой позиции крестика. - Проверено i18n:
title.close_editorсуществует вsrc/i18n/{en,ru}.json:585без изменений — заявление «новых строк нет» подтверждено. - Проверено, что продуктового кода для #660 в диапазоне
origin/dev...HEADнет (git diff origin/dev...HEAD --statпуст) — гейты сознательно не гонялись, этапspec.
Находки
High-1 (в скоупе). Скоуп п.3 переносит крестик внутрь .modes, но .modes
скрыт (display:none) на ширинах ≤480 px — перенос убирает крестик из шапки
телефона, где он сейчас виден независимо от .modes, и делает недоказуемым
собственный AC9, а на телефоне без физической клавиатуры — блокирует выход из
Plan/Devices вообще
Файл: тело issue #660, раздел ## ТЗ → Скоуп п.3 и AC4/AC9;
src/styles/chrome.styles.ts:150-153 (@media (max-width: 480px) { ... .head > .title, .head > .modes, .head > .spacer, .head > .header-action, .head > .summary-control { display: none; } ... }) и :166 (.head > .zoomctl, .head > .editor-close-slot { flex: none; } — обратите внимание, это отдельное от
.modes правило, .editor-close-slot в него не входит и остаётся видимым);
src/houseplan-card.ts:10812-10824 (текущая разметка: .editor-close-slot —
</div>-сосед .modes, а не её потомок); docs/USER-GUIDE.ru.md:277 («В
редакторе крестик закрытия остаётся в строке» — именно про телефонную шапку,
раздел «Шапка на телефоне»); docs/UX-MODES.md:17-20 («space tabs..., the zoom
cluster, one gear and — for an admin — the editor X slot of #647» — крестик
перечислен как отдельный от сегментированного контрола элемент шапки телефона,
которого на телефоне вообще нет, см. строки 31-38: сегментированный контрол —
desktop-виджет).
Что не так. На ширине ≤480 px CSS-медиазапрос прячет .modes целиком
(display: none), но НЕ прячет .editor-close-slot — это два разных селектора,
.editor-close-slot сегодня физический сосед .modes в разметке, поэтому его
видимость не зависит от видимости .modes. Это и есть тот механизм, которым
сегодня работает описанное в docs/USER-GUIDE.ru.md:277 поведение: «в редакторе
крестик закрытия остаётся в строке» на телефоне, хотя сама сегментированная
группа режимов на телефоне вообще не рендерится в этом смысле (телефонная шапка
— вкладки пространств, зум, шестерёнка и отдельно крестик, по
docs/UX-MODES.md:17-20; сами редакторы там открываются из меню шестерёнки, а не
через .modetab).
Скоуп п.3 без каких-либо оговорок по ширине требует: «Переместить единственный
фиксированный 24 px слот крестика внутрь .modes». Если слот становится
потомком .modes, он подчиняется тому же display: none на ≤480 px и исчезает
из телефонной шапки целиком — CSS не даёt способа выборочно показать потомка
элемента с display: none на родителе. Это не гипотеза: правило .modes не
запрещает read её как «ничего в её пределах не занимает места и не видно»,
таково поведение display: none в CSS без исключений для потомков.
Последствия по коду, не по домыслу:
- AC9 недоказуем как написан. AC9 буквально требует: «на 390/481/620 px нет ... потери доступа к закрытию редактора». 390 px — это ширина внутри телефонного брейкпоинта (≤480). План автотестов обещает именно такую проверку («mobile/layout smoke проверяет ... доступность zoom/menu/close на 390/481/620»). При буквальной реализации Скоуп п.3 эта проверка на 390 px обязана упасть — не потому, что смок неверный, а потому что доступ к закрытию в этой точке ширины реально теряется.
- Это не «менее удобно», а тупик для touch-only админа. Проверено
src/houseplan-editor-runtime.ts:916-935: повторный клик по тому же пункту меню («Редактор плана» и т.п.), когда он уже активен, — no-op (if (this.host._mode === mode) { ...; return; }, соответствует «re-clicking the active tab does nothing» изdocs/UX-MODES.md:49). ВheaderMenuItems()(src/header-menu.ts:62-77) нет отдельного пункта «Вернуться в просмотр».Esc— клавиатурное событие, недоступное чисто touch-пользователю телефона. Значит сегодня единственный способ выйти из Plan/Devices на телефоне — тап по.editor-close-slotв шапке. Если он пропадает вместе с.modes, администратор с телефоном без клавиатуры физически не может закрыть Plan или Devices после входа в них — полная потеря функциональности, не деградация. - Ничего в ТЗ этого не решает. «Не-скоуп» явно исключает «Изменение
мобильного меню» — но восстановление видимости крестика на ≤480 px требует
трогать CSS-структуру телефонной шапки (сам медиазапрос
.modes/родственные правила), а это прямо соседствует с исключённой поверхностью и нигде не выделено как разрешённое исключение. «Принято предположительно» перечисляет технические детали для desktop-раскладки (один DOM-слот, CSS-переменная размера, spacer), но не содержит ни слова про сохранение видимости слота на ≤480 px. «Риски» называют overflow на узких ширинах как риск, но не называют риск потери доступа к закрытию — хотя для него уже есть готовая AC (AC9), которая от этого и должна защищать.
Это не редакционная придирка: пункт Скоупа буквально противоречит собственному AC
того же документа и документированному поведению docs/USER-GUIDE.ru.md, и
разрешение противоречия («крестик остаётся отдельным элементом на ≤480 px» либо
«медиазапрос .modes переписывается, чтобы прятать только .modetab, но не
вложенный крестик») — продуктово наблюдаемая деталь (это ровно то, что видит
пользователь телефона), а не техническая, которую агенты «решают сами» по §7.1.
Как закрыть. В Скоуп п.3 явно оговорить поведение на ≤480 px: либо крестик
остаётся физическим соседом .modes в разметке на этой ширине (тогда
формулировка «переместить... внутрь .modes» ограничивается шириной >480 px и
это должно быть написано прямо), либо медиазапрос телефонной шапки переписывается
так, чтобы прятать только .modetab-кнопки, сохраняя видимость и позицию
крестика, — в этом случае это нужно явно внести в Скоуп и снять как исключение
из «Не-скоуп: изменение мобильного меню» (шапка ≠ меню шестерёнки, но
формулировка сейчас этого не разделяет). Добавить в AC9 или отдельным AC явное
требование: на ≤480 px в открытом редакторе крестик остаётся видимым, кликабельным
и в неизменной позиции шапки, как до #660. Указать способ доказательства (тот же
smoke_mobile_view_header.mjs, расширенный сценарием «admin входит в Plan/Devices
на 390 px, крестик виден и кликабелен»).
Что проверено и корректно
- High-1 из r1 закрыт содержательно. Раздел
## ТЗтеперь содержит все обязательные части §7.1: Сценарий, Что человек увидит до/после (явная пара), Проблема по коду (с конкретными строками/методами), Скоуп, Не-скоуп, Контракт поведения (AC1…AC10), UX/i18n/модель данных, таблица «AC · чем доказан · чем краснеет» — все 10 строк заполнены с конкретным механизмом и мутантом, План автотестов со ссылками на существующие файлы (demo/smoke_editor_tabs.mjs,demo/smoke_mobile_view_header.mjs,scripts/mutation-registry.mjs), Риски, Откат, Release-артефакты, «Принято предположительно». Структурно соответствует образцу #647. - High-2 из r1 закрыт содержательно. Скоуп п.7, AC10 и Release-артефакты
прямо называют обе фразы
docs/UX-MODES.md, которые нужно заменить («outside the editors», «all summary surfaces disappear»), без домысливания формулировки замены — только контракт, который она обязана передать. - Medium-1 из r1 закрыт содержательно. AC3 задаёт измеримые целевые значения (50 ±1 px на 768/1000/1200/1400, 40.5 ±1 px на 481/620) вместо «вдвое без чисел»; допуск назван явно, в духе AC соседних задач.
- Архитектурная состоятельность AC1/AC2 (desktop/mobile split summary
controls) подтверждена по коду. Вопреки формулировке «Проблема по коду»
(«...хотя desktop-вызов уже остаётся в шапке»), проверка
renderControls()(src/summary-panel-runtime-loaded.ts:262-263) показала: вызов кнопок в шапке безусловен по ширине уже сегодня, а деление desktop/mobile целиком на стороне CSS-класса.summary-control(chrome.styles.ts:152-153), не JS. Значит для AC1 достаточно снять guardhost._mode !== 'view'вrenderControls()— никакой новой width-detection логики не требуется, техническая реализуемость этой части ТЗ выше, чем предполагает её собственный текст. - AC7/AC8 (терминальный
Esc) реализуемы буквально как описано. Три ветки_onKey(src/houseplan-card.ts:2825-2995) действительно последовательноreturn-ят после каждой внутренней отмены; Decor уже заканчивается_setMode('view'); добавление того же терминального шага в конце веток Devices и Plan механически укладывается в существующую структуру, двойного действия по архитектуре не возникает. - i18n-заявление подтверждено.
title.close_editorсуществует вsrc/i18n/en.json:585иsrc/i18n/ru.json:585без изменений, новых ключей ТЗ не заявляет и не требует. - Q1/Q2 не переинтерпретированы. Текст «Принятые решения владельца» дословно совпадает с ответами владельца из комментария (сверено посимвольно), Скоуп п.1 и п.2 отражают именно Default/Альтернативу, без расширения.
- Track/трек не пересматривался и не должен был — обоснование r1 (два нарушенных критерия §5: несколько поверхностей + новый UX-контракт) относится к неизменной части текста.
Чего не проверял
- Гейты (
tsc,npm test,npm run build,check-docs.mjs, смоки, golden, инварианты) — не прогонял: этапspec, продуктового кода для #660 вorigin/dev...HEADнет (git diffпуст). Предмет код-ревью после реализации. - Не измерял независимо 100/81 px baseline или новые 50/40.5 px в браузере — как и в r1, принял измерения «Аналитики» на веру; это станет предметом код-ревью, когда появится браузерный смок.
- Не проверял, существует ли уже
scripts/mutation-registry.mjsс местом под новые мутанты #660 — план автотестов только обещает их добавить, это нормально для стадииspec(мутанты появляются в реализации). - Не проверял английский
docs/USER-GUIDE.mdна предмет отсутствующего раздела «Phone header» (в отличие отdocs/USER-GUIDE.ru.md:265-278, английская версия такого подраздела не содержит вовсе) — это существующее расхождение RU/EN-гайдов, не создано и не усугублено #660; не поднимаю отдельной находкой, но автору стоит иметь это в виду при правке Release-артефактов: обновлять нечего в EN-версии по этому месту не потому что она «уже верна», а потому что там нет соответствующего текста. - Не проверял production-числа анимации/transition сдвига кнопок при появлении
крестика (мгновенно или с анимацией) — текст ТЗ явно это не фиксирует, но AC5
(«соседние контролы не дёргаются») и общий контракт переходов в
docs/UX-MODES.md:47-55(короткий fade + интерполяция высоты) дают достаточный ориентир для «Принято предположительно»; не считаю это открытым вопросом, требующим отдельной находки. - Не проверял ширины
docs/TOUCH-SUPPORT.mdподробнее констатации, что редакторы остаются desktop-first/best-effort — вне скоупа этой задачи, как и в r1.
Закрытие раунда r1
| Находка (r1) | Чем закрыта | Где это видно |
|---|---|---|
High-1: раздел ## ТЗ отсутствовал целиком |
Добавлен полный раздел ## ТЗ со всеми обязательными подразделами §7.1 (Сценарий, До/После, Проблема, Скоуп/Не-скоуп, AC1…AC10 с доказательством, UX/i18n/данные, План автотестов, Риски, Откат, Release-артефакты, «Принято предположительно») |
тело issue #660, после ---, начиная с ## ТЗ |
High-2: противоречие с docs/UX-MODES.md, апдейт не запланирован |
Скоуп п.7, AC10 и Release-артефакты прямо называют обе цитаты канона на замену | тело issue #660, разделы «Скоуп» п.7, «AC10», «Release-артефакты» |
| Medium-1: нет допуска/целевых значений расстояния | AC3 задаёт 50 ±1 px (768–1400) и 40.5 ±1 px (481–620) | тело issue #660, «AC3. Gap» |
Все три находки r1 закрыты содержательно, без формальных отписок. Новый High-1 этого раунда — независимая находка на техническом стыке, который стал виден только после того, как появился текст для проверки (в r1 разбирать было нечего — раздела не было).
Унаследовано из r1 (без повторной проверки в этом раунде)
- Отказ от лёгкого трека (
small) и выбор полного трека. Обоснование — задействовано более одной поверхности и вводится новый UX-контракт — текст не менялся между r1 и r2; принято по SPEC-REVIEW-660-r1, раздел «Что проверено и корректно», без повторного анализа критериев §5 в этом раунде. - Продуктовая правомерность вопросов Q1/Q2 (оба — что видит/делает пользователь, оба с default-вариантом) — установлено в SPEC-REVIEW-660-r1, текст вопросов и ответов не редактировался, сверен только на дословное совпадение с новым «Принятые решения владельца» (см. «Что проверено и корректно» выше) — само правомерность формулировки вопросов заново не пересматривалась.
- Утверждения кода из комментария «Аналитика» (
renderControls,header-menu.tsguard,.editor-close-slotsibling, три ветки_onKey) были построчно проверены в r1 (docs/reviews/SPEC-REVIEW-660-r1.md, материал268f8f32087b) — в этом раунде они перепроверены заново (см. «Как проверялось» пп. 6-7), поскольку дельта признана нелокальной; формально это не «наследование», а независимое повторное подтверждение с тем же выводом.
Вердикт
Найден 1 High, в скоупе задачи. Все три находки SPEC-REVIEW-660-r1 закрыты
содержательно — структура и полнота раздела ## ТЗ больше не блокируют. Но
полный разбор впервые появившегося текста вскрыл новое, самостоятельное
противоречие: Скоуп п.3 (переместить крестик внутрь .modes) без оговорки по
ширине ломает видимость крестика на ≤480 px, потому что .modes скрыт
display: none в этом диапазоне, а .editor-close-slot сегодня — её сосед, а не
потомок. Это напрямую противоречит документированному поведению телефонной шапки
(docs/UX-MODES.md:17-20, docs/USER-GUIDE.ru.md:277) и делает недоказуемым
собственный AC9 задачи; для touch-only администратора на телефоне это не
деградация, а полная потеря способа выйти из Plan/Devices (проверено по коду:
_setMode не переключает обратно в View повторным кликом по активному пункту
меню, отдельного пункта «Просмотр» в меню нет, Esc недоступен без клавиатуры).
Разрешение — продуктово наблюдаемая деталь (что видит и может сделать
пользователь телефона), не техническая, которую можно решить между агентами по
умолчанию.
Красный вердикт: ТЗ возвращается автору на доработку Скоуп п.3 (и, при необходимости, AC9/Не-скоупа) с явной оговоркой поведения крестика на ≤480 px. Отдельные issue не заводятся — находка в скоупе.
Вердикт: красный · заход r2 · блокирующих циклов 2/4 · High: 1 · Medium: 0 → в задаче
Материал раунда
- Ветка:
dev, коммит4b8a82c08a7e— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
7cc2ff9fb72ff2627617fd7c86d240207b8f042fgit log --all --format='%H %T' | grep 7cc2ff9fb72f - Тело issue:
d20b5c1fa1ed9fe13ee58b29a71d6de650976a7ac810c2567b11fef1e83c42f0 - Вердикт конвейера:
red· High 1