autokap 1.0.7 → 1.0.10

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 (301) hide show
  1. package/assets/cursors/macos.svg +4 -0
  2. package/assets/cursors/windows.svg +15 -0
  3. package/assets/skill/OPCODE-REFERENCE.md +635 -0
  4. package/assets/skill/README.md +39 -0
  5. package/assets/skill/SKILL.md +454 -468
  6. package/assets/skill/STUDIO-SKILL.md +476 -0
  7. package/assets/skill/references/examples.md +104 -0
  8. package/assets/skill/references/interactive-demo.md +225 -0
  9. package/assets/skill/references/mock-data.md +178 -0
  10. package/dist/action-verifier.d.ts +29 -0
  11. package/dist/action-verifier.js +133 -0
  12. package/dist/agent-action-recovery.d.ts +45 -0
  13. package/dist/agent-action-recovery.js +370 -0
  14. package/dist/agent-message-utils.d.ts +21 -0
  15. package/dist/agent-message-utils.js +77 -0
  16. package/dist/agent-url-utils.d.ts +30 -0
  17. package/dist/agent-url-utils.js +138 -0
  18. package/dist/agent.d.ts +92 -8
  19. package/dist/agent.js +2936 -781
  20. package/dist/ak-tree.d.ts +39 -0
  21. package/dist/ak-tree.js +368 -0
  22. package/dist/alt-text.d.ts +26 -0
  23. package/dist/alt-text.js +55 -0
  24. package/dist/auth-capture.d.ts +17 -0
  25. package/dist/auth-capture.js +197 -0
  26. package/dist/benchmark.d.ts +59 -0
  27. package/dist/benchmark.js +135 -0
  28. package/dist/billing-operation-logging.d.ts +3 -1
  29. package/dist/billing-operation-logging.js +4 -0
  30. package/dist/browser-bar.d.ts +14 -6
  31. package/dist/browser-bar.js +145 -8
  32. package/dist/browser-pool.d.ts +7 -0
  33. package/dist/browser-pool.js +15 -5
  34. package/dist/browser-utils.d.ts +31 -0
  35. package/dist/browser-utils.js +97 -0
  36. package/dist/browser.d.ts +61 -11
  37. package/dist/browser.js +1513 -59
  38. package/dist/capture-alt-text.js +2 -1
  39. package/dist/capture-encryption.d.ts +3 -1
  40. package/dist/capture-encryption.js +21 -6
  41. package/dist/capture-language-preflight.js +14 -0
  42. package/dist/capture-llm-page-identity.js +22 -10
  43. package/dist/capture-page-identity.d.ts +5 -7
  44. package/dist/capture-page-identity.js +211 -78
  45. package/dist/capture-preset-credentials.d.ts +50 -0
  46. package/dist/capture-preset-credentials.js +127 -0
  47. package/dist/capture-request-plan.d.ts +2 -2
  48. package/dist/capture-request-plan.js +64 -16
  49. package/dist/capture-run-optimizer.js +48 -33
  50. package/dist/capture-selector-memory.d.ts +5 -0
  51. package/dist/capture-selector-memory.js +18 -0
  52. package/dist/capture-strategy.d.ts +36 -0
  53. package/dist/capture-strategy.js +96 -0
  54. package/dist/capture-studio-sync.d.ts +1 -0
  55. package/dist/capture-studio-sync.js +9 -3
  56. package/dist/capture-surface-contract.d.ts +36 -0
  57. package/dist/capture-surface-contract.js +299 -0
  58. package/dist/capture-transition-engine.d.ts +28 -0
  59. package/dist/capture-transition-engine.js +292 -0
  60. package/dist/capture-variant-state.d.ts +2 -0
  61. package/dist/capture-variant-state.js +26 -0
  62. package/dist/capture-verification.d.ts +35 -0
  63. package/dist/capture-verification.js +95 -0
  64. package/dist/capture-viewport-lock.d.ts +48 -0
  65. package/dist/capture-viewport-lock.js +74 -0
  66. package/dist/circuit-breaker.d.ts +42 -0
  67. package/dist/circuit-breaker.js +119 -0
  68. package/dist/cli-config.d.ts +9 -1
  69. package/dist/cli-config.js +112 -7
  70. package/dist/cli-contract.d.ts +19 -0
  71. package/dist/cli-contract.js +173 -0
  72. package/dist/cli-runner-local.d.ts +12 -0
  73. package/dist/cli-runner-local.js +115 -0
  74. package/dist/cli-runner.d.ts +34 -0
  75. package/dist/cli-runner.js +580 -0
  76. package/dist/cli-utils.d.ts +0 -1
  77. package/dist/cli-utils.js +2 -5
  78. package/dist/cli.js +1011 -267
  79. package/dist/clip-begin-frame-recorder.d.ts +44 -0
  80. package/dist/clip-begin-frame-recorder.js +250 -0
  81. package/dist/clip-capture-backend.d.ts +25 -0
  82. package/dist/clip-capture-backend.js +189 -0
  83. package/dist/clip-capture-loop.d.ts +61 -0
  84. package/dist/clip-capture-loop.js +111 -0
  85. package/dist/clip-frame-recorder.d.ts +63 -0
  86. package/dist/clip-frame-recorder.js +305 -0
  87. package/dist/clip-orchestrator.js +9 -2
  88. package/dist/clip-postprocess.d.ts +31 -2
  89. package/dist/clip-postprocess.js +194 -68
  90. package/dist/clip-runtime.d.ts +18 -0
  91. package/dist/clip-runtime.js +67 -0
  92. package/dist/clip-scale.d.ts +10 -0
  93. package/dist/clip-scale.js +21 -0
  94. package/dist/clip-screencast-recorder.d.ts +42 -0
  95. package/dist/clip-screencast-recorder.js +242 -0
  96. package/dist/clip-sidecar.d.ts +54 -0
  97. package/dist/clip-sidecar.js +208 -0
  98. package/dist/cookie-dismiss.d.ts +2 -0
  99. package/dist/cookie-dismiss.js +48 -13
  100. package/dist/cost-logging.d.ts +8 -0
  101. package/dist/cost-logging.js +160 -46
  102. package/dist/cost-resolution-monitor.d.ts +16 -0
  103. package/dist/cost-resolution-monitor.js +34 -0
  104. package/dist/credential-templates.js +2 -2
  105. package/dist/cursor-overlay-script.d.ts +6 -0
  106. package/dist/cursor-overlay-script.js +169 -0
  107. package/dist/dom-css-purger.d.ts +65 -0
  108. package/dist/dom-css-purger.js +333 -0
  109. package/dist/dom-font-inliner.d.ts +45 -0
  110. package/dist/dom-font-inliner.js +148 -0
  111. package/dist/dom-patch-resolver.d.ts +52 -0
  112. package/dist/dom-patch-resolver.js +242 -0
  113. package/dist/dom-serializer.d.ts +82 -0
  114. package/dist/dom-serializer.js +378 -0
  115. package/dist/element-capture.d.ts +1 -41
  116. package/dist/element-capture.js +202 -446
  117. package/dist/env-validation.d.ts +5 -0
  118. package/dist/env-validation.js +63 -0
  119. package/dist/execution-schema.d.ts +4753 -0
  120. package/dist/execution-schema.js +563 -0
  121. package/dist/execution-types.d.ts +936 -0
  122. package/dist/execution-types.js +66 -0
  123. package/dist/fonts-loader.d.ts +14 -0
  124. package/dist/fonts-loader.js +55 -0
  125. package/dist/hybrid-navigator.js +12 -12
  126. package/dist/index.d.ts +11 -6
  127. package/dist/index.js +11 -4
  128. package/dist/legacy/agent-action-recovery.d.ts +45 -0
  129. package/dist/legacy/agent-action-recovery.js +370 -0
  130. package/dist/legacy/agent-message-utils.d.ts +21 -0
  131. package/dist/legacy/agent-message-utils.js +77 -0
  132. package/dist/legacy/agent-url-utils.d.ts +30 -0
  133. package/dist/legacy/agent-url-utils.js +138 -0
  134. package/dist/legacy/agent.d.ts +226 -0
  135. package/dist/legacy/agent.js +6666 -0
  136. package/dist/legacy/clip-orchestrator.d.ts +148 -0
  137. package/dist/legacy/clip-orchestrator.js +957 -0
  138. package/dist/legacy/credential-templates.d.ts +5 -0
  139. package/dist/legacy/credential-templates.js +60 -0
  140. package/dist/legacy/hybrid-navigator.d.ts +138 -0
  141. package/dist/legacy/hybrid-navigator.js +468 -0
  142. package/dist/legacy/llm-usage.d.ts +17 -0
  143. package/dist/legacy/llm-usage.js +45 -0
  144. package/dist/legacy/prompt-cache.d.ts +10 -0
  145. package/dist/legacy/prompt-cache.js +24 -0
  146. package/dist/legacy/prompts.d.ts +175 -0
  147. package/dist/legacy/prompts.js +1038 -0
  148. package/dist/legacy/tools.d.ts +4 -0
  149. package/dist/legacy/tools.js +216 -0
  150. package/dist/legacy/video-agent.d.ts +143 -0
  151. package/dist/legacy/video-agent.js +4788 -0
  152. package/dist/legacy/video-observation.d.ts +36 -0
  153. package/dist/legacy/video-observation.js +192 -0
  154. package/dist/legacy/video-planner.d.ts +12 -0
  155. package/dist/legacy/video-planner.js +501 -0
  156. package/dist/legacy/video-prompts.d.ts +37 -0
  157. package/dist/legacy/video-prompts.js +569 -0
  158. package/dist/legacy/video-tools.d.ts +3 -0
  159. package/dist/legacy/video-tools.js +59 -0
  160. package/dist/legacy/video-variant-state.d.ts +29 -0
  161. package/dist/legacy/video-variant-state.js +80 -0
  162. package/dist/legacy/vision-model.d.ts +17 -0
  163. package/dist/legacy/vision-model.js +74 -0
  164. package/dist/llm-healer.d.ts +55 -0
  165. package/dist/llm-healer.js +213 -0
  166. package/dist/llm-provider.d.ts +29 -0
  167. package/dist/llm-provider.js +83 -0
  168. package/dist/logger.d.ts +6 -2
  169. package/dist/logger.js +15 -1
  170. package/dist/mockup-html.js +35 -25
  171. package/dist/mockup.d.ts +95 -2
  172. package/dist/mockup.js +427 -166
  173. package/dist/mouse-animation.d.ts +2 -2
  174. package/dist/mouse-animation.js +34 -20
  175. package/dist/opcode-actions.d.ts +42 -0
  176. package/dist/opcode-actions.js +524 -0
  177. package/dist/opcode-runner.d.ts +51 -0
  178. package/dist/opcode-runner.js +779 -0
  179. package/dist/openrouter-client.d.ts +40 -0
  180. package/dist/openrouter-client.js +16 -0
  181. package/dist/overlay-engine.d.ts +24 -0
  182. package/dist/overlay-engine.js +176 -0
  183. package/dist/postcondition.d.ts +16 -0
  184. package/dist/postcondition.js +269 -0
  185. package/dist/program-patcher.d.ts +25 -0
  186. package/dist/program-patcher.js +44 -0
  187. package/dist/program-signing.d.ts +1094 -0
  188. package/dist/program-signing.js +140 -0
  189. package/dist/prompts.d.ts +13 -5
  190. package/dist/prompts.js +224 -351
  191. package/dist/provider-config.d.ts +17 -0
  192. package/dist/provider-config.js +42 -0
  193. package/dist/recovery-chain.d.ts +37 -0
  194. package/dist/recovery-chain.js +374 -0
  195. package/dist/remote-browser.d.ts +28 -4
  196. package/dist/remote-browser.js +60 -5
  197. package/dist/safari-browser-bar.d.ts +15 -0
  198. package/dist/safari-browser-bar.js +95 -0
  199. package/dist/safari-toolbar-asset.d.ts +15 -0
  200. package/dist/safari-toolbar-asset.js +12 -0
  201. package/dist/security.d.ts +2 -1
  202. package/dist/security.js +49 -10
  203. package/dist/selector-resolver.d.ts +34 -0
  204. package/dist/selector-resolver.js +181 -0
  205. package/dist/semantic-resolver.d.ts +35 -0
  206. package/dist/semantic-resolver.js +161 -0
  207. package/dist/server-capture-runtime.d.ts +5 -3
  208. package/dist/server-capture-runtime.js +42 -95
  209. package/dist/server-credit-usage.d.ts +2 -2
  210. package/dist/server-project-webhooks.d.ts +15 -1
  211. package/dist/server-project-webhooks.js +34 -8
  212. package/dist/server-screenshot-watermark.js +27 -5
  213. package/dist/session-profile.js +164 -1
  214. package/dist/sf-pro-symbols.d.ts +1 -0
  215. package/dist/sf-pro-symbols.js +55 -0
  216. package/dist/skill-packaging.d.ts +28 -0
  217. package/dist/skill-packaging.js +169 -0
  218. package/dist/smart-wait.d.ts +27 -0
  219. package/dist/smart-wait.js +81 -0
  220. package/dist/status-bar-render.d.ts +20 -0
  221. package/dist/status-bar-render.js +410 -0
  222. package/dist/status-bar.d.ts +9 -0
  223. package/dist/status-bar.js +298 -14
  224. package/dist/svg-browser-bar.d.ts +33 -0
  225. package/dist/svg-browser-bar.js +206 -0
  226. package/dist/svg-status-bar.d.ts +36 -0
  227. package/dist/svg-status-bar.js +597 -0
  228. package/dist/svg-text.d.ts +61 -0
  229. package/dist/svg-text.js +118 -0
  230. package/dist/tools.js +89 -451
  231. package/dist/types.d.ts +248 -7
  232. package/dist/types.js +23 -1
  233. package/dist/v2/action-verifier.d.ts +29 -0
  234. package/dist/v2/action-verifier.js +133 -0
  235. package/dist/v2/alt-text.d.ts +26 -0
  236. package/dist/v2/alt-text.js +55 -0
  237. package/dist/v2/benchmark.d.ts +59 -0
  238. package/dist/v2/benchmark.js +135 -0
  239. package/dist/v2/capture-strategy.d.ts +30 -0
  240. package/dist/v2/capture-strategy.js +67 -0
  241. package/dist/v2/capture-verification.d.ts +35 -0
  242. package/dist/v2/capture-verification.js +95 -0
  243. package/dist/v2/circuit-breaker.d.ts +42 -0
  244. package/dist/v2/circuit-breaker.js +119 -0
  245. package/dist/v2/cli-runner-local.d.ts +11 -0
  246. package/dist/v2/cli-runner-local.js +91 -0
  247. package/dist/v2/cli-runner.d.ts +34 -0
  248. package/dist/v2/cli-runner.js +300 -0
  249. package/dist/v2/compiler-prompts.d.ts +27 -0
  250. package/dist/v2/compiler-prompts.js +123 -0
  251. package/dist/v2/compiler.d.ts +37 -0
  252. package/dist/v2/compiler.js +147 -0
  253. package/dist/v2/explorer.d.ts +41 -0
  254. package/dist/v2/explorer.js +56 -0
  255. package/dist/v2/index.d.ts +37 -0
  256. package/dist/v2/index.js +31 -0
  257. package/dist/v2/llm-healer.d.ts +62 -0
  258. package/dist/v2/llm-healer.js +166 -0
  259. package/dist/v2/llm-provider.d.ts +29 -0
  260. package/dist/v2/llm-provider.js +80 -0
  261. package/dist/v2/opcode-runner.d.ts +47 -0
  262. package/dist/v2/opcode-runner.js +634 -0
  263. package/dist/v2/overlay-engine.d.ts +24 -0
  264. package/dist/v2/overlay-engine.js +150 -0
  265. package/dist/v2/postcondition.d.ts +16 -0
  266. package/dist/v2/postcondition.js +249 -0
  267. package/dist/v2/program-patcher.d.ts +25 -0
  268. package/dist/v2/program-patcher.js +44 -0
  269. package/dist/v2/recovery-chain.d.ts +30 -0
  270. package/dist/v2/recovery-chain.js +368 -0
  271. package/dist/v2/schema.d.ts +2580 -0
  272. package/dist/v2/schema.js +295 -0
  273. package/dist/v2/selector-resolver.d.ts +34 -0
  274. package/dist/v2/selector-resolver.js +181 -0
  275. package/dist/v2/semantic-resolver.d.ts +35 -0
  276. package/dist/v2/semantic-resolver.js +161 -0
  277. package/dist/v2/smart-wait.d.ts +27 -0
  278. package/dist/v2/smart-wait.js +81 -0
  279. package/dist/v2/types.d.ts +444 -0
  280. package/dist/v2/types.js +19 -0
  281. package/dist/v2/web-playwright-local.d.ts +69 -0
  282. package/dist/v2/web-playwright-local.js +392 -0
  283. package/dist/version.d.ts +1 -0
  284. package/dist/version.js +5 -0
  285. package/dist/video-agent.js +18 -13
  286. package/dist/video-planner.js +2 -1
  287. package/dist/video-prompts.js +3 -3
  288. package/dist/web-playwright-local.d.ts +156 -0
  289. package/dist/web-playwright-local.js +989 -0
  290. package/dist/ws-auth.js +4 -1
  291. package/dist/ws-broadcast.d.ts +34 -0
  292. package/dist/ws-broadcast.js +85 -0
  293. package/dist/ws-connection-limits.d.ts +12 -0
  294. package/dist/ws-connection-limits.js +44 -0
  295. package/dist/ws-handler-utils.d.ts +32 -0
  296. package/dist/ws-handler-utils.js +139 -0
  297. package/dist/ws-handler.js +294 -164
  298. package/dist/ws-metrics-server.d.ts +9 -0
  299. package/dist/ws-metrics-server.js +31 -0
  300. package/dist/ws-server.js +41 -1
  301. package/package.json +62 -35
@@ -0,0 +1,779 @@
1
+ /**
2
+ * Capture Agent — Opcode Runner
3
+ *
4
+ * Deterministic execution engine for ExecutionProgram.
5
+ * Executes opcodes sequentially, verifies postconditions,
6
+ * delegates to recovery chain on failure, and respects circuit breaker.
7
+ */
8
+ import { isSoftOpcodeKind } from './execution-types.js';
9
+ import { evaluatePostcondition } from './postcondition.js';
10
+ import { ActionVerifier } from './action-verifier.js';
11
+ import { CircuitBreaker } from './circuit-breaker.js';
12
+ import { smartWaitForStability } from './smart-wait.js';
13
+ import { verifyCaptureQuality } from './capture-verification.js';
14
+ import { generateAltText } from './alt-text.js';
15
+ import { executeOpcodeCoreAction } from './opcode-actions.js';
16
+ import { logger } from './logger.js';
17
+ function formatOpcodeDebug(opcode) {
18
+ const fields = [];
19
+ const o = opcode;
20
+ if (typeof o.selector === 'string')
21
+ fields.push(`selector="${o.selector}"`);
22
+ if (typeof o.url === 'string')
23
+ fields.push(`url="${o.url}"`);
24
+ if (typeof o.text === 'string')
25
+ fields.push(`text="${o.text.slice(0, 40)}"`);
26
+ if (typeof o.stateName === 'string')
27
+ fields.push(`stateName="${o.stateName}"`);
28
+ if (typeof o.groupName === 'string')
29
+ fields.push(`group="${o.groupName}"`);
30
+ if (opcode.postcondition)
31
+ fields.push(`post=${opcode.postcondition.type}`);
32
+ const recovery = opcode.recovery;
33
+ if (recovery) {
34
+ const flags = [];
35
+ if (recovery.retries)
36
+ flags.push(`retries=${recovery.retries}`);
37
+ if (recovery.useSelectorMemory)
38
+ flags.push('selMem');
39
+ if (recovery.useAltInteraction)
40
+ flags.push('alt');
41
+ if (recovery.allowReload)
42
+ flags.push('reload');
43
+ if (recovery.allowHealer)
44
+ flags.push('healer');
45
+ if (flags.length)
46
+ fields.push(`recovery=[${flags.join(',')}]`);
47
+ }
48
+ return fields.length ? ` (${fields.join(', ')})` : '';
49
+ }
50
+ /** Default recovery chain for consumers that do not inject one. */
51
+ export class NoOpRecoveryChain {
52
+ async attempt() {
53
+ return { recovered: false, reason: 'no recovery chain configured' };
54
+ }
55
+ }
56
+ const MIN_CLIP_FINALIZATION_TIMEOUT_MS = 30000;
57
+ function resolveOpcodeTimeoutMs(opcode) {
58
+ if (opcode.kind === 'END_CLIP') {
59
+ return Math.max(opcode.timeoutMs, MIN_CLIP_FINALIZATION_TIMEOUT_MS);
60
+ }
61
+ return opcode.timeoutMs;
62
+ }
63
+ // ── Main execution function ─────────────────────────────────────────
64
+ export async function executeProgram(program, createAdapter, options = {}) {
65
+ const recoveryChain = options.recoveryChain ?? new NoOpRecoveryChain();
66
+ const maxParallelVariants = Math.max(1, Math.floor(options.maxParallelVariants ?? 1));
67
+ const startTime = Date.now();
68
+ const variantResults = new Array(program.variants.length);
69
+ const healerPatches = [];
70
+ const telemetry = {
71
+ llmCallCount: 0,
72
+ llmCostEur: 0,
73
+ llmStepUsages: [],
74
+ totalOpcodes: 0,
75
+ recoveredOpcodes: 0,
76
+ failedOpcodes: 0,
77
+ skippedOpcodes: 0,
78
+ healerInvocations: 0,
79
+ circuitBreakerTrips: 0,
80
+ };
81
+ let nextVariantIndex = 0;
82
+ const workerCount = Math.min(maxParallelVariants, program.variants.length);
83
+ const workers = Array.from({ length: workerCount }, async () => {
84
+ while (!options.abortSignal?.aborted) {
85
+ const currentIndex = nextVariantIndex++;
86
+ if (currentIndex >= program.variants.length) {
87
+ return;
88
+ }
89
+ const variant = program.variants[currentIndex];
90
+ options.onProgress?.({
91
+ type: 'variant_start',
92
+ variantId: variant.id,
93
+ message: `starting variant ${variant.id}`,
94
+ });
95
+ const variantResult = await executeVariant(program, variant, createAdapter, recoveryChain, telemetry, healerPatches, options);
96
+ variantResults[currentIndex] = variantResult;
97
+ options.onProgress?.({
98
+ type: 'variant_end',
99
+ variantId: variant.id,
100
+ status: variantResult.success ? 'ok' : 'failed',
101
+ message: variantResult.success
102
+ ? `variant ${variant.id} completed`
103
+ : `variant ${variant.id} failed: ${variantResult.error}`,
104
+ });
105
+ }
106
+ });
107
+ await Promise.all(workers);
108
+ const completedVariantResults = variantResults.filter((result) => Boolean(result));
109
+ const aborted = options.abortSignal?.aborted && completedVariantResults.length < program.variants.length;
110
+ const success = !aborted && completedVariantResults.length > 0 && completedVariantResults.every(v => v.success);
111
+ return {
112
+ programId: program.presetId,
113
+ success,
114
+ variantResults: completedVariantResults,
115
+ telemetry,
116
+ healerPatches: success ? healerPatches : [], // Only propagate patches on success
117
+ totalDurationMs: Date.now() - startTime,
118
+ error: aborted ? 'aborted' : (success ? undefined : completedVariantResults.find(v => !v.success)?.error),
119
+ };
120
+ }
121
+ // ── Variant execution ───────────────────────────────────────────────
122
+ async function executeVariant(program, variant, createAdapter, recoveryChain, telemetry, healerPatches, options) {
123
+ const startTime = Date.now();
124
+ const breaker = new CircuitBreaker();
125
+ const verifier = new ActionVerifier();
126
+ const opcodeResults = [];
127
+ const artifacts = [];
128
+ const executionState = {};
129
+ let adapter = null;
130
+ try {
131
+ adapter = await createAdapter(variant);
132
+ for (let i = 0; i < program.steps.length; i++) {
133
+ if (options.abortSignal?.aborted) {
134
+ return {
135
+ variantId: variant.id,
136
+ success: false,
137
+ opcodeResults,
138
+ durationMs: Date.now() - startTime,
139
+ artifacts,
140
+ error: 'aborted',
141
+ };
142
+ }
143
+ // Check circuit breaker before starting
144
+ const breakerState = breaker.isTripped();
145
+ if (breakerState.tripped) {
146
+ telemetry.circuitBreakerTrips++;
147
+ options.onProgress?.({
148
+ type: 'breaker_trip',
149
+ variantId: variant.id,
150
+ opcodeIndex: i,
151
+ message: breakerState.reason,
152
+ });
153
+ return {
154
+ variantId: variant.id,
155
+ success: false,
156
+ opcodeResults,
157
+ durationMs: Date.now() - startTime,
158
+ artifacts,
159
+ error: `circuit breaker tripped: ${breakerState.reason}`,
160
+ };
161
+ }
162
+ const opcode = program.steps[i];
163
+ options.onProgress?.({
164
+ type: 'opcode_start',
165
+ variantId: variant.id,
166
+ opcodeIndex: i,
167
+ opcodeKind: opcode.kind,
168
+ message: opcode.description,
169
+ });
170
+ const result = await executeOpcode(opcode, i, adapter, verifier, breaker, recoveryChain, telemetry, healerPatches, artifacts, options, variant.id, executionState, program.artifactPlan, program.mockDataGroups, variant, program.preconditions.credentials);
171
+ opcodeResults.push(result);
172
+ telemetry.totalOpcodes++;
173
+ if (result.status === 'recovered')
174
+ telemetry.recoveredOpcodes++;
175
+ if (result.status === 'failed')
176
+ telemetry.failedOpcodes++;
177
+ options.onProgress?.({
178
+ type: 'opcode_end',
179
+ variantId: variant.id,
180
+ opcodeIndex: i,
181
+ opcodeKind: opcode.kind,
182
+ status: result.status,
183
+ message: result.error ?? `${opcode.kind} ${result.status}`,
184
+ });
185
+ // Abort variant on fatal opcode failure
186
+ if (result.status === 'failed') {
187
+ return {
188
+ variantId: variant.id,
189
+ success: false,
190
+ opcodeResults,
191
+ durationMs: Date.now() - startTime,
192
+ artifacts,
193
+ error: `opcode ${i} (${opcode.kind}) failed: ${result.error}`,
194
+ };
195
+ }
196
+ }
197
+ return {
198
+ variantId: variant.id,
199
+ success: true,
200
+ opcodeResults,
201
+ durationMs: Date.now() - startTime,
202
+ artifacts,
203
+ };
204
+ }
205
+ catch (err) {
206
+ return {
207
+ variantId: variant.id,
208
+ success: false,
209
+ opcodeResults,
210
+ durationMs: Date.now() - startTime,
211
+ artifacts,
212
+ error: `unexpected error: ${err instanceof Error ? err.message : String(err)}`,
213
+ };
214
+ }
215
+ finally {
216
+ if (adapter) {
217
+ try {
218
+ await adapter.close();
219
+ }
220
+ catch { /* ignore close errors */ }
221
+ }
222
+ }
223
+ }
224
+ // ── Single opcode execution ─────────────────────────────────────────
225
+ /**
226
+ * Mark a soft opcode as skipped without aborting the variant.
227
+ * Increments telemetry and (for INJECT_MOCK_DATA) records the group result.
228
+ * Circuit breaker is intentionally not ticked.
229
+ */
230
+ function softSkipResult(opcode, index, startTime, reason, telemetry) {
231
+ telemetry.skippedOpcodes++;
232
+ if (opcode.kind === 'INJECT_MOCK_DATA') {
233
+ if (!telemetry.mockDataGroupResults)
234
+ telemetry.mockDataGroupResults = {};
235
+ telemetry.mockDataGroupResults[opcode.groupName] = 'skipped';
236
+ }
237
+ return {
238
+ opcodeIndex: index,
239
+ kind: opcode.kind,
240
+ status: 'skipped',
241
+ durationMs: Date.now() - startTime,
242
+ recoveryAttempts: 0,
243
+ error: reason,
244
+ };
245
+ }
246
+ async function executeOpcode(opcode, index, adapter, verifier, breaker, recoveryChain, telemetry, healerPatches, artifacts, options, variantId, executionState, artifactPlan, mockDataGroups, currentVariant, credentials) {
247
+ const startTime = Date.now();
248
+ const effectiveTimeoutMs = resolveOpcodeTimeoutMs(opcode);
249
+ const deadlineMs = startTime + effectiveTimeoutMs;
250
+ const isInteraction = ['CLICK', 'TYPE', 'PRESS_KEY', 'SCROLL'].includes(opcode.kind);
251
+ const isSoft = isSoftOpcodeKind(opcode.kind);
252
+ // Track page context for circuit breaker
253
+ try {
254
+ const url = await adapter.getCurrentUrl();
255
+ breaker.setPage(url);
256
+ }
257
+ catch { /* ignore */ }
258
+ // Execute with timeout
259
+ try {
260
+ logger.debug(`[opcode ${index}] ${opcode.kind} start — budget ${effectiveTimeoutMs}ms${formatOpcodeDebug(opcode)}`);
261
+ if (isInteraction) {
262
+ const beforeStart = Date.now();
263
+ await verifier.captureBeforeState(adapter);
264
+ logger.debug(`[opcode ${index}] captureBeforeState took ${Date.now() - beforeStart}ms`);
265
+ }
266
+ const actionBudgetMs = getRemainingTimeMs(deadlineMs);
267
+ if (actionBudgetMs <= 0) {
268
+ const reason = `timeout after ${effectiveTimeoutMs}ms`;
269
+ logger.debug(`[opcode ${index}] no budget left after captureBeforeState (deadline=${deadlineMs}, now=${Date.now()})`);
270
+ if (isSoft)
271
+ return softSkipResult(opcode, index, startTime, reason, telemetry);
272
+ return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, reason);
273
+ }
274
+ logger.debug(`[opcode ${index}] action exec start — actionBudget ${actionBudgetMs}ms`);
275
+ const actionStart = Date.now();
276
+ const result = await withTimeout(() => executeOpcodeAction(opcode, index, adapter, artifacts, telemetry, currentVariant, executionState, artifactPlan, mockDataGroups, options, credentials), actionBudgetMs);
277
+ logger.debug(`[opcode ${index}] action exec end — took ${Date.now() - actionStart}ms, success=${result.success}${result.error ? `, error=${result.error}` : ''}`);
278
+ if (!result.success) {
279
+ const reason = result.error ?? 'action failed';
280
+ if (isSoft)
281
+ return softSkipResult(opcode, index, startTime, reason, telemetry);
282
+ return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, reason);
283
+ }
284
+ // Verify postcondition
285
+ const postconditionBudgetMs = getRemainingTimeMs(deadlineMs);
286
+ if (postconditionBudgetMs <= 0) {
287
+ const reason = `timeout after ${effectiveTimeoutMs}ms`;
288
+ logger.debug(`[opcode ${index}] no budget left for postcondition check`);
289
+ if (isSoft)
290
+ return softSkipResult(opcode, index, startTime, reason, telemetry);
291
+ return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, reason);
292
+ }
293
+ const postStart = Date.now();
294
+ const postcondition = await evaluatePostcondition(adapter, withClampedPostconditionTimeout(opcode.postcondition, postconditionBudgetMs));
295
+ logger.debug(`[opcode ${index}] postcondition (${opcode.postcondition.type}) took ${Date.now() - postStart}ms — passed=${postcondition.passed}, reason="${postcondition.reason}"`);
296
+ if (!postcondition.passed) {
297
+ const reason = `postcondition failed: ${postcondition.reason}`;
298
+ if (isSoft)
299
+ return softSkipResult(opcode, index, startTime, reason, telemetry);
300
+ return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, reason);
301
+ }
302
+ // Verify action had effect (for interaction opcodes)
303
+ if (isInteraction) {
304
+ const verification = await verifier.verifyAfterAction(adapter);
305
+ if (!verification.hadEffect && opcode.postcondition.type !== 'always' && opcode.postcondition.type !== 'any_change') {
306
+ return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, `action had no effect: ${verification.summary}`);
307
+ }
308
+ }
309
+ // Record successful mock data application
310
+ if (opcode.kind === 'INJECT_MOCK_DATA') {
311
+ if (!telemetry.mockDataGroupResults)
312
+ telemetry.mockDataGroupResults = {};
313
+ telemetry.mockDataGroupResults[opcode.groupName] = 'applied';
314
+ }
315
+ breaker.recordSuccess(index);
316
+ return {
317
+ opcodeIndex: index,
318
+ kind: opcode.kind,
319
+ status: 'ok',
320
+ durationMs: Date.now() - startTime,
321
+ recoveryAttempts: 0,
322
+ };
323
+ }
324
+ catch (err) {
325
+ const errorMsg = err instanceof Error ? err.message : String(err);
326
+ if (isSoft)
327
+ return softSkipResult(opcode, index, startTime, errorMsg, telemetry);
328
+ return handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, errorMsg);
329
+ }
330
+ }
331
+ // ── Failure handling with recovery ──────────────────────────────────
332
+ async function handleFailure(opcode, index, adapter, verifier, isInteraction, breaker, recoveryChain, telemetry, healerPatches, options, variantId, currentVariant, startTime, deadlineMs, effectiveTimeoutMs, errorMsg) {
333
+ const breakerState = breaker.recordFailure(index, opcode.maxFailures);
334
+ if (breakerState.tripped) {
335
+ telemetry.circuitBreakerTrips++;
336
+ return {
337
+ opcodeIndex: index,
338
+ kind: opcode.kind,
339
+ status: 'failed',
340
+ durationMs: Date.now() - startTime,
341
+ recoveryAttempts: 0,
342
+ error: `${errorMsg} (circuit breaker: ${breakerState.reason})`,
343
+ };
344
+ }
345
+ const remainingTimeMs = getRemainingTimeMs(deadlineMs);
346
+ if (remainingTimeMs <= 0) {
347
+ return {
348
+ opcodeIndex: index,
349
+ kind: opcode.kind,
350
+ status: 'failed',
351
+ durationMs: Date.now() - startTime,
352
+ recoveryAttempts: 0,
353
+ error: `${errorMsg} (timeout after ${effectiveTimeoutMs}ms)`,
354
+ };
355
+ }
356
+ // Attempt recovery
357
+ options.onProgress?.({
358
+ type: 'recovery',
359
+ variantId,
360
+ opcodeIndex: index,
361
+ opcodeKind: opcode.kind,
362
+ message: `recovering from: ${errorMsg}`,
363
+ });
364
+ const recovery = await recoveryChain.attempt(opcode, index, adapter, {
365
+ remainingTimeMs,
366
+ maxDeterministicRetries: Math.max(0, opcode.maxFailures - breakerState.opcodeFailures),
367
+ currentVariant,
368
+ });
369
+ if (recovery.llmResult) {
370
+ telemetry.llmCallCount++;
371
+ telemetry.llmCostEur += recovery.llmResult.costEur;
372
+ telemetry.llmStepUsages.push({
373
+ stepType: 'healer_invocation',
374
+ generationId: recovery.llmResult.generationId,
375
+ model: recovery.llmResult.model,
376
+ promptTokens: recovery.llmResult.promptTokens,
377
+ completionTokens: recovery.llmResult.completionTokens,
378
+ });
379
+ telemetry.healerInvocations++;
380
+ }
381
+ if (recovery.recovered) {
382
+ if (recovery.patch) {
383
+ healerPatches.push(recovery.patch);
384
+ }
385
+ if (isInteraction) {
386
+ const verification = await verifier.verifyAfterAction(adapter);
387
+ if (!verification.hadEffect && opcode.postcondition.type !== 'always' && opcode.postcondition.type !== 'any_change') {
388
+ return {
389
+ opcodeIndex: index,
390
+ kind: opcode.kind,
391
+ status: 'failed',
392
+ durationMs: Date.now() - startTime,
393
+ recoveryAttempts: 1,
394
+ error: `${errorMsg} (recovery succeeded but action still had no effect: ${verification.summary})`,
395
+ };
396
+ }
397
+ }
398
+ breaker.recordSuccess(index);
399
+ return {
400
+ opcodeIndex: index,
401
+ kind: opcode.kind,
402
+ status: 'recovered',
403
+ durationMs: Date.now() - startTime,
404
+ recoveryAttempts: 1,
405
+ recoveryStrategy: recovery.strategy,
406
+ };
407
+ }
408
+ return {
409
+ opcodeIndex: index,
410
+ kind: opcode.kind,
411
+ status: 'failed',
412
+ durationMs: Date.now() - startTime,
413
+ recoveryAttempts: 1,
414
+ error: `${errorMsg} (recovery failed: ${recovery.reason})`,
415
+ };
416
+ }
417
+ // ── Opcode action dispatch ──────────────────────────────────────────
418
+ async function executeOpcodeAction(opcode, opcodeIndex, adapter, artifacts, telemetry, currentVariant, executionState, artifactPlan, mockDataGroups, runOptions, credentials) {
419
+ try {
420
+ void artifactPlan;
421
+ switch (opcode.kind) {
422
+ case 'NAVIGATE':
423
+ case 'DISMISS_OVERLAYS':
424
+ case 'CLICK':
425
+ case 'TYPE':
426
+ case 'PRESS_KEY':
427
+ case 'WAIT_FOR':
428
+ case 'SET_LOCALE':
429
+ case 'SET_THEME':
430
+ case 'SCROLL':
431
+ case 'HOVER':
432
+ case 'SELECT_OPTION':
433
+ case 'CHECK':
434
+ case 'DOUBLE_CLICK':
435
+ case 'DRAG':
436
+ case 'CLONE_ELEMENT':
437
+ case 'INJECT_MOCK_DATA':
438
+ case 'REMOVE_ELEMENT':
439
+ case 'SET_ATTRIBUTE':
440
+ return executeOpcodeCoreAction(opcode, adapter, { currentVariant, mockDataGroups, credentials });
441
+ case 'ASSERT_ROUTE':
442
+ return evaluateImmediateAssertion(await evaluatePostcondition(adapter, {
443
+ type: 'route_matches',
444
+ pattern: opcode.urlPattern,
445
+ waitMs: 1,
446
+ }), 'ASSERT_ROUTE failed');
447
+ case 'ASSERT_SURFACE':
448
+ return evaluateSurfaceAssertion(adapter, opcode.selectors, opcode.matchAll);
449
+ case 'CAPTURE_SCREENSHOT': {
450
+ await smartWaitForStability(adapter, { maxWaitMs: 5000 });
451
+ const captureUrl = await adapter.getCurrentUrl();
452
+ const takeBuffer = async () => {
453
+ if (opcode.elementSelector && adapter.takeElementScreenshot) {
454
+ return adapter.takeElementScreenshot(opcode.elementSelector);
455
+ }
456
+ if (opcode.elementSelector) {
457
+ throw new Error(`element capture requires adapter support for selector "${opcode.elementSelector}"`);
458
+ }
459
+ return adapter.takeScreenshot();
460
+ };
461
+ let buffer = await takeBuffer();
462
+ if (runOptions?.llmConfig) {
463
+ const verification = await verifyCaptureQuality(buffer, {
464
+ expectedDescription: opcode.description,
465
+ url: captureUrl,
466
+ locale: currentVariant?.locale,
467
+ theme: currentVariant?.theme,
468
+ }, runOptions.llmConfig);
469
+ if (verification.llmResult) {
470
+ telemetry.llmCallCount++;
471
+ telemetry.llmCostEur += verification.llmResult.costEur;
472
+ telemetry.llmStepUsages.push({
473
+ stepType: 'capture_verification',
474
+ generationId: verification.llmResult.generationId,
475
+ model: verification.llmResult.model,
476
+ promptTokens: verification.llmResult.promptTokens,
477
+ completionTokens: verification.llmResult.completionTokens,
478
+ });
479
+ }
480
+ if (!verification.passed) {
481
+ await smartWaitForStability(adapter, { maxWaitMs: 8000 });
482
+ const retryBuffer = await takeBuffer();
483
+ const retryVerification = await verifyCaptureQuality(retryBuffer, {
484
+ expectedDescription: opcode.description,
485
+ url: captureUrl,
486
+ locale: currentVariant?.locale,
487
+ theme: currentVariant?.theme,
488
+ }, runOptions.llmConfig);
489
+ if (retryVerification.llmResult) {
490
+ telemetry.llmCallCount++;
491
+ telemetry.llmCostEur += retryVerification.llmResult.costEur;
492
+ telemetry.llmStepUsages.push({
493
+ stepType: 'capture_verification',
494
+ generationId: retryVerification.llmResult.generationId,
495
+ model: retryVerification.llmResult.model,
496
+ promptTokens: retryVerification.llmResult.promptTokens,
497
+ completionTokens: retryVerification.llmResult.completionTokens,
498
+ });
499
+ }
500
+ if (retryVerification.passed) {
501
+ buffer = retryBuffer;
502
+ }
503
+ }
504
+ }
505
+ let altText;
506
+ if (runOptions?.llmConfig) {
507
+ try {
508
+ const altResult = await generateAltText(buffer, {
509
+ description: opcode.description,
510
+ url: captureUrl,
511
+ locale: currentVariant?.locale,
512
+ presetName: runOptions.presetName,
513
+ }, runOptions.llmConfig);
514
+ altText = altResult.altText;
515
+ if (altResult.llmResult) {
516
+ telemetry.llmCallCount++;
517
+ telemetry.llmCostEur += altResult.llmResult.costEur;
518
+ telemetry.llmStepUsages.push({
519
+ stepType: 'alt_text_generation',
520
+ generationId: altResult.llmResult.generationId,
521
+ model: altResult.llmResult.model,
522
+ promptTokens: altResult.llmResult.promptTokens,
523
+ completionTokens: altResult.llmResult.completionTokens,
524
+ });
525
+ }
526
+ }
527
+ catch {
528
+ // Alt text generation failed — non-fatal
529
+ }
530
+ }
531
+ // Extract page favicon for browser bar mockup
532
+ let tabIconData;
533
+ let tabIconMimeType;
534
+ if (adapter.extractFavicon) {
535
+ const favicon = await adapter.extractFavicon();
536
+ if (favicon) {
537
+ tabIconData = favicon.buffer;
538
+ tabIconMimeType = favicon.mimeType;
539
+ }
540
+ }
541
+ artifacts.push({
542
+ mediaMode: 'screenshot',
543
+ buffer,
544
+ mimeType: 'image/png',
545
+ captureType: opcode.elementSelector ? 'element' : 'fullpage',
546
+ captureUrl,
547
+ dimensions: currentVariant?.viewport,
548
+ captureId: opcode.captureId,
549
+ captureName: opcode.captureName ?? opcode.description,
550
+ elementSelector: opcode.elementSelector,
551
+ altText,
552
+ stepDescription: opcode.description,
553
+ stepIndex: opcodeIndex,
554
+ variantId: currentVariant?.id,
555
+ tabIconData,
556
+ tabIconMimeType,
557
+ });
558
+ break;
559
+ }
560
+ case 'BEGIN_CLIP':
561
+ if (executionState.activeClip) {
562
+ return { success: false, error: 'cannot start a new clip before the previous one ends' };
563
+ }
564
+ executionState.activeClip = {
565
+ clipId: opcode.clipId,
566
+ clipName: opcode.clipName,
567
+ };
568
+ await adapter.beginRecording({ mediaMode: 'clip' });
569
+ break;
570
+ case 'END_CLIP': {
571
+ const clipIdentity = resolveClipIdentity(executionState.activeClip, opcode);
572
+ const recording = await adapter.endRecording();
573
+ executionState.activeClip = undefined;
574
+ artifacts.push({
575
+ mediaMode: 'clip',
576
+ buffer: recording.buffer,
577
+ mimeType: recording.mimeType,
578
+ durationMs: recording.durationMs,
579
+ trimStartMs: recording.trimStartMs,
580
+ dimensions: undefined,
581
+ captureType: 'fullpage',
582
+ captureUrl: await adapter.getCurrentUrl(),
583
+ clipId: clipIdentity.clipId,
584
+ clipName: clipIdentity.clipName,
585
+ stepDescription: opcode.description,
586
+ stepIndex: opcodeIndex,
587
+ variantId: currentVariant?.id,
588
+ });
589
+ break;
590
+ }
591
+ case 'CAPTURE_DOM': {
592
+ if (opcode.selector) {
593
+ const attached = await adapter.waitFor({
594
+ selector: opcode.selector,
595
+ state: 'attached',
596
+ timeoutMs: 5000,
597
+ });
598
+ if (!attached) {
599
+ return {
600
+ success: false,
601
+ error: `CAPTURE_DOM could not find selector "${opcode.selector}" in the DOM`,
602
+ };
603
+ }
604
+ }
605
+ await smartWaitForStability(adapter, { maxWaitMs: 5000 });
606
+ if (!adapter.serializeDom) {
607
+ return {
608
+ success: false,
609
+ error: 'CAPTURE_DOM requires an adapter that implements serializeDom() — Interactive Demos pipeline (AUT-121)',
610
+ };
611
+ }
612
+ const captureUrl = await adapter.getCurrentUrl();
613
+ const serialized = await adapter.serializeDom(opcode.selector);
614
+ const buffer = Buffer.from(serialized.html, 'utf8');
615
+ // Take a viewport screenshot for the thumbnail
616
+ const thumbnailBuffer = await adapter.takeScreenshot();
617
+ artifacts.push({
618
+ mediaMode: 'dom',
619
+ buffer,
620
+ mimeType: 'text/html; charset=utf-8',
621
+ captureType: opcode.selector ? 'element' : 'fullpage',
622
+ captureUrl,
623
+ dimensions: serialized.viewport,
624
+ captureId: opcode.stateName,
625
+ captureName: opcode.stateName,
626
+ stepDescription: opcode.description,
627
+ stepIndex: opcodeIndex,
628
+ variantId: currentVariant?.id,
629
+ elementSelector: opcode.selector,
630
+ stateName: opcode.stateName,
631
+ domHtml: serialized.html,
632
+ domAssetUrls: serialized.assetUrls,
633
+ domHtmlBytes: buffer.byteLength,
634
+ domThumbnailBuffer: thumbnailBuffer,
635
+ });
636
+ break;
637
+ }
638
+ case 'CAPTURE_FRAGMENT': {
639
+ if (!adapter.serializeFragment) {
640
+ return {
641
+ success: false,
642
+ error: 'CAPTURE_FRAGMENT requires an adapter that implements serializeFragment() — Interactive Demos Phase 5',
643
+ };
644
+ }
645
+ // Optional hidden trigger button (mirrors the mock data trigger
646
+ // pattern). Useful for fragments that aren't reachable via
647
+ // visible UI. The trigger is clicked via clickHidden() so it
648
+ // bypasses visibility/disabled checks.
649
+ if (opcode.triggerSelector) {
650
+ if (!adapter.clickHidden) {
651
+ return {
652
+ success: false,
653
+ error: `CAPTURE_FRAGMENT.triggerSelector requires adapter.clickHidden() support`,
654
+ };
655
+ }
656
+ try {
657
+ await adapter.clickHidden({ selector: opcode.triggerSelector });
658
+ }
659
+ catch (err) {
660
+ return {
661
+ success: false,
662
+ error: `CAPTURE_FRAGMENT trigger click failed: ${err instanceof Error ? err.message : String(err)}`,
663
+ };
664
+ }
665
+ }
666
+ // Wait for the fragment selector to be present in the DOM (the
667
+ // trigger may have inserted it dynamically). Mirror the screenshot
668
+ // smart-wait so animations / portal mounts settle before serialization.
669
+ const visible = await adapter.waitFor({
670
+ selector: opcode.selector,
671
+ state: 'attached',
672
+ timeoutMs: 5000,
673
+ });
674
+ if (!visible) {
675
+ return {
676
+ success: false,
677
+ error: `CAPTURE_FRAGMENT could not find selector "${opcode.selector}" in the DOM`,
678
+ };
679
+ }
680
+ await smartWaitForStability(adapter, { maxWaitMs: 3000 });
681
+ const captureUrl = await adapter.getCurrentUrl();
682
+ const serialized = await adapter.serializeFragment(opcode.selector);
683
+ const buffer = Buffer.from(serialized.html, 'utf8');
684
+ const variantName = opcode.variantName ?? 'default';
685
+ artifacts.push({
686
+ mediaMode: 'dom',
687
+ buffer,
688
+ mimeType: 'text/html; charset=utf-8',
689
+ captureType: 'element',
690
+ captureUrl,
691
+ // captureId is the unique upload key — including the variant
692
+ // ensures multiple variants of the same fragment do not collide.
693
+ captureId: `${opcode.parentState}/${opcode.fragmentName}/${variantName}`,
694
+ captureName: opcode.fragmentName,
695
+ elementSelector: opcode.selector,
696
+ stepDescription: opcode.description,
697
+ stepIndex: opcodeIndex,
698
+ variantId: currentVariant?.id,
699
+ fragmentName: opcode.fragmentName,
700
+ fragmentVariantName: variantName,
701
+ parentStateName: opcode.parentState,
702
+ mountStrategy: opcode.mountStrategy ?? 'inline',
703
+ mountTargetSelector: opcode.mountTargetSelector,
704
+ domHtml: serialized.html,
705
+ domAssetUrls: serialized.assetUrls,
706
+ domHtmlBytes: buffer.byteLength,
707
+ });
708
+ break;
709
+ }
710
+ default: {
711
+ const _exhaustive = opcode;
712
+ return { success: false, error: `unknown opcode kind: ${opcode.kind}` };
713
+ }
714
+ }
715
+ return { success: true };
716
+ }
717
+ catch (err) {
718
+ return {
719
+ success: false,
720
+ error: err instanceof Error ? err.message : String(err),
721
+ };
722
+ }
723
+ }
724
+ // ── Helpers ─────────────────────────────────────────────────────────
725
+ async function withTimeout(fn, timeoutMs) {
726
+ return new Promise((resolve, reject) => {
727
+ const timer = setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
728
+ fn()
729
+ .then(result => { clearTimeout(timer); resolve(result); })
730
+ .catch(err => { clearTimeout(timer); reject(err); });
731
+ });
732
+ }
733
+ function getRemainingTimeMs(deadlineMs) {
734
+ return Math.max(0, deadlineMs - Date.now());
735
+ }
736
+ function resolveClipIdentity(activeClip, opcode) {
737
+ if (activeClip?.clipId && opcode.clipId && activeClip.clipId !== opcode.clipId) {
738
+ throw new Error(`END_CLIP clipId "${opcode.clipId}" does not match active clip "${activeClip.clipId}"`);
739
+ }
740
+ return {
741
+ clipId: opcode.clipId ?? activeClip?.clipId,
742
+ clipName: opcode.clipName ?? activeClip?.clipName ?? opcode.description,
743
+ };
744
+ }
745
+ function withClampedPostconditionTimeout(spec, maxWaitMs) {
746
+ return {
747
+ ...spec,
748
+ waitMs: Math.max(1, Math.min(spec.waitMs ?? maxWaitMs, maxWaitMs)),
749
+ };
750
+ }
751
+ function evaluateImmediateAssertion(result, prefix) {
752
+ return result.passed
753
+ ? { success: true }
754
+ : { success: false, error: `${prefix}: ${result.reason}` };
755
+ }
756
+ async function evaluateSurfaceAssertion(adapter, selectors, matchAll) {
757
+ const checks = await Promise.all(selectors.map(async (selector) => ({
758
+ selector,
759
+ result: await evaluatePostcondition(adapter, {
760
+ type: 'element_visible',
761
+ selector,
762
+ waitMs: 1,
763
+ }),
764
+ })));
765
+ if (matchAll) {
766
+ const failed = checks.find((entry) => !entry.result.passed);
767
+ return failed
768
+ ? { success: false, error: `ASSERT_SURFACE failed for "${failed.selector}": ${failed.result.reason}` }
769
+ : { success: true };
770
+ }
771
+ if (checks.some((entry) => entry.result.passed)) {
772
+ return { success: true };
773
+ }
774
+ return {
775
+ success: false,
776
+ error: `ASSERT_SURFACE failed: none of the selectors matched (${selectors.join(', ')})`,
777
+ };
778
+ }
779
+ //# sourceMappingURL=opcode-runner.js.map