mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
docs: specify resize controller extraction
Issue: #264 User-Visible: no
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
# 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. Зачем это делается
|
||||
|
||||
После #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. Мёртвое session-only поле `_rszSel` удаляется: после #277 оно не имеет
|
||||
renderer-потребителя и не влияет на пиксели, config или Undo. Escape в
|
||||
idle Resize, как и фактически сейчас, сразу возвращает neutral Draw.
|
||||
|
||||
Открытых продуктовых вопросов нет.
|
||||
|
||||
## 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`, не параллельный флаг;
|
||||
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. переходами `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` | `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. Удаление неиспользуемого
|
||||
`rszSel` из `WarmViewport` не меняет сериализуемый config или видимый state.
|
||||
- `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 и мёртвого `_rszSel`;
|
||||
- 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. Undo/Redo/reload — exact parity с поведением #277;
|
||||
18. 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_safe_resize.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. |
|
||||
| Removal of `_rszSel` changes warm/Escape behavior | Source/runtime parity test; it has no render consumer and idle Escape ends at Draw. |
|
||||
| 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.
|
||||
|
||||
Reference in New Issue
Block a user