Files
houseplan-card/docs/reviews/SPEC-REVIEW-186-r2.md
T
2026-08-19 15:33:45 +03:00

20 KiB
Raw Blame History

SPEC-REVIEW-186-r2

  • Issue: https://github.com/Matysh/houseplan-card/issues/186
  • ТЗ под ревью: docs/specs/186-partition-opening-jamb-margin.md (коммит 62f73faef737df44ad8bb838aac4beceb74db9a1), обычный трек — не small/trivial
  • Роль: ревьюер ТЗ (не автор), этап S4-spec-review
  • Трек: обычный, лимит циклов ревью ТЗ — 4 (§4 PROCESS.md)
  • Цикл: r2/4
  • Предыдущий цикл: SPEC-REVIEW-186-r1.md — красный, High-1 (§8/AC4 «full import — всегда strict» противоречил принятому decision #3), Low-1 (нет заголовка «Проблема»)

Скоуп ревью

Только правка после r1: коммит 62f73fa («docs: preserve legacy jambs on full restore») меняет §3 (пункт 6), §5, §8, UX-таблицу (§9), AC4, AC6 и таблицу рисков (§13), плюс добавляет заголовок «Проблема» (§2). Формула margin (§4), scope/не-scope (§5/§6 кроме одной строки), frontend-контракт (§7), AC1/AC2/AC3/ AC5, план проверок (§12), откат (§14), release-артефакты (§15) и принятые предположения (§16) не менялись — их точность уже подтверждена в r1 и переподтверждается здесь только там, где новая правка их касается.

Продуктовый код по-прежнему не существует: ветка issue/186-partition-jamb-margin содержит три коммита (7d4d3f0, 85f54b4, 62f73fa), все класса C (документация), все с трейлерами Issue: #186 / User-Visible: no. Гейты typecheck/test/build/smoke/golden/pytest не прогонялись — на этапе ревью ТЗ они не относятся к предмету (PROCESS.md §2.4/§8).

Как проверялось

  1. Перечитаны docs/SCOPE.md, AGENTS.md, PROCESS.md (действующая редакция) — без изменений с r1, инварианты те же.
  2. Прочитаны все восемь комментариев issue #186 подряд: аналитика (Q1–Q3 с defaults), решение владельца по Q1–Q3, хендофф «ТЗ готово к ревью», вердикт r1 (красный, High-1), дополнительный вопрос владельцу Q4 (после ревью), решение владельца по Q4, хендофф «Правки ТЗ после ревью r1», и служебный комментарий о несработавшем автоматическом ревью r1 (метка не переставилась — к предмету этого ревью не относится, поднято отдельно перестановкой метки вручную/владельцем).
  3. Построчно сверен git show 62f73fa -- docs/specs/186-partition-opening-jamb-margin.md с текстом решения владельца по Q4 и с хендоффом «Правки ТЗ после ревью r1»: правка ограничена ровно перечисленными разделами, посторонних изменений (формулы, AC1-3/5, scope-исключений, отката) нет — «попутных правок» не обнаружено.
  4. Формулировка §3 п.6 и §8 сверена дословно с текстом решения владельца по Q4 («полный backup/restore всегда сохраняет legacy near-end проёмы... при восстановлении поверх текущего плана, на пустой и на другой инсталляции. Full import проверяет структурную целостность и попадание проёма внутрь host, но не применяет новый jamb safety margin») — совпадение по существу полное, без добавленных условий и без потерянных случаев (текущий / пустой / чужой инстанс перечислены во всех трёх местах: §3, §8, AC4).
  5. Независимо перепроверена архитектурная посылка исправления (что full import физически не обязан быть strict) чтением текущего кода на этой же ветке:
    • custom_components/houseplan/import_export.py:1304-1345 (prepare_apply) — для kind == "full" вызывается только CONFIG_SCHEMA(config) в конце функции (строка 1345); validate_partition_opening_hosts в этой ветке не вызывается вовсе.
    • grep -n "validate_partition_opening_hosts" custom_components/houseplan/*.py — ровно три вызова: import_export.py:995 внутри build_space_merge() (путь kind == "space", т.е. частичный merge-импорт, не полный), websocket_api.py:1260 (config/set) и websocket_api.py:1372 (optimize). Ни один не относится к kind == "full".
    • Вывод: validate_partition_opening_hosts() (и, следовательно, будущий jamb-контракт «рядом с ним либо в его расширении», §8) уже сегодня структурно не покрывает full import — это не новое послабление ради #186, а естественное продолжение существующей архитектуры. Формулировка §8 не вводит специальный обход, а корректно описывает то, что и так происходит.
    • Отдельно проверено (не для находки, а чтобы не спутать с общим паттерном): docs/CONFIG-COMPATIBILITY.md:59 документирует другой прецедент (#157, invalid_passage_fields), где validate_opening_passages действительно вызывается для каждого импорта, включая full (import_export.py:1159, create_preview(), validate_all=True, до ветвления по kind). Это не противоречие: у #157 и #132/#186 разные валидаторы с разной историей вызова на full-import пути, и decision Q4 явно называет цену этого выбора («вручную изменённый полный backup тоже может содержать near-end geometry») — то есть асимметрия с #157 осознанная, а не незамеченная.
  6. Сверено, что merge-импорт (kind == "space", build_space_merge вызывает validate_partition_opening_hosts(merged_config, current_config) на import_export.py:995) остаётся вне пересмотренного decision Q4 (тот называет только «полный backup/restore»). ТЗ (§5/§8/AC4/AC6) не утверждает ничего противоположного про merge-путь и не молчит о нём как о решённом факте — merge и раньше (#132) переприсваивает id и потому уже трактует переносимые записи как новые для семантического валидатора; #186 просто наследует этот действующий паттерн, не решая для него ничего нового. Дополнительных вопросов владельцу это не требует.
  7. Перечитан заново весь текст ТЗ (не только диф) на предмет новых утверждений о поведении, поданных как факт без пометки assumption или ссылки на решение владельца — целенаправленно искал второй High-1. Не найдено: каждое утверждение §3/§5/§8/§9/AC4/AC6 после правки либо дословно повторяет decision Q1–Q4, либо является технической деталью уже покрытой §16 (имена helper/reason, previous-параметр).
  8. Перепроверены обязательные разделы §7.1 (таблица ниже) и добавленный заголовок «Проблема» — сравнение старой и новой версии (git show 62f73fa) подтверждает: над «До/после» появился отдельный вводный абзац с заголовком ## 2. Проблема, ровно то, что требовал Low-1.
  9. Проверена запись docs/specs/README.md:102 — ссылка issue ↔ ТЗ двусторонняя и не менялась в этом цикле, ещё присутствует.
  10. Проверены трейлеры всех трёх коммитов ветки (git log --format='%H%n%s%n%b') — Issue: #186 и User-Visible: no на каждом; User-Visible: no корректен, т.к. ни один коммит не меняет продукт.

Гейты (typecheck/test/build, browser smoke, golden, backend pytest) не прогонялись — продуктового кода нет, это ожидаемо на этапе ревью ТЗ.

Обязательные разделы (§7.1 PROCESS.md)

Раздел Есть Комментарий
Сценарий (персона/поверхность/момент) ✅ §1, без изменений в r2
Проблема ✅ §2, теперь отдельный заголовок с вводным абзацем — Low-1 закрыт
Что человек увидит до/после ✅ (см. Low-2) «До/После» под §2; формулировка не изменилась в r2 и содержит термины реализации, см. находку ниже
Скоуп / не-скоуп ✅ §5/§6; один пункт §5 переформулирован под decision Q4, без потери содержания
Контракт поведения ✅ §7/§8; §8 переписан под decision Q4, внутренне непротиворечив
UX ✅ §9, добавлена строка про full restore
Модель данных и миграция ✅ §10, без изменений
i18n ✅ §10, без изменений
AC1…ACn с доказательством ✅ AC4/AC6 переписаны под decision Q4, доказательство расширено на current/empty/foreign install
План автотестов ✅ §12, без изменений
Риски ✅ §13, добавлена строка «Full restore ошибочно становится strict»
Откат ✅ §14, без изменений
Release-артефакты ✅ §15, без изменений

Блок «Принятые технические предположения» (§16) не менялся в r2; при повторном чтении ни один из четырёх пунктов по-прежнему не маскирует продуктовый вопрос.

Находки

Low-2 — «что человек увидит» использует термины реализации

Файл: docs/specs/186-partition-opening-jamb-margin.md, §2, подраздел «До/После» (не менялся в r2, существовал и в r1).

PROCESS.md §7.1 требует для этого элемента «одной фразой, без терминов реализации». Текущий текст: «До: frontend считает валидным любой проём... Backend повторяет эту границу с одним лишь floating-point epsilon...», «После: у обоих торцов host резервируется остаток... математическими endpoints host». Используются архитектурные/кодовые термины (frontend, backend, epsilon, endpoints), а не то, что видит администратор в редакторе плана (например: «проём можно было поставить впритык к торцу стены, без места под наличник; теперь редактор всегда оставляет отступ, равный половине толщины стены»).

Ни один AC не опирается на эту формулировку и не становится двусмысленным — дефект чисто редакционный, той же природы, что и снятый в r1 Low-1.

Решение ревьюера: Low, не блокирует. Снимаю без возврата ТЗ на правку; рекомендую при следующей правке файла (не отдельным циклом) заменить «До/После» на формулировку в терминах того, что видит администратор в Plan editor, не изобретая новых решений — переформулировка, не новый контракт.

Что проверено и корректно

  • High-1 из r1 закрыт корректно и полностью. Коммит 62f73fa меняет ровно те разделы, что были нужны (§3 п.6, §5, §8, §9, AC4, AC6, §13), дословно соответствует принятому владельцем decision Q4, и не оставляет старой формулировки «full import без trusted previous — strict» ни в одном месте документа (проверено построчным перечитыванием всего файла, не только дифа).
  • Архитектурная посылка исправления подтверждена независимым чтением кода, а не принята на слово: validate_partition_opening_hosts() сегодня вызывается только в build_space_merge (частичный merge), config/set и optimize — ни разу для kind == "full" в prepare_apply(). Значит decision Q4 не вводит специальный обход общей архитектуры, а продолжает уже существующее устройство full-import пути. Асимметрия с прецедентом #157 (invalid_passage_fields, который действительно строг на full import) — реальная и явно принятая владельцем цена, а не незамеченное расхождение.
  • Merge-импорт (частичный, kind == "space") остаётся вне decision Q4, и ТЗ не утверждает о нём ничего, что противоречило бы этому — унаследованный из #132 паттерн переприсвоения id продолжает работать без нового решения.
  • Второй High не найден. Целенаправленный повторный проход по всему документу (не только по дифу r1→r2) не выявил новых утверждений о поведении, поданных как факт без пометки assumption или ссылки на решение владельца.
  • Формула margin (§4), AC1/AC2/AC3/AC5, откат (§14), touch-декларация (§10) и release-артефакты (§15) не менялись в этом цикле; их точность была независимо перепроверена в r1 (арифметика GRID_N=240, реальные call sites, существующий паттерн docs/CONFIG-COMPATIBILITY.md) и остаётся в силе.
  • Трейлеры и трассируемость: все три коммита ветки несут Issue: #186/User-Visible: no, запись issue ↔ ТЗ в docs/specs/README.md на месте, docs/specs/186-partition-opening-jamb-margin.md ссылается на issue и на #132.
  • Трек подтверждён обычным (не small/trivial) — критерии §5 PROCESS.md по-прежнему не выполняются (новый видимый граничный контракт, compatibility-решение), лимит цикла — 4, использован r2/4.

Чего не проверял

  • Реализацию — её по-прежнему нет: три коммита ветки все класса C (документация), продуктовый код (src/**, custom_components/houseplan/**/*.py) не менялся этой веткой — проверено чтением git show --stat каждого коммита.
  • Гейты typecheck/test/build/browser smoke/golden/pytest tests_backend — не относятся к этапу ревью ТЗ.
  • Полный повторный пересчёт формулы margin и всех восьми call sites resolvePartitionOpening() — не требовался: эта часть документа не менялась между r1 и r2, независимая проверка из r1 остаётся в силе и не переделывалась.
  • Оставшиеся детали merge-импорта (build_space_merge, duplicate_policy, same_source) за пределами вопроса «вызывается ли здесь validate_partition_opening_hosts» — не относится к предмету находки High-1 и её закрытия.
  • Численные оценки аналитики (5/10 · 6/10 · 5/10 · P2) — поле владельца, решено до написания ТЗ, вне предмета ревью.

Вердикт

Зелёный. High: 0 (High-1 из r1 закрыт decision Q4 и корректно отражён во всех затронутых разделах ТЗ; архитектурная посылка независимо перепроверена чтением текущего кода). Medium: 0. Low: 1 новая (Low-2 — «До/После» использует термины реализации вместо описания того, что видит администратор; снимается ревьюером без возврата, рекомендация — поправить при следующей правке файла). Low-1 из r1 (отсутствие заголовка «Проблема») закрыт правкой. ТЗ готово к переходу в S5-ready.

Вердикт: зелёный · цикл r2/4 · High: 0 · Medium: 0 → в задаче · Документ: docs/reviews/SPEC-REVIEW-186-r2.md