26 KiB
SPEC-REVIEW-611-r2
Issue: #611 — «Импорт пространства не перепривязывает
vacuum.map_routes[].space: на чужой установке импорт падает, на своей маршрут робота молча уводит на старый этаж»
Этап: spec (PROCESS.md §2.4)
Трек: small (лёгкий) — лимит циклов ревью ТЗ 2
Заход: r2 · блокирующих циклов израсходовано 1 из 2 (r1 был жёлтым и цикл потратил, зелёный его не тратит, §7.2/#227)
Ревьюер: независимая сессия, без контекста автора ТЗ
Вердикт: зелёный
Скоуп
ТЗ живёт в теле issue #611 (раздел ## ТЗ), не в docs/specs/. Предмет ревью r2 — дельта тела issue между
раундами r1 и r2 и то, чего эта дельта касается: контракт п.1–5, AC1–AC3, «Принятые предположения», «Откат» —
проверенные против фактического кода custom_components/houseplan/import_export.py и vacuum_routes.py.
Кодовая база не менялась между раундами: HEAD r1 был 61292c9ea05f... (материал r1), текущий HEAD
37aed2c9b13b333ba8adb2c97f32158e9606618b добавляет только сам документ docs/reviews/SPEC-REVIEW-611-r1.md
(коммит «docs: review document for #611») — продуктовый код (класс A) не тронут. Значит «дешёвые гейты» (§2.10)
здесь неприменимы по той же причине, что и в r1: артефакта для запуска ещё нет.
Разбор по дельте, а не заново (§2.10). Дельта — правка ровно того, что r1 назвал M1 (Medium, в скоупе):
контракт получил новый п.2 (_repair_target_space_refs), AC2 полностью переписан под ремонт целевого маршрута,
добавлена строка в «Принятые предположения» про переиспользование _lineage_resolver. Это не ребейз, не смена
подсистемы и не контракта поведения за пределами уже заявленного бага — объём дельты меньше исходной задачи и
локален к одной функции того же файла, поэтому полный разбор не требуется; проверялась дельта и всё, до чего она
дотягивается (AC2 целиком, консолидированный AC1, п.2–4 контракта, ремонт в «Откате»).
Как проверялось
- Получено тело issue #611 через
gh issue view --json bodyи полная история правок тела через GraphQLuserContentEdits(editor,editedAt,diff) — 3 правки автора (Matysh): создание 2026-09-22T19:53:51Z, заполнение ТЗ 2026-09-23T01:14:05Z (материал r1), правка по итогам r1 2026-09-23T01:24:00Z (материал r2, текущее состояние). Между node[1] (r1) и node[0] (r2) выполнен точныйdiff -u— дельта установлена по содержимому, а не по пересказу автора. - Найден вердикт r1 (комментарий issue, 2026-09-23T01:22:09Z) и документ
docs/reviews/SPEC-REVIEW-611-r1.md(уже закоммичен, HEAD37aed2c9). Единственная находка r1 — Medium M1: контракт п.1/AC1-AC2 перепривязывалиvacuum.map_routes[].spaceтолько для входящих маркеров, но не для второго прохода_repair_target_space_refs, который ремонтирует уже существующие целевые маркеры. - По дельте построчно сверены с кодом:
custom_components/houseplan/import_export.py:1013-1049(_lineage_resolver) — резолвер возвращает ровно пять исходов:live,exact,lineage,ambiguous,unrelated. Новый контракт п.2 покрывает все пять («живая ссылка... не меняется» = live; «доказанная exact-ссылка либо единственная безопасная lineage-ссылка... заменяется» = exact/lineage; «неоднозначная... сохраняется...preservedUnresolved» = ambiguous; «несвязанная... отклоняется последующей валидацией» = unrelated). Формулировка точна, без недостающих случаев.custom_components/houseplan/import_export.py:1052-1279(_repair_target_space_refs) прочитан целиком. Подтверждено:resolve_space = _lineage_resolver(...)(строка 1077) уже создан и уже применяется кmarker.spaceбезусловно, без гейтаmay_rebind_room(строка 1154) — контракт п.2 требует того же безусловного применения кvacuum.map_routes[].space, что технически корректно (это прямая ссылка на пространство, как иmarker.space, а не ссылка на комнату внутри чужого пространства, для которой гейт нужен). Подтверждено также: сейчас в этой функции нет ни одной строки, ремапящейmap_routes— ровно тот пробел, который описал M1, и он остаётся открытым до реализации; ТЗ его теперь явно контрактует.custom_components/houseplan/import_export.py:967-1001(_empty_reference_report,_report_remap) —remapped: {"incoming": {}, "target": {}}иpreservedUnresolvedуже существуют как открытые словари произвольных категорий; добавление категорииmarker.vacuum.map_routes.spaceв.targetне требует смены схемы отчёта.repaired_target_refs— существующий именованный счётчик, возвращаемый функцией (строка 1059) и попадающий вmerged_config(строка 1655) — контракт п.3/AC2 ссылаются на реальные имена.custom_components/houseplan/import_export.py:1415-1466(цикл по входящим маркерам) — подтверждает, что соглашение именования категорий (marker.vacuum.segment_mapдля входящих, строка 1463) прямо аналогично новойmarker.vacuum.map_routes.space— не изобретённое имя.custom_components/houseplan/vacuum_routes.py:104-142(effective_routes) — не менялся с r1; по-прежнему подтверждает п.4 (legacy-маршрут вычисляетspaceиз параметраdock_space, отдельного сохраняемого поля нет) — эта часть дельты не касалась, но проверена заново, так как п.4 в дельте переформулирован (убрана фраза «существующая валидация должна по-прежнему отвергать неизвестную ссылку» — не потеря гарантии: та же гарантия теперь явно и без потери смысла стоит в п.2 для целевого прохода, а для входящего прохода валидация всё так же выполняется один раз на объединённомmerged_configпосле обеих правок, строка 1627, — не задвоена и не обязана быть задвоена).custom_components/houseplan/import_export.py:1489-1496(виртуализация теряетvacuumцеликом) — не менялся; контракт п.5 (было п.4) по-прежнему точен.
- Перечитаны имена тестов, на которые ссылается новый AC2:
test_issue_244_space_import_repairs_existing_target_refs_with_exact_map,test_issue_265_space_import_repairs_unique_previous_generation_lineage,test_issue_244_space_import_does_not_repair_target_while_source_exists(существуют,tests_backend/test_ha_import_export.py:1507,1540,1589) иtest_issue_162_*/test_issue_443_*(существуют, строки 2678–2755) — доказательная база AC2 не выдумана, тесты рядом с ней реальны и уже упражняют именно_repair_target_space_refsсvacuum-блоком целевого маркера (сейчас — черезsegment_map; AC2 просит аналогичный тест черезmap_routes). - Проверено, не меняет ли дельта классификацию трека
small(§5): новая категория отчёта — просто ключ в уже существующем открытом словаре, не смена схемы/версии модели;smallостаётся верным. docs/CONFIG-COMPATIBILITY.md(«Vacuum map routes (#162)», строки 163–195) перечитан ещё раз — описывает только экспорт и downgrade-семантику, не претендует на поведение ремонта ссылок при импорте; заявление ТЗ «документация не требуется» остаётся самосогласованным и после расширения контракта.
Дешёвые гейты (tsc/test/build) и check-docs.mjs не гонялись — на этапе ревью ТЗ нет ни изменённого кода,
ни изменённого src/**; неприменимо ровно как в r1.
Находки
Находок нет. Дельта закрывает M1 полностью и не вводит новых Medium/High.
Рассмотрен и сознательно не поднят как находка один пограничный момент: AC3 («тест чувствителен к снятию
защиты») остаётся сформулирован в единственном числе и не перечисляет отдельно оба поведения контракта (ремап
входящего маршрута из п.1/AC1 и ремонт целевого маршрута из п.2/AC2). Это не блокирует и не делает ТЗ
неоднозначным: формулировка AC3 общая («перепривязку map_routes[].space]»), проект уже регистрирует несколько
мутантов на один файл под соседние issue (r1 отметил несколько адресов в scripts/mutation-registry.mjs), и
проверка фактической полноты мутационного покрытия обоих путей — предмет код-ревью (§2.7), а не ревью ТЗ: там
проверяется реестр и его способность падать, здесь — только проверяемость критерия по тексту. Low снят без
правки, с этой записью.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
M1 (Medium, в скоупе) — контракт п.1/AC1-AC2 перепривязывают vacuum.map_routes[].space только для входящих маркеров; второй проход _repair_target_space_refs (чинит уже существующие целевые маркеры через lineage/exact-map) пропускает то же поле. |
Добавлен контракт п.2: vacuum.map_routes[].space целевых маркеров проходит через тот же resolve_space, что и marker.space, с полным перечислением всех пяти исходов резолвера (live/exact/lineage/ambiguous/unrelated). AC2 полностью переписан под «ремонт целевого маршрута» с явной ссылкой на reference_report.remapped.target, repaired_target_refs, preservedUnresolved и тесты #244/#265. Раздел «Принятые предположения» добавляет: для целевого repair используются канонические правила _lineage_resolver, новый эвристический поиск не вводится. «Откат» и «Механизм»/«Проблема» переформулированы, чтобы явно называть _repair_target_space_refs. |
Тело issue #611, разделы «Контракт» п.2, «Критерии приёмки» AC2, «Принятые предположения» (правка автора 2026-09-23T01:24:00Z, подтверждено точным diff -u между версией тела на момент r1 и текущей). Код-грaунд: import_export.py:1013-1049 (_lineage_resolver, 5 исходов), :1077,1154 (resolve_space, уже создан и уже безусловно применяется к marker.space в том же проходе — контракт просит того же обращения с map_routes[].space), :967-971,1059,1655 (remapped.target, repaired_target_refs, preservedUnresolved — реальные существующие поля отчёта). |
Унаследовано из r1
Принято без повторной проверки (дельта их не касается):
- Механизм и репродукция (кроме двух предложений, добавленных про
_repair_target_space_refs, которые проверены заново выше) — сверены в r1 построчно с кодом на61292c9ea05f...; код с тех пор не менялся. - AC1, содержательная часть про
same_source=False/True,create_export(kind="space"),parse_document,build_space_merge— существующие публичные точки входа, уже подтверждённые в r1; дельта лишь консолидировала старые AC1+AC2(r1) в один AC1(r2), не изменив технических утверждений об этих вызовах. - П.4 контракта, первое подтверждение (
vacuum_routes.py:106-146, legacy-маршрут вычисляется изmarker.space, отдельногоroute.spaceнет) — переподтверждено в этом раунде отдельно (см. «Как проверялось», п.3), хотя формулировка в тексте не менялась по существу. - П.5 контракта (виртуализация теряет
vacuumцеликом; политика дубликатов; экспорт не меняется) — переподтверждено (import_export.py:1489-1496), текст не менялся по существу. - AC3 (мутационная защита) и ссылка на
scripts/mutation-registry.mjs— текст не менялся; принято по выводу r1 («ссылка не выдумана, файл действительно регистрирует backend-мутанты противimport_export.py»). Полнота охвата обоих путей — см. «Находки» выше, оставлено на код-ревью. - DoR-чеклист §2.5 (i18n — нет новых ключей; touch/performance — без влияния; откат — один revert,
реалистично) — переподтверждён в этом раунде тем, что дельта не добавляет ни новых пользовательских строк,
ни нового UX-контракта, ни миграции; тексты «Release-артефакты» не менялись между r1 и r2 (вне диапазона
правки, подтверждено
diff -u). - Продуктовая рамка и track
small— сценарий/что человек увидит (§7.1) и обоснование трекаsmallне менялись; сверено, что дельта не превращает задачу в задачу с новым UX-контрактом или миграцией (см. «Как проверялось», п.5).
Документ и материал предыдущего раунда: docs/reviews/SPEC-REVIEW-611-r1.md, код-база
61292c9ea05f0d7d3a98392c81f3be89f2706c7c, тело issue на момент r1 — правка автора от 2026-09-23T01:14:05Z
(идентифицирована через GraphQL userContentEdits, sha256 содержимого 94e2253da2c3…f1553, 6811 байт —
уточнение фактического размера: r1 указал 4369 байт при том же хеше, это арифметическая неточность прозы r1,
не расхождение материала).
Проверено и корректно
- Дельта устанавлена по точному диффу, а не по пересказу. Через GraphQL
userContentEditsполучены обе версии тела issue (до и после правки автора 2026-09-23T01:24:00Z) и сопоставленыdiff -u— видно ровно то, что изменилось: контракт п.2 (новый), п.3 (расширен на.target/repaired_target_refs), AC1 (консолидация двух прежних AC), AC2 (переписан), «Принятые предположения» (новая первая строка), «Откат»/«Механизм»/ «Проблема» (уточнены упоминанием_repair_target_space_refs). Ничего за пределами этого дельта не менялось. - Контракт п.2 технически реализуем без нового кода-пути.
resolve_spaceуже создаётся в_repair_target_space_refsи уже используется без доп. условий дляmarker.space— расширение наvacuum.map_routes[].spaceдля существующего маркера не требует новой архитектуры резолвинга, только новый цикл по списку маршрутов, аналогичный уже существующему циклу поsegment_map(:1166-1179). Пять исходов резолвера перечислены в контракте без пропусков. - AC2 доказуем существующей тестовой инфраструктурой. Тесты
test_issue_244_*/test_issue_265_*уже прогоняют целевой маркер сvacuum-блоком через тот же путь ремонта; AC2 просит симметричный тест для поляmap_routes— не новый вид теста, а естественное расширение существующего паттерна. - Единообразие именования сохранено.
marker.vacuum.map_routes.spaceпродолжает схемуmarker.vacuum.segment_map,marker.space,marker.room_id— не изобретённая категория. - Смена состава AC1/AC2 не потеряла содержания. Всё, что проверял r1 в старых AC1 (импорт на чужую
установку) и AC2 (импорт на исходную установку,
same_source=True), присутствует в новом AC1 дословно (оба вариантаsame_sourceявно перечислены). - Раздел «Принятые предположения» не подменяет решение владельца. Уточнение «используются канонические
exact/lineage/live/ambiguous правила
_lineage_resolver; новый самостоятельный эвристический поиск не вводится» — техническое решение в зоне «агенты решают сами» (§7.1), не продуктовый вопрос, и явно записано, а не домыслено молча.
Чего не проверял
- Исполнение кода — по-прежнему не проводилось; кода для AC2 ещё нет, весь разбор — чтением, отмечено как «проверено чтением, не исполнением».
- Полнота мутационного покрытия обоих путей (входящий ремап + целевой ремонт) по AC3 — см. «Находки»: сознательно оставлено код-ревью, поскольку это вопрос фактического количества зарегистрированных мутантов при реализации, а не однозначности критерия сейчас.
- Реальная частота срабатывания сценария M1 (существующие у пользователей конфигурации с детачем
пространства и явным
map_routesна целевом маркере) — не оценивалась, не требуется на этапе ревью ТЗ; тот же пункт был не проверен и в r1, дельта его не касается. - Байтовая точность величин, приведённых в прозе r1 (4369 против фактических 6811 байт при совпадающем sha256) — не является предметом этого ревью; расхождение зафиксировано выше в «Унаследовано из r1» как наблюдение, не как находка (сам хеш и, следовательно, содержимое, идентифицированы верно).
Материал раунда
- Issue: #611, тело проверено на состоянии после правки автора 2026-09-23T01:24:00Z (текущее на момент выдачи задачи ревьюеру r2).
- Тело issue (r2, sha256 реального содержимого через
hashlib.sha256над UTF-8 текстом, полученным и черезgh issue view --json body, и независимо через GraphQLuserContentEdits[0].diff, оба совпали):3c8e0418858130189ed12efb3ba8f2c2c31de4b3dd376453751ed89f91e27189(9081 байт). - Тело issue (r1, для дельты, через GraphQL
userContentEdits[1].diff):94e2253da2c31f34919377a64987560e85945a765281dac78e320a29231f1553(6811 байт). - Код-база:
61292c9ea05f0d7d3a98392c81f3be89f2706c7c(не менялась с r1; HEAD37aed2c9b13b333ba8adb2c97f32158e9606618bдобавляет толькоdocs/reviews/SPEC-REVIEW-611-r1.md, класс A не тронут). - Комментарий аналитики (2026-09-23T01:13:13Z) и вердикт r1 (2026-09-23T01:22:09Z) учтены.
Итог
Вердикт: зелёный. Единственная находка предыдущего раунда (M1, Medium, в скоупе) закрыта точно и полностью:
новый контракт п.2 и переписанный AC2 явно распространяют перепривязку vacuum.map_routes[].space на второй
код-путь (_repair_target_space_refs), формулировка корректно перечисляет все пять исходов резолвера, ссылается
на реальные имена полей и функций и опирается на уже существующую тестовую инфраструктуру (#244/#265). Новых
Medium/High дельта не вводит. ТЗ готово к разработке.
Материал раунда
- Ветка:
dev, коммит37aed2c9b13b— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
99f40908d3eb60c13a07143e745f366cd2d0b733git log --all --format='%H %T' | grep 99f40908d3eb - Тело issue:
bb14ef7839530fe63fc2aedfdcf59e5433cbb80fa20c2a3a84a0971f818fb78e - Вердикт конвейера:
green· High 0