Files
houseplan-card/docs/reviews/CODE-REVIEW-33-r1.md
T
2026-08-30 09:59:51 +00:00

212 lines
21 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-33-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/33
- ТЗ: docs/specs/033-config-schema-lifecycle.md (ревизия 3, зелёное ревью SPEC-REVIEW-33-r2)
- Ветка: `issue/33-config-schema-lifecycle`
- SHA материала ревью: `572cf928969fd8849299170ba3d42ff61d7ef423` (`git rev-parse HEAD`)
- Заход: r1 (первый код-ревью этого issue; ревью ТЗ не расходует бюджет код-ревью, §10.4/§2.10)
- Диапазон: `origin/dev..HEAD` — 4 коммита (`24681ee2` feat, `867f4762` feat, `f4318721` fix, `572cf928` build)
## Скоуп
Ревизия 3 ТЗ, три блока:
1. Генерируемый манифест схемы (`scripts/dump-config-schema.py` → `scripts/config-schema.json`, 265 листовых путей), pytest на свежесть.
2. Parity-тест 8 enum-пар backend↔frontend через `scripts/schema-compat-allowlist.mjs`; тест полноты `config-field-registry.mjs`; новые `as const`-декларации (`OPENING_TYPES`, `VACUUM_TRAIL_MODES`, `ZERO_WALL_STYLES`, `BG_MODES`); паспорта registry (`enforcedBy`, `schema: 'allow-extra'|'lovelace-card'`, 3 новых current-паспорта).
3. Lifecycle-фикстуры (oldest/current/future), pytest на lossless/round-trip; `config-audit.mjs` — контракт exit-code 0/3/2.
Рантайм-поведение не меняется (заявлено ТЗ, подтверждено чтением: новые `as const`-массивы нигде не потребляются продуктовым кодом, кроме вывода типа).
## Как проверялось
Дешёвые гейты прогнаны лично на SHA `572cf928`:
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | `npx tsc --noEmit` | чисто |
| unit | `npm test` | 1608 pass / 0 fail / 1 skipped (совпадает с хендоффом) |
| build + сверка бандла | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | идентичны |
| три копии бандла | `npm run bundle:sync` | git status чист после — дерево уже синхронизировано |
| docs fingerprint | `node scripts/check-docs.mjs` | "Documentation checks passed (7 files, 10 external links)" — обязателен, т.к. diff трогает `src/**` (sun.ts, types.ts) |
| бюджет | `npm run bundle:budget` | `initial View: 279147 B gzip (budget 300000 B, headroom 20853 B)` — совпадает с заявленным |
| backend, новый файл | `python3 -m pytest tests_backend/test_config_schema_manifest.py -q` | 3 passed (после `pip install pytest pytest-asyncio voluptuous` — в песочнице их не было) |
| backend, полный `tests_backend` | `python3 -m pytest tests_backend -q` | падает на сборе (`ModuleNotFoundError: homeassistant` в `test_coordinate_canonicalization.py`) — **не регрессия этой задачи**: файл не тронут диффом и не подпадает под `test_ha_*` исключение из `conftest.py`; ограничение окружения без HA, а не дефект #33 |
| дамп свежий | `python3 scripts/dump-config-schema.py --check` | `manifest fresh: 265 paths` |
| parity + audit unit | `node --test test/config-audit.test.mjs test/config-schema-parity.test.mjs` | 8/8 pass |
| model-invariants | `npm run invariants -- --config test/fixtures/config-lifecycle/{oldest-supported,current,future-fields}.json` (все три; diff трогает `types.ts`/geometry-смежные lifecycle-фикстуры) | все три: "Инварианты выполнены: ссылки разрешимы, записи толщины находятся"; на current/future — 4 информационных наблюдения (exact-endpoints вместо ключа), не нарушения |
| мутанты (по делу, не полный набор — дорогой гейт, PROCESS §8/§4) | `node scripts/mutation-gate.mjs --id=schema-manifest-enum-drift` и `--id=registry-selector-dead-decision` | оба «поймано 1 из 1» |
**Не прогонял и почему:**
- `python -m pytest tests_backend -q` (полный набор с HA) — недоступен в песочнице (нет `homeassistant`, нет `.venv-backend`); согласно AGENTS.md это ожидаемое ограничение локальной среды, а не гейт этой задачи. Новый файл `test_config_schema_manifest.py` — единственный, добавленный диффом, — прогнан отдельно и зелёный.
- Полный `npm run mutation-gate` (весь реестр мутантов) — дорогой предрелизный гейт (документировано в самом скрипте: «место — перед стабильным релизом»); прогнаны только два новых мутанта, относящихся к диффу.
- Браузерные смоки — `node scripts/smoke-select.mjs --base origin/dev --head HEAD` вернул **НЕОПРЕДЕЛЁННОСТЬ**: ни один из 205 смоков не связан с изменёнными символами (`BG_MODES`, `OPENING_TYPES`, `VACUUM_TRAIL_MODES`, `ZERO_WALL_STYLES` и их типы). Проверено чтением: `grep` по `src/*.ts` показывает, что эти константы нигде не потребляются продуктовым кодом кроме вывода типа — они существуют исключительно ради `test/config-schema-parity.test.mjs`. Раз рантайм-поведение этих символов не меняется, смок не может ничего увидеть; решение — не гонять сверх того, что автор уже прогнал (`smoke_space_card`, `smoke_decor` — сами по себе не обязательны, но безвредны).
- `npm run golden:verify` — diff не меняет рендер/геометрию/стили, только типы и build/test-инфраструктуру; не запускал.
## Находки
Все три — Medium, все в скоупе задачи (правятся в этом же issue, без Hig
h и без нового issue).
### M1 — AC7-тест не может поймать то, для чего он написан
`test/config-schema-parity.test.mjs:96-98`:
```js
test('#33 AC7: no production source imports the schema manifest', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', ...), 'utf8')
+ readFileSync(new URL('../src/houseplan-editor-runtime.ts', ...), 'utf8');
assert.ok(!source.includes('config-schema-manifest'), ...);
});
```
Манифест был переименован в коммите `f4318721` (`scripts/config-schema-manifest.json` →
`scripts/config-schema.json`, ровно из-за конфликта с глобом HACS `*manifest.json`), но
эта проверка по-прежнему ищет старую строку `'config-schema-manifest'`, которой больше
нет нигде в репозитории ни как файла, ни как валидного пути импорта.
**Воспроизведение (проверено исполнением, не чтением):** дописал в `src/houseplan-card.ts`
строку `fetch('./scripts/config-schema.json')` — реальный импорт манифеста под его текущим
именем — и перезапустил тест:
```
ok 3 - #33 AC7: no production source imports the schema manifest
```
Тест остался зелёным. AC7 («манифест не импортируется бандлом — контракт-проверка
отсутствия импорта») сегодня не проверяет вообще ничего: искомая строка не может
встретиться ни в каком корректном имени импорта.
Отдельно: проверка читает только 2 файла из ~60 в `src/**`, хотя и ТЗ («AC7-контракт
(нет импорта манифеста из src/\*\*)»), и план автотестов говорят про весь `src/**`. Реальную
защиту от роста бюджета сейчас даёт не этот тест, а эмпирический `npm run bundle:budget`
(прогнан лично, см. таблицу выше, budget соблюдён) — но именно регрессионный тест,
который должен ловить будущий случайный импорт, сегодня этого не делает.
**Правка:** искать актуальное имя (`config-schema.json` либо путь
`scripts/config-schema`) и читать весь `src/**` (например, `readdirSync` рекурсивно или
готовый список файлов сборки), а не два жёстко перечисленных файла.
### M2 — Дубликат записи в обоих CHANGELOG и в ARCHITECTURE.md
`docs/CHANGELOG.md:5-17`, `docs/CHANGELOG.ru.md:11-24`, `docs/ARCHITECTURE.md:1503-1550` — один и
тот же абзац/раздел присутствует **дважды подряд**, слово в слово.
**Причина, видна в истории:** `git diff 24681ee2 867f4762 --stat` показывает, что второй
`feat`-коммит (`867f4762`) добавил ровно те же 24/7/7 строк в
`ARCHITECTURE.md`/`CHANGELOG.md`/`CHANGELOG.ru.md`, которые первый (`24681ee2`) уже добавил —
похоже на случайное повторное применение шага «допиши changelog» при двух подряд идущих
коммитах с идентичным сообщением. Результат в `dev`-кандидате: пользователь, открывший
`CHANGELOG.md`, увидит одну и ту же строку изменений дважды подряд; `ARCHITECTURE.md` несёт
два одинаковых заголовка `## Schema as the source of truth (#33, 2026-08-30)`.
**Правка:** убрать вторую копию блока в каждом из трёх файлов (внутри этой же задачи —
Medium в скоупе, отдельный issue не заводится, #202).
### M3 — `config-audit.mjs` расширил exit-code 3 за пределы согласованного контракта, и для `decision-required` это семантически неверно
ТЗ ревизии 3 (согласовано в SPEC-REVIEW-33-r1/r2, зафиксировано в теле issue и в файле
ТЗ, Блок 3): «`3` — migration available (найдены поля со статусом
**migrate-\*/deprecated-read**)». Реализация (`scripts/config-audit.mjs:83-86`) добавляет
ещё два статуса в `MIGRATION_STATUSES`:
```js
const MIGRATION_STATUSES = new Set([
'migrate-on-write', 'migrate-on-settings-save', 'deprecated-read',
'drop-on-validation', 'decision-required',
]);
```
`drop-on-validation` (aspect/segments) можно защитить как «тоже legacy-остаток» — это в духе
контракта, хоть и не названо буквально. Но `decision-required` — это статус
`settings.group_lights` и `settings.exclude_integrations`
(`scripts/config-field-registry.mjs`): по собственному описанию регистри это ДЕЙСТВУЮЩИЕ,
поддерживаемые поля, чья судьба не решена архитектурно (issue #44), а не легаси-хвосты и
не что-то, что нужно мигрировать. Их `migration`-текст в registry прямо говорит: «decide
supported UI versus fixed exclusion rules before removal» — то есть миграции для
пользователя СЕГОДНЯ не существует вообще.
**Сценарий отказа:** владелец экспортирует у себя план, где реально используется
`settings.group_lights` (обычная включённая фича, не легаси), и прогоняет
`npm run audit:config -- house.json` для диагностики. Инструмент возвращает exit-код `3`
«migration available», хотя мигрировать почему-то нечего — поле продолжит жить как есть,
пока #44 не примет решение. Это вводит в заблуждение ровно того человека
(владельца/разработчика), для которого инструмент существует.
AC6 не ловит это: юнит-тест (`test/config-audit.test.mjs`) прогоняет только три фикстуры
Блока 3, ни одна из которых не содержит `group_lights`/`exclude_integrations`, поэтому
расширение осталось незамеченным собственным тестом задачи.
**Правка:** либо сузить `MIGRATION_STATUSES` до буквально согласованного набора
(`migrate-on-write`, `migrate-on-settings-save`, `deprecated-read`, и по желанию
`drop-on-validation` с явным примечанием почему), исключив `decision-required`; либо, если
разработчик хочет сохранить более широкий охват, обновить формулировку AC6/контракта в ТЗ
и явно объяснить, почему «decision-required» тоже сигнализирует «migration available» —
на сегодняшний день это фактически неверно для двух из четырёх статусов набора.
## Что проверено и корректно
- **AC1** (свежесть/детерминизм/полнота манифеста) — доказано исполнением:
`python3 scripts/dump-config-schema.py --check` → `manifest fresh: 265 paths`;
`tests_backend/test_config_schema_manifest.py::test_issue_33_manifest_is_fresh_and_deterministic`
зелёный (byte-identical + `len(fields) > 200`). Дамп корректно fail-closed на
незнакомые валидаторы (90/265 путей `opaque` — ожидаемо и не судится parity-тестом,
риск явно назван в ТЗ).
- **AC2/AC3** (parity + анти-гниение allow-list) — зелёный тест, и я лично сверил все
8 пар вручную по `custom_components/houseplan/validation.py` и итоговому
`scripts/config-schema.json` (fill_mode ×2, display, tap_action, opening.type,
trail_mode, zero_wall_style, bg_mode) — значения манифеста совпадают со схемой построчно.
**Тест умеет падать**: мутант `schema-manifest-enum-drift` (фантомное значение в
манифесте) поймал 1/1 при личном прогоне.
- **AC4** (полнота registry) — зелёный; вручную проверил трансляцию тегированных
вариантов манифеста (`<furniture>`, `<partition>`) в нетегированные пути и совпадение
с построением селектора из registry. **Тест умеет падать**: мутант
`registry-selector-dead-decision` (испорченный selector) поймал 1/1.
Новые записи (`decor_default_style`, `decor kind:'furniture'`,
`openings[].host=partition`) резолвятся в реальные пути манифеста — проверено чтением
и подтверждено прохождением самого AC4-теста (он бы упал на несуществующем пути).
- **AC5** (lossless фикстуры) — `tests_backend/test_config_schema_manifest.py::test_issue_33_lifecycle_fixtures_pass_the_schema_losslessly`
зелёный на всех трёх фикстурах; потери ограничены задокументированными
`spaces[].aspect`/`spaces[].segments` (vol.Remove). Дополнительно прогнал
`npm run invariants` на всех трёх фикстурах — геометрические инварианты (разрешимость
ссылок, ключ записи толщины = ключ решёточного ребра) не нарушены, только
информационные наблюдения на exact-endpoints, не связанные с #33.
`test_issue_33_future_fields_round_trip_exactly` зелёный — будущие поля на всех
уровнях (root/settings/space/marker) переживают валидацию точно.
- **AC6** (exit-коды 0/3/2) — юнит зелёный на всех трёх кодах (см. M3 по существу
контракта: коды технически работают, но множество триггеров шире согласованного).
- **AC7** (нет импорта в бандл, бюджет не растёт) — эмпирически подтверждено
(`npm run bundle:budget` → 279147/300000, не изменилось от базовой линии — прирост
бюджета отсутствует, манифест не тянется рантаймом); регрессионный тест-контракт
сломан, см. M1.
- Регистри: `enforcedBy` у 4 реализованных механизмов подтверждён чтением кода
(houseplan-card.ts материализация show_all, editor-runtime удаление weather_entity,
normalizeDeviceDisplay + `_dropLegacySegments` для ripple, `vol.Remove` для
aspect/segments) — соответствует находкам предыдущей ревизии владельца (актуализация
бэклога 2026-08-30).
- Переименование манифеста (`f4318721`) применено последовательно везде, где он
упоминается (dump-скрипт, allowlist, mutation-gate, оба теста, ТЗ, ARCHITECTURE.md) —
кроме забытой строки в самом AC7-тесте (M1).
- Трейлеры всех 4 коммитов корректны (`Issue: #33` на каждом, `User-Visible: yes` на
обоих `feat`, `no` на `fix`/`build`); оба changelog правлены в том же коммите, что и
`feat` (хотя и с дублированием, см. M2).
- Класс D (`dist/**`, `custom_components/houseplan/frontend/**`) обновлён отдельным
`build`-коммитом (`572cf928`), не смешан с продуктовым кодом — соответствует таблице
классов AGENTS.md.
- Не-скоуп соблюдён: ни рантайм-поведение конфига, ни судьба
`group_lights`/`exclude_integrations` (кроме паспорта) не тронуты; миграций и новых
ключей i18n нет.
## Чего не проверял
- Полный `tests_backend` с реальной Home Assistant (нет `homeassistant`/`.venv-backend` в
этой песочнице) — прогнан только новый файл задачи, который не требует HA.
- Полный набор мутантов (`node scripts/mutation-gate.mjs` без `--id`) — предрелизный
гейт по объёму, прогнаны только два мутанта, введённых этой задачей.
- `golden:verify` и браузерные смоки сверх обоснованного выше — diff не меняет визуал,
smoke-select не нашёл доказанной связи.
- Оговорённая вне скоупа судьба `group_lights`/`exclude_integrations` (#44) — не судил,
это прямо не-скоуп ТЗ.
## Вердикт
Жёлтый. High: 0, Medium: 3, все в скоупе — правятся в этом же issue без нового
цикла-issue (#202). Ядро задачи (генерируемый манифест, parity+allow-list, полнота
registry, lifecycle-фикстуры, exit-коды) реализовано добротно и доказано
исполняемыми тестами, включая мутационное доказательство «тест умеет падать» для
AC2 и AC4. Три находки — тест, который не может поймать регрессию, которую он
называет своей целью (M1), задвоенная документация из-за дублирующего коммита (M2)
и семантически неточный exit-код для одного из двух «decision-required» полей (M3).