@riddledc/riddle-proof 0.7.8 → 0.7.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.
package/README.md CHANGED
@@ -108,7 +108,11 @@ or as a stronger proof base before a change loop.
108
108
  { "name": "mobile", "width": 390, "height": 844 },
109
109
  { "name": "desktop", "width": 1440, "height": 1000 }
110
110
  ],
111
- "auth": "none"
111
+ "auth": "none",
112
+ "setup_actions": [
113
+ { "type": "click", "selector": "[data-testid='show-plans']" },
114
+ { "type": "wait_for_text", "selector": "body", "text": "Start building" }
115
+ ]
112
116
  },
113
117
  "checks": [
114
118
  { "type": "route_loaded", "expected_path": "/pricing" },
@@ -141,6 +145,15 @@ The package includes a generic starter profile at
141
145
  profile directory and replace the selector/text checks with app-specific
142
146
  invariants.
143
147
 
148
+ `target.setup_actions` is optional. Use it when the meaningful proof surface
149
+ appears only after a picker, tab, login stub, transport control, or other
150
+ bounded interaction. Supported setup actions are `click`, `wait`,
151
+ `wait_for_selector`, and `wait_for_text`; a failed setup action is recorded as
152
+ a failed `setup_actions_succeeded` check so the profile cannot pass without
153
+ reaching the intended state. Text-matched `click` actions prefer visible
154
+ matching elements, which keeps responsive layouts from selecting hidden desktop
155
+ or mobile-only links.
156
+
144
157
  The result uses `riddle-proof.profile-result.v1` and separates product failures
145
158
  from weak proof and environment blockers:
146
159
 
@@ -20,6 +20,12 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
20
20
  "no_mobile_horizontal_overflow",
21
21
  "no_fatal_console_errors"
22
22
  ];
23
+ var RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES = [
24
+ "click",
25
+ "wait",
26
+ "wait_for_selector",
27
+ "wait_for_text"
28
+ ];
23
29
  var DEFAULT_VIEWPORTS = [
24
30
  { name: "desktop", width: 1280, height: 800 }
25
31
  ];
@@ -72,6 +78,41 @@ function normalizeViewports(value) {
72
78
  function isSupportedCheckType(value) {
73
79
  return RIDDLE_PROOF_PROFILE_CHECK_TYPES.includes(value);
74
80
  }
81
+ function normalizeSetupActionType(value, index) {
82
+ const normalized = String(value || "").trim().replace(/-/g, "_");
83
+ if (RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES.includes(normalized)) {
84
+ return normalized;
85
+ }
86
+ throw new Error(`target.setup_actions[${index}].type ${value || "(missing)"} is not supported. Supported actions: ${RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES.join(", ")}`);
87
+ }
88
+ function normalizeSetupAction(input, index) {
89
+ if (!isRecord(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
90
+ const type = normalizeSetupActionType(stringValue(input.type), index);
91
+ const selector = stringValue(input.selector);
92
+ if ((type === "click" || type === "wait_for_selector" || type === "wait_for_text") && !selector) {
93
+ throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
94
+ }
95
+ if (type === "wait_for_text" && !stringValue(input.text) && !stringValue(input.pattern)) {
96
+ throw new Error(`target.setup_actions[${index}] wait_for_text requires text or pattern.`);
97
+ }
98
+ return {
99
+ type,
100
+ selector,
101
+ text: stringValue(input.text),
102
+ pattern: stringValue(input.pattern),
103
+ flags: stringValue(input.flags),
104
+ index: numberValue(input.index),
105
+ ms: numberValue(input.ms) ?? numberValue(input.wait_ms) ?? numberValue(input.waitMs),
106
+ timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
107
+ after_ms: numberValue(input.after_ms) ?? numberValue(input.afterMs),
108
+ continue_on_failure: input.continue_on_failure === true || input.continueOnFailure === true
109
+ };
110
+ }
111
+ function normalizeSetupActions(value) {
112
+ if (value === void 0) return void 0;
113
+ if (!Array.isArray(value)) throw new Error("target.setup_actions must be an array.");
114
+ return value.map(normalizeSetupAction);
115
+ }
75
116
  function normalizeCheck(input, index) {
76
117
  if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
77
118
  const type = stringValue(input.type);
@@ -139,7 +180,8 @@ function normalizeRiddleProofProfile(input, options = {}) {
139
180
  viewports: options.viewports?.length ? options.viewports : normalizeViewports(targetInput.viewports),
140
181
  auth: stringValue(targetInput.auth) || "none",
141
182
  wait_for_selector: stringValue(targetInput.wait_for_selector) || stringValue(targetInput.waitForSelector),
142
- wait_ms: numberValue(targetInput.wait_ms) ?? numberValue(targetInput.waitMs)
183
+ wait_ms: numberValue(targetInput.wait_ms) ?? numberValue(targetInput.waitMs),
184
+ setup_actions: normalizeSetupActions(targetInput.setup_actions ?? targetInput.setupActions)
143
185
  },
144
186
  checks,
145
187
  artifacts: Array.isArray(input.artifacts) ? input.artifacts.map((item) => String(item)).filter(Boolean) : ["screenshot", "console", "dom_summary", "proof_json"],
@@ -157,7 +199,13 @@ function resolveRiddleProofProfileTargetUrl(profile) {
157
199
  throw new Error("profile target URL could not be resolved.");
158
200
  }
159
201
  function routeForViewport(viewport) {
160
- return viewport?.route || {
202
+ if (viewport?.route) {
203
+ return {
204
+ ...viewport.route,
205
+ matched: viewport.route.matched || routePathMatches(viewport.route.observed, viewport.route.expected_path)
206
+ };
207
+ }
208
+ return {
161
209
  requested: "",
162
210
  observed: "",
163
211
  matched: false,
@@ -183,8 +231,17 @@ function matchText(sample, check) {
183
231
  }
184
232
  return sample.includes(check.text || "");
185
233
  }
234
+ function normalizeRoutePath(path) {
235
+ const value = path || "/";
236
+ if (value === "/") return "/";
237
+ return value.replace(/\/+$/, "") || "/";
238
+ }
239
+ function routePathMatches(observed, expected) {
240
+ return normalizeRoutePath(observed) === normalizeRoutePath(expected);
241
+ }
186
242
  function successfulRoute(route) {
187
- return route.matched && !route.error && (route.http_status === null || route.http_status === void 0 || route.http_status < 400);
243
+ const matched = route.matched || routePathMatches(route.observed, route.expected_path);
244
+ return matched && !route.error && (route.http_status === null || route.http_status === void 0 || route.http_status < 400);
188
245
  }
189
246
  function assessCheckFromEvidence(check, evidence) {
190
247
  const viewports = evidence.viewports || [];
@@ -202,7 +259,7 @@ function assessCheckFromEvidence(check, evidence) {
202
259
  const failed = viewports.filter((viewport) => !successfulRoute({
203
260
  ...viewport.route,
204
261
  expected_path: expectedPath,
205
- matched: viewport.route.observed === expectedPath || viewport.route.matched
262
+ matched: routePathMatches(viewport.route.observed, expectedPath) || viewport.route.matched
206
263
  }));
207
264
  return {
208
265
  type: check.type,
@@ -312,6 +369,48 @@ function assessCheckFromEvidence(check, evidence) {
312
369
  message: "Unsupported check type."
313
370
  };
314
371
  }
372
+ function assessSetupActionsFromEvidence(profile, evidence) {
373
+ if (!profile.target.setup_actions?.length) return void 0;
374
+ const actionCount = profile.target.setup_actions.length;
375
+ const failed = [];
376
+ const viewports = evidence.viewports || [];
377
+ for (const viewport of viewports) {
378
+ const results = viewport.setup_action_results || [];
379
+ for (const result of results) {
380
+ if (result.ok === false) {
381
+ failed.push({
382
+ viewport: viewport.name,
383
+ action: result.action ?? result.type ?? null,
384
+ selector: result.selector ?? null,
385
+ reason: result.reason ?? result.error ?? null
386
+ });
387
+ }
388
+ }
389
+ if (results.length < actionCount && results.every((result) => result.ok !== false)) {
390
+ failed.push({
391
+ viewport: viewport.name,
392
+ action: "setup_actions",
393
+ selector: null,
394
+ reason: `missing setup action results: ${results.length}/${actionCount}`
395
+ });
396
+ }
397
+ }
398
+ return {
399
+ type: "setup_actions_succeeded",
400
+ label: "setup actions succeeded",
401
+ status: failed.length ? "failed" : "passed",
402
+ evidence: {
403
+ action_count: actionCount,
404
+ viewports: viewports.map((viewport) => ({
405
+ name: viewport.name,
406
+ ok: (viewport.setup_action_results || []).length >= actionCount && (viewport.setup_action_results || []).every((result) => result.ok !== false),
407
+ result_count: (viewport.setup_action_results || []).length
408
+ })),
409
+ failed
410
+ },
411
+ message: failed.length ? `Setup actions failed in ${failed.length} viewport action(s).` : void 0
412
+ };
413
+ }
315
414
  function profileStatusFromEvidence(evidence, checks) {
316
415
  if (!evidence) return "proof_insufficient";
317
416
  const viewports = evidence.viewports || [];
@@ -323,7 +422,10 @@ function profileStatusFromEvidence(evidence, checks) {
323
422
  }
324
423
  function assessRiddleProofProfileEvidence(profile, evidence, options = {}) {
325
424
  const capturedAt = evidence?.captured_at || (/* @__PURE__ */ new Date()).toISOString();
326
- const checks = evidence ? profile.checks.map((check) => assessCheckFromEvidence(check, evidence)) : [];
425
+ const checks = evidence ? [
426
+ assessSetupActionsFromEvidence(profile, evidence),
427
+ ...profile.checks.map((check) => assessCheckFromEvidence(check, evidence))
428
+ ].filter((check) => Boolean(check)) : [];
327
429
  const status = profileStatusFromEvidence(evidence, checks);
328
430
  const firstViewport = evidence?.viewports?.[0];
329
431
  const screenshots = (evidence?.viewports || []).map((viewport) => viewport.screenshot_label).filter((label) => Boolean(label));
@@ -425,8 +527,16 @@ function createRiddleProofProfileInsufficientResult(input) {
425
527
  }
426
528
  function runtimeScriptAssessmentSource() {
427
529
  return String.raw`
530
+ function normalizeRoutePath(path) {
531
+ const value = path || "/";
532
+ if (value === "/") return "/";
533
+ return value.replace(/\/+$/, "") || "/";
534
+ }
535
+ function routePathMatches(observed, expected) {
536
+ return normalizeRoutePath(observed) === normalizeRoutePath(expected);
537
+ }
428
538
  function routeOk(route) {
429
- return Boolean(route && route.matched && !route.error && (route.http_status == null || route.http_status < 400));
539
+ return Boolean(route && (route.matched || routePathMatches(route.observed, route.expected_path)) && !route.error && (route.http_status == null || route.http_status < 400));
430
540
  }
431
541
  function textMatches(sample, check) {
432
542
  if (check.pattern) {
@@ -437,12 +547,53 @@ function textMatches(sample, check) {
437
547
  function assessProfile(profile, evidence) {
438
548
  const checks = [];
439
549
  const viewports = evidence.viewports || [];
550
+ if (profile.target && Array.isArray(profile.target.setup_actions) && profile.target.setup_actions.length) {
551
+ const actionCount = profile.target.setup_actions.length;
552
+ const failed = [];
553
+ for (const viewport of viewports) {
554
+ const results = viewport.setup_action_results || [];
555
+ for (const result of results) {
556
+ if (result && result.ok === false) {
557
+ failed.push({
558
+ viewport: viewport.name,
559
+ action: result.action || result.type || null,
560
+ selector: result.selector || null,
561
+ reason: result.reason || result.error || null,
562
+ });
563
+ }
564
+ }
565
+ if (results.length < actionCount && results.every((result) => !result || result.ok !== false)) {
566
+ failed.push({
567
+ viewport: viewport.name,
568
+ action: "setup_actions",
569
+ selector: null,
570
+ reason: "missing setup action results: " + results.length + "/" + actionCount,
571
+ });
572
+ }
573
+ }
574
+ checks.push({
575
+ type: "setup_actions_succeeded",
576
+ label: "setup actions succeeded",
577
+ status: failed.length ? "failed" : "passed",
578
+ evidence: {
579
+ action_count: actionCount,
580
+ viewports: viewports.map((viewport) => ({
581
+ name: viewport.name,
582
+ ok: (viewport.setup_action_results || []).length >= actionCount
583
+ && (viewport.setup_action_results || []).every((result) => !result || result.ok !== false),
584
+ result_count: (viewport.setup_action_results || []).length,
585
+ })),
586
+ failed,
587
+ },
588
+ message: failed.length ? "Setup actions failed in " + failed.length + " viewport action(s)." : undefined,
589
+ });
590
+ }
440
591
  for (const check of profile.checks || []) {
441
592
  if (check.type === "route_loaded") {
442
593
  const expectedPath = check.expected_path || new URL(evidence.target_url).pathname || "/";
443
594
  const failed = viewports.filter((viewport) => {
444
595
  const route = { ...(viewport.route || {}), expected_path: expectedPath };
445
- route.matched = route.observed === expectedPath || route.matched;
596
+ route.matched = routePathMatches(route.observed, expectedPath) || route.matched;
446
597
  return !routeOk(route);
447
598
  });
448
599
  checks.push({
@@ -599,6 +750,105 @@ function textMatches(sample, check) {
599
750
  }
600
751
  return String(sample || "").includes(check.text || "");
601
752
  }
753
+ function setupActionType(action) {
754
+ return String(action && action.type ? action.type : "").replace(/-/g, "_");
755
+ }
756
+ function setupNumber(value, fallback) {
757
+ const number = Number(value);
758
+ return Number.isFinite(number) && number >= 0 ? number : fallback;
759
+ }
760
+ function setupTextMatches(sample, action) {
761
+ if (action.pattern) {
762
+ try { return new RegExp(action.pattern, action.flags || "").test(sample || ""); } catch { return false; }
763
+ }
764
+ return String(sample || "").includes(action.text || "");
765
+ }
766
+ async function setupLocatorText(locator, index) {
767
+ return await locator.nth(index).textContent({ timeout: 1000 }).catch(() => "");
768
+ }
769
+ async function setupLocatorVisible(locator, index) {
770
+ return await locator.nth(index).isVisible({ timeout: 1000 }).catch(() => false);
771
+ }
772
+ async function executeSetupAction(action, ordinal) {
773
+ const type = setupActionType(action);
774
+ const base = { ok: false, action: type || "unknown", ordinal, selector: action.selector || null };
775
+ const timeout = setupNumber(action.timeout_ms, 5000);
776
+ try {
777
+ if (type === "wait") {
778
+ const ms = setupNumber(action.ms, 500);
779
+ await page.waitForTimeout(ms);
780
+ return { ...base, ok: true, ms };
781
+ }
782
+ if (type === "wait_for_selector") {
783
+ await page.waitForSelector(action.selector, { state: "visible", timeout });
784
+ return { ...base, ok: true, timeout_ms: timeout };
785
+ }
786
+ if (type === "click") {
787
+ const locator = page.locator(action.selector);
788
+ const count = await locator.count();
789
+ if (!count) return { ...base, reason: "selector_not_found", count };
790
+ let targetIndex = Number.isInteger(action.index) ? action.index : 0;
791
+ let matchedText = null;
792
+ let hiddenMatchIndex = -1;
793
+ let hiddenMatchedText = null;
794
+ if (action.text || action.pattern) {
795
+ targetIndex = -1;
796
+ for (let index = 0; index < count; index += 1) {
797
+ const text = await setupLocatorText(locator, index);
798
+ if (setupTextMatches(text, action)) {
799
+ const visible = await setupLocatorVisible(locator, index);
800
+ if (visible) {
801
+ targetIndex = index;
802
+ matchedText = text;
803
+ break;
804
+ }
805
+ if (hiddenMatchIndex < 0) {
806
+ hiddenMatchIndex = index;
807
+ hiddenMatchedText = text;
808
+ }
809
+ }
810
+ }
811
+ if (targetIndex < 0 && hiddenMatchIndex >= 0) return { ...base, reason: "matching_element_not_visible", count, target_index: hiddenMatchIndex, text: hiddenMatchedText };
812
+ if (targetIndex < 0) return { ...base, reason: "text_not_found", count };
813
+ }
814
+ if (targetIndex < 0 || targetIndex >= count) return { ...base, reason: "index_out_of_range", count, target_index: targetIndex };
815
+ await locator.nth(targetIndex).click({ timeout });
816
+ return { ...base, ok: true, count, target_index: targetIndex, text: matchedText };
817
+ }
818
+ if (type === "wait_for_text") {
819
+ const locator = page.locator(action.selector);
820
+ const startedAt = Date.now();
821
+ let lastText = "";
822
+ while (Date.now() - startedAt <= timeout) {
823
+ const count = await locator.count().catch(() => 0);
824
+ for (let index = 0; index < count; index += 1) {
825
+ const text = await setupLocatorText(locator, index);
826
+ lastText = text || lastText;
827
+ if (setupTextMatches(text, action)) {
828
+ return { ...base, ok: true, text, target_index: index, timeout_ms: timeout };
829
+ }
830
+ }
831
+ await page.waitForTimeout(100);
832
+ }
833
+ return { ...base, reason: "text_not_found", text: lastText, timeout_ms: timeout };
834
+ }
835
+ return { ...base, reason: "unsupported_action" };
836
+ } catch (error) {
837
+ return { ...base, error: String(error && error.message ? error.message : error).slice(0, 1000) };
838
+ }
839
+ }
840
+ async function executeSetupActions(actions) {
841
+ const results = [];
842
+ for (let index = 0; index < (actions || []).length; index += 1) {
843
+ const action = actions[index] || {};
844
+ const result = await executeSetupAction(action, index);
845
+ results.push(result);
846
+ const afterMs = setupNumber(action.after_ms, 0);
847
+ if (afterMs) await page.waitForTimeout(afterMs);
848
+ if (result.ok === false && action.continue_on_failure !== true) break;
849
+ }
850
+ return results;
851
+ }
602
852
  function expectedPathFor(check) {
603
853
  return check.expected_path || new URL(targetUrl).pathname || "/";
604
854
  }
@@ -633,6 +883,9 @@ async function captureViewport(viewport) {
633
883
  if (!navigationError && profile.target.wait_ms) {
634
884
  await page.waitForTimeout(profile.target.wait_ms);
635
885
  }
886
+ const setupActionResults = (!navigationError && !waitError)
887
+ ? await executeSetupActions(profile.target.setup_actions || [])
888
+ : [];
636
889
  const dom = await page.evaluate(() => {
637
890
  const body = document.body;
638
891
  const documentElement = document.documentElement;
@@ -682,7 +935,7 @@ async function captureViewport(viewport) {
682
935
  requested: targetUrl,
683
936
  observed: dom.pathname,
684
937
  expected_path: expectedPath,
685
- matched: dom.pathname === expectedPath,
938
+ matched: routePathMatches(dom.pathname, expectedPath),
686
939
  http_status: httpStatus,
687
940
  error: navigationError || waitError || undefined,
688
941
  },
@@ -694,6 +947,7 @@ async function captureViewport(viewport) {
694
947
  overflow_px: Math.max(0, (dom.scroll_width || 0) - (dom.client_width || viewport.width)),
695
948
  selectors,
696
949
  text_matches,
950
+ setup_action_results: setupActionResults,
697
951
  screenshot_label: screenshotLabel,
698
952
  navigation_error: navigationError,
699
953
  wait_error: waitError,
@@ -793,6 +1047,7 @@ export {
793
1047
  RIDDLE_PROOF_PROFILE_RESULT_VERSION,
794
1048
  RIDDLE_PROOF_PROFILE_STATUSES,
795
1049
  RIDDLE_PROOF_PROFILE_CHECK_TYPES,
1050
+ RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES,
796
1051
  slugifyRiddleProofProfileName,
797
1052
  normalizeRiddleProofProfile,
798
1053
  resolveRiddleProofProfileTargetUrl,