diff --git a/docs/reviews/SPEC-REVIEW-33-r1.md b/docs/reviews/SPEC-REVIEW-33-r1.md new file mode 100644 index 00000000..468c9f40 --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-33-r1.md @@ -0,0 +1,266 @@ +# 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» не нужен, разбор полный. + +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`, +описанное как «расширение теста»). Все три чинятся правкой текста ТЗ в +пределах той же ревизии, без изменения архитектуры и без обращения к +владельцу — вопросы технические, не продуктовые.