autokap 1.5.4 → 1.5.7

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.
@@ -47,6 +47,8 @@ Fallback when CSS selector fails. The runtime resolves via Playwright semantic l
47
47
  | `threshold` | number (0-1) | `screenshot_stable` | Pixel diff tolerance. Default: `0.01` |
48
48
  | `waitMs` | number | all except `always` | Max polling time (ms). Default: `5000` |
49
49
 
50
+ `any_change` is verifier-backed: the runtime must observe a URL, DOM, element state, overlay, or scroll change after the action. No-op actions fail.
51
+
50
52
  ---
51
53
 
52
54
  ## NAVIGATE
@@ -353,6 +355,8 @@ Assert the current URL matches a pattern. Pure validation, no navigation.
353
355
  **Can:** Validate the browser is on the expected page. Useful as a checkpoint before capture.
354
356
  **Cannot:** Navigate or change the URL. Use NAVIGATE for that.
355
357
 
358
+ `urlPattern` is the assertion source of truth. Use `postcondition: { "type": "always" }` or repeat the same `route_matches` pattern.
359
+
356
360
  ```json
357
361
  { "kind": "ASSERT_ROUTE", "urlPattern": "/dashboard/**", "postcondition": { "type": "route_matches", "pattern": "/dashboard/**" } }
358
362
  ```
@@ -369,6 +373,8 @@ Assert that specific elements are visible on the page. Pure validation.
369
373
  **Can:** Validate page state before capture. Confirm multiple elements are rendered.
370
374
  **Cannot:** Interact with elements or wait for them. Use WAIT_FOR for dynamic elements.
371
375
 
376
+ `selectors` and `matchAll` are the assertion source of truth. Use `postcondition: { "type": "always" }` or an `element_visible` postcondition for one of the asserted selectors.
377
+
372
378
  ```json
373
379
  { "kind": "ASSERT_SURFACE", "selectors": ["[data-ak=\"header\"]", "[data-ak=\"sidebar\"]"], "matchAll": true, "postcondition": { "type": "always" } }
374
380
  ```
@@ -382,16 +388,23 @@ Capture a screenshot of the viewport or a specific element.
382
388
  | `captureId` | string | no | Stable identifier for Studio/dev links. Should match preset page/element id |
383
389
  | `captureName` | string | no | Human-readable label shown in Studio |
384
390
  | `elementSelector` | string | no | CSS selector for element-level capture (crops to element bounds) |
391
+ | `outscale` | OutscaleConfig | no | Padding around the captured element (only applied with `elementSelector`). Typically set by the user post-generation. Omit by default. |
385
392
 
386
393
  **Can:** Full-page viewport capture, element-level capture (cropped), LLM verification (detects blank/error/loading/overlay states), alt text generation, favicon extraction.
387
394
  **Cannot:** Capture content below the fold in a single shot (use SCROLL first). Capture cross-origin iframe content.
388
395
 
389
396
  **Tip:** Without `elementSelector`, captures the full viewport. With `elementSelector`, captures only that element's bounding box — useful for component-level screenshots.
390
397
 
398
+ **`outscale` shape:** all fields optional. `padding` is uniform (pixels). `paddingTop/Right/Bottom/Left` override per side. `paddingPercent` (0–100) scales with the element. `clampToViewport` (default `true`) prevents the crop from exceeding the document. `backgroundColor` fills any uncovered area.
399
+
391
400
  ```json
392
401
  { "kind": "CAPTURE_SCREENSHOT", "captureId": "dashboard-main", "captureName": "Dashboard", "elementSelector": "[data-ak=\"main-content\"]", "postcondition": { "type": "always" } }
393
402
  ```
394
403
 
404
+ ```json
405
+ { "kind": "CAPTURE_SCREENSHOT", "captureName": "Pricing card", "elementSelector": "[data-ak=\"pricing\"]", "outscale": { "padding": 24 }, "postcondition": { "type": "always" } }
406
+ ```
407
+
395
408
  ## BEGIN_CLIP
396
409
 
397
410
  Start recording a clip. All interactions between BEGIN_CLIP and END_CLIP are recorded.
@@ -150,7 +150,7 @@ interface VariantSpec {
150
150
  | `DISMISS_OVERLAYS` | no | — | `overlay_dismissed` | Always after NAVIGATE |
151
151
  | `CLICK` | yes | `button?` | `element_visible` / `route_matches` | Postcondition = what CHANGED |
152
152
  | `TYPE` | yes | `text`, `clearFirst` | `any_change` | `{{email}}` / `{{password}}` for creds |
153
- | `PRESS_KEY` | no | `key` | `any_change` | `"Enter"`, `"Escape"`, `"Tab"`, etc. |
153
+ | `PRESS_KEY` | no | `key` | specific result / `any_change` | `"Enter"`, `"Escape"`, `"Tab"`, etc.; use `any_change` only when the key must visibly change state |
154
154
  | `WAIT_FOR` | yes* | `state` | `element_visible` | `"visible"` or `"attached"` (DOM only) |
155
155
  | `SLEEP` | no | `durationMs` (1..60000), `narrationTextByLocale?` | `always` | Pause N ms. Reserved for video narration anchors. Use `durationMs: 1` as a placeholder; AutoKap rewrites it during `autokap run` after generating TTS. |
156
156
  | `SCROLL` | no | `direction`, `targetSelector?`, `amount?` | `element_visible` | Use `targetSelector` for precise scroll |
@@ -161,9 +161,9 @@ interface VariantSpec {
161
161
  | `DRAG` | yes | `toSelector?` / `toTarget?` / `offset?` | `element_visible` / `any_change` | Animated cursor A→B. Use `toSelector` for Kanban-style drops, `offset` for sliders / canvas |
162
162
  | `SET_LOCALE` | no | `locale`, `method`, `storageHints?` | `always` | Use `"$variant"`. Prefer `method: "storage"` |
163
163
  | `SET_THEME` | no | `theme`, `method`, `storageHints?` | `always` | Use `"$variant"`. Prefer `method: "storage"` |
164
- | `ASSERT_ROUTE` | no | `urlPattern` | `route_matches` | Validation checkpoint |
165
- | `ASSERT_SURFACE` | no | `selectors[]`, `matchAll` | `always` | Validation checkpoint |
166
- | `CAPTURE_SCREENSHOT` | no | `captureId`, `captureName`, `elementSelector?` | `always` | `elementSelector` for element-level crop |
164
+ | `ASSERT_ROUTE` | no | `urlPattern` | `always` / matching `route_matches` | `urlPattern` is the source of truth |
165
+ | `ASSERT_SURFACE` | no | `selectors[]`, `matchAll` | `always` / matching `element_visible` | `selectors` + `matchAll` are the source of truth |
166
+ | `CAPTURE_SCREENSHOT` | no | `captureId`, `captureName`, `elementSelector?`, `outscale?` | `always` | `elementSelector` for element-level crop. `outscale` adds padding around the element (user-edited post-generation; omit by default) |
167
167
  | `BEGIN_CLIP` | no | `clipId`, `clipName` | `always` | Start recording |
168
168
  | `END_CLIP` | no | `clipId`, `clipName` | `always` | Stop recording. Same `clipId` as BEGIN_CLIP |
169
169
  | `CLONE_ELEMENT` | yes | `sourceSelector`, `containerSelector`, `count` | `always` | **Non-blocking.** Duplicate a template element N times |
@@ -183,7 +183,7 @@ interface VariantSpec {
183
183
  | `text_contains` | `selector`, `text` | After TYPE, SELECT_OPTION |
184
184
  | `overlay_dismissed` | — | After DISMISS_OVERLAYS |
185
185
  | `screenshot_stable` | `threshold?` (0-1), `waitMs?` | Before CAPTURE_SCREENSHOT when page has animations |
186
- | `any_change` | — | After TYPE, PRESS_KEY, DOUBLE_CLICK (soft check) |
186
+ | `any_change` | — | Verifier-backed state change after TYPE, CLICK, SELECT_OPTION, DOUBLE_CLICK, DRAG, etc.; no-op actions fail |
187
187
  | `always` | — | CAPTURE_SCREENSHOT, SET_LOCALE, SET_THEME, CHECK, BEGIN/END_CLIP |
188
188
 
189
189
  ## What Presets Can and Cannot Do
@@ -13,7 +13,7 @@ export interface ActionVerification {
13
13
  /** Summary for logging */
14
14
  summary: string;
15
15
  }
16
- export type ActionChangeKind = 'url_changed' | 'tree_structure_changed' | 'node_appeared' | 'node_disappeared' | 'overlay_changed' | 'no_change';
16
+ export type ActionChangeKind = 'url_changed' | 'tree_structure_changed' | 'node_appeared' | 'node_disappeared' | 'node_state_changed' | 'scroll_changed' | 'overlay_changed' | 'no_change';
17
17
  export interface ActionChange {
18
18
  kind: ActionChangeKind;
19
19
  detail: string;
@@ -99,6 +99,38 @@ export class ActionVerifier {
99
99
  detail: `title: "${this.beforeTree.page.title}" -> "${afterTree.page.title}"`,
100
100
  });
101
101
  }
102
+ // 6. Check focused/value/selection/text state on stable nodes. This is
103
+ // what makes `any_change` meaningful for TYPE, CHECK, SELECT_OPTION and
104
+ // tab-like controls that update state without changing the DOM shape.
105
+ const beforeState = collectVisibleNodeState(this.beforeTree.root);
106
+ const afterState = collectVisibleNodeState(afterTree.root);
107
+ const changedStates = [];
108
+ for (const [key, before] of beforeState) {
109
+ const after = afterState.get(key);
110
+ if (!after)
111
+ continue;
112
+ if (before !== after) {
113
+ changedStates.push(key);
114
+ if (changedStates.length >= 5)
115
+ break;
116
+ }
117
+ }
118
+ if (changedStates.length > 0) {
119
+ changes.push({
120
+ kind: 'node_state_changed',
121
+ detail: `${changedStates.length} visible node state change(s)`,
122
+ });
123
+ }
124
+ // 7. Check document or nested scroll movement. SCROLL often changes only
125
+ // scroll offsets; without this, a valid scroll can look like a no-op.
126
+ const beforeScroll = collectScrollState(this.beforeTree);
127
+ const afterScroll = collectScrollState(afterTree);
128
+ if (beforeScroll !== afterScroll) {
129
+ changes.push({
130
+ kind: 'scroll_changed',
131
+ detail: `${beforeScroll} -> ${afterScroll}`,
132
+ });
133
+ }
102
134
  // If no changes detected at all
103
135
  if (changes.length === 0) {
104
136
  changes.push({ kind: 'no_change', detail: 'no detectable changes in URL, tree, or overlays' });
@@ -130,4 +162,40 @@ function collectVisibleInteractiveIds(node) {
130
162
  walk(node);
131
163
  return ids;
132
164
  }
165
+ function collectVisibleNodeState(root) {
166
+ const states = new Map();
167
+ function keyFor(node) {
168
+ return node.sourceRef || node.id;
169
+ }
170
+ function walk(node) {
171
+ if (node.visible) {
172
+ states.set(keyFor(node), JSON.stringify({
173
+ label: node.label,
174
+ value: node.value ?? '',
175
+ disabled: node.state.disabled,
176
+ focused: node.state.focused,
177
+ checked: node.state.checked ?? null,
178
+ expanded: node.state.expanded ?? null,
179
+ selected: node.state.selected ?? null,
180
+ ownText: node.attributes.__ownText ?? '',
181
+ }));
182
+ }
183
+ node.children.forEach(walk);
184
+ }
185
+ walk(root);
186
+ return states;
187
+ }
188
+ function collectScrollState(tree) {
189
+ const parts = [
190
+ `page:${tree.page.scroll.scrollTop},${tree.page.scroll.scrollLeft}`,
191
+ ];
192
+ function walk(node) {
193
+ if (node.scroll) {
194
+ parts.push(`${node.sourceRef || node.id}:${node.scroll.scrollTop},${node.scroll.scrollLeft}`);
195
+ }
196
+ node.children.forEach(walk);
197
+ }
198
+ walk(tree.root);
199
+ return parts.join('|');
200
+ }
133
201
  //# sourceMappingURL=action-verifier.js.map
package/dist/browser.d.ts CHANGED
@@ -14,6 +14,8 @@ export interface StableSelectorSnapshot {
14
14
  ariaLabel?: string | null;
15
15
  title?: string | null;
16
16
  placeholder?: string | null;
17
+ dataAk?: string | null;
18
+ dataAkInteract?: string | null;
17
19
  dataTestId?: string | null;
18
20
  dataTest?: string | null;
19
21
  dataQa?: string | null;
package/dist/browser.js CHANGED
@@ -746,6 +746,8 @@ export function buildStableCssSelectorCandidates(snapshot) {
746
746
  push(`#${escapeCssIdentifier(snapshot.id)}`);
747
747
  }
748
748
  for (const [attr, value] of [
749
+ ['data-ak', snapshot.dataAk],
750
+ ['data-ak-interact', snapshot.dataAkInteract],
749
751
  ['data-testid', snapshot.dataTestId],
750
752
  ['data-test', snapshot.dataTest],
751
753
  ['data-qa', snapshot.dataQa],
@@ -1740,7 +1742,7 @@ export class Browser {
1740
1742
  if (id) {
1741
1743
  push(`#${CSS.escape(id)}`);
1742
1744
  }
1743
- for (const attr of ['data-testid', 'data-test', 'data-qa', 'data-cy']) {
1745
+ for (const attr of ['data-ak', 'data-ak-interact', 'data-testid', 'data-test', 'data-qa', 'data-cy']) {
1744
1746
  const value = node.getAttribute(attr);
1745
1747
  if (!value)
1746
1748
  continue;
@@ -1922,7 +1924,7 @@ export class Browser {
1922
1924
  try {
1923
1925
  return await page.evaluate(() => {
1924
1926
  const SKIP_TAGS = new Set(['STYLE', 'SCRIPT', 'SVG', 'NOSCRIPT', 'LINK', 'META', 'BR', 'HR', 'IFRAME', 'CANVAS', 'VIDEO', 'AUDIO', 'SOURCE', 'PICTURE', 'TEMPLATE']);
1925
- const KEEP_ATTRS = new Set(['id', 'role', 'aria-label', 'aria-expanded', 'aria-haspopup', 'aria-controls', 'aria-selected', 'aria-checked', 'aria-disabled', 'href', 'type', 'name', 'placeholder', 'alt', 'title', 'value', 'for', 'action', 'method', 'data-testid']);
1927
+ const KEEP_ATTRS = new Set(['id', 'role', 'aria-label', 'aria-expanded', 'aria-haspopup', 'aria-controls', 'aria-selected', 'aria-checked', 'aria-disabled', 'href', 'type', 'name', 'placeholder', 'alt', 'title', 'value', 'for', 'action', 'method', 'data-ak', 'data-ak-interact', 'data-ak-fill-input', 'data-ak-fill-trigger', 'data-testid']);
1926
1928
  const MAX_CHARS = 4000;
1927
1929
  let output = '';
1928
1930
  let stopped = false;
@@ -2335,7 +2337,7 @@ export class Browser {
2335
2337
  const snapshot = await page.evaluate(() => {
2336
2338
  const SKIP_TAGS = new Set(['STYLE', 'SCRIPT', 'LINK', 'META', 'NOSCRIPT', 'SOURCE', 'PATH', 'G', 'DEFS', 'CLIPPATH']);
2337
2339
  const SEMANTIC_TAGS = new Set(['NAV', 'MAIN', 'HEADER', 'FOOTER', 'SECTION', 'ARTICLE', 'ASIDE', 'FORM']);
2338
- const TEST_ID_ATTRS = ['data-testid', 'data-test', 'data-qa', 'data-cy'];
2340
+ const TEST_ID_ATTRS = ['data-ak', 'data-ak-interact', 'data-testid', 'data-test', 'data-qa', 'data-cy'];
2339
2341
  const quoteForSelector = (value) => value
2340
2342
  .replace(/\\/g, '\\\\')
2341
2343
  .replace(/"/g, '\\"');
@@ -2537,7 +2539,8 @@ export class Browser {
2537
2539
  const attrs = {};
2538
2540
  const copyAttrs = [
2539
2541
  'id', 'name', 'type', 'href', 'placeholder', 'title', 'role', 'aria-label',
2540
- 'aria-controls', 'aria-expanded', 'aria-haspopup', 'aria-modal', 'data-testid',
2542
+ 'aria-controls', 'aria-expanded', 'aria-haspopup', 'aria-modal', 'data-ak',
2543
+ 'data-ak-interact', 'data-ak-fill-input', 'data-ak-fill-trigger', 'data-testid',
2541
2544
  'data-test', 'data-qa', 'data-cy', 'alt',
2542
2545
  ];
2543
2546
  for (const attr of copyAttrs) {
@@ -2983,7 +2986,7 @@ export class Browser {
2983
2986
  const id = node.getAttribute('id');
2984
2987
  if (id)
2985
2988
  return `#${CSS.escape(id)}`;
2986
- for (const attr of ['data-testid', 'data-test', 'data-qa', 'data-cy']) {
2989
+ for (const attr of ['data-ak', 'data-ak-interact', 'data-testid', 'data-test', 'data-qa', 'data-cy']) {
2987
2990
  const value = node.getAttribute(attr);
2988
2991
  if (value)
2989
2992
  return `[${attr}="${quoteForSelector(value)}"]`;
@@ -4588,7 +4591,7 @@ export class Browser {
4588
4591
  if (id) {
4589
4592
  push(`#${CSS.escape(id)}`);
4590
4593
  }
4591
- for (const attr of ['data-testid', 'data-test', 'data-qa', 'data-cy']) {
4594
+ for (const attr of ['data-ak', 'data-ak-interact', 'data-testid', 'data-test', 'data-qa', 'data-cy']) {
4592
4595
  const value = node.getAttribute(attr);
4593
4596
  if (!value)
4594
4597
  continue;
@@ -17,6 +17,7 @@ export declare function buildCliRunCommand(presetId: string, options?: {
17
17
  local?: boolean;
18
18
  env?: string;
19
19
  dry?: boolean;
20
+ regenerateTts?: boolean;
20
21
  }): string;
21
22
  export declare function buildCliInstalledSetupCommand(cliKey: string): string;
22
23
  export declare const CLI_PUBLIC_COMMANDS: CliPublicCommandDescriptor[];
@@ -12,6 +12,7 @@ export function buildCliRunCommand(presetId, options = {}) {
12
12
  options.local ? " --local" : "",
13
13
  options.env ? ` --env ${options.env}` : "",
14
14
  options.dry ? " --dry" : "",
15
+ options.regenerateTts ? " --regenerate-tts" : "",
15
16
  ].join("");
16
17
  return `autokap run${flags} ${presetId}`;
17
18
  }
@@ -12,4 +12,5 @@ export declare function runLocal(presetId: string, opts: {
12
12
  env?: string;
13
13
  allowUploadFailure?: boolean;
14
14
  dry?: boolean;
15
+ regenerateTts?: boolean;
15
16
  }): Promise<void>;
@@ -31,6 +31,7 @@ export async function runLocal(presetId, opts) {
31
31
  program,
32
32
  allowUploadFailure: opts.allowUploadFailure,
33
33
  dryRun: opts.dry,
34
+ regenerateTts: opts.regenerateTts,
34
35
  headed: opts.headed,
35
36
  onProgress: (event) => {
36
37
  const prefix = `[capture][${event.variantId}]`;
@@ -31,6 +31,12 @@ export interface CLIRunnerOptions {
31
31
  allowUploadFailure?: boolean;
32
32
  /** Dry run: skip capture opcodes (CAPTURE_SCREENSHOT/BEGIN_CLIP/END_CLIP) and upload. 0 credits charged. */
33
33
  dryRun?: boolean;
34
+ /**
35
+ * Force fresh TTS synthesis for every narration segment instead of reusing
36
+ * cached audio. Only meaningful when `mediaMode === 'video'`. Every reused
37
+ * segment becomes billable at full rate.
38
+ */
39
+ regenerateTts?: boolean;
34
40
  /** Selector memory map (fetched from server or cached locally) */
35
41
  selectorMemory?: Record<string, string[]>;
36
42
  /** Show browser window. Default: false (headless) */
@@ -176,7 +176,7 @@ export async function runCapture(options) {
176
176
  return { success: false, runId, error: error instanceof Error ? error.message : String(error) };
177
177
  }
178
178
  if (!options.program && program.mediaMode === 'video') {
179
- const prepareResult = await prepareVideoSpeechForRun(config, options.presetId, runId);
179
+ const prepareResult = await prepareVideoSpeechForRun(config, options.presetId, runId, options.regenerateTts ?? false);
180
180
  if (!prepareResult.success) {
181
181
  return { success: false, runId, error: prepareResult.error };
182
182
  }
@@ -396,7 +396,10 @@ async function fetchProgram(config, presetId, environmentName) {
396
396
  }
397
397
  return { success: false, error: 'failed to fetch program: retry attempts exhausted' };
398
398
  }
399
- async function prepareVideoSpeechForRun(config, videoId, runId) {
399
+ async function prepareVideoSpeechForRun(config, videoId, runId, regenerateTts) {
400
+ if (regenerateTts) {
401
+ logger.info('[capture] Forcing TTS regeneration — all cached segments will be re-synthesized and billed.');
402
+ }
400
403
  logger.info('[capture] Generating speech, may take a few seconds...');
401
404
  const url = `${config.apiBaseUrl}/api/cli/video-prepare`;
402
405
  let response;
@@ -408,7 +411,7 @@ async function prepareVideoSpeechForRun(config, videoId, runId) {
408
411
  'Content-Type': 'application/json',
409
412
  [CLI_VERSION_HEADER]: APP_VERSION,
410
413
  },
411
- body: JSON.stringify({ videoId, runId }),
414
+ body: JSON.stringify(regenerateTts ? { videoId, runId, regenerateTts: true } : { videoId, runId }),
412
415
  });
413
416
  }
414
417
  catch (err) {
package/dist/cli.js CHANGED
@@ -251,6 +251,7 @@ program
251
251
  .option('--output <dir>', 'Optional output directory for local artifact copies')
252
252
  .option('--program <file>', 'Path to a program JSON file')
253
253
  .option('--dry', 'Dry run: execute all opcodes without capturing or uploading artifacts (0 credits charged)', false)
254
+ .option('--regenerate-tts', 'Force fresh TTS synthesis for video presets — ignore cached audio segments. Reused segments become billable at full rate.', false)
254
255
  .option('--debug', 'Verbose logging: per-substep timing, opcode dumps, recovery strategy traces', false)
255
256
  .action(async (presetId, opts) => {
256
257
  if (opts.debug) {
@@ -29,6 +29,16 @@ export declare const RecoveryPolicySchema: z.ZodObject<{
29
29
  allowReload: z.ZodBoolean;
30
30
  allowHealer: z.ZodBoolean;
31
31
  }, z.core.$strict>;
32
+ export declare const OutscaleConfigSchema: z.ZodObject<{
33
+ padding: z.ZodOptional<z.ZodNumber>;
34
+ paddingTop: z.ZodOptional<z.ZodNumber>;
35
+ paddingRight: z.ZodOptional<z.ZodNumber>;
36
+ paddingBottom: z.ZodOptional<z.ZodNumber>;
37
+ paddingLeft: z.ZodOptional<z.ZodNumber>;
38
+ paddingPercent: z.ZodOptional<z.ZodNumber>;
39
+ clampToViewport: z.ZodOptional<z.ZodBoolean>;
40
+ backgroundColor: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strict>;
32
42
  export declare const ExecutionOpcodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
33
43
  url: z.ZodString;
34
44
  description: z.ZodString;
@@ -495,6 +505,16 @@ export declare const ExecutionOpcodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject
495
505
  captureId: z.ZodOptional<z.ZodString>;
496
506
  captureName: z.ZodOptional<z.ZodString>;
497
507
  elementSelector: z.ZodOptional<z.ZodString>;
508
+ outscale: z.ZodOptional<z.ZodObject<{
509
+ padding: z.ZodOptional<z.ZodNumber>;
510
+ paddingTop: z.ZodOptional<z.ZodNumber>;
511
+ paddingRight: z.ZodOptional<z.ZodNumber>;
512
+ paddingBottom: z.ZodOptional<z.ZodNumber>;
513
+ paddingLeft: z.ZodOptional<z.ZodNumber>;
514
+ paddingPercent: z.ZodOptional<z.ZodNumber>;
515
+ clampToViewport: z.ZodOptional<z.ZodBoolean>;
516
+ backgroundColor: z.ZodOptional<z.ZodString>;
517
+ }, z.core.$strict>>;
498
518
  description: z.ZodString;
499
519
  postcondition: z.ZodObject<{
500
520
  type: z.ZodEnum<{
@@ -1688,6 +1708,16 @@ export declare const ExecutionProgramSchema: z.ZodObject<{
1688
1708
  captureId: z.ZodOptional<z.ZodString>;
1689
1709
  captureName: z.ZodOptional<z.ZodString>;
1690
1710
  elementSelector: z.ZodOptional<z.ZodString>;
1711
+ outscale: z.ZodOptional<z.ZodObject<{
1712
+ padding: z.ZodOptional<z.ZodNumber>;
1713
+ paddingTop: z.ZodOptional<z.ZodNumber>;
1714
+ paddingRight: z.ZodOptional<z.ZodNumber>;
1715
+ paddingBottom: z.ZodOptional<z.ZodNumber>;
1716
+ paddingLeft: z.ZodOptional<z.ZodNumber>;
1717
+ paddingPercent: z.ZodOptional<z.ZodNumber>;
1718
+ clampToViewport: z.ZodOptional<z.ZodBoolean>;
1719
+ backgroundColor: z.ZodOptional<z.ZodString>;
1720
+ }, z.core.$strict>>;
1691
1721
  description: z.ZodString;
1692
1722
  postcondition: z.ZodObject<{
1693
1723
  type: z.ZodEnum<{
@@ -2665,6 +2695,16 @@ export declare const HealerPatchSchema: z.ZodObject<{
2665
2695
  captureId: z.ZodOptional<z.ZodString>;
2666
2696
  captureName: z.ZodOptional<z.ZodString>;
2667
2697
  elementSelector: z.ZodOptional<z.ZodString>;
2698
+ outscale: z.ZodOptional<z.ZodObject<{
2699
+ padding: z.ZodOptional<z.ZodNumber>;
2700
+ paddingTop: z.ZodOptional<z.ZodNumber>;
2701
+ paddingRight: z.ZodOptional<z.ZodNumber>;
2702
+ paddingBottom: z.ZodOptional<z.ZodNumber>;
2703
+ paddingLeft: z.ZodOptional<z.ZodNumber>;
2704
+ paddingPercent: z.ZodOptional<z.ZodNumber>;
2705
+ clampToViewport: z.ZodOptional<z.ZodBoolean>;
2706
+ backgroundColor: z.ZodOptional<z.ZodString>;
2707
+ }, z.core.$strict>>;
2668
2708
  description: z.ZodString;
2669
2709
  postcondition: z.ZodObject<{
2670
2710
  type: z.ZodEnum<{
@@ -3588,6 +3628,16 @@ export declare const HealerPatchSchema: z.ZodObject<{
3588
3628
  captureId: z.ZodOptional<z.ZodString>;
3589
3629
  captureName: z.ZodOptional<z.ZodString>;
3590
3630
  elementSelector: z.ZodOptional<z.ZodString>;
3631
+ outscale: z.ZodOptional<z.ZodObject<{
3632
+ padding: z.ZodOptional<z.ZodNumber>;
3633
+ paddingTop: z.ZodOptional<z.ZodNumber>;
3634
+ paddingRight: z.ZodOptional<z.ZodNumber>;
3635
+ paddingBottom: z.ZodOptional<z.ZodNumber>;
3636
+ paddingLeft: z.ZodOptional<z.ZodNumber>;
3637
+ paddingPercent: z.ZodOptional<z.ZodNumber>;
3638
+ clampToViewport: z.ZodOptional<z.ZodBoolean>;
3639
+ backgroundColor: z.ZodOptional<z.ZodString>;
3640
+ }, z.core.$strict>>;
3591
3641
  description: z.ZodString;
3592
3642
  postcondition: z.ZodObject<{
3593
3643
  type: z.ZodEnum<{
@@ -4151,6 +4201,51 @@ export declare function safeParseProgramResult(data: unknown): z.ZodSafeParseRes
4151
4201
  }[] | undefined;
4152
4202
  };
4153
4203
  steps: ({
4204
+ urlPattern: string;
4205
+ description: string;
4206
+ postcondition: {
4207
+ type: "route_matches" | "element_visible" | "element_absent" | "text_contains" | "overlay_dismissed" | "screenshot_stable" | "any_change" | "always";
4208
+ pattern?: string | undefined;
4209
+ selector?: string | undefined;
4210
+ text?: string | undefined;
4211
+ threshold?: number | undefined;
4212
+ waitMs?: number | undefined;
4213
+ };
4214
+ recovery: {
4215
+ retries: number;
4216
+ useSelectorMemory: boolean;
4217
+ useAltInteraction: boolean;
4218
+ allowReload: boolean;
4219
+ allowHealer: boolean;
4220
+ };
4221
+ timeoutMs: number;
4222
+ maxFailures: number;
4223
+ kind: "ASSERT_ROUTE";
4224
+ stepId?: string | undefined;
4225
+ } | {
4226
+ selectors: string[];
4227
+ matchAll: boolean;
4228
+ description: string;
4229
+ postcondition: {
4230
+ type: "route_matches" | "element_visible" | "element_absent" | "text_contains" | "overlay_dismissed" | "screenshot_stable" | "any_change" | "always";
4231
+ pattern?: string | undefined;
4232
+ selector?: string | undefined;
4233
+ text?: string | undefined;
4234
+ threshold?: number | undefined;
4235
+ waitMs?: number | undefined;
4236
+ };
4237
+ recovery: {
4238
+ retries: number;
4239
+ useSelectorMemory: boolean;
4240
+ useAltInteraction: boolean;
4241
+ allowReload: boolean;
4242
+ allowHealer: boolean;
4243
+ };
4244
+ timeoutMs: number;
4245
+ maxFailures: number;
4246
+ kind: "ASSERT_SURFACE";
4247
+ stepId?: string | undefined;
4248
+ } | {
4154
4249
  state: "visible" | "attached";
4155
4250
  description: string;
4156
4251
  postcondition: {
@@ -4420,51 +4515,6 @@ export declare function safeParseProgramResult(data: unknown): z.ZodSafeParseRes
4420
4515
  maxFailures: number;
4421
4516
  kind: "DISMISS_OVERLAYS";
4422
4517
  stepId?: string | undefined;
4423
- } | {
4424
- urlPattern: string;
4425
- description: string;
4426
- postcondition: {
4427
- type: "route_matches" | "element_visible" | "element_absent" | "text_contains" | "overlay_dismissed" | "screenshot_stable" | "any_change" | "always";
4428
- pattern?: string | undefined;
4429
- selector?: string | undefined;
4430
- text?: string | undefined;
4431
- threshold?: number | undefined;
4432
- waitMs?: number | undefined;
4433
- };
4434
- recovery: {
4435
- retries: number;
4436
- useSelectorMemory: boolean;
4437
- useAltInteraction: boolean;
4438
- allowReload: boolean;
4439
- allowHealer: boolean;
4440
- };
4441
- timeoutMs: number;
4442
- maxFailures: number;
4443
- kind: "ASSERT_ROUTE";
4444
- stepId?: string | undefined;
4445
- } | {
4446
- selectors: string[];
4447
- matchAll: boolean;
4448
- description: string;
4449
- postcondition: {
4450
- type: "route_matches" | "element_visible" | "element_absent" | "text_contains" | "overlay_dismissed" | "screenshot_stable" | "any_change" | "always";
4451
- pattern?: string | undefined;
4452
- selector?: string | undefined;
4453
- text?: string | undefined;
4454
- threshold?: number | undefined;
4455
- waitMs?: number | undefined;
4456
- };
4457
- recovery: {
4458
- retries: number;
4459
- useSelectorMemory: boolean;
4460
- useAltInteraction: boolean;
4461
- allowReload: boolean;
4462
- allowHealer: boolean;
4463
- };
4464
- timeoutMs: number;
4465
- maxFailures: number;
4466
- kind: "ASSERT_SURFACE";
4467
- stepId?: string | undefined;
4468
4518
  } | {
4469
4519
  selector: string;
4470
4520
  description: string;
@@ -4610,6 +4660,16 @@ export declare function safeParseProgramResult(data: unknown): z.ZodSafeParseRes
4610
4660
  captureId?: string | undefined;
4611
4661
  captureName?: string | undefined;
4612
4662
  elementSelector?: string | undefined;
4663
+ outscale?: {
4664
+ padding?: number | undefined;
4665
+ paddingTop?: number | undefined;
4666
+ paddingRight?: number | undefined;
4667
+ paddingBottom?: number | undefined;
4668
+ paddingLeft?: number | undefined;
4669
+ paddingPercent?: number | undefined;
4670
+ clampToViewport?: boolean | undefined;
4671
+ backgroundColor?: string | undefined;
4672
+ } | undefined;
4613
4673
  stepId?: string | undefined;
4614
4674
  } | {
4615
4675
  description: string;
@@ -102,13 +102,35 @@ const AssertRouteOpcodeSchema = z.object({
102
102
  kind: z.literal('ASSERT_ROUTE'),
103
103
  ...opcodeBase,
104
104
  urlPattern: z.string().min(1),
105
- }).strict();
105
+ }).strict().superRefine((value, ctx) => {
106
+ const post = value.postcondition;
107
+ if (post.type === 'always')
108
+ return;
109
+ if (post.type === 'route_matches' && post.pattern === value.urlPattern)
110
+ return;
111
+ ctx.addIssue({
112
+ code: z.ZodIssueCode.custom,
113
+ message: 'ASSERT_ROUTE uses `urlPattern` as its source of truth; postcondition must be `always` or `route_matches` with the same pattern',
114
+ path: ['postcondition'],
115
+ });
116
+ });
106
117
  const AssertSurfaceOpcodeSchema = z.object({
107
118
  kind: z.literal('ASSERT_SURFACE'),
108
119
  ...opcodeBase,
109
120
  selectors: z.array(z.string().min(1)).min(1),
110
121
  matchAll: z.boolean(),
111
- }).strict();
122
+ }).strict().superRefine((value, ctx) => {
123
+ const post = value.postcondition;
124
+ if (post.type === 'always')
125
+ return;
126
+ if (post.type === 'element_visible' && post.selector && value.selectors.includes(post.selector))
127
+ return;
128
+ ctx.addIssue({
129
+ code: z.ZodIssueCode.custom,
130
+ message: 'ASSERT_SURFACE uses `selectors`/`matchAll` as its source of truth; postcondition must be `always` or `element_visible` for one of the asserted selectors',
131
+ path: ['postcondition'],
132
+ });
133
+ });
112
134
  const SemanticTargetSchema = z.object({
113
135
  text: z.string().optional(),
114
136
  role: z.string().optional(),
@@ -242,12 +264,23 @@ const ScrollOpcodeSchema = z.object({
242
264
  targetSelector: z.string().optional(),
243
265
  target: SemanticTargetSchema.optional(),
244
266
  }).strict();
267
+ export const OutscaleConfigSchema = z.object({
268
+ padding: z.number().min(0).optional(),
269
+ paddingTop: z.number().min(0).optional(),
270
+ paddingRight: z.number().min(0).optional(),
271
+ paddingBottom: z.number().min(0).optional(),
272
+ paddingLeft: z.number().min(0).optional(),
273
+ paddingPercent: z.number().min(0).max(100).optional(),
274
+ clampToViewport: z.boolean().optional(),
275
+ backgroundColor: z.string().optional(),
276
+ }).strict();
245
277
  const CaptureScreenshotOpcodeSchema = z.object({
246
278
  kind: z.literal('CAPTURE_SCREENSHOT'),
247
279
  ...opcodeBase,
248
280
  captureId: z.string().optional(),
249
281
  captureName: z.string().optional(),
250
282
  elementSelector: z.string().optional(),
283
+ outscale: OutscaleConfigSchema.optional(),
251
284
  }).strict();
252
285
  const BeginClipOpcodeSchema = z.object({
253
286
  kind: z.literal('BEGIN_CLIP'),
@@ -4,7 +4,7 @@
4
4
  * All types for the compiled execution model:
5
5
  * preset (natural language) -> ExecutionProgram (typed IR) -> deterministic runtime
6
6
  */
7
- import type { AKTree, BrowserStorageState, BrowserSessionStorageState, VideoCursorTheme, VideoPageSignals } from './types.js';
7
+ import type { AKTree, BrowserStorageState, BrowserSessionStorageState, OutscaleConfig, VideoCursorTheme, VideoPageSignals } from './types.js';
8
8
  import type { MockupOptions } from './mockup.js';
9
9
  /** Sentinel value that resolves to the current variant's locale or theme at runtime */
10
10
  export declare const VARIANT_PLACEHOLDER: "$variant";
@@ -247,6 +247,8 @@ export interface CaptureScreenshotOpcode extends OpcodeBase {
247
247
  captureName?: string;
248
248
  /** Optional element selector for element-level capture */
249
249
  elementSelector?: string;
250
+ /** Optional padding around the captured element. Only applied when `elementSelector` is set. */
251
+ outscale?: OutscaleConfig;
250
252
  }
251
253
  export interface BeginClipOpcode extends OpcodeBase {
252
254
  kind: 'BEGIN_CLIP';
@@ -809,7 +811,7 @@ export interface RuntimeAdapter {
809
811
  method: string | null;
810
812
  }>;
811
813
  takeScreenshot(): Promise<Buffer>;
812
- takeElementScreenshot?(selector: string): Promise<Buffer>;
814
+ takeElementScreenshot?(selector: string, outscale?: OutscaleConfig): Promise<Buffer>;
813
815
  takeCleanScreenshot(): Promise<Buffer>;
814
816
  beginRecording(options: RecordingOptions): Promise<void>;
815
817
  endRecording(): Promise<RecordingResult>;