@pygmalionjs/pygmalion 0.6.27 → 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.
@@ -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'];
@@ -0,0 +1,69 @@
1
+ import type { InteractiveStateDef, InteractiveStateOption } from './interactiveStates';
2
+ import type { PageModel } from './store';
3
+ /**
4
+ * Cheap kill switch, same shape as setCaptureSupplyMode: the host (or a
5
+ * console session) can turn speculation off without touching anything else.
6
+ * Disabling also drops any scheduled or in-progress round.
7
+ */
8
+ export declare function setInteractiveStatePrefetchEnabled(enabled: boolean): void;
9
+ export declare function interactiveStatePrefetchEnabled(): boolean;
10
+ /** Test/tuning hook for the trigger debounce. */
11
+ export declare function setInteractiveStatePrefetchDebounceMs(ms: number): void;
12
+ /**
13
+ * The page identity the store's applyInteractiveState would produce for one
14
+ * exclusive-axis option, computed WITHOUT mutating the page.
15
+ *
16
+ * DUPLICATED DERIVATION — store.ts applyInteractiveState is the source of
17
+ * truth. Extracting the shared pure core would touch that method's body,
18
+ * which is deliberately left alone; the agreement test in
19
+ * variantPrefetch.test.mjs pins the two implementations to each other, so a
20
+ * change to either fails loudly. Only the single-option branch is mirrored:
21
+ * combining (`multiple`) axes hold sets and are out of prefetch's scope.
22
+ *
23
+ * Returns null when selecting the option asks supply for nothing new — the
24
+ * base option, or the option this frame's capture already shows — because
25
+ * the store would reset the frame to its base recipe.
26
+ */
27
+ export declare function deriveInteractiveVariantIdentity(page: PageModel, def: InteractiveStateDef, option: InteractiveStateOption): PageModel | null;
28
+ export interface InteractiveVariantPrefetchCandidate {
29
+ axisId: string;
30
+ optionId: string;
31
+ /** The recipe key the option's real selection would publish under. */
32
+ cacheKey: string;
33
+ /** Hypothetical identity handed to prefetchPageInteractiveVariant. */
34
+ variantPage: PageModel;
35
+ }
36
+ /**
37
+ * The unproduced variants of one frame, in declaration order, capped at
38
+ * `limit`. An option is skipped when it needs no work (base or captured),
39
+ * its future key already has a snapshot, or the runner already tracks its
40
+ * variant id (a prefetch or a real request is on it). Only declared
41
+ * exclusive axes participate: combining axes hold sets with no single next
42
+ * click to predict, and automatic pseudo axes are synthesized per element —
43
+ * both stay on the on-demand path.
44
+ */
45
+ export declare function planInteractiveStatePrefetch(page: PageModel, previewRevision: string, limit?: number): InteractiveVariantPrefetchCandidate[];
46
+ /** Diagnostic counters for the headless debug surface and tests. */
47
+ export declare const __variantPrefetchDebug: {
48
+ requested: number;
49
+ rounds: number;
50
+ queued: number;
51
+ exhausted: number;
52
+ unavailable: number;
53
+ };
54
+ /**
55
+ * Starts a prefetch round for one frame immediately (no debounce): up to
56
+ * MAX_PREFETCH_WALKS_PER_ROUND idle-time walks, one at a time. Replaces any
57
+ * round already in progress — the newest selection is the best prediction.
58
+ */
59
+ export declare function startInteractiveStatePrefetch(page: PageModel, previewRevision: string): void;
60
+ /** Drops the scheduled trigger and the in-progress round, if any. */
61
+ export declare function cancelScheduledInteractiveStatePrefetch(): void;
62
+ /**
63
+ * Debounced trigger for the panel: called when a frame's axes become
64
+ * visible. The debounce keeps a designer stepping across frames from
65
+ * starting a round per keystroke of selection. Returns a cancel for the
66
+ * effect cleanup — cancelling stops the round, so speculation only runs
67
+ * while the frame is actually the selected one.
68
+ */
69
+ export declare function requestInteractiveStatePrefetch(page: PageModel, previewRevision: string): () => void;
@@ -92,7 +92,7 @@ export type { ConditionCoverageRoute, ConditionCoverageScreen, ConformanceScreen
92
92
  export { clearPreviewEnvironmentControlValues, getPreviewEnvironmentControlValue, getPreviewEnvironmentControls, getPreviewEnvironmentOverride, setPreviewEnvironmentControlValue, setPreviewEnvironmentControls, subscribePreviewEnvironmentControls, } from './editor/previewEnvironmentControls';
93
93
  export type { PreviewEnvironmentControlDef, PreviewEnvironmentControlOptionDef, } from './editor/previewEnvironmentControls';
94
94
  export { createDesignImportController, createDesignScreenCollection, DESIGN_IMPORT_KINDS, expandDesignScreenCasesViewports, expandDesignScreenCollectionViewports, mergeScreenPresets, } from './editor/designImport';
95
- export type { CreateDesignImportOptions, DesignBehaviorScenario, DesignImportAsset, DesignImportCollection, DesignImportController, DesignImportCoverage, DesignImportCoverageItem, DesignImportInitialPage, DesignImportKind, DesignImportManifest, DesignImportPage, DesignFrameHeightMode, DesignFrameSizeMode, DesignScenarioCoverage, DesignScenarioSource, DesignScreenCase, DesignScreenAssertion, DesignScreenAssertionReport, DesignScreenAssertionVisibility, DesignScreenAttributeAssertion, DesignScreenCaptureFailure, DesignScreenCaptureFailureCode, DesignScreenCaptureFailureStage, DesignScreenCaptureDefaults, DesignScreenCaptureRequirements, DesignScreenCaptureReport, DesignScreenCaptureSpec, DesignScreenCollectionResult, DesignScreenInteraction, DesignScreenInteractionAction, DesignScreenInteractionRunOptions, DesignScreenInteractionReport, DesignScreenPreset, DesignScreenPresetContext, DesignScreenPresetExecutionResult, DesignScreenPresetExecutor, DesignScreenPresetRequest, DesignScreenViewport, DesignScreenViewportExpansionOptions, DesignScreenViewportPagePatch, DesignSerializable, StoryboardEnvironment, StoryboardMediaDevice, StoryboardMediaFailure, StoryboardNetwork, StoryboardPermissionState, StoryboardRequestCondition, StoryboardRequestOutcome, } from './editor/designImport';
95
+ export type { CreateDesignImportOptions, DesignBehaviorScenario, DesignImportAsset, DesignImportCollection, DesignImportController, DesignImportCoverage, DesignImportCoverageItem, DesignImportInitialPage, DesignImportKind, DesignImportManifest, DesignImportPage, DesignFrameHeightMode, DesignFrameSizeMode, DesignScenarioCoverage, DesignScenarioSource, DesignScreenCase, DesignScreenAssertion, DesignScreenAssertionReport, DesignScreenAssertionVisibility, DesignScreenAttributeAssertion, DesignScreenCaptureFailure, DesignScreenCaptureFailureCode, DesignScreenCaptureFailureStage, DesignScreenCaptureDefaults, DesignScreenCaptureRequirements, DesignScreenCaptureReport, DesignScreenCaptureSpec, DesignScreenCollectionResult, DesignScreenDesiredState, DesignScreenDesiredStateExecutor, DesignScreenDesiredStateRequest, DesignScreenInteraction, DesignScreenInteractionAction, DesignScreenInteractionRunOptions, DesignScreenInteractionReport, DesignScreenPreset, DesignScreenPresetContext, DesignScreenPresetExecutionResult, DesignScreenPresetExecutor, DesignScreenPresetRequest, DesignScreenViewport, DesignScreenViewportExpansionOptions, DesignScreenViewportPagePatch, DesignSerializable, StoryboardEnvironment, StoryboardMediaDevice, StoryboardMediaFailure, StoryboardNetwork, StoryboardPermissionState, StoryboardRequestCondition, StoryboardRequestOutcome, } from './editor/designImport';
96
96
  export type { DomImportDiagnostic, DomImportMetrics, DomImportOptions, DomImportResult, } from './editor/domImport';
97
97
  export type { SharedSourceEditScope, SharedSourceFrameSummary, SharedSourceImpact, } from './editor/sharedSource';
98
98
  export type { NodeJSON } from './editor/store';
@@ -182,6 +182,9 @@ export declare const __debug: {
182
182
  noPath: number;
183
183
  resolvedNull: number;
184
184
  queued: number;
185
+ reparkQueued: number;
186
+ reparkAlreadyParked: number;
187
+ reparkNoExtraSteps: number;
185
188
  };
186
189
  states: Record<string, unknown>;
187
190
  };
@@ -201,8 +204,71 @@ export declare const __debug: {
201
204
  reuses: number;
202
205
  parks: number;
203
206
  evictions: number;
207
+ refreshes: number;
204
208
  };
205
209
  }>;
210
+ /**
211
+ * State-switch instrumentation: phase timings of recent flow-session walks
212
+ * (boot/replay/settle/stability/serialize/publish, per runner label, warm
213
+ * reuse vs cold boot) plus route-preview producer-gate counters. JSON-safe.
214
+ */
215
+ stateSwitchMetrics(): Promise<{
216
+ walks: {
217
+ byRunner: {
218
+ [k: string]: {
219
+ count: number;
220
+ completed: number;
221
+ coldBoots: number;
222
+ warmReuses: number;
223
+ stabilityTimeouts: number;
224
+ phases: Record<import("./editor/stateSwitchMetrics").StateSwitchPhase, import("./editor/stateSwitchMetrics").PhaseAggregate>;
225
+ };
226
+ };
227
+ recent: {
228
+ totals: {
229
+ replayMs: number;
230
+ settleMs: number;
231
+ stabilityMs: number;
232
+ serializeMs: number;
233
+ publishMs: number;
234
+ };
235
+ waypoints: {
236
+ screenId: string;
237
+ replayMs: number | null;
238
+ settleMs: number | null;
239
+ stabilityMs: number | null;
240
+ stabilityTimedOut: boolean;
241
+ serializeMs: number | null;
242
+ publishMs: number | null;
243
+ }[];
244
+ runner: string;
245
+ pathId: string;
246
+ reused: boolean;
247
+ completed: boolean;
248
+ startedAtEpochMs: number;
249
+ totalMs: number;
250
+ bootMs: number | null;
251
+ }[];
252
+ count: number;
253
+ completed: number;
254
+ coldBoots: number;
255
+ warmReuses: number;
256
+ stabilityTimeouts: number;
257
+ phases: Record<import("./editor/stateSwitchMetrics").StateSwitchPhase, import("./editor/stateSwitchMetrics").PhaseAggregate>;
258
+ };
259
+ routePreviewProducer: {
260
+ evaluations: number;
261
+ allowed: number;
262
+ suppressed: number;
263
+ byTrigger: {
264
+ endpointNotConfigured: number;
265
+ detailActive: number;
266
+ previewIntended: number;
267
+ };
268
+ };
269
+ }>;
270
+ /** Clears the state-switch metrics between headless audit scenarios. */
271
+ resetStateSwitchMetrics(): Promise<void>;
206
272
  };
207
273
  /**
208
274
  * Called from the actual button handler of the host screen fixture — Switches to the name matching page (common in editor and preview).
@@ -6,7 +6,7 @@ how the variation is reproduced, not from how different its pixels look.
6
6
  | Placement | Use when | Pygmalion declaration |
7
7
  | --- | --- | --- |
8
8
  | Frame | The variation is an independent task, journey checkpoint, structural composition, overlay context, or supported viewport that reviewers must navigate to directly. | `DesignScreenCase` |
9
- | Interaction state | The same booted screen reaches a deterministic, reversible visual endpoint through a local gesture. | `ScreenStateAxisDef` with `kind: 'interaction'` |
9
+ | Interaction state | The same booted screen has a deterministic, reversible visual endpoint, reached either directly or through a local gesture. | `ScreenStateAxisDef` with `kind: 'interaction'` and `desiredState` or `steps` |
10
10
  | Condition state | The same screen needs external data, network, storage, permission, media, or timing conditions before it boots. | `ScreenStateAxisDef` with `kind: 'condition'` |
11
11
  | Editable parameter | The variation only samples text length, row count, content, or another freely editable value and has no distinct product endpoint. | Edit mode, component props, list controls, viewport controls, or QA data |
12
12
  | Motion preview | The same screen continuously changes through CSS or Web Animations without becoming a new product state. | Automatically discovered **Motion** controls |
@@ -41,9 +41,10 @@ interaction states. They must not become duplicate frames.
41
41
 
42
42
  The W and H controls in the frame inspector change the frame viewport, not the
43
43
  canvas camera. A committed size is part of the frame fingerprint and its exact
44
- capture recipe. The request carries the route, width, height, conditions, and
45
- interactions that produced that fingerprint, and a returned artifact is seeded
46
- under that requested recipe instead of the authored catalog key.
44
+ capture recipe. The request carries the route, width, height, conditions,
45
+ desired state, and interactions that produced that fingerprint. A returned
46
+ artifact is seeded under that requested recipe instead of the authored catalog
47
+ key.
47
48
 
48
49
  This distinction is observable: resizing a frame may change responsive layout,
49
50
  while changing canvas zoom only changes how large the same frame appears in the
@@ -73,6 +74,12 @@ axis targets the same element and pseudo action, Pygmalion suppresses that
73
74
  automatic duplicate. Click-driven toggles and other application states remain
74
75
  declared axes unless they already have their own review frame.
75
76
 
77
+ In view-only mode, `requires.testId` is matched against the frozen preview's
78
+ lightweight test-id index as well as an imported layer tree. This keeps right
79
+ panel state controls available without turning selection into a layer import or
80
+ live boot, and still hides an axis from screens whose captured DOM lacks its
81
+ target.
82
+
76
83
  ## Automatic motion coverage
77
84
 
78
85
  Pygmalion discovers visible CSS animation owners, animated pseudo elements,
@@ -140,6 +147,11 @@ export const screenStateAxes: readonly ScreenStateAxisDef[] = [
140
147
  },
141
148
  ],
142
149
  },
150
+ {
151
+ id: 'collapsed',
152
+ label: 'Collapsed',
153
+ desiredState: 'collapsed',
154
+ },
143
155
  ],
144
156
  },
145
157
  {
@@ -167,11 +179,48 @@ export const screenStateAxes: readonly ScreenStateAxisDef[] = [
167
179
  ```
168
180
 
169
181
  An interaction option cannot declare `environment`. Every non-base condition
170
- option must declare one. An interaction option ending in `fill` is rejected
171
- unless a later targeted `wait` proves that the input reached a distinct visual
172
- result. This keeps empty strings and maximum-length samples out of the state
173
- panel while still allowing a search input to reproduce a real empty-result
174
- state.
182
+ option must declare one. A direct `desiredState` value cannot be combined with
183
+ steps or an environment on the same option. An interaction option ending in
184
+ `fill` is rejected unless a later targeted `wait` proves that the input reached
185
+ a distinct visual result. This keeps empty strings and maximum-length samples
186
+ out of the state panel while still allowing a search input to reproduce a real
187
+ empty-result state.
188
+
189
+ ## Direct desired state
190
+
191
+ Use direct desired state for a locally controlled endpoint whose final visual
192
+ state matters more than the gestures used to reach it. Pygmalion builds a
193
+ complete map from selected axes, such as `{ sidebar: 'collapsed' }`, and calls
194
+ the host's `executeDesiredState` adapter after mount and before any remaining
195
+ interaction steps. The map participates in capture and preview identity.
196
+
197
+ The adapter must replace its whole preview state, not merge it. Pygmalion calls
198
+ it with `{}` as well, so a warm iframe reused for another frame cannot retain a
199
+ previous override. A typical React host exposes the map through an external
200
+ store and lets each component read only its own axis. The adapter is preview
201
+ infrastructure: it must not dispatch the application's production store or
202
+ write persistent product state. When a reviewer uses the real control, the
203
+ component should release its preview override and continue from the currently
204
+ displayed value.
205
+
206
+ The right-panel selection starts delivery in the same input task. Once one
207
+ option has a live document, sibling direct-state options share that document:
208
+ Pygmalion skips boot, route entry, gesture replay, assertions, and geometry
209
+ settling, then sends the replacement map to the host adapter. The React commit
210
+ is the presentation boundary; snapshot serialization may continue afterward
211
+ without blocking the visible state. A live direct-state document is pinned
212
+ against normal warm-pool eviction and cannot be claimed through gesture-prefix
213
+ matching, because those signatures do not describe host-owned state.
214
+ Sibling delivery calls the live adapter directly rather than joining the replay
215
+ runner's FIFO. Frames that expose a direct-state axis also reserve their
216
+ activation-warmed document from unrelated gesture prefetch and align it
217
+ invisibly behind the frozen frame. The first selection can therefore reveal the
218
+ already-positioned React document after its adapter commit; speculative work
219
+ cannot reintroduce a queue between the panel click and that commit.
220
+
221
+ Keep gesture steps when the transition itself is under review, when effects
222
+ outside local render state establish the endpoint, or when the host has no safe
223
+ preview-only adapter for that state.
175
224
 
176
225
  ## Scroll positions
177
226