@riddledc/riddle-proof 0.7.172 → 0.7.174

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.
package/README.md CHANGED
@@ -397,7 +397,7 @@ when body matching overrides sequence order.
397
397
  `target.setup_actions` is optional. Use it when the meaningful proof surface
398
398
  appears only after a picker, tab, login stub, storage seed, form fill,
399
399
  transport control, or other bounded interaction. Supported setup actions are
400
- `click`, `drag`, `press`, `fill`, `set_input_value`, `set_range_value`,
400
+ `click`, `tap`, `drag`, `press`, `fill`, `set_input_value`, `set_range_value`,
401
401
  `deterministic_runtime`, `canvas_signature`, `assert_text_visible`, `assert_text_absent`,
402
402
  `assert_selector_count`, `assert_window_value`, `assert_window_number`,
403
403
  `local_storage`, `session_storage`, `clear_storage`, `clear_console`,
@@ -416,6 +416,12 @@ Use `click_count` / `clickCount` / `clicks` from 1 to 10 on a single `click`
416
416
  action for atomic double-click or double-submit contracts where modeling the
417
417
  interaction as repeated setup actions would incorrectly require the target to
418
418
  remain in the DOM after the first click.
419
+ Use `tap` for touch-first controls, especially canvas regions where a mobile
420
+ tap should produce trusted touch events and the browser's synthesized click.
421
+ It requires `selector`, defaults to a touch tap at the target center, and
422
+ accepts `x` / `y` or `from_x` / `from_y` plus `coordinate_mode: "ratio"` for
423
+ element-relative coordinates. Set `pointer_type` to `mouse`, `touch`, or `pen`
424
+ when the proof must distinguish input modality.
419
425
  Use `set_range_value` for HTML range inputs and React-controlled sliders. It
420
426
  accepts aliases such as `set-slider-value`, requires `selector` plus `value`,
421
427
  uses the native input value setter, dispatches bubbling `input` and `change`
@@ -44,6 +44,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
44
44
  ];
45
45
  var RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES = [
46
46
  "click",
47
+ "tap",
47
48
  "drag",
48
49
  "press",
49
50
  "fill",
@@ -595,6 +596,21 @@ function profileSetupDragReceipts(results) {
595
596
  reason: result.reason ?? result.error ?? null
596
597
  }));
597
598
  }
599
+ function profileSetupTapReceipts(results) {
600
+ return results.filter((result) => profileSetupResultAction(result) === "tap").map((result) => ({
601
+ ordinal: result.ordinal ?? null,
602
+ ok: result.ok !== false,
603
+ selector: result.selector ?? null,
604
+ frame_selector: result.frame_selector ?? null,
605
+ pointer_type: result.pointer_type ?? null,
606
+ input_dispatch: result.input_dispatch ?? null,
607
+ coordinate_mode: result.coordinate_mode ?? null,
608
+ x: result.x ?? null,
609
+ y: result.y ?? null,
610
+ duration_ms: result.duration_ms ?? null,
611
+ reason: result.reason ?? result.error ?? null
612
+ }));
613
+ }
598
614
  function profileSetupCanvasSignatureReceipts(results) {
599
615
  return results.filter((result) => profileSetupResultAction(result) === "canvas_signature").map((result) => ({
600
616
  ordinal: result.ordinal ?? null,
@@ -632,21 +648,28 @@ function profileSetupCanvasSignatureStableHashGroups(results) {
632
648
  }
633
649
  const warnings = [];
634
650
  for (const group of groups.values()) {
635
- const hashes = new Set(group.receipts.map((receipt) => receipt.hash));
636
- const labels = [...new Set(group.receipts.map((receipt) => receipt.label))];
637
- if (group.receipts.length < 2 || labels.length < 2 || hashes.size !== 1) continue;
638
- const visibleLabels = labels.slice(0, 8);
639
- warnings.push({
640
- selector: group.selector,
641
- frame_selector: group.frame_selector ?? null,
642
- hash: group.receipts[0].hash,
643
- count: group.receipts.length,
644
- label_count: labels.length,
645
- labels: visibleLabels,
646
- omitted_label_count: Math.max(0, labels.length - visibleLabels.length),
647
- ordinals: group.receipts.map((receipt) => receipt.ordinal).filter((value) => value !== void 0).slice(0, 12),
648
- reason: "stable_canvas_signature_hash"
649
- });
651
+ const receiptsByHash = /* @__PURE__ */ new Map();
652
+ for (const receipt of group.receipts) {
653
+ const hashReceipts = receiptsByHash.get(receipt.hash) || [];
654
+ hashReceipts.push(receipt);
655
+ receiptsByHash.set(receipt.hash, hashReceipts);
656
+ }
657
+ for (const [hash, receipts] of receiptsByHash.entries()) {
658
+ const labels = [...new Set(receipts.map((receipt) => receipt.label))];
659
+ if (receipts.length < 2 || labels.length < 2) continue;
660
+ const visibleLabels = labels.slice(0, 8);
661
+ warnings.push({
662
+ selector: group.selector,
663
+ frame_selector: group.frame_selector ?? null,
664
+ hash,
665
+ count: receipts.length,
666
+ label_count: labels.length,
667
+ labels: visibleLabels,
668
+ omitted_label_count: Math.max(0, labels.length - visibleLabels.length),
669
+ ordinals: receipts.map((receipt) => receipt.ordinal).filter((value) => value !== void 0).slice(0, 12),
670
+ reason: "stable_canvas_signature_hash"
671
+ });
672
+ }
650
673
  }
651
674
  return warnings;
652
675
  }
@@ -749,6 +772,8 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
749
772
  const sampledRangeValueReceipts = sampleProfileSetupSummaryItems(rangeValueReceipts, 8);
750
773
  const dragReceipts = profileSetupDragReceipts(results);
751
774
  const sampledDragReceipts = sampleProfileSetupSummaryItems(dragReceipts, 8);
775
+ const tapReceipts = profileSetupTapReceipts(results);
776
+ const sampledTapReceipts = sampleProfileSetupSummaryItems(tapReceipts, 8);
752
777
  const canvasSignatureReceipts = profileSetupCanvasSignatureReceipts(results);
753
778
  const sampledCanvasSignatureReceipts = sampleProfileSetupSummaryItems(canvasSignatureReceipts, 8);
754
779
  const clickedItems = results.filter((result) => profileSetupResultAction(result) === "click" && result.ok !== false).map((result) => {
@@ -813,6 +838,9 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
813
838
  drag_total: dragReceipts.length,
814
839
  drag_truncated: dragReceipts.length > sampledDragReceipts.length,
815
840
  drag: sampledDragReceipts,
841
+ tap_total: tapReceipts.length,
842
+ tap_truncated: tapReceipts.length > sampledTapReceipts.length,
843
+ tap: sampledTapReceipts,
816
844
  canvas_signature_total: canvasSignatureReceipts.length,
817
845
  canvas_signature_truncated: canvasSignatureReceipts.length > sampledCanvasSignatureReceipts.length,
818
846
  canvas_signature: sampledCanvasSignatureReceipts,
@@ -869,7 +897,7 @@ function isSupportedCheckType(value) {
869
897
  }
870
898
  function normalizeSetupActionType(value, index) {
871
899
  const normalizedInput = String(value || "").trim().replace(/-/g, "_");
872
- const normalized = normalizedInput === "clear_browser_storage" ? "clear_storage" : normalizedInput === "reset_console" || normalizedInput === "clear_browser_console" || normalizedInput === "reset_browser_console" ? "clear_console" : normalizedInput === "pointer_drag" || normalizedInput === "mouse_drag" || normalizedInput === "drag_to" ? "drag" : normalizedInput === "keyboard_press" || normalizedInput === "key_press" ? "press" : normalizedInput === "set_slider_value" || normalizedInput === "slider_value" || normalizedInput === "set_slider" || normalizedInput === "set_range" || normalizedInput === "range_value" || normalizedInput === "range_input" || normalizedInput === "set_range_input" ? "set_range_value" : normalizedInput === "deterministic_runtime" || normalizedInput === "mock_runtime" || normalizedInput === "mock_random" || normalizedInput === "mock_random_queue" || normalizedInput === "seed_random_queue" || normalizedInput === "set_random_queue" || normalizedInput === "mock_clock" || normalizedInput === "set_mock_clock" || normalizedInput === "set_runtime_determinism" || normalizedInput === "runtime_determinism" ? "deterministic_runtime" : normalizedInput === "canvas_hash" || normalizedInput === "capture_canvas_hash" || normalizedInput === "capture_canvas_signature" || normalizedInput === "canvas_state_signature" ? "canvas_signature" : normalizedInput === "capture_screenshot" || normalizedInput === "save_screenshot" || normalizedInput === "setup_screenshot" ? "screenshot" : normalizedInput === "accept_dialog" || normalizedInput === "accept_dialogs" || normalizedInput === "confirm_dialog" || normalizedInput === "set_dialog_response" ? "dialog_response" : normalizedInput === "dismiss_dialog" || normalizedInput === "dismiss_dialogs" || normalizedInput === "cancel_dialog" ? "dialog_response" : normalizedInput === "window_call_until" || normalizedInput === "call_until" || normalizedInput === "window_call_repeat_until" || normalizedInput === "repeat_window_call_until" ? "window_call_until" : normalizedInput === "window_evaluate" || normalizedInput === "browser_eval" || normalizedInput === "browser_evaluate" || normalizedInput === "evaluate_script" || normalizedInput === "profile_script" ? "window_eval" : normalizedInput;
900
+ const normalized = normalizedInput === "clear_browser_storage" ? "clear_storage" : normalizedInput === "reset_console" || normalizedInput === "clear_browser_console" || normalizedInput === "reset_browser_console" ? "clear_console" : normalizedInput === "pointer_drag" || normalizedInput === "mouse_drag" || normalizedInput === "drag_to" ? "drag" : normalizedInput === "pointer_tap" || normalizedInput === "touch_tap" || normalizedInput === "canvas_tap" ? "tap" : normalizedInput === "keyboard_press" || normalizedInput === "key_press" ? "press" : normalizedInput === "set_slider_value" || normalizedInput === "slider_value" || normalizedInput === "set_slider" || normalizedInput === "set_range" || normalizedInput === "range_value" || normalizedInput === "range_input" || normalizedInput === "set_range_input" ? "set_range_value" : normalizedInput === "deterministic_runtime" || normalizedInput === "mock_runtime" || normalizedInput === "mock_random" || normalizedInput === "mock_random_queue" || normalizedInput === "seed_random_queue" || normalizedInput === "set_random_queue" || normalizedInput === "mock_clock" || normalizedInput === "set_mock_clock" || normalizedInput === "set_runtime_determinism" || normalizedInput === "runtime_determinism" ? "deterministic_runtime" : normalizedInput === "canvas_hash" || normalizedInput === "capture_canvas_hash" || normalizedInput === "capture_canvas_signature" || normalizedInput === "canvas_state_signature" ? "canvas_signature" : normalizedInput === "capture_screenshot" || normalizedInput === "save_screenshot" || normalizedInput === "setup_screenshot" ? "screenshot" : normalizedInput === "accept_dialog" || normalizedInput === "accept_dialogs" || normalizedInput === "confirm_dialog" || normalizedInput === "set_dialog_response" ? "dialog_response" : normalizedInput === "dismiss_dialog" || normalizedInput === "dismiss_dialogs" || normalizedInput === "cancel_dialog" ? "dialog_response" : normalizedInput === "window_call_until" || normalizedInput === "call_until" || normalizedInput === "window_call_repeat_until" || normalizedInput === "repeat_window_call_until" ? "window_call_until" : normalizedInput === "window_evaluate" || normalizedInput === "browser_eval" || normalizedInput === "browser_evaluate" || normalizedInput === "evaluate_script" || normalizedInput === "profile_script" ? "window_eval" : normalizedInput;
873
901
  if (RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES.includes(normalized)) {
874
902
  return normalized;
875
903
  }
@@ -951,8 +979,8 @@ function normalizeSetupActionCoordinateMode(value, index) {
951
979
  }
952
980
  function normalizeSetupActionPointerType(value, type, index) {
953
981
  if (value === void 0 || value === null || value === "") return void 0;
954
- if (type !== "drag") {
955
- throw new Error(`target.setup_actions[${index}].pointer_type is only supported for drag actions.`);
982
+ if (type !== "drag" && type !== "tap") {
983
+ throw new Error(`target.setup_actions[${index}].pointer_type is only supported for drag/tap actions.`);
956
984
  }
957
985
  const normalized = String(value).trim().replace(/-/g, "_").toLowerCase();
958
986
  if (normalized === "mouse") return "mouse";
@@ -1031,11 +1059,11 @@ function normalizeSetupAction(input, index) {
1031
1059
  if (frameIndex !== void 0 && (!Number.isInteger(frameIndex) || frameIndex < 0)) {
1032
1060
  throw new Error(`target.setup_actions[${index}].frame_index must be a non-negative integer.`);
1033
1061
  }
1034
- if ((type === "click" || type === "drag" || type === "fill" || type === "set_input_value" || type === "set_range_value" || type === "canvas_signature" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
1062
+ if ((type === "click" || type === "tap" || type === "drag" || type === "fill" || type === "set_input_value" || type === "set_range_value" || type === "canvas_signature" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
1035
1063
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
1036
1064
  }
1037
- const fromX = type === "click" ? numberValue(valueFromOwn(input, "from_x", "fromX", "x", "click_x", "clickX", "start_x", "startX", "x1")) : numberValue(valueFromOwn(input, "from_x", "fromX", "start_x", "startX", "x1"));
1038
- const fromY = type === "click" ? numberValue(valueFromOwn(input, "from_y", "fromY", "y", "click_y", "clickY", "start_y", "startY", "y1")) : numberValue(valueFromOwn(input, "from_y", "fromY", "start_y", "startY", "y1"));
1065
+ const fromX = type === "click" || type === "tap" ? numberValue(valueFromOwn(input, "from_x", "fromX", "x", "click_x", "clickX", "start_x", "startX", "x1")) : numberValue(valueFromOwn(input, "from_x", "fromX", "start_x", "startX", "x1"));
1066
+ const fromY = type === "click" || type === "tap" ? numberValue(valueFromOwn(input, "from_y", "fromY", "y", "click_y", "clickY", "start_y", "startY", "y1")) : numberValue(valueFromOwn(input, "from_y", "fromY", "start_y", "startY", "y1"));
1039
1067
  const toX = numberValue(valueFromOwn(input, "to_x", "toX", "end_x", "endX", "x2"));
1040
1068
  const toY = numberValue(valueFromOwn(input, "to_y", "toY", "end_y", "endY", "y2"));
1041
1069
  const coordinateMode = normalizeSetupActionCoordinateMode(valueFromOwn(input, "coordinate_mode", "coordinateMode", "coords", "units"), index);
@@ -1055,6 +1083,21 @@ function normalizeSetupAction(input, index) {
1055
1083
  }
1056
1084
  }
1057
1085
  }
1086
+ if (type === "tap") {
1087
+ const hasTapCoordinate = fromX !== void 0 || fromY !== void 0;
1088
+ if (hasTapCoordinate && (fromX === void 0 || fromY === void 0)) {
1089
+ throw new Error(`target.setup_actions[${index}] tap coordinates require both x and y.`);
1090
+ }
1091
+ if (hasTapCoordinate && fromX !== void 0 && fromY !== void 0) {
1092
+ const tapCoordinates = [fromX, fromY];
1093
+ if (coordinateMode === "ratio" && tapCoordinates.some((value2) => value2 < 0 || value2 > 1)) {
1094
+ throw new Error(`target.setup_actions[${index}] tap ratio coordinates must be between 0 and 1.`);
1095
+ }
1096
+ if ((coordinateMode === void 0 || coordinateMode === "pixels") && tapCoordinates.some((value2) => value2 < 0)) {
1097
+ throw new Error(`target.setup_actions[${index}] tap pixel coordinates must be non-negative.`);
1098
+ }
1099
+ }
1100
+ }
1058
1101
  if (type === "drag") {
1059
1102
  if (fromX === void 0 || fromY === void 0 || toX === void 0 || toY === void 0) {
1060
1103
  throw new Error(`target.setup_actions[${index}] drag requires from_x, from_y, to_x, and to_y.`);
@@ -4409,24 +4452,31 @@ function profileSetupCanvasSignatureStableHashGroups(results) {
4409
4452
  }
4410
4453
  const warnings = [];
4411
4454
  for (const group of groups.values()) {
4412
- const hashes = new Set(group.receipts.map((receipt) => receipt.hash));
4413
- const labels = [...new Set(group.receipts.map((receipt) => receipt.label))];
4414
- if (group.receipts.length < 2 || labels.length < 2 || hashes.size !== 1) continue;
4415
- const visibleLabels = labels.slice(0, 8);
4416
- warnings.push({
4417
- selector: group.selector,
4418
- frame_selector: group.frame_selector || null,
4419
- hash: group.receipts[0].hash,
4420
- count: group.receipts.length,
4421
- label_count: labels.length,
4422
- labels: visibleLabels,
4423
- omitted_label_count: Math.max(0, labels.length - visibleLabels.length),
4424
- ordinals: group.receipts
4425
- .map((receipt) => receipt.ordinal)
4426
- .filter((value) => value !== undefined)
4427
- .slice(0, 12),
4428
- reason: "stable_canvas_signature_hash",
4429
- });
4455
+ const receiptsByHash = new Map();
4456
+ for (const receipt of group.receipts) {
4457
+ const hashReceipts = receiptsByHash.get(receipt.hash) || [];
4458
+ hashReceipts.push(receipt);
4459
+ receiptsByHash.set(receipt.hash, hashReceipts);
4460
+ }
4461
+ for (const [hash, receipts] of receiptsByHash.entries()) {
4462
+ const labels = [...new Set(receipts.map((receipt) => receipt.label))];
4463
+ if (receipts.length < 2 || labels.length < 2) continue;
4464
+ const visibleLabels = labels.slice(0, 8);
4465
+ warnings.push({
4466
+ selector: group.selector,
4467
+ frame_selector: group.frame_selector || null,
4468
+ hash,
4469
+ count: receipts.length,
4470
+ label_count: labels.length,
4471
+ labels: visibleLabels,
4472
+ omitted_label_count: Math.max(0, labels.length - visibleLabels.length),
4473
+ ordinals: receipts
4474
+ .map((receipt) => receipt.ordinal)
4475
+ .filter((value) => value !== undefined)
4476
+ .slice(0, 12),
4477
+ reason: "stable_canvas_signature_hash",
4478
+ });
4479
+ }
4430
4480
  }
4431
4481
  return warnings;
4432
4482
  }
@@ -6043,6 +6093,96 @@ async function executeSetupAction(action, ordinal, viewport) {
6043
6093
  await waitForAnyVisibleSelector(scope.context, action.selector, timeout);
6044
6094
  return { ...base, ...setupScopeEvidence(scope), ok: true, timeout_ms: timeout };
6045
6095
  }
6096
+ if (type === "tap") {
6097
+ const scope = await setupActionScope(action, timeout);
6098
+ if (!scope.ok) return setupScopeFailure(base, scope);
6099
+ const locator = scope.context.locator(action.selector);
6100
+ const count = await locator.count();
6101
+ if (!count) return { ...base, ...setupScopeEvidence(scope), reason: "selector_not_found", count };
6102
+ const targetIndex = Number.isInteger(action.index) ? action.index : 0;
6103
+ if (targetIndex < 0 || targetIndex >= count) return { ...base, ...setupScopeEvidence(scope), reason: "index_out_of_range", count, target_index: targetIndex };
6104
+ const target = locator.nth(targetIndex);
6105
+ await target.waitFor({ state: "visible", timeout });
6106
+ const box = await target.boundingBox();
6107
+ if (!box) return { ...base, ...setupScopeEvidence(scope), reason: "bounding_box_unavailable", count, target_index: targetIndex };
6108
+ const fromX = setupFiniteNumber(action.from_x ?? action.fromX ?? action.x ?? action.click_x ?? action.clickX);
6109
+ const fromY = setupFiniteNumber(action.from_y ?? action.fromY ?? action.y ?? action.click_y ?? action.clickY);
6110
+ const hasTapPosition = fromX !== undefined || fromY !== undefined;
6111
+ if (hasTapPosition && (fromX === undefined || fromY === undefined)) return { ...base, ...setupScopeEvidence(scope), reason: "missing_tap_coordinates", count, target_index: targetIndex };
6112
+ const mode = String(action.coordinate_mode || action.coordinateMode || (hasTapPosition ? "pixels" : "ratio")).trim();
6113
+ if (hasTapPosition && mode === "ratio" && [fromX, fromY].some((value) => value < 0 || value > 1)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_ratio_coordinates", count, target_index: targetIndex };
6114
+ if (hasTapPosition && mode !== "ratio" && [fromX, fromY].some((value) => value < 0)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_pixel_coordinates", count, target_index: targetIndex };
6115
+ const coordinate = (value, size) => mode === "ratio" ? value * size : value;
6116
+ const localX = hasTapPosition && fromX !== undefined ? fromX : 0.5;
6117
+ const localY = hasTapPosition && fromY !== undefined ? fromY : 0.5;
6118
+ const point = {
6119
+ x: box.x + coordinate(localX, box.width),
6120
+ y: box.y + coordinate(localY, box.height),
6121
+ };
6122
+ const durationMs = setupNumber(action.duration_ms ?? action.durationMs, 0);
6123
+ const pointerType = String(action.pointer_type || action.pointerType || "touch").trim().toLowerCase();
6124
+ if (pointerType === "touch" || pointerType === "pen") {
6125
+ const client = await page.context().newCDPSession(page);
6126
+ try {
6127
+ if (pointerType === "touch") {
6128
+ const touchPoint = {
6129
+ x: point.x,
6130
+ y: point.y,
6131
+ radiusX: 1,
6132
+ radiusY: 1,
6133
+ force: 1,
6134
+ id: 11,
6135
+ };
6136
+ await client.send("Input.dispatchTouchEvent", {
6137
+ type: "touchStart",
6138
+ touchPoints: [touchPoint],
6139
+ });
6140
+ if (durationMs) await page.waitForTimeout(durationMs);
6141
+ await client.send("Input.dispatchTouchEvent", {
6142
+ type: "touchEnd",
6143
+ touchPoints: [],
6144
+ });
6145
+ } else {
6146
+ await client.send("Input.dispatchMouseEvent", {
6147
+ type: "mousePressed",
6148
+ x: point.x,
6149
+ y: point.y,
6150
+ button: "left",
6151
+ buttons: 1,
6152
+ clickCount: 1,
6153
+ pointerType: "pen",
6154
+ });
6155
+ if (durationMs) await page.waitForTimeout(durationMs);
6156
+ await client.send("Input.dispatchMouseEvent", {
6157
+ type: "mouseReleased",
6158
+ x: point.x,
6159
+ y: point.y,
6160
+ button: "left",
6161
+ buttons: 0,
6162
+ clickCount: 1,
6163
+ pointerType: "pen",
6164
+ });
6165
+ }
6166
+ } finally {
6167
+ await client.detach().catch(() => {});
6168
+ }
6169
+ } else {
6170
+ await page.mouse.click(point.x, point.y);
6171
+ }
6172
+ return {
6173
+ ...base,
6174
+ ...setupScopeEvidence(scope),
6175
+ ok: true,
6176
+ count,
6177
+ target_index: targetIndex,
6178
+ coordinate_mode: hasTapPosition ? mode : undefined,
6179
+ x: hasTapPosition ? fromX : undefined,
6180
+ y: hasTapPosition ? fromY : undefined,
6181
+ pointer_type: pointerType,
6182
+ input_dispatch: pointerType === "touch" || pointerType === "pen" ? "cdp" : "playwright_mouse",
6183
+ duration_ms: durationMs || undefined,
6184
+ };
6185
+ }
6046
6186
  if (type === "drag") {
6047
6187
  const scope = await setupActionScope(action, timeout);
6048
6188
  if (!scope.ok) return setupScopeFailure(base, scope);