autokap 1.9.9 → 2.0.0

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.
@@ -56,13 +56,18 @@ export class NoOpRecoveryChain {
56
56
  const MIN_CLIP_FINALIZATION_TIMEOUT_MS = 30000;
57
57
  const DEFAULT_VIDEO_RECORDING_RESOLUTION = { width: 1920, height: 1080 };
58
58
  /**
59
- * Per-call bounds for the CAPTURE_SCREENSHOT action, kept well under the ~30s
60
- * global capture deadline so a hung Playwright call fails early and leaves budget
61
- * for recovery/recapture instead of voiding the variant. Fixed constants because
62
- * executeOpcodeAction has no access to the deadline; 12s + 3×3s worst case still
63
- * leaves >=12s of the 30s budget for the recovery path.
59
+ * Recovery budget reserved from the global capture deadline. The screenshot
60
+ * buffer (the artifact-critical call) may use the REST of the deadline, so a
61
+ * slow-but-completing high-res capture (e.g. a 5K device frame legitimately
62
+ * needing 15-30s under load) succeeds, while a truly hung one still fails with
63
+ * this slice left for the recovery/recapture path. Replaces the old flat 12s
64
+ * buffer cap, which was smaller than legitimate heavy captures and killed them
65
+ * (and killed the recapture too, since it re-applied the same 12s wall).
64
66
  */
65
- const CAPTURE_BUFFER_TIMEOUT_MS = 12000;
67
+ const CAPTURE_RECOVERY_RESERVE_MS = 10000;
68
+ /** Floor for the first-attempt capture budget when little global time remains. */
69
+ const MIN_CAPTURE_ACTION_BUDGET_MS = 8000;
70
+ /** Best-effort bound for browser-mockup metadata (URL / favicon / title). */
66
71
  const CAPTURE_METADATA_TIMEOUT_MS = 3000;
67
72
  /**
68
73
  * The compiled per-opcode action budget. For CAPTURE_SCREENSHOT this governs
@@ -510,7 +515,15 @@ async function executeOpcode(opcode, index, adapter, verifier, breaker, recovery
510
515
  const isPureWait = opcode.kind === 'WAIT_FOR';
511
516
  const usesGlobalDeadline = isPureWait || isArtifactProducingOpcode(opcode.kind);
512
517
  const actionDeadlineMs = usesGlobalDeadline ? globalDeadlineMs : deadlineMs;
513
- const actionBudgetMs = getRemainingTimeMs(actionDeadlineMs);
518
+ let actionBudgetMs = getRemainingTimeMs(actionDeadlineMs);
519
+ // Reserve a slice of the global deadline for recovery/recapture on the
520
+ // artifact-critical CAPTURE_SCREENSHOT: the capture uses the rest (so a slow
521
+ // high-res shot completes) yet a hung one still leaves room to recover.
522
+ // END_CLIP keeps its full budget — its finalization hard-fails without a
523
+ // recapture, so reserving would only shorten it.
524
+ if (opcode.kind === 'CAPTURE_SCREENSHOT') {
525
+ actionBudgetMs = Math.max(MIN_CAPTURE_ACTION_BUDGET_MS, actionBudgetMs - CAPTURE_RECOVERY_RESERVE_MS);
526
+ }
514
527
  if (actionBudgetMs <= 0) {
515
528
  const reason = `timeout after ${effectiveTimeoutMs}ms`;
516
529
  logger.debug(`[opcode ${index}] no budget left after captureBeforeState (deadline=${actionDeadlineMs}, now=${Date.now()})`);
@@ -891,13 +904,13 @@ async function executeOpcodeAction(opcode, opcodeIndex, adapter, artifacts, tele
891
904
  lowConfidenceReasons.push(`captured before visual stability: ${stability.reason}`);
892
905
  }
893
906
  const captureLowConfidenceReason = lowConfidenceReasons.join('; ') || undefined;
894
- // Capture the screenshot buffer FIRST and bound it this is the only
895
- // artifact-critical call. Under CPU contention (many cloud variants per
896
- // container) a Playwright screenshot can otherwise block the full ~30s
897
- // global deadline, leaving zero budget for recovery/recapture and voiding
898
- // the variant. Failing at ~12s keeps ≥12s of the 30s budget for the
899
- // recovery path (handleFailure → recapture in `failWithRecovery`).
900
- const buffer = await withTimeout(() => takeCaptureBuffer(adapter, opcode), CAPTURE_BUFFER_TIMEOUT_MS);
907
+ // Capture the screenshot buffer FIRST — the only artifact-critical call.
908
+ // It is bounded by the reserved action budget (the withTimeout around this
909
+ // whole action in executeOpcode) and the screenshot primitive's own
910
+ // timeout NOT a flat sub-cap so a legitimately slow high-res capture
911
+ // can use most of the deadline while a hung one still frees the reserved
912
+ // recovery slice (handleFailure → recapture in `failWithRecovery`).
913
+ const buffer = await takeCaptureBuffer(adapter, opcode);
901
914
  // Everything below is best-effort browser-mockup metadata. The buffer is
902
915
  // already in hand, so a slow/hung metadata call must NEVER throw out of
903
916
  // the action and discard a good screenshot. Each is individually bounded
@@ -277,13 +277,17 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
277
277
  stepId: z.ZodOptional<z.ZodString>;
278
278
  kind: z.ZodLiteral<"ASSERT_SURFACE">;
279
279
  }, z.core.$strict>, z.ZodObject<{
280
- selector: z.ZodString;
280
+ selector: z.ZodOptional<z.ZodString>;
281
281
  target: z.ZodOptional<z.ZodObject<{
282
282
  text: z.ZodOptional<z.ZodString>;
283
+ textByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
283
284
  role: z.ZodOptional<z.ZodString>;
284
285
  label: z.ZodOptional<z.ZodString>;
286
+ labelByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
285
287
  near: z.ZodOptional<z.ZodString>;
288
+ nearByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
286
289
  placeholder: z.ZodOptional<z.ZodString>;
290
+ placeholderByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
287
291
  exact: z.ZodOptional<z.ZodBoolean>;
288
292
  }, z.core.$strict>>;
289
293
  button: z.ZodOptional<z.ZodEnum<{
@@ -322,13 +326,17 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
322
326
  stepId: z.ZodOptional<z.ZodString>;
323
327
  kind: z.ZodLiteral<"CLICK">;
324
328
  }, z.core.$strict>, z.ZodObject<{
325
- selector: z.ZodString;
329
+ selector: z.ZodOptional<z.ZodString>;
326
330
  target: z.ZodOptional<z.ZodObject<{
327
331
  text: z.ZodOptional<z.ZodString>;
332
+ textByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
328
333
  role: z.ZodOptional<z.ZodString>;
329
334
  label: z.ZodOptional<z.ZodString>;
335
+ labelByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
330
336
  near: z.ZodOptional<z.ZodString>;
337
+ nearByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
331
338
  placeholder: z.ZodOptional<z.ZodString>;
339
+ placeholderByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
332
340
  exact: z.ZodOptional<z.ZodBoolean>;
333
341
  }, z.core.$strict>>;
334
342
  text: z.ZodString;
@@ -400,10 +408,14 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
400
408
  selector: z.ZodOptional<z.ZodString>;
401
409
  target: z.ZodOptional<z.ZodObject<{
402
410
  text: z.ZodOptional<z.ZodString>;
411
+ textByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
403
412
  role: z.ZodOptional<z.ZodString>;
404
413
  label: z.ZodOptional<z.ZodString>;
414
+ labelByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
405
415
  near: z.ZodOptional<z.ZodString>;
416
+ nearByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
406
417
  placeholder: z.ZodOptional<z.ZodString>;
418
+ placeholderByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
407
419
  exact: z.ZodOptional<z.ZodBoolean>;
408
420
  }, z.core.$strict>>;
409
421
  state: z.ZodEnum<{
@@ -579,10 +591,14 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
579
591
  targetSelector: z.ZodOptional<z.ZodString>;
580
592
  target: z.ZodOptional<z.ZodObject<{
581
593
  text: z.ZodOptional<z.ZodString>;
594
+ textByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
582
595
  role: z.ZodOptional<z.ZodString>;
583
596
  label: z.ZodOptional<z.ZodString>;
597
+ labelByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
584
598
  near: z.ZodOptional<z.ZodString>;
599
+ nearByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
585
600
  placeholder: z.ZodOptional<z.ZodString>;
601
+ placeholderByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
586
602
  exact: z.ZodOptional<z.ZodBoolean>;
587
603
  }, z.core.$strict>>;
588
604
  description: z.ZodString;
@@ -722,13 +738,17 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
722
738
  stepId: z.ZodOptional<z.ZodString>;
723
739
  kind: z.ZodLiteral<"END_CLIP">;
724
740
  }, z.core.$strict>, z.ZodObject<{
725
- selector: z.ZodString;
741
+ selector: z.ZodOptional<z.ZodString>;
726
742
  target: z.ZodOptional<z.ZodObject<{
727
743
  text: z.ZodOptional<z.ZodString>;
744
+ textByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
728
745
  role: z.ZodOptional<z.ZodString>;
729
746
  label: z.ZodOptional<z.ZodString>;
747
+ labelByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
730
748
  near: z.ZodOptional<z.ZodString>;
749
+ nearByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
731
750
  placeholder: z.ZodOptional<z.ZodString>;
751
+ placeholderByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
732
752
  exact: z.ZodOptional<z.ZodBoolean>;
733
753
  }, z.core.$strict>>;
734
754
  fingerprint: z.ZodOptional<z.ZodString>;
@@ -763,13 +783,17 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
763
783
  stepId: z.ZodOptional<z.ZodString>;
764
784
  kind: z.ZodLiteral<"HOVER">;
765
785
  }, z.core.$strict>, z.ZodObject<{
766
- selector: z.ZodString;
786
+ selector: z.ZodOptional<z.ZodString>;
767
787
  target: z.ZodOptional<z.ZodObject<{
768
788
  text: z.ZodOptional<z.ZodString>;
789
+ textByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
769
790
  role: z.ZodOptional<z.ZodString>;
770
791
  label: z.ZodOptional<z.ZodString>;
792
+ labelByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
771
793
  near: z.ZodOptional<z.ZodString>;
794
+ nearByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
772
795
  placeholder: z.ZodOptional<z.ZodString>;
796
+ placeholderByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
773
797
  exact: z.ZodOptional<z.ZodBoolean>;
774
798
  }, z.core.$strict>>;
775
799
  optionLabel: z.ZodOptional<z.ZodString>;
@@ -807,13 +831,17 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
807
831
  stepId: z.ZodOptional<z.ZodString>;
808
832
  kind: z.ZodLiteral<"SELECT_OPTION">;
809
833
  }, z.core.$strict>, z.ZodObject<{
810
- selector: z.ZodString;
834
+ selector: z.ZodOptional<z.ZodString>;
811
835
  target: z.ZodOptional<z.ZodObject<{
812
836
  text: z.ZodOptional<z.ZodString>;
837
+ textByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
813
838
  role: z.ZodOptional<z.ZodString>;
814
839
  label: z.ZodOptional<z.ZodString>;
840
+ labelByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
815
841
  near: z.ZodOptional<z.ZodString>;
842
+ nearByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
816
843
  placeholder: z.ZodOptional<z.ZodString>;
844
+ placeholderByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
817
845
  exact: z.ZodOptional<z.ZodBoolean>;
818
846
  }, z.core.$strict>>;
819
847
  checked: z.ZodBoolean;
@@ -849,13 +877,17 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
849
877
  stepId: z.ZodOptional<z.ZodString>;
850
878
  kind: z.ZodLiteral<"CHECK">;
851
879
  }, z.core.$strict>, z.ZodObject<{
852
- selector: z.ZodString;
880
+ selector: z.ZodOptional<z.ZodString>;
853
881
  target: z.ZodOptional<z.ZodObject<{
854
882
  text: z.ZodOptional<z.ZodString>;
883
+ textByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
855
884
  role: z.ZodOptional<z.ZodString>;
856
885
  label: z.ZodOptional<z.ZodString>;
886
+ labelByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
857
887
  near: z.ZodOptional<z.ZodString>;
888
+ nearByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
858
889
  placeholder: z.ZodOptional<z.ZodString>;
890
+ placeholderByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
859
891
  exact: z.ZodOptional<z.ZodBoolean>;
860
892
  }, z.core.$strict>>;
861
893
  fingerprint: z.ZodOptional<z.ZodString>;
@@ -890,13 +922,17 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
890
922
  stepId: z.ZodOptional<z.ZodString>;
891
923
  kind: z.ZodLiteral<"DOUBLE_CLICK">;
892
924
  }, z.core.$strict>, z.ZodObject<{
893
- selector: z.ZodString;
925
+ selector: z.ZodOptional<z.ZodString>;
894
926
  target: z.ZodOptional<z.ZodObject<{
895
927
  text: z.ZodOptional<z.ZodString>;
928
+ textByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
896
929
  role: z.ZodOptional<z.ZodString>;
897
930
  label: z.ZodOptional<z.ZodString>;
931
+ labelByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
898
932
  near: z.ZodOptional<z.ZodString>;
933
+ nearByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
899
934
  placeholder: z.ZodOptional<z.ZodString>;
935
+ placeholderByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
900
936
  exact: z.ZodOptional<z.ZodBoolean>;
901
937
  }, z.core.$strict>>;
902
938
  fingerprint: z.ZodOptional<z.ZodString>;
@@ -904,10 +940,14 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
904
940
  toSelector: z.ZodOptional<z.ZodString>;
905
941
  toTarget: z.ZodOptional<z.ZodObject<{
906
942
  text: z.ZodOptional<z.ZodString>;
943
+ textByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
907
944
  role: z.ZodOptional<z.ZodString>;
908
945
  label: z.ZodOptional<z.ZodString>;
946
+ labelByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
909
947
  near: z.ZodOptional<z.ZodString>;
948
+ nearByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
910
949
  placeholder: z.ZodOptional<z.ZodString>;
950
+ placeholderByLocale: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
911
951
  exact: z.ZodOptional<z.ZodBoolean>;
912
952
  }, z.core.$strict>>;
913
953
  toSelectorAlternates: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -8,7 +8,7 @@
8
8
  * 4. Targeted reload (reload page and retry)
9
9
  * 5. LLM Healer (last resort)
10
10
  */
11
- import { resolveSelector } from './selector-resolver.js';
11
+ import { resolveSelector, resolveSelectorByFuzzyText, semanticTargetFuzzyText, } from './selector-resolver.js';
12
12
  import { evaluatePostcondition, evaluatePostconditionWithProgress } from './postcondition.js';
13
13
  import { LLMHealer } from './llm-healer.js';
14
14
  import { serializeAKTree } from './ak-tree.js';
@@ -41,7 +41,7 @@ export class RecoveryChainImpl {
41
41
  return result;
42
42
  }
43
43
  // Strategy 2: Selector memory + alternate selectors
44
- if (recovery.useSelectorMemory && hasSelector(failedOpcode)) {
44
+ if (recovery.useSelectorMemory && (hasSelector(failedOpcode) || hasSemanticTarget(failedOpcode))) {
45
45
  logger.debug(`[recovery ${opcodeIndex}] strategy 2 (selector memory)`);
46
46
  const result = await trySelectorAlternatives(failedOpcode, opcodeIndex, adapter, this.selectorMemory, this.credentials, options.currentVariant);
47
47
  logger.debug(`[recovery ${opcodeIndex}] strategy 2 → recovered=${result.recovered}, reason=${result.reason}`);
@@ -49,7 +49,7 @@ export class RecoveryChainImpl {
49
49
  return result;
50
50
  }
51
51
  // Strategy 3: Alternative interaction methods
52
- if (recovery.useAltInteraction && hasSelector(failedOpcode)) {
52
+ if (recovery.useAltInteraction && (hasSelector(failedOpcode) || hasSemanticTarget(failedOpcode))) {
53
53
  logger.debug(`[recovery ${opcodeIndex}] strategy 3 (alt interaction: keyboard, js_dispatch, coords)`);
54
54
  const result = await tryAltInteraction(failedOpcode, adapter, this.credentials);
55
55
  logger.debug(`[recovery ${opcodeIndex}] strategy 3 → recovered=${result.recovered}, reason=${result.reason}`);
@@ -114,14 +114,14 @@ async function retryOpcode(opcode, adapter, maxRetries, remainingTimeMs, current
114
114
  }
115
115
  // ── Strategy 2: Selector memory ─────────────────────────────────────
116
116
  async function trySelectorAlternatives(opcode, opcodeIndex, adapter, selectorMemory, credentials, currentVariant) {
117
- if (!hasSelector(opcode)) {
118
- return { recovered: false, reason: 'opcode has no selector' };
117
+ if (!hasSelector(opcode) && !hasSemanticTarget(opcode)) {
118
+ return { recovered: false, reason: 'opcode has no selector or target' };
119
119
  }
120
120
  const selectorOpcode = opcode;
121
- const primarySelector = selectorOpcode.selector ?? '';
122
- const stepSignature = `${opcode.kind}:${primarySelector}`;
121
+ const stepSignature = buildStepSignature(opcode);
123
122
  const resolved = await resolveSelector(adapter, {
124
- primary: primarySelector,
123
+ primary: selectorOpcode.selector,
124
+ target: selectorOpcode.target,
125
125
  fingerprint: selectorOpcode.fingerprint,
126
126
  alternates: selectorOpcode.selectorAlternates,
127
127
  selectorMemory,
@@ -150,11 +150,26 @@ async function trySelectorAlternatives(opcode, opcodeIndex, adapter, selectorMem
150
150
  }
151
151
  // ── Strategy 3: Alternative interaction ─────────────────────────────
152
152
  async function tryAltInteraction(opcode, adapter, _credentials) {
153
- if (opcode.kind !== 'CLICK' || !opcode.selector) {
154
- return { recovered: false, reason: 'alt interaction only for CLICK opcodes with selectors' };
153
+ if (opcode.kind !== 'CLICK') {
154
+ return { recovered: false, reason: 'alt interaction only for CLICK opcodes' };
155
155
  }
156
156
  const clickOpcode = opcode;
157
- const sel = clickOpcode.selector;
157
+ let sel = clickOpcode.selector;
158
+ if (!sel && clickOpcode.target) {
159
+ // Target-only opcode: resolve a concrete selector from the AKTree using
160
+ // the target's text/label so the alternative interaction methods have
161
+ // something to act on.
162
+ try {
163
+ const tree = await adapter.getAKTree();
164
+ sel = resolveSelectorByFuzzyText(tree, semanticTargetFuzzyText(clickOpcode.target)) ?? undefined;
165
+ }
166
+ catch {
167
+ // fall through to the guard below
168
+ }
169
+ }
170
+ if (!sel) {
171
+ return { recovered: false, reason: 'alt interaction needs a selector or a target resolvable in the AKTree' };
172
+ }
158
173
  const methods = [
159
174
  { name: 'keyboard', options: { useKeyboard: true } },
160
175
  { name: 'js_dispatch', options: { useJsDispatch: true } },
@@ -291,6 +306,30 @@ async function tryHealer(failedOpcode, opcodeIndex, adapter, healer, programStep
291
306
  function hasSelector(opcode) {
292
307
  return 'selector' in opcode && typeof opcode.selector === 'string' && !!opcode.selector;
293
308
  }
309
+ function hasSemanticTarget(opcode) {
310
+ return 'target' in opcode && Boolean(opcode.target);
311
+ }
312
+ /**
313
+ * Stable signature for selector-memory lookups. Selector-based opcodes keep
314
+ * the legacy `KIND:selector` form; target-only opcodes derive a signature
315
+ * from the semantic descriptor so memory survives across runs.
316
+ */
317
+ function buildStepSignature(opcode) {
318
+ const { selector, target } = opcode;
319
+ if (selector)
320
+ return `${opcode.kind}:${selector}`;
321
+ if (target) {
322
+ const name = target.label
323
+ ?? target.text
324
+ ?? target.placeholder
325
+ ?? Object.values(target.labelByLocale ?? {})[0]
326
+ ?? Object.values(target.textByLocale ?? {})[0]
327
+ ?? Object.values(target.placeholderByLocale ?? {})[0]
328
+ ?? '';
329
+ return `${opcode.kind}:target:${target.role ?? ''}/${name}`;
330
+ }
331
+ return `${opcode.kind}:`;
332
+ }
294
333
  async function executeRawAction(opcode, adapter, currentVariant, credentials, suppressPageReloads = false) {
295
334
  const result = await executeOpcodeCoreAction(opcode, adapter, {
296
335
  currentVariant,
@@ -302,7 +341,7 @@ async function executeRawAction(opcode, adapter, currentVariant, credentials, su
302
341
  }
303
342
  }
304
343
  async function executeHealerPatchedAction(opcode, adapter, interactionMode, currentVariant, credentials, suppressPageReloads = false) {
305
- if (opcode.kind === 'CLICK' && interactionMode && interactionMode !== 'default') {
344
+ if (opcode.kind === 'CLICK' && opcode.selector && interactionMode && interactionMode !== 'default') {
306
345
  const options = interactionMode === 'keyboard'
307
346
  ? { useKeyboard: true }
308
347
  : interactionMode === 'js_dispatch'
@@ -7,15 +7,18 @@
7
7
  * 3. Selector memory candidates (from Supabase)
8
8
  * 4. Fuzzy text match on AKTree nodes
9
9
  */
10
- import type { RuntimeAdapter } from './execution-types.js';
10
+ import type { AKTree } from './types.js';
11
+ import type { RuntimeAdapter, SemanticTarget } from './execution-types.js';
11
12
  export interface ResolvedSelector {
12
13
  selector: string;
13
- strategy: 'primary' | 'fingerprint' | 'memory' | 'fuzzy_text' | 'alternate';
14
+ strategy: 'primary' | 'fingerprint' | 'memory' | 'fuzzy_text' | 'alternate' | 'target_text';
14
15
  confidence: 'high' | 'medium' | 'low';
15
16
  }
16
17
  export interface SelectorResolverOptions {
17
- /** Primary selector from the compiled opcode */
18
- primary: string;
18
+ /** Primary selector from the compiled opcode. Absent for target-only opcodes. */
19
+ primary?: string;
20
+ /** Semantic target from the compiled opcode (for text-based AKTree matching) */
21
+ target?: SemanticTarget;
19
22
  /** AKTree fingerprint for fuzzy matching */
20
23
  fingerprint?: string;
21
24
  /** Alternative selectors from the compiled opcode */
@@ -32,3 +35,15 @@ export interface SelectorResolverOptions {
32
35
  * Returns the first working selector, or null if nothing matches.
33
36
  */
34
37
  export declare function resolveSelector(adapter: RuntimeAdapter, options: SelectorResolverOptions): Promise<ResolvedSelector | null>;
38
+ /**
39
+ * Flattens a SemanticTarget into the text used for AKTree fuzzy matching:
40
+ * base label/text/placeholder plus every locale variant. Returns '' when the
41
+ * target carries no text at all (e.g. role-only).
42
+ */
43
+ export declare function semanticTargetFuzzyText(target: SemanticTarget): string;
44
+ /**
45
+ * Resolves a CSS selector for a visible interactive AKTree node matching the
46
+ * given text. Used by recovery strategies that need a concrete selector for a
47
+ * target-only opcode (e.g. coordinate clicks).
48
+ */
49
+ export declare function resolveSelectorByFuzzyText(tree: AKTree, text: string): string | null;
@@ -14,7 +14,7 @@
14
14
  export async function resolveSelector(adapter, options) {
15
15
  const tree = await adapter.getAKTree();
16
16
  // 1. Try primary selector
17
- if (await selectorExists(adapter, options.primary)) {
17
+ if (options.primary && await selectorExists(adapter, options.primary)) {
18
18
  return { selector: options.primary, strategy: 'primary', confidence: 'high' };
19
19
  }
20
20
  // 2. Try alternate selectors
@@ -32,7 +32,17 @@ export async function resolveSelector(adapter, options) {
32
32
  return { selector: match, strategy: 'fingerprint', confidence: 'medium' };
33
33
  }
34
34
  }
35
- // 4. Try selector memory
35
+ // 4. Try the semantic target's text/label against the AKTree
36
+ if (options.target) {
37
+ const targetText = semanticTargetFuzzyText(options.target);
38
+ if (targetText) {
39
+ const match = findNodeByFuzzyText(tree, targetText);
40
+ if (match) {
41
+ return { selector: match, strategy: 'target_text', confidence: 'medium' };
42
+ }
43
+ }
44
+ }
45
+ // 5. Try selector memory
36
46
  if (options.selectorMemory && options.stepSignature) {
37
47
  const candidates = options.selectorMemory[options.stepSignature];
38
48
  if (candidates) {
@@ -43,7 +53,7 @@ export async function resolveSelector(adapter, options) {
43
53
  }
44
54
  }
45
55
  }
46
- // 5. Try fuzzy text match
56
+ // 6. Try fuzzy text match on the opcode description
47
57
  if (options.description) {
48
58
  const match = findNodeByFuzzyText(tree, options.description);
49
59
  if (match) {
@@ -52,6 +62,32 @@ export async function resolveSelector(adapter, options) {
52
62
  }
53
63
  return null;
54
64
  }
65
+ /**
66
+ * Flattens a SemanticTarget into the text used for AKTree fuzzy matching:
67
+ * base label/text/placeholder plus every locale variant. Returns '' when the
68
+ * target carries no text at all (e.g. role-only).
69
+ */
70
+ export function semanticTargetFuzzyText(target) {
71
+ const parts = [
72
+ target.label,
73
+ target.text,
74
+ target.placeholder,
75
+ ...Object.values(target.labelByLocale ?? {}),
76
+ ...Object.values(target.textByLocale ?? {}),
77
+ ...Object.values(target.placeholderByLocale ?? {}),
78
+ ].filter((value) => Boolean(value && value.trim()));
79
+ return parts.join(' ');
80
+ }
81
+ /**
82
+ * Resolves a CSS selector for a visible interactive AKTree node matching the
83
+ * given text. Used by recovery strategies that need a concrete selector for a
84
+ * target-only opcode (e.g. coordinate clicks).
85
+ */
86
+ export function resolveSelectorByFuzzyText(tree, text) {
87
+ if (!text.trim())
88
+ return null;
89
+ return findNodeByFuzzyText(tree, text);
90
+ }
55
91
  // ── Helpers ─────────────────────────────────────────────────────────
56
92
  async function selectorExists(adapter, selector) {
57
93
  try {
@@ -25,6 +25,8 @@ export interface ResolveOptions {
25
25
  target?: SemanticTarget;
26
26
  /** Fallback selectors */
27
27
  selectorAlternates?: string[];
28
+ /** Active variant locale used to resolve the target's `*ByLocale` maps */
29
+ locale?: string;
28
30
  /** Timeout for visibility check (ms). Default: 3000 */
29
31
  timeoutMs?: number;
30
32
  }
@@ -33,3 +35,9 @@ export interface ResolveOptions {
33
35
  * Returns the first visible locator found, or null if nothing matches.
34
36
  */
35
37
  export declare function resolveTarget(page: Page, options: ResolveOptions): Promise<ResolvedTarget | null>;
38
+ /**
39
+ * Projects a SemanticTarget onto a single locale: each `*ByLocale` map is
40
+ * collapsed into its base field. Lookup order: exact locale tag → primary
41
+ * language subtag → base field → first non-empty map value.
42
+ */
43
+ export declare function localizeSemanticTarget(target: SemanticTarget, locale?: string): SemanticTarget;
Binary file