@pygmalionjs/pygmalion 0.6.27 → 0.6.29

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-ziDz92ma.js → FrozenRoutePreview-D9J35QsM.js} +3568 -2799
  2. package/dist-lib/pygmalion.js +11044 -10373
  3. package/dist-lib/testing.js +1 -1
  4. package/dist-lib/types/canvas/ShadowRoutePreview.d.ts +3 -0
  5. package/dist-lib/types/canvas/useFlowSession.d.ts +3 -1
  6. package/dist-lib/types/editor/designImport.d.ts +42 -3
  7. package/dist-lib/types/editor/flowSessionScheduler.d.ts +54 -5
  8. package/dist-lib/types/editor/flowSessions.d.ts +51 -9
  9. package/dist-lib/types/editor/frameInteraction.d.ts +7 -1
  10. package/dist-lib/types/editor/geometryStability.d.ts +122 -0
  11. package/dist-lib/types/editor/heldPseudoStates.d.ts +22 -0
  12. package/dist-lib/types/editor/interactiveSessionSurface.d.ts +32 -0
  13. package/dist-lib/types/editor/interactiveStates.d.ts +27 -14
  14. package/dist-lib/types/editor/previewBootstrap.d.ts +7 -1
  15. package/dist-lib/types/editor/previewReachability.d.ts +7 -0
  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 +70 -3
  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 +61 -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 +90 -0
  28. package/node/vite.mjs +1 -0
  29. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
- import type { DesignScreenInteraction, StoryboardEnvironment } from './designImport';
1
+ import type { DesignScreenInteraction, DesignSerializable, StoryboardEnvironment } from './designImport';
2
2
  import type { PageModel } from './store';
3
+ export { reachablePreviewTestIds } from './previewReachability';
3
4
  /**
4
5
  * Screen-state axes — same-frame variations reproduced by a gesture or a boot
5
6
  * condition.
@@ -10,16 +11,23 @@ import type { PageModel } from './store';
10
11
  * catalog (where each would cost a captured frame) and puts them in the
11
12
  * right panel, where a designer flips between them.
12
13
  *
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.
14
+ * An option either extends the ordinary interaction recipe, declares a boot
15
+ * condition, or supplies a host-owned desired-state value. Everything
16
+ * downstream keys off that complete reproduction contract.
17
17
  */
18
18
  export interface InteractiveStateOption {
19
19
  id: string;
20
20
  label: string;
21
21
  /** Absent or empty marks the base state — the screen as captured. */
22
22
  steps?: readonly DesignScreenInteraction[];
23
+ /**
24
+ * Host-owned value rendered directly after mount. Selecting the option puts
25
+ * it under the axis id in the frame's complete desired-state map.
26
+ *
27
+ * This is an alternative to both gesture replay and a boot environment: the
28
+ * host adapter translates the value into preview-only application state.
29
+ */
30
+ desiredState?: DesignSerializable;
23
31
  /**
24
32
  * Boot condition this option puts the frame under, for states no gesture can
25
33
  * reach: a failed request, an empty result, a stalled stream.
@@ -95,9 +103,9 @@ export interface InteractiveStateDef {
95
103
  * all, and declaring every combination as its own option is a power set.
96
104
  *
97
105
  * 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.
106
+ * steps, condition, or desired state) clears it. Steps run, conditions merge,
107
+ * and desired values collect in declaration order, so the result is stable no
108
+ * matter what order they were clicked in.
101
109
  *
102
110
  * Not compatible with `capturedWhen`: a per-frame base answers "which single
103
111
  * option is already on screen", which a combination has no answer to.
@@ -119,21 +127,26 @@ export type ScreenStateAxisDiagnostic = InteractiveStateDiagnostic;
119
127
  /**
120
128
  * Whether selecting this option asks supply for a different screen.
121
129
  *
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.
130
+ * Three ways do that: direct desired state, a gesture on the booted screen, or
131
+ * a boot condition. The base is the option that declares none of them and
132
+ * reproduces the capture. Judging the base by "no steps" alone would read a
133
+ * direct or environment-only option as a second base and refuse the declaration.
126
134
  */
127
135
  export declare function optionChangesSupply(option: InteractiveStateOption): boolean;
128
136
  export declare function validateInteractiveStates(defs: readonly InteractiveStateDef[]): InteractiveStateDiagnostic[];
129
137
  /** Validates both interaction and condition axes. */
130
138
  export declare const validateScreenStateAxes: typeof validateInteractiveStates;
139
+ /** Reads only the control-presence evidence needed by the view-only inspector. */
140
+ export declare function previewMarkupTestIds(markup: string | null | undefined, bodyAttributes?: string | null): readonly string[];
131
141
  /**
132
142
  * 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.
143
+ * either the imported tree or the frozen preview's reachable test-id index.
144
+ * View-only can therefore offer a real screen control without importing the
145
+ * whole layer tree, while a frame without that control still hides the axis.
135
146
  */
136
147
  export declare function interactiveStatesForPage(page: PageModel, defs: readonly InteractiveStateDef[]): InteractiveStateDef[];
148
+ /** The first axis whose options can render directly on a warm host document. */
149
+ export declare function directInteractiveStateAxisForPage(page: PageModel, defs?: readonly InteractiveStateDef[]): InteractiveStateDef | undefined;
137
150
  /** Preferred public name for axes applicable to one frame. */
138
151
  export declare const screenStateAxesForPage: typeof interactiveStatesForPage;
139
152
  /**
@@ -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
@@ -0,0 +1,7 @@
1
+ export declare const REACHABLE_TEST_IDS_ATTRIBUTE = "data-pygmalion-reachable-testids";
2
+ /** Test ids whose rendered controls can actually be reached from this frame. */
3
+ export declare function reachablePreviewTestIds(root: ParentNode): readonly string[];
4
+ /** Stamps capture-time reachability without mutating the running document. */
5
+ export declare function stampReachablePreviewTestIds(source: Document, clone: HTMLElement): void;
6
+ /** Reads a capture-time reachability index, or null for a legacy snapshot. */
7
+ export declare function indexedReachablePreviewTestIds(markup: string): readonly string[] | null;
@@ -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,14 @@ 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;
224
+ /**
225
+ * Imported layers still showing behind a direct live surface. The surface
226
+ * paints first; FrameView consumes this key and invalidates the old layers
227
+ * on the following frame so a large tree cannot block the visible switch.
228
+ */
229
+ interactiveLayerRefreshKey?: string;
218
230
  interactiveOptionId?: string;
219
231
  /**
220
232
  * Options held together on a combining axis (InteractiveStateDef.multiple).
@@ -222,6 +234,19 @@ export interface PageModel {
222
234
  * exclusive axis reads exactly as before.
223
235
  */
224
236
  interactiveOptionIds?: string[];
237
+ /**
238
+ * Held-pseudo steps applied as presentation only — the pseudo fast path.
239
+ *
240
+ * A pure-CSS pseudo option (hover/focus/focus-visible/active on a target
241
+ * without script-driven pseudo events) is a styling variation of the import
242
+ * already on screen, so the mounted frozen preview holds it client-side —
243
+ * exactly as it already holds the recipe's own terminal pseudo gestures.
244
+ * Kept OUTSIDE `interactions` on purpose: the recipe is the frame's cache
245
+ * identity (framePreviewKeys), and rewriting it would re-key the frame
246
+ * while its artifacts, snapshots, and fidelity verdicts still describe the
247
+ * base import.
248
+ */
249
+ heldPseudoInteractions?: DesignScreenInteraction[];
225
250
  /**
226
251
  * The case's own environment, kept so clearing an interactive option can put
227
252
  * `environment` back without reconstructing what the case declared. Same shape
@@ -241,6 +266,8 @@ export interface PageModel {
241
266
  assertions?: DesignScreenAssertion[];
242
267
  /** Host-declared warm instance group. Set when the screen state is declared, not replayed. */
243
268
  session?: string;
269
+ /** Reachable test ids observed in the frozen preview before editable layers are imported. */
270
+ previewTestIds?: readonly string[];
244
271
  layerImportCount?: number;
245
272
  layerImportTruncated?: boolean;
246
273
  layerImportDiagnostics?: readonly DomImportDiagnostic[];
@@ -446,6 +473,8 @@ export declare class EditorStore {
446
473
  private pageEditRevisions;
447
474
  /** pageId → edit revision at the moment a frozen preview was promoted without a boot (D2). */
448
475
  private provisionalLayerImports;
476
+ /** Latest paint-first direct-state commit scheduled for each page. */
477
+ private directStateCommitRevisions;
449
478
  /** pageId → source geometry awaiting its post-mount fidelity measurement. */
450
479
  private pendingLayerImportGeometry;
451
480
  /** View-only motion playback never becomes part of a frame recipe or undo history. */
@@ -520,6 +549,7 @@ export declare class EditorStore {
520
549
  scenarioId?: string;
521
550
  scenarioIds?: readonly string[];
522
551
  interactions?: DesignScreenInteraction[];
552
+ desiredState?: DesignScreenDesiredState;
523
553
  environment?: StoryboardEnvironment;
524
554
  preset?: DesignScreenPreset;
525
555
  assertions?: DesignScreenAssertion[];
@@ -677,14 +707,49 @@ export declare class EditorStore {
677
707
  */
678
708
  applyCardBadge(pageId: string, def: ScreenCardDef, cardNodeId: string, slot: CardSlotDef, option: CardBadgeOption, present: boolean): boolean;
679
709
  /**
680
- * Puts a frame into a declared interactive state by extending its recipe.
710
+ * Fast path for pure-CSS pseudo options: holds the state on the mounted
711
+ * frozen preview instead of re-walking the frame.
712
+ *
713
+ * Selecting "Hover" on a control is a CSS-only variation of the import
714
+ * already on screen, yet the ordinary path re-keys the recipe and drops the
715
+ * import — a hidden boot, a full recipe replay, and a re-import for a style
716
+ * the frozen mount can hold in place. A walk-produced snapshot for such an
717
+ * option shows nothing the client-side hold does not: the frozen mount
718
+ * re-applies held pseudo gestures from the recipe on every mount anyway
719
+ * (mountFrozenShadowPreview), so holding them over the existing verified
720
+ * base is the same pixels seconds earlier.
721
+ *
722
+ * When every step is a held-pseudo gesture whose target the mounted frozen
723
+ * preview resolves without a script-driven pseudo-event stamp, the
724
+ * selection is recorded as presentation state (`heldPseudoInteractions`)
725
+ * that the shadow preview merges into its held gestures. The recipe — and
726
+ * with it the preview cache key, artifact resolution, snapshot lookups,
727
+ * and fidelity verdicts — stays untouched, and returning to the base is
728
+ * removing the hold.
729
+ *
730
+ * Everything else falls back to the walk: combining axes, boot conditions,
731
+ * non-pseudo or missing-target steps, script-reactive targets, frames
732
+ * without imported layers or without a mounted non-stale frozen preview,
733
+ * and frames whose applied state was produced by a recipe rewrite.
734
+ */
735
+ private applyHeldPseudoInteractiveState;
736
+ /**
737
+ * Puts a frame into a declared interactive state through its reproduction
738
+ * contract: gesture recipe, boot environment, or direct desired state.
681
739
  *
682
740
  * The steps become part of `page.interactions`, which is what the preview
683
741
  * cache key, the live replay and the session walk all read — so the frame
684
742
  * asks for the variant through the ordinary supply instead of a parallel
685
743
  * path. Returning to the base option restores the captured recipe.
744
+ *
745
+ * Pure-CSS pseudo options short-circuit through the held-pseudo fast path
746
+ * (applyHeldPseudoInteractiveState) and never touch the recipe.
686
747
  */
687
- applyInteractiveState(pageId: string, def: InteractiveStateDef, optionId: string): boolean;
748
+ applyInteractiveState(pageId: string, def: InteractiveStateDef, optionId: string, options?: {
749
+ skipPaintFirstPreview?: boolean;
750
+ }): boolean;
751
+ /** Invalidates retained layers only after their direct live replacement painted. */
752
+ commitInteractiveLayerRefresh(pageId: string, refreshKey: string): boolean;
688
753
  /** The root viewport endpoint currently preserved in this frame recipe. */
689
754
  frameScrollRecipe(pageId: string): Readonly<{
690
755
  x: number;
@@ -731,6 +796,8 @@ export declare class EditorStore {
731
796
  /** Design importer automatically inserts the capture results into the existing frame. The route is maintained for live preview. */
732
797
  replacePageRootFromDom(pageId: string, rootJson: NodeJSON, result?: Pick<DomImportResult, 'count' | 'truncated'> & Partial<Pick<DomImportResult, 'diagnostics' | 'metrics' | 'geometry'>>): void;
733
798
  setPageLayerImportError(pageId: string, message: string): void;
799
+ /** Lets the view-only inspector classify axes from the frozen screen it already shows. */
800
+ setPagePreviewTestIds(pageId: string, testIds: readonly string[]): void;
734
801
  /**
735
802
  * Runs the fidelity measurement once the imported tree has committed and
736
803
  * 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).