Files
houseplan-card/docs/reviews/SPEC-REVIEW-265-r1.md
T
2026-08-25 11:01:14 +00:00

23 KiB
Raw Blame History

SPEC-REVIEW-265-r1

  • Issue: #265 — «Рефакторинг 2/5: один контракт шва импорта — ремап ссылок за собой и идемпотентная уборка»
  • Этап: S4-spec-review (ревью ТЗ, PROCESS.md §2.4)
  • Заход: r1 · блокирующих циклов израсходовано 0/4 (до вердикта)
  • ТЗ: docs/specs/265-import-reference-seam.md, ветка issue/265-import-seam-contract, SHA 2cf47fc1
  • Ревьюер: Claude (свежая сессия, без переписки с автором)

Скоуп ревью

Первый заход — разбор полный, дельта не применяется (§2.10 относится со второго цикла). Материал: тело issue #265, оба комментария аналитика/автора, файл ТЗ целиком (docs/specs/265-import-reference-seam.md, 454 строки) и правка docs/specs/README.md (+1 строка — таблица ссылок).

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

  1. Прочитаны docs/SCOPE.md, PROCESS.md (целиком, включая §2.4/§2.5/§2.9/§7.1), AGENTS.md.
  2. Прочитано тело issue #265 и оба комментария (Взял: …, аналитика, «ТЗ готово»).
  3. Проверены утверждения §2 «Подтверждённое состояние кода» против реального custom_components/houseplan/import_export.py: _fresh() (строка 804), _repair_target_space_refs() (825), build_space_merge() (875), create_preview()/revalidate_candidate()/prepare_apply() (1214, 1330, 1381). Все пять пунктов §2 подтверждены построчно — это не догадки: _fresh() действительно строит stem из текущего old (без канонизации), _repair_target_space_refs() действительно ремонтирует только exact-map пяти перечисленных полей, preview/apply действительно каждый раз вызывают build_space_merge() заново со случайным secrets.token_hex(4).
  4. Проверено утверждение §2.4 о scripts/model-invariants.mjs: функция checkReferences() (строки 118–194) действительно не проверяет marker.room_id, vacuum.segment_map, rooms[].open_to, marker:* controls/value badge и partition opening host — совпадает с текстом ТЗ.
  5. Проверены поля матрицы §7 против кода: controls[] с префиксом marker:, value_badge.ref, vacuum.segment_map, rooms[].open_to, openings[].host.id при host.kind == "partition" — все существуют в import_export.py (строки 482–497, 750–780, 848–853, 916–927, 970–1014). Матрица не изобретает несуществующие поля и не молчит про существующие, которых коснулась выборка.
  6. Проверены три «унаследованных решения владельца» (§3) против статуса и текста связанных issue/спеков: #244, #248, #252, #258, #262 — все CLOSED (gh issue view), тексты их ТЗ (docs/specs/244-*.md, 252-*.md, 262-*.md) подтверждают ровно те формулировки, которые #265 им приписывает (tombstone не собирается по сроку — #262 §6.2; отчёт живёт внутри «Оптимизировать планы» — #252 §1–2; неразрешимая ссылка не угадывается — #244 AC3/AC5/AC6). Ни один из трёх вопросов, которые исходный текст issue адресовал владельцу, не решён произвольно: все три закрыты уже принятыми (закрытыми) задачами, а не мнением автора.
  7. Сверена терминология: «Оптимизировать планы», «Общие настройки» — совпадает с docs/USER-GUIDE.ru.md (строки 402, 1425 и др.). «carrier» — совпадает с docs/WALL-THICKNESS.md. Новый текст отчёта (§9) — это черновик новой строки, не существующей ранее ни в одном каноне; это ожидаемо для новой фичи и не является нарушением («термин из USER-GUIDE» требование касается уже существующей терминологии интерфейса).
  8. Сверена структура документа с двумя прямыми «братьями» той же серии рефакторинга — docs/specs/244-orphan-space-references.md и docs/specs/252-optimize-orphan-layout-report.md (тот же автор, тот же класс задачи, ссылаются друг на друга) — чтобы отличить «этот документ так устроен потому что задача другая» от реального отступления от конвенции.
  9. Технически проверена состоятельность алгоритма канонизации lineage (§5.1, edge case 1): смоделирован случай, когда пользовательский id случайно принимает форму <namespace>_<stem>_<8 lowercase hex> и совпадает root'ом с реальным импортным id. Риск ложного совпадения существует, но заперт пятью одновременными условиями §5.2 (мертва исходная ссылка + ровно один живой кандидат + совместимый тип + отсутствие exact id + отсутствие второго кандидата с тем же root) и прямо назван эвристикой, а не идентичностью (§17). Блокирующим не считаю.
  10. node scripts/check-docs.mjs не запускался: diff не касается src/** (только docs/specs/**), гейт неприменим по своему собственному условию («любая правка фронтенда» — здесь правки фронтенда нет). Остальные гейты (typecheck/test/build/смоки/инварианты) относятся к этапу код-ревью (S7), а не спек-ревью (S4); на этом этапе кода еще нет.

Находки

Все находки — Medium, в скоупе задачи: чинятся тем же автором в этом же документе ТЗ, без нового цикла обсуждения продукта и без отдельного issue. High-находок нет.

M1 — «что человек увидит» слито со сценарием и написано языком реализации; поверхность/touch не названы

Файл: docs/specs/265-import-reference-seam.md, §1 (строки 19–51).

PROCESS.md §7.1 требует два отдельных обязательных раздела: сценарий (какая персона, на какой поверхности, в какой момент) и «что человек увидит» одной фразой, без терминов реализации — и прямо предупреждает: «ТЗ, которое не может ответить на эти два вопроса, описывает работу, а не изменение продукта». Оба прямых родственника этой серии соблюдают разделение и дают термин поверхности явно: 244-orphan-space-references.md §1–2 и 252-optimize-orphan-layout-report.md §1–2 оба содержат фразу вида «Optimize — desktop-first административная поверхность; View и kiosk только читают уже сохранённый результат» и отдельное «До/После» на бытовом языке («видит список внутренних id и не понимает, безопасно ли нажимать Apply»).

В §265 «До»/«После» — это фактически список внутренних инвариантов: «id строится от канонического корня lineage», «матрица plan-id ссылок», «неизменяемый candidate», «Apply пишет ровно тот кандидат». Ни одна из этих пяти строк не отвечает на вопрос, что увидит администратор в диалоге импорта. Единственная строка, которая касается видимого экрана («Optimize остаётся единственным инспектором») — про архитектурное решение, не про экран импорта. Заодно нигде в документе не упомянут docs/TOUCH-SUPPORT.md и статус поверхности (desktop-only админ-функция) — обязательный пункт DoR §2.5 («влияние на touch… названо»).

Почему это не мелочь: без явного «что человек увидит» ревьюер не может проверить AC10 на полноту (см. M2) — граница «это то же самое ТЗ, просто описано плохо» и «здесь спрятано непродуманное продуктовое решение» стирается именно тем, что раздел не выполняет свою функцию.

Как чинится: вынести из §9 уже готовые пользовательские формулировки («сохранены без изменений…», «Убрано забытых записей: N») в отдельное «После» одним-двумя предложениями без «lineage»/«canonical root»/«candidate»; добавить фразу про desktop-only поверхность и отсутствие touch-влияния, как в #244/#252.

M2 — AC10 хеджирует «если отчёт станет виден пользователю», хотя §9 безусловно описывает новый видимый текст

Файл: docs/specs/265-import-reference-seam.md, §9 (264–291) против AC10 (405–412).

§9 не оставляет пространства для сомнения: «Import preview показывает: количество восстановленных target-ссылок; количество сохранённых неразрешимых ссылок с формулировкой «сохранены без изменений; после импорта запустите "Оптимизировать планы"»; количество отброшенных incoming links…». Это конкретный, безусловный список новых пользовательских строк.

AC10 при этом формулирует i18n/changelog/screenshot обязательства условно: «Если новый report виден пользователю... Если итоговая реализация не меняет видимый UI, changelog фиксирует только "small fixes"». Это внутреннее противоречие: собственный §9 уже утверждает, что UI меняется, а AC10 оставляет автору лазейку объявить это невидимым. DoR §2.5 требует, чтобы «i18n: ключи en + ru перечислены» было решено до перехода в «Готово к разработке», а не отложено условием, которое своим же текстом документа уже снято.

Как чинится: снять условность из AC10 (текст отчёта уже зафиксирован в §9 — значит UI меняется точно) и либо перечислить конкретные i18n-ключи (даже черновые: import.report.remapped, import.report.preserved_unresolved, import.report.dropped_incoming и т.п.), либо явно сказать, что ключи называет реализация, а ТЗ фиксирует только текст — но без слова «если» там, где сам документ уже решил вопрос.

M3 — AC1–AC9 не называют способ доказательства (в отличие от требования DoR и от прямого соседа #244)

Файл: docs/specs/265-import-reference-seam.md, §13 (352–412), ср. docs/specs/244-orphan-space-references.md §14 (355–369).

PROCESS.md §2.5 требует: «AC1…ACn — пронумерованные проверяемые критерии приёмки; у каждого указано, чем он доказывается: unit/backend/smoke/ golden/«ревью кода»». #244 (тот же автор, та же неделя, тот же кластер задач) соблюдает это буквально — таблица АС там имеет отдельный столбец «доказательство» для каждой строки («Table-driven optimizer unit + idempotence unit.», «Backend import/export tests preview/revalidate/apply.» и т.д.).

В §265 из десяти AC явный метод доказательства называет только AC10 («canonical-doc screenshots/golden review»). AC1–AC9 — чистый текст критерия без указания, каким тестом он проверяется; §14/§15 дают только общий, не привязанный к номеру AC список («Python import/export unit/backend tests; frontend unit tests…»). Из этого списка не восстанавливается однозначно, что, например, AC2 (cross-generation repair) проверяется backend unit, а AC7 (идемпотентность) — какой конкретно раз запускаемым тестом.

Как чинится: добавить к каждому AC1–AC9 короткую пометку способа доказательства по образцу §14 из #244 — минимальная правка формата, не меняющая ни один критерий по содержанию.

M4 — нет отдельного обязательного раздела «Риски»

Файл: весь документ; ср. docs/specs/244-orphan-space-references.md §16 («Риски, производительность и security»).

PROCESS.md §7.1 перечисляет «риски» как самостоятельный обязательный раздел ТЗ, отдельно от AC и от edge cases. В §265 нет раздела с таким содержанием: §12 «Edge cases» — про корректность алгоритма на конкретных входах, не про риски проекта (например: риск ложного совпадения lineage при совпадающей пользовательской строке — уже частично обсуждён в edge case 1, но не назван явно как принятый риск с последствием; риск расхождения Python/TS реализаций общего lineage-алгоритма, который §5.3 признаёт возможным и лечит только conformance fixture — сам факт «может расходиться, лечим тестом» это и есть риск, который нигде не сформулирован как риск).

Как чинится: добавить раздел «Риски» (можно короткий, 3–5 пунктов): ложноположительное совпадение lineage у пользовательского id; расхождение Python/TS реализации до появления conformance fixture; неполнота матрицы §7, если найдётся ещё не учтённое plan-id поле; стоимость независимого прогона инвариантов на больших конфигурациях (уже частично закрыто AC9, достаточно сослаться).

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

  • Диагноз §2 (five claims про _fresh/_repair_target_space_refs/preview↔apply desync/model-invariants.mjs gaps) — точен, построчно сверен с кодом, не является догадкой.
  • Матрица внутренних ссылок §7 — все перечисленные поля существуют в коде; не нашёл полей, которые код трогает, а матрица замалчивает бы, в пределах выборки, которую успел свести (marker.space, marker.room_id, vacuum.segment_map, rooms[].open_to, markers[].controls[], value_badge.ref, openings[].host.id).
  • Три «унаследованных решения владельца» (§3) действительно наследуются от закрытых issue #244/#248/#252/#262, а не выданы автором за решение владельца без основания — все три вопроса, которые исходный текст #265 адресовал владельцу, закрыты предыдущими, уже принятыми задачами.
  • Скоуп/не-скоуп (§4) явный и не расширяет задачу дальше контракта шва импорта; явно исключены full-import semantics, merge пространств, HA-registry remap, удаление записей — совпадает с §SCOPE.md «never delete a user's file on an inference» и с принципом lossless Optimize.
  • Алгоритм канонизации lineage (§5) внутренне согласован, ограничен по глубине, не путает namespace при разборе (проверено на примере marker_room_x_deadbeef), и явно называет себя эвристикой, а не криптографическим доказательством происхождения — риск ложного совпадения признан и заперт пятью условиями §5.2.
  • Терминология («Оптимизировать планы», «carrier») соответствует docs/USER-GUIDE.ru.md и docs/WALL-THICKNESS.md, не изобретена.
  • Открытых продуктовых вопросов владельцу нет; вопросов, которые стоило бы туда вынести, я не нашёл — то, что можно было решить самостоятельно (алгоритм lineage, формат id, порядок remap), решено автором и явно помечено как допущение в §17, подлежащее спору с ревьюером, а не эскалации.
  • AC5/AC8 корректно защищают инвариант «lossless по умолчанию» и явно перечисляют regression-контракты пяти связанных задач, что закрывает риск повторной регрессии одной из них при рефакторинге общего шва.
  • Изменение файла docs/specs/README.md — верно оформленная строка таблицы, без добавления второго источника статуса (колонка «Статус ТЗ» не трогается).

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

  • Не запускал typecheck/test/build/смоки/инварианты — на этапе спек-ревью кода еще нет, эти гейты относятся к S7 (код-ревью) и будут релевантны при следующем заходе после реализации.
  • Не проверял node scripts/check-docs.mjs — diff не касается src/** (только docs/specs/**), условие гейта не выполнено, прогон не даёт дополнительной информации.
  • Не оценивал производительность реального lineage-индекса на больших инсталляциях эмпирически — только прочитал заявление AC9 о линейности и не нашёл в описанном алгоритме (§8) шагов, требующих декартова сравнения; оценка «читал, не исполнял».
  • Не проверял конкретные формулировки будущих i18n-строк на корректность русского/английского — они ещё не существуют, будет предметом код-ревью.

Вердикт

Технический диагноз и дизайн решения обоснованы, привязаны к реальному коду и к уже принятым решениям владельца по связанным задачам; ни одна находка не касается корректности алгоритма или заявленного продуктового поведения. Все четыре находки — про полноту обязательных разделов ТЗ по PROCESS.md §7.1 и DoR §2.5 (пользовательское «до/после» без языка реализации, безусловность i18n-обязательств, способ доказательства на каждый AC, отдельный раздел рисков), и все устраняются правкой текста того же документа без пересмотра дизайна.

Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 4 → в задаче