# Спек-ревью #162 — заход r2 - Issue: [#162](https://github.com/Matysh/houseplan-card/issues/162) — «Многоэтажный робот: сопоставление карт пространствам отдельно от базы» - Этап: spec (PROCESS.md §2.4) - Документ ТЗ: `docs/specs/162-vacuum-map-space-routing.md` - Заход: r2, SHA `4c56b389` - Заход r1: SHA `086b3682`, вердикт жёлтый, документ `docs/reviews/SPEC-REVIEW-162-r1.md` (закоммичен на `dd2b5d74`) - Блокирующих циклов израсходовано: 1 из 4 (жёлтые/красные тратят бюджет; этот заход зелёный — бюджет не меняется, PROCESS.md §4, #227) ## Скоуп разбора Правка r1→r2 — точечный ответ на единственную находку прошлого раунда (Medium, §11.3 «compatible source»), без ребейза на ушедший вперёд dev и без смены контракта поведения где-либо ещё. Дельта локальна: ``` git diff 086b3682..4c56b389 -- docs/specs/162-vacuum-map-space-routing.md ``` затрагивает только: - заголовок статуса документа (строка 7); - §11.3 «Legacy trails» (переписан, добавлен §11.3.1 «Правило усыновления»); - таблицу AC — строка AC14; - таблицу §16.5 — новый мутант M-H и итоговое число мутантов (7→8); - changelog в конце документа — новый bullet. Ни один другой раздел, AC или мутант диффом не затронут. Это укладывается в критерий локальности из инструкции (не ребейз, не новая подсистема, объём несопоставим с исходной задачей) — разбор ПОЛНЫЙ по дельте, остальное наследуется из r1 без повторной проверки. ## Как проверялось 1. Прочитан вердикт r1 и весь тред комментариев issue #162, включая ответ автора на Medium (комментарий 2026-09-03T15:28:32Z) и уточнение SHA (2026-09-03T15:29:15Z: правка перебазирована с `3ed21af5` на `4c56b389` после коммита артефакта ревью `dd2b5d74`). 2. Сверен факт: `git log --oneline 086b3682..4c56b389` → ровно два коммита (`dd2b5d74` документ ревью, `4c56b389` правка ТЗ). Рабочее дерево на `4c56b389` соответствует HEAD ветки — ревьюется именно эта версия. 3. Перечитан переписанный §11.3 и новый §11.3.1 целиком, сверены все перекрёстные ссылки внутри документа (§8.1 `VacuumMapRoute.source`/`map_id`, §8.2 nullish-контракт, §7.3 explicit conversion, AC13, AC14, M-H, §12). 4. Фактическая посылка нового текста — «серверная запись run физически несёт только `{map_id, started, ended, points}`, и recorder всегда пишет её по единственному текущему `marker.vacuum.source`» — перепроверена чтением `custom_components/houseplan/trails.py` заново (не унаследовано из r1, так как это ядро самой правки): - `TrailBook.on_point` создаёт `cur = {"map_id": map_id, "started": now, "ended": None, "points": []}` (`trails.py:98`) — поля `source` нет, утверждение верно; - `async_refresh` строит `pairs: source → [(marker_id, vac)]` из `v.get("source")` — ровно один root source на marker в момент подписки (`trails.py:167-184`); `_sample` вызывает `book.on_point(marker, ...)` для источника, на который сейчас подписан marker (`trails.py:350-384`). Это подтверждает, что «единственный свидетель» — текущий `marker.vacuum.source`, а не историческая величина, восстановить которую неоткуда — с этим ограничением текст ТЗ прямо работает (fail-closed при несовпадении/пустом source), а не скрывает его. 5. Проверено, что старый неопределённый термин «compatible source» пропал из нормативного текста (остался только в заголовке статуса документа как ссылка на закрытую находку — `grep` подтверждает единственное вхождение). 6. Проверена согласованность схемы: `VacuumMapRoute.map_id`/`source` — обязательные `string` (§7.1, docs/specs/162-vacuum-map-space-routing.md:254-260), `MarkerVacuumCfg.source` — `string | null` (строка 267), поэтому проверка «`rootSource` пуст/удалён» в §11.3.1 корректно покрывает `null`/`undefined`/`""`. 7. Пересчитано число мутантов в таблице §16.5 (7 строк M-A…M-G + новая M-H = 8) против итоговой фразы «каждый из восьми» — совпадает. 8. Дешёвые гейты кода не запускались: дельта раунда — правка одного .md-файла в `docs/specs/`, `src/**` и `custom_components/**/*.py` не затронуты (`git diff --stat 086b3682..4c56b389` — единственный файл, `docs/specs/162-vacuum-map-space-routing.md`). `npx tsc --noEmit`, `npm test`, `npm run build`, `node scripts/check-docs.mjs`, инварианты модели и смоки к этой правке неприменимы: они гоняют код, а код не менялся. Значения из r1 (tsc чисто, build собирается, `npm test` 1819/0/1, идентично базовой линии ТЗ) наследуются без повторного прогона — правка их не затрагивает. ## Находки Нет. Единственная находка r1 (Medium, §11.3, в скоупе) закрыта содержательно, новых находок делтой не внесено. ## Закрытие раунда r1 | Находка r1 | Чем закрыта | Где это видно | |---|---|---| | §11.3 п.2: термин «compatible source» не определён; серверная схема run не хранит `source`, поэтому «совместимость источника» нельзя вычислить сравнением с содержимым run буквально; AC14 не сводится к детерминированному unit-тесту | §11.3 переписан: явно назван единственный свидетель — корневой `marker.vacuum.source` в момент записи, а не поле run. Введена чистая функция `adoptLegacyRun(run, routes, rootSource)` с явным двухусловным отбором кандидатов (`map_id` строго, `source` — только если `rootSource` непустой) и ровно тремя исходами (unique / `orphan_run` / `ambiguous_run`), все три fail-closed кроме unique. AC14 переформулирован под эти три исхода со ссылкой на §11.3.1; добавлен мутант M-H на конкретную регрессию («при двух кандидатах берётся первый») | `docs/specs/162-vacuum-map-space-routing.md` §11.3 и §11.3.1 (строки ~529–566 после правки), таблица AC строка AC14, таблица §16.5 строка M-H, changelog-bullet в конце файла | Факт закрытия подтверждён не заявлением автора, а перечитыванием переписанного текста и независимой перепроверкой его фактической посылки в `custom_components/houseplan/trails.py` (см. «Как проверялось», п.4) — текст не просто переименовал термин, а свёл его к вычислимому по сохранённым данным правилу. ## Унаследовано из r1 Без повторной проверки в этом раунде — дельта их не задевает: - Все факты §3.2 «Подтверждённая техническая база», сверенные в r1 чтением `src/houseplan-card.ts:11334,12347`, `src/types.ts`, `src/vacuum.ts:310,322`, `src/space-deletion.ts`, `src/space-reference-repair.ts`, `test/core-file-budget.test.mjs` — правка их не касается. - Полнота обязательных разделов §7.1 PROCESS.md (21 раздел) и то, что все 20 AC пронумерованы и снабжены способом доказательства — делта меняет формулировку только одной строки (AC14), остальные 19 не тронуты. - Overpromise в `docs/USER-GUIDE.ru.md:1504`, зафиксированный как часть продуктового обоснования задачи. - Результаты дешёвых гейтов на коде: `npx tsc --noEmit` чисто, `npm run build` собирается, `npm test` → 1819 pass / 0 fail / 1 skip (совпадает с базовой линией ТЗ). - Продуктовая рамка (docs/SCOPE.md, J1/J6) и оценка сложности/риска — не предмет этого раунда, изменений в §1–§6, §9–§10, §13–§15, §17 нет. Источник наследования: `docs/reviews/SPEC-REVIEW-162-r1.md`, SHA `086b3682`. ## Что проверено и корректно (по дельте r2) - §11.3.1 определяет `adoptLegacyRun` как чистую функцию от только сохранённых данных — тестируема unit-тестом без HA, как и заявлено. - Оба поля, на которые опирается правило (`route.map_id`, `route.source`), обязательные строки в схеме `VacuumMapRoute` (§7.1) — отбор кандидатов не зависит от опциональных полей, кроме сознательно опционального `rootSource`. - Три исхода (unique/`orphan_run`/`ambiguous_run`) исчерпывающие и fail-closed по обоим отрицательным веткам: ничего не рисуется, `route_id` не дописывается, запись не переписывается и не удаляется — согласуется с общей fail-closed доктриной документа (§8, §12). - AC13 («byte-for-byte until routing edit») не сломан: §11.3 явно оставляет поведение при отсутствии explicit routes прежним «байт в байт». - Число мутантов в тексте и таблице согласовано (8). - Никаких новых недоказанных допущений (незаявленных как предположение) правка не вносит — единственное новое фактическое утверждение (структура serverной записи run и то, что recorder пишет её по текущему root source) проверено чтением кода, а не принято на слово. ## Чего не проверял - Реализацию — её нет, этап spec. Соответствие `adoptLegacyRun`/`trails.py` зеркалированию будет предметом code-review. - Остальные 19 AC и разделы вне §11.3/§11.3.1/AC14/§16.5 — не проверялись повторно, приняты по наследованию из r1 (см. выше), т.к. дельта их не касается. - Гейты кода (tsc/test/build/check-docs/invariants/smoke) в этом раунде не запускал: дельта не трогает `src/**` и `custom_components/**/*.py`, значения из r1 остаются в силе без изменений входных файлов. ## Вердикт Зелёный. Находка r1 закрыта по существу и проверяемо, новых находок нет, бюджет циклов не расходуется (PROCESS.md §4, #227). --- ## Материал раунда - Ветка: `issue/162-vacuum-map-space-routing`, коммит `4c56b389ca9e` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `ec0c63dac8b0e34afb2f04aa30708ee5b8b3216e` ``` git log --all --format='%H %T' | grep ec0c63dac8b0 ``` - ТЗ `docs/specs/162-vacuum-map-space-routing.md`, блоб `a8edcfbe6c37700095f6f8582258eac4fcb58ad9` ``` git log --all --find-object=a8edcfbe6c37700095f6f8582258eac4fcb58ad9 -- docs/specs/162-vacuum-map-space-routing.md ```