`space/delete` (both runtimes), Optimize Undo and Import apply now treat `asset-wait` like every reload path: nothing was adopted, so no toast, no space switch, no history/undo reset — the dialog is released and the scheduled reload owns the rest. Unit and smoke cover the refused branch for all four paths; the spec's reactivity risk row states the real mechanism. Issue: #500 User-Visible: no
36 KiB
#500 — Одна граница владения config/adoption
- Issue: https://github.com/Matysh/houseplan-card/issues/500
- Тип / приоритет: tech-debt / P3
- Статус ТЗ: готово к ревью (r3 — учтены High-1/High-2 r1 и High-3 r2)
- Трек: полный; §5 не проходит по сложности и риску (7/10), числу поверхностей (7 модулей) и state-контракту revision+fingerprint, который читают все feature-runtime и multi-client сценарии
- Оценка: пользовательская ценность 5/10; ценность для разработки 8/10; сложность 7/10; риск 7/10
- Класс: A (
src/**) — по прямому указанию владельца 2026-09-10 - Связано: #490 (lost-ACK recovery — четвёртый вызывающий seam), #493 (lifecycle generation панели — вторая граница из issue, здесь не трогается), #491/#495 (backend-транзакции — не трогаются), #34 → #425 (umbrella не возобновляется), #314 (rollback отклонённой записи), #368 (revision-less write)
1. Пользовательский сценарий
Пользователь этого не наблюдает. Персона, ради которой существует граница, —
администратор дома, который правит план с двух клиентов (десктоп и планшет),
и домочадцы, чей View получает чужие правки живьём. Момент — любой приход
authoritative конфига в карточку: первая загрузка, реакция на config_updated,
recovery после потерянного ответа, удаление пространства, Optimize, копия
пространства, тёплый старт из кэша.
2. До и после
До: идентичность конфига (rev, fingerprint) и его тело хранятся в полях
хоста, которые пишут шесть модулей в шестнадцати местах; каждый новый путь
adoption сам вспоминает, что перед заменой структуры надо подготовить фон,
начать continuity-кандидата, а после — пересобрать устройства и кэш. Один из
четырёх существующих путей (space/delete) этого не делает и переписывает
ревизию из другого ответа.
После: ревизии, fingerprint и приём ответов сервера принадлежат одному типизированному модулю; все четыре пути adoption проходят одну и ту же последовательность; прямая запись идентичности вне модуля запрещена lint-тестом. Для пользователя ничего не меняется, кроме одного: удаление пространства при параллельной смене фона другим клиентом больше не даёт переходного пустого фона.
3. Проблема
Материал: origin/dev = a824acc1.
- Тройка
(_serverCfg, _cfgRev, _cfgContentFingerprint)и пара(_layout, _layoutRev, _layoutContentFingerprint)— поля хоста без владельца._serverCfg =— 18 присваиваний в 8 модулях (houseplan-card.ts6,houseplan-editor-runtime.ts6,plan-optimize-write.ts,serialized-write-queue.ts,space-copy-runtime.ts,summary-panel-runtime-loaded.ts,vacuum-calibration-write.ts:108,editors/vacuum-maps-section.ts:107);_cfgRev =— 6 в 4 модулях;_layoutRev =— 7 в 4; fingerprint config — 10 мест в 5 модулях, включая_cacheSnapshot(), который его попутно пересчитывает; fingerprint layout — 5 мест. - Seam
_adoptStructuralResponses(houseplan-card.ts:4231) имеет семь вызывающих в двух профилях. Reload — три:_loadFromServer(4315),_reloadConfigOnly(4496), summary recovery (summary-panel-runtime-loaded.ts:611); пред-условия (сравнение fingerprint →_signer.prepareImage→_beginContinuityCandidate) и пост-шаги (_syncDecorAssets,_adoptInitialSpace,_resumePendingNavMode,_cacheSnapshot,_restoreZoom,_regSignature = '',_maybeRebuildDevices) продублированы во всех трёх. Post-write — четыре, каждый после собственной парной записи перечитываетconfig/get+layout/get:space/deleteвhouseplan-onboarding-runtime.ts:476и его байт-в-байт дубликат вhouseplan-editor-runtime.ts:8693,plan/optimize_undo(houseplan-editor-runtime.ts:9577),import/apply(houseplan-editor-runtime.ts:9736). Ни один из четырёх не проходит гейт фона и continuity перед сменой структуры. - Оба
space/deleteпосле adoption перезаписывают_cfgRev/_layoutRevревизиями из ответаspace/delete(houseplan-onboarding-runtime.ts:477–478,houseplan-editor-runtime.ts:8694–8695) — второй писатель той же ревизии после adoption. Тот же класс дефекта, что M1 r1 в #490, в двух копиях. SummaryPanelHostэкспортирует восемь внутренних шагов adoption как публичный контракт (summary-panel-host.ts:84–94): любой следующий feature-runtime получит тот же набор ножей и повторит #490 M1.houseplan-card.ts— 13 699 строк при потолке 13 700 (core-file-budget).
4. Скоуп
- Новый модуль
src/config-adoption.ts— владелец идентичности config и layout и единственная точка превращения ответа сервера в локальное состояние (§6.1–6.2). - Единая гейт-последовательность adoption для всех семи путей в двух
профилях (§6.3);
_adoptStructuralResponsesстановится внутренней деталью модуля. - Хост оставляет
_serverCfg,_cfgRev,_cfgContentFingerprint,_layout,_layoutRev,_layoutContentFingerprintкак делегаты к модулю (чтение — всюду как раньше; запись — только через модуль). - Сужение
SummaryPanelHostи host-контракта onboarding-runtime до одного метода adoption вместо восьми шагов. - Lint-тест «идентичность пишет только модуль» по образцу
single-source-numbers; allowlist локальных замен_serverCfg/_layout— зафиксированный список, только вниз. Vacuum-писатели (vacuum-calibration-write.ts,editors/vacuum-maps-section.ts) переходят наbeginOptimistic/stageLocalConfig/rollbackOptimisticмодуля вместе с переносомrollbackOptimisticи в allowlist не входят. - Regression witness
space/delete(оба входа) при параллельной сменеplan_url— до переноса; тесты модуля на синтетических ответах; мутанты на защиты. docs/ARCHITECTURE.md— абзац о границе;core-file-budget— храповик вниз.
5. Не входит
- Feature lifecycle registry (вторая граница из issue): #493 дал панели рабочий образец, открытого инцидента нет; отдельная задача при появлении второго потребителя.
- Иммутабельность
_serverCfgи перевод локальных правок редактора ({ ...cfg, settings }, геометрические жесты in-place) на команды модуля — это umbrella-rewrite (#34/#425). Локальные замены остаются в editor runtime под allowlist. - Backend:
websocket_api.py, пары Optimize/Undo/Import (#491/#495). - Изменение формата кэша localStorage
LS_CFG, протокола WS, схемы конфига. - Поведение
_sendConfigCandidateпри отсутствииrevв ответе (rev + 1) — сохраняется как есть, централизуется и помечается долгом (§15). - Любая правка UX, i18n, настроек, golden.
6. Контракт границы
6.1 Состояние и владение
// src/config-adoption.ts
export interface StructuralIdentity {
config: ServerConfig | null; // тело; ссылка та же, что видит редактор
configRev: number;
configFingerprint: string; // fingerprint принятого/отправленного тела
layout: DeviceLayout;
layoutRev: number;
layoutFingerprint: string;
}
Инварианты:
- I1.
configRevиlayoutRevменяются только тремя способами: adoption authoritative ответа, приём ответа собственной записи, восстановление из кэша. Никакой другой код их не присваивает (AC1). - I2. Ревизия принимается только вместе с телом, к которому относится:
adoptAuthoritative(cfgResp)берётcfgResp.rev;acceptConfigWrite(candidate, response)берётresponse.revдляcandidate. Ревизия одного ответа не прикладывается к телу другого (закрывает п.3 §3; AC4). - I3. Одинаковый fingerprint при adoption — эхо: тело, история геометрии, drag и viewport не трогаются, ревизия обновляется (как сегодня, AC3).
- I4. Ссылочная идентичность: модуль хранит тот объект, который получил,
без клонирования — редактор сравнивает
host._serverCfg === cfg(houseplan-editor-runtime.ts:8099) и мутирует тело in-place.
6.2 API модуля
export class ConfigAdoption implements StructuralIdentity {
/** Чистое сравнение без мутации: что изменится, если принять ответ. */
compare(cfgResp?: AuthoritativeConfigResponse, layResp?: LayoutResponse):
{ configChanged: boolean; layoutChanged: boolean; candidateConfig: ServerConfig | null };
/** Принять authoritative ответ(ы); побочные эффекты хоста — через callbacks. */
adoptAuthoritative(cfgResp, layResp?, layoutOverride?): { configChanged; layoutChanged };
/** После собственного config/set: fingerprint = candidate, rev = ответ. */
acceptConfigWrite(candidate: ServerConfig, response: { rev?: number }): void;
/** После layout/set или собственного layout-события. */
acceptLayoutWrite(layout: DeviceLayout, response: { rev?: number }): void;
/** После парной записи (plan/optimize, copy, import): оба тела + обе ревизии. */
acceptPairWrite(config, layout, response: { config_rev?; layout_rev? }): void;
/** Локальная замена тела редактором до записи; идентичность не меняется. */
stageLocalConfig(config: ServerConfig): void;
stageLocalLayout(layout: DeviceLayout): void;
/** Тёплый старт и кэш: формат LS_CFG неизменен. */
restoreCached(snapshot: CachedSnapshot): boolean;
snapshot(): CachedSnapshot;
/** Optimistic write (#314): захват и точный откат по rev+fingerprint. */
beginOptimistic(previous, attempted): OptimisticAttempt<ServerConfig>;
rollbackOptimistic(attempt): boolean;
}
Побочные эффекты хоста при смене структуры (очистка _geometryHistory,
_devicePositionHistory, _cancelDeviceDrag, _pendingPhysicalWrites,
_clearRoomFocus, _cancelCameraTransition, _clearGeometryGesture,
_seedDecorStyle, virtual lights, capabilities, _continuity.note) остаются в
хосте и вызываются модулем через типизированный AdoptionHost-интерфейс в
том же порядке, что сегодня в _adoptStructuralResponses — модуль владеет
идентичностью и последовательностью, не логикой этих подсистем.
6.3 Единая гейт-последовательность
export async function adoptAuthoritativeGated(host: AdoptionHost, input: {
cfgResp?: AuthoritativeConfigResponse; layResp?: LayoutResponse;
reason: 'structural-response' | 'config-reload' | 'summary-recovery'
| 'space-delete' | 'optimize-undo' | 'import-apply';
profile: 'reload' | 'post-write';
}): Promise<'adopted' | 'asset-wait'>;
Шаги 1–4 общие для обоих профилей, шаг 5 — по профилю. Профиль reload —
ровно как сегодня в _reloadConfigOnly/summary recovery:
compare→ если структура не изменилась, шаги 2–3 пропускаются.await host._signer.prepareImage(hass, host._candidateBackdrop(candidate)); при отказе —_continuity.note('asset-failed'),_scheduleLoadRetry(true), возврат'asset-wait', ничего не адоптировано.- Если
_continuity.hasCompleteFrame && state === 'steady'—_beginContinuityCandidate(reason, true). adoptAuthoritative(...).- Пост-шаги профиля
reload:_syncDecorAssets(candidate)(fire-and-forget),_adoptInitialSpace(_model, true),_resumePendingNavMode(),_cacheSnapshot(),_restoreZoom()если видимое пространство сменилось,_regSignature = '',_maybeRebuildDevices(). Профильpost-writeпост-шагов не имеет: последовательность заканчивается шагом 4, а хвост каждого post-write вызывающего (очистки,_cfgEpoch++, выбор пространства,_cacheSnapshot, toast) остаётся в вызывающем без изменений — включая_adoptInitialSpace(_model, true)в ветке_hasFixedFloorуimport/apply(houseplan-editor-runtime.ts:9757) и_commitSpace(...)у обоихspace/deleteи уimport/applyбез фиксированного этажа. Общий хвост у четырёх путей разный по составу и порядку; вычленять из него «общую часть» значило бы менять порядок побочных эффектов, чего это ТЗ не делает.
Особенности вызывающих остаются снаружи и не размножаются:
_loadFromServer — флаги _connectionWasLost, _serverStorage, warm-viewport
логика _warmVpArmed, _loadOk, подписки; _reloadConfigOnly — continuity
кандидат 'config-reload' до запроса и ветка _cfgWriting; summary —
confirmedSummaryPanelWriteRecovery и генерация; space/delete —
_commitSpace при удалении текущего пространства и toast; optimize_undo —
сброс _canOptimizeUndo/_undoKind, очистки историй, _cfgEpoch++, toast;
import/apply — очистки _dirtyPos/_sentPos/_defPos, снапшотов
устройств, _signer.invalidate + _resign, _cfgEpoch++, выбор
пространства (_adoptInitialSpace при _hasFixedFloor, иначе
_commitSpace(nextSpace)), _cacheSnapshot. Две копии
space/delete обязаны вызывать одну последовательность; слияние их в один
метод хоста допустимо, но не требуется этим ТЗ.
Единственное намеренное изменение поведения — гейт на post-write путях.
Все четыре post-write пути получают шаги 2–3 (готовность фона и continuity
перед сменой структуры), а оба space/delete перестают перезаписывать
_cfgRev/_layoutRev ревизиями из ответа space/delete — обе ревизии
берутся из адоптированных config/get и layout/get (I2). Backend отдаёт в
space/delete те же значения, что затем вернёт get, поэтому в штатном
случае числа совпадают; расходятся они только при параллельной записи между
delete и get — и тогда прежний код ставил ревизию старше тела, а
следующая запись получала ложный conflict. Для optimize_undo и
import/apply изменение сводится к ожиданию prepareImage при смене
plan_url (Undo/Import могут вернуть другой фон) — сегодня они адоптируют
структуру до подписания фона, тот же переходный дефект, что M1 #490.
6.4 Сужение публичных контрактов
SummaryPanelHost теряет _beginContinuityCandidate, _adoptStructuralResponses,
_syncDecorAssets, _adoptInitialSpace, _resumePendingNavMode,
_restoreZoom, _cacheSnapshot, _candidateBackdrop, _scheduleLoadRetry,
_signer, _continuity и получает один _adoptAuthoritative(cfgResp, reason).
Host-интерфейсы onboarding-runtime и editor-runtime
(houseplan-editor-runtime.ts:842) теряют _adoptStructuralResponses и
получают тот же метод с профилем post-write. plan-optimize-write.ts и
space-copy-runtime.ts вместо пяти полей идентичности получают
acceptPairWrite. serialized-write-queue.ts сохраняет чистые
enqueueSerializedWrite/optimisticAttempt; rollbackOptimistic переезжает
в модуль (единственный писатель тела из отката), и вместе с ним на модуль
переходят все его вызывающие — editor runtime, vacuum-calibration-write.ts,
editors/vacuum-maps-section.ts.
7. UX
Нет изменений. Персоны View и киоск не затрагиваются; touch-контракт
(docs/TOUCH-SUPPORT.md) не меняется.
8. Модель данных и миграция
Нет. Формат LS_CFG (config, rev, config_fingerprint, layout, layout_rev, layout_fingerprint, virtual_lights) сохраняется побайтово; старый кэш без
fingerprint восстанавливается с пересчётом, как сегодня. Схема конфига, WS
и docs/CONFIG-COMPATIBILITY.md — без изменений.
9. i18n
Нет новых ключей; toast.cfg_reload_failed, toast.space_deleted,
toast.conflict используются как прежде.
10. Критерии приёмки
| # | AC | Чем доказан | Чем краснеет |
|---|---|---|---|
| AC1 | Идентичность пишет только модуль: в src/** вне config-adoption.ts нет присваиваний _cfgRev, _layoutRev, _cfgContentFingerprint, _layoutContentFingerprint (в т.ч. через host.); присваивания тел _serverCfg =/_layout = вне модуля — только локальные замены до записи (staging) в allowlist { 'houseplan-editor-runtime.ts': N, 'houseplan-card.ts': M } (размещение устройств), счётчики зафиксированы и могут только уменьшаться; во всех остальных файлах src/** — ноль, в том числе plan-optimize-write.ts, serialized-write-queue.ts, space-copy-runtime.ts, summary-panel-runtime-loaded.ts, houseplan-onboarding-runtime.ts, vacuum-calibration-write.ts, editors/vacuum-maps-section.ts (последние два — через stageLocalConfig/beginOptimistic/rollbackOptimistic модуля) |
unit test/config-adoption-ownership.test.mjs (regex по src/**, по образцу single-source-numbers) |
вернуть одно присваивание в plan-optimize-write.ts — тест красный; поднять счётчик allowlist без правки теста — красный |
| AC2 | Все семь путей adoption (§3 п.2) идут через adoptAuthoritativeGated; имя _adoptStructuralResponses не встречается в src/** вне config-adoption.ts; SummaryPanelHost, host-интерфейсы onboarding- и editor-runtime не содержат шагов §6.4 |
unit (grep по src/** на отсутствие имён вне модуля) + npm run typecheck |
оставить вызов _adoptStructuralResponses в любом из семи мест — красный |
| AC3 | Поведенческая нейтральность: таблица переходов модуля на синтетических ответах совпадает с сегодняшней — эхо (тот же fingerprint: тело и истории не тронуты, rev обновлён), смена config (истории очищены, fingerprint новый), только layout, ответ без rev (старый rev сохранён), layoutOverride, virtual lights, профиль post-write завершается шагом 4 и не выполняет пост-шагов профиля reload (хост-callbacks _adoptInitialSpace/_resumePendingNavMode/_restoreZoom/_syncDecorAssets не вызываются модулем); хвосты четырёх post-write вызывающих не меняются — сверка диффом на ревью; существующие свидетели зелёные без изменения ожиданий: demo/smoke_summary_panel.mjs (lost-ACK + параллельная правка + порядок prepare → adopt), demo/smoke_ws_resilience.mjs, test/render-invalidation.test.mjs, test/serialized-write-queue.test.mjs, test/summary-panel-runtime.test.mjs, test/config-store.test.mjs |
unit test/config-adoption.test.mjs + перечисленные смоки/юниты |
мутант «эхо тоже очищает историю» — unit красный; мутант «prepareImage не ждём» — smoke_summary_panel красный (recoveryPreparesBackdropBeforeAdoption, уже есть) |
| AC4 | Post-write пути: для каждого из четырёх (space/delete ×2 входа, optimize_undo, import/apply) при параллельной смене plan_url другим клиентом prepareImage вызывается до adoption; после обоих space/delete configRev/layoutRev равны ревизиям адоптированных config/get/layout/get, а не ответа delete |
demo/smoke_post_write_adoption.mjs на production bundle: четыре сценария с synthetic HA, моки prepareImage и порядок вызовов (по образцу recoveryPreparesBackdropBeforeAdoption), для space/delete — оба входа (onboarding и editor runtime) и проверка ревизий; unit модуля «ревизия не берётся из чужого ответа» (I2) |
вернуть host._cfgRev = response.config_rev в любой копии delete — смок и unit красные; убрать гейт в любом из четырёх — смок красный |
| AC5 | Тёплый старт: snapshot() → restoreCached() восстанавливает идентичность ровно (rev, оба fingerprint, layout, virtual lights); ключи LS_CFG неизменны; кэш без fingerprint восстанавливается с пересчётом |
unit test/config-adoption.test.mjs (round-trip и фикстура старого кэша) |
переименовать ключ или потерять layout_rev — красный |
| AC6 | Optimistic rollback (#314) через модуль: откат только при совпадении rev и fingerprint попытки; откат не меняет rev; конфликтный reload побеждает | test/serialized-write-queue.test.mjs перенесён/адаптирован + мутант |
мутант «откат без проверки rev» — красный |
| AC7 | Бюджеты: core-file-budget для houseplan-card.ts опущен до нового размера (храповик вниз); общий бюджет initial View (INITIAL_VIEW_GZIP_BUDGET, 301 066 Б) не поднят; если факт выходит за полосу потолка, потолок перецентрируется по практике #485/#506 — с датированной записью причины и измеренного факта в scripts/bundle-budget.mjs, а рост называется в хендоффе; новых WS-запросов на путях adoption нет (те же config/get/layout/get, что сегодня) |
test/core-file-budget.test.mjs, npm run bundle:budget, test/bundle-assets.test.mjs (полоса ±2000, запас ≥500 Б с обеих сторон); отсутствие новых callWS — ревью диффа |
поднять core-file-budget или общий бюджет — красный (по правилу тестов); перецентровка без записи — находка ревью |
| AC8 | Документация: docs/ARCHITECTURE.md описывает границу (владелец, три способа смены ревизии, единая последовательность); changelog не трогается (User-Visible: no) |
ревью кода | — |
11. План автотестов
test/config-adoption.test.mjs— таблица переходов AC3, round-trip AC5, I2/I4 (ссылка та же; ревизия не от чужого ответа), rollback AC6.test/config-adoption-ownership.test.mjs— lint AC1/AC2 поsrc/**.demo/smoke_post_write_adoption.mjs— AC4 на production bundle с synthetic HA: второй клиент меняетplan_urlмежду записью иget; четыре сценария (space/deleteиз onboarding и из editor runtime,optimize_undo,import/apply), порядокprepare → adopt, ревизии изget.- Существующие:
smoke_summary_panel,smoke_ws_resilience,render-invalidation,serialized-write-queue,summary-panel-runtime,config-store,core-file-budget— без изменения ожиданий, кроме адаптации импорта вserialized-write-queue.test.mjs. - Мутанты в
scripts/mutation-gate.mjs(§2.7, защиты в продуктовом коде):config-adoption-echo-clears-history,config-adoption-rev-from-foreign-response,config-adoption-rollback-ignores-rev,post-write-skips-asset-gate(снимает гейт вpost-writeпрофиле — смок AC4 красный на всех четырёх). - Гейт по диффу:
typecheck,test,build,bundle:budget,no-new-any; смоки поsmoke-selectдля_serverCfg/_cfgRev/_adoptStructuralResponses; golden/perf — предрелизный набор (диффом не задеты: рендер не меняется).
12. Риски и защита
| Риск | Защита |
|---|---|
Реактивность: _serverCfg и _layout — реактивные свойства Lit (static properties), и willUpdate держит на changed.has('_serverCfg') инвариант эпохи геометрии и инвалидацию render-lifecycle; замена тела внутри владельца минует аксессор |
Владелец сообщает о каждой замене ссылки тела хуком onBodyReplaced(field, previous), карточка подключает его к requestUpdate(field, previous) — ровно то, что делал аксессор; эхо и изменения только идентичности событие не порождают. Юнит на контракт в test/config-adoption.test.mjs; свидетель — smoke_room_resize (покраснел на первом кандидате именно из-за этого, r1 Validate) |
| Редактор сравнивает и мутирует тело по ссылке | I4 — без клонирования; unit на идентичность ссылки |
Скрытый порядок побочных эффектов в _adoptStructuralResponses |
Порядок переносится один в один, тест-таблица AC3 фиксирует наблюдаемые результаты; ревьюер сверяет диффом |
Ребейз-конфликты в houseplan-card.ts/houseplan-editor-runtime.ts (горячие файлы) |
Небольшие коммиты: (1) модуль + тесты, (2) хост-делегаты, (3) четыре пути, (4) сужение контрактов, (5) lint + бюджеты + docs; scripts/rebase-on-dev.mjs |
| Гейт на post-write путях — единственное изменение поведения | Собственный AC4 и смок на все четыре пути; зафиксировано в §6.3 как намеренное |
Профиль post-write случайно получит пост-шаги reload (_adoptInitialSpace, _restoreZoom) или хвост вызывающего изменится при переносе |
Профиль явный в сигнатуре и не имеет пост-шагов; таблица AC3 включает кейс «post-write завершается шагом 4»; хвосты вызывающих в диффе не меняются (ревью); смок AC4 проверяет сегодняшнее правило выбора пространства после Import: при _hasFixedFloor — _adoptInitialSpace, иначе целевое/прежнее пространство — а не его отсутствие |
| Windows-гейт владельца | Никаких новых spawn/путей; тесты — чистые Node |
13. Откат
Один revert коммитов ветки issue/500-config-adoption-boundary; данные,
кэш и протокол не менялись, откатывать нечего кроме кода. Флага Labs нет —
рефакторинг без пользовательской поверхности.
14. Release-артефакты
- Changelog RU/EN: нет (
User-Visible: noна всех коммитах). - Пользовательская документация: нет.
- Golden/скриншоты: нет (визуал не меняется).
docs/ARCHITECTURE.md: абзац о границе (AC8).- Performance: бюджеты по AC7 (
core-file-budgetвниз; потолок initial View перецентрирован с записью, общий бюджет не тронут), без новых замеров. - Security: нет.
15. Принято предположительно (техническое, менять свободно)
- Имя и место модуля —
src/config-adoption.ts, классConfigAdoption; хост держит один экземпляр_adoptionи делегаты-геттеры. - Побочные эффекты хоста при смене структуры остаются в хосте и вызываются
через
AdoptionHost— модуль владеет идентичностью и порядком, не подсистемами. Альтернатива «перенести очистки в модуль» отвергнута: это тянет за собой историю геометрии, камеру и жесты (umbrella). _sendConfigCandidateсохраняетrev ?? rev + 1; централизуется вacceptConfigWrite, помечается// TODO(#500-followup); отдельный issue после этой задачи — угаданная ревизия при ответе безrevпротиворечит I2, но менять её здесь значит менять поведение с backend-заглушками (демо).- Allowlist AC1:
houseplan-editor-runtime.ts(замены_serverCfg/_layoutдо записи) иhouseplan-card.ts(staging_layoutпри размещении устройств,applyDevicePlacement); начальные значения — фактические числа после переноса, только вниз. _reloadLayoutOnly(слияниеmineповерх ответа) используетacceptLayoutWrite(merged, resp)— семантика та же: тело слитое, ревизия ответа; отдельного «adopt layout with override» не заводится.- Lifecycle registry не начинается даже частично: post-write пути берут готовую последовательность, а не новый механизм регистрации.
- Отказ гейта на post-write пути (
asset-wait): вызывающий не выполняет хвост (тост, выбор пространства, очистки), освобождаетbusyсвоего диалога и выходит — как reload-пути; запланированный_scheduleLoadRetryадоптирует структуру позже. Сервер операцию уже выполнил, локальное состояние догоняет ретраем (ревью кода r1, M1). - Два профиля вместо одного: у
reloadпост-шаги общие и одинаковые в трёх местах, уpost-writeхвосты разные (Import выбирает пространство по_hasFixedFloor, Delete —_commitSpace, Undo — сброс undo-состояния), поэтомуpost-write= только гейт + adoption, хвосты остаются на месте. Склеивать профили значило бы менять поведение Import/Undo/Delete (выбор пространства, viewport). Третьего профиля не заводится; новый вызывающий с иным набором шагов — повод пересмотреть границу, а не добавить ветку.