diff --git a/docs/reviews/CODE-REVIEW-648-r1.md b/docs/reviews/CODE-REVIEW-648-r1.md new file mode 100644 index 00000000..0df6286c --- /dev/null +++ b/docs/reviews/CODE-REVIEW-648-r1.md @@ -0,0 +1,359 @@ +# CODE-REVIEW-648-r1 + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/648 — + «Sections: вертикальный resize карточки и container-owned высота плана» +- **Этап:** `S7-code-review` (код-ревью, PROCESS.md §2.7) +- **Трек:** полный (`P2`, см. SPEC-REVIEW-648-r1.md) +- **Материал:** `git log --oneline origin/dev..HEAD` и + `git diff origin/dev...HEAD`; вершина ветки — + `b7fb7cf7e7bce13a44ef0e1af3b97541ddb0dffa`, база — `origin/dev` = + `f68878cc44b818b6cfbdb3866302e6febaab2445`. Три коммита: + `7f48e690` (feat, User-Visible: yes), `102b2ed7` (test, User-Visible: no), + `b7fb7cf7` (test, User-Visible: no). +- **Заход:** r1 · блокирующих циклов израсходовано 0 из 4 (первый раунд + код-ревью; предыдущий раунд — ревью ТЗ, `docs/reviews/SPEC-REVIEW-648-r1.md`, + зелёный) +- **Роль:** ревьюер кода (не автор) + +## Скоуп ревью + +Диапазон меняет: + +- `src/houseplan-card.ts` — новое публичное реактивное свойство `layout` + (HA-сигнал), геттер `_containerOwnedHeight` (`panelHost || layout === + 'grid'`), `getGridOptions()` → `{ columns: 'full', rows: 10, min_rows: 6 }`, + замена `panelHost` на `_containerOwnedHeight` в четырёх местах расчёта + высоты сцены (mode-transition target, `measuredCardHeaderHeight`, + `settleSoftStageLayout`, inline `style="height:..."`); +- `src/boot-soft-layout.ts` — переименование параметра `panelHost` → + `containerOwnedHeight` в `measuredCardHeaderHeight`/`settleSoftStageLayout` + (поведение не меняется, кроме нового источника `true`); +- `src/styles/base.styles.ts` — CSS-селекторы `:host([layout="grid"])` + добавлены рядом с существующими `:host([panel-host])`; +- `demo/smoke_sections_resize.mjs` (новый, 221 строка) — browser smoke; + `demo/smoke_houseplan_panel.mjs` — соседний regression-oracle (AC7); + `test/houseplan-panel.test.mjs` — обновлён текстовый oracle контракта #486; +- `scripts/mutation-registry.mjs` (4 новых мутанта), `scripts/smoke-links.mjs` + (регистрация smoke по символам); +- документация: `docs/ARCHITECTURE.md`, `docs/CHANGELOG.md`, + `docs/CHANGELOG.ru.md`, `docs/STATUS-FEATURES.md`, `docs/TESTING.md`, + `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`; +- `dist/**`, `custom_components/houseplan/frontend/**` (класс D, пересобранный + бандл — сверено побайтно, см. «Как проверялось» п.2). + +Первый вопрос — какую строку `docs/SCOPE.md` задача обслуживает: прямого +Core-job нет, это инфраструктурная UX-правка поверхности, на которой живут +J1–J7 для персоны Home admin (Sections — основной тип dashboard в HA, +задача не расширяет функциональность плана и не входит в «Out of scope»). +Ровно так же было принято ревьюером ТЗ в SPEC-REVIEW-648-r1.md — конфликта +со SCOPE.md нет. + +## Как проверялось + +1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `docs/process/REVIEWER.md`, + тело issue #648 (раздел `## ТЗ`, K1–K12, AC1–AC10), и + `docs/reviews/SPEC-REVIEW-648-r1.md` (единственный предыдущий раунд — + ревью ТЗ, зелёный, без Medium/High). +2. Прочитан полный `git diff origin/dev...HEAD` по каждому изменённому + файлу `src/**`, `demo/smoke_sections_resize.mjs`, + `demo/smoke_houseplan_panel.mjs`, `test/houseplan-panel.test.mjs`, + `scripts/mutation-registry.mjs`, `scripts/smoke-links.mjs`, всех + изменённых `docs/*.md`. +3. Пересобрал бандл (`npm run build` → `npm run bundle:sync`) на материале — + `git status` после сборки чист: committed `dist/**` и + `custom_components/houseplan/frontend/**` побайтно совпадают с + пересобранными, обновлять было нечего. +4. Прогнаны гейты (список — «Что прогнал» ниже); дешёвые (`typecheck` + внутри `build`, `npm test`, `npm run build`) уже подтверждены зелёным + Validate на этом точном SHA (см. ссылку в задании), их не переганивал; + `check-docs.mjs`, `no-new-any.mjs`, `no-new-private-writes.mjs`, + `bundle:budget` прогнал сам, так как диф трогает `src/**`, а инструкция + не считает их покрытыми чужим прогоном за меня. +5. Прогнал сам `smoke_sections_resize.mjs` и `smoke_houseplan_panel.mjs` + (браузерные смоки Validate не гоняет вне `heavy`-условий — они не в + материале, который мне назвали зелёным). Оба зелёные. +6. Через `node scripts/mutation-registry.mjs --id=` прогнал все четыре + новых мутанта задачи (`sections-grid-falls-back-to-viewport-height`, + `sections-editor-transition-uses-window-height`, + `sections-grid-drops-stage-flex-chain`, + `sections-resize-drops-stage-refit-observer`) — каждый краснит именно + `smoke_sections_resize.mjs`, репозиторий чист после каждого прогона + (скрипт сам возвращает мутант и пересобирает). Это заполняет колонку + «чем краснеет» для защитных AC2/AC4/AC5/AC7/AC8 предметно, а не по + заявлению автора. +7. Прогнал `node scripts/smoke-select.mjs --base f68878cc4... --head + b7fb7cf7e...` — «прямое совпадение» вернуло 19 смоков, включая оба + названных в AC (`smoke_sections_resize.mjs`, `smoke_houseplan_panel.mjs`) + и ряд смоков, делящих символы `_hdrH`/`_bootSoft`/`_stageEl`/`_warmSlot` + с изменённым `boot-soft-layout.ts`. Из них выбрал и прогнал + `smoke_render_invalidation.mjs` (единственный, читающий `_bootSoft` + напрямую — сигнатура `settleSoftStageLayout` изменилась) и + `smoke_warm_dialogs.mjs` (`_hdrH`+`_warmSlot`, соседняя soft-layout + цепочка) как точечную regression-проверку рефактора параметра + `panelHost → containerOwnedHeight`; оба зелёные. Остальные 15 прямых + совпадений и 28 слабых связей не гонял — сочтено непропорциональным для + рефактора, который для существующих layout (`layout` не установлен) + математически не меняет значение `containerOwnedHeight` (было + `panelHost`, осталось `panelHost || false`), см. «Чего не проверял». +8. Экспериментально проверил (не для отчёта CI, а для собственной + уверенности): вручную заменил `language: 'ru'` на `'de'` в фикстуре + `smoke_sections_resize.mjs`, прогнал — все проверки, включая + `minimumSixRowsStayContained`, прошли и на немецкой локали; откатил + правку (`git status` после отката чист). Это ограничивает находку + Medium-1 ниже: код сейчас не ломается на German, но CI никогда не + узнает, если сломается. +9. Проверил трейлеры всех трёх коммитов (`git log`) — `Issue: #648` на + каждом, `User-Visible: yes` только на `7f48e690`, и оба changelog входят + в этот же коммит (`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md` — видно в + `git show --stat 7f48e690`). +10. Сверил термины режимов (Plan/Devices/Backdrop → «План»/«Устройства»/ + «Подложка») со смоком, использующим внутренние имена `plan`/`devices`/ + `decor` — совпадает с существующим соответствием в `docs/USER-GUIDE.ru.md` + (уже проверено ревьюером ТЗ). +11. Проверил, что `houseplan-space-card` (`src/space-card.ts`) не получил + ни `getGridOptions`, ни ссылок на новое свойство `layout` (grep) — + юнит-тест это же утверждает через `assert.doesNotMatch`. +12. Проверил «одно число — один источник» (§8): 10/6 фигурируют только как + возврат `getGridOptions()` (единственный источник) и как проза в + changelog/USER-GUIDE/ARCHITECTURE/STATUS-FEATURES/TESTING, дословно + совпадающая с этим возвратом; отдельного хардкода этих чисел или + пиксельных производных (≈632/≈376 из K2) в `src/**` или изменённых + `docs/*.md` не нашёл (grep). + +## Находки + +### Medium-1 (в скоупе задачи). AC3 документально обещает browser smoke +«RU/DE», а поставленный smoke проверяет минимальную высоту только на +русской локали + +**Файл:** `demo/smoke_sections_resize.mjs:64-101` (секция `minimumGeometry`), +конфиг `card.setConfig(config)` строка 72. + +**Что не так.** Таблица AC issue #648 для AC3 называет доказательство +буквально: «Browser smoke RU/DE, geometry + scroll oracle» — для +наблюдаемого результата «На минимальных 6 строках View при узкой ширине и +**длинной локали** не выходит за slot; header доступен...». Раздел i18n +того же ТЗ поясняет зачем: «Существующие RU/DE длинные подписи входят в +визуальную проверку минимальной высоты» — RU и DE выбраны вместе не +случайно: `docs/TESTING.md:484-485` фиксирует немецкую локаль как +установленный в проекте эталон «длинных подписей», ломающих узкую/минимальную +раскладку («German View... fit at desktop and 390 px without horizontal +overflow or clipped actions»). + +Фактически `card.setConfig(config)` в `demo/smoke_sections_resize.mjs:72` +задаёт `language: 'ru'` один раз на весь смок — включая блок +`minimumGeometry` (строки 91–101), который единственный проверяет минимум +6 строк на узкой ширине 390 px. Немецкая локаль нигде в файле не +упоминается (`grep -n "language\|'de'" demo/smoke_sections_resize.mjs` не +находит `de`). Половина заявленного в AC3 покрытия не существует как +исполняемый тест. + +**Почему это находка, а не блокер существующей защиты.** Я не нашёл живого +дефекта: ручной эксперимент (заменил `'ru'` на `'de'` в этом же файле, +прогнал, откатил) показал, что на текущем коде `minimumSixRowsStayContained` +и `headerContained`/`menuReachable` проходят и на немецкой локали. Это не +регрессия сегодняшнего дня. + +**Сценарий отказа.** Ближайшее изменение хедера/меню (например, более +длинная подпись пункта меню, добавленная в рамках другой задачи) может +сузить запас по немецкой локали на 390 px/6 строках, ничего не сломав по +русской. Ни один автотест в этом дифе и ни один существующий golden/сценарий +(проверено — `docs/golden`, `demo/docs` не содержат Sections/grid-сценария) +это не заметит: `smoke_sections_resize.mjs` — единственный исполняемый +свидетель AC3, и он проверяет только RU. Заявленная в согласованном ТЗ +защита «длинная локаль не переполняет минимальный slot» наполовину не +существует как код. + +**Почему в скоупе и не блокирует зелёный.** Правка дешёвая — обернуть +существующий блок `minimumGeometry` (или отдельно вызвать `setConfig` с +`language: 'de'`) в цикл по `['ru', 'de']` и агрегировать оба результата в +`minimumSixRowsStayContained`; список поверхностей и файлов не меняется, не +требует новой инфраструктуры. Без High-находок это делает вердикт жёлтым, а +не переоткрывает задачу отдельным issue (#202). + +## Что проверено и корректно + +- **AC1** (grid defaults + `getCardSize`): `getGridOptions()` возвращает + ровно `{ columns: 'full', rows: 10, min_rows: 6 }` без `max_rows` + (`src/houseplan-card.ts:3762`); подтверждено исполнением обоих браузерных + смоков (`executableGridDefaults`, `sectionsDefaultsRemainAvailableInPanel`) + и юнитом, не читающим монолит как единственное доказательство (юнит — + дополнение к исполняемому свидетелю, не замена). `space-card.ts` не имеет + `getGridOptions` — подтверждено grep и юнитом. +- **AC2/AC3** (контейнерная высота, минимум 6 строк на 390 px): смок + `smoke_sections_resize.mjs` реально измеряет `getBoundingClientRect()` + `host`/`card`/`ha-card`/`header`/`stage` на живом собранном бандле — + прогнан мной, зелёный (см. «Как проверялось» п.5). Кроме RU-ограничения + из Medium-1, оракул содержательный: допуск 1 px, `stage.height > 0`, + `cardScroll`/`haCardScroll` ≤ 1 (нет overflow/пустого хвоста). +- **AC4** (серия 6→10→14→6, камера/аспект не сбрасываются, нет ошибок + ResizeObserver): проверено исполнением; мутант + `sections-resize-drops-stage-refit-observer` (отключение + `_roViewport.observe(stage)`) краснит именно этот smoke — «чем краснеет» + предметно подтверждено мной, не только заявлено автором. +- **AC5** (переходы Plan/Devices/Backdrop не меняют внешний rect, нет + busy-хвоста): смок ждёт реальный `_modeTransition.state` (не форсирует + приватное состояние), проверяет `entering.to.stageHeight <= + card.clientHeight + 1` на входе и выходе для всех трёх режимов при 10 и 6 + строках. Мутант `sections-editor-transition-uses-window-height` + (возврат `innerHeight` вместо `containerOwnedHeight ? this.clientHeight`) + краснит smoke — подтверждено прогоном. +- **AC6** (explicit `grid_options` остаётся главным, config не мутируется): + `Object.freeze(config)` в фикстуре + сверка `JSON.stringify(config) === + configBefore` и `card._config.grid_options === config.grid_options` по + идентичности объекта — сильнее обычного deep-equal, ловит скрытое + клонирование. Не отдельный юнит-файл, а часть browser smoke (ТЗ называет + «Unit + browser smoke» как два способа доказательства; по факту это один + исполняемый смок) — расхождение в названии способа доказательства не + снижает фактическое покрытие и не поднимается до отдельной находки (по + аналогии со снятыми Low ревью ТЗ). +- **AC7** (без `layout=grid` — старое поведение; `space-card` без grid + defaults): `card.layout = null` в смоке восстанавливает `100dvh` в + реальном инлайн-стиле (`ordinaryCardKeepsViewportHeight`), проверено + исполнением; `smoke_houseplan_panel.mjs` (panel-host, не grid) + обновлён и прогнан мной отдельно — зелёный, панельная контейнерная + высота не задета рефактором `panelHost → containerOwnedHeight` + (математически `containerOwnedHeight === panelHost` когда `layout` не + установлен). +- **AC8** (resize не создаёт кликов/тапов, hit-coordinates верны после + resize): `moreInfo` счётчик остаётся 0, `stableMode === 'view'`, + `postResizeHitCoordinatesMatchCamera` сверяет реальную матрицу + `getScreenCTM()` SVG после шторма resize с `card._view` — не заявление, а + геометрическая проверка через реальный DOM. +- **AC9** (30 изменений высоты без ResizeObserver loop/error): цикл из 30 + `frame()`-тиков с чередованием 6/14 строк, финальная геометрия и время + (`stormElapsed < 3000`) проверены исполнением; `finish()` в + `demo/serve.mjs` считает необработанные исключения карточки + (`_pageErrors`) и красит прогон при их появлении — не только заявленные + булевы поля. +- **AC10** (документация + credit): `docs/CHANGELOG.md`/`.ru.md`, + `docs/USER-GUIDE.md`/`.ru.md`, `docs/ARCHITECTURE.md`, + `docs/STATUS-FEATURES.md`, `docs/TESTING.md` все обновлены и + проверены чтением; `@pando80` упомянут в обоих changelog с ссылкой на + #648. `check-docs.mjs --screenshots=warn` (режим обычного пуша) прогнан + мной — зелёный (единственная находка — WARN о неизменном скриншот- + отпечатке, ожидаемо: диф не меняет визуальные пиксели плана/chrome, + ТЗ прямо разрешает это в «Плане автотестов» п.7). +- **Трейлеры и changelog**: `Issue: #648` на всех трёх коммитах, + `User-Visible: yes` только на `feat`-коммите и оба changelog редактируются + именно в нём — соответствует AGENTS.md. +- **Одно число — один источник** (§8): 10 и 6 строк существуют один раз — + как возврат `getGridOptions()`; вся документация и тесты лишь + переиздают это значение прозой/сравнением, вторичного независимого + источника не заведено. +- **Бандл воспроизводим**: пересборка (`npm run build && npm run + bundle:sync`) на материале не породила диффа — закоммиченный `dist/**` и + зеркала в `custom_components/houseplan/frontend/**`, + `demo/srv/assets/**` побайтно совпадают с исходниками этого SHA. +- **Гейты, специфичные для `src/**`-дифа**: `no-new-any.mjs` (0 новых + `any` в 31 добавленной строке `src/**`), `no-new-private-writes.mjs` + (0 новых записей в приватное состояние карточки из 223 добавленных строк + смоков — только чтения, что разрешено), `bundle:budget` (initial View + 291761 B, в пределах потолка) — все прогнаны мной и зелёные. +- **Юнит-тесты**: `npm test` — 3086 тестов, 3085 pass, 1 skip (не связан с + этой задачей), 0 fail. +- **Область изменений**: `space-card.ts`, `panel-host`, Masonry, kiosk не + тронуты кодом (только упомянуты в документации как «не меняются») — + подтверждено grep по всем изменённым файлам и фактическим отсутствием + правок вне `houseplan-card.ts`/`boot-soft-layout.ts`/`base.styles.ts`. + +## Чего не проверял + +- **Реальное поведение Home Assistant Sections вне этого репозитория.** + Весь механизм задачи опирается на внешний контракт: что настоящий HA при + размещении карточки в Sections/grid-дашборде действительно устанавливает + на элементе свойство `layout = 'grid'` (K3/K11, оставлено ревью ТЗ как + «принято предположительно, поменять свободно» — технический, не + продуктовый вопрос). В этой песочнице нет ни реального HA, ни доступа к + инструментам веб-поиска/GitHub-поиска (запрошенные `WebSearch` и + `mcp__github__search_code` не были разрешены), поэтому свериться с + исходником `home-assistant/frontend` я не смог. Все AC2–AC10 в этом + дифе проверены только против синтетической фикстуры, которая + устанавливает `card.layout = 'grid'` вручную (`demo/smoke_sections_resize. + mjs:77`) — они доказывают, что *если* HA присылает этот сигнал, карточка + ведёт себя правильно, но не доказывают, что HA действительно его + присылает именно так (то же имя свойства, тот же момент жизненного цикла). + Это прямой риск: если предположение неверно, весь фиче-код останется + мёртвым в проде при полностью зелёных гейтах. Технически это ровно то + место, которое ревью ТЗ явно делегировало код-ревью — фиксирую как + открытый остаточный риск, а не как находку, потому что задача не может + быть проверена исполнением без реального HA, а сам код внутри доступного + материала последователен и не содержит внутренних противоречий. +- **15 из 19 «прямых совпадений» и все 28 «слабых связей» `smoke- + select.mjs`.** Прогнал только `smoke_sections_resize.mjs`, + `smoke_houseplan_panel.mjs` (названы в AC), `smoke_render_invalidation.mjs` + и `smoke_warm_dialogs.mjs` (точечно — единственные, трогающие изменённую + сигнатуру `settleSoftStageLayout`/`_bootSoft` напрямую). Остальные не + гонял: рефактор `panelHost → containerOwnedHeight` в + `boot-soft-layout.ts` математически не меняет значение для существующих + (не-`grid`) карточек, поэтому регрессия там маловероятна; полный прогон + матрицы — предрелизная обязанность, а не гейт код-ревью (§8). +- **`golden:verify`** — не прогонял. ТЗ прямо говорит: «Golden не + требуется, если визуальные пиксели плана/chrome не меняются» — диф не + трогает цвета/геометрию плана, только контейнерную высоту и CSS-flex; + `check-docs.mjs` подтверждает отсутствие изменений в + зафиксированных скриншот-сценариях документации (только WARN о + фингерпринте, не ERROR). +- **`npm run invariants`** — не прогонял: диф не меняет геометрическую + модель плана (стены/комнаты/устройства) и не трогает ссылки на неё, + только презентационный слой карточки. +- **`python -m pytest tests_backend`** — не прогонял: диф не касается + `custom_components/houseplan/**/*.py`. +- **Performance-профили** — не названы в AC этой задачи; ТЗ прямо + запрещает новый observer/polling, а не вводит его (новый постоянный + наблюдатель не добавлен — использован существующий `_roViewport`). +- **`typecheck`/`npm test`/`npm run build`/`golden`/`performance_smoke`/ + browser `smoke`-матрица целиком в CI** — доверился зелёному Validate на + точном SHA `b7fb7cf7` (ссылка в задании) для `typecheck`/`test`/`build`; + `npm test` и `npm run build` тем не менее перегнал сам (см. выше) для + собственной уверенности перед прогоном смоков на пересобранном бандле — + оба зелёные, совпадают с Validate. + +## Вердикт + +0 High, 1 Medium (в скоупе задачи, Medium-1 — неполное покрытие AC3 +браузерным смоком: заявлены RU/DE, реализована только RU). Без High-находок +это жёлтый вердикт: автор дополняет `demo/smoke_sections_resize.mjs` +немецкой локалью в проверке минимальной высоты и проходит повторный раунд +код-ревью — отдельный issue не заводится (#202). + +Всё остальное — AC1–AC2, AC4–AC10, трейлеры, changelog, гейты `typecheck`/ +`npm test`/`npm run build`/`no-new-any`/`no-new-private-writes`/ +`bundle:budget`/`check-docs`, четыре новых защитных мутанта и оба +относящихся к задаче browser smoke — проверено исполнением и корректно. + +**Жёлтый.** + +--- + +Вердикт: жёлтый · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 1 → в задаче + +--- + + + +## Материал раунда + +- Ветка: на момент ревью без явного имени в рабочей копии; вершина — + `b7fb7cf7e7bce13a44ef0e1af3b97541ddb0dffa`. +- База сравнения: `origin/dev` = `f68878cc44b818b6cfbdb3866302e6febaab2445`. +- Дерево материала (`git rev-parse HEAD^{tree}` на момент ревью): снято на + вершине `b7fb7cf7e7bce13a44ef0e1af3b97541ddb0dffa`, рабочая копия чиста + до и после всех локальных гейтов/мутаций (`git status --porcelain` + пуст). +- Предыдущий раунд (другой этап): `docs/reviews/SPEC-REVIEW-648-r1.md`, + вердикт `green`, High 0, Medium 0. + +--- + + + +## Материал раунда + +- Ветка: `issue/648-sections-card-resize`, коммит `b7fb7cf7e7bc` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. +- Дерево материала: `a7fe21e21d802c04986cbfab31f3065266c8e9ff` + ``` + git log --all --format='%H %T' | grep a7fe21e21d80 + ``` +- Тело issue: `8400466150bde494c01aef14b7d5961be36d721b08a28c69e800bd88ac645154` +- Вердикт конвейера: `yellow` · High 0 diff --git a/docs/reviews/INDEX.md b/docs/reviews/INDEX.md index 7129c2d0..65b62bb8 100644 --- a/docs/reviews/INDEX.md +++ b/docs/reviews/INDEX.md @@ -7,6 +7,7 @@ | #649 | [SPEC-REVIEW-649-r1.md](SPEC-REVIEW-649-r1.md) | spec · r1 | 🟡 жёлтый | 0 | 1 | в скоупе задачи (возвращается автору); принято ревьюером с записью, правки не требует | `lab.js` `houseplan-card.ts` | | #649 | [SPEC-REVIEW-649-r2.md](SPEC-REVIEW-649-r2.md) | spec · r2 | 🟢 зелёный | 0 | 0 | — | — | | #648 | [SPEC-REVIEW-648-r1.md](SPEC-REVIEW-648-r1.md) | spec · r1 | 🟢 зелёный | 0 | 0 | «Сценарий» не называет персону и; AC10 называет provenance-gate | `docs/SCOPE.md` `scripts/validate-commit-provenance.mjs` `validate.yml` | +| #648 | [CODE-REVIEW-648-r1.md](CODE-REVIEW-648-r1.md) | code · r1 | 🟡 жёлтый | 0 | 1 | AC3 документально обещает browser smoke | `demo/smoke_sections_resize.mjs` `docs/TESTING.md` `smoke_sections_resize.mjs` | | #647 | [SPEC-REVIEW-647-r1.md](SPEC-REVIEW-647-r1.md) | spec · r1 | 🟡 жёлтый | 0 | 2 | П.4 ТЗ переносит на новый слот прежнее; ТЗ не упоминает и не защищает документированный | `src/styles/dialogs.styles.ts` `src/styles.ts` `smoke_glow_blending.mjs` `smoke_test_facade.mjs` `smoke_unified_wall_tool.mjs` `docs/UX-MODES.md` `docs/reviews/CODE-REVIEW-195-r1.md` `src/styles/chrome.styles.ts` | | #647 | [SPEC-REVIEW-647-r2.md](SPEC-REVIEW-647-r2.md) | spec · r2 | 🟢 зелёный | 0 | 0 | — | — | | #647 | [CODE-REVIEW-647-r1.md](CODE-REVIEW-647-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | — | — |