@riddledc/riddle-proof 0.7.60 → 0.7.62

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
@@ -240,7 +240,7 @@ where the second request must carry newer state.
240
240
  `target.setup_actions` is optional. Use it when the meaningful proof surface
241
241
  appears only after a picker, tab, login stub, storage seed, form fill,
242
242
  transport control, or other bounded interaction. Supported setup actions are
243
- `click`, `press`, `fill`, `set_input_value`, `assert_text_visible`,
243
+ `click`, `drag`, `press`, `fill`, `set_input_value`, `assert_text_visible`,
244
244
  `assert_text_absent`, `assert_selector_count`, `assert_window_value`,
245
245
  `assert_window_number`, `local_storage`, `session_storage`, `clear_storage`, `wait`,
246
246
  `wait_for_selector`, `wait_for_text`, and `window_call`; a failed setup action
@@ -253,6 +253,12 @@ stable enough for Playwright's default click actionability checks. Use `press`
253
253
  with a Playwright key name, such as `Enter`, `Space`, or `ArrowLeft`,
254
254
  when a route's intended browser control is keyboard-driven; omit `selector` for
255
255
  a page-level key press, or provide `selector` to press against a focused element.
256
+ Use `drag` for pointer-driven controls such as canvas launch areas, sliders, or
257
+ drag-to-aim games. Provide `selector`, `from_x`, `from_y`, `to_x`, and `to_y`;
258
+ coordinates are element-relative pixels by default. Set `coordinate_mode:
259
+ "ratio"` to make coordinates relative to the target element size, for example
260
+ `from_x: 0.5, from_y: 0.5, to_x: 0.2, to_y: 0.5`. Optional `steps` and
261
+ `duration_ms` control how gradually the pointer moves before release.
256
262
  Use setup assertions when the pre-click or pre-navigation state is part of the contract,
257
263
  for example a fresh row must be present, stale copy must be absent, exactly one
258
264
  source link must exist before clicking into the final route, or a canvas app's
@@ -278,6 +284,12 @@ and setup assertions then execute in that frame context and record
278
284
  Use `frame_index` / `frameIndex` when more than one matching iframe is present;
279
285
  it defaults to the first frame.
280
286
 
287
+ Profiles with setup actions also include a compact
288
+ `setup_actions_succeeded.evidence.setup_summary`. The summary groups each
289
+ viewport's final route, final URL, action counts, clicked targets, iframe URLs,
290
+ compact text samples, and failures so setup-heavy clickthrough or iframe proofs
291
+ can be reviewed without reading every raw setup-action result.
292
+
281
293
  `target.timeout_sec` is optional. Use it for known-heavy profile targets so the
282
294
  profile carries its own hosted Riddle worker budget; an explicit CLI `--timeout`
283
295
  still overrides the profile value for one-off runs.
@@ -34,6 +34,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
34
34
  ];
35
35
  var RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES = [
36
36
  "click",
37
+ "drag",
37
38
  "press",
38
39
  "fill",
39
40
  "set_input_value",
@@ -174,6 +175,78 @@ function toJsonValue(value) {
174
175
  if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
175
176
  return String(value);
176
177
  }
178
+ function compactProfileSetupSummaryText(value, limit = 160) {
179
+ const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
180
+ if (!text) return void 0;
181
+ if (text.length <= limit) return text;
182
+ return `${text.slice(0, Math.max(0, limit - 15)).trimEnd()}... (${text.length} chars)`;
183
+ }
184
+ function profileSetupResultAction(value) {
185
+ const action = value.action ?? value.type;
186
+ return typeof action === "string" && action ? action : "unknown";
187
+ }
188
+ function profileSetupFrameUrls(viewport) {
189
+ const urls = [];
190
+ const frames = viewport.frames || {};
191
+ for (const container of Object.values(frames)) {
192
+ if (!isRecord(container) || !Array.isArray(container.frames)) continue;
193
+ for (const frame of container.frames) {
194
+ if (!isRecord(frame)) continue;
195
+ const url = typeof frame.url === "string" ? frame.url : void 0;
196
+ if (url && !urls.includes(url)) urls.push(url);
197
+ }
198
+ }
199
+ return urls.slice(0, 10);
200
+ }
201
+ function profileSetupActionCounts(results) {
202
+ const counts = {};
203
+ for (const result of results) {
204
+ const action = profileSetupResultAction(result);
205
+ counts[action] = (counts[action] || 0) + 1;
206
+ }
207
+ return toJsonValue(counts);
208
+ }
209
+ function profileSetupSummary(viewports, actionCount) {
210
+ return toJsonValue({
211
+ viewport_count: viewports.length,
212
+ action_count: actionCount ?? null,
213
+ viewports: viewports.map((viewport) => {
214
+ const results = viewport.setup_action_results || [];
215
+ const failed = results.filter((result) => result.ok === false);
216
+ const clicked = results.filter((result) => profileSetupResultAction(result) === "click" && result.ok !== false).map((result) => ({
217
+ ordinal: result.ordinal ?? null,
218
+ selector: result.selector ?? null,
219
+ frame_selector: result.frame_selector ?? null,
220
+ text: compactProfileSetupSummaryText(result.text)
221
+ })).slice(0, 8);
222
+ const text_samples = results.filter((result) => result.ok !== false && typeof result.text === "string" && (profileSetupResultAction(result) === "assert_text_visible" || profileSetupResultAction(result) === "assert_text_absent" || profileSetupResultAction(result) === "wait_for_text")).map((result) => ({
223
+ ordinal: result.ordinal ?? null,
224
+ action: profileSetupResultAction(result),
225
+ frame_selector: result.frame_selector ?? null,
226
+ text: compactProfileSetupSummaryText(result.text)
227
+ })).filter((item) => item.text).slice(-6);
228
+ return {
229
+ name: viewport.name,
230
+ ok: (actionCount === void 0 ? results.length > 0 : results.length >= actionCount) && failed.length === 0,
231
+ result_count: results.length,
232
+ observed_path: viewport.route?.observed ?? null,
233
+ final_url: viewport.url ?? null,
234
+ action_counts: profileSetupActionCounts(results),
235
+ frame_action_count: results.filter((result) => result.frame_selector).length,
236
+ frame_urls: profileSetupFrameUrls(viewport),
237
+ clicked,
238
+ text_samples,
239
+ failed: failed.map((result) => ({
240
+ ordinal: result.ordinal ?? null,
241
+ action: profileSetupResultAction(result),
242
+ selector: result.selector ?? null,
243
+ frame_selector: result.frame_selector ?? null,
244
+ reason: result.reason ?? result.error ?? null
245
+ }))
246
+ };
247
+ })
248
+ });
249
+ }
177
250
  function normalizeName(value, fallback) {
178
251
  const name = stringValue(value) || fallback;
179
252
  return name.replace(/\s+/g, " ").trim();
@@ -204,7 +277,7 @@ function isSupportedCheckType(value) {
204
277
  }
205
278
  function normalizeSetupActionType(value, index) {
206
279
  const normalizedInput = String(value || "").trim().replace(/-/g, "_");
207
- const normalized = normalizedInput === "clear_browser_storage" ? "clear_storage" : normalizedInput === "keyboard_press" || normalizedInput === "key_press" ? "press" : normalizedInput;
280
+ const normalized = normalizedInput === "clear_browser_storage" ? "clear_storage" : normalizedInput === "pointer_drag" || normalizedInput === "mouse_drag" || normalizedInput === "drag_to" ? "drag" : normalizedInput === "keyboard_press" || normalizedInput === "key_press" ? "press" : normalizedInput;
208
281
  if (RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES.includes(normalized)) {
209
282
  return normalized;
210
283
  }
@@ -234,6 +307,13 @@ function normalizeSetupActionRepeat(input, index) {
234
307
  }
235
308
  return repeat;
236
309
  }
310
+ function normalizeSetupActionCoordinateMode(value, index) {
311
+ if (value === void 0 || value === null || value === "") return void 0;
312
+ const normalized = String(value).trim().replace(/-/g, "_").toLowerCase();
313
+ if (normalized === "pixels" || normalized === "pixel" || normalized === "px") return "pixels";
314
+ if (normalized === "ratio" || normalized === "relative" || normalized === "fraction") return "ratio";
315
+ throw new Error(`target.setup_actions[${index}].coordinate_mode ${String(value)} is not supported. Supported coordinate modes: pixels, ratio.`);
316
+ }
237
317
  function normalizeSetupAction(input, index) {
238
318
  if (!isRecord(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
239
319
  const type = normalizeSetupActionType(stringValue(input.type), index);
@@ -243,9 +323,25 @@ function normalizeSetupAction(input, index) {
243
323
  if (frameIndex !== void 0 && (!Number.isInteger(frameIndex) || frameIndex < 0)) {
244
324
  throw new Error(`target.setup_actions[${index}].frame_index must be a non-negative integer.`);
245
325
  }
246
- if ((type === "click" || type === "fill" || type === "set_input_value" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
326
+ if ((type === "click" || type === "drag" || type === "fill" || type === "set_input_value" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
247
327
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
248
328
  }
329
+ const fromX = numberValue(valueFromOwn(input, "from_x", "fromX", "start_x", "startX", "x1"));
330
+ const fromY = numberValue(valueFromOwn(input, "from_y", "fromY", "start_y", "startY", "y1"));
331
+ const toX = numberValue(valueFromOwn(input, "to_x", "toX", "end_x", "endX", "x2"));
332
+ const toY = numberValue(valueFromOwn(input, "to_y", "toY", "end_y", "endY", "y2"));
333
+ const coordinateMode = normalizeSetupActionCoordinateMode(valueFromOwn(input, "coordinate_mode", "coordinateMode", "coords", "units"), index);
334
+ if (type === "drag") {
335
+ if (fromX === void 0 || fromY === void 0 || toX === void 0 || toY === void 0) {
336
+ throw new Error(`target.setup_actions[${index}] drag requires from_x, from_y, to_x, and to_y.`);
337
+ }
338
+ if (coordinateMode === "ratio" && [fromX, fromY, toX, toY].some((value2) => value2 < 0 || value2 > 1)) {
339
+ throw new Error(`target.setup_actions[${index}] drag ratio coordinates must be between 0 and 1.`);
340
+ }
341
+ if ((coordinateMode === void 0 || coordinateMode === "pixels") && [fromX, fromY, toX, toY].some((value2) => value2 < 0)) {
342
+ throw new Error(`target.setup_actions[${index}] drag pixel coordinates must be non-negative.`);
343
+ }
344
+ }
249
345
  if (type === "wait_for_text" && !stringValue(input.text) && !stringValue(input.pattern)) {
250
346
  throw new Error(`target.setup_actions[${index}] wait_for_text requires text or pattern.`);
251
347
  }
@@ -292,12 +388,23 @@ function normalizeSetupAction(input, index) {
292
388
  }
293
389
  }
294
390
  const hasExpectedReturn = hasOwn(input, "expect_return") || hasOwn(input, "expectReturn") || hasOwn(input, "expected_return") || hasOwn(input, "expectedReturn");
391
+ const steps = numberValue(input.steps);
392
+ if (type === "drag" && steps !== void 0 && (!Number.isInteger(steps) || steps < 1 || steps > 100)) {
393
+ throw new Error(`target.setup_actions[${index}].steps must be an integer from 1 to 100.`);
394
+ }
295
395
  return {
296
396
  type,
297
397
  selector,
298
398
  frame_selector: frameSelector,
299
399
  frame_index: frameIndex,
300
400
  force: type === "click" && (input.force === true || input.force_click === true || input.forceClick === true),
401
+ coordinate_mode: coordinateMode,
402
+ from_x: fromX,
403
+ from_y: fromY,
404
+ to_x: toX,
405
+ to_y: toY,
406
+ duration_ms: numberValue(input.duration_ms) ?? numberValue(input.durationMs),
407
+ steps,
301
408
  key,
302
409
  value,
303
410
  value_json: hasJsonValue ? toJsonValue(input.value_json ?? input.valueJson ?? input.json) : void 0,
@@ -1246,6 +1353,7 @@ function assessSetupActionsFromEvidence(profile, evidence) {
1246
1353
  ok: (viewport.setup_action_results || []).length >= actionCount && (viewport.setup_action_results || []).every((result) => result.ok !== false),
1247
1354
  result_count: (viewport.setup_action_results || []).length
1248
1355
  })),
1356
+ setup_summary: profileSetupSummary(viewports, actionCount),
1249
1357
  failed
1250
1358
  },
1251
1359
  message: failed.length ? `Setup actions failed in ${failed.length} viewport action(s).` : void 0
@@ -1659,6 +1767,90 @@ function requiredNetworkMockHitCount(mock) {
1659
1767
  if (Array.isArray(mock.responses) && mock.responses.length) return mock.responses.length;
1660
1768
  return 1;
1661
1769
  }
1770
+ function compactProfileSetupSummaryText(value, limit) {
1771
+ limit = limit || 160;
1772
+ const text = typeof value === "string" ? value.replace(/\\s+/g, " ").trim() : "";
1773
+ if (!text) return undefined;
1774
+ if (text.length <= limit) return text;
1775
+ return text.slice(0, Math.max(0, limit - 15)).trimEnd() + "... (" + text.length + " chars)";
1776
+ }
1777
+ function profileSetupResultAction(result) {
1778
+ const action = result && (result.action || result.type);
1779
+ return typeof action === "string" && action ? action : "unknown";
1780
+ }
1781
+ function profileSetupFrameUrls(viewport) {
1782
+ const urls = [];
1783
+ const frames = viewport && viewport.frames || {};
1784
+ for (const container of Object.values(frames)) {
1785
+ if (!container || typeof container !== "object" || Array.isArray(container) || !Array.isArray(container.frames)) continue;
1786
+ for (const frame of container.frames) {
1787
+ if (!frame || typeof frame !== "object" || Array.isArray(frame)) continue;
1788
+ const url = typeof frame.url === "string" ? frame.url : null;
1789
+ if (url && !urls.includes(url)) urls.push(url);
1790
+ }
1791
+ }
1792
+ return urls.slice(0, 10);
1793
+ }
1794
+ function profileSetupActionCounts(results) {
1795
+ const counts = {};
1796
+ for (const result of results || []) {
1797
+ const action = profileSetupResultAction(result);
1798
+ counts[action] = (counts[action] || 0) + 1;
1799
+ }
1800
+ return counts;
1801
+ }
1802
+ function profileSetupSummary(viewports, actionCount) {
1803
+ return {
1804
+ viewport_count: (viewports || []).length,
1805
+ action_count: actionCount ?? null,
1806
+ viewports: (viewports || []).map((viewport) => {
1807
+ const results = viewport.setup_action_results || [];
1808
+ const failed = results.filter((result) => result && result.ok === false);
1809
+ const clicked = results
1810
+ .filter((result) => result && profileSetupResultAction(result) === "click" && result.ok !== false)
1811
+ .map((result) => ({
1812
+ ordinal: result.ordinal ?? null,
1813
+ selector: result.selector ?? null,
1814
+ frame_selector: result.frame_selector ?? null,
1815
+ text: compactProfileSetupSummaryText(result.text),
1816
+ }))
1817
+ .slice(0, 8);
1818
+ const textSamples = results
1819
+ .filter((result) => result && result.ok !== false && typeof result.text === "string" && (
1820
+ profileSetupResultAction(result) === "assert_text_visible"
1821
+ || profileSetupResultAction(result) === "assert_text_absent"
1822
+ || profileSetupResultAction(result) === "wait_for_text"
1823
+ ))
1824
+ .map((result) => ({
1825
+ ordinal: result.ordinal ?? null,
1826
+ action: profileSetupResultAction(result),
1827
+ frame_selector: result.frame_selector ?? null,
1828
+ text: compactProfileSetupSummaryText(result.text),
1829
+ }))
1830
+ .filter((item) => item.text)
1831
+ .slice(-6);
1832
+ return {
1833
+ name: viewport.name,
1834
+ ok: (actionCount === undefined ? results.length > 0 : results.length >= actionCount) && failed.length === 0,
1835
+ result_count: results.length,
1836
+ observed_path: viewport.route && viewport.route.observed || null,
1837
+ final_url: viewport.url || null,
1838
+ action_counts: profileSetupActionCounts(results),
1839
+ frame_action_count: results.filter((result) => result && result.frame_selector).length,
1840
+ frame_urls: profileSetupFrameUrls(viewport),
1841
+ clicked,
1842
+ text_samples: textSamples,
1843
+ failed: failed.map((result) => ({
1844
+ ordinal: result.ordinal ?? null,
1845
+ action: profileSetupResultAction(result),
1846
+ selector: result.selector ?? null,
1847
+ frame_selector: result.frame_selector ?? null,
1848
+ reason: result.reason || result.error || null,
1849
+ })),
1850
+ };
1851
+ }),
1852
+ };
1853
+ }
1662
1854
  function assessProfile(profile, evidence) {
1663
1855
  const checks = [];
1664
1856
  const viewports = evidence.viewports || [];
@@ -1763,6 +1955,7 @@ function assessProfile(profile, evidence) {
1763
1955
  && (viewport.setup_action_results || []).every((result) => !result || result.ok !== false),
1764
1956
  result_count: (viewport.setup_action_results || []).length,
1765
1957
  })),
1958
+ setup_summary: profileSetupSummary(viewports, actionCount),
1766
1959
  failed,
1767
1960
  },
1768
1961
  message: failed.length ? "Setup actions failed in " + failed.length + " viewport action(s)." : undefined,
@@ -2505,6 +2698,71 @@ async function executeSetupAction(action, ordinal) {
2505
2698
  await scope.context.waitForSelector(action.selector, { state: "visible", timeout });
2506
2699
  return { ...base, ...setupScopeEvidence(scope), ok: true, timeout_ms: timeout };
2507
2700
  }
2701
+ if (type === "drag") {
2702
+ const scope = await setupActionScope(action, timeout);
2703
+ if (!scope.ok) return setupScopeFailure(base, scope);
2704
+ const locator = scope.context.locator(action.selector);
2705
+ const count = await locator.count();
2706
+ if (!count) return { ...base, ...setupScopeEvidence(scope), reason: "selector_not_found", count };
2707
+ const targetIndex = Number.isInteger(action.index) ? action.index : 0;
2708
+ if (targetIndex < 0 || targetIndex >= count) return { ...base, ...setupScopeEvidence(scope), reason: "index_out_of_range", count, target_index: targetIndex };
2709
+ const target = locator.nth(targetIndex);
2710
+ await target.waitFor({ state: "visible", timeout });
2711
+ const box = await target.boundingBox();
2712
+ if (!box) return { ...base, ...setupScopeEvidence(scope), reason: "bounding_box_unavailable", count, target_index: targetIndex };
2713
+ const mode = String(action.coordinate_mode || action.coordinateMode || "pixels").trim();
2714
+ const coordinate = (value, size) => mode === "ratio" ? value * size : value;
2715
+ const fromX = setupFiniteNumber(action.from_x ?? action.fromX ?? action.start_x ?? action.startX ?? action.x1);
2716
+ const fromY = setupFiniteNumber(action.from_y ?? action.fromY ?? action.start_y ?? action.startY ?? action.y1);
2717
+ const toX = setupFiniteNumber(action.to_x ?? action.toX ?? action.end_x ?? action.endX ?? action.x2);
2718
+ const toY = setupFiniteNumber(action.to_y ?? action.toY ?? action.end_y ?? action.endY ?? action.y2);
2719
+ if (fromX === undefined || fromY === undefined || toX === undefined || toY === undefined) return { ...base, ...setupScopeEvidence(scope), reason: "missing_drag_coordinates", count, target_index: targetIndex };
2720
+ if (mode === "ratio" && [fromX, fromY, toX, toY].some((value) => value < 0 || value > 1)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_ratio_coordinates", count, target_index: targetIndex };
2721
+ if (mode !== "ratio" && [fromX, fromY, toX, toY].some((value) => value < 0)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_pixel_coordinates", count, target_index: targetIndex };
2722
+ const start = {
2723
+ x: box.x + coordinate(fromX, box.width),
2724
+ y: box.y + coordinate(fromY, box.height),
2725
+ };
2726
+ const end = {
2727
+ x: box.x + coordinate(toX, box.width),
2728
+ y: box.y + coordinate(toY, box.height),
2729
+ };
2730
+ const requestedSteps = setupNumber(action.steps, 8);
2731
+ const steps = Math.min(100, Math.max(1, Math.floor(requestedSteps || 8)));
2732
+ const durationMs = setupNumber(action.duration_ms ?? action.durationMs, 0);
2733
+ await page.mouse.move(start.x, start.y);
2734
+ await page.mouse.down();
2735
+ try {
2736
+ if (durationMs && steps > 1) {
2737
+ for (let step = 1; step <= steps; step += 1) {
2738
+ const progress = step / steps;
2739
+ await page.mouse.move(
2740
+ start.x + (end.x - start.x) * progress,
2741
+ start.y + (end.y - start.y) * progress,
2742
+ );
2743
+ await page.waitForTimeout(durationMs / steps);
2744
+ }
2745
+ } else {
2746
+ await page.mouse.move(end.x, end.y, { steps });
2747
+ }
2748
+ } finally {
2749
+ await page.mouse.up().catch(() => {});
2750
+ }
2751
+ return {
2752
+ ...base,
2753
+ ...setupScopeEvidence(scope),
2754
+ ok: true,
2755
+ count,
2756
+ target_index: targetIndex,
2757
+ coordinate_mode: mode,
2758
+ from_x: fromX,
2759
+ from_y: fromY,
2760
+ to_x: toX,
2761
+ to_y: toY,
2762
+ steps,
2763
+ duration_ms: durationMs || undefined,
2764
+ };
2765
+ }
2508
2766
  if (type === "press") {
2509
2767
  const key = String(action.key || "").trim();
2510
2768
  if (!key) return { ...base, reason: "missing_key" };