@agent-scope/render 1.17.1 → 1.17.3

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 (2) hide show
  1. package/README.md +640 -0
  2. package/package.json +4 -3
package/README.md ADDED
@@ -0,0 +1,640 @@
1
+ # `@agent-scope/render`
2
+
3
+ Dual-path render engine for React components. Routes simple (flexbox-only) components through a pure Node.js Satori pipeline, and complex components through a warm Playwright `BrowserPool`. A `RenderMatrix` generates the full Cartesian product of prop axes; `SpriteSheetGenerator` composites the results into a single annotated PNG.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @agent-scope/render
9
+ # or
10
+ bun add @agent-scope/render
11
+ ```
12
+
13
+ Peer dependencies: `react`, `react-dom`.
14
+
15
+ ## What it does / when to use
16
+
17
+ | Use case | Choose |
18
+ |---|---|
19
+ | Flexbox-only component, screenshot needed fast (~8 ms) | `SatoriRenderer` |
20
+ | CSS grid, absolute positioning, animations, real browser CSS | `BrowserPool` |
21
+ | Render all prop combinations in a grid | `RenderMatrix` |
22
+ | Composite results into a single labelled PNG | `SpriteSheetGenerator` |
23
+ | Layout context stress-testing | `contextAxis` + `RenderMatrix` |
24
+ | Edge-case prop stress-testing | `stressAxis` + `RenderMatrix` |
25
+
26
+ ---
27
+
28
+ ## Architecture
29
+
30
+ ```
31
+ complexityClass?
32
+
33
+ ┌────────────────┴────────────────┐
34
+ "simple" "complex"
35
+ │ │
36
+ SatoriRenderer BrowserPool
37
+ React → Satori SVG Playwright pages
38
+ → resvg-js PNG (inject-don't-navigate)
39
+ │ │
40
+ └─────────────┬────────────────────┘
41
+
42
+ RenderResult
43
+
44
+ ┌─────────────┴───────────────┐
45
+ RenderMatrix SpriteSheetGenerator
46
+ (Cartesian product) (PNG composite)
47
+ ```
48
+
49
+ ### Satori path
50
+
51
+ `SatoriRenderer` runs entirely in Node.js with no browser overhead:
52
+
53
+ 1. Load font (Inter `.woff` by default; custom TTF/OTF/WOFF supported — **not** woff2, opentype.js limitation)
54
+ 2. Wrap element with mock providers (`ThemeContext`, `LocaleContext`, generic fallbacks)
55
+ 3. Call `satori(element, { width, height, fonts })` → SVG string
56
+ 4. Call `new Resvg(svg).render().asPng()` → PNG `Buffer`
57
+
58
+ Target: **~8 ms** per render at 375×812.
59
+
60
+ ### BrowserPool path
61
+
62
+ `BrowserPool` maintains N browser instances × M pages per browser. The "inject-don't-navigate" pattern keeps pages warm:
63
+
64
+ 1. `init()` launches Chromium instances and pre-loads each page **once** with a skeleton HTML containing `window.__renderComponent`, `window.__componentRegistry`, and `window.__registerComponent`
65
+ 2. `render(name, props)` calls `page.evaluate(() => window.__renderComponent(name, props))` — no `page.goto()` between renders
66
+ 3. Waits for `window.__renderReady === true`
67
+ 4. Screenshots the component bounding box (not the full viewport)
68
+ 5. Optionally extracts DOM tree, computed styles, console output, and accessibility info
69
+
70
+ ---
71
+
72
+ ## API Reference
73
+
74
+ ### `SatoriRenderer`
75
+
76
+ ```typescript
77
+ import { SatoriRenderer } from '@agent-scope/render';
78
+
79
+ const renderer = new SatoriRenderer(config?: RendererConfig);
80
+ ```
81
+
82
+ **`RendererConfig`**
83
+
84
+ ```typescript
85
+ interface RendererConfig {
86
+ /** Path to a TTF/OTF/WOFF font file. NOT woff2 — opentype.js limitation.
87
+ * Defaults to bundled Inter woff (@fontsource/inter). */
88
+ fontPath?: string;
89
+ /** Font family name. Defaults to "Inter". */
90
+ fontFamily?: string;
91
+ /** Default viewport. Defaults to { width: 375, height: 812 }. */
92
+ defaultViewport?: ViewportOptions;
93
+ }
94
+ ```
95
+
96
+ **Methods**
97
+
98
+ ```typescript
99
+ // Pre-load and cache the font (call before the first render for consistent timing).
100
+ renderer.preloadFont(): Promise<void>
101
+
102
+ // Render a React element to PNG.
103
+ renderer.render(
104
+ element: React.ReactElement,
105
+ options?: SatoriRenderOptions,
106
+ descriptor?: Pick<ComponentDescriptor, 'requiredContexts'>,
107
+ ): Promise<RenderResult>
108
+
109
+ // Shorthand: render at explicit viewport size.
110
+ renderer.renderAt(
111
+ element: React.ReactElement,
112
+ width: number,
113
+ height: number,
114
+ descriptor?: Pick<ComponentDescriptor, 'requiredContexts'>,
115
+ ): Promise<RenderResult>
116
+ ```
117
+
118
+ **`SatoriRenderOptions`**
119
+
120
+ ```typescript
121
+ interface SatoriRenderOptions {
122
+ viewport?: { width: number; height: number };
123
+ container?: Partial<ContainerOptions>;
124
+ capture?: Partial<CaptureOptions>;
125
+ environment?: EnvironmentOptions;
126
+ }
127
+
128
+ interface ContainerOptions {
129
+ type: 'centered' | 'flex-row' | 'flex-col' | 'none';
130
+ padding: number; // pixels, applied to all sides
131
+ background?: string; // CSS colour string
132
+ }
133
+
134
+ interface CaptureOptions {
135
+ screenshot: boolean; // default: true
136
+ styles: boolean; // default: true
137
+ timing: boolean; // default: true
138
+ }
139
+
140
+ interface EnvironmentOptions {
141
+ viewport?: ViewportOptions;
142
+ theme?: string; // passed to mock ThemeContext
143
+ locale?: string; // e.g. "en-US"
144
+ }
145
+ ```
146
+
147
+ **Example**
148
+
149
+ ```typescript
150
+ import React from 'react';
151
+ import { SatoriRenderer } from '@agent-scope/render';
152
+
153
+ const renderer = new SatoriRenderer();
154
+ await renderer.preloadFont();
155
+
156
+ const result = await renderer.render(
157
+ <button style={{ padding: 16, background: '#0070f3', color: '#fff' }}>
158
+ Click me
159
+ </button>,
160
+ { viewport: { width: 375, height: 200 } },
161
+ );
162
+
163
+ console.log(result.width, result.height, result.renderTimeMs);
164
+ // → 375 200 7.4
165
+ ```
166
+
167
+ ---
168
+
169
+ ### `BrowserPool`
170
+
171
+ ```typescript
172
+ import { BrowserPool } from '@agent-scope/render';
173
+
174
+ const pool = new BrowserPool(config?: BrowserPoolConfig);
175
+ await pool.init();
176
+ ```
177
+
178
+ **`BrowserPoolConfig`**
179
+
180
+ ```typescript
181
+ interface BrowserPoolConfig {
182
+ /**
183
+ * Named size preset. Mutually exclusive with `size`.
184
+ * Defaults to "local".
185
+ *
186
+ * | Preset | browsers | pagesPerBrowser | max concurrency |
187
+ * |--------------|----------|-----------------|-----------------|
188
+ * | local | 1 | 5 | 5 |
189
+ * | ci-standard | 3 | 15 | 45 |
190
+ * | ci-large | 6 | 20 | 120 |
191
+ */
192
+ preset?: 'local' | 'ci-standard' | 'ci-large';
193
+ /** Custom size config — overrides preset when provided. */
194
+ size?: { browsers: number; pagesPerBrowser: number };
195
+ viewportWidth?: number; // default: 1440
196
+ viewportHeight?: number; // default: 900
197
+ /** Max ms to wait for a free slot before acquire() rejects. Default: 30_000. */
198
+ acquireTimeoutMs?: number;
199
+ }
200
+ ```
201
+
202
+ **Methods**
203
+
204
+ ```typescript
205
+ // Launch browsers and pre-load skeleton HTML. Must be called before render().
206
+ pool.init(): Promise<void>
207
+
208
+ // Render a registered component by name.
209
+ pool.render(
210
+ componentName: string,
211
+ props?: Record<string, unknown>,
212
+ options?: RenderOptions,
213
+ ): Promise<RenderResult>
214
+
215
+ // Acquire a free page slot manually (advanced — prefer render()).
216
+ pool.acquire(): Promise<PageSlot>
217
+
218
+ // Return a slot to the pool (wakes the next queued waiter).
219
+ pool.release(slot: PageSlot): void
220
+
221
+ // Gracefully drain in-flight renders then close all browsers.
222
+ pool.close(): Promise<void>
223
+
224
+ // Introspection
225
+ pool.totalSlots: number
226
+ pool.freeSlots: number
227
+ pool.activeSlots: number
228
+ pool.queueDepth: number
229
+ pool.isInitialized: boolean
230
+ pool.isClosed: boolean
231
+ ```
232
+
233
+ **`RenderOptions`**
234
+
235
+ ```typescript
236
+ interface RenderOptions {
237
+ captureDom?: boolean; // capture DOM tree. Default: false
238
+ captureStyles?: boolean; // capture computed styles. Default: true
239
+ captureConsole?: boolean; // capture console output. Default: false
240
+ captureA11y?: boolean; // capture accessibility snapshot. Default: false
241
+ viewportWidth?: number; // override pool-level viewport
242
+ viewportHeight?: number;
243
+ timeoutMs?: number; // wait for window.__renderReady. Default: 10_000
244
+ screenshotPadding?: number; // px around bounding box crop. Default: 24
245
+ minScreenshotWidth?: number; // Default: 320
246
+ minScreenshotHeight?: number;// Default: 200
247
+ }
248
+ ```
249
+
250
+ **Example**
251
+
252
+ ```typescript
253
+ import { BrowserPool } from '@agent-scope/render';
254
+
255
+ const pool = new BrowserPool({ preset: 'ci-standard' });
256
+ await pool.init();
257
+
258
+ const result = await pool.render('Button', { label: 'Click me' }, {
259
+ captureDom: true,
260
+ captureConsole: true,
261
+ });
262
+
263
+ // result.dom.elementCount, result.console.errors, etc.
264
+ await pool.close();
265
+ ```
266
+
267
+ **Pool exhaustion & queuing** (from `browser-pool.test.ts`)
268
+
269
+ When all slots are busy, `acquire()` queues callers FIFO and resumes them on `release()`. With `acquireTimeoutMs: 50` and one slot:
270
+
271
+ ```typescript
272
+ const slot = await pool.acquire();
273
+ // Second acquire times out after 50 ms when no slot is released:
274
+ await pool.acquire(); // throws: "BrowserPool.acquire() timed out after 50ms"
275
+ pool.release(slot);
276
+ ```
277
+
278
+ ---
279
+
280
+ ### `RenderMatrix`
281
+
282
+ Generates the Cartesian product of all axis value combinations and renders each cell.
283
+
284
+ ```typescript
285
+ import { RenderMatrix } from '@agent-scope/render';
286
+
287
+ const matrix = new RenderMatrix(
288
+ renderer, // anything implementing MatrixRenderer
289
+ axes, // MatrixAxis[]
290
+ options?, // { complexityClass?, concurrency? }
291
+ );
292
+
293
+ const result: MatrixResult = await matrix.render();
294
+ ```
295
+
296
+ **`MatrixAxis`**
297
+
298
+ ```typescript
299
+ interface MatrixAxis<T = unknown> {
300
+ name: string; // axis name, used as prop key
301
+ values: T[]; // values along this axis
302
+ }
303
+ ```
304
+
305
+ **`MatrixRenderer` interface**
306
+
307
+ ```typescript
308
+ interface MatrixRenderer {
309
+ renderCell(
310
+ props: Record<string, unknown>,
311
+ complexityClass: ComplexityClass,
312
+ ): Promise<RenderResult>;
313
+ }
314
+ ```
315
+
316
+ Both `SatoriRenderer` and `BrowserPool` can be adapted to this interface.
317
+
318
+ **`MatrixResult`**
319
+
320
+ ```typescript
321
+ interface MatrixResult {
322
+ cells: MatrixCell[]; // flat list, row-major order
323
+ axes: MatrixAxis[];
324
+ axisLabels: string[][]; // [axisIndex][valueIndex] → display label
325
+ stats: MatrixStats;
326
+ rows: number; // last axis cardinality
327
+ cols: number; // product of remaining axes
328
+ }
329
+
330
+ interface MatrixCell {
331
+ props: Record<string, unknown>; // axis name → value for this cell
332
+ result: RenderResult;
333
+ index: number; // flat row-major index
334
+ axisIndices: number[]; // per-axis indices
335
+ }
336
+
337
+ interface MatrixStats {
338
+ totalCells: number;
339
+ totalRenderTimeMs: number;
340
+ avgRenderTimeMs: number;
341
+ minRenderTimeMs: number;
342
+ maxRenderTimeMs: number;
343
+ wallClockTimeMs: number;
344
+ }
345
+ ```
346
+
347
+ **Cell ordering** (from `matrix.test.ts`)
348
+
349
+ First axis iterates slowest (row-major). For axes `[['A','B'], [1,2]]`:
350
+
351
+ ```
352
+ index 0 → { x: 'A', y: 1 }
353
+ index 1 → { x: 'A', y: 2 }
354
+ index 2 → { x: 'B', y: 1 }
355
+ index 3 → { x: 'B', y: 2 }
356
+ ```
357
+
358
+ **Example: 3×3×2 = 18 cells**
359
+
360
+ ```typescript
361
+ const matrix = new RenderMatrix(renderer, [
362
+ { name: 'variant', values: ['primary', 'secondary', 'ghost'] },
363
+ { name: 'size', values: ['sm', 'md', 'lg'] },
364
+ { name: 'disabled', values: [false, true] },
365
+ ]);
366
+
367
+ const result = await matrix.render();
368
+ // result.cells.length === 18
369
+ // result.stats.avgRenderTimeMs — average per-cell render time
370
+ ```
371
+
372
+ **Grid dimensions**
373
+
374
+ ```
375
+ rows = last axis cardinality
376
+ cols = totalCells / rows
377
+
378
+ // Single-axis: [{ name: 'v', values: ['a','b','c'] }]
379
+ // → rows: 3, cols: 1
380
+
381
+ // Two axes: [2 values, 3 values]
382
+ // → rows: 3, cols: 2, cells: 6
383
+ ```
384
+
385
+ **`cartesianProduct` utility**
386
+
387
+ ```typescript
388
+ import { cartesianProduct } from '@agent-scope/render';
389
+
390
+ cartesianProduct([[1,2], ['a','b']]);
391
+ // → [[1,'a'], [1,'b'], [2,'a'], [2,'b']]
392
+ ```
393
+
394
+ ---
395
+
396
+ ### `SpriteSheetGenerator`
397
+
398
+ Composites a `MatrixResult` into a single labelled PNG.
399
+
400
+ ```typescript
401
+ import { SpriteSheetGenerator } from '@agent-scope/render';
402
+
403
+ const gen = new SpriteSheetGenerator(options?: SpriteSheetOptions);
404
+ const { png, coordinates, width, height } = await gen.generate(matrixResult);
405
+ ```
406
+
407
+ **`SpriteSheetOptions`**
408
+
409
+ ```typescript
410
+ interface SpriteSheetOptions {
411
+ cellPadding?: number; // px around each cell. Default: 8
412
+ borderWidth?: number; // border thickness. Default: 1
413
+ background?: string; // CSS hex. Default: "#f5f5f5"
414
+ borderColor?: string; // Default: "#cccccc"
415
+ labelHeight?: number; // top label row height. Default: 32
416
+ labelWidth?: number; // left label column width. Default: 80
417
+ labelDpi?: number; // label text DPI. Default: 72
418
+ labelBackground?: string; // Default: "#e8e8e8"
419
+ }
420
+ ```
421
+
422
+ **`SpriteSheetResult`**
423
+
424
+ ```typescript
425
+ interface SpriteSheetResult {
426
+ png: Buffer; // full composite PNG
427
+ coordinates: CellCoordinateMap; // CellBounds[] indexed by flat cell index
428
+ width: number; // total sprite sheet width in pixels
429
+ height: number; // total sprite sheet height in pixels
430
+ }
431
+
432
+ interface CellBounds {
433
+ x: number; // left edge in sprite sheet
434
+ y: number; // top edge in sprite sheet
435
+ width: number; // content width (excludes padding)
436
+ height: number; // content height (excludes padding)
437
+ }
438
+ ```
439
+
440
+ **Layout rules** (from `sprite-sheet.test.ts`)
441
+
442
+ - Cells in the same **column** share the same `x` coordinate
443
+ - Cells in the same **row** share the same `y` coordinate
444
+ - Row `y` values increase monotonically: `coords[0].y < coords[1].y < coords[2].y`
445
+ - Larger `cellPadding` → larger total sprite sheet dimensions
446
+
447
+ ```typescript
448
+ const gen = new SpriteSheetGenerator({ cellPadding: 12, background: '#fff' });
449
+ const { png, coordinates } = await gen.generate(matrixResult);
450
+
451
+ // Access cell 3's position:
452
+ const { x, y, width, height } = coordinates[3];
453
+ ```
454
+
455
+ ---
456
+
457
+ ### Composition Contexts
458
+
459
+ Ten built-in layout environments for use as `RenderMatrix` axes.
460
+
461
+ ```typescript
462
+ import { contextAxis, COMPOSITION_CONTEXTS, getContext } from '@agent-scope/render';
463
+ ```
464
+
465
+ **Available context IDs**
466
+
467
+ | ID | Description |
468
+ |---|---|
469
+ | `centered` | Centered in viewport (flex, align/justify center) |
470
+ | `flex-row` | Flex row with sibling placeholders |
471
+ | `flex-col` | Flex column with sibling placeholders |
472
+ | `grid` | CSS grid cell (3-column auto-fill) |
473
+ | `sidebar` | Narrow 240 px sidebar |
474
+ | `scroll` | Scrollable container, fixed 300 px height |
475
+ | `full-width` | Stretches to full viewport width |
476
+ | `constrained` | Centered, max-width 1024 px |
477
+ | `rtl` | `dir="rtl"`, `lang="ar"` — bidi layout testing |
478
+ | `nested-flex` | Three levels of nested flex containers |
479
+
480
+ **Usage**
481
+
482
+ ```typescript
483
+ import { RenderMatrix, contextAxis } from '@agent-scope/render';
484
+
485
+ // All 10 contexts × 2 variants = 20 cells
486
+ const matrix = new RenderMatrix(renderer, [
487
+ contextAxis(['centered', 'flex-row', 'sidebar', 'rtl']),
488
+ { name: 'variant', values: ['primary', 'secondary'] },
489
+ ]);
490
+
491
+ // Or use all 10:
492
+ const allContextMatrix = new RenderMatrix(renderer, [
493
+ contextAxis(),
494
+ { name: 'size', values: ['sm', 'md', 'lg'] },
495
+ ]);
496
+ ```
497
+
498
+ **Helper functions**
499
+
500
+ ```typescript
501
+ getContext('rtl') // → CompositionContext
502
+ getContexts(['rtl', 'centered']) // → CompositionContext[]
503
+ contextAxis(['centered', 'rtl', 'grid']) // → MatrixAxis<CompositionContext>
504
+ ```
505
+
506
+ ---
507
+
508
+ ### Content Stress Presets
509
+
510
+ Seven categories of edge-case prop values for `RenderMatrix` axes.
511
+
512
+ ```typescript
513
+ import { stressAxis, getStressPreset, ALL_STRESS_PRESETS } from '@agent-scope/render';
514
+ ```
515
+
516
+ **Available categories**
517
+
518
+ | ID | Values | What it exercises |
519
+ |---|---|---|
520
+ | `text.short` | `""`, `" "`, `"A"`, `"OK"`, `"Hello"`, `"Submit"`, `"Cancel"` | Empty, whitespace, single-char |
521
+ | `text.long` | 80–200 char strings, long words, 100-char runs | Overflow, text wrapping |
522
+ | `text.unicode` | Japanese, Chinese, Korean, diacritics, emoji, zero-width space, RTL override | CJK, emoji, bidi |
523
+ | `text.rtl` | Arabic and Hebrew strings, mixed Arabic+LTR | Right-to-left layout |
524
+ | `number.edge` | `0`, `-0`, `1`, `-1`, `NaN`, `Infinity`, `-Infinity`, `999999`, `MAX_SAFE_INTEGER` | Numeric extremes |
525
+ | `list.count` | `[]`, 1-item, 3-item, 10-item, 100-item, 1000-item arrays | Empty/huge lists |
526
+ | `image.states` | `null`, `""`, broken URL, 1×1 data URI, 200×200, 4000×4000 | Image error states |
527
+
528
+ **Usage**
529
+
530
+ ```typescript
531
+ import { RenderMatrix, stressAxis } from '@agent-scope/render';
532
+
533
+ // 7 short-text values × 2 disabled states = 14 cells
534
+ const matrix = new RenderMatrix(renderer, [
535
+ stressAxis('text.short'),
536
+ { name: 'disabled', values: [false, true] },
537
+ ]);
538
+
539
+ // Direct preset access:
540
+ const preset = getStressPreset('number.edge');
541
+ // preset.values → [0, -0, 1, -1, NaN, Infinity, ...]
542
+ // preset.valueLabels → ['zero', 'neg-zero', 'one', ...]
543
+ ```
544
+
545
+ ---
546
+
547
+ ### `RenderResult`
548
+
549
+ Shared return type for both render paths.
550
+
551
+ ```typescript
552
+ interface RenderResult {
553
+ screenshot: Buffer; // PNG as a Buffer
554
+ width: number; // pixel width of the rendered component
555
+ height: number; // pixel height
556
+ renderTimeMs: number; // wall-clock render time in ms
557
+ computedStyles: Record<string, Record<string, string>>;
558
+ // BrowserPool only (when captureXxx options enabled):
559
+ dom?: { tree: DOMNode; elementCount: number; boundingBox: BoundingBox };
560
+ console?: { errors: string[]; warnings: string[]; logs: string[] };
561
+ accessibility?: { role: string; name: string; violations: string[] };
562
+ }
563
+ ```
564
+
565
+ The `computedStyles` key for the BrowserPool path is `"[data-reactscope-root] > *"` and captures: `display`, `flexDirection`, `flexWrap`, `alignItems`, `justifyContent`, `gridTemplateColumns`, `position`, `width`, `height`, `color`, `backgroundColor`, `fontSize`, `fontFamily`, `fontWeight`, `borderRadius`, `boxShadow`, `opacity`, `transform`, and more.
566
+
567
+ ---
568
+
569
+ ### Structured error handling
570
+
571
+ ```typescript
572
+ import { safeRender, RenderError, detectHeuristicFlags } from '@agent-scope/render';
573
+ // or from the sub-path:
574
+ import { safeRender } from '@agent-scope/render/errors';
575
+
576
+ const outcome = await safeRender(
577
+ () => pool.render('Button', props),
578
+ { props, sourceLocation: { file: 'Button.tsx', line: 12, column: 0 } },
579
+ );
580
+
581
+ if (outcome.crashed) {
582
+ console.error(outcome.error.heuristicFlags);
583
+ // e.g. ['MISSING_PROVIDER', 'TYPE_MISMATCH']
584
+ } else {
585
+ const png = outcome.result.screenshot;
586
+ }
587
+ ```
588
+
589
+ **`HeuristicFlag` values**: `MISSING_PROVIDER`, `UNDEFINED_PROP`, `TYPE_MISMATCH`, `NETWORK_REQUIRED`, `ASYNC_NOT_SUSPENDED`, `HOOK_CALL_VIOLATION`, `ELEMENT_TYPE_INVALID`, `HYDRATION_MISMATCH`, `CIRCULAR_DEPENDENCY`, `UNKNOWN_ERROR`.
590
+
591
+ ---
592
+
593
+ ## Real test payloads
594
+
595
+ ### Matrix: 2×2 produces 4 cells with correct prop sets (from `matrix.test.ts`)
596
+
597
+ ```typescript
598
+ const matrix = new RenderMatrix(renderer, [
599
+ { name: 'variant', values: ['primary', 'secondary'] },
600
+ { name: 'size', values: ['sm', 'md'] },
601
+ ]);
602
+ const result = await matrix.render();
603
+
604
+ // result.cells.map(c => c.props):
605
+ // [{ variant: 'primary', size: 'sm' }, { variant: 'primary', size: 'md' },
606
+ // { variant: 'secondary', size: 'sm' }, { variant: 'secondary', size: 'md' }]
607
+
608
+ // result.stats:
609
+ // { totalCells: 4, avgRenderTimeMs: 12.5, minRenderTimeMs: 5, maxRenderTimeMs: 20 }
610
+ ```
611
+
612
+ ### BrowserPool: clipped screenshot dimensions match bounding box (from `browser-pool.test.ts`)
613
+
614
+ ```typescript
615
+ // Component bounding box: 40×40, viewport: 1440×900
616
+ // result.width === 40, result.height === 40 (not 1440×900)
617
+ ```
618
+
619
+ ### SpriteSheet: coordinate alignment (from `sprite-sheet.test.ts`)
620
+
621
+ ```typescript
622
+ const matrix = makeMockMatrixResult(3, 2); // 3 rows, 2 cols
623
+ const { coordinates } = await gen.generate(matrix);
624
+
625
+ // Col 0: cells 0,1,2 share x:
626
+ coordinates[0].x === coordinates[1].x === coordinates[2].x
627
+
628
+ // Row 0: cells 0 and 3 share y:
629
+ coordinates[0].y === coordinates[3].y
630
+
631
+ // y increases down rows:
632
+ coordinates[0].y < coordinates[1].y < coordinates[2].y
633
+ ```
634
+
635
+ ---
636
+
637
+ ## Used by
638
+
639
+ - `@agent-scope/cli` — invokes `BrowserPool` and `SatoriRenderer` to render component screenshots during `scope render` runs
640
+ - `@agent-scope/manifest` — provides `ComplexityClass` to route renders between paths
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-scope/render",
3
- "version": "1.17.1",
3
+ "version": "1.17.3",
4
4
  "description": "Render paths for React components: Satori (simple/flexbox) and Playwright BrowserPool (complex)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,7 +30,8 @@
30
30
  "module": "./dist/index.js",
31
31
  "types": "./dist/index.d.ts",
32
32
  "files": [
33
- "dist"
33
+ "dist",
34
+ "README.md"
34
35
  ],
35
36
  "scripts": {
36
37
  "build": "tsup",
@@ -42,7 +43,7 @@
42
43
  "dependencies": {
43
44
  "@fontsource/inter": "^5.2.8",
44
45
  "@resvg/resvg-js": "^2.6.2",
45
- "@agent-scope/manifest": "1.17.1",
46
+ "@agent-scope/manifest": "1.17.3",
46
47
  "playwright": "^1.50.0",
47
48
  "react": "^18.3.1",
48
49
  "react-dom": "^18.3.1",