Files
houseplan-card/docs/specs/220-space-tab-reorder.md
T
Codex 5ba46dfd97 docs: take the atomic write out of the "free to change" block (#220 M3)
The assumptions section is explicitly labelled "technical, free to change",
and it held a requirement that AC3 and a mutant already test as a fact. Read
literally, it invited splitting the write in two — reopening the very window
in which markers move. The point now states the opposite: everything else in
that section is free, this one is normative and lives in section 8.3.

Issue: #220
User-Visible: no
2026-08-20 23:28:26 +03:00

22 KiB
Raw Blame History

Issue #220 — порядок пространств перетаскиванием вкладок

  • Issue: https://github.com/Matysh/houseplan-card/issues/220
  • Связанные контракты: #210 (фиксированный этаж), #170 (fallback-привязка маркера), #3 (комнаты без HA-зоны)
  • Тип: feature, обычный полный трек
  • Приоритет: P2
  • Пользовательское изменение: да

1. Сценарий и персона

Персона: администратор плана — тот, кто заводит пространства и поддерживает план в актуальном виде (docs/SCOPE.md, job J6).

Сценарий: дом рос не по порядку. Сначала завели «Квартиру», через месяц — «Подвал», потом «Мансарду». Вкладки стоят в порядке создания, а читается дом снизу вверх. Сегодня переставить их нечем: порядок вкладок — это порядок массива config.spaces, и единственный способ его изменить — удалить пространство и завести заново, потеряв планировку, устройства и привязки.

Момент: администратор в режиме редактора видит панель вкладок, берёт вкладку мышью и перетаскивает на новое место.

2. Что человек увидит до и после

До: вкладки стоят в порядке заведения; изменить порядок невозможно.

После: в режимах редактора вкладку можно взять мышью и перетащить; во время перетаскивания видно, куда она встанет; после отпускания порядок сохраняется и переживает перезагрузку. Свайп между этажами и стрелки киоска идут в новом порядке. Во View и на сенсорных экранах ничего не меняется: там вкладка по-прежнему только переключает пространство.

3. Подтверждённая причина

Порядок вкладок задан порядком массива: панель рендерится прямым проходом по модели (houseplan-card.ts:15717), navigationSpaces.map(...), отдельного поля сортировки нет. Запись идёт штатным _writeConfig (:6692) через houseplan/config/set с expected_rev; изменение порядка — обычная правка массива, схема и миграции не нужны.

Порядок массива несёт смысл в трёх местах. Проверено по коду dev:

Место Код Что зависит
Fallback-пространство маркера houseplan-card.ts:3350, :4273 → devices.ts:1058, :1063, :1249 firstSpaceId = _model[0]?.id: маркеры без явного пространства садятся в первое
Свайп и карусель logic.ts:1870-1879 spaceIds[(i + 1) % n] — сосед по индексу
floor числом (#210) houseplan-card.ts:1165 _fixedFloorState числовое значение разрешается как позиция, есть ветвь out-of-range-index

Первое — риск потери данных на ровном месте, второе и третье обязаны следовать новому порядку либо предупреждать.

4. Продуктовые решения владельца (2026-08-20)

  1. Перетаскивание — только мышь и только в режимах редактора (plan, devices, decor). Во View и киоске поведение вкладок не меняется вовсе.
  2. Числовой floor из #210 — после успешной перестановки показать предупреждение один раз.
  3. Клавиатурная альтернатива не нужна — docs/SCOPE.md честно фиксирует отсутствие клавиатурной навигации в редакторах.

5. Цели

  • Порядок пространств меняется без потери данных и переживает перезагрузку.
  • Ни один маркер не меняет своё размещение из-за перестановки.
  • Навигация (свайп, карусель) следует новому порядку немедленно.
  • Скрытая зависимость «позиция в массиве = смысл» становится явной и покрытой тестами.

6. Scope

  • Панель вкладок: обработчики pointerdown/pointermove/pointerup на .tab, порог начала перетаскивания, индикатор места вставки, курсор.
  • Запись нового порядка config.spaces через существующий _writeConfig.
  • Развязка firstSpaceId с позицией в массиве (см. §8).
  • Предупреждение о числовом floor (§4.2), ключи i18n en+ru.
  • docs/USER-GUIDE.md и docs/USER-GUIDE.ru.md, оба changelog.

7. Не входит в задачу

  • Перетаскивание на сенсорных экранах и во View — решение владельца §4.1.
  • Клавиатурная альтернатива — §4.3.
  • Порядок комнат, устройств, вложений, вкладок режимов (plan/devices/decor).
  • Сортировка «по имени» и любые автоматические порядки.
  • Изменение самого механизма floor из #210: числовая адресация остаётся как есть, задача только предупреждает.

8. Контракт поведения

8.1. Перетаскивание

  • Drag начинается только при _canEdit, не в киоске, this._mode !== 'view', event.pointerType === 'mouse' и после смещения ≥ 4 px по горизонтали. До порога это обычный клик — переключение пространства сохраняется.
  • Вкладка «+» (.tabadd) в перестановке не участвует и не может стать целью.
  • Во время перетаскивания видно место вставки; при отпускании вне панели порядок не меняется.
  • Одно пространство — перетаскивать нечего, обработчики не навешиваются.

8.2. Запись

  • Новый порядок пишется целиком массивом spaces штатным _writeConfig с expected_rev; конфликт ревизий обрабатывается как у любой другой правки.
  • Активное пространство после перестановки не меняется: перетащили не ту, что открыта — открытая остаётся открытой; перетащили открытую — она открыта на новом месте.

8.3. Маркеры не двигаются: материализация вместо нового поля

Обязательное свойство одно: ни один маркер не меняет space из-за изменения порядка. Достигается оно не хранением якоря, а тем, что перестановка делает явной ту привязку, которая до неё держалась на позиции.

Норматив. В той же транзакции записи, что и новый порядок, каждый маркер, чьё пространство сегодня разрешается через firstSpaceId — то есть у него нет ни area, ведущей в пространство, ни собственного space — получает явное space, равное тому пространству, в котором он находится сейчас, до перестановки. После этого его размещение от порядка не зависит вовсе, и firstSpaceId перестаёт быть для него значимым.

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

  • новых полей нет — CONFIG_SCHEMA, scripts/config-field-registry.mjs и docs/CONFIG-COMPATIBILITY.md не трогаются;
  • запись идёт одним config/set с уже существующими полями маркеров;
  • правка данных минимальна и сохраняет наблюдаемое состояние: маркер остаётся ровно там, где пользователь его видел;
  • откат не нужен: явное space — валидное и предпочтительное состояние, которое карточка и так пишет при любом сохранении маркера.

Граница. Материализуются только маркеры, разрешавшиеся через firstSpaceId. Маркеры с area или с уже заданным space не трогаются — проверяется AC3.

Если исполнитель обнаружит случай, который материализация не покрывает (например, маркер вообще не попал в текущую модель), это не повод возвращать якорь молча: такой случай выносится в ревью как находка.

8.4. Навигация

swipeTarget продолжает работать по индексам — он получает уже переупорядоченный spaceIds, поэтому изменений не требует. AC4 фиксирует, что свайп идёт в новом порядке немедленно, без перезагрузки.

8.5. Предупреждение о числовом floor

После первой успешной перестановки в текущей сессии карточка показывает тост: «Порядок пространств изменён. Если где-то этаж карточки задан номером, проверьте такие панели». Показывается один раз за сессию, независимо от числа перестановок, и не блокирует работу.

9. Данные, i18n, a11y, touch, privacy, security

Touch editor: not exposed. Классификация по docs/TOUCH-SUPPORT.md §153: перетаскивание вкладок на сенсорных экранах не появляется вовсе — ни как degraded-вариант, ни как долгое нажатие. Это решение владельца §4.1, а не недоделка. Обоснование: вкладки живут во View, где переключение пространств — fully supported на тач и release-blocking; любой жест на вкладке рискует съесть тап. Порядок пространств меняется на десктопе, как и остальная работа с планом.

Safety floor §69 того же документа соблюдён по построению: на тач-устройстве поведение вкладок не меняется ни на йоту, значит ни потери данных, ни случайной мутации при мультитаче новая функция внести не может.

  • Данные: порядок элементов массива spaces плюс материализация неявной привязки маркеров (§8.3). Новых полей конфигурации не появляется, схема и CONFIG_SCHEMA не меняются — см. §8.3 о том, почему выбран путь без нового поля и что это значит для docs/CONFIG-COMPATIBILITY.md.
  • i18n: один новый ключ тоста (en + ru) и, при необходимости, title вкладки в режиме редактора.
  • a11y: без изменений — клавиатурной навигации в редакторах нет и не обещано (docs/SCOPE.md).
  • privacy / security: новых данных и путей записи нет.

10. Performance

Панель вкладок — единицы элементов. Обработчики навешиваются только в режимах редактора и только для мыши. Влияния на бюджеты нет; performance-профили не затрагиваются.

11. Риски

  1. Drag съедает клик. Порог 4 px и проверка pointerType — единственное, что отделяет перестановку от переключения. AC2 проверяет обе стороны.
  2. Маркеры уезжают. Главный риск задачи: firstSpaceId сегодня буквально «первый по порядку». Закрывается §8.3 + AC3 + мутант.
  3. Числовой floor. Предупреждение — смягчение, а не защита: чужие панели отсюда не видны. Записано в USER-GUIDE.
  4. Конкурентная правка. Перестановка уходит с expected_rev; на конфликт реагирует общий механизм — AC6.

12. Acceptance criteria

  1. AC1 — перестановка и запись. Перетаскивание вкладки меняет её позицию; порядок сохраняется на сервере и переживает перезагрузку страницы. Доказательство: smoke.
  2. AC2 — клик не сломан. Клик по вкладке без смещения переключает пространство; смещение ≥ 4 px начинает перетаскивание и клик не срабатывает. Доказательство: smoke.
  3. AC3 — маркеры не двигаются. Конфигурация с маркером, чьё размещение опирается на fallback-пространство, после перестановки даёт то же space для каждого маркера; такой маркер получает явное space в той же записи, а маркеры с area или с уже заданным space остаются побитово прежними. Тест красный до §8.3. Доказательство: unit (buildDevices + запись).
  4. AC4 — навигация следует порядку. После перестановки swipeTarget возвращает нового соседа немедленно, без перезагрузки. Доказательство: unit + smoke.
  5. AC5 — границы включения. Обработчики не навешиваются во View, в киоске, при pointerType !== 'mouse' и при единственном пространстве; вкладка «+» не участвует. Доказательство: unit (чистая функция решения) + smoke для View.
  6. AC6 — конкурентная правка. Перестановка уходит с expected_rev; конфликт ревизий не молчит и не теряет порядок. Доказательство: unit либо smoke по существующему механизму.
  7. AC7 — предупреждение о числовом floor. После первой перестановки в сессии показан тост; повторные перестановки его не повторяют. Доказательство: smoke.
  8. AC8 — release-артефакты. Оба changelog, оба USER-GUIDE описывают перестановку и её ограничения (мышь, редактор, числовой floor); dist, demo и integration bundle идентичны друг другу. Доказательство: diff + сверка копий бандла.

13. План автотестов

  • test/ — юниты на чистую функцию «можно ли начать перетаскивание» (AC5) и на стабильность firstSpaceId в buildDevices (AC3); проверка swipeTarget на переупорядоченном списке (AC4).
  • demo/smoke_space_tab_reorder.mjs — новый смок: перестановка мышью, запись, сохранение активного пространства, клик без смещения, отсутствие drag во View, тост о числовом floor (AC1, AC2, AC4, AC5, AC7).
  • Существующие смоки навигации и smoke_fixed_floor (#210) — прогон без правок.

14. Мутационный гейт (scripts/mutation-gate.mjs)

id Патч Guard
tab-reorder-not-persisted перестановка меняет только локальную модель, _writeConfig не зовётся смок AC1
tab-reorder-eats-click убрать порог смещения — любой pointerdown начинает drag смок AC2
reorder-skips-materialization записывать новый порядок, не материализуя привязку маркеров юнит AC3
materialization-touches-bound-markers материализовать space и у маркеров с area юнит AC3
tab-reorder-ignores-pointer-type снять проверку pointerType === 'mouse' юнит AC5

15. Release-артефакты

  • docs/CHANGELOG.md + docs/CHANGELOG.ru.md — User-Visible: yes;
  • docs/USER-GUIDE.md + docs/USER-GUIDE.ru.md — раздел про панель вкладок: перестановка мышью в редакторе, ограничение по тачу, оговорка про числовой floor;
  • golden не затрагивается: панель вкладок в матрице не участвует, визуальных изменений в состоянии покоя нет;
  • performance-профили не затрагиваются.

16. Откат

Порядок — обычные данные, обратной миграции не требуется: администратор перетаскивает вкладки назад. Код откатывается снятием обработчиков; развязка firstSpaceId (§8.3) остаётся полезной сама по себе и откату не подлежит.

17. Принятые предположения (техническое, менять свободно)

  1. Порог 4 px взят как минимально заметное намеренное движение; точное число не продуктовое решение и может быть изменено ревьюером.

  2. Механика вставки — вставка перетаскиваемой вкладки перед той, над серединой которой отпущена мышь. Альтернатива (обмен местами) отвергнута: при переносе через несколько позиций она даёт неожиданный результат.

  3. Атомарность записи предположением не является. Требование «порядок и материализация уходят одним config/set» — норматив §8.3, его проверяет AC3 и стережёт мутант reorder-skips-materialization. Разносить запись на две нельзя: между ними возникает окно, где порядок уже новый, а привязка ещё старая — ровно тот риск, ради которого §8.3 написан. Здесь пункт оставлен только как указатель: свободно меняется всё остальное в этом разделе, но не это (находка M3 ревью r2; прежняя редакция §17 держала норматив под заголовком «менять свободно»).

    Историческая справка к §8.3: первая редакция предлагала хранить якорь firstSpaceId в settings — отвергнута по M2 ревью r1 как новое поле конфигурации.

  4. Тост о числовом floor показывается всегда, а не только когда числовой floor действительно где-то используется: карточка не видит чужие панели, поэтому условие проверить нечем.