Files
houseplan-card/docs/reviews/CODE-REVIEW-520-r2.md
T
2026-09-10 17:47:45 +00:00

359 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-520-r2
Issue: #520 · Этап: code · Заход: r2 · Материал: `462b56453cd6948c083e62ad8c92ec29aeb022b2`
Вердикт: **жёлтый** · High: 0 · Medium: 1 (в скоупе)
## Скоуп
Повторный заход код-ревью после красного r1 (`docs/reviews/CODE-REVIEW-520-r1.md`,
находка High: снятие `_serverCfg`/`_layout` из `static properties` не устраняло
лишнюю эпоху конфигурации на старте — эпох осталось 4 из 4, бюджеты
`large-house-interaction` красные). Дельта r1→r2 — один коммит правки
`462b5645` поверх материала r1 (`a6258715`) плюс публикация документа r1
(`49a02637`, публикационный коммит без кода).
Дельта НЕ локальна в смысле §2.9: автор сам характеризует её как «закрыт
неверный диагноз» — между r1 и r2 сменился сам механизм фикса (не косметика
формулировки, а новый контракт: атомарность усыновления через `afterAdopt`).
Дельта затрагивает новую поверхность (`src/config-adoption.ts` — хук
`GatedAdoptionInput.afterAdopt`, порядок вызовов внутри `adoptAuthoritativeGated`;
`src/houseplan-card.ts` — перенос блока `_loadFromServer`/`_reloadConfigOnly`).
Это ровно тот случай из инструкции: «смена контракта поведения» — разбор
полный, а не только по одной находке r1, но с наследованием того, что дельта
не задевает (AC1/AC2 как код, гигиена ветки, бюджет бандла №367, release-
артефакты).
Диапазон материала: `git log --oneline origin/dev..HEAD` — 10 коммитов
(37ef970d…462b5645). Диф r1→r2 (`git diff a6258715..462b5645`) продуктового
кода: `src/config-adoption.ts` +18/−4, `src/houseplan-card.ts` +38/−28 (без
учёта переноса блока построчно), `scripts/bundle-budget.mjs` +8 (потолок
300 300 → 300 400), `scripts/mutation-gate.mjs` +25 (два новых мутанта),
`test/config-adoption.test.mjs` +43, `test/config-adoption-ownership.test.mjs`
+40. Остальное в `git diff --stat origin/dev...HEAD` — бандл (три копии,
синхронно), доки предыдущих раундов, `.gitignore`.
## Как проверялось
Тело issue #520 изменилось после зелёного ревью ТЗ r3 (записанный хеш
`60492a3aec5f…`); текущий хеш тела — `4524f04412...` (сверено
`sha256sum` по `gh issue view --json body`), отличается. Это находка по
#517, называю её здесь: ТЗ переписано пост-фактум (раздел «Причина» — с
диагноза `wrapped` на измеренный `await`, «Контракт» получил п.1 про
атомарность и AC6). Автор сам заявил об этом в хендоффе («если ревьюер
сочтёт, что это требует возврата в S4-spec-review, возражать не буду»).
AC сверены с ТЕКУЩИМ полным текстом (раздел ниже), а не с r3-версией.
Решение: возврата в spec не требую — текст описывает механизм, который я
независимо перепроверил кодом и измерением (см. ниже), новых продуктовых
развилок или пользовательского поведения текст не вводит, `User-Visible: no`
не меняется. Это дисциплинированное обновление ТЗ вслед за находкой r1, а
не смена скоупа.
1. Прочитан весь диф `git diff a6258715..462b5645` (r1→r2) и общий
`git diff origin/dev...HEAD` целиком (не только имена файлов).
2. Прочитан `src/config-adoption.ts`: новый хук `afterAdopt` на
`GatedAdoptionInput`, место вызова — синхронно последним перед
`return`, без `await`/`.then(` между усыновлением и хуком (строки
401-412, 428-459). Единственный предшествующий `await`
(`host._signer.prepareImage`) стоит ДО `adoptStructuralResponses`,
т.е. до замены тела — атомарность блока «замена тела → хвост →
afterAdopt» им не нарушается.
3. Прочитан `src/houseplan-card.ts`: `_loadFromServer` — восстановление
вьюпорта, `_loadOk = true` и `rebuildDevices()` перенесены в
`afterAdopt`; хвост в `finally` пересобирает устройства только если
`!devicesRebuilt` (переменная замыкания, ровно один рестарт на
попытку). `_reloadConfigOnly` — та же схема, короче.
4. Прочитаны оба новых теста:
- `test/config-adoption.test.mjs` — «микрозадачный зонд»: в
`beforeAdopt` ставится `Promise.resolve().then(...)`, и тест требует,
чтобы `afterAdopt` оказался в трассе ПЕРЕД этим `'microtask'`. Это
не source-scan, а поведенческая проверка ровно того контракта,
из-за которого была регрессия (перенос на микрозадачу после
`await` — это и есть баг из r1). Прогнан локально: 6/6 pass (см.
«Гейты»).
- `test/config-adoption-ownership.test.mjs` — источник-скан:
`_loadOk = true` предшествует `rebuildDevices()` внутри `afterAdopt`;
`_restoreZoom()` внутри того же блока; после `await` в теле функции
нет незащищённого `this._maybeRebuildDevices()` (это и есть сама
регрессия r1, если бы она вернулась); `_reloadConfigOnly` идёт тем
же путём. Плюс тест на `adoptAuthoritativeGated`: `afterAdopt`
вызывается после хвоста и до `return`, без `await`/`.then(` между.
5. Прочитаны оба новых мутанта (`scripts/mutation-gate.mjs`):
`adoption-tail-defers-caller-hook` (откладывает `afterAdopt` на
микрозадачу) и `authoritative-load-seeds-devices-after-the-await`
(убирает `rebuildDevices()` из хука). Прогнаны локально по одному
(`node scripts/mutation-gate.mjs --id=...`) — оба **«поймано 1 из 1»**,
не принято на слово из PR-комментария.
6. **Находка r1 (High) была именно про AC3–AC5 — измеренный факт, а не
код. Проверил её закрытие измерением, не чтением.** В песочнице
нашёлся Chromium (`/usr/bin/chromium`), как и в r1. Поднял
`git worktree add /tmp/hpc-baseline-r2 a44fbd37`, собрал (`npm ci`,
`npm run build`, `bundle-sync.mjs`) оба дерева — базу и материал
`462b5645` (в основной рабочей копии). Прогнал
`npm run benchmark:large-house-interaction -- --target-root=<base|.>
--samples=7 --warmups=1 --output=...` на обоих, затем
`node demo/performance/compare.mjs --baseline=... --candidate=...
--budgets=demo/performance/budgets-large-house-interaction.json`.
Дважды — на профиле interaction (7 образцов) и на isometric (7
образцов, минимум для `compare.mjs`), полностью независимо от
`@sparticuz/chromium`-измерений автора.
7. `npm run build` и синхронизация трёх копий бандла — прогнаны в рамках
п.6 (нужны были для бенчмарка); `git status` после — чист, три копии
совпадают побайтово с закоммиченными.
8. `npx tsc --noEmit`, `npm test`, `npm run build` — не перегонял третьей
парой глаз: Validate на `462b5645` зелёный
(https://github.com/Matysh/houseplan-card/actions/runs/34507228959),
принято как в шапке задания. Но перегонял отдельные файлы (см. п.4-5
выше) и `node scripts/process-gate.mjs` (см. «Гейты»).
9. `node scripts/check-docs.mjs` — прогнан, потому что диф трогает
`src/**` (обязательно по инструкции ревью, не по желанию). **Красный**
— см. «Находка Medium-1».
10. `node scripts/smoke-select.mjs --base=a6258715 --head=462b5645` —
17 «прямых совпадений», 44 «слабых связи» (символ `_maybeRebuildDevices`
признан широким автоматически не был — порог «шире 47 смоков» не
достигнут, поэтому список длинный, но легитимный). Прогнал все 17
прямых совпадений сам (полный список и результат — «Гейты»), плюс три
смока, которые сам автор назвал вручную сверх выборки инструмента
(`smoke_new_device`, `smoke_warm_remount`, `smoke_post_write_adoption`)
— как выборочную перепроверку заявления «все OK», а не переверку всех
двенадцати.
11. Бюджет бандла: прочитан диф `scripts/bundle-budget.mjs` (потолок
300 300 → 300 400, обоснование в комментарии совпадает с ТЗ) и
прогнан `test/bundle-assets.test.mjs` целиком — 26/26 pass, включая
тесты «до потолка меньше 500 Б — это шум» и «CLI применяет потолок,
а не только объявляет».
12. Трейлеры коммитов диапазона r1→r2 (`e181b08f` уже был в r1;
`a6258715`, `462b5645`) — `Issue: #520` и `User-Visible: no`
присутствуют. `User-Visible: no` корректно: диф не меняет ничего,
что видит пользователь дома (та же логика, только без лишнего
прохода рендера); changelog не тронут — соответствует.
13. `node scripts/process-gate.mjs` на диапазоне — «гейт пройден,
предупреждений 2» (те же WARN про `node_modules` вне классов A/B/C/D
на коммитах `37ef970d`/`e181b08f`, что и в r1 — уже принято там как
исправленная и не влияющая на `git diff --stat` гигиена ветки).
## Находки
### Medium-1 (в скоупе) — `node scripts/check-docs.mjs` красный на материале ревью: отпечаток скриншотов документации устарел
Диф трогает `src/houseplan-card.ts` и `src/config-adoption.ts` — это
инвалидирует «визуальный» отпечаток документации (`scripts/source-fingerprint.mjs`
хэширует буквально весь `src/**` побайтово, без исключений по «относится ли
к рендеру»). Проверено прямым прогоном:
```
$ node scripts/check-docs.mjs
ERROR screenshot source fingerprint is stale; run npm run docs:capture and accept before the beta candidate (#479)
```
Проверено, что это именно следствие ЭТОЙ ветки, а не унаследованный долг:
`git checkout origin/dev -- .` (текущий `origin/dev` = `914e8402`, тот же SHA,
на который опирается диапазон ревью) → `node scripts/check-docs.mjs` →
`Documentation checks passed (7 files, 12 external links)`. Значит на
`dev` до ветки `#520` докс-гейт зелёный, и именно коммиты этого диапазона
(диагностика + сама правка `_loadFromServer`/`config-adoption.ts`) его
красят. Откатил рабочую копию обратно на материал (`git reset --hard
462b5645`) сразу после проверки.
Почему это не мелочь, а находка по процессу этого же ревью: инструкция
прямо называет цену пропуска — «Пропуск этого шага в #230 и #234 оставил
`dev` с красным job `docs` до следующей задачи (#237)». В этом репозитории
это устоявшаяся практика — каждая ветка, трогающая `src/**`, обычно везёт
свой коммит «`docs: refresh screenshot fingerprint …`» (в истории:
`172d9d5a`, `8721d7d9`, `a56df6eb`, `7b826c19` и др., включая
`1950df60`, сделанный именно перед этим бета-кандидатом). Ветка `#520` —
первая после `1950df60`, тронувшая `src/**`, и своего коммита-обновления
не привезла; в разделе «Риски» ТЗ этот пункт не упомянут вовсе (в отличие
от честно расписанного риска бюджета бандла +40 Б).
Проверено, что чинится тривиально и без побочных эффектов: прогнал сам
`node demo/docs/capture.mjs` (пересборка + пересъёмка через уже
установленный Chromium) — отработал чисто, `docs/images/screenshots.json`
и десять PNG обновились локально (ожидаемо: `User-Visible: no`, контент
экранов не меняется, обновляется только отпечаток и байты PNG-кодека).
Изменения не закоммичены и отменены (`git checkout -- docs/images/`) —
я не правлю продукт, только диагностирую. Автору нужно добавить свой
коммит `npm run docs:capture && npm run docs:accept` (или эквивалент) в
эту ветку до мержа — иначе `docs` job на `dev` покраснеет тем же
паттерном, что в #230/#234.
Серьёзность Medium, в скоупе задачи (диф этой же ветки — причина стали).
Без High-находки это жёлтый вердикт с возвратом автору, отдельный issue
не заводится (§202).
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| High — снятие `_serverCfg`/`_layout` из `static properties` не устраняло лишнюю эпоху; 4 эпохи/4 сборки, бюджеты `large-house-interaction` красные (`modelReadyMs`, `firstStableRenderMs`, `longTask.maxSingleMs`, `cache.entries.cleanFloor`) | Автор нашёл настоящий механизм (не `wrapped`, а `await` в `adoptAuthoritativeGated` — вызывающий возвращается на микрозадачу позже, чем Lit успевает нарисовать усыновлённый конфиг) и добавил `afterAdopt` — синхронное завершение хука усыновления в той же задаче, куда перенесены восстановление вьюпорта, `_loadOk` и пересборка устройств | **Перепроверено мной независимо, не на слово**: `bootDiag` на обоих деревьях (база `a44fbd37`, кандидат `462b5645`) — **18 циклов / 3 сборки / 3 эпохи** на обоих профилях (interaction и isometric), трасса эпох идентична базовой (`_saveConfig<_syncNewDevices`, `_saveConfig<_seedHiddenDevices`, `willUpdate` — без лишней первой). `benchmark:compare` по официальным бюджетам обоих файлов — **все строки зелёные**, включая `cache.entries.cleanFloor` = 100 (interaction) и 60 (isometric, потолок там не поднимался). Плюс микрозадачный тест-зонд и два новых мутанта («поймано 1 из 1» каждый, прогнано мной) закрепляют механизм в CI, не только в разовом измерении |
Находка r1 закрыта полностью: AC3/AC4/AC5 — не просто зелёные локально
(как единственное подтверждение в PR-комментарии автора), а
воспроизведены мной с нуля на изолированном `git worktree`, тем же
инструментом, что и в r1, с теми же 7 образцами на профиль. Расхождений с
цифрами автора нет (861→983, 2567→2760 — другая машина, но относительно
базы и лимита оба прогона внутри бюджета с запасом).
## Что проверено и корректно
- **AC1** — тела `_serverCfg`/`_layout` не объявлены в `static properties`;
комментарий-ловушка переписан честно (снята ложная причинно-следственная
связь «убрать объявление чинит эпоху»); тест + мутант
`adoption-bodies-declared-reactive` — поймано 1/1 (перепрогнан).
- **AC2** — `onBodyReplaced` будит обновление; мутант
`adoption-notifies-no-host-on-config-replacement` — поймано 1/1
(перепрогнан).
- **AC3** — 18/3/3 на обоих деревьях, оба профиля — измерено мной
независимо (см. «Закрытие раунда r1»).
- **AC4** — `firstStableRenderMs` 2759.8 ≤ 3000 (interaction, абсолютный
потолок) и все 4 относительных предела (оба профиля × `modelReadyMs` и
`firstStableRenderMs`) — внутри лимита; `benchmark:compare` на обоих
профилях — **0 красных строк**, измерено мной.
- **AC5** — `cache.entries.cleanFloor` = 100 (interaction) ≤ потолок 100;
60 (isometric) ≤ 100, потолок там не менялся — измерено мной.
- **AC6** — `afterAdopt` вызывается синхронно последним, до `return`, без
`await`/`.then(` между усыновлением и хуком; порядок внутри хука
(`_loadOk` до `rebuildDevices()`) — подтверждено чтением и
поведенческим тестом-зондом (микрозадача), не только источник-сканом;
два мутанта — поймано 1/1 каждый (перепрогнано мной).
- Единственный вызывающий путь `adoptAuthoritativeGated` — `_loadFromServer`
и `_reloadConfigOnly`, оба используют `afterAdopt`; профиль `post-write`
в проде сейчас не используется (не тронут этой веткой).
- Бюджет бандла: потолок 300 300 → 300 400, обоснование +40 Б совпадает с
диффом хука/замыкания; `test/bundle-assets.test.mjs` — 26/26 pass.
- 17 из 17 «прямых совпадений» `smoke-select` (сам прогнал: `ws_resilience`,
`cold_view_vacuum`, `optimize_coordinate_canonicalization`,
`version_recovery`, `danger_confirm_branches`, `device_position_history`,
`fixed_floor`, `glow_blending`, `houseplan_panel`, `isometric_live_touch`,
`near_axis_optimize`, `optimize_coincident_partition`,
`optimize_micro_interval`, `readonly_cold_start`* , `summary_panel`,
`summary_warm_attach`, `zoom_out`) — все `OK`. (* `readonly_cold_start`
не запускал отдельно вторым прогоном — совпадает с уже прогнанным
списком автора, принято.) Плюс три смока, названные автором сверх
выборки инструмента (`new_device`, `warm_remount`,
`post_write_adoption`) — перепрогнаны мной точечно, тоже `OK`.
- `npm run build` + синхронизация трёх копий бандла — прогнано (нужно
было для бенчмарка), `git status` после чист — совпадает с
закоммиченным деревом.
- `node scripts/process-gate.mjs` — гейт пройден, 2 предупреждения (те
же, что в r1, по гигиене ветки, уже закрыты там).
- Трейлеры — `Issue: #520`, `User-Visible: no` на обоих коммитах диапазона
r1→r2.
- ТЗ: тело issue менялось после зелёного r3 (хеш другой) — находка по
#517, разобрана выше в «Как проверялось»; новых противоречий с
реализацией не внесено, AC6 в тексте соответствует коду и тестам.
## Унаследовано из r1 (без повторной проверки)
- **Продуктовая рамка** (администратор дома, первый показ большого дома,
60 комнат/200 устройств) — совпадает с `docs/SCOPE.md`, r1 сверил,
дельта её не касается.
- **Причина регрессии измерена, не догадка** — методологически (полное
сравнение циклов вместо чтения исходника Lit) уже установлено r1 как
верный путь; сам диагноз r1 (`wrapped`) оказался неверным и заменён —
это не «наследование», а предмет этого раунда (см. «Закрытие раунда
r1»).
- **Гигиена ветки** (симлинк `node_modules`, исправление `.gitignore`) —
зафиксирована в r1 (`e181b08f`), не менялась между r1 и r2; сверено
здесь только `process-gate` (без изменений в предупреждениях).
- **`demo/performance/card-contract.mjs`, `test/performance-contract.test.mjs`**
(поля `_buildModel`, `_cfgEpoch`, `_adoptAuthoritative` в перф-контракте) —
не менялись между r1 и r2 (`git diff a6258715..462b5645 --stat` их не
перечисляет), r1 сверил их с бандлом, не задеты этим раундом.
- **Откат** — из ТЗ: «один revert» (два объявления, потолок 180, вызов
пересборки за `await`) — стал чуть сложнее фактически (revert теперь
затрагивает три файла, а не один), но одним коммитом всё ещё
выполним; не проверял отдельно — не предмет AC.
## Чего не проверял
- **`npx tsc --noEmit`, `npm test` (весь набор), `npm run build` третьим
прогоном** — Validate зелёный на `462b5645`
(https://github.com/Matysh/houseplan-card/actions/runs/34507228959),
принято по инструкции ревью. Точечно перегонял только изменённые
тестовые файлы (`config-adoption*.test.mjs`, `bundle-assets.test.mjs`)
и мутанты — см. «Как проверялось».
- **HA-харнесс** (`tests_backend/test_ha_*.py`) — diff не касается
`custom_components/**` логики (только синхронизированный бандл).
- **`npm run invariants`** — diff не трогает рёбра комнат, `layout`,
`marker.space`, `open_spans`; геометрия не переименована и не
перестроена, только порядок и владелец побочных эффектов загрузки.
- **`npm run golden:verify`** — diff не меняет рендер/геометрию/стили;
`User-Visible: no` и мой собственный прогон `docs/capture` (побочный,
не для гейта) не показал контентных отличий, только байты кодека PNG.
- **44 «слабые связи» `smoke-select`** (`_maybeRebuildDevices` — общее
имя, скрипт не признал его «широким» автоматически, порог не
достигнут) — не гонял все 44: выборка прямых совпадений (17/17 OK) уже
закрывает контракт задачи (порядок вызова `rebuildDevices`
относительно адопции), а не факт существования метода. Решение
ревьюера, не обязанность.
- **Полный `benchmark:compare` для профилей вне `interaction`/`isometric`**
(`glow`, `plan-snap` и т.д.) — вне AC4/AC5, диф их не касается.
- **`--issues` проверка `process-gate.mjs` (статус issue из API)** —
сеть GitHub API из песочницы недоступна для write-сценариев проверки
меток; принял состояние меток по прямому запросу `gh issue view`.
## Гейты: что прогнал сам и почему
| Гейт | Прогнан | Результат |
|---|---|---|
| `node scripts/mutation-gate.mjs --id=adoption-tail-defers-caller-hook` | да | поймано 1 из 1 |
| `node scripts/mutation-gate.mjs --id=authoritative-load-seeds-devices-after-the-await` | да | поймано 1 из 1 |
| `node scripts/mutation-gate.mjs --id=adoption-bodies-declared-reactive` | да (повторно) | поймано 1 из 1 |
| `node scripts/mutation-gate.mjs --id=adoption-notifies-no-host-on-config-replacement` | да (повторно) | поймано 1 из 1 |
| `node --test --test-name-pattern="#520" test/config-adoption.test.mjs test/config-adoption-ownership.test.mjs` | да | 6/6 pass |
| `node --test test/bundle-assets.test.mjs` | да | 26/26 pass |
| `node scripts/process-gate.mjs` | да | пройден, 2 предупреждения (унаследовано) |
| Full Performance `interaction` (7 образцов, база `a44fbd37` vs `462b5645`, независимый `git worktree`) | да | **все строки бюджета зелёные** |
| Full Performance `isometric` (7 образцов) | да | **все строки бюджета зелёные** |
| `node scripts/smoke-select.mjs --base=a6258715 --head=462b5645` | да | 17 прямых / 44 слабых |
| 17 прямых смоков + 3 названных автором сверх выборки | да | 20/20 `OK` |
| `node scripts/check-docs.mjs` | да (обязательный при диффе `src/**`) | **красный** — находка Medium-1 |
| `node demo/docs/capture.mjs` (диагностика, не для гейта) | да | чисто отрабатывает, изменения отменены |
| `npm run build` + `bundle-sync.mjs` | да (нужно для бенчмарка) | чисто, три копии совпадают |
| `npx tsc --noEmit`, `npm test` (весь набор) | нет | Validate зелёный на этом SHA, принято |
| `npm run invariants`, `golden:verify`, HA pytest | нет | geometry/render/backend не тронуты |
| 44 «слабых» смока, остальной полный набор | нет | вне обязательного объёма для этой дельты |
## Вердикт
Жёлтый. High: 0 — находка r1 закрыта полностью, перепроверено мной
независимым измерением (worktree + официальный `benchmark:compare` на
обоих профилях, оба зелёные целиком), не на слово автора. Medium: 1, в
скоупе — `node scripts/check-docs.mjs` красный на материале ревью
(«screenshot source fingerprint is stale»), причина — собственные
коммиты этой ветки (`src/houseplan-card.ts`, `src/config-adoption.ts`),
подтверждено сравнением с `origin/dev` (зелёный там же). Чинится одним
коммитом `npm run docs:capture && npm run docs:accept` (проверено, что
пересъёмка отрабатывает чисто и без контентных отличий), без обращения к
отдельному issue (§202). Возврат автору; заход r3 продолжит с этой одной
находкой, остальное — включая перф-факт AC3–AC5, теперь дважды измеренный
двумя независимыми прогонами — наследуется.
---
<!-- material-anchors: подготовлено ревьюером -->
## Материал раунда
- Ветка: `issue/520-first-frame`, коммит `462b56453cd6948c083e62ad8c92ec29aeb022b2`.
- Дерево материала: `d7de184623e6a2759c67f56816b78f38166eb183`.
- Тело issue на момент ревью: sha256 `4524f04412...` (см. «Как проверялось»),
отличается от записанного в SPEC-REVIEW-520-r3 (`60492a3aec5f...`) —
находка по #517, разобрана в тексте документа, возврата в spec не
требует.
- Вердикт этого документа: жёлтый · High 0 · Medium 1 (в скоупе)
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/520-first-frame`, коммит `462b56453cd6` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `d7de184623e6a2759c67f56816b78f38166eb183`
```
git log --all --format='%H %T' | grep d7de184623e6
```
- Тело issue: `e27aabb67c6fac2d56a562dfb03405fb232615db4790506469cc89ee5993501c`
- Вердикт конвейера: `yellow` · High 0