Files
houseplan-card/docs/reviews/SPEC-REVIEW-333-r1.md
T
2026-08-28 05:26:53 +00:00

66 lines
16 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-333-r1
Issue: [#333](https://github.com/Matysh/houseplan-card/issues/333) — «Ограничения стыков (#329): optimize и import пишут конфиг мимо валидатора — решить судьбу лазейки и мёртвого except»
Трек: `small` (ТЗ в теле issue, ревью — комментарий; лимит циклов ревью ТЗ — 2)
Заход: r1 · блокирующих циклов израсходовано 0 из 2 (правило #227: зелёный вердикт бюджет не тратит)
Материал: тело issue #333 (ревизия 1 ТЗ) на момент ревью; `dev` на SHA `59ae6b10`
## Скоуп
ТЗ меняет ровно один эндпоинт бэкенда, `ws_plan_optimize` (`websocket_api.py`), добавляя в него вызов `validate_junction_limits` (уже применяемый в `ws_config_set`), плюс симметричное обновление rev-кэша `rt.junction_baseline` и правку докстринга `junction_limits.py` + §5 спеки #329, чтобы текст перестал обещать защиту, которой не было. Import/restore осознанно остаются вне гейта — решение владельца, зафиксированное в S2-аналитике issue.
Это соответствует SCOPE.md: задача не добавляет пользователю ничего нового, а закрывает дыру в существующем контракте целостности геометрии (тот же класс инвариантов, что и П1–П4 из #329) — держит план «честным» при эволюции (J6) и не даёт крафтовому/устаревшему клиенту протащить повреждение, которое проявится как визуальный дефект («трезубец» и т.п.) и легализуется наследованием для всех последующих `config/set`. Продуктовой рамки «какая персона, что видит» ТЗ явно не формулирует, но для малого трека (§5 PROCESS.md) обязательный шаблон — «проблема · контракт · AC1…ACn с доказательством · откат», без отдельного сценарного раздела; продукт-вопросов, требующих owner'а, здесь нет — решение уже принято и явно процитировано в теле issue («Контракт (решение владельца 2026-08-28)»).
## Как проверялось
Ревью ТЗ на этом этапе не гоняет тестовые гейты (кода ещё нет — стадия `S4-spec-review`, а не код-ревью). Проверка состояла в сверке контракта и AC с реальным состоянием кода на `dev`:
- прочитан `custom_components/houseplan/junction_limits.py` целиком (докстринг, `validate_junction_limits`, `_migrated_spaces`, кэш-параметр `baseline_counts`);
- прочитаны `docs/specs/329-junction-limits.md` (§3, §5, AC9/AC10) и `docs/specs/330-junction-limits-performance.md` (§4.2 — паттерн rev-кэша, который #333 предлагает повторить);
- прочитан текущий код `ws_config_set` (websocket_api.py:1290–1419, включая `rt.junction_baseline` на :1330 и :1396) и `ws_plan_optimize` (websocket_api.py:1626–1783, включая `_validate_optimize_cpu` :1666–1699 и except-список :1709–1716) — подтверждена находка B1: `validate_junction_limits` в optimize действительно не вызывается, `JunctionLimitError` в except-списке действительно мёртв;
- проверено существование `tests_backend/test_ha_websocket.py` (52 теста) как места для нового HA-теста AC1/AC2;
- проверено, что `test/junction-limits.test.mjs` (AC10 #329) гоняет `optimizePlans` из `test-build/plan-optimizer.js` напрямую — это TS-алгоритм, не WS-обвязка, поэтому AC2 корректно требует ОТДЕЛЬНОГО HA-теста поверх него, а не полагается на AC10 целиком;
- проверено содержимое `scripts/check-docs.mjs` — что именно оно проверяет (см. находку M1);
- проверено `docs/CONFIG-COMPATIBILITY.md` на предмет других мест, обещающих гейт optimize/import — таких мест, кроме §329/§5, не найдено (раздел про host-эксепшен из #276/#280 — другая тема, не задета).
Гейты `typecheck`/`test`/`build` не гонялись — на этом этапе нет диффа кода, гонять их не над чем; это решение по стадии, а не пропуск.
## Находки
### M1 — AC4 называет способ доказательства, который не проверяет заявленное (Medium, в скоупе задачи)
**Где:** тело issue #333, ТЗ, раздел AC, пункт AC4: «Докстринг и спека #329 §5 описывают реальный периметр; `check-docs` зелёный.»
**В чём дефект:** `check-docs` (`scripts/check-docs.mjs`) проверяет фиксированный список публичных документов (`README.md`, `README.ru.md`, `docs/USER-GUIDE.md`/`.ru.md`, `docs/TOUCH-SUPPORT.md`, `docs/DECOR-EDITOR.md`, `docs/VACUUM.md`) — ссылки, заголовки и отпечаток скриншотов относительно `src/**`. Ни `docs/specs/329-junction-limits.md`, ни `custom_components/houseplan/junction_limits.py` в этом списке нет и не может быть: гейт не читает питоновские докстринги и не знает про `docs/specs/`. Задача не трогает `src/**`, поэтому `check-docs` даже не обязателен к запуску по правилам §8 — но AC4 предъявляет его как ДОКАЗАТЕЛЬСТВО текстовой правки, которую он физически не видит.
**Сценарий проявления:** автор меняет докстринг и §5 спеки #329 неточно (например, оставляет фразу, из-за которой сформулирован B1 в исходном аудите — «hostile client cannot post» — не полностью честной), прогоняет `check-docs` (он зелёный, как и был бы зелёным без всякой правки текста вовсе), и код-ревью получает формальное «AC4 доказан гейтом», хотя единственный способ проверить точность формулировки — прочитать текст. Это ровно тот класс несоответствия, который сама задача устраняет для докстринга (обещание защиты, которой нет) — только теперь применительно к собственному критерию приёмки.
**Как починить в этой же задаче:** заменить способ доказательства AC4 на «код-ревью: чтение изменённого докстринга `junction_limits.py` и §5 `docs/specs/329-junction-limits.md`» (по образцу того, как ревью само отвечает на вопрос «оно вообще работает» для находок без автотеста, PROCESS.md §2.7). `check-docs` при этом можно оставить в списке гейтов реализации как обычную обязательную проверку по правилу «правка `src/**` → check-docs» — но она не является доказательством именно AC4.
Находок класса High нет. Находок вне скоупа задачи нет — не заводится issue.
## Что проверено и корректно
- **Техническая точность контракта.** П.1 контракта («optimize вызывает `validate_junction_limits(candidate, stored, …)` внутри `_validate_optimize_cpu`, после миграции кандидата») соответствует реальной структуре `_validate_optimize_cpu`: кандидат уже прогнан через `commit_wall_segment_model`/`CONFIG_SCHEMA` к моменту, где логично добавить вызов (после :1690, рядом с `validate_marker_controls`/`validate_opening_passages` и т.п., :1691–1698) — те валидаторы не меняют геометрию стен, так что точная позиция вставки — не более чем техническая деталь, оставленная реализации.
- **Наследование не ломается.** `validate_junction_limits` уже устроена как «нарушение по правилу, не по носителю», с явным допуском унаследованных нарушений (`before.get(rule, 0)`) — вызов её в optimize не требует новой логики наследования, только новой точки вызова. AC2 корректно ссылается на существующий механизм.
- **Симметрия с rev-кэшем #330 (п.2 контракта, AC3).** Паттерн `rt.junction_baseline = (int(new_rev), candidate_counts)` уже есть в `ws_config_set` (:1396) один в один для повторения в `ws_plan_optimize` после успешной записи новой config-редакции; AC3 корректно нацелен на то, что именно должно тестироваться (следующий `config/set` не должен пересчитывать `previous`).
- **AC1/AC2 доказуемость.** Названный файл теста (`test_ha_websocket.py`) существует и уже содержит HA-тесты для `houseplan/plan/optimize` (roundtrip, undo, marker-cycle-отказ и т.п.) — структура для нового теста на месте, добавление теста «крафтованный payload с новым нарушением угла отклонён» технически осуществимо в этом файле без новой инфраструктуры.
- **AC2 корректно не полагается только на существующий AC10.** Существующий AC10-юнит (#329) гоняет `optimizePlans` напрямую из `test/junction-limits.test.mjs` — это чистый TS-алгоритм без WS-обвязки; он доказывает «честный клиент не создаёт нарушений», но не доказывает, что backend-путь `ws_plan_optimize` их проверяет. АС2 правильно требует ОТДЕЛЬНОГО HA-теста поверх него.
- **Обновление докстринга/спеки не противоречиво.** §5 спеки #329 в текущей редакции утверждает «Оптимизация планов проверку не проходит» — после этой задачи это утверждение станет буквально неверным, и контракт explicit это учитывает (п.3: переписать §5 честно). §3 спеки #329 («не имеет права создавать новые [нарушения]») при этом не требует правки — он и раньше был про постусловие, а не про механизм проверки; #333 просто добавляет механизм, который его наконец гарантирует.
- **Откат.** «Чистый revert одного коммита» корректен: код ошибки (`junction_limit_<rule>`) уже объявлен и зеркалируется на фронте, WS-контракт не меняется, новых полей конфига нет.
- **Отсутствие открытых продуктовых вопросов.** Контракт прямо процитирован как решение владельца от 2026-08-28; вариант выбран («A-суженный»), альтернатива (import тоже гейтить) явно отклонена с указанной причиной (спека #329 §3 — restore не блокируется никогда). Нет утверждения о поведении, не подкреплённого ссылкой на код или спеку.
- **Соответствие малому треку.** Один эндпоинт, ни новой миграции, ни нового UX-контракта (тот же канал ошибок, тот же стабильный код), влияние на перф названо и обосновано (~40 мс тёплых поверх уже существующего executor-пути #330, тот же порядок величины, что и `config/set`) — метка `small` держится.
## Чего не проверял
- **Не гонял `typecheck`/`test`/`build`.** На стадии ревью ТЗ кода ещё нет; эти гейты относятся к код-ревью (§2.7) и будут прогнаны там на реальном диффе.
- **Не проверял точную формулировку будущего текста докстринга/§5** — его ещё не существует; ревью ТЗ может судить только контракт («что должно быть сказано»), а не готовую формулировку. Это ровно то, что должно быть доказано на код-ревью — способом, исправленным находкой M1.
- **Не проверял производительность эмпирически** (нет кода для замера); оценка «~40 мс» в S2-аналитике не перепроверена бенчем — это её собственное заявление, не факт этого ревью. Она правдоподобна по порядку величины (тот же путь и данные, что и `validate_junction_limits` в `config/set`, для которого бюджеты уже откалиброваны в #330 §5), но не является измерением.
- **Не проверял, потребуется ли optimize сам читать `rt.junction_baseline` перед вызовом валидатора** (а не только писать после) ради полной симметрии с #330 §4.2. Контракт и AC3 формулируют только запись кэша после успешного optimize — этого достаточно для того, что AC3 обещает (следующий `config/set` быстрый), и optimize — редкое явное действие пользователя, а не путь на каждое движение курсора, так что отсутствие чтения кэша внутри самого optimize не выглядит регрессией уровня #330. Не блокирующее замечание, не завожу отдельным пунктом.
## Вердикт
Одна находка Medium **в скоупе** задачи (AC4 — неприменимый способ доказательства), High нет. По правилу §2.4/§2.7 (решение владельца 2026-08-19, #202) это жёлтый вердикт: правка вносится в тот же issue, отдельный issue не заводится.
**Вердикт: жёлтый · заход r1 · блокирующих циклов 0/2 · High: 0 · Medium: 1 → в задаче**