Files
houseplan-card/docs/reviews/SPEC-REVIEW-33-r1.md
T
2026-08-30 09:26:20 +00:00

267 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 #33 — r1
- Issue: https://github.com/Matysh/houseplan-card/issues/33
- Этап: ревью ТЗ (PROCESS.md §2.4)
- ТЗ: `docs/specs/033-config-schema-lifecycle.md`, ревизия 2
- SHA материала: `8335191bff03cacf5e8dfa1dbc6ef3102e2ceb69` (docs-only коммит)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Вердикт: **жёлтый**
## Скоуп
Задача инженерная (страховка от schema drift): генерируемый манифест схемы
(Voluptuous → JSON), parity-тест frontend/backend enum с машиночитаемым
allow-list, тест полноты `config-field-registry.mjs`, три lifecycle-фикстуры.
Поведение конфига не меняется. Класс A (schema-потребители), полный трек —
решение аналитики 2026-08-15 не оспаривается. Продуктовая рамка по
`docs/SCOPE.md`: задача не закрывает ни один Core user job напрямую, это
защитная инфраструктура под J6 («Keep the plan true as the home evolves»,
конкретно — «не испортить старый план молча»); ТЗ сам называет это прямо в
разделе «Что человек увидит» и не пытается выдать инженерную работу за
пользовательскую ценность. Замечаний по соответствию SCOPE нет.
## Как проверялось
Это первый заход (r1) на текущей ревизии ТЗ — раздел «Унаследовано из
r<N-1>» не нужен, разбор полный.
1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` §2.4/§2.10/§7.1,
`docs/CONFIG-COMPATIBILITY.md` (канонический документ этой подсистемы).
2. Прочитано тело issue #33 и все три комментария (актуализация 2026-08-14,
актуализация 2026-08-30 с ревизией решения, объявление ревизии 2).
3. Прочитан весь текст ТЗ ревизии 2 целиком.
4. Каждое фактическое утверждение ТЗ и опорного комментария сверено с текущим
деревом (не с памятью и не с рассказом автора):
- `scripts/config-field-registry.mjs` — прочитан целиком, посчитаны записи;
- `custom_components/houseplan/validation.py` — найдены оба `vol.Remove`
(`aspect`, `segments`), найдены `extra=vol.ALLOW_EXTRA`/`PREVENT_EXTRA`
по всему файлу;
- `custom_components/houseplan/__init__.py` — проверено, что импорт
`custom_components.houseplan.validation` без `homeassistant` падает
(воспроизведено локально: `ModuleNotFoundError`), что подтверждает
собственную оговорку ТЗ о заглушках;
- `src/logic.ts`, `src/types.ts`, `src/plan-optimizer.ts`,
`src/houseplan-editor-runtime.ts`, `src/zero-walls.ts`, `src/vacuum.ts` —
проверено существование (или отсутствие) именованных экспортируемых
enum-констант для каждой из 7 пар, названных в Блоке 2;
- `scripts/config-audit.mjs` (весь файл, 103 строки) и
`test/config-audit.test.mjs` — проверено текущее поведение exit-code;
- `src/houseplan-card.ts:3893-3895`, `src/houseplan-editor-runtime.ts:9600-9618`
— проверены оба заявленных «реализованных жизнью» решения (show_all,
weather_entity);
- `package.json` — сверено реальное имя npm-скрипта.
5. Ни один из артефактов, которые Блок 1–3 объявляют новыми
(`scripts/dump-config-schema.py`, `scripts/config-schema-manifest.json`,
`scripts/schema-compat-allowlist.mjs`, `test/config-schema-parity.test.mjs`,
`test/fixtures/config-lifecycle/`), в дереве не существует — подтверждено
`ls`. Это ожидаемо: этап ТЗ, кода ещё нет.
6. Дешёвый гейт: `npx tsc --noEmit` на HEAD — чисто (0 ошибок). `npm
test`/`npm run build`/`node scripts/check-docs.mjs` не гонял: diff этого
раунда — один файл, `docs/specs/033-config-schema-lifecycle.md`, ничего в
`src/**` не тронуто, отпечаток скриншотов не мог устареть. Прогон
test/build на неизменной кодовой базе не сказал бы ничего об этой ревизии
ТЗ (кода по ней ещё нет), поэтому не соразмерен задаче ревью ТЗ.
7. Смоки/golden/инварианты модели/perf — не прогонял: этап ТЗ, продукт не
меняется, ни один из этих гейтов не применим до появления кода.
## Находки
Все три ниже — **Medium, в скоупе задачи** (High нет). Они не про
«разработчику будет неудобно», а про то, что ТЗ утверждает как решённый факт
то, что не подтверждается текущим деревом или существующим словарём
подсистемы — то есть ровно тот вид догадки, который проходит ревью, потому
что выглядит решением.
### M1 — статус `implemented` не существует ни в перечислении, ни в каноне
Блок 2 (Актуализация registry): «4 реализованных решения переводятся в
статус `implemented`» (show_all, weather_entity, ripple, aspect/segments).
Проверено чтением: `CONFIG_FIELD_STATUSES` в
`scripts/config-field-registry.mjs:362-369` перечисляет ровно шесть значений
— `current`, `decision-required`, `deprecated-read`, `migrate-on-write`,
`migrate-on-settings-save`, `drop-on-validation` — `implemented` среди них
нет. Канонический `docs/CONFIG-COMPATIBILITY.md`, раздел «Status meanings»,
описывает семантику тех же (без `current`) значений и тоже не знает
`implemented`.
Более того, все четыре названных поля УЖЕ носят точный статус механизма:
`show_all` → `migrate-on-write` (строка 34), `weather_entity` →
`migrate-on-settings-save` (76), `display=ripple` → `deprecated-read` (90),
`aspect`/`segments` → `drop-on-validation` (146, 160) — и эти статусы
подтверждены чтением кода (`houseplan-card.ts:3895`,
`houseplan-editor-runtime.ts:9618`, `_dropLegacySegments`,
`validation.py:1590,1670`). Это не ось «pending → done»: `migrate-on-write`
описывает *постоянно действующий* механизм, а не незавершённую задачу —
поле остаётся `migrate-on-write`, пока открыто окно read-совместимости,
независимо от того, когда код появился. Приравнивание «решение реализовано
жизнью» к новому статусу `implemented` не стыкуется со словарём, который сам
же документ (`CONFIG-COMPATIBILITY.md`) объявляет каноническим.
**Воспроизведение:** `grep -n "CONFIG_FIELD_STATUSES" -A8
scripts/config-field-registry.mjs` — шесть значений, `implemented` нет;
`grep -n "^| \`" docs/CONFIG-COMPATIBILITY.md` — то же самое в таблице.
**Чем чинится:** ТЗ должно явно сказать одно из двух — либо это НЕ смена
`status` (тогда описать, что именно меняется — `migration`/`compatibility`
как текст, статус остаётся прежним, точным), либо это добавление седьмого
значения в перечисление и таблицу CONFIG-COMPATIBILITY.md, с объяснением,
как оно взаимодействует с AC4 (тест полноты) и AC6 (exit-коды audit).
Сейчас ни то, ни другое не написано — решение выдано как готовое, но оно не
существует в модели данных, которой должно принадлежать.
### M2 — 4 из 7 «const-деклараций фронта» для parity-теста не существуют
Блок 2: «parity-тест ... сверяет enum-пары с const-декларациями фронта:
fill_mode ↔ `SPACE_FILL_MODES`/`ROOM_FILL_MODES`, display ↔ `DISPLAY_MODES`,
opening.type, tap_action ↔ `TAP_ACTIONS`, vacuum.trail_mode,
zero_wall_style, bg_mode» — семь пар представлены как один и тот же,
уже готовый механизм.
Проверено чтением: именованные экспортируемые массивы существуют только для
3 из 7 — `SPACE_FILL_MODES`/`ROOM_FILL_MODES`, `DISPLAY_MODES`, `TAP_ACTIONS`
(все в `src/logic.ts:898-919`, экспорт подтверждён). Для остальных четырёх
такой декларации во фронтенде нет:
- `opening.type` — TS union-тип `'door' | 'window' | 'gate' | 'passage'`
(`src/types.ts:212`), не runtime-значение; enum нельзя прочитать без
отдельного массива;
- `vacuum.trail_mode` — только inline-литерал
`['never', 'cleaning', 'always']` внутри `if` в
`src/plan-optimizer.ts:285`, не экспортируется;
- `zero_wall_style` — только строковые литералы `'dashed'`/`'solid'`,
разбросанные по `houseplan-editor-runtime.ts`, `houseplan-onboarding-runtime.ts`,
`zero-walls.ts:53`, без единого списка;
- `bg_mode` — на бэкенде есть `_BG_MODE` (`validation.py:1280`), на фронте
сопоставимой именованной константы не нашлось.
**Воспроизведение:** `grep -rn "OPENING_TYPES\|trail_mode.*=.*\[\|ZERO_WALL_STYLES\|BG_MODES" src/*.ts`
— пусто для всех четырёх.
**Чем чинится:** не архитектурная проблема (ввести 4 новых экспортируемых
константы — тривиально и не меняет поведение), но ТЗ сейчас читается так,
будто вся инфраструктура для сравнения уже на месте одинаково для всех
семи пар — это неверно для четырёх. Раз ТЗ прямо перечисляет эти пары как
цель Блока 2, минимальная правка — явно назвать, что для этих четырёх полей
работа включает добавление разделяемой константы (или иной механизм
чтения TS union), а не только «сверить».
### M3 — AC6 описывает несуществующее поведение как «расширение теста»
Блок 3: «Юнит фронта: `config-audit.mjs` на этих фикстурах даёт ожидаемые
counts (clean / migration available), exit-codes различимы — расширение
существующего `test/config-audit.test.mjs`.» AC6: «`config-audit.mjs`
различает exit-codes на фикстурах (юнит).»
Проверено чтением всего `scripts/config-audit.mjs` (103 строки) и
`test/config-audit.test.mjs`: сегодня `process.exitCode` устанавливается
в `2` только при ошибке разбора/использования (`--json` без файла, битый
JSON, неизвестный флаг); при успешном разборе `exitCode` остаётся
дефолтным (0) **независимо от количества и статуса находок**. Различения
«clean» и «migration available» через exit-code в инструменте нет вовсе —
это не существующее поведение, которое расширяется тестом, а новая
CLI-логика, которую только предстоит спроектировать и написать.
**Воспроизведение:** чтение `scripts/config-audit.mjs:56-103` — единственные
присвоения `process.exitCode` на строках 74, 79, 102, все на ветках ошибок.
**Чем чинится:** ТЗ должно явно зафиксировать контракт — какие коды что
означают (например: 0 — чисто, 1 — есть находки со статусом
`deprecated-read`/`migrate-on-write`/…, 2 — ошибка, как сейчас), иначе
реализация придумывает эту схему по ходу без утверждённого текста ТЗ, а
AC6 в текущей формулировке нельзя разобрать «доказано / не доказано»: тест
против чего сверяться, если контракт не описан.
### Дополнительно (Low, не блокирует, зафиксировано для правки заодно)
- **L1** — раздел «Что человек увидит до и после» называет команду
`npm run config:audit`; реальный npm-скрипт — `audit:config`
(`package.json:19`, тот же, что описан в `docs/CONFIG-COMPATIBILITY.md`).
Опечатка/инверсия слов, но это единственное окно к видимому эффекту
задачи и должно цитировать точную команду.
- **L2** — шапка ТЗ и оба комментария-актуализации утверждают «registry = 24
записи»; фактический размер `CONFIG_FIELD_REGISTRY` на HEAD — 27 элементов
(24 литеральных объявления + 3, порождённые `.map(['entity','attr','unit'])`
на строках 346-359). Не влияет ни на один AC (тест полноты не хардкодит
число), чисто фактическая неточность в вводных цифрах ревизии.
## Что проверено и корректно
- Оба «обязательных первых» продуктовых раздела (§7.1) на месте и честны:
сценарий и «что человек увидит» прямо говорят «ничего не меняется
визуально», без попытки выдать инфраструктуру за пользовательскую
ценность.
- Все обязательные разделы §7.1 присутствуют: сценарий, что видит
пользователь, проблема, скоуп/не-скоуп, контракт поведения, UX/i18n,
модель данных и миграция, критерии приёмки с доказательствами, план
автотестов, риски, откат, release-артефакты, принятые предположения.
- Ключевое архитектурное решение ревизии 2 (манифест генерируется из
Voluptuous, а не пишется руками на 212 путей) — правильный ответ на
находку из предыдущей актуализации (человекописанный registry систематически
отстаёт от кода) и не пытается зафиксировать «212» как проверяемое число:
AC1 проверяет 100%-покрытие и байт-идентичность, а не конкретную цифру —
это ограждает тест от дрейфа числа путей в будущем.
- Утверждения о «уже реализованных жизнью» решениях подтверждены чтением
кода: `settings.show_all` действительно удаляется при материализации
(`houseplan-card.ts:3895`), `settings.weather_entity` — при сохранении
настроек (`houseplan-editor-runtime.ts:9618`), `spaces[].aspect` и
`spaces[].segments` действительно `vol.Remove` в схеме
(`validation.py:1590,1670`). Это не догадки автора — это точное описание
текущего состояния.
- Собственная оговорка ТЗ о необходимости «заглушек родительских пакетов»
для импорта `validation.py` без `homeassistant` — подтверждена
независимо: `custom_components/houseplan/__init__.py` действительно
импортирует `homeassistant.components.frontend` на верхнем уровне, и
прямой импорт `custom_components.houseplan.validation` без HA
воспроизводимо падает `ModuleNotFoundError`. Автор не спрятал эту
сложность — назвал её и предложил решение.
- Не-скоуп сформулирован конкретно и без утечки: судьба
`group_lights`/`exclude_integrations` явно оставлена #44, здесь они
получают только паспорт `allow-extra` — согласуется с текстом issue.
- Имена реальных identifiers, которые ТЗ действительно проверило (не
выдумало): `SPACE_FILL_MODES`, `ROOM_FILL_MODES`, `DISPLAY_MODES`,
`TAP_ACTIONS` — все существуют и экспортируются из `src/logic.ts` именно
под этими именами.
- Откат — тривиален и честен (только новые файлы и данные-паспорта, рантайм
не тронут).
## Чего не проверял
- Не гонял `npm test`, `npm run build`, `node scripts/check-docs.mjs` — diff
этого раунда не касается `src/**` (только spec-файл), эти гейты не
сказали бы ничего нового о самой ревизии ТЗ; см. «Как проверялось», п.6.
- Не гонял смоки, golden, инварианты модели, performance-профили — этап ТЗ,
ни один продукт-путь не изменён, эти гейты неприменимы до реализации.
- Не проверял корректность оценки сложности (6/10) и риска (6/10) из
комментария 2026-08-30 по существу — это управленческая оценка, не
предмет ревью ТЗ.
- Не искал независимо каждый из decor/openings/marker путей на предмет ещё
не найденных `PREVENT_EXTRA`-узлов сверх `openings[].host` (Партиция и
Wall варианты, `validation.py:1500-1516`) — этого узла достаточно, чтобы
показать, что формулировка Блока 3 «future-поля переживают валидацию
losslessly на всех уровнях» не универсальна; решил не расширять список
находок мимо M3 без нового наблюдения, но отмечаю здесь на случай, если
автор при доработке ТЗ захочет пройтись по всем `extra=` в
`validation.py` целиком, а не точечно.
## Гейты — сводка
| Гейт | Прогнан | Результат |
|---|---|---|
| `npx tsc --noEmit` | да | чисто, 0 ошибок |
| `npm test` | нет | diff docs-only, не относится к предмету ревью ТЗ |
| `npm run build` | нет | то же |
| `node scripts/check-docs.mjs` | нет | diff не касается `src/**` |
| смоки / golden / инварианты / perf | нет | этап ТЗ, продукт не меняется |
## Итог
0 High, 3 Medium (все в скоупе, все — про утверждения ТЗ, не подтверждённые
текущим деревом или каноническим словарём), 2 Low (правятся заодно).
Вердикт жёлтый: архитектурное решение ревизии 2 верное и хорошо
обосновано, но три места выдают непринятое или несуществующее решение за
факт (M1: статус `implemented`, M2: отсутствующие frontend-константы для
4 из 7 enum-пар, M3: несуществующее exit-code-поведение `config-audit.mjs`,
описанное как «расширение теста»). Все три чинятся правкой текста ТЗ в
пределах той же ревизии, без изменения архитектуры и без обращения к
владельцу — вопросы технические, не продуктовые.