Files
houseplan-card/docs/reviews/CODE-REVIEW-333-r2.md
T
2026-08-28 06:06:23 +00:00

192 lines
18 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-333-r2
Issue: #333 · Заход: r2 · Блокирующих циклов израсходовано 1 из 2
Материал: `git log --oneline origin/dev..HEAD` = три коммита на текущем
`HEAD` = `4ded9c0b` (`git rev-parse HEAD` сверен непосредственно перед
подведением итогов):
```
4ded9c0b test: the #248 roundtrip fixture is seeded, not first-written (#333 r1-H1)
783cc68b docs: review document for #333 (публикация CODE-REVIEW-333-r1.md)
5a2dd333 fix: plan/optimize passes the junction gate; import stays free by design (#333)
```
Родитель `5a2dd333` — `b37a5987` (= `origin/dev` на момент проверки).
## 0. Важное расхождение с материалом прошлого раунда — рассмотрено и снято
r1-документ (`docs/reviews/CODE-REVIEW-333-r1.md`) называет проверенным
коммитом `c29df29b` с родителем `73406225`. Такого SHA в текущей истории
нет — между r1-ревью и фиксом r1-H1 ветка была перебазирована на ушедший
вперёд `dev`: между `73406225` и текущим родителем `b37a5987` легло 17
чужих коммитов issue #337 (лениво загружаемый рантайм редактора, полная
перестройка бандлинга — 114 файлов, ~34 тыс. строк).
Это ровно сценарий «ребейз на ушедший вперёд dev» из брифа, который по
умолчанию требует полного разбора, а не разбора по дельте. Проверил, что
разбор действительно должен остаться полным или можно сузить:
- **Пересечения файлов нет.** `git diff --stat 73406225 5a2dd333^`
(весь чужой рывок dev) не касается ни одного файла, который трогает
#333 (`custom_components/houseplan/{junction_limits,websocket_api}.py`,
`docs/specs/329-junction-limits.md`, `scripts/mutation-gate.mjs`,
`tests_backend/test_ha_websocket.py`) — только `src/**`, бандлинг и
фронтенд-доки.
- **Номера строк не сдвинулись.** `websocket_api.py:1626`
(`ws_plan_optimize`), `:1783` (`rt.junction_baseline = …`), `:1396`
(`config/set`) — те же номера, что называет r1-документ. Файл ребейзом
физически не задет.
- Поэтому содержательно к делу можно подойти по дельте (закрытие r1-H1),
но **гейты прогнал заново и полностью**, а не унаследовал: у ребейза
такого масштаба легко мог быть скрытый эффект на сборку/реестр
мутаций/бэкенд-набор, а зелёного Validate на этом SHA нет (см. §2). Это
тот случай, где полнота разбора обеспечена не «на всякий случай», а
проверкой конкретного факта (непересечение файлов), которая и позволяет
не заводить AC1–AC4 заново, ограничившись закрытием H1.
## 1. Скоуп
Не изменился с r1: ТЗ (тело issue, small-трек, ревизия 2, оба раунда
spec-ревью зелёные) требует (1) `validate_junction_limits` в
`ws_plan_optimize` после миграции кандидата, наследование по правилу;
(2) обновление `rt.junction_baseline` после успешного optimize; (3)
import/restore вне гейта, докстринг и спека §5 — честно. Продуктовая
рамка (`docs/SCOPE.md`) и корректность `User-Visible: no` — без изменений
с r1, задача только про то, доказан ли контракт и не сломано ли что-то
существующее.
Предмет r2 — единственная правка после r1: тестовая фикстура
`test_plan_optimize_persists_exact_storage_roundtrip_target`
(`tests_backend/test_ha_websocket.py`) теперь сеет исходный конфиг в
хранилище перед вызовом `plan/optimize`, вместо пустого `previous`.
Продуктовый код не тронут ни строкой — весь коммит `4ded9c0b`
ограничен тестовым файлом.
## 2. Как проверялось
Зелёного Validate на `4ded9c0b` нет (сообщение брифа) — прогнал сам,
полностью (обоснование §0):
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | чисто |
| Frontend-юниты | `npm test` | 1439 passed, 0 failed, 1 skipped |
| Сборка + синхрон бандла | `npm run build`; `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js`; сверка списков файлов `dist/houseplan-assets/` и `custom_components/houseplan/frontend/houseplan-assets/` | все три копии идентичны |
| Реестр мутаций | `node scripts/mutation-gate.mjs --check` | зелёный, `junction-limit-optimize-unguarded` зарегистрирован, patch-строка совпадает с реальным кодом (см. ниже) |
| Мутант убивает тест | ручной патч `return validate_junction_limits(...)` → `return {}, None` в `websocket_api.py:1706-1708`, `pytest -k test_333_optimize_refuses_a_crafted_violation` | тест красный (`assert not True`), дерево возвращено (`git status` чист) — тест умеет падать |
| Backend, полный набор | `python -m pytest tests_backend -q` (харнес `pytest-homeassistant-custom-component`+`home-assistant-frontend` установлены в сессии) | **432 passed, 1 skipped, 1 error** (error — известный teardown-артефакт Python 3.12/харнеса, см. §5, не по телу теста) |
| Backend, целевые тесты | `pytest -k "test_plan_optimize_persists_exact_storage_roundtrip_target or test_333_optimize_refuses_a_crafted_violation_and_keeps_the_plan or test_333_optimize_inherits_stored_violations or test_333_optimize_refreshes_the_junction_baseline_cache"` | все 4 PASSED |
| Смок-отбор | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «исполняемого frontend-диффа нет» — браузерные смоки не выбираются, diff не трогает `src/**` |
| check-docs | не гонял | diff #333 (все 3 коммита) не трогает `src/**` ни строкой — критерий запуска не выполнен |
| `npm run invariants` | не гонял | diff не меняет геометрию/рёбра/записи толщины/`layout`/`marker.space`/`open_spans` — только добавляет вызов уже существующего Python-валидатора и правит тест-фикстуру |
| performance-профили | не гонял | не названы в AC, дорогая цепочка уже в executor (#330), диффом не тронута |
| «Одно число — один источник» | рассмотрено | diff не вводит и не меняет ни одной пользовательски видимой величины — `User-Visible: no` во всех трёх коммитах корректен |
Дополнительно проверил рабочее дерево после ручного патча мутанта:
`git status` — чисто, никаких файлов не осталось изменёнными
(`dist/houseplan-card.js` случайно поменял режим/mtime после
`npm run build` — вернул `git checkout -- dist/houseplan-card.js`).
## 3. Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| **H1 (High).** Optimize с пустым `previous` (`expected_config_rev: 0`) на фикстуре #248 засчитывал легитимную стену 6 см как НОВОЕ нарушение П3 (правило «первая запись не может прийти уже сломанной») — существующий зелёный тест `test_plan_optimize_persists_exact_storage_roundtrip_target` падал на `assert response["success"]`. | Коммит `4ded9c0b`: фикстура теперь СЕЕТСЯ в `runtime.config_store` как хранимый документ (`rev: 1`) ПЕРЕД вызовом `plan/optimize` (`expected_config_rev: 1` вместо `0`), поэтому optimize видит непустой `previous` и 6-см стена читается как унаследованное нарушение — ремонтный поток, как в AC2. Ревизии в ассертах сдвинуты на 1 (`config_rev == 2`), storage-утверждения #248 (intent/pending/final/каноническая сериализация) не тронуты. | `tests_backend/test_ha_websocket.py:626-646` (комментарий + новый `async_save`), запуск: `pytest -k test_plan_optimize_persists_exact_storage_roundtrip_target` → **PASSED** (был **FAILED** на `c29df29b`/эквивалентном `5a2dd333`). Полный `pytest tests_backend -q`: 432 passed / 0 failed (было 1 failed) |
| **Medium, вне скоупа.** Бриф ревью ссылался на Validate-run `c29df29b` (33145399107) как на «зелёные дешёвые гейты», но job'ы фронтенда и бэкенда в нём были `skipped`, не `success`, из-за потери диапазона коммитов классификатором после force-push. | Не в этой ветке — заведён отдельно, как и требует правило (Medium вне скоупа → отдельный issue, не «оставлено в тексте»). | #347 (`bug, infra, P1, S1-new`, ссылка на #333) — существует, открыт, метки на месте (проверено `gh issue view 347`) |
Оба пункта r1 закрыты доказательно, не декларативно: H1 — перезапуском
ранее красного теста и полного бэкенд-набора; Medium — проверкой, что
issue реально существует с нужными метками.
## 4. Унаследовано из r1 (без повторной проверки содержания)
Из `docs/reviews/CODE-REVIEW-333-r1.md` (материал: коммит `c29df29b`,
родитель `73406225`) принимаю без повторного разбора — с довеском:
файлы, к которым относятся эти выводы, не менялись НИ РАЗУ между
`c29df29b`/`73406225` и текущим `HEAD`/`origin/dev` (см. §0, номера строк
совпадают), поэтому дельта их не задевает:
- **Техническая точность контракта п.1–3** — вызов
`validate_junction_limits` в `_validate_optimize_cpu` после миграции
кандидата, то же место конвейера, что в `config/set`; `JunctionLimitError`
в except-списке перестал быть мёртвым.
- **AC1 (крафтованный payload отклонён, `config`/`rev` побайтово
неизменны)** и структурная гарантия неизменности layout (отказ до
первого `async_save_*`).
- **AC2 (наследование легаси-нарушения в optimize)** — тест
`test_333_optimize_inherits_stored_violations`.
- **AC3 (обновление `rt.junction_baseline`)** — симметрия с
`config/set` (`websocket_api.py:1396` / `:1783`).
- **AC4 (докстринг + спека §5 честны)** — текст `junction_limits.py:1-15`
и `docs/specs/329-junction-limits.md` §5 (абзац о периметре, строки
127-134) — сверил построчно ещё раз (§2, «Как проверялось» этого
документа не потребовалось трогать код, только перечитать текст) и
подтверждаю: абзац идентичен тому, что видел r1 — diff между
`origin/dev...HEAD` для этого файла ограничен ровно этим одним
абзацем, второй раз не добавлялся и не менялся.
- **Реестр мутаций** — `junction-limit-optimize-unguarded` описан честно;
я самостоятельно перепрогнал и `--check`, и ручное убийство мутанта
(не чисто унаследовал — но результат совпадает с r1).
- **Паритет П1–П4** — `test_parity_with_the_frontend_checks` не тронут,
прошёл в составе полного набора.
- **Трейлеры** — `Issue: #333`, `User-Visible: no` верны во всех трёх
коммитах этого раунда (проверено заново, т.к. коммиты новые).
- **Соответствие малому треку** — не переоценивал; объём (закрытие
одного High одним тестовым коммитом) трек не нарушает.
## 5. Находки
Новых находок нет. High: 0. Medium: 0.
## 6. Что проверено и корректно
- r1-H1 закрыт по существу, а не косметически: подтверждено, что
предмет теста (#248, байт-точность storage) не пострадал — все
storage-ассерты (`intent_write`, `pending`, `final_config`,
`final_layout`, каноническая сериализация JSON) остались в тесте
дословно, изменились только сид и ожидаемые номера ревизий.
- Семантика фикса согласована с контрактом: посеянный `previous`
идентичен `candidate` (`source["config"]` без изменений), то есть это
echo-оптимизация того же класса, что AC2 — 6-см стена наследуется, а
не легализуется заново; ровно то поведение, которое и должен доказывать
тест про репутацию хранилища.
- Полный бэкенд-набор (432/0/1 error-teardown) и целевые 4 теста
зелёные лично прогнанными командами, не по докладу автора.
- Мутант `junction-limit-optimize-unguarded` реально убивает целевой
тест — проверено вручную в этом раунде.
- Рабочее дерево после всех ручных экспериментов чистое
(`git status` пуст).
- Ребейз на ушедший вперёд `dev` (#337) не затронул ни один файл этой
задачи — подтверждено диффом файлов, а не предположением.
## 7. Чего не проверял и почему
- **AC1–AC3 заново «с нуля»** — не пересобирал заново обоснование
каждого, ограничился перезапуском тестов и сверкой, что r1-текст
описывает код без расхождений (см. §4) — файлы, которых касаются эти
AC, дельтой не тронуты.
- **check-docs, golden:verify, browser-смоки** — diff (все три коммита
#333) не трогает `src/**` ни разу; `smoke-select.mjs` подтвердил
инструментом.
- **`npm run invariants`** — diff не меняет геометрию/ссылки на неё.
- **Полный `node scripts/mutation-gate.mjs`** — избыточен, диффу
соответствует один мутант, проверен точечно.
- **performance-профили** — не названы в AC, чувствительный путь
(#330 executor) не тронут.
- **Спор о line-level точности спеки/докстринга AC4** — не перечитывал
файл целиком заново построчно посимвольно, положился на факт «diff
`origin/dev...HEAD` для этого файла = один абзац, тот же, что видел
r1» — это сильнее, чем повторное чтение вслепую, потому что исключает
случайный второй правки в этом раунде.
## 8. Вердикт
r1-H1 закрыт доказательно (ранее красный тест теперь зелёный, полный
бэкенд-набор чист), r1-Medium корректно вынесен в #347 вне этой ветки.
Ребейз на ушедший вперёд `dev` между раундами рассмотрен явно и не
потребовал полного повторного разбора AC, так как ни один файл #333 им
не затронут — но все дешёвые гейты и полный бэкенд-набор прогнаны заново
(зелёного Validate на `4ded9c0b` нет). Новых находок нет.
Вердикт: зелёный · заход r2 · блокирующих циклов 1/2 · High: 0 · Medium: 0