docs: review document for #333

Issue: #333
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-28 05:26:53 +00:00
parent 59ae6b1064
commit ff6e53a48a
+65
View File
@@ -0,0 +1,65 @@
# 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 → в задаче**