diff --git a/docs/specs/456-copy-space.md b/docs/specs/456-copy-space.md new file mode 100644 index 00000000..a1def3ee --- /dev/null +++ b/docs/specs/456-copy-space.md @@ -0,0 +1,611 @@ +# ТЗ #456 — копирование пространства без комнат и устройств + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/456 +- **Приоритет:** P2 +- **Тип:** feature +- **Трек:** полный — новый UX, преобразование геометрии и составная запись с + возможной предварительной оптимизацией +- **Решения владельца:** Q1 Default; Q2 — отдельное подтверждение только при + необходимой оптимизации; Q3 — после успеха перейти в копию; Q4 Default +- **Связанные контракты:** `docs/SCOPE.md` J4/J6, + `docs/WALL-THICKNESS.md`, `docs/BACKDROP.md`, #132, #173, #248, #442 + +## 1. Сценарий + +Администратор дома на desktop уже настроил один этаж и хочет создать второй с +той же строительной геометрией: несущими стенами, самостоятельными +перегородками, колоннами, проёмами, декоративными элементами, видом и +подложкой. Комнаты на новом этаже отличаются, поэтому копировать их и связанные +с ними устройства нельзя. + +В настройках исходного пространства пользователь нажимает **«Копировать»**, +проверяет предложенное имя и создаёт новое пространство. Если исходный план +сначала требуется привести к канонической геометрии, House Plan отдельно +предупреждает, что Optimize затронет весь план. После успеха открывается копия, +готовая к рисованию новых комнат поверх перенесённых стен. + +## 2. Что человек увидит до и после + +**До:** повторяющийся этаж приходится создавать с нуля и вручную заново +рисовать все стены, проёмы, колонны и декор. + +**После:** одна команда создаёт новое пространство с той же физической основой, +видом и подложкой, но без комнат, устройств и их привязок; пользователь сразу +попадает в копию и начинает размечать новые комнаты. + +## 3. Проблема и подтверждённое текущее состояние + +Диалог пространства живёт в `src/houseplan-editor-runtime.ts`; в режиме edit +его footer сейчас содержит Delete, Cancel и Save. Обычное создание пространства +добавляет пустую модель через `createEmptySpaceConfig`, записывает полный config +и выбирает новый space. В onboarding отдельная копия диалога обслуживает случай, +когда пространств ещё нет, и к этой функции не относится. + +Побайтовая копия пространства невозможна: + +- в model v9 каждый `wall_segments[]` обязан иметь одного или двух владельцев в + `rooms[]`; при пустом `rooms` схема отвергает такие сегменты; +- перенос контуров в `room_drafts` дублирует общие стены и на реалистичном плане + нарушает junction valence; +- `partitions[]` — существующая модель самостоятельных стен без комнаты; она + допускает толщину, участвует в snap и умеет быть host проёма. + +Проверенный путь — преобразовать каждый канонический `wall_segment` источника в +новую partition, также скопировать уже существующие partitions и перевесить +проёмы по карте старых и новых ID. `Optimize` уже строит канонический candidate +в `optimizePlans`, проверяет его через `_checkOptimizeGeometry` и сохраняет весь +config/layout серверной командой `houseplan/plan/optimize` с ревизиями. + +Открытых дублей задачи не найдено. Изменение укладывается в J4/J6: это GUI-путь +создания и дальнейшего обслуживания повторяющихся этажей, а не отдельная модель +здания или внешний редактор. + +## 4. Скоуп + +1. Добавить в настройки существующего пространства кнопку + **«Копировать»**, визуально отделённую от опасной группы Delete. +2. Добавить компактный диалог имени нового пространства с уникальным + предложенным именем. +3. Перед копированием использовать тот же расчёт Optimize, тот же preflight и + те же правила канонизации, что команда «Оптимизировать». +4. Если Optimize действительно меняет план, запросить отдельное подтверждение, + явно сообщив, что будет оптимизирован весь план. +5. Создать копию без rooms, room drafts, open spans, compatibility walls, + markers и любых привязок устройств. +6. Перенести в копию все физические стены, проёмы, wall columns, decor, + display settings, grid scale, view box и backdrop с новыми geometry ID. +7. После успешной записи выбрать копию и открыть редактор Плана с новым, + пустым состоянием инструмента «Стены». +8. Добавить unit, integration, mutation и browser smoke доказательства, а также + обновить пользовательскую документацию и оба changelog при реализации. + +## 5. Не-скоуп + +- копирование комнат, HA Areas, room labels и их layout-позиций; +- копирование markers, устройств, их позиций, групп, vacuum routes или + calibration; +- копирование `contact`/`lock` привязок проёмов; +- копирование незавершённых цепочек `room_drafts`; +- автоматическое распознавание или создание комнат в новом пространстве; +- преобразование partitions обратно в контурные стены до рисования комнаты; +- копирование пространства между установками HA; +- изменение самой логики Optimize, его отчёта и правил очистки; +- кнопка Copy в onboarding; +- новый общий Undo для создания/удаления пространства; +- изменение поведения обычных Create, Save и Delete. + +## 6. Контракт UX + +### 6.1. Точка входа + +Кнопка **«Копировать»** видна только при `spaceDialog.mode === 'edit'` и стоит в +footer рядом с действиями пространства, но не внутри +`dialog-action-danger`. У неё иконка `mdi:content-copy`, обычный neutral/ghost +стиль и локализованные label/title. + +Кнопка disabled, пока текущий диалог busy. Нажатие не сохраняет возможные +несохранённые изменения полей исходного диалога: как Delete, Copy действует над +последним сохранённым server config. Это состояние не маскируется под Save. + +### 6.2. Диалог имени + +Нажатие открывает отдельный компактный диалог: + +- заголовок сообщает, что создаётся копия текущего пространства; +- единственное редактируемое поле — имя нового пространства; +- значение по умолчанию — первый свободный вариант `«<исходное имя> (N)»`, где + поиск начинается с `(2)` и сравнение выполняется по точной строке после trim; +- пустое/состоящее из пробелов имя блокирует основную кнопку; +- вручную введённое имя, совпадающее с существующим, допускается: общая схема + пространств сегодня не требует уникальных title; +- Cancel/Esc закрывает только copy flow и возвращает исходный диалог без + изменений. + +Кнопка подтверждения называется **«Создать копию»**. Если Optimize не нужен, +нажатие этой кнопки является единственным подтверждением и сразу запускает +запись — второго confirmation dialog нет. + +### 6.3. Предварительный Optimize + +После подтверждения имени frontend строит `optimizePlans` candidate на +актуальном server config и layout с тем же reference context, что штатный +диалог Optimize. + +1. Если расчёт бросает ошибку миграции, используется существующая понятная + диагностика, копия не создаётся. +2. Если `changed === false`, copy candidate строится из текущего server config. +3. Если `changed === true`, выполняется тот же geometry preflight. +4. При `preflight.ok === false` копия не создаётся; пользователь получает + существующую диагностику с проблемным пространством/геометрией. +5. При `changed === true && preflight.ok === true` показывается отдельное + warning-confirmation. Оно прямо говорит: перед копированием House Plan + оптимизирует **весь план**, изменения затронут и другие пространства, после + чего будет создана копия `<имя>`. +6. Cancel в warning возвращает в диалог имени с сохранённым значением и не + пишет config/layout. +7. Accept выполняет штатную серверную optimize-транзакцию с ожидаемыми config и + layout revision. Копия строится только из принятого сервером оптимизированного + config, никогда из дооптимизационного snapshot. + +Отдельная упрощённая оптимизация, другой epsilon, собственный preflight или +клиентская запись optimize candidate через обычный config/set запрещены. + +### 6.4. Состояния ожидания и ошибки + +С момента нажатия «Создать копию» до окончательного успеха/отказа основное +действие disabled и показывает busy. Повторный click, Enter или повторное +событие confirmation не запускают вторую операцию. + +Ошибка показывает локализованный toast/диагностику и оставляет возможность +повторить операцию. Никакое частично добавленное пространство не остаётся в +локальной модели после отклонённой записи. + +## 7. Контракт данных копии + +### 7.1. Базовый объект + +Новый space получает: + +- новый валидный `space.id`, уникальный среди `config.spaces`; +- выбранный `title` после trim; +- обязательные `rooms: []`, `wall_segments: []` и копию `view_box`; +- позицию сразу после source space в `config.spaces`; +- только перечисленные ниже optional keys, если они присутствовали у источника. + +Неизвестные top-level поля пространства не копируются автоматически. Расширить +allowlist можно только отдельным осознанным изменением, чтобы новый server key с +привязкой к комнате/устройству не начал незаметно клонироваться. + +### 7.2. Контурные стены и самостоятельные перегородки + +В `partitions` копии последовательно входят: + +1. по одной новой partition на каждый `source.wall_segments[]` с теми же `a`, + `b`, `cm` и новым ID; +2. копии всех `source.partitions[]` с теми же `a`, `b`, `cm` и новыми ID. + +Предварительный Optimize отвечает за примирение геометрически совпадающих +контурных стен и самостоятельных partitions. Copy сам не объединяет и не +сдвигает отрезки и не меняет толщину. Порядок источника сохраняется, чтобы +результат был детерминирован и удобен для диагностики. + +Создаются две явные карты ID: + +- `source wall_segment id -> copied partition id`; +- `source partition id -> copied partition id`. + +Все новые ID уникальны внутри нового space не только в своём массиве, но среди +partitions, openings, decor и wall columns. ID исходника не переиспользуются. + +### 7.3. Проёмы + +Каждый `source.openings[]` создаёт ровно один opening в копии: + +- получает новый ID; +- сохраняет `type`, `x`, `y`, `angle`, `length`, `invert`, `flip_h`, `flip_v` и + прочие геометрические/визуальные поля; +- `contact` и `lock` удаляются независимо от их значений; +- wall-host становится partition-host по первой карте ID; +- partition-host остаётся partition-host по второй карте ID; +- `host.t` сохраняется без пересчёта и округления. + +Если хотя бы у одного opening отсутствует host, host имеет неизвестный kind/ID +или не находится в построенной карте, Copy отказывает до записи с понятной +диагностикой. Молчаливо пропускать opening, оставлять старый host либо создавать +в копии инертный unhosted opening нельзя: обещание функции — перенести все +проёмы. + +### 7.4. Колонны и декор + +`wall_columns` и `decor` deep-copy целиком с сохранением порядка и заменой +каждого object ID. Геометрия, стиль, текстовые шаблоны, ссылки на decor assets и +все разрешённые schema extra-поля сохраняются. + +Backdrop/decor asset blobs не дублируются. Оба пространства ссылаются на тот же +asset URL/ID; действующий reference accounting не позволяет удалить файл, пока +на него ссылается хотя бы одно пространство. + +### 7.5. Вид и подложка + +Deep-copy выполняется для: + +- `settings`; +- `zero_wall_style`; +- `cell_cm`; +- `view_box`; +- `plan_url`, `plan_aspect`, `plan_x`, `plan_y`, `plan_scale`, + `plan_scale_x`, `plan_scale_y`, `plan_angle`. + +Optional key сохраняет именно presence semantics: отсутствующий у источника +ключ не материализуется произвольным default. Текущая camera/zoom-позиция из +local navigation/layout не копируется: у нового space ещё нет собственной +локальной истории камеры, а `view_box` задаёт его исходный кадр. + +### 7.6. Что отсутствует + +В новом space нет `room_drafts`, `open_spans` и legacy `walls`. `rooms` и +`wall_segments` присутствуют как обязательные пустые массивы model v9. + +Вне нового space не создаются и не меняются: + +- `markers[]`, включая `space`, `room_id`, controls, vacuum routes и bindings; +- layout-позиции устройств, групп и room labels; +- HA Areas/entities/devices; +- global settings. + +## 8. Лимиты и fail-closed проверки + +До первой записи copy flow проверяет: + +- `config.spaces.length < 50`; +- итоговое число copied partitions не больше 2000; +- openings не больше 500; +- decor не больше 1000; +- wall columns не больше 500; +- каждый обязательный source array и geometry ID пригоден для однозначного + преобразования; +- все opening hosts разрешаются по ID-картам; +- итоговый candidate проходит frontend geometry/preflight invariants. + +Хотя openings/decor/columns источника уже должны укладываться в per-space cap, +явная проверка остаётся защитой от устаревшего/частичного клиента и даёт +человеческую ошибку вместо backend schema exception. + +Если после Optimize candidate всё ещё нарушает junction/geometry safety, Copy +отказывает и использует текущую preflight-диагностику. Новому space нельзя +передавать `baseline_counts` или иным способом наследовать геометрический долг. + +Все проверки fail closed: исключение валидатора, нечисловая координата, +дублирующийся source ID или неоднозначная карта host означает no write. + +## 9. Запись, конкурентность, rollback и Undo + +### 9.1. Без Optimize + +Copy строит отдельный immutable candidate от актуального server config, +добавляет новый space после source и отправляет одну revision-guarded запись +полного config. Live object может быть принят только вместе с успешным ответом +либо обязан откатиться к exact before/reloaded server truth по паттерну #442. + +### 9.2. С Optimize + +Операция намеренно состоит из двух серверных транзакций: + +1. существующий `houseplan/plan/optimize` атомарно принимает config+layout; +2. revision-guarded config write добавляет копию в уже оптимизированный config. + +Если первая транзакция не принята, вторая не начинается. Если Optimize принят, +а запись копии затем отклонена/теряет ответ/конфликтует, frontend перечитывает +server truth: частичной копии локально нет, но принятая оптимизация остаётся. +Автоматически откатывать полезную оптимизацию из-за отказа Copy запрещено. + +Любое внешнее изменение revision между шагами приводит к обычному conflict: +автоматического merge/retry со старым copy candidate нет. Пользователь может +запустить Copy повторно после reload. + +### 9.3. Undo + +Создание пространства не добавляется в space-scoped `_geometryHistory`: эта +история умеет откатывать геометрию существующего space, но не удаление целого +пространства. Поведение совпадает с обычным Create; отдельный Ctrl+Z для Copy в +эту задачу не входит. + +Если выполнялся Optimize, его существующий server-side Undo остаётся доступен +по действующим правилам. Он не должен случайно удалять уже успешно созданную +копию: после следующей config-записи возможность optimize undo обязана следовать +текущему backend revision contract, а не показывать устаревшую кнопку. + +## 10. Переход после успеха + +После принятой записи: + +1. copy/name/space dialogs закрываются; +2. новый space выбирается через канонический `_commitSpace`; +3. открывается режим `plan`; +4. активируется инструмент «Стены»; +5. `_path`, cursor preview, active draft ID, selection, drag/resize и прочее + transient editor state очищаются; +6. новый клик начинает новую цепочку и не продолжает цепочку источника; +7. показывается локализованный toast с именем созданной копии. + +Переход выполняется только после принятой записи. При ошибке пользователь +остаётся на исходном space/copy flow, а выбранное пространство не меняется. + +## 11. Режимы, touch и доступность + +- Точка входа существует только в desktop-first редакторе настроек; View и + kiosk не получают новых действий. +- Диалог имени работает стандартными click, keyboard input, Enter, Escape и + focus правилами `hp-dialog`. +- Warning confirmation использует общую доступную confirmation surface, но + warning-стиль и broom/copy icon, а не destructive красный Delete. +- Фокус при открытии имени установлен в поле, текст удобно выделить/заменить; + при возврате из отменённого warning значение и фокус восстанавливаются. +- Busy имеет текстовое состояние, а не только изменение цвета. +- Onboarding и его touch/best-effort поведение не меняются. + +## 12. Модель данных, миграция и совместимость + +- Новых config keys, model version, backend schema и миграции нет. +- Новый space является обычным валидным model-v9 space и читается предыдущей + версией card/integration как пространство без комнат с partitions. +- Source space не меняется, кроме изменений штатного Optimize, если он был + отдельно подтверждён. +- Shared backdrop/decor references сохраняют действующий storage lifecycle. +- Downgrade не требует cleanup: созданную копию можно удалить штатной командой. +- Функция недоступна без server storage/config так же, как обычное редактирование + пространства. + +## 13. Архитектура и потолки файлов + +Чистая логика должна жить в отдельном модуле, например `src/space-copy.ts`, и не +зависеть от Lit/HA runtime: + +- построение следующего имени; +- предварительная проверка лимитов/source integrity; +- генерация collision-free ID через инъецируемую фабрику; +- построение ID-карт; +- создание immutable copy candidate/result; +- machine-readable причины отказа. + +`src/space-dialog.ts` может хранить тип transient copy dialog и небольшие +presentation helpers. Runtime только оркестрирует диалоги, Optimize, запись и +переход режима. + +Потолки `src/houseplan-editor-runtime.ts` и `src/houseplan-card.ts` на старте +задачи выбраны полностью. Повышать их ради #456 нельзя. Реализация обязана +вынести не меньше существующего связанного кода, чем добавляет в core, либо +расширить lazy boundary без роста разрешённого line budget. Оба runtime должны +использовать один source of truth; onboarding не получает Copy. + +## 14. i18n + +Все новые строки добавляются синхронно в `ru`, `en`, `de`, `fr`: + +- label/title кнопки Copy; +- title и поле имени copy dialog; +- «Создать копию»; +- warning о whole-plan Optimize; +- success toast с именем; +- ошибки limit/source opening/unsafe geometry, если существующий переводимый + diagnostic неприменим. + +В текстах нельзя показывать внутренние слова `wall_segments`, `partitions`, +host, preflight, revision или schema. Для пользователя это стены, проёмы, +пространство и оптимизация всего плана. + +## 15. Производительность и безопасность + +- Построение копии — O(W + P + O + C + D), где это wall segments, partitions, + openings, columns и decor источника; вложенного поиска opening host по массиву + для каждого opening быть не должно. +- Deep-copy ограничен одним source space и перечисленными ключами; весь config + не клонируется на каждый render/keypress. +- Расчёт Optimize запускается только по явному submit, не при открытии диалога и + не при каждом изменении имени. +- Нового render-time пути, listener, polling, interval и фоновой работы нет. +- UI не принимает HTML из title/error и не строит asset path из пользовательской + строки. +- ID ограничены схемой, не содержат title и не коллидируют внутри нового space. +- Revision guard и serialization не позволяют двум быстрым submissions создать + две копии или перезаписать изменение другого клиента. + +## 16. Критерии приёмки и доказательства + +### AC1. Copy доступна только в настройках существующего пространства + +В edit-space footer есть neutral **«Копировать»** вне danger group; в Create, +onboarding, View и kiosk её нет. Busy блокирует повторное нажатие. + +**Доказательство:** source contract test + production-bundle browser smoke для +edit/create/onboarding. + +### AC2. Диалог имени следует принятому контракту + +Поле получает следующий свободный `Название (N)`, допускает вручную введённый +duplicate title, trim-ит результат и не позволяет submit пустого имени. +Cancel/Esc ничего не меняет. + +**Доказательство:** pure unit tests name resolver + browser dialog test; +мутанты «всегда `(2)`» и «пустое имя создаёт space» краснеют. + +### AC3. Без изменений Optimize нет лишнего подтверждения + +При `changed === false` один submit имени выполняет ровно одну config-запись и +не открывает warning confirmation. + +**Доказательство:** runtime orchestration test со spy на confirm/WS; мутант +«всегда спрашивать confirmation» краснеет. + +### AC4. Нужная оптимизация подтверждается и выполняется первой + +При `changed === true && preflight.ok === true` до любой записи показано +сообщение про оптимизацию всего плана. Cancel даёт zero writes. Accept сначала +вызывает штатный `houseplan/plan/optimize`, принимает его revision, затем строит +и сохраняет копию именно оптимизированного source. + +**Доказательство:** integration test порядка событий/candidates + browser smoke; +мутанты «копировать до Optimize», «не упомянуть весь план» и «Cancel пишет» +краснеют. + +### AC5. Небезопасный Optimize блокирует Copy + +Ошибка migration или `preflight.ok === false` приводит к существующей понятной +диагностике и zero optimize/copy writes. `baseline_counts` копии не создаётся. + +**Доказательство:** runtime negative tests на throw и failed preflight + fixture +с junction violation. + +### AC6. Все физические стены перенесены без комнат + +Для source с комнатными wall segments и самостоятельными partitions копия +имеет `rooms: []`, `wall_segments: []` и partitions count +`wall_segments.length + partitions.length`. Координаты и `cm` каждой стены +сохранены, все ID новые и уникальны. + +**Доказательство:** pure fixture test, large-house fixture и actual backend +`CONFIG_SCHEMA`/geometry validators; мутанты «копировать rooms», «пропустить +существующие partitions», «взять default thickness» и «переиспользовать ID» +краснеют. + +### AC7. Все проёмы перевешены, а device bindings очищены + +Wall- и partition-hosted opening представлены один-к-одному; host указывает на +соответствующую новую partition, `t` и вся видимая геометрия сохранены, +`contact`/`lock` отсутствуют. Unknown/unhosted source opening даёт отказ до +записи. + +**Доказательство:** unit mapping table для door/window/gate/passage + actual +backend `validate_partition_opening_hosts` и passage checks; мутанты «старый +host», «потерять opening», «сохранить bindings» и «молча пропустить unknown» +краснеют. + +### AC8. Декор, колонны, вид и подложка совпадают + +Decor и columns deep-copy с новыми ID; перечисленные display/view/backdrop keys +совпадают по значениям и presence. Asset references остаются общими, но +удаление/изменение объекта одной копии не мутирует объект другой. + +**Доказательство:** deep-equality/aliasing unit test со всеми decor kinds и +backdrop transforms + backend round-trip; мутант shallow-copy краснеет. + +### AC9. Комнатные и device-данные не протекают в копию + +В новом space нет room drafts/open spans/legacy walls; config markers, layout +room labels/device/group positions и vacuum routes байт-в-байт равны состоянию +до Copy. + +**Доказательство:** full-config fixture diff с allowlist; мутанты «копировать +draft», «клонировать marker» и «создать layout key» краснеют. + +### AC10. Лимиты дают понятный отказ без частичного состояния + +50 spaces, более 2000 результирующих partitions и каждый защищаемый cap +блокируют операцию до write. Backend rejection, lost response и conflict не +оставляют локальную копию и не меняют выбранный space. + +**Доказательство:** boundary unit tests + rejected optimistic write integration +tests по паттерну #442. + +### AC11. Принятый Optimize переживает отказ Copy + +Если optimize-транзакция успешна, а следующая config-запись отклонена, reload +показывает оптимизированный server config без новой копии. Автоматического +optimize undo нет. + +**Доказательство:** two-step fake server integration test с разными revisions; +мутант «откатить Optimize вместе с Copy» краснеет. + +### AC12. После успеха пользователь находится в чистой копии + +Новый space вставлен после source, выбран только после accepted write, открыт +Plan editor с инструментом «Стены» и пустым transient drawing/selection state. +Первый click начинает новую цепочку. + +**Доказательство:** runtime state-transition test + production-bundle browser +smoke на исходном пространстве с активным draft/selection. + +### AC13. Core budgets, i18n и bundle parity соблюдены + +Четыре языка содержат все новые ключи; lazy/eager runtime loads без ошибки; +потолки core файлов не повышены; собранный bundle содержит тот же copy contract, +что source. + +**Доказательство:** `i18n`, `i18n-dead-keys`, `core-file-budget`, runtime loader, +bundle freshness/tree tests и production-bundle smoke. + +## 17. План автотестов + +1. Новый `test/space-copy.test.mjs`: names, ID maps, all copy fields, deep-copy, + limits, malformed IDs/hosts, deterministic order и negative allowlist. +2. Расширение `test/space-dialog.test.mjs`: edit-only button, neutral group, + dialog field, disabled/busy/cancel semantics и onboarding absence. +3. Runtime orchestration test: unchanged/changed/preflight-fail, confirmation, + WS order, revisions, double-submit, rollback/reload и final transition. +4. Large-house fixture: wall graph + existing partitions + 34 openings; actual + Python schema/semantic validators принимают результат. +5. Mutation tests минимум для всех мутантов AC2–AC12; surviving mutant считается + отсутствующим доказательством, а не advisory. +6. Production-bundle browser smoke: кнопка → имя → no-optimize success; кнопка → + optimize warning → Cancel; warning → Accept → новая вкладка space/Plan. +7. В цикле реализации: `npm run typecheck`, `npm test`, `npm run build`. +8. Перед бетой по общему процессу: golden, smoke и performance. Новый render-time + baseline не нужен, но общий performance gate подтверждает отсутствие + регрессии idle/render. +9. Backend HA harness остаётся каноничным в Linux CI; Windows не используется + как доказательство полного harness из-за `fcntl`. + +## 18. Риски и меры + +| Риск | Мера | +|---|---| +| orphan wall segments при пустых rooms | преобразовывать только в partitions; actual schema test | +| потеря самостоятельных перегородок | отдельная ID-карта и AC6 | +| проём ссылается на старую стену | обязательное разрешение host до записи и backend validator | +| Copy строится из состояния до Optimize | строгий двухшаговый orchestration test | +| Optimize меняет другие этажи неожиданно | отдельное явное whole-plan confirmation | +| конфликт между двумя транзакциями | revision guard, reload, без stale retry | +| локальная призрачная копия после отказа | immutable candidate/rollback по #442 | +| shallow-copy связывает объекты двух spaces | deep-copy и aliasing test | +| рост core сверх потолка | pure module/extraction и обязательный budget gate | +| новый allow-extra key протекает в copy | top-level allowlist и negative fixture | + +## 19. Откат + +Кодовый rollback удаляет кнопку, copy dialog/orchestration, pure copy module, +новые i18n-ключи и тесты. Schema/model migration отсутствует, поэтому уже +созданные пространства остаются обычными валидными spaces и не требуют +преобразования. + +Пользовательский откат конкретной успешно созданной копии — штатное Delete +space с действующим подтверждением и dependency guard. Если перед Copy был +подтверждён Optimize, его доступность Undo определяется существующим backend +revision contract; Copy не создаёт собственного скрытого snapshot. + +## 20. Release-артефакты + +- `CHANGELOG.md` и `CHANGELOG.ru.md`: одна пользовательская запись о Copy без + публикации внутренних ключей/модели; +- `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`: настройки пространства, + состав копии, отсутствие комнат/устройств и предупреждение whole-plan + Optimize; +- при необходимости screenshot/golden диалога имени и optimize warning; +- release-note не обещает копирование комнат или межустановочный перенос. + +## 21. Принятые предположения + +Эти решения технические либо мелкие продуктовые и могут быть свободно изменены +ревьюером без нового вопроса владельцу, если сохраняются AC: + +1. Copy работает над сохранённым source config; несохранённые поля открытого + settings dialog не применяются и не входят в копию. +2. Копия вставляется сразу после source в порядке пространств. +3. Точное duplicate title разрешено, но default всегда предлагается уникальный. +4. Unhosted/unknown opening блокирует весь Copy, поскольку молчаливый пропуск не + выполняет обещание «копируются все проёмы». +5. Текущая локальная camera history не копируется; копируются persistent + `view_box` и display/backdrop settings. +6. Создание копии не использует `_geometryHistory`; rollback rejected write и + server Optimize Undo — два разных контракта. +7. Machine-readable copy errors преобразуются в пользовательские i18n строки в + runtime, а pure module не знает о HA/Lit. diff --git a/docs/specs/README.md b/docs/specs/README.md index 7a8b54a3..4e72f547 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -178,6 +178,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#448](https://github.com/Matysh/houseplan-card/issues/448) Единый бессрочный переключатель `hp_alpha` | [448-alpha-switch.md](448-alpha-switch.md) | | [#449](https://github.com/Matysh/houseplan-card/issues/449) Двойной клик/тап по свободному фону вписывает весь план | [449-double-fit-all.md](449-double-fit-all.md) | | [#451](https://github.com/Matysh/houseplan-card/issues/451) Фильтрация render и лёгкий live-слой взаимодействий | [451-render-performance.md](451-render-performance.md) | +| [#456](https://github.com/Matysh/houseplan-card/issues/456) Копирование пространства без комнат и устройств | [456-copy-space.md](456-copy-space.md) | ## P3