@pygmalionjs/pygmalion 0.5.18 → 0.5.20

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.
@@ -18,7 +18,24 @@ export interface PreviewArtifactExpectation {
18
18
  export interface PreviewArtifactFrameRequest {
19
19
  id: string;
20
20
  fingerprint?: string;
21
+ /** The recipe the fingerprint stands for; travels only where a capture may run. */
22
+ recipe?: RoutePreviewFrameRecipe;
21
23
  }
24
+ /**
25
+ * Where a capture request sits among captures waiting on the same identity.
26
+ * Smaller runs first. Mirrors `PREVIEW_CAPTURE_PRIORITY` in
27
+ * `node/preview-artifact-plugin.mjs` (browser code cannot import the Node
28
+ * plugin); a shared test pins the pair.
29
+ */
30
+ export declare const PREVIEW_CAPTURE_PRIORITY: Readonly<{
31
+ /** A frame the designer selected: whatever else is queued waits. */
32
+ selected: 0;
33
+ /** The canvas in front of the designer being filled. */
34
+ canvas: 10;
35
+ /** A canvas nobody is looking at yet. */
36
+ background: 20;
37
+ }>;
38
+ export type PreviewCapturePriority = (typeof PREVIEW_CAPTURE_PRIORITY)[keyof typeof PREVIEW_CAPTURE_PRIORITY];
22
39
  export interface PreviewArtifactTransportRequest extends PreviewArtifactExpectation {
23
40
  /** Frames to fetch. Omitted asks for the whole bundle, as earlier versions did. */
24
41
  frames?: readonly PreviewArtifactFrameRequest[];
@@ -26,6 +43,8 @@ export interface PreviewArtifactTransportRequest extends PreviewArtifactExpectat
26
43
  generate?: boolean;
27
44
  /** Return only exact/missing/stale classification, without frame payloads. */
28
45
  resolveOnly?: boolean;
46
+ /** Where this capture queues among others on the identity; see PREVIEW_CAPTURE_PRIORITY. */
47
+ priority?: number;
29
48
  signal?: AbortSignal;
30
49
  captureBaseUrl?: string;
31
50
  }
@@ -36,14 +55,29 @@ export interface PreviewArtifactHttpResponse {
36
55
  readonly statusText?: string;
37
56
  json(): Promise<unknown>;
38
57
  }
39
- export type PreviewArtifactFetch = (input: string, init: {
58
+ /**
59
+ * A probe reads; it travels as GET with the frame identities in the query. A
60
+ * capture request carries a recipe per frame, which a query string cannot
61
+ * hold past a handful of frames, so it travels as a JSON body.
62
+ */
63
+ export type PreviewArtifactFetchInit = {
40
64
  method: 'GET';
41
65
  headers: {
42
66
  accept: 'application/json';
43
67
  };
44
68
  cache: 'no-store';
45
69
  signal?: AbortSignal;
46
- }) => Promise<PreviewArtifactHttpResponse>;
70
+ } | {
71
+ method: 'POST';
72
+ headers: {
73
+ accept: 'application/json';
74
+ 'content-type': 'application/json';
75
+ };
76
+ body: string;
77
+ cache: 'no-store';
78
+ signal?: AbortSignal;
79
+ };
80
+ export type PreviewArtifactFetch = (input: string, init: PreviewArtifactFetchInit) => Promise<PreviewArtifactHttpResponse>;
47
81
  export interface PreviewArtifactHttpTransportOptions {
48
82
  endpoint: string;
49
83
  fetcher?: PreviewArtifactFetch;
@@ -134,6 +168,23 @@ export interface PreviewFramePage {
134
168
  interactions?: unknown;
135
169
  assertions?: unknown;
136
170
  }
171
+ /**
172
+ * What the fingerprint of a frame stands for.
173
+ *
174
+ * The fingerprint is a hash, so a generator handed one alone can only capture
175
+ * what the host declared for that id — a different screen wearing the requested
176
+ * name. Sending the recipe beside it is what lets a frame held in a declared
177
+ * state be captured as the state it is actually in.
178
+ *
179
+ * The interaction list stays opaque here for the same reason the page's does:
180
+ * this package transports the host's recipe, it does not read it.
181
+ */
182
+ export interface RoutePreviewFrameRecipe {
183
+ environment?: StoryboardEnvironment;
184
+ interactions?: unknown;
185
+ }
186
+ /** The recipe behind createRoutePreviewFrameFingerprint, for the same page. */
187
+ export declare function routePreviewFrameRecipe(page: PreviewFramePage, baselineEnvironment?: StoryboardEnvironment): RoutePreviewFrameRecipe | undefined;
137
188
  /**
138
189
  * Identity of one frame's capture, derived the same way wherever it is needed.
139
190
  *
@@ -181,9 +232,10 @@ export declare function validatePreviewArtifactBundle(value: unknown, expectatio
181
232
  * an empty or stale subset is valid transport data, but it is not a warm cache.
182
233
  */
183
234
  export declare function resolvePreviewArtifactFrames(artifact: RoutePreviewArtifactBundle, expectation: PreviewArtifactExpectation, frames: readonly PreviewArtifactFrameRequest[]): PreviewArtifactFrameResolution;
184
- export declare function bootstrapPreviewArtifact({ namespace, sourceRevision, transport, signal, frames, generate, }: PreviewArtifactExpectation & {
235
+ export declare function bootstrapPreviewArtifact({ namespace, sourceRevision, transport, signal, frames, generate, priority, }: PreviewArtifactExpectation & {
185
236
  transport: PreviewArtifactTransport;
186
237
  signal?: AbortSignal;
187
238
  frames?: readonly PreviewArtifactFrameRequest[];
188
239
  generate?: boolean;
240
+ priority?: number;
189
241
  }): Promise<PreviewArtifactBootstrapResult>;
@@ -5,6 +5,16 @@
5
5
  * install, preview boot). 420s gives that chain a minute of headroom.
6
6
  */
7
7
  export declare const MIRROR_SYNC_POLL_TIMEOUT_MS = 420000;
8
+ /**
9
+ * Poll cadence while the mirror sits idle.
10
+ *
11
+ * Idle means nobody has asked for the checkout yet, and the ask can come from
12
+ * outside this editor's own controls — a frame requesting its capture reaches
13
+ * the server directly. The server has no way to tell the page that the work it
14
+ * triggered has landed, so an idle mirror is watched rather than assumed
15
+ * settled. This only reads status; it never orders a checkout.
16
+ */
17
+ export declare const MIRROR_IDLE_POLL_INTERVAL_MS = 2000;
8
18
  /** Poll cadence while the mirror reports 'syncing'. */
9
19
  export declare const MIRROR_SYNC_POLL_INTERVAL_MS = 500;
10
20
  /** First automatic boot-retry delay after a failed refresh. */
@@ -1,6 +1,7 @@
1
1
  import { type RoutePreviewArtifactBundleV2 } from './routePreviewArtifactV2.js';
2
2
  import { type RoutePreviewArtifactBundleV3, type RoutePreviewArtifactV3Diagnostic, type RoutePreviewArtifactV3Status } from './routePreviewArtifactV3.js';
3
3
  import type { StoryboardEnvironment } from './designImport';
4
+ import type { RoutePreviewFrameRecipe } from './previewBootstrap';
4
5
  import { type InstanceResolver } from './fiberMap';
5
6
  type SlotRelease = () => void;
6
7
  export interface RoutePreviewRecipe {
@@ -64,6 +65,13 @@ export interface RoutePreviewArtifactSeedResult {
64
65
  export interface RoutePreviewArtifactFrameIdentity {
65
66
  id: string;
66
67
  fingerprint?: string;
68
+ /**
69
+ * The recipe the fingerprint stands for, carried so a generator can reproduce
70
+ * it. Without this a host can only re-capture what it declared for the id and
71
+ * label it with the requested fingerprint — the frame then answers "exact"
72
+ * with the screen the axis was supposed to change.
73
+ */
74
+ recipe?: RoutePreviewFrameRecipe;
67
75
  }
68
76
  /** Lifecycle of resolving one frame against an exact artifact endpoint. */
69
77
  export type RoutePreviewArtifactResolution = {
@@ -88,6 +96,7 @@ export type RoutePreviewArtifactFrameRequestResult = {
88
96
  status: 'unavailable';
89
97
  message?: string;
90
98
  notPrepared?: boolean;
99
+ absent?: readonly string[];
91
100
  } | {
92
101
  status: 'rejected';
93
102
  message: string;
@@ -164,7 +173,11 @@ export declare function isRoutePreviewSnapshotHydrated(key: string): boolean;
164
173
  * Persistent cache identity excluding random mirror ports.
165
174
  * By normalizing the object key order, the same screen recipe always uses the same key.
166
175
  */
167
- type RoutePreviewFrameRequester = (frames: readonly RoutePreviewArtifactFrameIdentity[]) => RoutePreviewArtifactFrameRequestResult | Promise<RoutePreviewArtifactFrameRequestResult> | void;
176
+ export interface RoutePreviewFrameRequestOptions {
177
+ /** Where the capture queues among others; see PREVIEW_CAPTURE_PRIORITY. */
178
+ priority?: number;
179
+ }
180
+ type RoutePreviewFrameRequester = (frames: readonly RoutePreviewArtifactFrameIdentity[], options?: RoutePreviewFrameRequestOptions) => RoutePreviewArtifactFrameRequestResult | Promise<RoutePreviewArtifactFrameRequestResult> | void;
168
181
  /** Installed by the host adapter, which owns the transport. */
169
182
  export declare function setRoutePreviewArtifactFrameRequester(requester: RoutePreviewFrameRequester | null): void;
170
183
  /**
@@ -172,6 +185,7 @@ export declare function setRoutePreviewArtifactFrameRequester(requester: RoutePr
172
185
  * only an active or deliberately hovered frame may boot the live application.
173
186
  */
174
187
  export declare function setRoutePreviewArtifactEndpointConfigured(configured: boolean): void;
188
+ export declare function isRoutePreviewArtifactEndpointConfigured(): boolean;
175
189
  export declare function shouldStartRoutePreviewProducer({ detailActive, previewIntended, }: {
176
190
  detailActive: boolean;
177
191
  previewIntended: boolean;
@@ -195,19 +209,25 @@ export declare function shouldRequestRoutePreviewArtifactFrame({ cacheHydrated,
195
209
  */
196
210
  export declare function requestRoutePreviewArtifactFrame(id: string, fingerprint?: string, options?: {
197
211
  retry?: boolean;
212
+ recipe?: RoutePreviewFrameRecipe;
213
+ priority?: number;
198
214
  }): Promise<RoutePreviewArtifactResolution> | null;
199
215
  /**
200
216
  * Asks for many frames' captures in one requester call.
201
217
  *
202
- * The background sweep delegates its interaction screens in chunks, and a
203
- * chunk must not fan out into one transport round trip per frame. Identities
204
- * already resolved exactly, already generating, or already failed are skipped
205
- * (an error is retried only on request); the remaining frames share a single
206
- * requester call and a single resolution, and each identity joins the same
207
- * in-flight dedupe map the single-frame request uses.
218
+ * A canvas being filled and the background sweep both name frames in chunks,
219
+ * and a chunk must not fan out into one transport round trip per frame — nor
220
+ * into one capture job per frame, which is what left the worker's lanes idle.
221
+ * Identities already resolved exactly, already generating, or already failed
222
+ * are skipped (an error is retried only on request); the remaining frames
223
+ * share a single requester call and a single resolution, and each identity
224
+ * joins the same in-flight dedupe map the single-frame request uses. Each
225
+ * frame's recipe travels with it, so a generator can reproduce a variant
226
+ * instead of re-capturing what the host declared for the id.
208
227
  */
209
228
  export declare function requestRoutePreviewArtifactFrames(frames: readonly RoutePreviewArtifactFrameIdentity[], options?: {
210
229
  retry?: boolean;
230
+ priority?: number;
211
231
  }): Promise<RoutePreviewArtifactResolution> | null;
212
232
  export declare function createRoutePreviewRecipeKey(recipe: RoutePreviewRecipe): string;
213
233
  /**
@@ -196,6 +196,12 @@ export interface PageModel {
196
196
  /** Declared interactive axis the frame is currently held in, if any. */
197
197
  interactiveStateId?: string;
198
198
  interactiveOptionId?: string;
199
+ /**
200
+ * Options held together on a combining axis (InteractiveStateDef.multiple).
201
+ * Kept beside the single-option field rather than replacing it so an
202
+ * exclusive axis reads exactly as before.
203
+ */
204
+ interactiveOptionIds?: string[];
199
205
  /**
200
206
  * The case's own environment, kept so clearing an interactive option can put
201
207
  * `environment` back without reconstructing what the case declared. Same shape
@@ -0,0 +1,37 @@
1
+ import type { StoryboardGraphFrameView, StoryboardGraphViewModel } from './storyboardGraphView';
2
+ /**
3
+ * The journey a catalog actually tells, and everything that hangs off it.
4
+ *
5
+ * A storyboard reads as a list of relations: starts, branches, aliases. Each
6
+ * is true and none of them is the thing a person is looking for, which is
7
+ * "where am I in the walk through this product". Measured on one real catalog:
8
+ * 75 screens, 50 declared paths, and 30 of those paths two screens long — so
9
+ * the previous/next of a single frame was, for most frames, a fragment with
10
+ * nothing before or after it. The walk itself was never shown.
11
+ *
12
+ * The spine is the longest run of declared transitions from a start. On that
13
+ * same catalog it is twelve steps — entry through to the recommendations after
14
+ * a recording — and every other screen sits one hop off it. That makes the
15
+ * spine an index a reader can hold: the product's main line, with each
16
+ * detour named under the step it leaves from.
17
+ */
18
+ export interface StoryboardJourney {
19
+ /** Frame ids along the main line, in order. Empty when nothing is declared. */
20
+ spine: readonly string[];
21
+ /** Frames one hop off the spine, keyed by the spine step they leave from. */
22
+ detours: ReadonlyMap<string, readonly string[]>;
23
+ /** Where a frame sits: its spine step, for a frame that is not on the spine. */
24
+ detourParent: ReadonlyMap<string, string>;
25
+ /**
26
+ * Frames the walk never reaches. A frame is not automatically wrong for
27
+ * being here — an error state is a real end — but a catalog where half the
28
+ * screens are unreachable from its own main line is describing that.
29
+ */
30
+ offJourney: readonly string[];
31
+ }
32
+ /** Resolves the journey a model describes. */
33
+ export declare function storyboardJourney(model: StoryboardGraphViewModel): StoryboardJourney;
34
+ /** The step a frame is read against: itself when on the spine, else its parent. */
35
+ export declare function journeyAnchorFor(journey: StoryboardJourney, frameId: string | null | undefined): string | null;
36
+ /** Frames that end the walk — nothing declared follows them. */
37
+ export declare function journeyDeadEnds(model: StoryboardGraphViewModel): readonly StoryboardGraphFrameView[];
@@ -205,7 +205,7 @@ export declare function pygmalionOpenPreview(): void;
205
205
  export declare function pygmalionResetCurrentEdits(): boolean;
206
206
  export interface InitialPageDef extends DesignImportInitialPage {
207
207
  }
208
- export declare function PygmalionEditor({ registry, tokens, initialPages, initialCanvas, initialEditMode, componentConnections, viewportPresets, previewEnvironmentControls, designImport, onTokensChange, appOrigin, previewRevision, previewCacheNamespace, previewArtifacts, previewArtifactEndpoint, previewConcurrency, previewOpen, onPreviewOpenChange, flowCanvas, frameSurface, screenFlows, scenarioCoverage, screenDimensions, screenLists, sectionHeaders, sectionHeaderLabels, screenLanes, frameBranchKinds, frameLabelLabels, screenCards, liveScreenBudget, interactiveStates, surfaceClassifications, onApply, onDesignChange, onInspectApply, onInspectPreview, onInspectImpact, storyboard, storyboardDiscovery, storyboardDiscoveryEndpoint, }: {
208
+ export declare function PygmalionEditor({ registry, tokens, initialPages, initialCanvas, initialEditMode, componentConnections, viewportPresets, previewEnvironmentControls, designImport, onTokensChange, appOrigin, previewRevision, previewCacheNamespace, previewArtifacts, previewArtifactEndpoint, previewConcurrency, previewOpen, onPreviewOpenChange, flowCanvas, frameSurface, captureSupply, screenFlows, screenFlowConcurrency, scenarioCoverage, screenDimensions, screenLists, sectionHeaders, sectionHeaderLabels, screenLanes, frameBranchKinds, frameLabelLabels, screenCards, liveScreenBudget, interactiveStates, surfaceClassifications, onApply, onDesignChange, onInspectApply, onInspectPreview, onInspectImpact, storyboard, storyboardDiscovery, storyboardDiscoveryEndpoint, }: {
209
209
  registry?: ComponentRegistry;
210
210
  tokens?: TokenDef[];
211
211
  initialPages?: InitialPageDef[];
@@ -257,14 +257,34 @@ export declare function PygmalionEditor({ registry, tokens, initialPages, initia
257
257
  * promotion-ready without a mount wait.
258
258
  */
259
259
  frameSurface?: 'bitmap' | 'dom';
260
+ /**
261
+ * Who fills a canvas first. `worker-first` asks the capture worker behind
262
+ * `previewArtifactEndpoint` for the active canvas in batches (nearest the
263
+ * viewport first, other canvases after, in the background) and keeps the
264
+ * walking sessions and per-screen sweep as the fallback for what the worker
265
+ * cannot answer. `browser-first` fills the canvas inside the editor — the
266
+ * walking sessions and sweep — and uses the worker only for the frame the
267
+ * designer selects. Defaults to worker-first when an endpoint is configured
268
+ * and browser-first otherwise.
269
+ */
270
+ captureSupply?: 'worker-first' | 'browser-first';
260
271
  /**
261
272
  * Host-declared flow paths — the captureless surface supply. One hidden
262
273
  * live instance boots per path and walks its waypoints, publishing a
263
274
  * frozen snapshot per screen into the preview store; frames whose screen a
264
275
  * flow claims wait for the delivery instead of booting live themselves.
265
276
  * Screens no flow claims keep the classic artifact/sweep/boot supply.
277
+ * Under worker-first supply, flows walk only the screens the worker
278
+ * released.
266
279
  */
267
280
  screenFlows?: readonly ScreenFlowPath[];
281
+ /**
282
+ * How many flow walks run at once. Defaults to 1, because mocked backends
283
+ * are commonly single-session; a host whose fixtures survive concurrent
284
+ * sessions can raise it — measured, two walks filled a canvas in half the
285
+ * time with no more long tasks on the editor thread.
286
+ */
287
+ screenFlowConcurrency?: number;
268
288
  /**
269
289
  * Host-declared scenario coverage ledger: where each QA scenario is
270
290
  * verified (frame, folded screen state, component toggle, viewport
@@ -1,7 +1,10 @@
1
- import type { StoryboardConnectionMode } from '../canvas/StoryboardConnections';
2
1
  export declare const LayerTree: import("react").FunctionComponent<{
2
+ /**
3
+ * Which half of the panel to draw. 'story' answers where am I going — the
4
+ * canvases and the walk through them; 'frames' answers what is this made of
5
+ * — the frame list and the tree of the one in front of you.
6
+ */
7
+ view?: "story" | "frames";
3
8
  onFrameFocus?: (id: string) => void;
4
- storyboardConnectionMode?: StoryboardConnectionMode;
5
- onStoryboardConnectionModeChange?: (mode: StoryboardConnectionMode) => void;
6
9
  storyboardReady?: boolean;
7
10
  }>;
@@ -1,12 +1,9 @@
1
1
  import type { StoryboardGraphViewModel } from '../editor/storyboardGraphView';
2
2
  import type { StoryboardCompositionModel } from '../editor/storyboardComposition';
3
- import type { StoryboardConnectionMode } from '../canvas/StoryboardConnections';
4
3
  export interface StoryboardGraphPanelProps {
5
4
  model: StoryboardGraphViewModel;
6
5
  onSelectFrame: (frameId: string) => void;
7
6
  activeFrameId?: string | null;
8
- connectionMode: StoryboardConnectionMode;
9
- onConnectionModeChange: (mode: StoryboardConnectionMode) => void;
10
7
  /**
11
8
  * Structure for a canvas with no routes, where a journey graph has nothing
12
9
  * to say. Present only on catalog canvases.
@@ -15,9 +12,4 @@ export interface StoryboardGraphPanelProps {
15
12
  /** Frame names for composition rows, which carry ids rather than frames. */
16
13
  frameNameById?: ReadonlyMap<string, string>;
17
14
  }
18
- /**
19
- * Generic navigation and coverage surface for a project-supplied storyboard
20
- * graph. It never interprets domain labels and delegates camera focus to the
21
- * editor's existing frame-selection action.
22
- */
23
- export declare function StoryboardGraphPanel({ model, onSelectFrame, activeFrameId, connectionMode, onConnectionModeChange, composition, frameNameById, }: StoryboardGraphPanelProps): import("react").JSX.Element | null;
15
+ export declare function StoryboardGraphPanel({ model, onSelectFrame, activeFrameId, composition, frameNameById, }: StoryboardGraphPanelProps): import("react").JSX.Element | null;
@@ -879,6 +879,10 @@ export function pygmalionDevMirrorPlugin(options) {
879
879
  let previewPort = null;
880
880
  let syncPromise = null;
881
881
  let sharedLockPathPromise = null;
882
+ // Assigned once the server is configured. A capture asks for the checkout
883
+ // through the composition handle below, and that demand has to reach the same
884
+ // starter the preview-origin requests use.
885
+ let demandRuntime = () => {};
882
886
  let lastVerifiedAt = 0;
883
887
  let revision = 0;
884
888
  let status = {
@@ -1197,6 +1201,12 @@ export function pygmalionDevMirrorPlugin(options) {
1197
1201
  pygmalion: {
1198
1202
  acquireLease: (label) => acquireDevMirrorLease({ repoRoot, mirrorRoot, label }),
1199
1203
  activeMirrorRoot: () => mirrorRoot,
1204
+ // A capture needs the checkout as much as a live frame does, but it asks
1205
+ // at another plugin's endpoint. Without these two the demand never
1206
+ // reached the starter: the frame had no origin to request, so nothing
1207
+ // ever started the work the frame was waiting for.
1208
+ requestRuntime: () => demandRuntime(),
1209
+ runtimePrepared: () => status.state === 'ready',
1200
1210
  },
1201
1211
  configureServer(server) {
1202
1212
  server.httpServer?.once('close', () => {
@@ -1248,6 +1258,7 @@ export function pygmalionDevMirrorPlugin(options) {
1248
1258
  })
1249
1259
  .catch(() => undefined);
1250
1260
  };
1261
+ demandRuntime = startRuntime;
1251
1262
 
1252
1263
  server.middlewares.use(async (req, res, next) => {
1253
1264
  const url = new URL(req.url ?? '/', 'http://localhost');