mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -1,238 +1,182 @@
|
||||
# ТЗ #405 — Удаление черновика доказуемо завершается, а порог свидетелей не исчезает вместе с кадрами
|
||||
# ТЗ #405 — Удаление черновика доказуемо завершается
|
||||
|
||||
- Issue: https://github.com/Matysh/houseplan-card/issues/405
|
||||
- Приоритет: P2, infra; полный трек — две несвязанные поверхности (транзакция
|
||||
геометрии в `src/**` и приёмка скриншотов в `scripts/**`), критерий «одна
|
||||
поверхность» для `small` не выполняется. Класс файлов A + B: правка
|
||||
`src/houseplan-editor-runtime.ts` делает задачу продуктовой по §1
|
||||
- Ревизия: 1 (2026-08-31)
|
||||
- Приоритет: P2, infra
|
||||
- Трек: полный — исправление пересекает три модуля (`editor-secondary`,
|
||||
editor runtime и фасад карточки), поэтому критерий `small` «один модуль» не
|
||||
выполняется
|
||||
- Ревизия: 2 (2026-09-01), правки по `SPEC-REVIEW-405-r1`
|
||||
|
||||
## Сценарий
|
||||
|
||||
Две проверки, которые выглядят работающими и не работают.
|
||||
|
||||
Первая: смок утверждает, что удаление незавершённого контура удаляет его
|
||||
целиком. Утверждение зеленеет только потому, что подтверждение в смоке
|
||||
резолвится мгновенно; у живого человека подтверждение — это клик, то есть
|
||||
макрозадача, и к моменту проверки удаление ещё не произошло. Утверждение
|
||||
ложное, и вместе с ним ложны утверждения ниже по файлу.
|
||||
|
||||
Вторая: приёмка скриншотов требует «кадров-свидетелей» — необъявленных кадров,
|
||||
совпавших байт-в-байт, — чтобы доказать, что съёмка велась в той же среде.
|
||||
Требование считается от числа уцелевших кадров, поэтому снимается вместе с
|
||||
ними: `git rm docs/images/*.png` — и доказывать больше нечем, но и не нужно.
|
||||
Пользователь редактора плана выбирает незавершённый контур, нажимает удаление и
|
||||
подтверждает опасное действие. Само подтверждение асинхронно: в реальном UI оно
|
||||
завершается только после отдельного клика. Внешний вызывающий и автоматический
|
||||
смок должны иметь возможность дождаться всей операции, а не только синхронного
|
||||
входа в обработчик.
|
||||
|
||||
## Что человек увидит до и после
|
||||
|
||||
Видимого поведения продукта задача не меняет.
|
||||
Видимое поведение интерфейса не меняется: кнопка удаления, диалог подтверждения
|
||||
и результат подтверждённого или отменённого удаления остаются прежними.
|
||||
|
||||
**До**: (1) вызывающий не может дождаться удаления черновика — метод синхронный,
|
||||
а работа асинхронная; смок это скрывает мгновенным стабом; (2) удаление всех
|
||||
закоммиченных кадров обнуляет порог свидетелей, и тотальная перерисовка
|
||||
принимается без `--no-witnesses --reason`, то есть без следа в манифесте.
|
||||
**После**: удаление черновика доказуемо завершено к моменту, когда вызывающий
|
||||
его дождался; порог свидетелей зависит от набора сценариев, а не от того, что
|
||||
осталось на диске.
|
||||
До исправления программный вызывающий получает `undefined` и не может узнать,
|
||||
когда отложенное удаление завершилось. После исправления он получает промис,
|
||||
который завершается вместе с подтверждением и транзакцией удаления.
|
||||
|
||||
## Проблема и контракты по пунктам
|
||||
## Проблема
|
||||
|
||||
### (1) M2 — оброненный промис в удалении черновика
|
||||
`_deletePhysicalSelection` объявлен синхронным. Для выбранного draft он вызывает
|
||||
асинхронный `_deleteDraftWhole()`, отбрасывает возвращённый промис и немедленно
|
||||
возвращает управление. `demo/smoke_free_walls.mjs` пишет
|
||||
`await c._deletePhysicalSelection()`, но фактически ждёт `undefined`.
|
||||
|
||||
Цепочка синхронна на всём протяжении, а конец её асинхронный:
|
||||
Текущий стаб `_confirmDanger = async () => true` резолвится в ближайшей
|
||||
микрозадаче, поэтому гонка обычно скрыта. Если подтверждение резолвить через
|
||||
макрозадачу, как реальный пользовательский клик, проверка выполняется до
|
||||
удаления; незавершённый контур остаётся в состоянии и загрязняет последующие
|
||||
утверждения смока.
|
||||
|
||||
| Место | Сигнатура |
|
||||
|---|---|
|
||||
| `editor-secondary.ts:221` | `runContext(captured, current, action: () => void): void` |
|
||||
| `houseplan-editor-runtime.ts:5290` | `_runEditorContext(contextId, action: () => void): void` |
|
||||
| `houseplan-editor-runtime.ts:3073` | `_deletePhysicalSelection = (): void =>` |
|
||||
| `houseplan-editor-runtime.ts:3077` | `if (sel.kind === 'draft') { this._deleteDraftWhole(); return; }` ← промис отброшен |
|
||||
| `houseplan-editor-runtime.ts:3119` | `_deleteDraftWhole = async (): Promise<void> =>` — внутри `await _confirmDanger(...)` |
|
||||
## Техническая карта текущей цепочки
|
||||
|
||||
`demo/smoke_free_walls.mjs:194,199` пишет `await c._deletePhysicalSelection()`,
|
||||
но ждёт `undefined` — ровно один микротаск. Зеленеет проверка только потому,
|
||||
что стаб на `:16` (`c._confirmDanger = async () => true`) резолвится в том же
|
||||
сливе микрозадач.
|
||||
Номера строк ориентировочные и относятся к `dev` на момент ревизии 2.
|
||||
|
||||
**Воспроизведено исполнением**: единственная правка копии смока — подтверждение
|
||||
резолвится через макрозадачу, как настоящий клик:
|
||||
| Место | Текущее поведение | Требуемое поведение |
|
||||
|---|---|---|
|
||||
| `src/editor-secondary.ts:221` `runContext` | принимает `() => void`, отбрасывает результат | обобщённо возвращает результат действия или `undefined`, если context уже неактуален |
|
||||
| `src/houseplan-editor-runtime.ts:5290` `_runEditorContext` | принимает `() => void`, отбрасывает результат `runContext` | обобщённо пробрасывает результат `runContext` |
|
||||
| `src/houseplan-card.ts:8929` `_runEditorContext` | фасад типизирован как `void` | зеркалит обобщённый тип runtime и пробрасывает результат |
|
||||
| `src/houseplan-editor-runtime.ts:3073` `_deletePhysicalSelection` | `(): void`, draft-ветка вызывает `_deleteDraftWhole()` без `return`/`await` | всегда возвращает `Promise<void>`, draft-ветка дожидается `_deleteDraftWhole()` |
|
||||
| `src/houseplan-card.ts:7897` `_deletePhysicalSelection` | фасад типизирован как `(): void` | фасад типизирован как `(): Promise<void>` и возвращает промис runtime |
|
||||
| `src/houseplan-editor-runtime.ts:3119` `_deleteDraftWhole` | уже `async (): Promise<void>` | без изменения контракта |
|
||||
|
||||
```js
|
||||
c._confirmDanger = () => new Promise((r) => setTimeout(() => r(true), 0));
|
||||
```
|
||||
Фасад `src/houseplan-card.ts:7897` обязателен в правке: именно экземпляр
|
||||
`houseplan-card` находится в `window.__card` и вызывается смоком. Если runtime
|
||||
станет `Promise<void>`, а фасад останется `(): void`, `tsc --noEmit` получит
|
||||
`TS2322`. Соседние фасады `_deleteDraftWhole` и `_deleteDraftSegment` уже
|
||||
следуют требуемому паттерну `(): Promise<void>`.
|
||||
|
||||
```
|
||||
"deleteRemovesPartition": true,
|
||||
"deleteOnDraftRemovesWholeOutline": false,
|
||||
FAILED (2):
|
||||
- deleteOnDraftRemovesWholeOutline: expected true, got false
|
||||
- invalidThicknessCreatesNothing: expected true, got false
|
||||
```
|
||||
## Контракт поведения
|
||||
|
||||
Второй провал важнее первого: оброненный промис отравляет и последующие
|
||||
проверки — состояние на момент их выполнения не то, которое смок считает
|
||||
подготовленным. Цена дефекта не «одна проверка без доказательства», а «дальше
|
||||
по файлу доказательства условны».
|
||||
1. `_deletePhysicalSelection()` всегда возвращает `Promise<void>` во всех
|
||||
ветках, включая синхронное удаление partition/column и ранний выход без
|
||||
выбора.
|
||||
2. Для draft промис завершается только после решения пользователя и, при
|
||||
подтверждении, после фиксации транзакции удаления.
|
||||
3. Отмена подтверждения резолвит промис без удаления и без отклонения.
|
||||
4. `runContext` и оба фасада не обрывают возвращаемое действие. При устаревшем
|
||||
context действие не запускается, возвращается `undefined`.
|
||||
5. UI-обработчики вправе игнорировать промис: кнопки не блокируются, новый
|
||||
индикатор ожидания не появляется.
|
||||
6. Синхронные ветки удаления сохраняют существующее поведение, но оборачивают
|
||||
завершение в уже разрешённый промис.
|
||||
|
||||
**Контракт**: асинхронная операция обязана быть дождавшейся — то есть
|
||||
возвращать промис до самого внешнего вызывающего, который захочет её дождаться.
|
||||
UI ждать не обязан (кнопка не блокируется, поведение для человека не меняется),
|
||||
но возможность дождаться должна существовать, иначе проверить операцию нечем.
|
||||
## Скоуп
|
||||
|
||||
Практически: `_deletePhysicalSelection` возвращает `Promise<void>` и
|
||||
пробрасывает промис `_deleteDraftWhole`; `_runEditorContext` и `runContext`
|
||||
возвращают результат действия вместо `void` (обобщённый параметр), чтобы
|
||||
промис не терялся на полпути. Обработчики кнопок остаются как есть — они
|
||||
результат игнорируют.
|
||||
- типы и возврат значения по цепочке `runContext` → `_runEditorContext`;
|
||||
- runtime- и card-фасады `_deletePhysicalSelection`;
|
||||
- задержанный стаб подтверждения и утверждения удаления/отмены в
|
||||
`demo/smoke_free_walls.mjs`;
|
||||
- отрицательный мутант, доказывающий, что смок ловит отброшенный промис.
|
||||
|
||||
**Связка с #404**: если `_deleteDraftWhole` когда-нибудь отклонится, это станет
|
||||
`unhandledrejection`, и его ловит тот же гард смоков — с той же слепотой к
|
||||
хвосту, что чинит #404. Задачи независимы, но обе нужны, чтобы отказ удаления
|
||||
не исчезал бесследно.
|
||||
## Не-скоуп
|
||||
|
||||
### (2) M4 — порог свидетелей обнуляется вместе с кадрами
|
||||
|
||||
`scripts/docs-accept.mjs:117-123` заполняет `committed` только по файлам,
|
||||
лежащим на диске:
|
||||
|
||||
```js
|
||||
const onDisk = resolve(ROOT, 'docs/images', entry.file);
|
||||
if (existsSync(onDisk)) committed[scenario.id] = sha256(readFileSync(onDisk));
|
||||
```
|
||||
|
||||
`docs-acceptance.mjs:113` считает `floor = docsWitnessFloor(withCommitted.length)`,
|
||||
а формула на `:44` при нуле даёт ноль.
|
||||
|
||||
**Воспроизведено** модельным прогоном чистой функции — контрастная пара на одном
|
||||
наборе из десяти сцен и одной декларации «изменились все десять»:
|
||||
|
||||
```
|
||||
1) PNG на месте: floor=0 свидетелей=0 → отказ: «свидетелей 0 из необходимых 1»
|
||||
2) PNG удалены: floor=0 свидетелей=0 → отказа НЕТ, принимаются все 10
|
||||
```
|
||||
|
||||
Щель не требует хака: `git rm docs/images/*.png` плюс `--expect-change` на все
|
||||
десять. Предшествующая проверка `strayed` не мешает — при пустом `committed`
|
||||
каждый кадр «разошёлся», но все они объявлены.
|
||||
|
||||
Порог задумывался (комментарий `docs-acceptance.mjs:33-43`) закрыть ровно
|
||||
«попытку объявить изменёнными все кадры разом, когда свидетелей не остаётся
|
||||
вовсе». Именно этот случай он и пропускает, потому что считает от числа
|
||||
уцелевших кадров.
|
||||
|
||||
**Контракт**: порог считается от размера набора сценариев — величины, известной
|
||||
из `DOC_SCREENSHOTS` и не зависящей от содержимого диска. Отсутствие
|
||||
закоммиченного кадра — не смягчающее обстоятельство, а само по себе повод
|
||||
требовать явного обхода: `--no-witnesses --reason="…"` остаётся единственной
|
||||
законной дорогой, и причина остаётся в манифесте.
|
||||
|
||||
**Осознанное следствие**: первая приёмка набора, у которого кадров ещё нет
|
||||
(новый сценарий, чистый клон без `docs/images`), потребует
|
||||
`--no-witnesses --reason`. Это правильно: доказать среду в такой ситуации
|
||||
нечем, и единственное, что можно сделать честно, — оставить письменную причину.
|
||||
|
||||
## Скоуп / не-скоуп
|
||||
|
||||
**В скоупе**: возврат промиса по цепочке `runContext` →
|
||||
`_runEditorContext` → `_deletePhysicalSelection`; стаб подтверждения и
|
||||
ожидание в `demo/smoke_free_walls.mjs`; формула и источник порога в
|
||||
`scripts/docs-acceptance.mjs`; смоки, тесты и мутанты.
|
||||
|
||||
**Не в скоупе**: остальные асинхронные операции редактора (`_deleteDraftSegment`
|
||||
и прочие имеют своих вызывающих и свои смоки — если у них тот же узор, это
|
||||
новый issue, а не попутная правка); блокировка кнопки на время удаления —
|
||||
это UX-решение, которого задача не обещает; гард uncaught exception (#404);
|
||||
затирание `acceptance.declared` при повторной приёмке (#406 «д») — соседняя
|
||||
строка того же файла, но другой контракт.
|
||||
- блокировка кнопки и новый UX ожидания;
|
||||
- изменение диалога опасного подтверждения;
|
||||
- другие асинхронные операции редактора и `_deleteDraftSegment`;
|
||||
- общий гард `unhandledrejection` (#404);
|
||||
- docs witness floor и сохранение `acceptance.declared`: исправлены отдельно в
|
||||
#409, коммит `8119c523`;
|
||||
- аналогичный golden witness floor: #408.
|
||||
|
||||
## UX
|
||||
|
||||
Видимых изменений нет. Кнопка удаления работает как прежде и по-прежнему не
|
||||
ждёт завершения записи.
|
||||
UI не меняется. Подтверждение, отмена, фокус и доступность диалога сохраняются.
|
||||
Touch-контракт не затронут: не добавляются жесты, цели касания или новые
|
||||
состояния кнопок.
|
||||
|
||||
## Модель данных и миграция
|
||||
|
||||
Не применимо. Формат конфига и формат манифеста скриншотов не меняются:
|
||||
`acceptance.floor` остаётся числом, меняется только способ его вычисления.
|
||||
Конфиг, сохранённая геометрия и compatibility-поля не меняются. Миграция не
|
||||
нужна.
|
||||
|
||||
## i18n
|
||||
|
||||
Новых строк нет.
|
||||
Новых и изменённых строк нет.
|
||||
|
||||
## Производительность
|
||||
|
||||
Влияния нет: новая асинхронная работа не добавляется, наружу пробрасывается уже
|
||||
существующий промис. UI продолжает игнорировать его там, где ожидание не нужно.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- **AC1**. Удаление черновика доказуемо завершено к моменту, когда вызывающий
|
||||
дождался возвращённого промиса, **при подтверждении, резолвящемся через
|
||||
макрозадачу**. Доказательство: `smoke_free_walls` со стабом
|
||||
`() => new Promise((r) => setTimeout(() => r(true), 0))`.
|
||||
- **AC2**. **Отрицательный прогон обязателен**: на неисправленном коде тот же
|
||||
смок с тем же стабом краснеет (`deleteOnDraftRemovesWholeOutline: false`).
|
||||
Доказательство: мутант через штатный раннер `scripts/mutation-gate.mjs`.
|
||||
- **AC3**. Отказ подтверждения не удаляет черновик, и вызывающий это узнаёт:
|
||||
промис резолвится, контур на месте. Доказательство: та же пара проверок со
|
||||
стабом, возвращающим `false`.
|
||||
- **AC4**. Поведение кнопок не изменилось: обработчик не ждёт промис, интерфейс
|
||||
не блокируется. Доказательство: существующие смоки редактора зелёные без
|
||||
правок их утверждений.
|
||||
- **AC5**. Удаление перегородки и колонны (синхронные ветки того же метода)
|
||||
работают как прежде. Доказательство: `deleteRemovesPartition` и соседние
|
||||
проверки `smoke_free_walls` зелёные.
|
||||
- **AC6**. Порог свидетелей не зависит от наличия файлов на диске: удаление
|
||||
всех закоммиченных кадров и объявление всех изменёнными **отказывает**.
|
||||
Доказательство: тест на чистой функции с `committed: {}`.
|
||||
- **AC7**. Штатная приёмка не изменилась: один изменённый кадр из десяти,
|
||||
объявленный `--expect-change`, принимается, девять свидетелей засчитаны.
|
||||
Доказательство: существующие тесты `test/docs-acceptance.test.mjs` зелёные
|
||||
без правок их утверждений.
|
||||
- **AC8**. `--no-witnesses --reason="…"` остаётся единственным законным обходом
|
||||
и по-прежнему требует непустой причины, которая попадает в манифест.
|
||||
Доказательство: существующий тест обхода зелёный.
|
||||
- **AC1.** При подтверждении, которое резолвится через `setTimeout(..., 0)`,
|
||||
`await window.__card._deletePhysicalSelection()` возвращает управление только
|
||||
после полного удаления draft. Доказательство: `demo/smoke_free_walls.mjs`.
|
||||
- **AC2.** Тот же смок умеет падать: мутант, который снова отбрасывает промис
|
||||
`_deleteDraftWhole`, получает
|
||||
`deleteOnDraftRemovesWholeOutline: false`. Доказательство:
|
||||
`scripts/mutation-gate.mjs`.
|
||||
- **AC3.** При задержанном решении `false` ожидание завершается, draft остаётся,
|
||||
новая транзакция истории не появляется. Доказательство:
|
||||
`demo/smoke_free_walls.mjs`.
|
||||
- **AC4.** Удаление partition и column, отсутствие выбора и ветка partition с
|
||||
проёмами сохраняют прежний результат, но возвращают `Promise<void>`.
|
||||
Доказательство: существующие и дополненные проверки
|
||||
`demo/smoke_free_walls.mjs`, плюс `tsc --noEmit` для типов.
|
||||
- **AC5.** Возврат действия не теряется в `runContext`, обоих
|
||||
`_runEditorContext` и card-фасаде `_deletePhysicalSelection`; устаревший
|
||||
context по-прежнему не запускает действие. Доказательство: unit-тест
|
||||
`EditorSecondaryController.runContext` и `tsc --noEmit`.
|
||||
- **AC6.** Визуальное поведение кнопок не меняется и UI не ждёт промис.
|
||||
Доказательство: существующие смоки редактора зелёные без изменения их
|
||||
пользовательских утверждений; ревью кода обработчиков `@click`.
|
||||
|
||||
## План автотестов
|
||||
|
||||
**Смок** (`demo/smoke_free_walls.mjs`, правка существующего):
|
||||
|
||||
1. Стаб подтверждения переводится на макрозадачу (AC1, AC3).
|
||||
2. Ожидание становится настоящим — `await` на возвращённом промисе.
|
||||
3. Проверка отказа подтверждения добавляется рядом (AC3).
|
||||
|
||||
**Тесты** (`test/docs-acceptance.test.mjs`, дополнение):
|
||||
|
||||
4. `committed: {}` + все объявлены → отказ с упоминанием свидетелей (AC6).
|
||||
5. Порог при полном наборе равен прежнему — 1 из 10 (AC7).
|
||||
|
||||
**Мутанты** (`scripts/mutation-gate.mjs`):
|
||||
|
||||
- `draft-delete-drops-the-promise`: вернуть `_deletePhysicalSelection` к `void`
|
||||
без проброса → смок AC1 краснеет;
|
||||
- `witness-floor-counts-survivors`: вернуть счёт порога от числа уцелевших
|
||||
кадров → тест AC6 краснеет.
|
||||
1. В `demo/smoke_free_walls.mjs` заменить мгновенный стаб подтверждения на
|
||||
управляемый задержанный стаб и отдельно проверить подтверждение и отмену.
|
||||
2. Сохранить прямой вызов публичного тестового фасада `window.__card`, чтобы
|
||||
проверка проходила через `src/houseplan-card.ts`, а не только через runtime.
|
||||
3. Добавить unit-проверку `runContext`: актуальный context возвращает как
|
||||
синхронное значение, так и промис; устаревший context не вызывает action и
|
||||
возвращает `undefined`.
|
||||
4. Добавить мутант `draft-delete-drops-the-promise`, который восстанавливает
|
||||
потерю промиса и обязан краснить целевой смок.
|
||||
5. Прогнать `npx tsc --noEmit`, `npm test`, `npm run build`, выбранные
|
||||
`smoke-select` смоки и `node scripts/no-new-any.mjs`.
|
||||
|
||||
## Риски
|
||||
|
||||
- **Возврат промиса меняет типы у трёх функций сразу.** `runContext` вызывается
|
||||
не только из удаления. Смягчение: возвращаемый тип обобщается, существующие
|
||||
вызывающие результат игнорируют — для них ничего не меняется; `typecheck`
|
||||
доказывает, что никто не сломан.
|
||||
- **Ожидание промиса в смоке может замаскировать гонку, а не убрать её.**
|
||||
Смягчение: AC2 требует, чтобы на неисправленном коде смок краснел — то есть
|
||||
доказывает, что проверка меряет именно ожидание.
|
||||
- **Ужесточение порога сломает первую приёмку на чистом клоне.** Смягчение:
|
||||
следствие названо явно (`--no-witnesses --reason`), сообщение об отказе уже
|
||||
подсказывает эту дорогу дословно.
|
||||
- **Изменение порога затрагивает golden.** `goldenWitnessFloor` намеренно
|
||||
одинаков с `docsWitnessFloor` (комментарий `:33`). Смягчение: правится
|
||||
источник числа, а не формула; если правка формулы окажется неизбежной — она
|
||||
зеркалится в golden в той же задаче, иначе два набора получат разные правила,
|
||||
чего комментарий и запрещает.
|
||||
- Обобщение `runContext` затрагивает все его вызовы. Смягчение: для действий,
|
||||
возвращающих `void`, выведенный контракт остаётся `void | undefined`; типы и
|
||||
существующие смоки доказывают отсутствие изменения поведения.
|
||||
- Асинхронный `_deletePhysicalSelection` превращает синхронные исключения его
|
||||
тела в отклонение промиса. UI промис игнорирует, поэтому не расширяем скоуп до
|
||||
общего error-handling; #404 отвечает за наблюдаемость отклонений в смоках.
|
||||
- Слишком быстрый стаб снова скроет дефект. Поэтому AC1 и AC2 требуют
|
||||
макрозадачу и отрицательный мутант, а не только зелёный штатный прогон.
|
||||
|
||||
## Откат
|
||||
|
||||
Обе половины локальны: тип возвращаемого значения у трёх функций и один
|
||||
аргумент в вычислении порога. Данные пользователя и формат манифеста не
|
||||
затрагиваются.
|
||||
Откат локален: вернуть синхронные сигнатуры и прежний стаб смока. Данные и
|
||||
конфиг пользователя не требуют обратной миграции.
|
||||
|
||||
## Release-артефакты
|
||||
|
||||
- `docs/CHANGELOG.md` / `docs/CHANGELOG.ru.md`: **не требуется**
|
||||
(User-Visible: no) — видимого поведения задача не меняет.
|
||||
- Скриншоты не меняются.
|
||||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`: не требуются
|
||||
(`User-Visible: no`), так как UI и пользовательский контракт не меняются;
|
||||
- документация пользователя, golden, скриншоты, performance- и
|
||||
security-артефакты не меняются.
|
||||
|
||||
## Принятые технические предположения
|
||||
|
||||
- Для проброса результата используется generic `T`, а не перечисление
|
||||
`void | Promise<void>`: это сохраняет точный тип каждого action и не создаёт
|
||||
отдельного async-контракта у нейтрального context-гварда.
|
||||
- `_deletePhysicalSelection` имеет единый `Promise<void>` во всех ветках, а не
|
||||
union `void | Promise<void>`, чтобы вызывающий всегда мог безопасно ждать
|
||||
завершения без анализа выбранного типа геометрии.
|
||||
- Ссылки на строки ориентировочны; исполнение определяется символами и
|
||||
контрактами из таблицы, если `dev` сдвинет номера строк перед реализацией.
|
||||
|
||||
Reference in New Issue
Block a user