Files
houseplan-card/docs/reviews/SPEC-REVIEW-377-r1.md
T
2026-08-29 17:58:51 +00:00

249 lines
22 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.
# SPEC-REVIEW-377-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/377 — «Персист цвета
декора по умолчанию в серверный конфиг»
- Этап: ТЗ на ревью (PROCESS.md §2.4), **полный трек** (выделено из #376в по
SPEC-REVIEW-376-r1 H1: новый персистентный ключ `settings.decor_default_style`
+ кросс-модульный путь записи — критерий §5 «нет новых compatibility-полей»
не проходит)
- ТЗ: `docs/specs/377-decor-default-persist.md`, ревизия 1, зафиксирована
комментарием автора на dev `e2e4cec1`
- Материал ревью: тело issue #377 + оба комментария (аналитика §2.2, объявление
ревизии 1) сверены с `dev` на `e2e4cec1`
- Заход: r1 · блокирующих циклов израсходовано **0 из 4** (полный трек)
## Скоуп ревью
Первый заход — предыдущего вердикта по #377 нет, разбор полный. Проверялось:
соответствие `docs/SCOPE.md`; корректность классификации полного трека (уже
установлена предшествующим SPEC-REVIEW-376-r1, проверена повторно на
согласованность); наличие всех обязательных разделов §7.1; однозначность и
доказуемость каждого AC; отсутствие догадок, выданных за факт — каждое
техническое утверждение ТЗ о текущем коде сверено построчно с исходниками, а
не принято на слово.
Диапазон правки этого раунда — файл `docs/specs/377-decor-default-persist.md`
(добавлен, продуктовый код не менялся): `git diff --stat 4eede5bc..e2e4cec1`
показывает только два review-документа (#375, #376) и новый файл ТЗ.
## Как проверялось
1. Прочитаны целиком `docs/SCOPE.md`, `PROCESS.md` (§2.4, §2.9/§4 бюджет
циклов, §5 критерии трека, §7.1 обязательные разделы, §12 запреты), сверены
с сохранённым в памяти пониманием процесса.
2. Прочитано тело issue #377 и оба комментария (аналитика, объявление ревизии).
3. Прочитан весь текст `docs/specs/377-decor-default-persist.md`.
4. Прочитан предшествующий `docs/reviews/SPEC-REVIEW-376-r1.md` целиком —
проверить, что классификация полного трека и содержание находок этого
документа корректно унаследованы новым ТЗ, а не переизобретены.
5. Каждое фактическое утверждение ТЗ о текущем коде сверено с `dev`:
- `src/houseplan-card.ts:984` — `private _decorStyle: DecorStyle = {
...DEFAULT_DECOR_STYLE }` — подтверждено, только in-memory поле.
- `src/houseplan-editor-runtime.ts:4481, 5306-5419` — оба места записи
(тулбар и контекстный трей) действительно пишут в один и тот же
`this.host._decorStyle` — подтверждено.
- `src/houseplan-editor-runtime.ts:1764` — `_writeConfig()` сериализованный
путь записи, использующий `enqueueSerializedWrite`/`_sendConfigCandidate`
— подтверждено, это общий канал, новый не создаётся.
- `src/houseplan-editor-runtime.ts:9403-9417` (`_saveSettingsDialog`) —
подтверждён паттерн «дефолт = отсутствие ключа» для `bg_color`/
`fill_colors`, на который ссылается контракт п.5.
- `custom_components/houseplan/validation.py:1883` — `vol.Optional(
"settings", default=dict): vol.Schema({...}, extra=vol.ALLOW_EXTRA)` —
номер строки и `ALLOW_EXTRA` подтверждены дословно; используется как
основание раздела «Откат» (старый бэкенд/фронт не ломается на новом
ключе) — корректно.
- `custom_components/houseplan/validation.py:1307-1330` (`_DECOR_COMMON`,
rect/ellipse) — диапазоны, предложенные для схемы
`decor_default_style` (opacity 0..1, width_cm 0.1..100, fill_opacity
0..1), дословно совпадают с уже принятыми диапазонами для decor-фигур —
не придуманы заново.
- `custom_components/houseplan/import_export.py:1145-1168` (`build_space_merge`)
и `:1660-1670` (полный импорт) — подтверждено: пространственный импорт
копирует `target_config = _json_copy(current_config)` и не трогает
глобальный `settings`, полный импорт переносит `settings` целиком за
вычетом `known_devices`/`new_device_ids` только при `!same_source` —
заявление «settings переносится целиком» для AC6 корректно в заявленном
объёме (полный экспорт/импорт, не пространственный).
- `src/editors/decor/geometry.ts:144` — `width_cm: Math.max(0.1, Math.min(
100, Number(style.widthCm) || 0.1))` — существующий явный код конвертации
из `DecorStyle.widthCm` (camelCase) в персистентное поле `width_cm`
(snake_case). Это прямо противоречит утверждению ТЗ (см. находку M1
ниже).
- `src/editors/decor/types.ts:78-85` — интерфейс `DecorStyle` объявлен как
`{ color, opacity, widthCm, fill, fillColor, fillOpacity }` —
**camelCase**, не snake_case, как утверждает ТЗ (см. M1).
- `docs/CHANGELOG.ru.md` (запись #360) — подтверждён факт существования
тулбар-пикера «основной цвет и прозрачность», на который опирается
раздел «Сценарий».
- `demo/smoke_*.mjs`, `debounce()` (`src/houseplan-editor-runtime.ts:722`) —
подтверждена техническая осуществимость дебаунс-записи (используемый
примитив уже существует в модуле), план автотестов не требует нового
инфраструктурного решения.
6. Проверено отсутствие незакрытых меток неопределённости («уточнить», TBD,
голый «?») — не найдено.
7. Проверена структура на соответствие обязательному списку §7.1: сценарий ·
что человек увидит до/после · проблема · скоуп/не-скоуп · контракт
поведения · UX · модель данных и миграция · i18n · AC1…AC7 с доказательством
· план автотестов · риски · откат · release-артефакты — все присутствуют.
8. Гейты (`tsc`/`test`/`build`/`check-docs`/инварианты/смоки) не прогонялись:
diff этого раунда — только новый файл в `docs/specs/`, продуктовый код
(`src/**`, `custom_components/**`) не тронут, гейтам нечего было бы
доказать (тот же вывод, что и в SPEC-REVIEW-376-r1 для аналогичной стадии).
## Находки
### M1 (Medium, в скоупе) — контракт называет формат ключа `DecorStyle` неверно и не как предположение, а как проверенный факт
**Файл:** `docs/specs/377-decor-default-persist.md`, раздел «Контракт
поведения», пункт 7.
Текст: «Формат ключа — snake_case, поля фронтового `DecorStyle`
(`src/editors/decor/types.ts:78`): `color`, `opacity`, `width_cm`, `fill`,
`fill_color`, `fill_opacity`.»
Это не так: `src/editors/decor/types.ts:78-85` объявляет `DecorStyle` в
**camelCase** — `color, opacity, widthCm, fill, fillColor, fillOpacity`.
Snake_case-именование (`width_cm`, `fill_color`, `fill_opacity`) относится к
персистентным полям фигур (`DecorBase`/`DecorRect` в том же файле, строки
10-52), не к `DecorStyle`. Существующий код уже явно разводит эти две
конвенции: `src/editors/decor/geometry.ts:144` содержит ручную конвертацию
`width_cm: Math.max(0.1, Math.min(100, Number(style.widthCm) || 0.1))` именно
потому, что `DecorStyle.widthCm` и персистентное `width_cm` — разные
идентификаторы.
Показательно, что ровно эта развилка (snake/camelCase формат нового ключа)
в предшественнике этой задачи была корректно оформлена как явное
предположение: SPEC-REVIEW-376-r1, раздел «Что проверено и корректно» —
«Раздел «Принятые предположения» корректно выносит технические, не
наблюдаемые пользователем решения (**формат ключа snake/camelCase**, путь
записи только из runtime-редактора) в явный блок». В нынешнем ТЗ #377 такого
раздела «Принятые предположения» нет вообще, а тот же вопрос вместо этого
подан как проверенный факт со ссылкой на конкретную строку — и эта ссылка,
при сверке, факт не подтверждает.
**Почему это находка, а не мелочь.** AC2 требует «конфиг с ключом →
`_decorStyle` = мердж ключа поверх `DEFAULT_DECOR_STYLE`», а AC1/AC6 фиксируют
формат серверного ключа как snake_case. Между snake_case-ключом настроек и
camelCase-полем `_decorStyle` нужен явный маппинг (`width_cm ↔ widthCm`,
`fill_color ↔ fillColor`, `fill_opacity ↔ fillOpacity`); без него код,
написанный «по букве» текущего пункта 7 (взять ключи настроек и наложить их
на `_decorStyle` как если бы имена совпадали), молча не будет мержить три из
шести полей — регресс ровно на тех полях, где имя меняется. Пока
формулировка стоит как «факт со ссылкой», а не как предположение, у автора
кода нет сигнала, что это место требует отдельного решения.
**Чем чинится в этой же задаче:** один абзац, например: «Ключ настроек
snake_case (`color`, `opacity`, `width_cm`, `fill`, `fill_color`,
`fill_opacity`) конвертируется в camelCase-поля `DecorStyle`
(`color`, `opacity`, `widthCm`, `fill`, `fillColor`, `fillOpacity`) по прямому
соответствию имён — по аналогии с уже существующей конвертацией в
`src/editors/decor/geometry.ts:144`. Принято предположительно, поменять
свободно.» Ссылку на `types.ts:78` при этом стоит поправить на то, что там
реально объявлено (camelCase), либо убрать как источник утверждения о
snake_case.
## Что проверено и корректно
- Классификация полного трека обоснована и унаследована корректно: причина
(«новый персистентный ключ + кросс-модульный путь записи», критерий §5 «нет
новых compatibility-полей» не пройден) совпадает с находкой H1
SPEC-REVIEW-376-r1, ТЗ прямо на неё ссылается, а не переизобретает
рассуждение заново.
- Оба Medium предшественника (SPEC-REVIEW-376-r1 M1 «нет раздела откат», M2
«нет AC на текстовые пункты») к #377 не относятся — они были про пункты
(а/б/д/е) исходного #376, которые остались в #376 на `small`; #377 —
только пункт (в), и его собственный раздел «Откат» и AC1…AC7 присутствуют
полностью, каждый со способом доказательства (`unit`/`pytest`/`смок`/
`гейт`).
- Задача в скоупе `docs/SCOPE.md`: полиш уже принятой фичи #360 (главный
тулбар), не открывает новый Core user job, не входит в «Out of scope»;
соответствует «Design consequence» — правка касается только
Background-редактора (admin-only поверхность), View не получает новых
путей записи.
- Продуктовых вопросов владельцу не осталось: решение «персистить в
серверный конфиг» уже принято владельцем 29.08 и зафиксировано в теле
issue; ТЗ на него ссылается, а не переизобретает.
- Сценарий и «что человек увидит до/после» отвечают на оба обязательных
вопроса §7.1 конкретно, без терминов реализации, и соответствуют
фактическому поведению тулбар-пикера #360 (подтверждено записью в
CHANGELOG.ru.md).
- Контракт поведения (пп. 1-6, кроме уже описанного в M1 п.7) построчно
сверен с кодом и подтверждён: легаси-путь без ключа, дебаунс через
существующий примитив, единственный сериализованный канал записи
`_writeConfig`/`expected_rev` (#340), удаление ключа при возврате к
дефолту по образцу `bg_color`/`fill_colors`, запись только из
editor-runtime (холодный View класса #357 не задет — подтверждено: чтения
`_decorStyle` в `houseplan-card.ts` ограничены превью черновика/мебели,
которые в View не рендерятся).
- Диапазоны валидации в наброске схемы (`opacity` 0..1, `width_cm` 0.1..100,
`fill_opacity` 0..1) не придуманы заново, а дословно совпадают с уже
принятыми диапазонами для decor-фигур той же кодовой базы — снижает риск,
что AC1 окажется неисполнимым или несогласованным с соседней схемой.
- AC6 (импорт/экспорт byte-в-byte) корректно ограничен по объёму: заявление
подтверждено для полного экспорта/импорта; пространственный
(`build_space_merge`) импорт глобальный `settings` не трогает вовсе, что не
противоречит контракту (ключ глобальный, не per-space) и не заявлено в AC6.
- i18n корректно отмечен как незадетый — в UI не добавляется новый текст,
проверка существующим паритет-гейтом достаточна.
- Риски названы конкретно вместе со снимающим механизмом (гонка вкладок →
штатный conflict #340 + дебаунс; захламление settings → дебаунс + удаление
ключа при дефолте; обратная совместимость → опциональность в обе стороны),
а не общими словами.
- Release-артефакты (CHANGELOG.md+.ru.md, USER-GUIDE.ru.md) названы по
правилу `docs/specs/README.md` — ключ меняет пользовательское поведение
(переживание перезагрузки), артефакты перечислены явно.
## Чего не проверял
- Гейты (`npx tsc --noEmit`, `npm test`, `npm run build`, `check-docs.mjs`,
инварианты модели, browser-смоки) — не прогонял: diff этого раунда не
затрагивает `src/**` или `custom_components/**`, только новый файл
`docs/specs/377-decor-default-persist.md`. Гейтам, завязанным на код,
нечего было бы доказать на стадии ТЗ (тот же вывод сделан в
SPEC-REVIEW-376-r1 для той же стадии).
- Реализуемость дебаунса «1000 мс» и точную формулировку будущего
предложения в `USER-GUIDE.ru.md` — контракт называет смысл и число, не
финальный текст промо-абзаца; это в пределах допустимой недосказанности
для технической/текстовой детали, не находка.
- Согласованность нового ключа `settings.decor_default_style` с
`scripts/config-field-registry.mjs` — реестр по `docs/CONFIG-COMPATIBILITY.md`
прямо заявлен как неполный («initially covers the known compatibility and
internal-field debt... not yet the complete canonical schema»); регистрация
нового (не легаси) поля не требуется этим документом и станет предметом
код-ревью, если понадобится.
- Точный будущий API взаимодействия дебаунс-таймера с существующей очередью
`_writeChain`/`enqueueSerializedWrite` при одновременном геометрическом
изменении — техническая деталь реализации, агенты решают её сами (§7.1),
не наблюдаема пользователем и не входит в критерии готовности ТЗ.
## Низкая находка (снята с записью)
- Раздел «UX»/«Риски» не содержит явной фразы о влиянии на touch-контракт
(`docs/TOUCH-SUPPORT.md`), которую требует чек-лист DoR §2.5. По существу
влияние действительно нулевое: задача не добавляет новых элементов UI
(«Ничего нового в UI не появляется», уже добавленные тулбар/трей-пикеры
#360 не меняются), поэтому вывод «нет влияния» тривиально следует из уже
написанного текста. Снимаю без правки, а не как повод для цикла — но
фиксирую, чтобы автор при следующей правке ТЗ добавил явную строку «touch:
нет» для формального прохождения DoR-чек-листа без повторной проверки
ревьюером.
## Вердикт
ТЗ структурно полное (все обязательные разделы §7.1 на месте), классификация
полного трека унаследована корректно от SPEC-REVIEW-376-r1 и не оспаривается,
почти все технические утверждения о текущем коде подтвердились построчно.
Единственная находка — M1: контракт называет формат ключа `DecorStyle`
snake_case со ссылкой на конкретную строку кода, которая при проверке
показывает обратное (camelCase), и не помечает это место как предположение,
хотя тот же вопрос в предшествующем ТЗ (#376) был корректно оформлен именно
как явное предположение. Без исправления этого пункта реализация,
написанная «по букве» контракта, рискует не замержить три из шести полей
стиля при чтении настроек — то есть ровно то поведение, которое проверяет
AC2. High-находок нет, Medium — один, в скоупе, чинится в этой же задаче
одним абзацем без нового цикла по существу решения (только техническая
правка формулировки ТЗ).
`Вердикт: жёлтый · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 1 → в задаче`