@pygmalionjs/pygmalion 0.2.39 → 0.2.41

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.
@@ -1,4 +1,4 @@
1
- import { F as s, N as a, e as r, s as i, a as t } from "./App-CJz8NrxS.js";
1
+ import { F as s, N as a, e as r, s as i, a as t } from "./App-kzN-1Je6.js";
2
2
  export {
3
3
  s as FrozenRoutePreviewView,
4
4
  a as NodeModel,
@@ -85,6 +85,11 @@ function generationAllowed(url) {
85
85
  return !['0', 'false', 'no'].includes(raw.trim().toLowerCase());
86
86
  }
87
87
 
88
+ function resolutionOnly(url) {
89
+ const raw = url.searchParams.get('resolve');
90
+ return raw != null && ['1', 'true', 'yes'].includes(raw.trim().toLowerCase());
91
+ }
92
+
88
93
  function requestedFrames(url) {
89
94
  const raw = url.searchParams.get('frames');
90
95
  if (raw == null) return undefined;
@@ -373,6 +378,15 @@ export function pygmalionPreviewArtifactPlugin({
373
378
  return;
374
379
  }
375
380
  if (wanted !== undefined) {
381
+ if (resolutionOnly(url)) {
382
+ const resolution = await artifactStore.resolveFrameSelection(
383
+ namespace,
384
+ sourceRevision,
385
+ wanted,
386
+ );
387
+ sendJson(response, 200, { ok: true, ...resolution });
388
+ return;
389
+ }
376
390
  let picked = await artifactStore.readFrameSelection(
377
391
  namespace,
378
392
  sourceRevision,
@@ -18,6 +18,7 @@ const LOCK_TIMEOUT_MS = 30_000;
18
18
  const LOCK_STALE_MS = 120_000;
19
19
  const LOCK_POLL_MS = 40;
20
20
  const OBJECT_HASH = /^[a-f0-9]{64}$/u;
21
+ const OBJECT_CACHE_MAX_BYTES = 64 * 1024 * 1024;
21
22
 
22
23
  function sha256(value) {
23
24
  return createHash('sha256').update(value).digest('hex');
@@ -182,8 +183,7 @@ function upsertFrame(entries, next) {
182
183
  (entry) =>
183
184
  entry.id !== next.id ||
184
185
  entry.fingerprint !== next.fingerprint ||
185
- (next.fingerprint == null &&
186
- entry.sourceRevision !== next.sourceRevision),
186
+ entry.sourceRevision !== next.sourceRevision,
187
187
  ),
188
188
  next,
189
189
  ];
@@ -216,6 +216,34 @@ export function createPreviewArtifactStore({
216
216
  const legacyFile = path.resolve(artifactFile);
217
217
  const root = path.resolve(storeDirectory);
218
218
  let legacyCache = null;
219
+ const objectCache = new Map();
220
+ let objectCacheBytes = 0;
221
+
222
+ const rememberObject = (hash, artifact, bytes) => {
223
+ const previous = objectCache.get(hash);
224
+ if (previous) objectCacheBytes -= previous.bytes;
225
+ objectCache.delete(hash);
226
+ objectCache.set(hash, { artifact, bytes });
227
+ objectCacheBytes += bytes;
228
+ while (objectCacheBytes > OBJECT_CACHE_MAX_BYTES && objectCache.size > 1) {
229
+ const oldestHash = objectCache.keys().next().value;
230
+ const oldest = objectCache.get(oldestHash);
231
+ objectCache.delete(oldestHash);
232
+ objectCacheBytes -= oldest?.bytes ?? 0;
233
+ }
234
+ };
235
+
236
+ const forgetObject = (hash) => {
237
+ const cached = objectCache.get(hash);
238
+ if (!cached) return;
239
+ objectCache.delete(hash);
240
+ objectCacheBytes -= cached.bytes;
241
+ };
242
+
243
+ const legacyFileSignature = async () => {
244
+ const stat = await fs.stat(legacyFile);
245
+ return [stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(':');
246
+ };
219
247
 
220
248
  const namespaceHash = (namespace) => sha256(namespace);
221
249
  const manifestPath = (namespace) =>
@@ -242,6 +270,7 @@ export function createPreviewArtifactStore({
242
270
  const serialized = JSON.stringify(artifact);
243
271
  const hash = sha256(serialized);
244
272
  const target = objectPath(hash);
273
+ rememberObject(hash, artifact, Buffer.byteLength(serialized));
245
274
  try {
246
275
  await fs.access(target);
247
276
  return hash;
@@ -253,11 +282,20 @@ export function createPreviewArtifactStore({
253
282
 
254
283
  const readObject = async (hash) => {
255
284
  if (!OBJECT_HASH.test(hash)) return null;
285
+ const cached = objectCache.get(hash);
286
+ if (cached) {
287
+ objectCache.delete(hash);
288
+ objectCache.set(hash, cached);
289
+ return cached.artifact;
290
+ }
256
291
  try {
257
292
  const raw = await fs.readFile(objectPath(hash), 'utf8');
258
293
  const artifact = JSON.parse(raw);
259
- if (sha256(JSON.stringify(artifact)) !== hash) return null;
260
- return validateRoutePreviewArtifactBundle(artifact).valid ? artifact : null;
294
+ const serialized = JSON.stringify(artifact);
295
+ if (sha256(serialized) !== hash) return null;
296
+ if (!validateRoutePreviewArtifactBundle(artifact).valid) return null;
297
+ rememberObject(hash, artifact, Buffer.byteLength(serialized));
298
+ return artifact;
261
299
  } catch {
262
300
  return null;
263
301
  }
@@ -392,7 +430,10 @@ export function createPreviewArtifactStore({
392
430
  await Promise.all(
393
431
  [...previousObjectReferences]
394
432
  .filter((hash) => !retainedObjectReferences.has(hash))
395
- .map((hash) => fs.rm(objectPath(hash), { force: true })),
433
+ .map(async (hash) => {
434
+ await fs.rm(objectPath(hash), { force: true });
435
+ forgetObject(hash);
436
+ }),
396
437
  );
397
438
 
398
439
  if (materializeLegacy) {
@@ -417,6 +458,7 @@ export function createPreviewArtifactStore({
417
458
  if (materialized) {
418
459
  await writeJsonAtomic(legacyFile, materialized);
419
460
  legacyCache = {
461
+ signature: await legacyFileSignature(),
420
462
  hash: sha256(JSON.stringify(materialized)),
421
463
  status: 'valid',
422
464
  artifact: materialized,
@@ -430,6 +472,21 @@ export function createPreviewArtifactStore({
430
472
  };
431
473
 
432
474
  const importLegacyArtifact = async () => {
475
+ let signature;
476
+ try {
477
+ signature = await legacyFileSignature();
478
+ } catch (error) {
479
+ if (error?.code === 'ENOENT') {
480
+ legacyCache = {
481
+ signature: 'missing',
482
+ status: 'missing',
483
+ artifact: null,
484
+ };
485
+ return legacyCache;
486
+ }
487
+ throw error;
488
+ }
489
+ if (legacyCache?.signature === signature) return legacyCache;
433
490
  let raw;
434
491
  try {
435
492
  raw = await fs.readFile(legacyFile, 'utf8');
@@ -441,12 +498,17 @@ export function createPreviewArtifactStore({
441
498
  try {
442
499
  artifact = JSON.parse(raw);
443
500
  } catch {
444
- return { status: 'invalid', artifact: null };
501
+ legacyCache = { signature, status: 'invalid', artifact: null };
502
+ return legacyCache;
445
503
  }
446
504
  const hash = sha256(JSON.stringify(artifact));
447
- if (legacyCache?.hash === hash) return legacyCache;
448
505
  if (!validateRoutePreviewArtifactBundle(artifact).valid) {
449
- legacyCache = { hash, status: 'invalid', artifact: null };
506
+ legacyCache = {
507
+ signature,
508
+ hash,
509
+ status: 'invalid',
510
+ artifact: null,
511
+ };
450
512
  return legacyCache;
451
513
  }
452
514
  // The compatibility contract treats this file as one exact snapshot. Frame
@@ -454,7 +516,7 @@ export function createPreviewArtifactStore({
454
516
  // idempotent entries below ensure restarting Vite cannot promote this seed
455
517
  // above a newer partial capture merely by importing it again.
456
518
  await publishArtifact(artifact, { materializeLegacy: false });
457
- legacyCache = { hash, status: 'valid', artifact };
519
+ legacyCache = { signature, hash, status: 'valid', artifact };
458
520
  return legacyCache;
459
521
  };
460
522
 
@@ -484,20 +546,64 @@ export function createPreviewArtifactStore({
484
546
  return materializeManifest(manifest);
485
547
  };
486
548
 
549
+ const indexFrameCandidates = (manifest, wanted) => {
550
+ const wantedIds = new Set(wanted.map((request) => request.id));
551
+ const candidatesById = new Map();
552
+ for (const entry of manifest?.frames ?? []) {
553
+ if (!wantedIds.has(entry.id)) continue;
554
+ const candidates = candidatesById.get(entry.id) ?? [];
555
+ candidates.push(entry);
556
+ candidatesById.set(entry.id, candidates);
557
+ }
558
+ for (const candidates of candidatesById.values()) {
559
+ candidates.sort((left, right) => right.generation - left.generation);
560
+ }
561
+ return candidatesById;
562
+ };
563
+
564
+ /** Resolves freshness from the compact manifest without loading frame payloads. */
565
+ const resolveFrameSelection = async (namespace, sourceRevision, wanted) => {
566
+ const manifest = await readManifest(namespace);
567
+ const candidatesById = indexFrameCandidates(manifest, wanted);
568
+ const exact = [];
569
+ const missing = [];
570
+ const stale = [];
571
+ for (const request of wanted) {
572
+ const candidates = candidatesById.get(request.id) ?? [];
573
+ if (!candidates.length) {
574
+ missing.push(request.id);
575
+ continue;
576
+ }
577
+ const current = candidates.some(
578
+ (entry) =>
579
+ entry.sourceRevision === sourceRevision &&
580
+ (request.fingerprint == null ||
581
+ entry.fingerprint === request.fingerprint),
582
+ );
583
+ (current ? exact : stale).push(request.id);
584
+ }
585
+ return { exact, missing, stale };
586
+ };
587
+
487
588
  const readFrameSelection = async (namespace, sourceRevision, wanted) => {
488
589
  const manifest = await readManifest(namespace);
489
590
  let aggregate = null;
490
591
  if (manifest) {
592
+ // Index once per read. The earlier path filtered the complete manifest for
593
+ // every wanted frame, which made a storyboard probe O(wanted × entries).
594
+ const candidatesById = indexFrameCandidates(manifest, wanted);
491
595
  for (const request of wanted) {
492
- const candidates = manifest.frames
493
- .filter((entry) => entry.id === request.id)
494
- .sort((a, b) => b.generation - a.generation);
596
+ const candidates = candidatesById.get(request.id) ?? [];
495
597
  const exact = candidates.filter((entry) =>
496
- request.fingerprint != null
497
- ? entry.fingerprint === request.fingerprint
498
- : entry.sourceRevision === sourceRevision,
598
+ entry.sourceRevision === sourceRevision &&
599
+ (request.fingerprint == null ||
600
+ entry.fingerprint === request.fingerprint),
499
601
  );
500
- const ordered = [...exact, ...candidates.filter((entry) => !exact.includes(entry))];
602
+ const exactSet = new Set(exact);
603
+ const ordered = [
604
+ ...exact,
605
+ ...candidates.filter((entry) => !exactSet.has(entry)),
606
+ ];
501
607
  for (const entry of ordered) {
502
608
  const artifact = await readObject(entry.object);
503
609
  if (
@@ -505,7 +611,23 @@ export function createPreviewArtifactStore({
505
611
  artifact.namespace === namespace &&
506
612
  artifact.frames[request.id]
507
613
  ) {
508
- aggregate = mergeRoutePreviewArtifactV3(aggregate, artifact);
614
+ const frame = artifact.frames[request.id];
615
+ const withProvenance = {
616
+ ...artifact,
617
+ frames: {
618
+ ...artifact.frames,
619
+ [request.id]: {
620
+ ...frame,
621
+ ...(entry.sourceRevision == null
622
+ ? {}
623
+ : { sourceRevision: entry.sourceRevision }),
624
+ },
625
+ },
626
+ };
627
+ aggregate = mergeRoutePreviewArtifactV3(
628
+ aggregate,
629
+ withProvenance,
630
+ );
509
631
  break;
510
632
  }
511
633
  }
@@ -514,7 +636,7 @@ export function createPreviewArtifactStore({
514
636
  return selectRoutePreviewArtifactFrames(
515
637
  aggregate ?? emptyBundle(namespace, sourceRevision),
516
638
  wanted,
517
- { includeStale: true },
639
+ { includeStale: true, sourceRevision },
518
640
  );
519
641
  };
520
642
 
@@ -526,5 +648,6 @@ export function createPreviewArtifactStore({
526
648
  readExactArtifact,
527
649
  readFrameSelection,
528
650
  readLatestArtifact,
651
+ resolveFrameSelection,
529
652
  };
530
653
  }
@@ -398,9 +398,9 @@ export function createRoutePreviewArtifactV3(input) {
398
398
  {
399
399
  status: rawCapture.status,
400
400
  viewport: copyViewport(rawCapture.viewport),
401
- // What this frame was captured from. A consumer compares its own
402
- // fingerprint against this one, so a frame survives a revision bump
403
- // whose files it does not depend on.
401
+ // Content identity within the source revision that captured this frame.
402
+ // Store selections preserve the revision as per-frame provenance when
403
+ // frames from separate revisions share one response bundle.
404
404
  ...(rawCapture.fingerprint == null
405
405
  ? {}
406
406
  : { fingerprint: String(rawCapture.fingerprint) }),
@@ -490,6 +490,10 @@ export function validateRoutePreviewArtifactV3(bundle) {
490
490
  errors.push(`Frame "${frameId}" fingerprint is invalid.`);
491
491
  continue;
492
492
  }
493
+ if (!validOptionalIdentifier(rawFrame.sourceRevision)) {
494
+ errors.push(`Frame "${frameId}" source revision is invalid.`);
495
+ continue;
496
+ }
493
497
  const diagnostics = rawFrame.diagnostics;
494
498
  if (
495
499
  !Array.isArray(diagnostics) ||
@@ -622,9 +626,9 @@ function withFrames(bundle, frames) {
622
626
  /**
623
627
  * Picks the frames a consumer asked for and reports what it must still capture.
624
628
  *
625
- * A frame is usable when it exists and its stored fingerprint matches the
626
- * requested one. Requests without a fingerprint accept whatever is stored, which
627
- * keeps the whole-bundle callers of earlier versions working.
629
+ * A frame is usable when it exists and its stored revision and fingerprint match
630
+ * the requested provenance. Callers that omit either constraint retain the
631
+ * looser compatibility behavior of earlier versions.
628
632
  */
629
633
  export function selectRoutePreviewArtifactFrames(bundle, wanted, options = {}) {
630
634
  if (!isPlainRecord(bundle) || !isPlainRecord(bundle.frames)) {
@@ -647,7 +651,12 @@ export function selectRoutePreviewArtifactFrames(bundle, wanted, options = {}) {
647
651
  continue;
648
652
  }
649
653
  const fingerprint = isPlainRecord(request) ? request.fingerprint : undefined;
650
- if (fingerprint != null && frame.fingerprint !== fingerprint) {
654
+ const frameSourceRevision = frame.sourceRevision ?? bundle.sourceRevision;
655
+ if (
656
+ (options.sourceRevision != null &&
657
+ frameSourceRevision !== options.sourceRevision) ||
658
+ (fingerprint != null && frame.fingerprint !== fingerprint)
659
+ ) {
651
660
  stale.push(id);
652
661
  if (options.includeStale === true) frames[id] = frame;
653
662
  continue;
@@ -674,7 +683,23 @@ export function mergeRoutePreviewArtifactV3(base, incoming) {
674
683
  if (base.namespace !== incoming.namespace) {
675
684
  throw new TypeError('Route preview artifacts belong to different namespaces.');
676
685
  }
677
- const frames = { ...base.frames, ...incoming.frames };
686
+ const mixedRevisions = base.sourceRevision !== incoming.sourceRevision;
687
+ const preserveRevision = (frames, sourceRevision) =>
688
+ Object.fromEntries(
689
+ Object.entries(frames).map(([id, frame]) => [
690
+ id,
691
+ mixedRevisions &&
692
+ isPlainRecord(frame) &&
693
+ frame.sourceRevision == null &&
694
+ sourceRevision != null
695
+ ? { ...frame, sourceRevision }
696
+ : frame,
697
+ ]),
698
+ );
699
+ const frames = {
700
+ ...preserveRevision(base.frames, base.sourceRevision),
701
+ ...preserveRevision(incoming.frames, incoming.sourceRevision),
702
+ };
678
703
  if (Object.keys(frames).length > ROUTE_PREVIEW_ARTIFACT_V3_LIMITS.frameCount) {
679
704
  throw new RangeError('Route preview artifact has too many frames.');
680
705
  }
@@ -692,13 +717,15 @@ export function mergeRoutePreviewArtifactV3(base, incoming) {
692
717
  return withFrames(
693
718
  {
694
719
  namespace: incoming.namespace,
695
- // The newest capture names the bundle, but a frame keeps the fingerprint it
696
- // was captured with, so the two no longer have to agree.
697
- ...(incoming.sourceRevision == null
698
- ? base.sourceRevision == null
699
- ? {}
700
- : { sourceRevision: base.sourceRevision }
701
- : { sourceRevision: incoming.sourceRevision }),
720
+ // A mixed bundle has no honest bundle-wide revision. Its frames carry
721
+ // their own provenance instead, including known revisions on both sides.
722
+ ...(mixedRevisions
723
+ ? {}
724
+ : incoming.sourceRevision == null
725
+ ? base.sourceRevision == null
726
+ ? {}
727
+ : { sourceRevision: base.sourceRevision }
728
+ : { sourceRevision: incoming.sourceRevision }),
702
729
  assets,
703
730
  },
704
731
  frames,
@@ -29,6 +29,31 @@ const MAX_SOURCE_BYTES = 1024 * 1024;
29
29
  const MAX_ENVIRONMENT_BYTES = 16 * 1024;
30
30
  const MAX_EXECUTION_RESULTS_BYTES = 64 * 1024 * 1024;
31
31
 
32
+ function normalizeRoutePath(value) {
33
+ if (typeof value !== 'string' || !value.trim()) return null;
34
+ const pathname = value.trim().split(/[?#]/, 1)[0].replace(/\/{2,}/g, '/');
35
+ const rooted = pathname.startsWith('/') ? pathname : `/${pathname}`;
36
+ return rooted.length > 1 ? rooted.replace(/\/+$/, '') : rooted;
37
+ }
38
+
39
+ function routePatternMatches(pattern, concrete) {
40
+ const normalizedPattern = normalizeRoutePath(pattern);
41
+ const normalizedConcrete = normalizeRoutePath(concrete);
42
+ if (!normalizedPattern || !normalizedConcrete) return false;
43
+ const patternSegments = normalizedPattern.split('/').filter(Boolean);
44
+ const concreteSegments = normalizedConcrete.split('/').filter(Boolean);
45
+ let concreteIndex = 0;
46
+ for (const segment of patternSegments) {
47
+ if (segment === '*' || segment.startsWith('*')) return true;
48
+ const concreteSegment = concreteSegments[concreteIndex];
49
+ if (segment.endsWith('?') && concreteSegment == null) continue;
50
+ if (concreteSegment == null) return false;
51
+ if (!segment.startsWith(':') && segment !== concreteSegment) return false;
52
+ concreteIndex += 1;
53
+ }
54
+ return concreteIndex === concreteSegments.length;
55
+ }
56
+
32
57
  function normalizedRelativePath(root, file) {
33
58
  return path.relative(root, file).split(path.sep).join('/');
34
59
  }
@@ -1151,12 +1176,14 @@ function associateRouteCandidates(manifest, graph, fileDigests = new Map()) {
1151
1176
  };
1152
1177
  });
1153
1178
 
1154
- const knownRoutes = new Set(routes.map((route) => route.path));
1179
+ const knownRoutes = routes.map((route) => route.path);
1180
+ const targetIsKnown = (target) =>
1181
+ knownRoutes.some((route) => routePatternMatches(route, target));
1155
1182
  const uniqueEdges = [
1156
1183
  ...new Map(
1157
1184
  edges.map((edge) => [
1158
1185
  `${edge.from}:${edge.to}:${edge.kind}:${edge.evidence.sourcePath}:${edge.evidence.line}`,
1159
- { ...edge, unresolvedTarget: !knownRoutes.has(edge.to) },
1186
+ { ...edge, unresolvedTarget: !targetIsKnown(edge.to) },
1160
1187
  ]),
1161
1188
  ).values(),
1162
1189
  ].sort((a, b) => a.id.localeCompare(b.id));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pygmalionjs/pygmalion",
3
- "version": "0.2.39",
3
+ "version": "0.2.41",
4
4
  "description": "Code-backed DOM design sandbox and visual QA editor",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
package/storyboard.d.ts CHANGED
@@ -318,7 +318,7 @@ export interface RoutePreviewArtifactFrameRequest {
318
318
  export declare function selectRoutePreviewArtifactFrames(
319
319
  bundle: unknown,
320
320
  wanted: readonly (RoutePreviewArtifactFrameRequest | string)[],
321
- options?: { includeStale?: boolean },
321
+ options?: { includeStale?: boolean; sourceRevision?: string },
322
322
  ): {
323
323
  bundle: RoutePreviewArtifactBundleV3;
324
324
  missing: string[];
package/types.d.ts CHANGED
@@ -994,6 +994,8 @@ export interface RoutePreviewArtifactV3ScreenshotAsset {
994
994
  export interface RoutePreviewArtifactV3Frame {
995
995
  readonly status: RoutePreviewArtifactV3Status;
996
996
  readonly viewport: RoutePreviewArtifactV3Viewport;
997
+ readonly sourceRevision?: string;
998
+ readonly fingerprint?: string;
997
999
  readonly snapshot?: RoutePreviewArtifactBundleV2['frames'][string];
998
1000
  readonly screenshot?: RoutePreviewArtifactV3ScreenshotReference;
999
1001
  readonly diagnostics: readonly RoutePreviewArtifactV3Diagnostic[];
@@ -1204,6 +1206,26 @@ export interface StoryboardGraphEdgeInput {
1204
1206
  label?: string;
1205
1207
  }
1206
1208
 
1209
+ export interface StoryboardPathInput {
1210
+ /** Stable author-owned identity used as the edge id prefix. */
1211
+ id: string;
1212
+ /** Ordered screen ids visited by this path. */
1213
+ screenIds: readonly string[];
1214
+ kind?: string;
1215
+ label?: string;
1216
+ }
1217
+
1218
+ export interface StoryboardDefinition {
1219
+ paths?: readonly StoryboardPathInput[];
1220
+ edges?: readonly StoryboardGraphEdgeInput[];
1221
+ startScreenIds?: readonly string[];
1222
+ nonVisualScenarioIds?: readonly string[];
1223
+ }
1224
+
1225
+ export declare function createStoryboardPathEdges(
1226
+ paths: readonly StoryboardPathInput[],
1227
+ ): StoryboardGraphEdgeInput[];
1228
+
1207
1229
  export interface StoryboardGraphBuildOptions {
1208
1230
  screens: readonly DesignScreenCase[];
1209
1231
  assets?: readonly DesignImportAsset[];
@@ -1450,6 +1472,8 @@ export interface StoryboardDiscoveryGraphOptions {
1450
1472
  manifest: StoryboardDiscoveryManifest;
1451
1473
  pages: readonly StoryboardResolvedPage[];
1452
1474
  assets?: readonly DesignImportAsset[];
1475
+ /** Host-authored user journeys layered over source-inferred transitions. */
1476
+ storyboard?: StoryboardDefinition;
1453
1477
  }
1454
1478
 
1455
1479
  export interface StoryboardDiscoveryResult {
@@ -1486,6 +1510,7 @@ export declare function resolveStoryboardScreenCases(
1486
1510
  pages: readonly StoryboardResolvedPage[],
1487
1511
  manifest: StoryboardDiscoveryManifest,
1488
1512
  ): DesignScreenCase[];
1513
+ export declare function createStoryboardRouteScenarioId(route: string): string;
1489
1514
  export declare function getStoryboardDiscoveryBaselineEnvironment(
1490
1515
  manifest: StoryboardDiscoveryManifest,
1491
1516
  ): StoryboardEnvironment | undefined;
@@ -1945,6 +1970,8 @@ export declare function PygmalionEditor(props: {
1945
1970
  registry?: ComponentRegistry;
1946
1971
  tokens?: TokenDef[];
1947
1972
  initialPages?: InitialPageDef[];
1973
+ /** Canvas shown on first mount. Useful when an earlier catalog canvas is expensive to render. */
1974
+ initialCanvas?: string;
1948
1975
  /** Explicit external-design to registered-code component mappings. */
1949
1976
  componentConnections?: readonly DesignComponentConnectionDef[];
1950
1977
  /** Named frame widths the application supports — rendered as one-click buttons beside the W/H boxes. */
@@ -1989,6 +2016,8 @@ export declare function PygmalionEditor(props: {
1989
2016
  ) => Promise<InspectPreviewResult>;
1990
2017
  /** Check the reverse dependency closure of the changed source file. */
1991
2018
  onInspectImpact?: (componentFiles: string[]) => Promise<InspectImpactResult>;
2019
+ /** Authored screen paths and starts merged with source-discovered navigation. */
2020
+ storyboard?: StoryboardDefinition;
1992
2021
  /** Detect portable browser branches from source before route frames boot. Default true. */
1993
2022
  storyboardDiscovery?: boolean;
1994
2023
  /** Vite endpoint that returns the generated generic storyboard environment manifest. */