Files
houseplan-card/docs/specs/282-stable-wall-segment-identity.md
T
2026-08-26 02:31:31 +03:00

663 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue #282 — стабильная идентичность сегментов стен (ADR Stage 1)
- **Issue:** https://github.com/Matysh/houseplan-card/issues/282
- **ADR:** [`docs/adr/282-wall-geometry-representation.md`](../adr/282-wall-geometry-representation.md)
- **Статус:** принято независимым ревью; Stage 1 реализован и проверен
- **Тип / приоритет:** tech-debt / P1
- **Поставляемый этап:** Stage 1 — stored identity
- **Целевая версия модели:** `PLAN_MODEL_VERSION = 8`
- **Пользовательское изменение:** да, но без нового интерфейса
## 1. Сценарий
**Персона:** администратор дома из `docs/SCOPE.md`, который уже построил план и
поддерживает его в Plan editor на desktop. **Момент:** Resize, Split, Merge,
изменение толщины, размещение проёма, импорт либо Optimize меняют геометрию
существующего плана. View, kiosk и touch-потребители затем должны увидеть тот же
дом без пропавшей толщины, переехавшего проёма или нового шва.
Задача обслуживает J6: «Keep the plan true as the home evolves». Это не новая
возможность рисования, а устранение причины, по которой уже сохранённая стена
теряет смысл после дальнейшего редактирования.
## 2. Что человек увидит до и после
**До:** после сложного изменения внешне та же стена иногда теряет толщину,
получает толщину соседнего участка либо отделяется от своего проёма. **После:**
та же операция сохраняет принадлежность толщины и проёмов стене; внешний вид и
состав кнопок не меняются.
Единственное новое видимое состояние — честный отказ от структурной записи,
если старый план невозможно преобразовать без потери данных. View продолжает
работать по compatibility reader; диалог/тост называет причину, предлагает
сначала запустить «Оптимизировать планы», а при повторном отказе — исправить
конфликтующую геометрию стен.
## 3. Проблема и подтверждённая причина
Сегодня contour wall одновременно является:
1. ребром `rooms[].poly`;
2. разреженной записью толщины `space.walls[]`, найденной по вычисленному
`wallKey` (квантованные midpoint + angle);
3. пространственно найденным носителем room opening без сохранённой ссылки.
Геометрическая операция обязана заново вывести все три связи. Пропущенный или
слегка отличающийся re-key теряет толщину (#253/#258); частичное совпадение
создаёт неоднозначную роль (#299); Split/Resize вынуждены материализовать и
переименовывать атомы до изменения контура. Канонизация координат #291 убрала
битовый noise, но не устранила вывод идентичности из изменяемой геометрии.
Independent `partitions[]` уже имеют стабильный `id`, а hosted openings уже
ссылаются на него. Stage 1 переносит этот доказанный принцип на contour walls и
на незавершённые draft segments. Renderer и вычисление junction geometry пока
не переписываются: они получают compatibility projection из новой модели.
## 4. Скоуп
В задачу входят:
- стабильный persisted `id` каждого атомарного contour segment;
- ссылки комнат на эти ID в порядке обхода контура;
- толщина на самом segment record, включая отсутствие физического тела;
- стабильные ID draft segments; существующие partition IDs сохраняются;
- wall-host для door/window/gate/passage на contour segment;
- детерминированная v7 → v8 миграция и runtime-проекция без записи на read;
- единый identity writer/barrier для Resize, Split, Merge, Delete, draw/close,
thickness, opening placement, Optimize, Undo/recovery и import/export;
- compatibility projection `rooms[].poly`, `walls[]`, legacy unhosted openings;
- backend validation, limits, import remap и fail-closed stale-client policy;
- unit/backend/invariant/mutation/smoke/golden/performance evidence;
- обновление канонической документации и обоих changelog.
## 5. Не-скоуп
- integer node indices и новый `STORAGE_VERSION` (ADR Stage 2);
- отказ от `rooms[].poly` как compatibility projection (Stage 3);
- единый persisted planar graph для room walls и partitions (Stage 3);
- closed-form junction renderer и удаление polygon booleans (Stage 4);
- новый UI, изменение жестов, новые инструменты или иная геометрия результата;
- исправление видимого дефекта конкретного junction, не вызванного identity;
- индивидуальная настройка ID либо их показ пользователю;
- автоматическое притягивание authored off-grid geometry;
- удаление legacy reader в этом релизе.
Stages 2–4 требуют отдельных issues и поставляются независимо. Закрытие #282
после Stage 1 означает завершение текущего исполнимого этапа ADR, а не отмену
оставшегося направления.
## 6. Целевая persisted-модель
### 6.1 Contour segments
В пространство добавляется полный каталог атомарных contour walls:
```ts
interface StoredWallSegment {
id: string; // stable, unique inside one space
a: [number, number];
b: [number, number];
cm: number; // 0..100; 0 = axis without a physical body
}
interface RoomCfg {
poly: number[][]; // required compatibility projection in v8
wall_ids: string[]; // one id for every consecutive poly edge
}
interface SpaceCfg {
wall_segments: StoredWallSegment[];
walls?: LegacyWallEntry[]; // generated compatibility projection
}
```
`wall_segments[]` содержит **каждый** атом contour, а не только участок с
положительной толщиной. В v8 `wall_segments[].id/a/b/cm` и
`rooms[].wall_ids[]` являются authoritative для identity и толщины.
`rooms[].poly` остаётся authoritative для формы комнаты до ADR Stage 3, но в
v8 его стороны и `wall_ids` обязаны образовывать exact согласованную пару:
- `wall_ids.length === poly.length`;
- segment с соответствующим ID совпадает с ребром `poly[i] → poly[i+1]` в
прямом либо обратном направлении;
- один ID может ссылаться из одной комнаты (`outer`) или двух комнат (`shared`);
- ссылка из трёх и более комнат, orphan segment либо дублированный ID невалидны;
- shared segment обязан иметь одни exact endpoints и один `cm` для владельцев.
Если общий участок занимает только часть длинной стороны комнаты, миграция
добавляет коллинеарные vertices и делит сторону на атомы. Площадь, winding,
silhouette и видимый SVG при этом не меняются.
### 6.2 Толщина 0
`cm: 0` в catalog означает действующее сегодняшнее состояние «контур есть,
физического wall body нет». Это техническое представление полной модели, а не
пользовательская возможность issue #306:
- Plan UI в #282 по-прежнему предлагает прежний диапазон и инструменты;
- legacy sparse interval без `walls[]` мигрирует в segment `cm: 0`;
- virtual/open boundary продолжает определяться `open_spans/open_to`, а не
одним `cm: 0`;
- body/area/opening/light semantics остаются byte/geometry-equivalent v7.
Разрешение пользователю рисовать zero-thickness walls и отказ от virtual walls
остаются в приостановленном #306 и не входят скрытым изменением сюда.
### 6.3 Independent walls и drafts
`partitions[].id` уже является стабильной identity и не мигрирует в новый
catalog до Stage 3. Его endpoints/cm остаются на partition record.
Каждая `room_drafts[].segments[]` получает `id`:
```ts
segments: Array<{ id: string; cm: number }>
```
ID сохраняется при продолжении/перемещении draft. При превращении draft в
partition либо contour соответствующий segment наследует ID, если его carrier
не был разделён. При split действуют lineage-правила §8.
### 6.4 Openings
Host становится tagged union:
```ts
type OpeningHost =
| { kind: 'partition'; id: string; t: number }
| { kind: 'wall'; id: string; t: number };
```
Новые room openings всегда сохраняют wall host. `t` направлен от stored `a` к
`b`; визуальный flip/створка от направления segment не зависят. Legacy opening
без host остаётся читаемым и получает runtime host spatial resolver. На первой
структурной записи host материализуется только при единственном доказанном
carrier; неоднозначность блокирует миграцию, а не выбирает ближайший наугад.
### 6.5 Compatibility projection `walls[]`
`walls[]` временно остаётся persisted projection для старых клиентов и
неизменённого renderer:
- генерируется централизованно из `wall_segments` (положительные `cm`, exact
`a/b`, вычисленный compatibility `key`);
- не является source of truth в v8;
- v8 backend проверяет его semantic parity с catalog, но key drift сам по себе
не делает catalog невалидным;
- новый frontend никогда не присваивает `walls[]` напрямую;
- runtime geometry получает прежний `WallEntry[]` через один adapter;
- в v7 отсутствие catalog сохраняет нынешний reader без записи.
Re-key остаётся только деталью compatibility serializer. Ни Resize, ни Split,
ни business logic не используют key как identity после v8 migration.
## 7. Атомизация и deterministic migration v7 → v8
### 7.1 Breakpoints
До вычисления breakpoint в том же local candidate и до любых v8 ID/ref
запускается существующий write-barrier #291 `canonicalizeConfigGeometry`.
Порог и правило принадлежат только ему: текущий `LATTICE_NOISE_STEPS = 1e-4`
шага сетки. Все слова `exact` ниже означают побитовое равенство **уже
канонизированных** координат; дополнительный wall-thickness epsilon и третий
допуск Stage 1 не вводит. Если канонизация или последующая атомизация
отклоняет candidate, исходный v7 document остаётся byte-equivalent.
Каждая room boundary разбивается в точках:
- vertices всех room polygons;
- начала/концы коллинеарного overlap другой комнаты;
- exact `walls[].a/b` и доказанные границы key-only legacy record;
- `open_spans` и доказанные shared `open_to` spans;
- пересечения с structural nodes, уже используемые текущим wall profile.
Opening edges не делят segment сами по себе: opening хранит `t` и ширину.
Нулевая длина, self-overlap, конфликтующие positive thickness records либо
неоднозначный key-only carrier блокируют миграцию всего пространства.
### 7.2 Толщина
Для каждого атома выбирается ровно действующий v7 resolver:
1. exact matching `walls[].a/b`;
2. exact covering record;
3. legacy key/midpoint fallback;
4. отсутствие записи → `cm: 0`.
Разные positive candidates на одном атоме — blocker, не `min/max/last wins`.
Open boundary не удаляет segment и не меняет `cm`; light/body projection
применяет open cut отдельно, как до миграции.
### 7.3 ID migration
Первоначальный ID детерминирован для повторяемой миграции одного v7 document:
```text
wall-<base32(sha256(space-id | canonical-a | canonical-b | sorted-owner-ids))[0:20]>
```
Endpoint order не влияет на hash. Collision после усечения разрешается
детерминированным `-2`, `-3` после сортировки полной digest; collision полного
digest либо duplicate resulting ID блокирует миграцию.
Это **только seed миграции**. После сохранения ID никогда не вычисляется заново
из geometry. Новые IDs создаёт один monotonic/random id factory с проверкой
уникальности в space; импорт использует общий bounded remapper.
### 7.4 Момент миграции
Read-only загрузка v7 не пишет Store и не повышает model version. Runtime строит
immutable projected catalog для View и editor selection.
Атомарная v8 materialization выполняется:
- перед первой успешной structural config mutation пространства;
- явным Optimize plans;
- при добавлении v7 space в уже v8 config;
- при full/space import candidate, если target требует v8.
Изменение только настроек устройства/пространства, layout marker position или
View state не мигрирует wall model. Успешная materialization записывает catalog,
room refs, hosted openings, compatibility projection и `model_version: 8` одной
config transaction/Undo command. Ошибка оставляет candidate byte-equivalent.
## 8. Identity lineage при операциях
ID сохраняется, когда физический/топологический carrier остаётся тем же объектом,
даже если изменились endpoints, angle, length, owner или compatibility key.
### 8.1 Move и Resize
- whole segment, перенесённый/растянутый одной доказанной операцией, сохраняет ID;
- shared segment, одинаково преобразованный обоими owners, остаётся одним ID;
- несовместимые transforms owners отклоняются preflight;
- изменение только endpoints никогда не создаёт новый ID.
### 8.2 Split одного segment
При появлении breakpoint исходный ID получает ровно один child:
1. child, содержащий старый midpoint внутри;
2. при midpoint на новой границе — child, содержащий canonical old `a`;
3. остальные children получают новые IDs в геометрическом порядке от old `a`.
Openings rehost по их physical centre/interval и получают пересчитанный `t`.
Opening, пересекающий новую границу между children с разными условиями,
блокирует операцию до записи.
### 8.3 Merge segments
Слияние разрешено только для коллинеарных соседей с одинаковыми `cm`, owner set,
open-boundary role и без semantic breakpoint. Survivor ID выбирается:
1. segment с большим количеством существующих opening hosts;
2. затем самый длинный;
3. затем лексикографически меньший ID.
Все refs/hosts переводятся на survivor с пересчитанным `t`. Остальные IDs
исчезают; tombstone не хранится, поскольку Undo содержит полный snapshot.
### 8.4 Split/Merge/Delete rooms
- смена outer ↔ shared сама по себе ID не меняет;
- segment, остающийся на границе хотя бы одной surviving room, сохраняется;
- полностью удалённый carrier удаляется только в уже подтверждённом user flow;
- Keep walls переводит contour segment в partition с тем же ID, если ID не
конфликтует с существующим partition;
- обратное включение unambiguous partition в contour сохраняет ID.
### 8.5 Draft close/cancel
Сохранённый draft segment сохраняет ID между сессиями. Незавершённый in-memory
rubber band ID не получает. Cancel не расходует persisted ID. Закрытие room
переносит IDs; atomization применяет §8.2.
### 8.6 Undo/Redo и concurrency
Undo/Redo восстанавливает exact snapshots с теми же IDs. Config revision guard
проверяется до lineage calculation и ещё раз при commit; stale revision не
может частично сохранить catalog. Concurrent edit получает обычный conflict и
перезагружает authoritative snapshot.
## 9. Writers и единый identity barrier
Новый pure-модуль (рабочее имя `src/wall-segment-model.ts`) владеет:
- v7 projection/migration;
- validation/parity;
- compatibility `walls[]` projection;
- ID lineage split/merge/rehost;
- persisted/runtime fingerprints.
Каждая structural mutation работает над local candidate, сначала пропускает
его через `canonicalizeConfigGeometry` #291, затем через
`commitWallSegmentModel(candidate)` и только после этого достигает
`_commitPhysicalGeometry`/backend write. Канонизация и identity migration входят
в одну атомарную commit-транзакцию; результат первой является единственным
координатным input второй.
Source guard перечисляет все structural writer entrances и падает, если новый
путь записывает `rooms.poly`, `walls`, `wall_segments`, `partitions`, drafts или
architectural openings мимо barrier.
Запрещено исправлять задачу добавлением ID assignments в десятки обработчиков.
Локальные контроллеры могут передать lineage hint, но один barrier доказывает и
пересобирает согласованную модель.
Backend mirror проверяет:
- schema bounds/unique IDs/references;
- geometry/ref/parity invariants;
- v8 catalog ↔ compatibility projection;
- wall/partition opening host and `t` fit;
- отсутствие direct Store writer bypass.
Backend не придумывает lineage для валидного v8 candidate; он либо принимает
его целиком, либо отклоняет.
## 10. Старые клиенты и совместимость
### 10.1 v7 data на новом клиенте
View/read работает через runtime projection. Первая structural write предлагает
тот же operation вместе с migration; успех атомарный. Export до migration
сохраняет фактический v7 document, не штампует v8.
### 10.2 v8 data на старом клиенте
Старый клиент может сохранить неизвестные поля, но не способен согласованно
обновить refs/catalog. Backend обнаруживает изменение structural projection без
соответствующего v8 catalog и отклоняет весь config write кодом
`wall_model_client_outdated`. UI нового клиента локализует его как требование
обновить/перезагрузить карточку. View/read старого клиента может использовать
compatibility `poly/walls`; безопасное редактирование старым клиентом не
обещается.
### 10.3 Import/export/duplicate
- full replace v8 сохраняет IDs после полной validation;
- space import и duplicate remap every wall/draft/partition ID and all
`wall_ids`/opening hosts одним lineage table;
- collision с target никогда не resolve-ится геометрическим совпадением;
- v7 import в v7 target сохраняется v7; в v8 target материализуется до merge;
- plan-only export/import включает catalog/refs/hosts и не оставляет dangling IDs;
- future unknown fields сохраняются на record/room/space как сейчас;
- future model version отклоняется до migration.
### 10.4 Configuration lifecycle
`wall_segments`, `rooms[].wall_ids`, `room_drafts[].segments[].id` и wall host
регистрируются в `scripts/config-field-registry.mjs` как current v8. `walls.key`
и unhosted room openings получают documented compatibility statuses, но reader
удаляется только отдельной задачей после окна поддержки.
## 11. UX, accessibility, touch и security
- кнопки, поля, значения толщины и pointer gestures не меняются;
- View/kiosk output и actions не меняются;
- hover/hit-test/selection используют ID internally, но показывают прежние ink;
- editor остаётся desktop-first; touch View acceptance блокирующий;
- screen reader/keyboard contract не расширяется;
- ID никогда не показываются в обычном UI и не попадают в toast/telemetry;
- новые HA service calls, URL/HTML или permission boundaries отсутствуют;
- failure UI не раскрывает geometry/config data, только локализованный класс
blocker и действие пользователя.
## 12. i18n
Новые RU/EN ключи (точное имя можно привести к namespace проекта):
| Key | RU | EN |
|---|---|---|
| `toast.wall_model_migration_blocked` | `Не удалось обновить модель стен: {reason}. План не изменён. Запустите «Оптимизировать планы»; если ошибка повторится, исправьте конфликтующую геометрию стен.` | `The wall model could not be updated: {reason}. The plan was not changed. Run “Optimize plans”; if the error repeats, fix the conflicting wall geometry.` |
| `toast.wall_model_client_outdated` | `Обновите карточку и перезагрузите страницу перед редактированием плана.` | `Update the card and reload the page before editing the plan.` |
| `gs.wall_segments_migrated` | `Стабилизировано сегментов стен: {n}.` | `Wall segments stabilised: {n}.` |
Reason names проходят через bounded enum и отдельные localized labels; raw IDs,
exception text и config values не интерполируются.
## 13. Лимиты и производительность
- catalog ограничен суммой допустимых room polygon sides после atomization;
hard backend cap — `MAX_ROOMS × MAX_POLY_POINTS` (текущие 200 000), а общий
import/export byte cap остаётся более строгим практическим ограничением;
- duplicate IDs, refs и parity проверяются O(V + E) через maps;
- spatial atomization migration допускает O(E log E), но не O(E²) на обычном
payload; shared-axis grouping обязано иметь spatial/axis index;
- read/render tick не мигрирует и не хэширует полный config повторно;
- runtime projection и catalog fingerprint кэшируются по config revision;
- large-house structural render p95 и editor commit p95 не регрессируют более
чем на 20% same-run baseline;
- migration 10 000 contour atoms укладывается в 500 ms p95 в Node benchmark и
не создаёт второго полного deep clone сверх transaction snapshot;
- новые limits не отклоняют ни один валидный v7 fixture до попытки migration.
## 14. Критерии приёмки
### AC1. Детерминированная lossless migration
v7 fixtures с outer/shared/partial-overlap/T/X/diagonal/open span/key-only,
exact walls и lattice-noise по обе стороны порога #291 дважды дают
byte-equivalent v8 candidate. Шум внутри порога сначала канонизируется и не
создаёт micro-segments; координата вне порога не схлопывается молча. Polygon
area, winding, wall-body path, clean floor, opening projection, Glow/sun
barriers и isometric geometry до/после равны по существующим строгим/golden
контрактам.
**Доказательство:** TS migration unit matrix + golden/static path comparison +
backend fixture parity.
### AC2. Полный и валидный catalog
Для каждого v8 space все contour atoms имеют unique stable ID; каждая poly side
имеет ровно один wall ref; owner count 1/2; orphan/dangling/duplicate/third-owner,
zero-length и conflicting thickness отклоняются. Positive legacy thickness и
thin contour сохраняют прежний вид.
**Доказательство:** frontend/backend shared valid+invalid JSON fixture.
### AC3. Identity не зависит от geometry после migration
Move/Resize/angle/length/key change whole segment сохраняют ID. Мутант,
перевычисляющий ID из endpoints после каждого commit, убивается тестом.
**Доказательство:** pure lineage tests + Resize browser smoke.
### AC4. Split/merge lineage
Split выбирает survivor по midpoint/old-a; merge — по hosts/length/id. Room refs
и hosted opening `t` обновляются атомарно. Повторение операции после Undo/Redo
возвращает exact те же IDs.
**Доказательство:** parameterized unit tests для directions/ties/openings +
Split/Merge smoke.
### AC5. Все structural operations проходят barrier
Draw/close, wall thickness, opening placement/move/delete, Resize, Split, Merge,
room Delete/Keep walls, partition conversion, Optimize, Undo/Redo/recovery и
import вызывают один identity barrier. Direct assignments не могут попасть в
persisted write.
**Доказательство:** executable source/AST guard + bypass mutant для каждого
writer family.
### AC6. Room openings hosted
Новый room opening всегда имеет `{kind:'wall', id, t}`. Legacy unique carrier
материализуется; ambiguous/missing/intersecting split блокирует transaction.
Door/window/gate/passage, flip, contact/lock и Glow tunnel не меняются.
**Доказательство:** opening unit matrix + `smoke_opening_measure`/targeted host
smoke + backend schema tests.
### AC7. Compatibility projection
v8 → legacy `poly/walls/unhosted visual input` projection даёт текущим full,
static, hidden-isometric и light consumers прежние inputs. Runtime code не ищет
business identity через `wallKey`; key генерирует только adapter/compat layer.
**Доказательство:** source guard + projection snapshots + existing geometry
suite.
### AC8. Read-only и атомарный failure
Открытие v7 config, View, kiosk, navigation и device/layout-only save не пишут
catalog/model version. Structural migration blocker оставляет config/layout/rev
byte-equivalent и сохраняет работающий View.
**Доказательство:** store call-count tests + failure fixture + browser smoke.
### AC9. Старый клиент fail-closed
Попытка изменить v8 `poly/walls/openings` без согласованного catalog отклоняется
bounded error; ни один partial Store write не происходит. Byte-equivalent
round-trip старого клиента принимается.
**Доказательство:** backend old-client write tests.
### AC10. Import/export и ID remap
Full/space/plan-only export-import, duplicate and backup restore сохраняют либо
полностью remap-ят ID/ref/host graph. Collision, dangling ref и future model
отклоняются до apply; preview counts согласованы с candidate.
**Доказательство:** HA import/export harness + shared lineage fixture.
### AC11. Limits и unknown fields
Boundary cases catalog/draft IDs/refs проходят или fail atomically. Unknown
sibling fields на config/space/room/segment/opening survive migration and
round-trip. Atomization не truncates data ради лимита.
**Доказательство:** backend validation/property tests.
### AC12. Geometry regression floor
Все тесты #197/#224/#249/#253/#258/#261/#271–#280/#288–#302 остаются зелёными.
Точный ID не является поводом удалить существующую union isolation либо
ослабить geometry preflight.
**Доказательство:** `npm test`, model invariants, named wall smokes/goldens.
### AC13. View/touch parity
Desktop View, kiosk и phone/touch fixture визуально равны pre-migration baseline;
room/device/opening taps и pinch не получают editor behaviour.
**Доказательство:** targeted Playwright view/touch smoke + golden comparison.
### AC14. Performance
Выполнены budgets §13; migration не запускается на render/live HA tick.
**Доказательство:** same-run benchmark с machine-readable result.
### AC15. Optimize UX
Optimize preview отдельно сообщает количество materialized segment IDs. Cancel
— zero writes; Confirm — одна config transaction и одна Undo; второй run no-op.
**Доказательство:** optimizer unit test + targeted dialog smoke.
### AC16. Документация и compatibility registry
Обновлены `WALL-THICKNESS.md`, `ARCHITECTURE.md`, `CONFIG-COMPATIBILITY.md`,
`CANVAS.md`, user guides RU/EN, `TESTING.md`, ADR status и config-field registry.
Термины ID/catalog не попадают в пользовательскую инструкцию кроме объяснения
автоматического обновления старого плана.
**Доказательство:** docs/config audit gates.
### AC17. Gates
Перед code review зелёные:
- `npm run typecheck`;
- `npm test`;
- `npm run build` + `npm run bundle:sync` и bundle parity;
- native Windows pure-backend subset;
- named identity/Resize/Split/Merge/opening/import smokes из AC;
- model invariants, mutation gate, config audit и migration benchmark.
Полный Linux backend, all-smoke, golden и performance_smoke остаются точным-SHA
предрелизным gate по процессу.
## 15. План реализации
1. Ввести pure catalog types, deterministic migration и shared fixtures без
подключения product writers.
2. Добавить backend schema/parity и model version, оставить read-only v7 path.
3. Подключить runtime compatibility projection; доказать render equivalence.
4. Перевести room wall selection/thickness/opening host на ID.
5. Подключить writer barrier и lineage hints по операциям, затем source guard.
6. Подключить import/export/duplicate/Undo/recovery и stale-client rejection.
7. Добавить Optimize preview/report, i18n, docs и changelog.
8. Прогнать fast gates, named smokes, build/sync; передать независимому reviewer.
Каждый промежуточный commit компилируется и не включает half-migrated writer в
default path. Feature flag в persisted config не добавляется: v8 включается
только фактом успешной materialization.
## 16. Риски и меры
| Риск | Мера |
|---|---|
| Catalog и poly расходятся | один commit barrier + backend parity + stale-client reject |
| Shared partial edge получает два ID | global atomization по axis/owners + shared fixture AC1/2 |
| Resize пересоздаёт ID | explicit lineage hints + AC3 mutant |
| Split теряет opening | deterministic child rule и physical rehost AC4/6 |
| Старый клиент портит v8 | compatibility projection для read, fail-closed structural write AC9 |
| Migration выбирает неверный key-only wall | текущий resolver без новых эвристик; ambiguity blocks |
| 200k theoretical edges дают freeze | indexed O(E log E), async preview boundary, benchmark/limits |
| Новый writer обходит model | executable source/AST guard AC5 |
| Новая identity используется для удаления старых geometry guards | explicit AC12 prohibition |
| #306 конфликтует с моделью `cm:0` | #282 не меняет UI/open-boundary semantics; #306 later rebases on catalog |
## 17. Откат
До первой v8 записи — чистый revert кода, данные v7 не менялись. После v8
materialization старый stable release может показать compatibility `poly/walls`,
но безопасное редактирование им не гарантируется.
Штатный rollback продукта:
1. отключить structural v8 writes, сохранив v8 read/projection;
2. выпустить follow-up reader hotfix;
3. не удалять catalog и IDs автоматически;
4. восстановить pre-migration config только из обычного backup/Undo,
инициированного пользователем.
Автоматический downgrade v8 → v7 запрещён: он теряет hosted wall identity и
может неоднозначно схлопнуть атомы. Revert implementation-коммита без v8 reader
после публикации также запрещён.
## 18. Release-артефакты
- implementation commit: `Issue: #282`, `User-Visible: yes`;
- оба changelog со ссылкой на #282 и формулировкой про устойчивое редактирование,
без обещания нового UI;
- generated bundle snapshots синхронизированы;
- новые golden baselines принимаются только при реальной visual delta и только
из полного Linux artifact по процессу;
- beta/RC обязательна до stable;
- issue закрывается только после успешного beta gate на exact SHA.
## 19. Принятые технические предположения (оспоримы ревьюером)
1. Stage 1 поставляется в #282; Stages 2–4 получают отдельные issues.
2. `wall_segments + room.wall_ids` — canonical identity/thickness, а `poly` и
generated `walls[]` временно остаются compatibility projection до Stage 3.
3. Independent partitions не переносятся в catalog: их ID уже stable. Draft
segment IDs добавляются, потому что draft переживает сессию и конвертируется.
4. `cm:0` нужен для полного catalog, но не открывает пользователю функционал
#306 и не заменяет `open_spans/open_to` этой задачей.
5. Migration выполняется lazy on structural write/Optimize, не на read и не на
device/layout-only save; внутри одной транзакции она всегда получает
candidate после `canonicalizeConfigGeometry` #291 с его порогом `1e-4` шага.
6. Старый client может читать projection, но structural writes в v8 fail-closed.
7. Deterministic hash используется только для первоначальной migration; после
неё geometry никогда не определяет ID.
8. Existing wall renderer и junction algorithms получают adapter projection и
не переписываются до ADR Stage 4.