# SPEC-REVIEW-220-r2 - **Issue:** https://github.com/Matysh/houseplan-card/issues/220 - **ТЗ:** `docs/specs/220-space-tab-reorder.md` (текущий коммит `e57e1d9`, «docs: close M1 and M2 from the spec review of #220») - **Ревьюер:** Claude (роль «ревьюер ТЗ», PROCESS.md §2.4) - **Заход:** r2 · блокирующих циклов 1/4 - **Вердикт:** жёлтый · High: 0 · Medium: 1 (в скоупе задачи) ## Скоуп ревью Разбор по дельте (PROCESS.md §2.9, #214), а не заново — с одним расширением. Раунд r1 закрыл два Medium (M1, M2) точечной правкой текста ТЗ, но правка по M2 не косметическая: §8.3 полностью переписан — вместо «хранить якорь в `settings`» теперь «материализовать привязку в той же записи». Это смена контракта поведения (новая нормативная процедура записи конфигурации), а не переформулировка факта, поэтому именно этот кусок разобран полно — включая сверку с кодом `devices.ts`/`houseplan-card.ts` заново, а не по памяти r1. Остальное (§1–§7, §10–§16, AC1/2/4/5/6/7/8, мутационный гейт вне AC3, план автотестов) дельта не касается — унаследовано из r1 без повторной проверки (раздел ниже). ## Как проверялось 1. Найден вердикт r1 в комментариях issue #220 (`https://github.com/Matysh/houseplan-card/issues/220#issuecomment-5361187761`, `2026-08-20T20:17:49Z`) и документ `docs/reviews/SPEC-REVIEW-220-r1.md`, зафиксировавший ТЗ на коммите `0fd2331`. 2. Объявлена дельта: `git diff 0fd2331..HEAD -- docs/specs/220-space-tab-reorder.md` (HEAD = `e57e1d9`). Дельта — 58 вставок / 19 удалений, только §8.3, §9, AC3, мутационная таблица, §17.3; остальные разделы файла побитово не менялись. 3. Комментарий владельца о закрытии r1 называет коммит `d0a5bfa` — такого объекта в репозитории нет (`git cat-file -t d0a5bfa` → `fatal: Not a valid object name`), вероятно переписан ребейзом веток issue. Не доверяю названному SHA, проверка сделана по факту — прямым диффом файла между `0fd2331` (зафиксирован в документе r1) и текущим `HEAD` (`e57e1d9`, единственный коммит после `c3278dd`/review-doc r1, который трогает файл ТЗ) — содержимое дословно совпадает с тем, что владелец описал в комментарии, расхождение только в имени SHA в тексте комментария. 4. Каждая находка r1 (M1, M2) сверена не по заявлению автора, а по строке текста ТЗ — см. таблицу «Закрытие раунда r1». 5. `docs/TOUCH-SUPPORT.md` (§153–165, «Documentation rule») прочитан повторно, построчно сверена ровно та формулировка ярлыка, которую требует правило. 6. Новая нормативная процедура §8.3 (материализация в той же записи) сверена с фактическим кодом разрешения `firstSpaceId`: `src/devices.ts:1045-1065` (`resolveExplicitMarkerPlacement`, включая ветку `manualRoomWithoutArea`, ранее не разбиравшуюся отдельно ни в issue, ни в r1) и `src/devices.ts:1249` (виртуальный маркер). Оба случая подтверждают общий критерий ТЗ «нет ни `area`, ведущей в пространство, ни собственного `space`» — он корректно обобщает все три ветки кода, включая ветку marker-без-HA-area из #3, которую ТЗ не называет по номеру, но покрывает по содержанию. Также проверено третье использование `firstSpaceId` (`src/houseplan-card.ts:18818`, черновик маркера в диалоге) — это непостоянный preview, не пишется в конфиг, к обязательству §8.3 («ни один маркер не меняет `space`») не относится: диалог всегда пересчитывает его заново по актуальной модели. 7. AC3 (единственный AC, задетый дельтой) перепроверен на однозначность и доказуемость с учётом новой формулировки. AC1, AC2, AC4–AC8 дельтой не задеты — унаследованы из r1 без повторной проверки. ## Закрытие раунда r1 | Находка r1 | Чем закрыта | Где это видно | |---|---|---| | **M1** — нет обязательной touch-классификации по `docs/TOUCH-SUPPORT.md` §153 | В §9 добавлена строка **`Touch editor: not exposed.`** с обоснованием (вкладки живут во View, где переключение — fully supported/release-blocking; жест на вкладке рискует съесть тап) и отдельно — что safety floor §69 соблюдён по построению | `docs/specs/220-space-tab-reorder.md:156-165`; формулировка ярлыка совпадает с требуемой буква в букву (сверено с `docs/TOUCH-SUPPORT.md:160-162`) | | **M2** — §9 отрицал новое поле конфигурации, §8.3/§17.3 предполагали якорь в `settings` | §8.3 переписан: вместо якоря — материализация существующего поля `marker.space` в той же записи, что и порядок; §9 теперь говорит «новых полей конфигурации не появляется... см. §8.3», и это уже не противоречит остальному документу, а согласуется с ним; §17.3 переписан в терминах материализации, старое предположение про якорь явно помечено отвергнутым | `docs/specs/220-space-tab-reorder.md:107-139` (§8.3), `:167-170` (§9), `:268-271` (§17.3); AC3 (`:202-206`) и мутационная таблица (`:240-241`) синхронно обновлены под новую механику | Обе находки закрыты не декларативно: правка убирает саму причину противоречия (M2) и добавляет предметное содержание, а не просто ярлык (M1). Новых полей конфигурации в изменённом варианте действительно не появляется — проверено по коду: материализация пишет уже существующее поле маркера `space`, `CONFIG_SCHEMA` и `scripts/config-field-registry.mjs` в этой задаче не упоминаются как изменяемые, и это утверждение больше не противоречит §9. ## Унаследовано из r1 Принято без повторной проверки в этом раунде — проверено и подтверждено в `docs/reviews/SPEC-REVIEW-220-r1.md` на коммите `0fd2331`, дельта эти разделы не касается: - Персона, сценарий и job `docs/SCOPE.md` J6 (§1–§2 ТЗ). - Продуктовые решения владельца §4 (тач/права/клавиатура) и их соответствие owner-decision в комментариях issue. - §8.1, §8.2, §8.4, §8.5 контракта (порог 4px, `.tabadd`, запись через `_writeConfig`/`expected_rev`, `swipeTarget`, тост о числовом `floor`). - AC1, AC2, AC4, AC5, AC6, AC7, AC8 — однозначность и способ доказательства. - Мутанты `tab-reorder-not-persisted`, `tab-reorder-eats-click`, `tab-reorder-ignores-pointer-type` (не переписаны в дельте). - §11 (риски), §15 (release-артефакты), §16 (откат) кроме уже отражённых в дельте формулировок. - Low-находка r1 про `.tabedit`/`pointerdown` — оставлена без правки экспертным решением r1 («реализационная деталь, накрываемая кодревью»), дельта её не касается, пересматривать нет причины. ## Находки ### M3 — Medium, в скоупе. §17.3 маркирует нормативное требование как «свободно меняемое» Заголовок §17 — «Принятые предположения (**техническое, менять свободно**)». Пункт 3 внутри него (`docs/specs/220-space-tab-reorder.md:268-271`) гласит: «Материализация привязки (§8.3) выполняется в том же `config/set`, что и порядок, а не отдельной записью: две записи дали бы окно, в котором порядок уже новый, а привязка ещё старая». Но именно это же самое требование в §8.3 названо не предположением, а **«Норматив»** (`:113`, буквально это слово стоит заголовком абзаца) — и оно уже вошло в тестируемый контракт: AC3 требует «маркер получает явное `space` **в той же записи**» (`:203-204`), а мутант `reorder-skips-materialization` (`:240`) специально ловит его нарушение. **Почему это не формальность.** §17.3 своим же текстом объясняет, почему атомарность нельзя менять свободно: если порядок и материализация уйдут двумя разными записями `config/set`, то в промежутке между ними `firstSpaceId = model[0]?.id` уже пересчитан по новому порядку — маркер, ещё не материализованный, в это окно резолвится в **не то** пространство. Если исполнитель прочитает заголовок §17 буквально («менять свободно») и раздельными записями, а не заголовок §8.3 («Норматив»), он получит ровно тот риск, ради которого писан весь раздел 8.3 и ради которого задача оценена в сложность 4/10 (см. §11, риск №2 «Маркеры уезжают» — главный риск задачи). Опасность усугубляется тем, что при таком (неверном) выборе реализации собственный юнит-тест AC3, написанный тем же исполнителем под ту же (неверную) модель, скорее всего будет проверять «согласованность после двух записей», а не «атомарность одной записи» — то есть мутационный гейт `reorder-skips-materialization` перестанет быть надёжным барьером именно в том сценарии, для которого его писали. **Чем закрывается:** убрать пункт 3 из §17 (он не является свободным предположением — норма уже сформулирована в §8.3 как обязательная и проверяется AC3), либо явно пометить его как исключение из «менять свободно» с отсылкой на §8.3/AC3. Правка текстовая, AC и скоуп не меняются. ## AC — что перепроверено дельтой | AC | Задет дельтой | Однозначен | Доказательство | Комментарий | |---|---|---|---|---| | AC1, AC2, AC4, AC5, AC6, AC7, AC8 | нет | — | — | унаследованы из r1 без повторной проверки | | AC3 | да | да | `unit` (`buildDevices` + запись) | формулировка усилена («маркеры с `area`/`space` — побитово прежними»), однозначна и доказуема; ровно вокруг него — находка M3 (не сам AC, а соседний раздел §17.3, который создаёт риск неверной трактовки реализации, проверяемой этим же AC) | ## Что проверено и корректно - M1 и M2 закрыты предметно, а не декларативно — см. таблицу выше; §9 больше не противоречит §8.3. - Новая механика §8.3 (материализация) корректно обобщает все три реальных пути резолюции `firstSpaceId` в `devices.ts`, включая ветку `manualRoomWithoutArea` (маркер ручной комнаты без HA area, контракт #3), которую ни issue, ни r1 не разбирали пофамильно — критерий «нет `area`, ведущей в пространство, и нет собственного `space`» покрывает её без исключений. - Материализация не задевает preview-путь черновика маркера (`houseplan-card.ts:18818`) — он не персистентный, обязательство §8.3 на него не распространяется, и в ТЗ об этом ничего лишнего не заявлено. - Новая пара мутантов (`reorder-skips-materialization`, `materialization-touches-bound-markers`) корректно закрывает ровно два направления ошибки материализации (пропуск и избыточность), заменив собой устаревший `first-space-follows-order`. - Владелец сам в комментарии честно отделил решение от подгонки: назвал две альтернативы снятия противоречия M2 («признать поле» / «обойтись без него») и выбрал вторую с объяснением — это ровно то, что PROCESS требует от продуктовых/технических решений, выносимых в ревью. ## Чего не проверял - Не проверял реализацию — кода ещё нет, это ревью ТЗ. - Не запускал `tsc`/`npm test`/`npm run build` — класс изменения C (документация), гейты неприменимы, как и в r1. - Не пересматривал разделы, не тронутые дельтой (§1–§7, §10–§16, AC1/2/4-8, три из пяти мутантов) — они унаследованы из r1 на коммите `0fd2331`, дельта их не меняла ни байтом. - Не проверял точность SHA `d0a5bfa`, названного владельцем в комментарии о закрытии r1 — объект не существует в репозитории; вместо этого верификация сделана по прямому диффу файла до `HEAD` (`e57e1d9`), см. «Как проверялось», п.3. Расхождение SHA не влияет на вывод ревью, но зафиксировано как наблюдение. ## Итог Обе находки r1 закрыты предметно. Правка §8.3 (материализация вместо якоря) — смена контракта поведения, а не косметика, и именно в ней найдена новая находка M3: §17 маркирует как «свободно меняемое» требование, которое сам же документ в §8.3 называет обязательным норматив и проверяет тестируемым AC3. Находка в скоупе, чинится точечной правкой текста (перенос/каветирование пункта 17.3), без изменения AC, скоупа или контракта поведения. High-находок нет. Вердикт — жёлтый: правка проходит третий цикл ревью ТЗ (заход r3, лимит циклов ревью ТЗ — 4).