@pygmalionjs/pygmalion 0.6.26 → 0.6.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist-lib/{FrozenRoutePreview-BQDZOtJG.js → FrozenRoutePreview-Xm7o3urC.js} +3287 -2670
  2. package/dist-lib/pygmalion.js +9923 -9530
  3. package/dist-lib/style.css +1 -1
  4. package/dist-lib/testing.js +1 -1
  5. package/dist-lib/types/canvas/SectionBoxes.d.ts +2 -3
  6. package/dist-lib/types/canvas/useFlowSession.d.ts +3 -1
  7. package/dist-lib/types/editor/designImport.d.ts +42 -3
  8. package/dist-lib/types/editor/flowSessionScheduler.d.ts +54 -5
  9. package/dist-lib/types/editor/flowSessions.d.ts +49 -9
  10. package/dist-lib/types/editor/frameLabels.d.ts +9 -0
  11. package/dist-lib/types/editor/geometryStability.d.ts +122 -0
  12. package/dist-lib/types/editor/heldPseudoStates.d.ts +22 -0
  13. package/dist-lib/types/editor/interactiveSessionSurface.d.ts +13 -0
  14. package/dist-lib/types/editor/interactiveStates.d.ts +26 -14
  15. package/dist-lib/types/editor/previewBootstrap.d.ts +7 -1
  16. package/dist-lib/types/editor/projectRuntime.d.ts +5 -0
  17. package/dist-lib/types/editor/routePreview.d.ts +5 -0
  18. package/dist-lib/types/editor/stateSwitchMetrics.d.ts +150 -0
  19. package/dist-lib/types/editor/store.d.ts +57 -2
  20. package/dist-lib/types/editor/storyboardDiscovery.d.ts +1 -0
  21. package/dist-lib/types/editor/variantPrefetch.d.ts +69 -0
  22. package/dist-lib/types/lib.d.ts +67 -1
  23. package/docs/screen-state-contract.md +58 -9
  24. package/node/dev-mirror.mjs +80 -8
  25. package/node/preview-artifact-plugin.mjs +14 -1
  26. package/node/preview-artifact-store.mjs +39 -3
  27. package/node/storyboard-capture-runtime.mjs +40 -0
  28. package/node/vite.mjs +1 -0
  29. package/package.json +1 -1
@@ -0,0 +1,122 @@
1
+ /** Final backstop: a document that never settles is serialized best-effort. */
2
+ export declare const GEOMETRY_STABILITY_TIMEOUT_MS = 3000;
3
+ /**
4
+ * Secondary cap once perpetual motion is being ignored: when every rect that
5
+ * still moves belongs to an ignored element but structural churn (element
6
+ * count, sampled identity) keeps the strict check from passing, the document
7
+ * will not settle by waiting longer.
8
+ */
9
+ export declare const PERPETUAL_RESIDUE_TIMEOUT_MS = 1000;
10
+ /**
11
+ * An element that moved on every consecutive sample pair for this long is
12
+ * treated as perpetually driven even when no animation claims it. One-shot
13
+ * springs settle well inside this window, so they are never reclassified —
14
+ * and a mover that pauses for even one pair either settles the wait outright
15
+ * or is disqualified from reclassification for good.
16
+ */
17
+ export declare const PERSISTENT_MOVER_MS = 1500;
18
+ /**
19
+ * Floor on observed pairs before the persistent-mover rule may fire, so a
20
+ * slow sampling cadence cannot classify from a handful of observations.
21
+ */
22
+ export declare const PERSISTENT_MOVER_MIN_PAIRS = 10;
23
+ /** How many elements a geometry sample inspects, from the top of the body. */
24
+ export declare const GEOMETRY_SAMPLE_LIMIT = 160;
25
+ /** One sampled element: an opaque identity plus its rounded geometry. */
26
+ export interface GeometryElementSample {
27
+ /** Stable identity across samples — in the DOM, the element itself. */
28
+ key: unknown;
29
+ /** Rounded rect signature, e.g. "12,40,300,24". */
30
+ rect: string;
31
+ /**
32
+ * True when an effectively-infinite animation drives this element or an
33
+ * ancestor, so its movement must not block convergence.
34
+ */
35
+ perpetual: boolean;
36
+ }
37
+ /** A point-in-time view of the document's geometry. */
38
+ export interface GeometrySample {
39
+ /** Total rendered element count — a structure signal beyond the sampled window. */
40
+ elementCount: number;
41
+ elements: readonly GeometryElementSample[];
42
+ /** Sample timestamp from the caller's clock, in milliseconds. */
43
+ at: number;
44
+ }
45
+ export type GeometrySettleReason =
46
+ /** Everything held still for one full sample pair — the strict criterion. */
47
+ 'stable'
48
+ /** Everything held still except elements owned by perpetual motion. */
49
+ | 'stable-ignoring-perpetual'
50
+ /** Ignored motion plus structural churn: settled at the lower cap. */
51
+ | 'perpetual-residue'
52
+ /** The final backstop: serialized best-effort. */
53
+ | 'timeout';
54
+ export interface GeometryVerdict {
55
+ settled: boolean;
56
+ reason?: GeometrySettleReason;
57
+ }
58
+ export interface GeometryStabilityJudgeOptions {
59
+ timeoutMs?: number;
60
+ perpetualResidueTimeoutMs?: number;
61
+ persistentMoverMs?: number;
62
+ persistentMoverMinPairs?: number;
63
+ }
64
+ export interface GeometryStabilityJudge {
65
+ /** Consumes the next sample and reports whether the wait may end. */
66
+ next(sample: GeometrySample): GeometryVerdict;
67
+ }
68
+ /**
69
+ * Creates the convergence policy for one stability wait. The strict
70
+ * criterion is unchanged from the original hash comparison: one sample pair
71
+ * with identical element count, identical sampled identities in order, and
72
+ * identical rects. Movement owned by perpetual animations is exempt from
73
+ * the rect comparison; structure changes never are (they fall through to
74
+ * the residue cap or the timeout).
75
+ */
76
+ export declare function createGeometryStabilityJudge(options?: GeometryStabilityJudgeOptions): GeometryStabilityJudge;
77
+ export interface AnimationEffectLike {
78
+ target?: unknown;
79
+ getTiming?: () => {
80
+ iterations?: number;
81
+ };
82
+ getComputedTiming?: () => {
83
+ endTime?: unknown;
84
+ };
85
+ }
86
+ export interface AnimationLike {
87
+ effect?: AnimationEffectLike | null;
88
+ }
89
+ interface RectLike {
90
+ left: number;
91
+ top: number;
92
+ width: number;
93
+ height: number;
94
+ }
95
+ export interface GeometryElementLike {
96
+ nodeType: number;
97
+ getBoundingClientRect(): RectLike;
98
+ contains?(other: GeometryElementLike): boolean;
99
+ }
100
+ export interface GeometryDocumentLike {
101
+ body?: {
102
+ querySelectorAll(selectors: string): ArrayLike<GeometryElementLike>;
103
+ } | null;
104
+ getAnimations?(options?: {
105
+ subtree?: boolean;
106
+ }): AnimationLike[];
107
+ }
108
+ /** True for an animation that will never finish on its own. */
109
+ export declare function isEffectivelyPerpetualAnimation(animation: AnimationLike): boolean;
110
+ /**
111
+ * Elements owned by animations that will never finish on their own. CSS
112
+ * animations with an infinite iteration count and Web Animations API loops
113
+ * both surface here; one-shot animations never do.
114
+ */
115
+ export declare function collectPerpetualMotionRoots(doc: GeometryDocumentLike): GeometryElementLike[];
116
+ /**
117
+ * Reads one geometry sample from a document: rounded rects of the first
118
+ * `limit` elements, each flagged when a perpetual animation drives it or an
119
+ * ancestor (a transform loop on a container moves every descendant rect).
120
+ */
121
+ export declare function sampleGeometry(doc: GeometryDocumentLike, at: number, limit?: number): GeometrySample;
122
+ export {};
@@ -2,6 +2,28 @@ import type { DesignScreenInteraction } from './designImport';
2
2
  export type HeldPseudoState = 'hover' | 'focus' | 'focus-visible' | 'active';
3
3
  /** The pseudo gestures still held after the complete recipe has run. */
4
4
  export declare function heldPseudoInteractionsFromRecipe(interactions: readonly DesignScreenInteraction[]): DesignScreenInteraction[];
5
+ /** True when the interaction is a gesture the frozen preview can hold via CSS. */
6
+ export declare function isHeldPseudoInteraction(interaction: DesignScreenInteraction): boolean;
7
+ /**
8
+ * Why these steps cannot be held on the given frozen document, or an empty
9
+ * list when client-side holding reproduces them faithfully.
10
+ *
11
+ * A target stamped with script-driven pseudo events (PSEUDO_EVENT_ATTRIBUTE)
12
+ * answers the gesture in JavaScript, which a frozen document cannot replay —
13
+ * such steps need the ordinary walk. A missing non-optional target also
14
+ * blocks: the live app may still render it, so the walk keeps that chance.
15
+ */
16
+ export declare function heldPseudoHoldBlockers(root: ParentNode, steps: readonly DesignScreenInteraction[]): string[];
17
+ /**
18
+ * The recipe a preview must hold: the page's own interactions plus any
19
+ * presentation-layer held gestures (the pseudo fast path). The held steps
20
+ * come last, so they override the recipe's terminal gestures exactly as a
21
+ * rewritten recipe would.
22
+ */
23
+ export declare function presentationInteractions(page: {
24
+ interactions?: readonly DesignScreenInteraction[];
25
+ heldPseudoInteractions?: readonly DesignScreenInteraction[];
26
+ }): readonly DesignScreenInteraction[] | undefined;
5
27
  export declare function heldPseudoStatesFromInteractions(interactions: readonly DesignScreenInteraction[]): HeldPseudoState[];
6
28
  /**
7
29
  * Converts pseudo-class selectors into stable attribute selectors while
@@ -10,9 +10,22 @@ export interface InteractiveSessionSurface {
10
10
  iframe: HTMLIFrameElement;
11
11
  screenId: string;
12
12
  }
13
+ /** Stable presentation slot shared by sibling direct-state options. */
14
+ export declare function directInteractiveSessionSurfaceId(pageId: string, stateId: string): string;
15
+ /**
16
+ * Connects the store's synchronous option selection to the session scheduler
17
+ * without making the store import the scheduler that already depends on it.
18
+ */
19
+ export declare function setDirectInteractiveStateRequestHandler(handler: ((pageId: string) => void) | null): void;
20
+ /** Starts direct-state delivery in the same task as the right-panel click. */
21
+ export declare function requestDirectInteractiveState(pageId: string): boolean;
13
22
  /** Exposes a runner-owned iframe without transferring its ownership. */
14
23
  export declare function exposeInteractiveSessionSurface(screenId: string, iframe: HTMLIFrameElement): void;
15
24
  /** Releases a surface only when the caller still owns the registered iframe. */
16
25
  export declare function releaseInteractiveSessionSurface(screenId: string, iframe: HTMLIFrameElement): void;
26
+ /** Clears a presentation slot when its page returns to an authored state. */
27
+ export declare function clearInteractiveSessionSurface(screenId: string): void;
28
+ /** Active direct-state surfaces pin their warm React document against eviction. */
29
+ export declare function hasInteractiveSessionSurfaceIframe(iframe: HTMLIFrameElement): boolean;
17
30
  export declare function getInteractiveSessionSurface(screenId: string): InteractiveSessionSurface | null;
18
31
  export declare function subscribeInteractiveSessionSurface(screenId: string, listener: () => void): () => void;
@@ -1,4 +1,4 @@
1
- import type { DesignScreenInteraction, StoryboardEnvironment } from './designImport';
1
+ import type { DesignScreenInteraction, DesignSerializable, StoryboardEnvironment } from './designImport';
2
2
  import type { PageModel } from './store';
3
3
  /**
4
4
  * Screen-state axes — same-frame variations reproduced by a gesture or a boot
@@ -10,16 +10,23 @@ import type { PageModel } from './store';
10
10
  * catalog (where each would cost a captured frame) and puts them in the
11
11
  * right panel, where a designer flips between them.
12
12
  *
13
- * The steps are ordinary interactions, so a selected option simply extends
14
- * the frame's recipe. Everything downstream the preview cache key, the
15
- * live replay, the session walk — already keys off that recipe, which is
16
- * why no supply path needs to know this feature exists.
13
+ * An option either extends the ordinary interaction recipe, declares a boot
14
+ * condition, or supplies a host-owned desired-state value. Everything
15
+ * downstream keys off that complete reproduction contract.
17
16
  */
18
17
  export interface InteractiveStateOption {
19
18
  id: string;
20
19
  label: string;
21
20
  /** Absent or empty marks the base state — the screen as captured. */
22
21
  steps?: readonly DesignScreenInteraction[];
22
+ /**
23
+ * Host-owned value rendered directly after mount. Selecting the option puts
24
+ * it under the axis id in the frame's complete desired-state map.
25
+ *
26
+ * This is an alternative to both gesture replay and a boot environment: the
27
+ * host adapter translates the value into preview-only application state.
28
+ */
29
+ desiredState?: DesignSerializable;
23
30
  /**
24
31
  * Boot condition this option puts the frame under, for states no gesture can
25
32
  * reach: a failed request, an empty result, a stalled stream.
@@ -95,9 +102,9 @@ export interface InteractiveStateDef {
95
102
  * all, and declaring every combination as its own option is a power set.
96
103
  *
97
104
  * Selecting toggles the option in the set; the base option (the one with no
98
- * steps and no condition) clears it. Steps run and conditions merge in
99
- * declaration order, so the result is stable no matter what order they were
100
- * clicked in.
105
+ * steps, condition, or desired state) clears it. Steps run, conditions merge,
106
+ * and desired values collect in declaration order, so the result is stable no
107
+ * matter what order they were clicked in.
101
108
  *
102
109
  * Not compatible with `capturedWhen`: a per-frame base answers "which single
103
110
  * option is already on screen", which a combination has no answer to.
@@ -119,21 +126,26 @@ export type ScreenStateAxisDiagnostic = InteractiveStateDiagnostic;
119
126
  /**
120
127
  * Whether selecting this option asks supply for a different screen.
121
128
  *
122
- * Two ways to do that a gesture on the booted screen, or a boot condition —
123
- * and the base is the option that does neither: it reproduces the capture.
124
- * Judging the base by "no steps" alone would read an environment-only option as
125
- * a second base and refuse the declaration.
129
+ * Three ways do that: direct desired state, a gesture on the booted screen, or
130
+ * a boot condition. The base is the option that declares none of them and
131
+ * reproduces the capture. Judging the base by "no steps" alone would read a
132
+ * direct or environment-only option as a second base and refuse the declaration.
126
133
  */
127
134
  export declare function optionChangesSupply(option: InteractiveStateOption): boolean;
128
135
  export declare function validateInteractiveStates(defs: readonly InteractiveStateDef[]): InteractiveStateDiagnostic[];
129
136
  /** Validates both interaction and condition axes. */
130
137
  export declare const validateScreenStateAxes: typeof validateInteractiveStates;
138
+ /** Reads only the control-presence evidence needed by the view-only inspector. */
139
+ export declare function previewMarkupTestIds(markup: string | null | undefined): readonly string[];
131
140
  /**
132
141
  * Axes available on one frame. The test-id requirement is checked against
133
- * the imported tree, so an axis stays hidden until the frame actually shows
134
- * the control it drives a frame without a sidebar never offers to fold it.
142
+ * either the imported tree or the frozen preview's lightweight test-id index.
143
+ * View-only can therefore offer a real screen control without importing the
144
+ * whole layer tree, while a frame without that control still hides the axis.
135
145
  */
136
146
  export declare function interactiveStatesForPage(page: PageModel, defs: readonly InteractiveStateDef[]): InteractiveStateDef[];
147
+ /** The first axis whose options can render directly on a warm host document. */
148
+ export declare function directInteractiveStateAxisForPage(page: PageModel, defs?: readonly InteractiveStateDef[]): InteractiveStateDef | undefined;
137
149
  /** Preferred public name for axes applicable to one frame. */
138
150
  export declare const screenStateAxesForPage: typeof interactiveStatesForPage;
139
151
  /**
@@ -10,7 +10,7 @@ export interface PreviewCacheNamespaceOptions {
10
10
  recipeVersion?: number;
11
11
  scope?: string;
12
12
  }
13
- export type StoryboardCaptureRecipeInput = Pick<DesignScreenCase, 'route' | 'previewRoute' | 'path' | 'componentName' | 'gallery' | 'width' | 'height' | 'environment' | 'preset' | 'interactions' | 'assertions'>;
13
+ export type StoryboardCaptureRecipeInput = Pick<DesignScreenCase, 'route' | 'previewRoute' | 'path' | 'componentName' | 'gallery' | 'width' | 'height' | 'environment' | 'preset' | 'desiredState' | 'interactions' | 'assertions'>;
14
14
  export interface PreviewArtifactExpectation {
15
15
  namespace: string;
16
16
  sourceRevision: string;
@@ -18,6 +18,8 @@ export interface PreviewArtifactExpectation {
18
18
  export interface PreviewArtifactFrameRequest {
19
19
  id: string;
20
20
  fingerprint?: string;
21
+ /** Capture recipe identity without its source dependency digest. */
22
+ recipeFingerprint?: string;
21
23
  /** The recipe the fingerprint stands for; travels only where a capture may run. */
22
24
  recipe?: RoutePreviewFrameRecipe;
23
25
  }
@@ -174,6 +176,7 @@ export interface PreviewFramePage {
174
176
  height?: number;
175
177
  environment?: StoryboardEnvironment;
176
178
  preset?: unknown;
179
+ desiredState?: unknown;
177
180
  interactions?: unknown;
178
181
  assertions?: unknown;
179
182
  }
@@ -194,6 +197,7 @@ export interface RoutePreviewFrameRecipe {
194
197
  height?: number;
195
198
  environment?: StoryboardEnvironment;
196
199
  preset?: unknown;
200
+ desiredState?: unknown;
197
201
  interactions?: unknown;
198
202
  assertions?: unknown;
199
203
  }
@@ -218,6 +222,8 @@ export declare function resolveRoutePreviewFrameRequestPage(request: PreviewArti
218
222
  * now captures per frame instead of demanding the whole catalog up front.
219
223
  */
220
224
  export declare function createRoutePreviewFrameFingerprint(page: PreviewFramePage, previewRevision: string, baselineEnvironment?: StoryboardEnvironment): string;
225
+ /** The reproducible capture recipe, kept separate from changing source content. */
226
+ export declare function createRoutePreviewFrameRecipeFingerprint(page: PreviewFramePage, baselineEnvironment?: StoryboardEnvironment): string;
221
227
  export declare function createPreviewCacheNamespace({ sourceRevision, recipes, recipeVersion, scope, }: PreviewCacheNamespaceOptions): string;
222
228
  /**
223
229
  * Device pixels captured per CSS pixel. Must mirror
@@ -13,6 +13,11 @@ export interface PygmalionDevMirrorStatus {
13
13
  warning: string | null;
14
14
  error: string | null;
15
15
  appOrigin: string | null;
16
+ /** Present when the proxy child is absent or serves another ref's checkout. */
17
+ runtimeDrift?: {
18
+ expectedAppRoot: string;
19
+ actualAppRoot: string | null;
20
+ };
16
21
  }
17
22
  /** One revision the mirror can be repointed at. */
18
23
  export interface PygmalionSourceRef {
@@ -12,6 +12,7 @@ export interface RoutePreviewRecipe {
12
12
  height: number;
13
13
  environment?: unknown;
14
14
  preset?: unknown;
15
+ desiredState?: unknown;
15
16
  interactions?: unknown;
16
17
  assertions?: unknown;
17
18
  }
@@ -55,6 +56,7 @@ export interface RoutePreviewArtifactPage {
55
56
  height?: number;
56
57
  environment?: StoryboardEnvironment;
57
58
  preset?: unknown;
59
+ desiredState?: unknown;
58
60
  interactions?: unknown;
59
61
  assertions?: unknown;
60
62
  }
@@ -66,6 +68,8 @@ export interface RoutePreviewArtifactSeedResult {
66
68
  export interface RoutePreviewArtifactFrameIdentity {
67
69
  id: string;
68
70
  fingerprint?: string;
71
+ /** Capture recipe identity without its source dependency digest. */
72
+ recipeFingerprint?: string;
69
73
  /**
70
74
  * The recipe the fingerprint stands for, carried so a generator can reproduce
71
75
  * it. Without this a host can only re-capture what it declared for the id and
@@ -210,6 +214,7 @@ export declare function shouldRequestRoutePreviewArtifactFrame({ cacheHydrated,
210
214
  */
211
215
  export declare function requestRoutePreviewArtifactFrame(id: string, fingerprint?: string, options?: {
212
216
  retry?: boolean;
217
+ recipeFingerprint?: string;
213
218
  recipe?: RoutePreviewFrameRecipe;
214
219
  priority?: number;
215
220
  }): Promise<RoutePreviewArtifactResolution> | null;
@@ -0,0 +1,150 @@
1
+ export type StateSwitchPhase = 'boot' | 'replay' | 'settle' | 'stability' | 'serialize' | 'publish';
2
+ type Clock = () => number;
3
+ /** Test hook: replace the phase clock (pass null to restore the default). */
4
+ export declare function setStateSwitchMetricsClock(next: Clock | null): void;
5
+ interface WaypointTiming {
6
+ screenId: string;
7
+ replayMs: number | null;
8
+ settleMs: number | null;
9
+ stabilityMs: number | null;
10
+ stabilityTimedOut: boolean;
11
+ serializeMs: number | null;
12
+ publishMs: number | null;
13
+ }
14
+ /**
15
+ * Live handle for one walk. Created by beginStateSwitchWalk, fed by the
16
+ * mark/record hooks inside walkPath, and closed by endStateSwitchWalk.
17
+ */
18
+ export interface StateSwitchWalkTrace {
19
+ runner: string;
20
+ pathId: string;
21
+ /** True when the walk resumed on a parked warm instance (no boot). */
22
+ reused: boolean;
23
+ startedAtEpochMs: number;
24
+ startedAt: number;
25
+ markAt: number | null;
26
+ bootMs: number | null;
27
+ waypoints: WaypointTiming[];
28
+ current: WaypointTiming | null;
29
+ ended: boolean;
30
+ }
31
+ export interface PhaseAggregate {
32
+ count: number;
33
+ totalMs: number;
34
+ maxMs: number;
35
+ }
36
+ export interface WalkAggregate {
37
+ count: number;
38
+ completed: number;
39
+ coldBoots: number;
40
+ warmReuses: number;
41
+ stabilityTimeouts: number;
42
+ phases: Record<StateSwitchPhase, PhaseAggregate>;
43
+ }
44
+ export interface WalkRecord {
45
+ runner: string;
46
+ pathId: string;
47
+ reused: boolean;
48
+ completed: boolean;
49
+ startedAtEpochMs: number;
50
+ totalMs: number;
51
+ bootMs: number | null;
52
+ /** Per-phase sums across the walk's waypoints, for quick reading. */
53
+ totals: {
54
+ replayMs: number;
55
+ settleMs: number;
56
+ stabilityMs: number;
57
+ serializeMs: number;
58
+ publishMs: number;
59
+ };
60
+ waypoints: WaypointTiming[];
61
+ }
62
+ /** Opens a walk trace; called once per walkPath invocation. */
63
+ export declare function beginStateSwitchWalk(runner: string, pathId: string, reused: boolean): StateSwitchWalkTrace;
64
+ /** Opens the next waypoint's timing slot and drops any stale phase mark. */
65
+ export declare function beginStateSwitchWaypoint(trace: StateSwitchWalkTrace, screenId: string): void;
66
+ /** Stamps the start of the next phase; recordStateSwitchPhase reads it. */
67
+ export declare function markStateSwitchPhase(trace: StateSwitchWalkTrace): void;
68
+ /**
69
+ * Closes the phase opened by the last mark: attributes the elapsed time to
70
+ * the walk (boot) or the current waypoint, and feeds the aggregates. Without
71
+ * a preceding mark this records nothing.
72
+ */
73
+ export declare function recordStateSwitchPhase(trace: StateSwitchWalkTrace, phase: StateSwitchPhase): void;
74
+ /**
75
+ * Closes the walk: bumps the walk counters and pushes one record into the
76
+ * bounded history. Safe to call once per trace only; later calls no-op.
77
+ */
78
+ export declare function endStateSwitchWalk(trace: StateSwitchWalkTrace, completed: boolean): void;
79
+ /**
80
+ * One producer-gate evaluation. The gate is a pure predicate evaluated per
81
+ * frame render, so `allowed` is an upper bound on actual producer starts;
82
+ * the byTrigger ratio is the signal (which condition keeps opening the gate).
83
+ */
84
+ export declare function recordRoutePreviewProducerDecision(input: {
85
+ endpointConfigured: boolean;
86
+ detailActive: boolean;
87
+ previewIntended: boolean;
88
+ }): void;
89
+ /**
90
+ * Plain JSON-safe snapshot for the headless debug surface. `walks.recent` is
91
+ * oldest-first and bounded to the last WALK_HISTORY_CAPACITY walks.
92
+ */
93
+ export declare function getStateSwitchMetricsSnapshot(): {
94
+ walks: {
95
+ byRunner: {
96
+ [k: string]: {
97
+ count: number;
98
+ completed: number;
99
+ coldBoots: number;
100
+ warmReuses: number;
101
+ stabilityTimeouts: number;
102
+ phases: Record<StateSwitchPhase, PhaseAggregate>;
103
+ };
104
+ };
105
+ recent: {
106
+ totals: {
107
+ replayMs: number;
108
+ settleMs: number;
109
+ stabilityMs: number;
110
+ serializeMs: number;
111
+ publishMs: number;
112
+ };
113
+ waypoints: {
114
+ screenId: string;
115
+ replayMs: number | null;
116
+ settleMs: number | null;
117
+ stabilityMs: number | null;
118
+ stabilityTimedOut: boolean;
119
+ serializeMs: number | null;
120
+ publishMs: number | null;
121
+ }[];
122
+ runner: string;
123
+ pathId: string;
124
+ reused: boolean;
125
+ completed: boolean;
126
+ startedAtEpochMs: number;
127
+ totalMs: number;
128
+ bootMs: number | null;
129
+ }[];
130
+ count: number;
131
+ completed: number;
132
+ coldBoots: number;
133
+ warmReuses: number;
134
+ stabilityTimeouts: number;
135
+ phases: Record<StateSwitchPhase, PhaseAggregate>;
136
+ };
137
+ routePreviewProducer: {
138
+ evaluations: number;
139
+ allowed: number;
140
+ suppressed: number;
141
+ byTrigger: {
142
+ endpointNotConfigured: number;
143
+ detailActive: number;
144
+ previewIntended: number;
145
+ };
146
+ };
147
+ };
148
+ /** Drops every counter and the walk history; test and audit boundary hook. */
149
+ export declare function resetStateSwitchMetrics(): void;
150
+ export {};
@@ -3,7 +3,7 @@ import { type InteractiveStateDef } from './interactiveStates';
3
3
  import { type ScreenDimensionDef } from './screenDimensions';
4
4
  import { type CardBadgeOption, type CardSlotDef, type ScreenCardDef } from './screenCards';
5
5
  import { type ScreenListDef } from './screenLists';
6
- import { type DesignImportAsset, type DesignImportKind, type DesignFrameSizeMode, type DesignScreenAssertion, type DesignScreenInteraction, type DesignScreenPreset, type StoryboardEnvironment } from './designImport';
6
+ import { type DesignImportAsset, type DesignImportKind, type DesignFrameSizeMode, type DesignScreenAssertion, type DesignScreenDesiredState, type DesignScreenInteraction, type DesignScreenPreset, type StoryboardEnvironment } from './designImport';
7
7
  import { type DomImportDiagnostic, type DomImportResult } from './domImport';
8
8
  import { type LayerImportFidelityReport, type MeasurableElement } from './importFidelity';
9
9
  import { type DesignFrameHeightMode } from './frameHeight';
@@ -204,6 +204,10 @@ export interface PageModel {
204
204
  interactions?: DesignScreenInteraction[];
205
205
  /** Captured recipe, kept while a declared interactive state extends it. */
206
206
  baseInteractions?: DesignScreenInteraction[];
207
+ /** Captured host-owned desired state, restored when a direct option is cleared. */
208
+ authoredDesiredState?: DesignScreenDesiredState;
209
+ /** Complete host-owned UI state applied after the preview application mounts. */
210
+ desiredState?: DesignScreenDesiredState;
207
211
  /** View-only scroll endpoint currently held on this frame's preview recipe. */
208
212
  previewScroll?: {
209
213
  x: number;
@@ -215,6 +219,8 @@ export interface PageModel {
215
219
  previewScrollInteraction?: DesignScreenInteraction;
216
220
  /** Declared interactive axis the frame is currently held in, if any. */
217
221
  interactiveStateId?: string;
222
+ /** The active option is rendered through host desired state, without replay. */
223
+ interactiveStateUsesDesiredState?: boolean;
218
224
  interactiveOptionId?: string;
219
225
  /**
220
226
  * Options held together on a combining axis (InteractiveStateDef.multiple).
@@ -222,6 +228,19 @@ export interface PageModel {
222
228
  * exclusive axis reads exactly as before.
223
229
  */
224
230
  interactiveOptionIds?: string[];
231
+ /**
232
+ * Held-pseudo steps applied as presentation only — the pseudo fast path.
233
+ *
234
+ * A pure-CSS pseudo option (hover/focus/focus-visible/active on a target
235
+ * without script-driven pseudo events) is a styling variation of the import
236
+ * already on screen, so the mounted frozen preview holds it client-side —
237
+ * exactly as it already holds the recipe's own terminal pseudo gestures.
238
+ * Kept OUTSIDE `interactions` on purpose: the recipe is the frame's cache
239
+ * identity (framePreviewKeys), and rewriting it would re-key the frame
240
+ * while its artifacts, snapshots, and fidelity verdicts still describe the
241
+ * base import.
242
+ */
243
+ heldPseudoInteractions?: DesignScreenInteraction[];
225
244
  /**
226
245
  * The case's own environment, kept so clearing an interactive option can put
227
246
  * `environment` back without reconstructing what the case declared. Same shape
@@ -241,6 +260,8 @@ export interface PageModel {
241
260
  assertions?: DesignScreenAssertion[];
242
261
  /** Host-declared warm instance group. Set when the screen state is declared, not replayed. */
243
262
  session?: string;
263
+ /** Test ids observed in the frozen preview before editable layers are imported. */
264
+ previewTestIds?: readonly string[];
244
265
  layerImportCount?: number;
245
266
  layerImportTruncated?: boolean;
246
267
  layerImportDiagnostics?: readonly DomImportDiagnostic[];
@@ -520,6 +541,7 @@ export declare class EditorStore {
520
541
  scenarioId?: string;
521
542
  scenarioIds?: readonly string[];
522
543
  interactions?: DesignScreenInteraction[];
544
+ desiredState?: DesignScreenDesiredState;
523
545
  environment?: StoryboardEnvironment;
524
546
  preset?: DesignScreenPreset;
525
547
  assertions?: DesignScreenAssertion[];
@@ -677,12 +699,43 @@ export declare class EditorStore {
677
699
  */
678
700
  applyCardBadge(pageId: string, def: ScreenCardDef, cardNodeId: string, slot: CardSlotDef, option: CardBadgeOption, present: boolean): boolean;
679
701
  /**
680
- * Puts a frame into a declared interactive state by extending its recipe.
702
+ * Fast path for pure-CSS pseudo options: holds the state on the mounted
703
+ * frozen preview instead of re-walking the frame.
704
+ *
705
+ * Selecting "Hover" on a control is a CSS-only variation of the import
706
+ * already on screen, yet the ordinary path re-keys the recipe and drops the
707
+ * import — a hidden boot, a full recipe replay, and a re-import for a style
708
+ * the frozen mount can hold in place. A walk-produced snapshot for such an
709
+ * option shows nothing the client-side hold does not: the frozen mount
710
+ * re-applies held pseudo gestures from the recipe on every mount anyway
711
+ * (mountFrozenShadowPreview), so holding them over the existing verified
712
+ * base is the same pixels seconds earlier.
713
+ *
714
+ * When every step is a held-pseudo gesture whose target the mounted frozen
715
+ * preview resolves without a script-driven pseudo-event stamp, the
716
+ * selection is recorded as presentation state (`heldPseudoInteractions`)
717
+ * that the shadow preview merges into its held gestures. The recipe — and
718
+ * with it the preview cache key, artifact resolution, snapshot lookups,
719
+ * and fidelity verdicts — stays untouched, and returning to the base is
720
+ * removing the hold.
721
+ *
722
+ * Everything else falls back to the walk: combining axes, boot conditions,
723
+ * non-pseudo or missing-target steps, script-reactive targets, frames
724
+ * without imported layers or without a mounted non-stale frozen preview,
725
+ * and frames whose applied state was produced by a recipe rewrite.
726
+ */
727
+ private applyHeldPseudoInteractiveState;
728
+ /**
729
+ * Puts a frame into a declared interactive state through its reproduction
730
+ * contract: gesture recipe, boot environment, or direct desired state.
681
731
  *
682
732
  * The steps become part of `page.interactions`, which is what the preview
683
733
  * cache key, the live replay and the session walk all read — so the frame
684
734
  * asks for the variant through the ordinary supply instead of a parallel
685
735
  * path. Returning to the base option restores the captured recipe.
736
+ *
737
+ * Pure-CSS pseudo options short-circuit through the held-pseudo fast path
738
+ * (applyHeldPseudoInteractiveState) and never touch the recipe.
686
739
  */
687
740
  applyInteractiveState(pageId: string, def: InteractiveStateDef, optionId: string): boolean;
688
741
  /** The root viewport endpoint currently preserved in this frame recipe. */
@@ -731,6 +784,8 @@ export declare class EditorStore {
731
784
  /** Design importer automatically inserts the capture results into the existing frame. The route is maintained for live preview. */
732
785
  replacePageRootFromDom(pageId: string, rootJson: NodeJSON, result?: Pick<DomImportResult, 'count' | 'truncated'> & Partial<Pick<DomImportResult, 'diagnostics' | 'metrics' | 'geometry'>>): void;
733
786
  setPageLayerImportError(pageId: string, message: string): void;
787
+ /** Lets the view-only inspector classify axes from the frozen screen it already shows. */
788
+ setPagePreviewTestIds(pageId: string, testIds: readonly string[]): void;
734
789
  /**
735
790
  * Runs the fidelity measurement once the imported tree has committed and
736
791
  * laid out — one frame for React, one for layout. FrameView re-invokes the
@@ -57,6 +57,7 @@ export interface StoryboardResolvedPage {
57
57
  state?: string;
58
58
  scenarioId?: string;
59
59
  interactions?: DesignScreenCase['interactions'];
60
+ desiredState?: DesignScreenCase['desiredState'];
60
61
  environment?: StoryboardEnvironment;
61
62
  preset?: DesignScreenCase['preset'];
62
63
  assertions?: DesignScreenCase['assertions'];