feat(adoption): one owner for config/layout identity and the adoption sequence

`src/config-adoption.ts` owns config/layout with revision and fingerprint;
the host keeps `_serverCfg`/`_cfgRev`/`_layout`/`_layoutRev` as delegates.
All seven authoritative adoptions go through `adoptAuthoritativeGated`
(backdrop readiness → continuity → adopt → profile tail); the post-write
paths (space/delete ×2, optimize_undo, import/apply) gain the gate and take
revisions from the re-read bodies. `rollbackOptimistic` moves to the owner;
plan-optimize, space copy and the vacuum writers stop assigning identity.

Issue: #500
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-10 11:41:16 +00:00
parent a44fbd373d
commit 7194fb2df0
57 changed files with 2082 additions and 1063 deletions
+7 -14
View File
@@ -1,6 +1,6 @@
import type { ConfigAdoption } from './config-adoption';
import type { DeviceLayout } from './device-position-history';
import type { ServerConfig } from './types';
import { contentFingerprint } from './visual-continuity';
interface Clearable { clear(): void }
interface Flushable { pending(): boolean; flush(): void }
@@ -9,11 +9,10 @@ export interface PlanOptimizeWriteHost {
hass: { callWS(message: Record<string, unknown>): Promise<Record<string, unknown>> };
_saveConfigDebounced: Flushable;
_writeChain: Promise<void>;
_cfgRev: number;
_cfgContentFingerprint: string;
_layoutRev: number;
_serverCfg: ServerConfig | null;
_layout: DeviceLayout;
readonly _cfgRev: number;
readonly _layoutRev: number;
/** #500: identity owner — bodies and revisions of the pair land through it. */
readonly _adoption: ConfigAdoption;
_geometryHistory: Clearable;
_devicePositionHistory: Clearable;
_cancelDeviceDrag(): boolean;
@@ -30,9 +29,6 @@ export interface PlanOptimizeWriteHost {
requestUpdate(): void;
}
const finiteRevision = (value: unknown, fallback: number): number =>
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
/** The one adoption path shared by the Optimize dialog and Copy space. */
export async function commitPlanOptimization(
host: PlanOptimizeWriteHost,
@@ -48,14 +44,11 @@ export async function commitPlanOptimization(
expected_config_rev: host._cfgRev,
expected_layout_rev: host._layoutRev,
});
host._serverCfg = config;
host._cfgContentFingerprint = contentFingerprint(config);
host._layout = layout;
// Both bodies with both revisions from the reply that accepted them (#500 I2).
host._adoption.acceptPairWrite(config, layout, response);
host._geometryHistory.clear();
host._cancelDeviceDrag();
host._devicePositionHistory.clear();
host._cfgRev = finiteRevision(response.config_rev, host._cfgRev + 1);
host._layoutRev = finiteRevision(response.layout_rev, host._layoutRev + 1);
host._canOptimizeUndo = response.can_undo === true;
host._undoKind = response.can_undo === true ? 'optimize' : null;
host._dirtyPos.clear();