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

22 KiB
Raw Blame History

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, описанное как «расширение теста»). Все три чинятся правкой текста ТЗ в пределах той же ревизии, без изменения архитектуры и без обращения к владельцу — вопросы технические, не продуктовые.