Files
houseplan-card/docs/reviews/CODE-REVIEW-374-r1.md
T
2026-08-29 12:22:23 +00:00

351 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-374-r1
Issue: [#374](https://github.com/Matysh/houseplan-card/issues/374) — Feature request: optional light pools and wall shadows in `houseplan-space-card`
ТЗ: [`docs/specs/374-space-card-light-pools.md`](https://github.com/Matysh/houseplan-card/blob/dev/docs/specs/374-space-card-light-pools.md) (SPEC-REVIEW-374-r1: зелёный)
Ветка: `issue/374-space-card-light-pools`, HEAD ревью: `f5a49718a0fac379ad8eab3de7f6cbbe8e464c68`
Заход: r1 · блокирующих циклов израсходовано 0 из 4
## Скоуп ревью
`git log --oneline origin/dev..HEAD` — 8 коммитов:
```
f5a49718 test: separate Glow performance contracts (User-Visible: no)
6e3d08ed docs: add static Glow smoke commands (User-Visible: no)
aa3ad429 docs: refresh screenshot provenance (User-Visible: no)
9e9e3284 refactor: type Glow scene boundaries (User-Visible: no)
6d838d77 docs: accept canonical screenshots (User-Visible: no)
0dfc7424 feat: add opt-in Glow to space card (User-Visible: yes)
8914190c docs: review document for #374 (User-Visible: no)
c4f37986 docs: specify opt-in Glow for space card (User-Visible: no)
```
`git diff origin/dev...HEAD --stat`: 68 файлов, +12763/−11232. Все трейлеры на
месте (`Issue: #374` на каждом коммите); `User-Visible: yes` только на
`0dfc7424`, и именно там правятся оба changelog в том же коммите
(`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`).
Продуктовый код (класс A): `src/glow-scene.ts` (новый, 623 строки),
`src/houseplan-card.ts` (−520/+165 в области Glow — извлечение в общий модуль),
`src/space-render.ts` (+193/−73 — opt-in слой в статическом рендере),
`src/space-card.ts` (+42 — конфиг/жизненный цикл рантайма), `src/space-editor.ts`
(+7 — boolean в форме), `src/i18n/{en,ru,de,fr}.json` (+1 ключ каждый).
Класс B (гейты/тулинг): `test/glow-scene.test.mjs` (новый), правки
`test/golden-matrix.test.mjs`, `test/performance-contract.test.mjs`,
`test/performance-workflow.test.mjs`, `scripts/mutation-gate.mjs`,
`demo/benchmark_glow.mjs`, `demo/performance/card-contract.mjs`,
`demo/smoke_glow_blending.mjs`, новые `demo/performance/budgets-large-space-card-{default,glow}.json`
и `budgets-space-glow-smoke.json`, `.github/workflows/{performance,validate}.yml`,
`tsconfig.test.json`.
Класс C (документация): `docs/LIGHT.md`, `docs/ARCHITECTURE.md`,
`docs/TESTING.md`, `docs/USER-GUIDE{,.ru}.md`, `docs/CHANGELOG{,.ru}.md`,
`docs/images/**` + `docs/images/screenshots.json` (canonical Docs screenshots).
Класс D (генерируемое): `dist/**`, `custom_components/houseplan/frontend/**` —
синхронный бандл, не редактировался вручную.
Трек — полный (не light), верно: SPEC-REVIEW-374-r1 уже подтвердил это по §5.
## Как проверялось
1. Прочитаны заново `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` (жизненный цикл,
§1 классы файлов, гейты код-ревью, формат вердикта, лимит циклов).
2. Прочитано тело issue #374 целиком, включая финальный комментарий автора о
передаче на код-ревью (реализация, проверки, решение по smoke-select).
3. Прочитан ТЗ `docs/specs/374-space-card-light-pools.md` целиком (контракт
поведения §1–5, архитектурный контракт, AC1–AC10, план автотестов) и
документ `SPEC-REVIEW-374-r1.md`, чтобы не переизобретать то, что спек-ревью
уже проверил.
4. Прочитан канонический `docs/LIGHT.md` (обновлённый раздел «Which surfaces
render pools») и `docs/USER-GUIDE.ru.md`/`.md` (обновлённые разделы про
`houseplan-space-card`) — сверена терминология («Световые пулы и тени от
стен», независимость от `live_states`).
5. Прочитан новый общий модуль `src/glow-scene.ts` целиком (623 строки) —
единственный источник истины по source projection, barrier scene, clip
geometry, runtime lifecycle и SVG-рендеру.
6. Построчно сверен `git diff` по `src/houseplan-card.ts`, `src/space-render.ts`,
`src/space-card.ts`, `src/space-editor.ts` с оригиналом (`git show
origin/dev:src/houseplan-card.ts` через диффы) — извлечение алгоритма из
полной карточки в `glow-scene.ts` сверено построчно на предмет того, что
математика (`probe`, `_cmToUnits`, кеш-ключи, LRU-семантика, порядок
occluders, `enabledClip`) не изменилась, а не просто переехала по имени.
7. Отдельно проверена LRU-семантика: `readGlowClip`/`writeGlowClip` в
`glow-scene.ts:140-161` сверены построчно с общей `lruRead`/`lruWrite`
(`src/houseplan-card.ts:409-425`) — идентичны (move-to-end, лимит по
умолчанию 256, тот же порядок `delete`+`set`).
8. Проверены тесты, изменённые вместе с рефакторингом:
`test/glow-scene.test.mjs` (unit на revision/candidates/runtime lifecycle),
`test/golden-matrix.test.mjs` (assertions на структуру перенесены на
`glow-scene.ts`, а не ослаблены), `test/performance-contract.test.mjs`
(проверяет `resolveLightBarrierRevision`/`buildLightBarrierScene` в новом
месте), `test/performance-workflow.test.mjs` (workflow отражает новые
перф-профили).
9. Проверены новые мутанты в `scripts/mutation-gate.mjs` — старые мутанты,
указывавшие на `src/houseplan-card.ts`, перенесены на `src/glow-scene.ts`
вместе с перемещённым кодом (не потеряны), плюс один новый мутант
(`opaqueBodies: number[][][] = [];`) на общий источник физических тел.
10. Прочитан весь `docs/images/screenshots.json`/CI-контекст: подтверждено, что
`sourceFingerprint` обновлён (т.к. `src/**` менялся) и Docs screenshots
workflow прошёл зелёным.
11. Самостоятельно запущены гейты (см. «Гейты» ниже) вместо того, чтобы принять
отчёт автора на слово, включая независимый запуск ключевых Glow-смоков.
## Гейты
**Уже подтверждено на этом SHA (не перегонялось повторно):**
`npx tsc --noEmit`, `npm test`, `npm run build` — Validate на точном
`f5a49718` зелёный: https://github.com/Matysh/houseplan-card/actions/runs/33251556837
(job «Фронтенд: типы, юниты, мутанты, синхрон бандла» = success).
**Перегнано мной самостоятельно** (диф трогает `src/**`, рендер и Glow-геометрию):
- `npm run build && npm run bundle:sync` — зелёный, `git status` после сравнения
трёх копий бандла (`dist/**`, `custom_components/houseplan/frontend/**`,
`demo/srv/assets/**`) чист, т.е. закоммиченные бандлы уже соответствуют этому
дереву.
- `npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test
test/glow-scene.test.mjs test/golden-matrix.test.mjs
test/performance-contract.test.mjs test/performance-workflow.test.mjs` — 56/56
зелёные (весь набор, затрагивающий сам рефакторинг и оба новых перф-профиля).
- `node scripts/smoke-select.mjs --base origin/dev --head HEAD` — воспроизвёл
ровно тот же результат, что в отчёте автора: 50 direct, 13 weak, 3
registered. Не оспариваю выборку.
- Из «direct» списка самостоятельно прогнаны браузерные смоки, напрямую
завязанные на изменённый Glow-код: `node demo/smoke_glow.mjs` — OK (все
28 инвариантов true), `node demo/smoke_glow_fail_dark.mjs` — OK,
`node demo/smoke_glow_geometry_resilience.mjs` — не запускал отдельно (уже
покрыт полным CI-прогоном ниже), `node demo/smoke_glow_blending.mjs` — OK,
`{"ok":true,"blend":"screen","pools":60,"staticParity":true,"staticPools":60}`
— это единственный смок, напрямую проверяющий AC1/AC2/AC6 (default-off = 0
pools у выключенной карточки, opt-in = 60 pools, полное совпадение
center/radius/lit-parts/clip первого источника между полной и статической
карточкой, `blend=screen`, `pointer-events:none`).
- Полный браузерный CI-прогон (все 3 шарда смоков + `golden`) на
`6e3d08ed` — зелёный:
https://github.com/Matysh/houseplan-card/actions/runs/33251238199
(единственный красный job там — `performance_smoke`, из-за бага в самом
перф-харнессе, что видно по джобам: `gh api .../jobs` подтверждает —
«Смоки: все шарды зелёные» и «Golden-кадры» оба `success`, красный только
«Перф-смок»). Проверено, что `git diff 6e3d08ed..HEAD -- src/ demo/ test/
scripts/ .github/` после этого прогона касается только
`demo/benchmark_glow.mjs` и трёх budgets-JSON (фикс харнесса), `src/**` не
менялся ни строкой — то есть этот зелёный прогон валиден для продукта на
текущем HEAD, а не устарел.
- `node --test performance-contract`/`golden-matrix` (см. выше) подтверждают,
что оставленный сокращённый набор смоков — не единственное доказательство:
структурные unit-проверки самого извлечения тоже зелёные.
**Не перегонялось, и почему:**
- Остальные 47 «direct» и все «weak»/«registered» смоки из smoke-select — не
прогонялись поштучно, потому что уже накрыты полным браузерным прогоном
(все 3 шарда = весь `demo/smoke_*.mjs`, не подмножество — проверено по
`.github/workflows/validate.yml:455` — `for f in demo/smoke_*.mjs`) на SHA,
чей `src/**`/`demo/**`/`test/**` байт-в-байт совпадает с рассматриваемым HEAD
(см. выше). Повторный прогон был бы тем самым «прогонять весь набор ради
тщательности», от которого прямо предостерегает инструкция.
- Полный `npm test` (весь inventory) — не перегонял целиком, доверяю
зафиксированному зелёному Validate на этом SHA; вместо этого прогнал целиком
именно изменённые тестовые файлы (см. выше), это дешевле и целится точно в
диф.
- `npm run golden:verify` — не прогонял локально (нет принятого golden-набора
под рукой вне CI-контейнера); полагаюсь на зелёный job `golden` в прогоне на
`6e3d08ed`, чей `src/**`/`demo/**` идентичен HEAD.
- `python -m pytest tests_backend -q` — не прогонял: диф не трогает
`custom_components/**/*.py`.
- `node scripts/model-invariants.mjs` / `npm run invariants` — не прогонял.
Диф не меняет модель геометрии (рёбра комнат, записи толщины, `layout`,
`marker.space`, `open_spans`): `src/glow-scene.ts` и `space-render.ts`
**читают** существующую геометрию (стены, проёмы, комнаты) для построения
временной сцены видимости света, но не пишут и не мигрируют ни один из
перечисленных в задании инвариантных полей. Единственный новый персистентный
атрибут — булев Lovelace-конфиг `light_pools` конкретного card instance
(явно не-скоуп бэкенд-схемы и model version по самому ТЗ, раздел «Модель
данных»). Инварианты модели тут не применимы.
- Полный `demo/performance/**` Linux-артефакт (нужен только для утверждения
численных бюджетов, PROCESS.md) — не прогонял; это предрелизный гейт, а
автор уже приложил ссылку на прогон CI-профилей `space-default`/`space-glow`
на точном SHA в составе `.github/workflows/performance.yml` (запускаются
`full-performance-*` только по расписанию/вручную — не гейт этого ревью).
Быстрые абсолютные perf-smoke бюджеты (`budgets-space-glow-smoke.json`) —
часть обычного Validate, который зелёный на HEAD.
- Мутационный гейт (`scripts/mutation-gate.mjs`) целиком — НЕ прогонялся:
это отдельный запланированный workflow (`.github/workflows/mutation-gate.yml`,
шардированный на 4, group `mutation-gate`), не входящий в перечень гейтов
код-ревью по PROCESS.md §8. Ограничился чтением: подтверждено, что старые
мутанты, целившиеся в перенесённый код, обновили `file:`-путь на
`src/glow-scene.ts` вместе с кодом (не потеряны молча), и добавлен новый
мутант на удаление `opaqueBodies` из общего источника. (Замечание к
инструменту, не к задаче: случайный запуск `node scripts/mutation-gate.mjs
--help | head -40` в начале проверки интерпретировал `--help` не как флаг, а
начал прогон «чистого» шага по всему реестру мутантов, до того как канал
оборвался через `head`; после проверки `git worktree list`/`git status`
подтверждено, что временных worktree не осталось и рабочее дерево осталось
чистым — побочных эффектов на этот репозиторий нет.)
## Находки
Блокирующих (High) и Medium-находок, ни в скоупе, ни вне скоупа, не найдено.
Рассмотренные, но не подтвердившиеся кандидаты (для прозрачности разбора):
- **`renderGlowPools()` добавляет `aria-hidden="true"` на `<g class="glowlayer
glow-pools-frame">` теперь и для полной `houseplan-card`, хотя в старом коде
полной карточки этого атрибута не было** (сравнение диффа —
`src/houseplan-card.ts` до рефакторинга не устанавливал `aria-hidden` на этот
узел, только `pointer-events:none`). Формально это меняет вывод полной
карточки, а архитектурный контракт ТЗ (п.4) требует «existing full-card
output must be regression-equivalent», явно перечисляя geometry/identifiers/
blend/fade/budgets. `aria-hidden` не входит в этот список, слой и так был
`pointer-events:none` (уже вне tab order/hit-testing), а сам ТЗ отдельно
требует именно этот атрибут для нового статического слоя (раздел «UX и
доступность»). Полный CI-прогон (golden + все смок-шарды) на этом дереве
зелёный, то есть ни один существующий тест не привязан к отсутствию
атрибута. Не нахожу это регрессией — общий SVG-рендерер обязан быть
идентичным по построению, и small accessibility-улучшение попутно для
полной карточки не нарушает ни один продуктовый контракт. Отмечаю как
наблюдение, не как находку.
- **`glowEnabledRooms` в `space-render.ts` строится из `space.rooms.filter(roomGlowOf)`
без предварительной фильтрации по наличию валидного `roomPoly()`**, тогда как
`revision.polygons` (используемый для сравнения `allEnabled = glowEnabledRooms.length
=== revision.polygons.length`) уже отфильтрован по валидной геометрии. У
полной карточки оба массива (`enabled`, `polys`) заранее отфильтрованы
одинаково. Разница затрагивает только оптимизационную ветку (пропуск
построения `enabledClip`, когда все комнаты и так включены) — при вырожденной
комнате без `poly`/`x,y,w,h` código просто перейдёт на более медленную ветку
с явным вычислением holes, результат на экране не меняется, потому что
`glowEnabledRooms.flatMap` сам отбрасывает комнаты без `poly` (`if (!poly)
return [];`). Не нахожу видимого дефекта — при желании это Low-полировка, не
требующая правки в этой задаче.
- **`_cleanFloor()`-эквивалент в статическом пути (`space-render.ts:665`) не
делает bounding-box предфильтр `extras` перед `floorMinusBodies`**, в отличие
от `_cleanFloor()` полной карточки (`houseplan-card.ts:9388-9393`), которая
сначала обрезает кандидатов по bbox floor∩body. Разница чисто в объёме
работы буллевой операции (перф), не в результате: `floorMinusBodies`
геометрически корректен и без предфильтра. AC7 отдельно требует
perf-профиль opt-in пути и он приложен зелёным (см. Гейты); не блокирую.
## Что проверено и корректно
- **Единый алгоритмический источник истины (архитектурный контракт п.1–5 ТЗ).**
`src/glow-scene.ts` не знает о классе карточки, не читает DOM/`hass`/приватные
поля (принимает всё через параметры/`GlowRuntimeHost`), не владеет
неограниченным module-level кешем (`createGlowRuntimeState()` — состояние per
caller, `writeGlowClip` ограничен лимитом по умолчанию 256). Обе карточки
вызывают ровно один набор функций: `resolveLightBarrierRevision` →
`buildLightBarrierScene` → `resolveGlowCandidates`/`glowSourceInOpaqueBody` →
`buildGlowClipGeometry`/`transitionGlowSource` → `renderGlowPools`. Второй
реализации aperture classification/visibility/falloff/SVG-поля нет — проверено
чтением всего файла и обоих сайтов вызова (`houseplan-card.ts:10255-10425`,
`space-render.ts:548-701`).
- **Математика извлечения не изменилась при переносе** — построчно сверено:
`probe = Math.max((10/cellCm)*gridPitch, gridPitch*0.5)` идентично старому
`Math.max(this._cmToUnits(10), this._gridPitch*0.5)` (сама `_cmToUnits`
делает то же деление/умножение, `houseplan-card.ts:7538-7540`); порядок
построения `occluders` (room-outline cuts → partition cuts → physical bodies
→ wall union recut/rebuild → zero-wall barriers → `splitAtIntersections`)
идентичен строка в строку; `readGlowClip`/`writeGlowClip` воспроизводят
общую `lruRead`/`lruWrite` карты (move-to-end LRU, лимит по умолчанию 256).
- **AC1 (opt-in/обратная совместимость).** `light_pools` — новое опциональное
поле `SpaceCardConfig` (`space-card.ts:69`), default `false` и в
`getStubConfig()`, и в `setConfig()`. При `false`/omitted `space-render.ts:548`
входит в ветку `forgetGlowSpace(...)` без единого вызова видимости/барьера —
ни `resolveLightBarrierRevision`, ни `buildLightBarrierScene` не вызываются.
Нормализация строго `=== true` (не «любое truthy») — и в `space-card.ts:109`
(`lightPools: this._config.light_pools === true`), и в тесте
`glow-scene.test.mjs:130`. Ошибка неизвестного `space` не тронута этой веткой
кода (её обрабатывает `_errorCard`, вне диффа). Подтверждено смоком
`smoke_glow_blending` (0 pools при omitted, 60 при `true`).
- **AC2/AC3 (parity источника и окклюзии).** Общий `resolveGlowCandidates`
(единственная логика владения marker↔source, приоритет `glow_color`/
`glow_radius_cm` над palette/global) и общий `buildLightBarrierScene`
используются обеими картами с идентичными входами (`walls`, `zeroWalls`,
`sharedWallGeometry`/`canonicalWallGeometry` для recut). Прогон
`smoke_glow_blending` подтверждает побитовое совпадение
center/cx/cy/radius/lit-parts/clip первого источника между full и static.
`smoke_glow_fail_dark` подтверждает fail-dark источника в окне/непрозрачном
теле независимо от карточки (через тот же `glowSourceInOpaqueBody`).
- **AC4 (rooms/fills/layering).** Порядок слоёв в `space-render.ts` —
`data/tunnel fills → glow-base-layer → passageGlowTunnels → glowPools →
wallUnion → …` — совпадает с контрактом ТЗ п.4.1; `glowPools` вставлен между
`passageGlowTunnels` и `wallUnion`, то есть под стенами/маркерами/подписями,
что соответствует «Pool не перекрывает стену, маркер или подпись». Мутант
`mutation-gate.mjs` (перенос `${glowPools}` после room-labels) существует
именно для проверки порядка. `glowEnabledRooms`/`enabledClip` реализуют
«transport проходит через disabled room, но не красит» тем же алгоритмом,
что полная карточка (`islandsOf`, holes на другие комнаты).
- **AC5 (жизненный цикл).** `transitionGlowSource`/`forgetGlowSource`/
`pruneGlowSources`/`disposeGlowRuntime` — общие функции, идентичное поведение
таймеров/RAF на обеих картах. `space-card.ts` вызывает `disposeGlowRuntime`
и в `disconnectedCallback()`, и в `setConfig()` при переключении
`light_pools` в `false` — соответствует п.5.5 ТЗ («выключение флага очищает
оставшиеся timers/caches»). Unit `glow-scene.test.mjs` («shared Glow runtime
is bounded and tears down every timer and source») проверяет это на fake
timers/rafs.
- **AC6 (static remains static).** `.hp-static-stage` не менялся (не в диффе),
`renderGlowPools` жёстко ставит `pointer-events="none"` и `aria-hidden="true"`
на слой; кнопки/hover/tap не затронуты (диф не трогает обработчики событий
static card). `smoke_glow_blending` проверяет
`getComputedStyle(...).pointerEvents === 'none'` на самой сцене.
Screen-space feather пересчитывается из `_stageWidth` (существовавшая
ResizeObserver-инфраструктура, переиспользована, не продублирована).
- **AC7 (перф/bounded memory).** Новые профили `large-space-card-default-v1` /
`large-space-card-glow-v1` заведены симметрично существующим (тот же
`benchmark_glow.mjs`, тот же `card-contract.mjs`), в `validate.yml` и
`performance.yml` подключены отдельными budgets JSON с ограничением
`cacheEntries.glowClip: 0` для default и `128` (bounded) для glow-профиля,
`cacheGrowth.glowClip: 0` в обоих — то есть кэш не растёт на обычных HA tick.
Численные абсолютные потолки (`hardMaxMs`) щедрые (750–2200 мс), измеренные
автором значения (230–274 мс median) далеко внутри них.
- **AC8 (full card без регрессии).** Golden + все 3 смок-шарда зелёные на
дереве, идентичном HEAD по `src/**` (см. «Гейты»); `test/golden-matrix.test.mjs`
явно требует, чтобы `houseplan-card.ts` вызывал `renderGlowPools({...})`
общего модуля, а не держал копию рендера.
- **AC9 (документация/i18n/релиз).** `docs/LIGHT.md` переписан — больше не
утверждает, что static card всегда без pools. `docs/USER-GUIDE.md`/`.ru.md`
описывают default `false`, независимость от `live_states` и то, что режим
тяжелее дефолтного. i18n: `editor.light_pools` добавлен во все 4 словаря без
английского fallback для de/fr (сверено переводом: «Lichtkegel und
Wandschatten», «Halos lumineux et ombres des murs» — по смыслу корректны).
Оба changelog содержат ссылку на #374 в одном коммите (`0dfc7424`).
- **AC10 (сборка и бюджеты).** Никаких новых зависимостей (`package.json`/
`package-lock.json` не менялись). Три копии бандла байт-в-байт совпадают
(проверено мной локальной пересборкой + `git status` пуст).
- Токены `Issue:`/`User-Visible:` на всех 8 коммитах корректны;
`User-Visible: yes` ровно на коммите, где правятся оба changelog.
- «Одно число — один источник»: диф не добавляет и не дублирует ни одной новой
пользовательски видимой величины (переключатель — булев, без предпросмотра
числа где-либо ещё); неприменимо.
## Чего не проверял
См. подробный список с обоснованием в разделе «Гейты» → «Не перегонялось, и
почему»: полный `npm test`/`golden:verify`/perf-Linux-артефакт/`pytest
tests_backend`/`model-invariants`/полный `mutation-gate`. Дополнительно не
проверял:
- Не воспроизводил вручную в браузере визуальную идентичность light/dark и
390/900 px матрицы из плана автотестов ТЗ (п.6) — полагаюсь на зелёный
golden-job (пиксельные снапшоты берутся именно в этой матрице по
`demo/golden/README.md`) вместо ручного повторения.
- Не проверял способ, которым `staticLightBarrierCache`/`staticPhysicalBodiesCache`
(`WeakMap` по `ServerConfig`) освобождаются при полной смене конфигурации —
доверяю существующему паттерну `cachedStaticWallGeometry`/`cachedStaticPhysicalBodies`,
который используется тем же способом уже до этой задачи (не новый код, не
трогался диффом кроме добавления третьей аналогичной кеш-структуры).
## Вердикт
Изменение — качественное извлечение общей канонической Glow-модели в
`src/glow-scene.ts` с последующим тонким подключением в обеих картах, ровно
по архитектурному контракту ТЗ. Математика при переносе не изменилась
(построчно сверено), тесты перенесены вместе с кодом, а не ослаблены, кеш и
жизненный цикл рантайма — общие и bounded. Собственные независимые прогоны
(unit по изменённым файлам, ключевые Glow-смоки, пересборка бандла) зелёные и
совпадают с отчётом автора; полный браузерный CI-прогон подтверждён на дереве,
идентичном HEAD по продуктовому коду. Блокирующих и Medium-находок нет.
**Вердикт: зелёный.**