mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
462 lines
31 KiB
Markdown
462 lines
31 KiB
Markdown
# Issue #264 — отдельный контроллер Resize и commit-инвариант кладки
|
||
|
||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/264
|
||
- **Статус:** первая редакция для независимого ревью; канонический статус задаётся
|
||
метками issue
|
||
- **Тип / приоритет:** tech-debt / P1
|
||
- **Оценка:** пользовательская ценность 2/10; ценность для разработки 9/10;
|
||
сложность 7/10; риск 8/10
|
||
- **Класс изменения:** A + B + C, полный процесс
|
||
- **User-Visible:** no — DOM, пиксели, тексты, жесты и persisted config не меняются
|
||
- **Связано:** #34, #253, #254, #277; `docs/RESIZE.md`,
|
||
`docs/specs/034-frontend-decomposition.md`
|
||
|
||
## 1. Сценарий, персона и проблема
|
||
|
||
Домашний администратор на desktop открывает редактор плана существующего этажа,
|
||
выбирает Resize и перетаскивает обычную либо общую стену. Для него этот слайс
|
||
обязан быть полностью незаметен: допустимость ручки, движение, измерения,
|
||
сохранение, отмена и повтор остаются теми же, что в выпущенном контракте #277.
|
||
Задача нужна не ради нового пользовательского шага, а чтобы следующие изменения
|
||
этого критичного пути можно было делать и проверять изолированно.
|
||
|
||
После #277 Resize имеет безопасный fixed-topology контракт, но его конечный
|
||
автомат по-прежнему встроен в `HouseplanCard`: там одновременно живут pointer
|
||
session, immutable snapshot, eligibility cache, live preview, измерения,
|
||
fail-closed commit, Undo и восстановление wall-union cache. Любая следующая
|
||
правка этого инструмента снова меняет крупнейший компонент проекта и должна
|
||
доказывать корректность переходов по разрозненным полям `_rsz*`.
|
||
|
||
#264 — refactor-only слайс канонической декомпозиции #34. Он выносит состояние
|
||
и переходы одного инструмента в отдельный typed controller. Геометрические
|
||
формулы остаются в `src/resize.ts`, а Lit/DOM, перевод, toast, config/history,
|
||
серверная запись и рендер остаются у корневого компонента.
|
||
|
||
Одновременно transaction boundary получает production-инвариант сохранения
|
||
wall records. Именно нарушение этого свойства вызвало #253. Сейчас похожая
|
||
проверка существует как отдельная реализация в CLI-инвариантах и как inline
|
||
сравнение внутри Resize; после #264 алгоритм находится в одном production-
|
||
модуле и используется обоими путями.
|
||
|
||
### Что человек увидит
|
||
|
||
Ничего нового. До и после изменения:
|
||
|
||
- те же handles, disabled reasons, preview, подписи длины/площади и cursor;
|
||
- те же границы safe drag, snap и clamp;
|
||
- те же commit, Cancel/Escape, pointercancel/lostpointercapture и Undo/Redo;
|
||
- те же пиксели View, Plan и остальных редакторов;
|
||
- тот же JSON успешного Resize byte-for-byte.
|
||
|
||
Если внутренний инвариант нарушен, сохраняется уже принятое в #277 поведение:
|
||
preview останавливается либо commit отменяется с существующим безопасным
|
||
сообщением; config write и Undo-команда не создаются.
|
||
|
||
## 2. Зафиксированные решения и исходный контракт
|
||
|
||
1. #277 уже реализован и выпущен. Его ТЗ и `docs/RESIZE.md` являются
|
||
функциональным каноном; #264 не пересматривает eligibility, geometry,
|
||
disabled reasons, preview или commit semantics.
|
||
2. Старый продуктовый вопрос из тела #264 закрыт решением #277: при любом
|
||
нарушении инварианта жест **fail closed** — без частичного применения,
|
||
диагностики с пользовательскими данными, write или Undo.
|
||
3. Владелец явно выбрал #264 следующим слайсом, поэтому прежнее сомнение
|
||
«сначала небольшой диалог или Resize» закрыто.
|
||
4. Schema/model version, compatibility fields и миграции не меняются.
|
||
5. `allowClear` для Resize всегда `false`. Очистка толщины относится к другому
|
||
явному инструменту и не может быть побочным эффектом перемещения стены.
|
||
6. Opened/disabled handles и мышь/клавиатура/touch сохраняют поведение #277.
|
||
Осознанная desktop-first деградация редакторов из `docs/TOUCH-SUPPORT.md`
|
||
не расширяется.
|
||
7. `_rszSel` не имеет renderer-потребителя после #277, но не является полностью
|
||
мёртвым: клик по площади комнаты запоминает latent selection, после чего
|
||
первый Escape очищает её и оставляет Resize активным, а второй возвращает
|
||
neutral Draw. Это наблюдаемое keyboard-поведение сохраняется. Само поле
|
||
уезжает из root в controller как `selectedRoomId`; warm viewport продолжает
|
||
переносить это session-only значение через typed getter/restore, не меняя
|
||
сериализуемый config.
|
||
|
||
Открытых продуктовых вопросов нет.
|
||
|
||
## 3. Термины
|
||
|
||
- **Root / host** — `HouseplanCard`, composition shell, владелец браузерных и
|
||
инфраструктурных side effects.
|
||
- **Controller** — новый `ResizeController`, единственный владелец состояния
|
||
жеста и допустимых переходов.
|
||
- **Adapter / port** — typed callback, которым root предоставляет controller
|
||
текущие данные или выполняет host-specific операцию без обратной записи в
|
||
controller internals.
|
||
- **Geometry snapshot** — immutable JSON-снимок `SpaceGeometryState` перед
|
||
pointerdown плюс identity/fingerprint, с которым сверяется pointerup.
|
||
- **Preview candidate** — полный exact space candidate, построенный только из
|
||
pre-drag snapshot; он никогда не является частью `_serverCfg`.
|
||
- **Commit outcome** — typed значение controller, разрешающее root применить
|
||
ровно этот preview candidate одной geometry-командой.
|
||
- **Wall record profile** — мультимножество конечных числовых `cm` у записей
|
||
`walls`; в exact-режиме включает `0`, потому что после #306 это значимое
|
||
состояние стены.
|
||
|
||
## 4. Архитектурная граница
|
||
|
||
### 4.1 Controller владеет
|
||
|
||
Новый `src/resize-controller.ts` владеет только Resize orchestration:
|
||
|
||
1. состояниями `idle` и `dragging`; наличие принятого preview — часть
|
||
`dragging`, не параллельный флаг; idle дополнительно хранит необязательный
|
||
`selectedRoomId`, который сохраняет существующую двухшаговую Escape-ветку;
|
||
2. pointer id, start point, `SafeResizePlan`, immutable rooms/openings/options,
|
||
snapshot identity, current accepted delta, moved flag, changed room ids и
|
||
однократным preview-rejection notification flag;
|
||
3. eligibility cache, ограниченным одним space/snapshot/grid context;
|
||
4. ссылкой на exact accepted preview, live measurement model и retained
|
||
pre-drag wall-union token как opaque generic value;
|
||
5. переходами `select/clearSelection`, `resolve → begin → move* → finish` и
|
||
`begin/move → cancel`; wrong pointer id даёт no-op;
|
||
6. вызовом существующих pure `safeResizePointerDisplacement`, snap adapter,
|
||
`clampSafeResize`, `applySafeResize` и `validateSafeResize` в прежнем порядке;
|
||
7. правилом «failed projection сохраняет последний полный accepted preview,
|
||
delta и labels»;
|
||
8. формированием typed `commit | no-op | rejected | cancelled` outcome.
|
||
|
||
Controller не экспортирует изменяемые поля. Root читает immutable getters/
|
||
snapshots (`dragging`, `preview`, `liveLabels`) и меняет состояние только
|
||
публичными командами controller.
|
||
|
||
### 4.2 Root сохраняет
|
||
|
||
`src/houseplan-card.ts` остаётся владельцем:
|
||
|
||
- DOM/PointerEvent: `preventDefault`, `stopPropagation`, pointer capture,
|
||
перевод координат через `_svgPoint` и маршрутизация событий;
|
||
- Lit reactivity и `requestUpdate`;
|
||
- `_t`, disabled-reason copy и toast;
|
||
- получение current rooms/openings/obstacles/thickness/options из модели;
|
||
- построение preview space, rekey wall metadata и wall-segment endpoints;
|
||
- common physical geometry preflight и retained wall geometry/cache;
|
||
- построение локализованных live labels и их DOM/SVG render;
|
||
- `_serverCfg`, `_cfgEpoch`, geometry command stack, suppression synthetic
|
||
click, `_commitPhysicalGeometry` и serialized write queue;
|
||
- tool/mode/space lifecycle и сброс других инструментов.
|
||
|
||
Root handlers после выноса являются тонкими adapters: переводят Event в
|
||
primitive input, вызывают controller, исполняют outcome и запрашивают render.
|
||
Они не держат зеркальных `_rszDrag/_rszPreview/_rszLive` полей и не дублируют
|
||
проверки state transition.
|
||
|
||
### 4.3 Что остаётся pure geometry
|
||
|
||
`src/resize.ts` остаётся единственным источником topology/eligibility/range/
|
||
candidate math. Controller импортирует его функции, но не копирует алгоритмы.
|
||
`src/resize-labels.ts` остаётся единственным источником placement math.
|
||
|
||
Не создаются второй room model, второй wall model, новый render tree или общий
|
||
framework контроллеров для остальных инструментов.
|
||
|
||
## 5. Typed ports и outcomes
|
||
|
||
Конкретные имена типов могут уточняться при реализации, но граница обязательна:
|
||
|
||
```ts
|
||
type ResizeControllerOutcome<TSpace, TSnapshot, TWallUnion> =
|
||
| { kind: 'commit'; preview: Readonly<ResizePreview<TSpace>>;
|
||
before: TSnapshot }
|
||
| { kind: 'rejected'; reason: 'stale-snapshot' | 'invalid-topology'
|
||
| 'invalid-candidate' | 'wall-records' }
|
||
| { kind: 'no-op' }
|
||
| { kind: 'cancelled'; restoreWallUnion: TWallUnion | null };
|
||
```
|
||
|
||
Controller получает через typed inputs/ports:
|
||
|
||
- current context identity и immutable snapshot;
|
||
- `snap(point)`;
|
||
- `resolve(roomId, edge)` либо данные для вызова pure resolver;
|
||
- `project(snapshot, candidate)` → success с exact preview/retained geometry
|
||
либо bounded failure reason;
|
||
- `validatePreview(exactPreview)` — common physical preflight на pointerup;
|
||
- `measure(candidate, plan)` — presentation model без DOM measurement.
|
||
|
||
Ports не получают controller instance и не могут менять его поля. Exceptions
|
||
adapter'ов перехватываются на границе и превращаются в bounded fail-closed
|
||
failure; exception text, ids и координаты не попадают в UI.
|
||
|
||
Root исполняет только `kind: 'commit'` и только один раз. Любой другой outcome
|
||
сначала удаляет overlay/controller session, затем приводит render к committed
|
||
модели и не вызывает `_commitPhysicalGeometry`.
|
||
|
||
## 6. Конечный автомат
|
||
|
||
| Текущее состояние | Команда | Условие | Результат |
|
||
|---|---|---|---|
|
||
| `idle` | `resolve` | тот же context identity | cache hit либо один pure resolver call |
|
||
| `idle` | `selectRoom` | клик по площади комнаты | сохранить latent `selectedRoomId`, без visual/config effect |
|
||
| `idle` | `escape` | `selectedRoomId` существует | очистить selection, остаться в Resize |
|
||
| `idle` | `escape` | selection отсутствует | typed outcome `exit-tool`; root включает Draw |
|
||
| `idle` | `begin` | tool Resize, enabled plan | `dragging`, immutable session captured |
|
||
| `idle` | `begin` | disabled | `idle`, no capture/preview/write; root показывает reason |
|
||
| `dragging` | `move` | wrong pointer id | no-op |
|
||
| `dragging` | `move` | delta прежний | no-op |
|
||
| `dragging` | `move` | projection valid | replace exact preview/delta/labels atomically |
|
||
| `dragging` | `move` | projection invalid/throws | retain last accepted triple; notification only once |
|
||
| `dragging` | `finish` | no movement/zero delta/no preview | clear session, `no-op` |
|
||
| `dragging` | `finish` | stale snapshot, invalid topology/candidate/invariant | clear session, `rejected`, zero write/Undo |
|
||
| `dragging` | `finish` | all guards pass | clear session, one `commit` with exact preview |
|
||
| `dragging` | `cancel`/pointer cancel/lost capture/Escape/Ctrl+Z/tool/space/mode exit | matching active session | clear session, `cancelled`, zero write/Undo; eligible cache invalidated as today |
|
||
| `idle` | cancel/finish | — | no-op |
|
||
|
||
Every transition is synchronous and deterministic for the same inputs. A late
|
||
lostpointercapture after successful pointerup sees `idle` and cannot cancel or
|
||
commit twice.
|
||
|
||
## 7. Preview и transaction boundary
|
||
|
||
1. `begin` captures exact snapshot once. Every move projects from a fresh deep
|
||
copy of that snapshot, never from previous preview or mutable `_serverCfg`.
|
||
2. Projected rooms, openings, `walls` and `wall_segments` are the same objects
|
||
subsequently shown by render and offered for commit.
|
||
3. Preview success is atomic: preview, accepted delta and live labels replace
|
||
the previous accepted triple together. Failure replaces none of them.
|
||
4. Candidate projection keeps the existing fixed-topology
|
||
`rekeyWallsAfterMoveChecked(..., 'fixed-topology')`, changed-record carrier
|
||
check and common physical preflight.
|
||
5. Immediately after rekey, exact wall-record preservation is checked against
|
||
the immutable before snapshot. Failure returns bounded `wall-metadata` and
|
||
cannot publish the preview.
|
||
6. `finish` proves current snapshot identity, `validateSafeResize`, identity of
|
||
the accepted preview, common physical preflight and exact wall-record
|
||
preservation **again** immediately before returning `commit`.
|
||
7. Root copies exactly `rooms`, `openings`, `walls` and `wall_segments` from
|
||
commit outcome and calls one named history command. No simplify/rebuild or
|
||
second candidate computation is allowed between outcome and write.
|
||
|
||
## 8. Один инвариант wall records
|
||
|
||
Новый `src/wall-record-preservation.ts` является единственным алгоритмом:
|
||
|
||
```ts
|
||
checkWallRecordsPreserved(before, after, {
|
||
allowClear?: boolean,
|
||
exactMultiplicity?: boolean,
|
||
}): WallRecordViolation[]
|
||
```
|
||
|
||
Контракт:
|
||
|
||
- `allowClear: true` явно отключает проверку; Resize его не передаёт;
|
||
- default/CLI mode сохраняет текущую семантику #254: каждое положительное
|
||
конечное значение `cm`, существовавшее before, должно присутствовать after
|
||
хотя бы один раз; законная merge одинаковых записей не считается потерей;
|
||
- `exactMultiplicity: true` сравнивает полное мультимножество всех конечных
|
||
числовых `cm`, включая `0`; это режим fixed-topology Resize;
|
||
- порядок records, key и endpoints не участвуют в профиле: их корректность
|
||
отдельно доказывают rekey/carrier/physical checks;
|
||
- `NaN`, infinity и non-number не превращаются неявно в `0`; такие записи не
|
||
маскируют потерю валидной записи и проверяются существующими model gates;
|
||
- violation — bounded structured data без room ids/coordinates.
|
||
|
||
`scripts/model-invariants.mjs` удаляет собственную реализацию и re-export/import
|
||
production helper из `test-build`, который команда `npm run invariants` уже
|
||
собирает до запуска CLI. Это тот же действующий паттерн, что production
|
||
preflight и near-axis в этом script. Unit tests проверяют обе семантики через
|
||
один импорт и отдельно доказывают, что Resize вызывает exact mode на preview и
|
||
finish.
|
||
|
||
## 9. Lifecycle, cache и восстановление
|
||
|
||
- Eligibility cache key сохраняет семантику #277: space, cell/grid scale и
|
||
exact geometry snapshot. Snapshot вычисляется root один раз на layer render,
|
||
а controller cache хранит results по `(roomId, edge)`.
|
||
- Cache очищается при successful commit, tool/mode/space lifecycle reset и при
|
||
смене context identity. Он bounded одной Map текущего context.
|
||
- Cancel восстанавливает retained pre-drag wall-union alias только если root
|
||
подтверждает прежнюю snapshot identity, как сейчас; иначе cache silently
|
||
отбрасывается.
|
||
- Preview по-прежнему увеличивает structural epoch и запрещает reuse clean-floor
|
||
cache. После cancel/no-op/reject root возвращается к committed model.
|
||
- Warm remount не переносит active gesture/preview. `WarmViewport.rszSel`
|
||
продолжает сохранять latent selection, но читает/восстанавливает его только
|
||
через controller API; это module/session state, не сериализуемый config.
|
||
- `dispose`/disconnect, tool switch, space switch and mode exit must leave
|
||
controller idle and release opaque retained references.
|
||
|
||
## 10. В скоупе
|
||
|
||
- новый controller и перенос в него Resize state/transitions/cache;
|
||
- typed root adapters и commit outcome;
|
||
- extraction единого production wall-record invariant;
|
||
- удаление controller-owned `_rsz*` mirrors из root и перенос latent
|
||
`_rszSel`-семантики в controller;
|
||
- unit tests state machine/invariant, source-boundary guard и действующие
|
||
production pointer smokes;
|
||
- актуализация architecture/Resize/testing/status docs;
|
||
- синхронизация tracked bundle как обычный результат build.
|
||
|
||
## 11. Не входит
|
||
|
||
- изменение geometry, UI, CSS, i18n, disabled reasons или hotkeys;
|
||
- изменение schema/model version/migrations/compatibility JSON;
|
||
- перенос `_wallThick*`, `_opening*`, drawing, labels renderer или общего
|
||
geometry command stack;
|
||
- новый общий controller framework/base class/dependency injection container;
|
||
- переработка `src/resize.ts` и оптимизация boolean geometry;
|
||
- новое логирование, telemetry или пользовательский recovery dialog;
|
||
- cleanup иных мёртвых полей монолита.
|
||
|
||
Найденные вне этой границы дефекты получают отдельные issue и не исправляются
|
||
попутно.
|
||
|
||
## 12. Ошибки и edge cases
|
||
|
||
Обязательная матрица:
|
||
|
||
1. disabled handle mouse/keyboard/touch — controller не входит в dragging;
|
||
2. synthetic pointer, на котором capture throws, не ломает session;
|
||
3. wrong pointer id на move/up/cancel — no-op;
|
||
4. repeated identical move — не перестраивает preview/labels;
|
||
5. first failed projection — preview отсутствует, pointer визуально остаётся на
|
||
committed model, toast один;
|
||
6. failed projection after success — остаётся последний полный safe preview;
|
||
7. failure adapter exception — то же bounded поведение;
|
||
8. zero-delta pointerup — no write/Undo;
|
||
9. stale config/space snapshot на pointerup — reject, zero write/Undo;
|
||
10. topology validation failure — reject, zero write/Undo;
|
||
11. wall record count/value loss, включая `cm:0` — preview/commit reject;
|
||
12. reordered records с тем же exact profile — разрешено;
|
||
13. carrier/physical preflight failure — reject;
|
||
14. Escape, Ctrl/Cmd+Z, pointercancel, lost capture, tool/mode/space exit — один
|
||
cancel, exact committed geometry, zero write/Undo;
|
||
15. late cancel after up — no second outcome;
|
||
16. success — one outcome, one history command, one scheduled write;
|
||
17. room-area click в idle Resize + Escape + Escape сохраняет текущую
|
||
последовательность «очистить latent selection → выйти в Draw»;
|
||
18. Undo/Redo/reload — exact parity с поведением #277;
|
||
19. disconnected root не оставляет controller references/timers/listeners.
|
||
|
||
## 13. Performance
|
||
|
||
Изменение не должно ухудшить действующие бюджеты #277:
|
||
|
||
- handle layer computes geometry snapshot once per render, not per edge;
|
||
- warm eligibility cache не вызывает повторный resolver для того же key;
|
||
- pointermove p95 на large-house fixture ≤16 ms и ≤20% хуже same-run baseline;
|
||
- pointerup preflight p95 ≤75 ms;
|
||
- commit invariant O(number of wall records), без polygon work и без JSON
|
||
stringify каждого record сверх уже необходимого snapshot;
|
||
- controller создаёт не более одного accepted preview на успешный distinct
|
||
delta и не сохраняет историю preview после завершения session.
|
||
|
||
Отдельной новой performance feature нет; доказательство — существующий
|
||
`benchmark:safe-resize`, render benchmark/call-count и review профиля diff.
|
||
|
||
## 14. Touch и accessibility
|
||
|
||
View/kiosk не затронуты. Plan editor остаётся desktop-first, но safety floor
|
||
обязателен на touch: finger-sized handles, disabled reason on tap, pointer id
|
||
guard, pointercancel/lost capture and pinch cancellation remain exactly as in
|
||
#277. Controller receives primitives after DOM event handling and therefore не
|
||
может отменить `preventDefault`, aria attributes, focusability или hit area.
|
||
|
||
## 15. Файлы и модули
|
||
|
||
Планируемый diff:
|
||
|
||
- `src/resize-controller.ts` — новый controller и typed outcomes;
|
||
- `src/wall-record-preservation.ts` — единый invariant;
|
||
- `src/houseplan-card.ts` — composition/adapters/render wiring, удаление mirrors;
|
||
- `scripts/model-invariants.mjs` — import/re-export production invariant;
|
||
- `tsconfig.test.json` — test-build новых production modules;
|
||
- `test/resize-controller.test.mjs` — state transition matrix;
|
||
- `test/wall-record-preservation.test.mjs` и/или
|
||
`test/model-invariants.test.mjs` — shared semantics;
|
||
- `test/resize-production-path.test.mjs` — source boundary/exact guard;
|
||
- существующие `demo/smoke_room_resize.mjs`,
|
||
`demo/smoke_resize_pointer_real_plan.mjs`,
|
||
`demo/smoke_resize_wall_thickness.mjs`,
|
||
`demo/smoke_resize_inner_dimensions.mjs` — behavioral parity;
|
||
- `docs/ARCHITECTURE.md`, `docs/RESIZE.md`, `docs/TESTING.md`,
|
||
`docs/STATUS.md` — фактическая граница и проверка.
|
||
|
||
Иные файлы добавляются только если `smoke-select`/build sync механически этого
|
||
требуют. RU/EN i18n и changelog не меняются, потому что `User-Visible: no`.
|
||
|
||
## 16. Критерии приёмки
|
||
|
||
| AC | Критерий | Доказательство |
|
||
|---|---|---|
|
||
| AC1 | В root нет mutable `_rszDrag`, `_rszPreview`, `_rszLive`, `_rszEligibilityCache` и `_rszSel`; состояние и cache принадлежат одному `ResizeController`. | Source guard + code review. |
|
||
| AC2 | Controller реализует таблицу §6: wrong pid/duplicate events/no-op/cancel/reject/success дают ровно один детерминированный outcome. | Unit. |
|
||
| AC3 | Preview строится из immutable pre-drag snapshot, никогда не пишет `_serverCfg`; failed projection сохраняет последний полный accepted preview/delta/labels и уведомляется один раз. | Unit + production pointer smoke. |
|
||
| AC4 | Pointerup возвращает commit только для exact displayed preview после snapshot/topology/common-preflight checks; root выполняет ровно одну geometry command/write, остальные outcomes — 0. | Unit + production pointer smoke. |
|
||
| AC5 | Cancel/Escape/Ctrl+Z/pointercancel/lost capture/tool/mode/space exit возвращают committed model, 0 Undo/write и не допускают late double outcome. | Unit + browser smoke. |
|
||
| AC6 | Один production helper сохраняет старую CLI presence-семантику и exact Resize-семантику, включая `cm:0`; script не содержит копии counting/comparison algorithm. | Unit + source guard + `npm run invariants`. |
|
||
| AC7 | Exact invariant вызывается при projection и повторно в final controller guard; искусственная потеря/изменение multiplicity даёт fail-closed до commit. | Injectable unit + production-path test. |
|
||
| AC8 | Safe Resize #277 остаётся функционально идентичным: eligibility, clamp, opening stops, preview, measurements, Undo/Redo/reload и disabled a11y проходят существующие тесты. | Existing units + selected smokes. |
|
||
| AC9 | Handle render вычисляет snapshot один раз, warm cache bounded текущим context и не повторяет resolver; бюджеты §13 зелёные. | Call-count unit + benchmark. |
|
||
| AC10 | DOM/CSS/i18n/config schema и tracked visual baselines не меняются; targeted golden verify не показывает diff. | Source/diff review + golden verify. |
|
||
| AC11 | Typecheck, unit, build, docs and model-invariant gates зелёные; tracked bundle синхронизирован. | Local gates + CI. |
|
||
| AC12 | Architecture/Resize/testing/status docs описывают controller boundary, single-source invariant и refactor-only status; changelog не изменён. | Docs gate + review. |
|
||
|
||
## 17. План тестирования
|
||
|
||
### Unit
|
||
|
||
- fake typed ports без DOM для каждой строки автомата §6 и edge cases §12;
|
||
- prove a failed state transition by mutation/negative input, not only happy
|
||
snapshots;
|
||
- presence/exact/allowClear profiles: duplicates, count decrease, count
|
||
increase, reordered values, zero, invalid numeric values;
|
||
- controller exact-guard call count at preview and finish;
|
||
- eligibility cache hit/miss/invalidation and one snapshot per rendered layer.
|
||
|
||
### Production path / browser
|
||
|
||
- update source contract so tests forbid state mirrors and inline wall-cm
|
||
comparison in root;
|
||
- execute real pointerdown/move/up and cancel paths through bundled card;
|
||
- selected existing smokes from `node scripts/smoke-select.mjs --base origin/dev
|
||
--head HEAD`, at minimum safe Resize, wall thickness and inner dimensions;
|
||
- verify one write/Undo on success and zero on every fail/cancel path.
|
||
|
||
### Visual / performance
|
||
|
||
- targeted existing Resize golden scenes in light/dark must match committed
|
||
baselines; new baselines are forbidden unless review proves unrelated drift;
|
||
- existing safe-resize and safe-resize-render benchmarks stay inside §13.
|
||
|
||
Full prerelease smoke/golden/performance matrix remains a release-stage gate,
|
||
not a substitute for targeted implementation checks.
|
||
|
||
## 18. Документация, compatibility и release artifacts
|
||
|
||
- **Schema/migration:** none; successful persisted payload byte-equivalent.
|
||
- **i18n:** no keys added/changed/removed.
|
||
- **Changelog RU/EN:** no entries; refactor-only, `User-Visible: no`.
|
||
- **User guide:** no change.
|
||
- **Architecture/engineering docs:** update files from §15 in implementation
|
||
commit.
|
||
- **Golden/screenshots:** verify only; no accepted visual delta expected.
|
||
- **Security/backend:** no impact.
|
||
|
||
## 19. Риски и меры
|
||
|
||
| Риск | Мера |
|
||
|---|---|
|
||
| Shallow extraction leaves two owners of state | AC1 forbids mirrors; root gets immutable getters and outcomes only. |
|
||
| Controller absorbs renderer/HA and becomes a second monolith | Explicit ownership matrix §4; no Lit/DOM/i18n/config write imports. |
|
||
| Preview and commit diverge during refactor | Exact preview object identity + final guards + AC3/AC4. |
|
||
| Common invariant weakens #277 exact contract to coarse CLI presence | Two modes in one helper; Resize always exact, AC6/AC7 include multiplicity and zero walls. |
|
||
| Zero-thickness walls from #306 disappear silently | Exact profile includes finite numeric `cm:0`. |
|
||
| Adapter exception leaks partial state | Boundary catches and maps to bounded reject, state cleared atomically. |
|
||
| Cancel loses expensive pre-drag wall union reuse | Opaque retained token and snapshot-identity restore test. |
|
||
| Перенос `_rszSel` меняет warm/Escape behavior | Controller сохраняет latent selection, warm getter/restore и двухшаговый Escape; dedicated unit/browser assertion. |
|
||
| Large mechanical diff hides behavior change | Separate spec and implementation commits, targeted source guard, selected smokes, golden no-diff and independent review. |
|
||
|
||
## 20. Rollback
|
||
|
||
Rollback is a normal revert of the implementation commit(s), including generated
|
||
bundle and documentation. There is no data migration, feature flag or persisted
|
||
new field to undo. Because successful payloads remain byte-identical, a plan
|
||
edited by the refactored controller opens unchanged after rollback.
|