docs: specify stable wall segment identity

Issue: #282
User-Visible: no
This commit is contained in:
Matysh
2026-08-26 02:20:57 +03:00
parent 5f0a7f5065
commit 7a6331d359
@@ -0,0 +1,645 @@
# 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)
- **Статус:** первая редакция для независимого ревью
- **Тип / приоритет:** 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; диалог/тост называет причину и предлагает
Optimize либо исправление конфликтной геометрии.
## 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
Каждая 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 и проходит
`commitWallSegmentModel(candidate)` до `_commitPhysicalGeometry`/backend write.
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.` |
| `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 дважды дают byte-equivalent v8 candidate. 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.
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.