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

16 KiB
Raw Blame History

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 → в задаче