16 KiB
SPEC-REVIEW-333-r1
Issue: #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 → в задаче