Files
houseplan-card/docs/reviews/CODE-REVIEW-360-r2.md
T
2026-08-29 09:05:48 +00:00

253 lines
23 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-360-r2
Issue: #360 «Основной default-цвет декора на главной панели»
Этап: код-ревью (PROCESS.md §2.7)
Заход: r2 · блокирующих циклов израсходовано 0 из 2 (§4: зелёный вердикт бюджет не тратит, #227)
SHA материала ревью: `ae6486de7a8b7eeb87dd313926c52c510fdd3545`
Диапазон: `git diff origin/dev...HEAD` (origin/dev = `e21f02b295dff30339906a7b57071bb4113d8cb3`, 0 позади / 5 впереди)
## Вердикт
**Зелёный.** High: 0, Medium: 0 (ни в скоупе, ни вне скоупа), Low: 0.
## Почему разбор полный, а не по дельте (PROCESS.md §2.10, §7.2)
Формально это второй заход код-ревью (`r1` уже дал зелёный вердикт на SHA
`f91b0ff5`, документ `docs/reviews/CODE-REVIEW-360-r1.md`), но между `r1` и
этим заходом ветка **дважды не сливалась без конфликта** и **дважды
ребейзилась**: сперва на `a041934b` (после чего вершина стала `f91b0ff5`),
затем — после того как в `dev` вошёл #369 (`8ee1c91f`, `2c984d8f`) — на
`e21f02b2` (вершина стала `ae6486de`). Второй ребейз принёс в `dev` новый
инвариант `_decorPointerDown` («только основная кнопка мыши двигает
объекты», #369(e)) — это ровно тот случай из §2.10/§7.2 «ребейз на ушедший
вперёд `dev` — после ребейза это другой код», когда сокращение объёма до
чистой дельты не допускается. Поэтому ниже — полный разбор AC1–AC8 и полный
прогон гейтов на точном SHA `ae6486de`, а не только точка, которую тронул
ребейз.
При этом сам разбор показал, что дельта **фактически** локальна: единственный
продуктовый файл `src/houseplan-editor-runtime.ts` побайтово идентичен между
проверенным в `r1` `f91b0ff5` и текущим `ae6486de` (сверено `git diff
origin/dev...HEAD -- src/houseplan-editor-runtime.ts` — блок `+9` строк в
`_renderDecorBar` тот же, что цитирует `CODE-REVIEW-360-r1.md`). Всё, что
изменилось после `r1`, — один коммит `ae6486de` (тест-only, 1 строка) плюс
служебные последствия ребейза (пересборка бандлов, повторная съёмка
скриншотов, документ `r1` в `docs/reviews/`).
## Закрытие раунда r1
`r1` (SHA `f91b0ff5`) был зелёным без находок — таблица «находка → закрытие»
пуста по построению. Единственное, что требует внимания в `r2`, — не
находка `r1`, а последствие двух последующих ребейзов:
| Что изменилось после r1 | Чем закрыто | Где видно |
|---|---|---|
| Слияние `r1` отменено предохранителем #312 (вершина ветки после `golden:accept` разошлась с проверенным `f91b0ff5`) | Автор подтвердил и показал, что разница после `f91b0ff5` — только штатное принятие 10 golden-эталонов из полного Linux CI (commit `577e654e`, класс D, `Release`+`Baseline-Reviewed` на месте) | Комментарии issue 08:44–08:45Z; `git show --stat 577e654e` — только `demo/golden/baselines/**` |
| Конфликт ребейза на `dev` (дважды, из-за class D путей и `docs/images/**`) | Оба раза — авторский `git rebase origin/dev` + разрешение конфликта в пользу продукта #360 поверх новых генераций; заявлено «продуктовый код после f91b0ff5 не менялся» | Комментарии 08:30Z и 08:55Z; проверено этим ревью — диф `src/houseplan-editor-runtime.ts` идентичен |
| `dev` получил #369(e): «только основная кнопка мыши» в `_decorPointerDown` | `demo/smoke_decor.mjs` получил явный `button: 0` в синтетическом `PointerEvent` для text-инструмента (коммит `ae6486de`) | `git show ae6486de` — 1 строка; перепроверено этим ревью (см. «тест умеет падать» ниже) |
## Унаследовано из r1
Из `docs/reviews/CODE-REVIEW-360-r1.md` (SHA `f91b0ff5`) без повторного
самостоятельного анализа архитектуры контракта принято:
- контракт «один постоянный `hp-color-opacity` в `_renderDecorBar`, без
условия по `_decorTool`» и его противопоставление условному контекстному
picker'у в `_renderDecorSecondary` — код не менялся, перечитан заново в
этом заходе и подтверждён тем же;
- разбор AC4 (событие `hp-color-opacity-change` несёт только `color`/`opacity`,
`fillColor`/`fillOpacity` не затрагиваются) — код обработчика не менялся;
- рассуждение о неотделимости этого выбора от требования ТЗ «не хрупкий
общий счётчик call sites» для `test/color-picker.test.mjs` — сверено, что
сам текст теста в этом заходе не менялся относительно того, что описал `r1`.
Не наследовано, перепроверено заново в этом заходе (потому что либо файл
менялся после `r1`, либо это требование самого §2.10 при неполной дельте):
все гейты (`typecheck`/`test`/`build`/`check-docs`/`no-new-any`/`bundle:budget`),
все три прямых смока, дисциплина «тест умеет падать» для новой строки
`ae6486de`, целостность трейлеров и changelog по всем 5 коммитам диапазона,
уникальность `decor-default-color` в собранном бандле.
## Скоуп диффа
5 коммитов `origin/dev...HEAD`, все с `Issue: #360`:
| Коммит | Класс | User-Visible | Содержание |
|---|---|---|---|
| `822c8000` feat: expose decor default color in main toolbar | A+B+C+D | yes | продуктовый код (+9 строк), `test/color-picker.test.mjs`, `demo/smoke_decor.mjs`, `docs/DECOR-EDITOR.md`, `docs/USER-GUIDE.ru.md`, оба changelog, синхронные бандлы, один экран документации |
| `577e654e` test: accept decor default color golden frames | D | no | только `demo/golden/baselines/**`; `Release: v1.69.0-beta.3` + `Baseline-Reviewed: …/33243321606` на месте |
| `1f032d51` docs: review document for #360 | C | no | публикация `docs/reviews/CODE-REVIEW-360-r1.md` |
| `7daaae72` docs: accept background picker screenshots | C | no | `docs/images/*.png` + `screenshots.json` (переснято канонической workflow из-за смены `sourceFingerprint`) |
| `ae6486de` test: model primary decor pointer explicitly | B | no | 1 строка в `demo/smoke_decor.mjs` — явный `button: 0` |
Диффу соответствует ровно одна цель ТЗ: сделать существующий session-default
`_decorStyle.color`/`_decorStyle.opacity` видимым и управляемым с основной
панели редактора подложки при любом инструменте, ничего больше не меняя.
Заявленный не-скоуп ТЗ (заливка фигур, уже размещённые объекты, persisted
модель, i18n, touch-контракт, другие редакторы) не задет — подтверждено
построчным чтением диффа и списком изменённых файлов (`git diff --stat`):
i18n-файлы, `custom_components/**/*.py`, `manifest.json`/`hacs.json`,
геометрические поля (`layout`, `marker.space`, `open_spans`, рёбра) в дифф не
входят.
## Как проверялось
Зелёного Validate на `ae6486de` нет (не найден/не завершён), все гейты
прогнаны локально мной, на точном SHA:
| Гейт | Статус | Примечание |
|---|---|---|
| `npx tsc --noEmit` | 🟢 | без вывода |
| `npm test` | 🟢 | 1538 тестов: 1537 passed / 1 skipped / 0 failed |
| `npm run build` | 🟢 | `git status` после сборки чист — `dist/**` побайтово совпадает с закоммиченным |
| `npm run bundle:sync` | 🟢 | `custom_components/houseplan/frontend/**` и `demo/srv/assets/**` синхронны, рабочее дерево чисто после |
| `node scripts/check-docs.mjs` | 🟢 | «Documentation checks passed (7 files, 10 external links)» — обязателен, diff трогает `src/**` |
| `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | 🟢 | 9 добавленных строк в 1 файле, новых `any` нет |
| `npm run bundle:budget` | 🟢 | initial View 273697 B / budget 282000 B (headroom 8303 B) |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | выполнен | 3 «прямых совпадения» по символу `_decorStyle`: `smoke_decor.mjs`, `smoke_color_picker.mjs`, `smoke_furniture.mjs` — все три ниже прогнаны |
| `node demo/smoke_decor.mjs` | 🟢 | целевой смок ТЗ (план тестов, п.6); все новые проверки AC1–AC6 (`defaultStyleLivesInMainBarAndContextTray`, `defaultStyleVisibleForEveryDecorTool`, `mainPickerUpdatesOneSessionDefault`, `mainPickerLeavesExistingObjectsUntouched`, `contextPickerUpdatesMainPicker`, `everyNewDecorKindUsesMainDefault`, `mainDefaultDoesNotOverwriteShapeFill`, `objectDialogRefreshesMainDefault`, `narrowToolbarKeepsToolsAndCloseApart`, `narrowToolbarKeepsDefaultPickerUsable`, `defaultPickerReadableInLightAndDark`) — `true` |
| `node demo/smoke_color_picker.mjs` | 🟢 | прямое совпадение smoke-select |
| `node demo/smoke_furniture.mjs` | 🟢 | прямое совпадение smoke-select (мебель — потребитель `_decorStyle`) |
| `npm run golden:verify` | **не прогонялся** | см. обоснование ниже |
| `npm run invariants -- --config …` | **не прогонялся** | diff не трогает геометрию/`layout`/`marker.space`/`open_spans`/рёбра — единственный продуктовый файл правит только UI-toolbar |
| `python -m pytest tests_backend -q` | **не прогонялся** | `custom_components/**/*.py` не тронут |
| performance-профили | **не прогонялись** | не названы в AC; чувствительный к перфу код (рендер геометрии, маски) не тронут — добавлен один статический биндинг в toolbar |
### Обоснование пропуска `golden:verify`
Диф меняет видимый результат (10 golden-кадров редактора + 10 docs-скриншотов),
но локальный `golden:verify` не даёт здесь новой информации:
1. Единственный рендер-значимый продуктовый файл (`src/houseplan-editor-runtime.ts`)
побайтово не менялся с момента, когда полный Linux CI (run `33243705008`,
SHA `237e8d9f`) уже прогнал `golden:verify` **зелёным** по принятым
эталонам того же изменения — это подтверждено выше построчным диффом.
2. Сами эталоны (`577e654e`) приняты штатно —
`npm run golden:accept -- --reviewed` по полному Linux-артефакту, с
`Release`/`Baseline-Reviewed`, не мной и не для «зелёного CI ради
зелёного CI».
3. Docs-скриншоты (`7daaae72`) переснимал не автор локально, а канонический
`Docs screenshots` (`workflow_dispatch`, run `33244132186`), что и требует
§8; `check-docs.mjs` в этом заходе подтверждает: текущий `sourceFingerprint`
совпадает с деревом.
Таким образом, локальный `golden:verify` по всей 143-кадровой матрице был бы
предрелizным гейтом (§8: «Полные наборы — предрелизный гейт, а не гейт
ревью»), не добавляющим доказательства к тому, что уже проверено CI на этом
самом рендер-коде.
### Дисциплина «тест умеет падать»
Для новой (после `r1`) строки `ae6486de` — единственного изменения, не
унаследованного от `r1`, — проверено практически: временно откатил `button: 0`
в синтетическом `PointerEvent` `demo/smoke_decor.mjs` (text-инструмент) —
`node demo/smoke_decor.mjs` упал: `FAILED (1): everyNewDecorKindUsesMainDefault:
expected true, got false` (без основной кнопки `_decorPointerDown` из #369(e)
возвращает `false` и текст не создаётся). Файл восстановлен
(`cp`/оригинал), `node demo/smoke_decor.mjs` вновь зелёный, `git status`
чист.
Для остального диффа (продуктовый код `_renderDecorBar`, основной массив
`smoke_decor.mjs`, `test/color-picker.test.mjs`) дисциплина уже доказана в
`CODE-REVIEW-360-r1.md` (временное изъятие блока `<hp-color-opacity
class="decor-default-color">` роняло и смок, и unit-тест) — код этих файлов
с `r1` не изменился, повторное разрушительное исполнение не добавляет
доказательства.
## Разбор по AC
- **AC1 (постоянная доступность).** `_renderDecorBar` (src/houseplan-editor-runtime.ts:5412)
рендерит новый `hp-color-opacity.decor-default-color` безусловно (вне
`_renderDecorSecondary`, где картинка контекстного picker'а зависит от
`_decorTool`). В исходнике и в собранном бандле
(`houseplan-editor-runtime-DpcuLOlh.js`) ровно одно вхождение класса
`decor-default-color` (проверено `grep -c`). Смок `defaultStyleVisibleForEveryDecorTool`
перебирает все 8 инструментов. **Доказано чтением + browser-смоком.**
- **AC2 (все новые типы).** Смок `everyNewDecorKindUsesMainDefault` создаёт
line/rect/ellipse/text/furniture после смены default и сравнивает
`color`/`opacity` каждого с новым значением. **Доказано смоком.**
- **AC3 (существующее неизменно).** `mainPickerLeavesExistingObjectsUntouched`
сравнивает JSON контрольного объекта побайтово до/после смены default.
**Доказано.**
- **AC4 (заливка независима).** `mainDefaultDoesNotOverwriteShapeFill`:
`fill_color`/`fill_opacity` новых rect/ellipse остаются прежними. В коде
обработчик пишет `{ ...this.host._decorStyle, ...e.detail }`, а `e.detail`
из `hp-color-opacity-change` содержит только `color`/`opacity` (проверено
чтением `src/hp-color-opacity.ts`, событие не несёт fill-полей).
**Доказано кодом и смоком.**
- **AC5 (единый state).** `mainPickerUpdatesOneSessionDefault` (main→context),
`contextPickerUpdatesMainPicker` (context→main), `objectDialogRefreshesMainDefault`
(сохранение properties-диалога существующей линии обновляет main picker
через уже существующий `_decorSaveShape`, диффом не изменённый). Оба
picker'а читают один `this.host._decorStyle` — «одно число, один источник»
выполнено конструктивно, а не two independent renders одного значения.
**Доказано.**
- **AC6 (responsive/a11y).** `narrowToolbarKeepsToolsAndCloseApart` (560×820:
инструменты и Undo/Redo не наезжают на закреплённый `.editbar-end`, все 8
кнопок и close на месте), `narrowToolbarKeepsDefaultPickerUsable` (popover
целиком в viewport), `defaultPickerReadableInLightAndDark` (trigger 40×40,
`visibility: visible` в обеих темах) — все `true`. Golden/screenshot-ревью
по AC6 — по решению самого ТЗ (план тестов, п.6, подтверждено зелёным
SPEC-REVIEW-360-r2) предрелизный гейт; здесь дополнительно подтверждено,
что полный CI уже прогнал его зелёным на этом же рендер-коде (см. выше).
**Доказано смоком + чтением; golden — по плану ТЗ.**
- **AC7 (совместимость и гейты).** Новых persisted-полей и i18n-ключей нет
(`git diff --name-only` не содержит `src/i18n/*.json` и
`custom_components/**/translations/**`); `decor.color`/`space.opacity`
переиспользованы, значения в `ru.json` («Цвет»/«Прозрачность») совпадают с
текстом `USER-GUIDE.ru.md`. `_decorStyle` остаётся session-only полем.
typecheck/test/build/bundle-budget/no-new-any зелёные. **Доказано чтением
и гейтами.**
- **AC8 (документация).** `docs/DECOR-EDITOR.md` и `docs/USER-GUIDE.ru.md`
описывают постоянный main-bar picker, его связь с контекстным и
сохранение через properties-диалог; оба changelog правлены в том же
коммите `822c8000`, что и продуктовый код, со ссылкой на #360.
`check-docs.mjs` зелёный. **Доказано.**
## Что проверено и корректно
- Единственная точка изменения продуктового кода — 9 строк в
`_renderDecorBar`, без второй модели цвета и без нативного
`<input type="color">` (unit-тест на отсутствие таких input по всем
`src/**` зелёный).
- «Одно число — один источник» выполнено конструктивно: main и context
picker — два рендера одного и того же поля `_decorStyle`, не два разных
вычисления одной величины.
- Провенанс всех 5 коммитов корректен: терминальные трейлеры `Issue: #360` +
ровно один `User-Visible`, `Release`+`Baseline-Reviewed` на коммите класса D
(`577e654e`), оба changelog — в том же коммите, что и продуктовый код.
- Бандлы (`dist/**`, `custom_components/houseplan/frontend/**`,
`demo/srv/assets/**`) синхронны с исходником побайтово после чистой
пересборки в этом заходе.
- Единственное послеревьюшное изменение (`ae6486de`) — механическая, точечная
правка теста под независимо принятый инвариант #369(e), продуктовый код и
контракт #360 не задевает; проверена на способность падать.
## Чего не проверял
- `npm run golden:verify` целиком — обоснование выше (рендер-код не менялся
с зелёного полного CI-прогона на этом же коде; локальный прогон 143-кадровой
матрицы не добавляет доказательства и является предрелизным гейтом).
- `npm run invariants` — diff не касается геометрии/`layout`/`marker.space`/
`open_spans`/рёбер.
- `pytest tests_backend` — `custom_components/**/*.py` не менялся.
- Производительность — не названа в AC, чувствительный к перфу код не тронут.
- Ручное визуальное тестирование в браузере вне Playwright — не проводилось;
вместо него — целевой и два смежных browser-смока плюс явная проверка
«тест умеет падать» на живом бандле для единственной новой строки.
## Итог
Реализация дословно соответствует контракту ТЗ (`small`, revision 2, зелёное
SPEC-REVIEW-360-r2): один постоянный `hp-color-opacity` на главной панели,
без побочных изменений в персистентной модели, заливке фигур, существующих
объектах, i18n или touch/View-контракте. Оба предыдущих откатa слияния
(#312 и конфликт ребейза) не были содержательными циклами код-ревью — код
никто не читал, вердикт `r1` не аннулирован, а этот, второй, заход —
независимый полный разбор на итоговом, дважды ребейзнутом SHA, поскольку
дельта после `r1` не локальна по правилу §2.10/§7.2. Единственное
послеревьюшное изменение — однострочная тестовая адаптация к независимо
принятому #369(e), проверенная на способность падать. Все AC1–AC8 доказаны
браузерным смоком, unit-тестом или чтением кода. Оснований для Medium/High
нет.