@riddledc/riddle-proof 0.7.53 → 0.7.55

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
@@ -303,6 +303,22 @@ These checks are useful for audit/no-diff profiles where the product should
303
303
  show a fallback state and avoid rendering stale loaders, duplicate rows, or
304
304
  missing-resource iframes.
305
305
 
306
+ Use `url_search_param_equals` and `url_search_param_absent` when the final URL
307
+ is part of the contract, such as deep-link recovery that must drop a stale
308
+ local identifier while preserving shareable query state:
309
+
310
+ ```json
311
+ [
312
+ { "type": "url_search_param_absent", "param": "seq" },
313
+ { "type": "url_search_param_equals", "param": "song", "expected_value": "monkberry-moon-delight-tab" },
314
+ { "type": "url_search_param_equals", "param": "view", "expected_value": "trainer" }
315
+ ]
316
+ ```
317
+
318
+ The check uses the captured browser URL for each viewport after setup actions
319
+ and waits have completed. `search_param` and `key` are accepted as aliases for
320
+ `param`; `value` is accepted as an alias for `expected_value`.
321
+
306
322
  Use `selector_text_order` when a table, list, or card group must show visible
307
323
  items in a specific order after setup actions such as sorting or filtering:
308
324
 
@@ -12,6 +12,8 @@ var RIDDLE_PROOF_PROFILE_STATUSES = [
12
12
  ];
13
13
  var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
14
14
  "route_loaded",
15
+ "url_search_param_equals",
16
+ "url_search_param_absent",
15
17
  "selector_visible",
16
18
  "selector_absent",
17
19
  "selector_count_at_least",
@@ -488,6 +490,13 @@ function normalizeCheck(input, index) {
488
490
  if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
489
491
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
490
492
  }
493
+ if ((type === "url_search_param_equals" || type === "url_search_param_absent") && !stringValue(input.param) && !stringValue(input.search_param) && !stringValue(input.searchParam) && !stringValue(input.key)) {
494
+ throw new Error(`checks[${index}] ${type} requires param.`);
495
+ }
496
+ const expectedValue = stringFromOwn(input, "expected_value", "expectedValue", "value");
497
+ if (type === "url_search_param_equals" && expectedValue === void 0) {
498
+ throw new Error(`checks[${index}] url_search_param_equals requires expected_value.`);
499
+ }
491
500
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
492
501
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
493
502
  }
@@ -508,6 +517,8 @@ function normalizeCheck(input, index) {
508
517
  type,
509
518
  label: stringValue(input.label),
510
519
  expected_path: stringValue(input.expected_path),
520
+ param: stringValue(input.param) || stringValue(input.search_param) || stringValue(input.searchParam) || stringValue(input.key),
521
+ expected_value: expectedValue,
511
522
  expected_routes: expectedRoutes,
512
523
  selector: stringValue(input.selector),
513
524
  expected_texts: expectedTexts,
@@ -760,6 +771,32 @@ function successfulRoute(route, targetUrl) {
760
771
  const matched = route.matched || routePathMatches(route.observed, route.expected_path, targetUrl);
761
772
  return matched && !route.error && (route.http_status === null || route.http_status === void 0 || route.http_status < 400);
762
773
  }
774
+ function parseEvidenceUrl(value, targetUrl) {
775
+ if (!value) return void 0;
776
+ try {
777
+ return targetUrl ? new URL(value, targetUrl) : new URL(value);
778
+ } catch {
779
+ try {
780
+ return new URL(value);
781
+ } catch {
782
+ return void 0;
783
+ }
784
+ }
785
+ }
786
+ function observedUrlForViewport(viewport, targetUrl) {
787
+ return parseEvidenceUrl(viewport.url, targetUrl) || parseEvidenceUrl(viewport.route?.observed, targetUrl) || parseEvidenceUrl(viewport.route?.requested, targetUrl) || parseEvidenceUrl(targetUrl, void 0);
788
+ }
789
+ function searchParamObservation(viewport, targetUrl, param) {
790
+ const url = observedUrlForViewport(viewport, targetUrl);
791
+ const values = url ? url.searchParams.getAll(param) : [];
792
+ return {
793
+ viewport: viewport.name,
794
+ url: url?.href || null,
795
+ value: values.length ? values[0] : null,
796
+ values,
797
+ present: values.length > 0
798
+ };
799
+ }
763
800
  function viewportsForCheck(check, viewports) {
764
801
  if (!check.viewports?.length) return viewports;
765
802
  const names = new Set(check.viewports);
@@ -797,6 +834,44 @@ function assessCheckFromEvidence(check, evidence) {
797
834
  message: failed.length ? `Route did not load as ${expectedPath} in ${failed.length} viewport(s).` : void 0
798
835
  };
799
836
  }
837
+ if (check.type === "url_search_param_equals") {
838
+ const param = check.param || "";
839
+ const expectedValue = check.expected_value ?? "";
840
+ const observations = viewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
841
+ const failed = observations.filter((observation) => observation.value !== expectedValue);
842
+ return {
843
+ type: check.type,
844
+ label: checkLabel(check),
845
+ status: failed.length ? "failed" : "passed",
846
+ evidence: {
847
+ param,
848
+ expected_value: expectedValue,
849
+ observed_values: observations.map((observation) => observation.value),
850
+ observed_all_values: observations.map((observation) => observation.values),
851
+ observed_urls: observations.map((observation) => observation.url),
852
+ viewports: observations.map((observation) => toJsonValue(observation))
853
+ },
854
+ message: failed.length ? `URL search param ${param} did not equal ${JSON.stringify(expectedValue)} in ${failed.length} viewport(s).` : void 0
855
+ };
856
+ }
857
+ if (check.type === "url_search_param_absent") {
858
+ const param = check.param || "";
859
+ const observations = viewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
860
+ const failed = observations.filter((observation) => observation.present);
861
+ return {
862
+ type: check.type,
863
+ label: checkLabel(check),
864
+ status: failed.length ? "failed" : "passed",
865
+ evidence: {
866
+ param,
867
+ observed_values: observations.map((observation) => observation.value),
868
+ observed_all_values: observations.map((observation) => observation.values),
869
+ observed_urls: observations.map((observation) => observation.url),
870
+ viewports: observations.map((observation) => toJsonValue(observation))
871
+ },
872
+ message: failed.length ? `URL search param ${param} was present in ${failed.length} viewport(s).` : void 0
873
+ };
874
+ }
800
875
  if (check.type === "selector_visible") {
801
876
  const key = selectorKey(check);
802
877
  const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.visible_count || 0) < 1);
@@ -1332,6 +1407,29 @@ function routePathMatches(observed, expected, targetUrl) {
1332
1407
  function routeOk(route, targetUrl) {
1333
1408
  return Boolean(route && (route.matched || routePathMatches(route.observed, route.expected_path, targetUrl)) && !route.error && (route.http_status == null || route.http_status < 400));
1334
1409
  }
1410
+ function parseEvidenceUrl(value, targetUrl) {
1411
+ if (!value) return null;
1412
+ try { return targetUrl ? new URL(value, targetUrl) : new URL(value); } catch {}
1413
+ try { return new URL(value); } catch {}
1414
+ return null;
1415
+ }
1416
+ function observedUrlForViewport(viewport, targetUrl) {
1417
+ return parseEvidenceUrl(viewport && viewport.url, targetUrl)
1418
+ || parseEvidenceUrl(viewport && viewport.route && viewport.route.observed, targetUrl)
1419
+ || parseEvidenceUrl(viewport && viewport.route && viewport.route.requested, targetUrl)
1420
+ || parseEvidenceUrl(targetUrl, null);
1421
+ }
1422
+ function searchParamObservation(viewport, targetUrl, param) {
1423
+ const url = observedUrlForViewport(viewport, targetUrl);
1424
+ const values = url ? url.searchParams.getAll(param) : [];
1425
+ return {
1426
+ viewport: viewport && viewport.name,
1427
+ url: url ? url.href : null,
1428
+ value: values.length ? values[0] : null,
1429
+ values,
1430
+ present: values.length > 0,
1431
+ };
1432
+ }
1335
1433
  function textMatches(sample, check) {
1336
1434
  if (check.pattern) {
1337
1435
  try { return new RegExp(check.pattern, check.flags || "").test(sample || ""); } catch { return false; }
@@ -1636,6 +1734,46 @@ function assessProfile(profile, evidence) {
1636
1734
  });
1637
1735
  continue;
1638
1736
  }
1737
+ if (check.type === "url_search_param_equals") {
1738
+ const param = check.param || "";
1739
+ const expectedValue = check.expected_value == null ? "" : String(check.expected_value);
1740
+ const observations = checkViewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
1741
+ const failed = observations.filter((observation) => observation.value !== expectedValue);
1742
+ checks.push({
1743
+ type: check.type,
1744
+ label: check.label || check.type,
1745
+ status: failed.length ? "failed" : "passed",
1746
+ evidence: {
1747
+ param,
1748
+ expected_value: expectedValue,
1749
+ observed_values: observations.map((observation) => observation.value),
1750
+ observed_all_values: observations.map((observation) => observation.values),
1751
+ observed_urls: observations.map((observation) => observation.url),
1752
+ viewports: observations,
1753
+ },
1754
+ message: failed.length ? "URL search param " + param + " did not equal " + JSON.stringify(expectedValue) + " in " + failed.length + " viewport(s)." : undefined,
1755
+ });
1756
+ continue;
1757
+ }
1758
+ if (check.type === "url_search_param_absent") {
1759
+ const param = check.param || "";
1760
+ const observations = checkViewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
1761
+ const failed = observations.filter((observation) => observation.present);
1762
+ checks.push({
1763
+ type: check.type,
1764
+ label: check.label || check.type,
1765
+ status: failed.length ? "failed" : "passed",
1766
+ evidence: {
1767
+ param,
1768
+ observed_values: observations.map((observation) => observation.value),
1769
+ observed_all_values: observations.map((observation) => observation.values),
1770
+ observed_urls: observations.map((observation) => observation.url),
1771
+ viewports: observations,
1772
+ },
1773
+ message: failed.length ? "URL search param " + param + " was present in " + failed.length + " viewport(s)." : undefined,
1774
+ });
1775
+ continue;
1776
+ }
1639
1777
  if (check.type === "selector_visible") {
1640
1778
  const selector = check.selector || "";
1641
1779
  const failed = checkViewports.filter((viewport) => !viewport.selectors || !viewport.selectors[selector] || viewport.selectors[selector].visible_count < 1);
@@ -2516,7 +2654,7 @@ async function frameEvidence(selector) {
2516
2654
  const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
2517
2655
  const viewportWidth = clientWidth || window.innerWidth;
2518
2656
  const overflowOffenders = [];
2519
- function isContainedByHorizontalScroller(element) {
2657
+ function isHandledByHorizontalOverflowAncestor(element, rect) {
2520
2658
  let current = element.parentElement;
2521
2659
  while (current && current !== body && current !== documentElement) {
2522
2660
  const style = window.getComputedStyle(current);
@@ -2526,6 +2664,11 @@ async function frameEvidence(selector) {
2526
2664
  const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
2527
2665
  if (contained) return true;
2528
2666
  }
2667
+ if (overflowX === "hidden" || overflowX === "clip" || style.overflow === "hidden" || style.overflow === "clip") {
2668
+ const currentRect = current.getBoundingClientRect();
2669
+ const clippedByAncestor = rect.left < currentRect.left - 0.5 || rect.right > currentRect.right + 0.5;
2670
+ if (clippedByAncestor) return true;
2671
+ }
2529
2672
  current = current.parentElement;
2530
2673
  }
2531
2674
  return false;
@@ -2539,7 +2682,7 @@ async function frameEvidence(selector) {
2539
2682
  const rightOverflow = Math.max(0, rect.right - viewportWidth);
2540
2683
  const overflow = Math.max(leftOverflow, rightOverflow);
2541
2684
  if (overflow <= 0.5) continue;
2542
- if (isContainedByHorizontalScroller(element)) continue;
2685
+ if (isHandledByHorizontalOverflowAncestor(element, rect)) continue;
2543
2686
  const tag = element.tagName ? element.tagName.toLowerCase() : "element";
2544
2687
  const id = element.id ? "#" + element.id : "";
2545
2688
  const className = typeof element.className === "string"
@@ -2919,7 +3062,7 @@ async function captureViewport(viewport) {
2919
3062
  const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
2920
3063
  const viewportWidth = clientWidth || window.innerWidth;
2921
3064
  const overflowOffenders = [];
2922
- function isContainedByHorizontalScroller(element) {
3065
+ function isHandledByHorizontalOverflowAncestor(element, rect) {
2923
3066
  let current = element.parentElement;
2924
3067
  while (current && current !== body && current !== documentElement) {
2925
3068
  const style = window.getComputedStyle(current);
@@ -2929,6 +3072,11 @@ async function captureViewport(viewport) {
2929
3072
  const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
2930
3073
  if (contained) return true;
2931
3074
  }
3075
+ if (overflowX === "hidden" || overflowX === "clip" || style.overflow === "hidden" || style.overflow === "clip") {
3076
+ const currentRect = current.getBoundingClientRect();
3077
+ const clippedByAncestor = rect.left < currentRect.left - 0.5 || rect.right > currentRect.right + 0.5;
3078
+ if (clippedByAncestor) return true;
3079
+ }
2932
3080
  current = current.parentElement;
2933
3081
  }
2934
3082
  return false;
@@ -2942,7 +3090,7 @@ async function captureViewport(viewport) {
2942
3090
  const rightOverflow = Math.max(0, rect.right - viewportWidth);
2943
3091
  const overflow = Math.max(leftOverflow, rightOverflow);
2944
3092
  if (overflow <= 0.5) continue;
2945
- if (isContainedByHorizontalScroller(element)) continue;
3093
+ if (isHandledByHorizontalOverflowAncestor(element, rect)) continue;
2946
3094
  const tag = element.tagName ? element.tagName.toLowerCase() : "element";
2947
3095
  const id = element.id ? "#" + element.id : "";
2948
3096
  const className = typeof element.className === "string"
package/dist/cli.cjs CHANGED
@@ -6885,6 +6885,8 @@ var RIDDLE_PROOF_PROFILE_STATUSES = [
6885
6885
  ];
6886
6886
  var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
6887
6887
  "route_loaded",
6888
+ "url_search_param_equals",
6889
+ "url_search_param_absent",
6888
6890
  "selector_visible",
6889
6891
  "selector_absent",
6890
6892
  "selector_count_at_least",
@@ -7361,6 +7363,13 @@ function normalizeCheck(input, index) {
7361
7363
  if ((type === "text_visible" || type === "text_absent") && !stringValue2(input.text) && !stringValue2(input.pattern)) {
7362
7364
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
7363
7365
  }
7366
+ if ((type === "url_search_param_equals" || type === "url_search_param_absent") && !stringValue2(input.param) && !stringValue2(input.search_param) && !stringValue2(input.searchParam) && !stringValue2(input.key)) {
7367
+ throw new Error(`checks[${index}] ${type} requires param.`);
7368
+ }
7369
+ const expectedValue = stringFromOwn(input, "expected_value", "expectedValue", "value");
7370
+ if (type === "url_search_param_equals" && expectedValue === void 0) {
7371
+ throw new Error(`checks[${index}] url_search_param_equals requires expected_value.`);
7372
+ }
7364
7373
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
7365
7374
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
7366
7375
  }
@@ -7381,6 +7390,8 @@ function normalizeCheck(input, index) {
7381
7390
  type,
7382
7391
  label: stringValue2(input.label),
7383
7392
  expected_path: stringValue2(input.expected_path),
7393
+ param: stringValue2(input.param) || stringValue2(input.search_param) || stringValue2(input.searchParam) || stringValue2(input.key),
7394
+ expected_value: expectedValue,
7384
7395
  expected_routes: expectedRoutes,
7385
7396
  selector: stringValue2(input.selector),
7386
7397
  expected_texts: expectedTexts,
@@ -7633,6 +7644,32 @@ function successfulRoute(route, targetUrl) {
7633
7644
  const matched = route.matched || routePathMatches(route.observed, route.expected_path, targetUrl);
7634
7645
  return matched && !route.error && (route.http_status === null || route.http_status === void 0 || route.http_status < 400);
7635
7646
  }
7647
+ function parseEvidenceUrl(value, targetUrl) {
7648
+ if (!value) return void 0;
7649
+ try {
7650
+ return targetUrl ? new URL(value, targetUrl) : new URL(value);
7651
+ } catch {
7652
+ try {
7653
+ return new URL(value);
7654
+ } catch {
7655
+ return void 0;
7656
+ }
7657
+ }
7658
+ }
7659
+ function observedUrlForViewport(viewport, targetUrl) {
7660
+ return parseEvidenceUrl(viewport.url, targetUrl) || parseEvidenceUrl(viewport.route?.observed, targetUrl) || parseEvidenceUrl(viewport.route?.requested, targetUrl) || parseEvidenceUrl(targetUrl, void 0);
7661
+ }
7662
+ function searchParamObservation(viewport, targetUrl, param) {
7663
+ const url = observedUrlForViewport(viewport, targetUrl);
7664
+ const values = url ? url.searchParams.getAll(param) : [];
7665
+ return {
7666
+ viewport: viewport.name,
7667
+ url: url?.href || null,
7668
+ value: values.length ? values[0] : null,
7669
+ values,
7670
+ present: values.length > 0
7671
+ };
7672
+ }
7636
7673
  function viewportsForCheck(check, viewports) {
7637
7674
  if (!check.viewports?.length) return viewports;
7638
7675
  const names = new Set(check.viewports);
@@ -7670,6 +7707,44 @@ function assessCheckFromEvidence(check, evidence) {
7670
7707
  message: failed.length ? `Route did not load as ${expectedPath} in ${failed.length} viewport(s).` : void 0
7671
7708
  };
7672
7709
  }
7710
+ if (check.type === "url_search_param_equals") {
7711
+ const param = check.param || "";
7712
+ const expectedValue = check.expected_value ?? "";
7713
+ const observations = viewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
7714
+ const failed = observations.filter((observation) => observation.value !== expectedValue);
7715
+ return {
7716
+ type: check.type,
7717
+ label: checkLabel(check),
7718
+ status: failed.length ? "failed" : "passed",
7719
+ evidence: {
7720
+ param,
7721
+ expected_value: expectedValue,
7722
+ observed_values: observations.map((observation) => observation.value),
7723
+ observed_all_values: observations.map((observation) => observation.values),
7724
+ observed_urls: observations.map((observation) => observation.url),
7725
+ viewports: observations.map((observation) => toJsonValue(observation))
7726
+ },
7727
+ message: failed.length ? `URL search param ${param} did not equal ${JSON.stringify(expectedValue)} in ${failed.length} viewport(s).` : void 0
7728
+ };
7729
+ }
7730
+ if (check.type === "url_search_param_absent") {
7731
+ const param = check.param || "";
7732
+ const observations = viewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
7733
+ const failed = observations.filter((observation) => observation.present);
7734
+ return {
7735
+ type: check.type,
7736
+ label: checkLabel(check),
7737
+ status: failed.length ? "failed" : "passed",
7738
+ evidence: {
7739
+ param,
7740
+ observed_values: observations.map((observation) => observation.value),
7741
+ observed_all_values: observations.map((observation) => observation.values),
7742
+ observed_urls: observations.map((observation) => observation.url),
7743
+ viewports: observations.map((observation) => toJsonValue(observation))
7744
+ },
7745
+ message: failed.length ? `URL search param ${param} was present in ${failed.length} viewport(s).` : void 0
7746
+ };
7747
+ }
7673
7748
  if (check.type === "selector_visible") {
7674
7749
  const key = selectorKey(check);
7675
7750
  const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.visible_count || 0) < 1);
@@ -8189,6 +8264,29 @@ function routePathMatches(observed, expected, targetUrl) {
8189
8264
  function routeOk(route, targetUrl) {
8190
8265
  return Boolean(route && (route.matched || routePathMatches(route.observed, route.expected_path, targetUrl)) && !route.error && (route.http_status == null || route.http_status < 400));
8191
8266
  }
8267
+ function parseEvidenceUrl(value, targetUrl) {
8268
+ if (!value) return null;
8269
+ try { return targetUrl ? new URL(value, targetUrl) : new URL(value); } catch {}
8270
+ try { return new URL(value); } catch {}
8271
+ return null;
8272
+ }
8273
+ function observedUrlForViewport(viewport, targetUrl) {
8274
+ return parseEvidenceUrl(viewport && viewport.url, targetUrl)
8275
+ || parseEvidenceUrl(viewport && viewport.route && viewport.route.observed, targetUrl)
8276
+ || parseEvidenceUrl(viewport && viewport.route && viewport.route.requested, targetUrl)
8277
+ || parseEvidenceUrl(targetUrl, null);
8278
+ }
8279
+ function searchParamObservation(viewport, targetUrl, param) {
8280
+ const url = observedUrlForViewport(viewport, targetUrl);
8281
+ const values = url ? url.searchParams.getAll(param) : [];
8282
+ return {
8283
+ viewport: viewport && viewport.name,
8284
+ url: url ? url.href : null,
8285
+ value: values.length ? values[0] : null,
8286
+ values,
8287
+ present: values.length > 0,
8288
+ };
8289
+ }
8192
8290
  function textMatches(sample, check) {
8193
8291
  if (check.pattern) {
8194
8292
  try { return new RegExp(check.pattern, check.flags || "").test(sample || ""); } catch { return false; }
@@ -8493,6 +8591,46 @@ function assessProfile(profile, evidence) {
8493
8591
  });
8494
8592
  continue;
8495
8593
  }
8594
+ if (check.type === "url_search_param_equals") {
8595
+ const param = check.param || "";
8596
+ const expectedValue = check.expected_value == null ? "" : String(check.expected_value);
8597
+ const observations = checkViewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
8598
+ const failed = observations.filter((observation) => observation.value !== expectedValue);
8599
+ checks.push({
8600
+ type: check.type,
8601
+ label: check.label || check.type,
8602
+ status: failed.length ? "failed" : "passed",
8603
+ evidence: {
8604
+ param,
8605
+ expected_value: expectedValue,
8606
+ observed_values: observations.map((observation) => observation.value),
8607
+ observed_all_values: observations.map((observation) => observation.values),
8608
+ observed_urls: observations.map((observation) => observation.url),
8609
+ viewports: observations,
8610
+ },
8611
+ message: failed.length ? "URL search param " + param + " did not equal " + JSON.stringify(expectedValue) + " in " + failed.length + " viewport(s)." : undefined,
8612
+ });
8613
+ continue;
8614
+ }
8615
+ if (check.type === "url_search_param_absent") {
8616
+ const param = check.param || "";
8617
+ const observations = checkViewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
8618
+ const failed = observations.filter((observation) => observation.present);
8619
+ checks.push({
8620
+ type: check.type,
8621
+ label: check.label || check.type,
8622
+ status: failed.length ? "failed" : "passed",
8623
+ evidence: {
8624
+ param,
8625
+ observed_values: observations.map((observation) => observation.value),
8626
+ observed_all_values: observations.map((observation) => observation.values),
8627
+ observed_urls: observations.map((observation) => observation.url),
8628
+ viewports: observations,
8629
+ },
8630
+ message: failed.length ? "URL search param " + param + " was present in " + failed.length + " viewport(s)." : undefined,
8631
+ });
8632
+ continue;
8633
+ }
8496
8634
  if (check.type === "selector_visible") {
8497
8635
  const selector = check.selector || "";
8498
8636
  const failed = checkViewports.filter((viewport) => !viewport.selectors || !viewport.selectors[selector] || viewport.selectors[selector].visible_count < 1);
@@ -9373,7 +9511,7 @@ async function frameEvidence(selector) {
9373
9511
  const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
9374
9512
  const viewportWidth = clientWidth || window.innerWidth;
9375
9513
  const overflowOffenders = [];
9376
- function isContainedByHorizontalScroller(element) {
9514
+ function isHandledByHorizontalOverflowAncestor(element, rect) {
9377
9515
  let current = element.parentElement;
9378
9516
  while (current && current !== body && current !== documentElement) {
9379
9517
  const style = window.getComputedStyle(current);
@@ -9383,6 +9521,11 @@ async function frameEvidence(selector) {
9383
9521
  const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
9384
9522
  if (contained) return true;
9385
9523
  }
9524
+ if (overflowX === "hidden" || overflowX === "clip" || style.overflow === "hidden" || style.overflow === "clip") {
9525
+ const currentRect = current.getBoundingClientRect();
9526
+ const clippedByAncestor = rect.left < currentRect.left - 0.5 || rect.right > currentRect.right + 0.5;
9527
+ if (clippedByAncestor) return true;
9528
+ }
9386
9529
  current = current.parentElement;
9387
9530
  }
9388
9531
  return false;
@@ -9396,7 +9539,7 @@ async function frameEvidence(selector) {
9396
9539
  const rightOverflow = Math.max(0, rect.right - viewportWidth);
9397
9540
  const overflow = Math.max(leftOverflow, rightOverflow);
9398
9541
  if (overflow <= 0.5) continue;
9399
- if (isContainedByHorizontalScroller(element)) continue;
9542
+ if (isHandledByHorizontalOverflowAncestor(element, rect)) continue;
9400
9543
  const tag = element.tagName ? element.tagName.toLowerCase() : "element";
9401
9544
  const id = element.id ? "#" + element.id : "";
9402
9545
  const className = typeof element.className === "string"
@@ -9776,7 +9919,7 @@ async function captureViewport(viewport) {
9776
9919
  const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
9777
9920
  const viewportWidth = clientWidth || window.innerWidth;
9778
9921
  const overflowOffenders = [];
9779
- function isContainedByHorizontalScroller(element) {
9922
+ function isHandledByHorizontalOverflowAncestor(element, rect) {
9780
9923
  let current = element.parentElement;
9781
9924
  while (current && current !== body && current !== documentElement) {
9782
9925
  const style = window.getComputedStyle(current);
@@ -9786,6 +9929,11 @@ async function captureViewport(viewport) {
9786
9929
  const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
9787
9930
  if (contained) return true;
9788
9931
  }
9932
+ if (overflowX === "hidden" || overflowX === "clip" || style.overflow === "hidden" || style.overflow === "clip") {
9933
+ const currentRect = current.getBoundingClientRect();
9934
+ const clippedByAncestor = rect.left < currentRect.left - 0.5 || rect.right > currentRect.right + 0.5;
9935
+ if (clippedByAncestor) return true;
9936
+ }
9789
9937
  current = current.parentElement;
9790
9938
  }
9791
9939
  return false;
@@ -9799,7 +9947,7 @@ async function captureViewport(viewport) {
9799
9947
  const rightOverflow = Math.max(0, rect.right - viewportWidth);
9800
9948
  const overflow = Math.max(leftOverflow, rightOverflow);
9801
9949
  if (overflow <= 0.5) continue;
9802
- if (isContainedByHorizontalScroller(element)) continue;
9950
+ if (isHandledByHorizontalOverflowAncestor(element, rect)) continue;
9803
9951
  const tag = element.tagName ? element.tagName.toLowerCase() : "element";
9804
9952
  const id = element.id ? "#" + element.id : "";
9805
9953
  const className = typeof element.className === "string"
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  profileStatusExitCode,
11
11
  resolveRiddleProofProfileTargetUrl,
12
12
  resolveRiddleProofProfileTimeoutSec
13
- } from "./chunk-IKT7AKZN.js";
13
+ } from "./chunk-2JFDTCIC.js";
14
14
  import {
15
15
  createRiddleApiClient,
16
16
  parseRiddleViewport
package/dist/index.cjs CHANGED
@@ -8726,6 +8726,8 @@ var RIDDLE_PROOF_PROFILE_STATUSES = [
8726
8726
  ];
8727
8727
  var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
8728
8728
  "route_loaded",
8729
+ "url_search_param_equals",
8730
+ "url_search_param_absent",
8729
8731
  "selector_visible",
8730
8732
  "selector_absent",
8731
8733
  "selector_count_at_least",
@@ -9202,6 +9204,13 @@ function normalizeCheck(input, index) {
9202
9204
  if ((type === "text_visible" || type === "text_absent") && !stringValue5(input.text) && !stringValue5(input.pattern)) {
9203
9205
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
9204
9206
  }
9207
+ if ((type === "url_search_param_equals" || type === "url_search_param_absent") && !stringValue5(input.param) && !stringValue5(input.search_param) && !stringValue5(input.searchParam) && !stringValue5(input.key)) {
9208
+ throw new Error(`checks[${index}] ${type} requires param.`);
9209
+ }
9210
+ const expectedValue = stringFromOwn(input, "expected_value", "expectedValue", "value");
9211
+ if (type === "url_search_param_equals" && expectedValue === void 0) {
9212
+ throw new Error(`checks[${index}] url_search_param_equals requires expected_value.`);
9213
+ }
9205
9214
  if (type === "selector_count_at_least" && numberValue3(input.min_count) === void 0) {
9206
9215
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
9207
9216
  }
@@ -9222,6 +9231,8 @@ function normalizeCheck(input, index) {
9222
9231
  type,
9223
9232
  label: stringValue5(input.label),
9224
9233
  expected_path: stringValue5(input.expected_path),
9234
+ param: stringValue5(input.param) || stringValue5(input.search_param) || stringValue5(input.searchParam) || stringValue5(input.key),
9235
+ expected_value: expectedValue,
9225
9236
  expected_routes: expectedRoutes,
9226
9237
  selector: stringValue5(input.selector),
9227
9238
  expected_texts: expectedTexts,
@@ -9474,6 +9485,32 @@ function successfulRoute(route, targetUrl) {
9474
9485
  const matched = route.matched || routePathMatches(route.observed, route.expected_path, targetUrl);
9475
9486
  return matched && !route.error && (route.http_status === null || route.http_status === void 0 || route.http_status < 400);
9476
9487
  }
9488
+ function parseEvidenceUrl(value, targetUrl) {
9489
+ if (!value) return void 0;
9490
+ try {
9491
+ return targetUrl ? new URL(value, targetUrl) : new URL(value);
9492
+ } catch {
9493
+ try {
9494
+ return new URL(value);
9495
+ } catch {
9496
+ return void 0;
9497
+ }
9498
+ }
9499
+ }
9500
+ function observedUrlForViewport(viewport, targetUrl) {
9501
+ return parseEvidenceUrl(viewport.url, targetUrl) || parseEvidenceUrl(viewport.route?.observed, targetUrl) || parseEvidenceUrl(viewport.route?.requested, targetUrl) || parseEvidenceUrl(targetUrl, void 0);
9502
+ }
9503
+ function searchParamObservation(viewport, targetUrl, param) {
9504
+ const url = observedUrlForViewport(viewport, targetUrl);
9505
+ const values = url ? url.searchParams.getAll(param) : [];
9506
+ return {
9507
+ viewport: viewport.name,
9508
+ url: url?.href || null,
9509
+ value: values.length ? values[0] : null,
9510
+ values,
9511
+ present: values.length > 0
9512
+ };
9513
+ }
9477
9514
  function viewportsForCheck(check, viewports) {
9478
9515
  if (!check.viewports?.length) return viewports;
9479
9516
  const names = new Set(check.viewports);
@@ -9511,6 +9548,44 @@ function assessCheckFromEvidence(check, evidence) {
9511
9548
  message: failed.length ? `Route did not load as ${expectedPath} in ${failed.length} viewport(s).` : void 0
9512
9549
  };
9513
9550
  }
9551
+ if (check.type === "url_search_param_equals") {
9552
+ const param = check.param || "";
9553
+ const expectedValue = check.expected_value ?? "";
9554
+ const observations = viewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
9555
+ const failed = observations.filter((observation) => observation.value !== expectedValue);
9556
+ return {
9557
+ type: check.type,
9558
+ label: checkLabel(check),
9559
+ status: failed.length ? "failed" : "passed",
9560
+ evidence: {
9561
+ param,
9562
+ expected_value: expectedValue,
9563
+ observed_values: observations.map((observation) => observation.value),
9564
+ observed_all_values: observations.map((observation) => observation.values),
9565
+ observed_urls: observations.map((observation) => observation.url),
9566
+ viewports: observations.map((observation) => toJsonValue(observation))
9567
+ },
9568
+ message: failed.length ? `URL search param ${param} did not equal ${JSON.stringify(expectedValue)} in ${failed.length} viewport(s).` : void 0
9569
+ };
9570
+ }
9571
+ if (check.type === "url_search_param_absent") {
9572
+ const param = check.param || "";
9573
+ const observations = viewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
9574
+ const failed = observations.filter((observation) => observation.present);
9575
+ return {
9576
+ type: check.type,
9577
+ label: checkLabel(check),
9578
+ status: failed.length ? "failed" : "passed",
9579
+ evidence: {
9580
+ param,
9581
+ observed_values: observations.map((observation) => observation.value),
9582
+ observed_all_values: observations.map((observation) => observation.values),
9583
+ observed_urls: observations.map((observation) => observation.url),
9584
+ viewports: observations.map((observation) => toJsonValue(observation))
9585
+ },
9586
+ message: failed.length ? `URL search param ${param} was present in ${failed.length} viewport(s).` : void 0
9587
+ };
9588
+ }
9514
9589
  if (check.type === "selector_visible") {
9515
9590
  const key = selectorKey(check);
9516
9591
  const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.visible_count || 0) < 1);
@@ -10046,6 +10121,29 @@ function routePathMatches(observed, expected, targetUrl) {
10046
10121
  function routeOk(route, targetUrl) {
10047
10122
  return Boolean(route && (route.matched || routePathMatches(route.observed, route.expected_path, targetUrl)) && !route.error && (route.http_status == null || route.http_status < 400));
10048
10123
  }
10124
+ function parseEvidenceUrl(value, targetUrl) {
10125
+ if (!value) return null;
10126
+ try { return targetUrl ? new URL(value, targetUrl) : new URL(value); } catch {}
10127
+ try { return new URL(value); } catch {}
10128
+ return null;
10129
+ }
10130
+ function observedUrlForViewport(viewport, targetUrl) {
10131
+ return parseEvidenceUrl(viewport && viewport.url, targetUrl)
10132
+ || parseEvidenceUrl(viewport && viewport.route && viewport.route.observed, targetUrl)
10133
+ || parseEvidenceUrl(viewport && viewport.route && viewport.route.requested, targetUrl)
10134
+ || parseEvidenceUrl(targetUrl, null);
10135
+ }
10136
+ function searchParamObservation(viewport, targetUrl, param) {
10137
+ const url = observedUrlForViewport(viewport, targetUrl);
10138
+ const values = url ? url.searchParams.getAll(param) : [];
10139
+ return {
10140
+ viewport: viewport && viewport.name,
10141
+ url: url ? url.href : null,
10142
+ value: values.length ? values[0] : null,
10143
+ values,
10144
+ present: values.length > 0,
10145
+ };
10146
+ }
10049
10147
  function textMatches(sample, check) {
10050
10148
  if (check.pattern) {
10051
10149
  try { return new RegExp(check.pattern, check.flags || "").test(sample || ""); } catch { return false; }
@@ -10350,6 +10448,46 @@ function assessProfile(profile, evidence) {
10350
10448
  });
10351
10449
  continue;
10352
10450
  }
10451
+ if (check.type === "url_search_param_equals") {
10452
+ const param = check.param || "";
10453
+ const expectedValue = check.expected_value == null ? "" : String(check.expected_value);
10454
+ const observations = checkViewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
10455
+ const failed = observations.filter((observation) => observation.value !== expectedValue);
10456
+ checks.push({
10457
+ type: check.type,
10458
+ label: check.label || check.type,
10459
+ status: failed.length ? "failed" : "passed",
10460
+ evidence: {
10461
+ param,
10462
+ expected_value: expectedValue,
10463
+ observed_values: observations.map((observation) => observation.value),
10464
+ observed_all_values: observations.map((observation) => observation.values),
10465
+ observed_urls: observations.map((observation) => observation.url),
10466
+ viewports: observations,
10467
+ },
10468
+ message: failed.length ? "URL search param " + param + " did not equal " + JSON.stringify(expectedValue) + " in " + failed.length + " viewport(s)." : undefined,
10469
+ });
10470
+ continue;
10471
+ }
10472
+ if (check.type === "url_search_param_absent") {
10473
+ const param = check.param || "";
10474
+ const observations = checkViewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
10475
+ const failed = observations.filter((observation) => observation.present);
10476
+ checks.push({
10477
+ type: check.type,
10478
+ label: check.label || check.type,
10479
+ status: failed.length ? "failed" : "passed",
10480
+ evidence: {
10481
+ param,
10482
+ observed_values: observations.map((observation) => observation.value),
10483
+ observed_all_values: observations.map((observation) => observation.values),
10484
+ observed_urls: observations.map((observation) => observation.url),
10485
+ viewports: observations,
10486
+ },
10487
+ message: failed.length ? "URL search param " + param + " was present in " + failed.length + " viewport(s)." : undefined,
10488
+ });
10489
+ continue;
10490
+ }
10353
10491
  if (check.type === "selector_visible") {
10354
10492
  const selector = check.selector || "";
10355
10493
  const failed = checkViewports.filter((viewport) => !viewport.selectors || !viewport.selectors[selector] || viewport.selectors[selector].visible_count < 1);
@@ -11230,7 +11368,7 @@ async function frameEvidence(selector) {
11230
11368
  const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
11231
11369
  const viewportWidth = clientWidth || window.innerWidth;
11232
11370
  const overflowOffenders = [];
11233
- function isContainedByHorizontalScroller(element) {
11371
+ function isHandledByHorizontalOverflowAncestor(element, rect) {
11234
11372
  let current = element.parentElement;
11235
11373
  while (current && current !== body && current !== documentElement) {
11236
11374
  const style = window.getComputedStyle(current);
@@ -11240,6 +11378,11 @@ async function frameEvidence(selector) {
11240
11378
  const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
11241
11379
  if (contained) return true;
11242
11380
  }
11381
+ if (overflowX === "hidden" || overflowX === "clip" || style.overflow === "hidden" || style.overflow === "clip") {
11382
+ const currentRect = current.getBoundingClientRect();
11383
+ const clippedByAncestor = rect.left < currentRect.left - 0.5 || rect.right > currentRect.right + 0.5;
11384
+ if (clippedByAncestor) return true;
11385
+ }
11243
11386
  current = current.parentElement;
11244
11387
  }
11245
11388
  return false;
@@ -11253,7 +11396,7 @@ async function frameEvidence(selector) {
11253
11396
  const rightOverflow = Math.max(0, rect.right - viewportWidth);
11254
11397
  const overflow = Math.max(leftOverflow, rightOverflow);
11255
11398
  if (overflow <= 0.5) continue;
11256
- if (isContainedByHorizontalScroller(element)) continue;
11399
+ if (isHandledByHorizontalOverflowAncestor(element, rect)) continue;
11257
11400
  const tag = element.tagName ? element.tagName.toLowerCase() : "element";
11258
11401
  const id = element.id ? "#" + element.id : "";
11259
11402
  const className = typeof element.className === "string"
@@ -11633,7 +11776,7 @@ async function captureViewport(viewport) {
11633
11776
  const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
11634
11777
  const viewportWidth = clientWidth || window.innerWidth;
11635
11778
  const overflowOffenders = [];
11636
- function isContainedByHorizontalScroller(element) {
11779
+ function isHandledByHorizontalOverflowAncestor(element, rect) {
11637
11780
  let current = element.parentElement;
11638
11781
  while (current && current !== body && current !== documentElement) {
11639
11782
  const style = window.getComputedStyle(current);
@@ -11643,6 +11786,11 @@ async function captureViewport(viewport) {
11643
11786
  const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
11644
11787
  if (contained) return true;
11645
11788
  }
11789
+ if (overflowX === "hidden" || overflowX === "clip" || style.overflow === "hidden" || style.overflow === "clip") {
11790
+ const currentRect = current.getBoundingClientRect();
11791
+ const clippedByAncestor = rect.left < currentRect.left - 0.5 || rect.right > currentRect.right + 0.5;
11792
+ if (clippedByAncestor) return true;
11793
+ }
11646
11794
  current = current.parentElement;
11647
11795
  }
11648
11796
  return false;
@@ -11656,7 +11804,7 @@ async function captureViewport(viewport) {
11656
11804
  const rightOverflow = Math.max(0, rect.right - viewportWidth);
11657
11805
  const overflow = Math.max(leftOverflow, rightOverflow);
11658
11806
  if (overflow <= 0.5) continue;
11659
- if (isContainedByHorizontalScroller(element)) continue;
11807
+ if (isHandledByHorizontalOverflowAncestor(element, rect)) continue;
11660
11808
  const tag = element.tagName ? element.tagName.toLowerCase() : "element";
11661
11809
  const id = element.id ? "#" + element.id : "";
11662
11810
  const className = typeof element.className === "string"
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  resolveRiddleProofProfileTimeoutSec,
59
59
  slugifyRiddleProofProfileName,
60
60
  summarizeRiddleProofProfileResult
61
- } from "./chunk-IKT7AKZN.js";
61
+ } from "./chunk-2JFDTCIC.js";
62
62
  import {
63
63
  DEFAULT_RIDDLE_API_BASE_URL,
64
64
  DEFAULT_RIDDLE_API_KEY_FILE,
package/dist/profile.cjs CHANGED
@@ -55,6 +55,8 @@ var RIDDLE_PROOF_PROFILE_STATUSES = [
55
55
  ];
56
56
  var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
57
57
  "route_loaded",
58
+ "url_search_param_equals",
59
+ "url_search_param_absent",
58
60
  "selector_visible",
59
61
  "selector_absent",
60
62
  "selector_count_at_least",
@@ -531,6 +533,13 @@ function normalizeCheck(input, index) {
531
533
  if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
532
534
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
533
535
  }
536
+ if ((type === "url_search_param_equals" || type === "url_search_param_absent") && !stringValue(input.param) && !stringValue(input.search_param) && !stringValue(input.searchParam) && !stringValue(input.key)) {
537
+ throw new Error(`checks[${index}] ${type} requires param.`);
538
+ }
539
+ const expectedValue = stringFromOwn(input, "expected_value", "expectedValue", "value");
540
+ if (type === "url_search_param_equals" && expectedValue === void 0) {
541
+ throw new Error(`checks[${index}] url_search_param_equals requires expected_value.`);
542
+ }
534
543
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
535
544
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
536
545
  }
@@ -551,6 +560,8 @@ function normalizeCheck(input, index) {
551
560
  type,
552
561
  label: stringValue(input.label),
553
562
  expected_path: stringValue(input.expected_path),
563
+ param: stringValue(input.param) || stringValue(input.search_param) || stringValue(input.searchParam) || stringValue(input.key),
564
+ expected_value: expectedValue,
554
565
  expected_routes: expectedRoutes,
555
566
  selector: stringValue(input.selector),
556
567
  expected_texts: expectedTexts,
@@ -803,6 +814,32 @@ function successfulRoute(route, targetUrl) {
803
814
  const matched = route.matched || routePathMatches(route.observed, route.expected_path, targetUrl);
804
815
  return matched && !route.error && (route.http_status === null || route.http_status === void 0 || route.http_status < 400);
805
816
  }
817
+ function parseEvidenceUrl(value, targetUrl) {
818
+ if (!value) return void 0;
819
+ try {
820
+ return targetUrl ? new URL(value, targetUrl) : new URL(value);
821
+ } catch {
822
+ try {
823
+ return new URL(value);
824
+ } catch {
825
+ return void 0;
826
+ }
827
+ }
828
+ }
829
+ function observedUrlForViewport(viewport, targetUrl) {
830
+ return parseEvidenceUrl(viewport.url, targetUrl) || parseEvidenceUrl(viewport.route?.observed, targetUrl) || parseEvidenceUrl(viewport.route?.requested, targetUrl) || parseEvidenceUrl(targetUrl, void 0);
831
+ }
832
+ function searchParamObservation(viewport, targetUrl, param) {
833
+ const url = observedUrlForViewport(viewport, targetUrl);
834
+ const values = url ? url.searchParams.getAll(param) : [];
835
+ return {
836
+ viewport: viewport.name,
837
+ url: url?.href || null,
838
+ value: values.length ? values[0] : null,
839
+ values,
840
+ present: values.length > 0
841
+ };
842
+ }
806
843
  function viewportsForCheck(check, viewports) {
807
844
  if (!check.viewports?.length) return viewports;
808
845
  const names = new Set(check.viewports);
@@ -840,6 +877,44 @@ function assessCheckFromEvidence(check, evidence) {
840
877
  message: failed.length ? `Route did not load as ${expectedPath} in ${failed.length} viewport(s).` : void 0
841
878
  };
842
879
  }
880
+ if (check.type === "url_search_param_equals") {
881
+ const param = check.param || "";
882
+ const expectedValue = check.expected_value ?? "";
883
+ const observations = viewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
884
+ const failed = observations.filter((observation) => observation.value !== expectedValue);
885
+ return {
886
+ type: check.type,
887
+ label: checkLabel(check),
888
+ status: failed.length ? "failed" : "passed",
889
+ evidence: {
890
+ param,
891
+ expected_value: expectedValue,
892
+ observed_values: observations.map((observation) => observation.value),
893
+ observed_all_values: observations.map((observation) => observation.values),
894
+ observed_urls: observations.map((observation) => observation.url),
895
+ viewports: observations.map((observation) => toJsonValue(observation))
896
+ },
897
+ message: failed.length ? `URL search param ${param} did not equal ${JSON.stringify(expectedValue)} in ${failed.length} viewport(s).` : void 0
898
+ };
899
+ }
900
+ if (check.type === "url_search_param_absent") {
901
+ const param = check.param || "";
902
+ const observations = viewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
903
+ const failed = observations.filter((observation) => observation.present);
904
+ return {
905
+ type: check.type,
906
+ label: checkLabel(check),
907
+ status: failed.length ? "failed" : "passed",
908
+ evidence: {
909
+ param,
910
+ observed_values: observations.map((observation) => observation.value),
911
+ observed_all_values: observations.map((observation) => observation.values),
912
+ observed_urls: observations.map((observation) => observation.url),
913
+ viewports: observations.map((observation) => toJsonValue(observation))
914
+ },
915
+ message: failed.length ? `URL search param ${param} was present in ${failed.length} viewport(s).` : void 0
916
+ };
917
+ }
843
918
  if (check.type === "selector_visible") {
844
919
  const key = selectorKey(check);
845
920
  const failed = viewports.filter((viewport) => (viewport.selectors?.[key]?.visible_count || 0) < 1);
@@ -1375,6 +1450,29 @@ function routePathMatches(observed, expected, targetUrl) {
1375
1450
  function routeOk(route, targetUrl) {
1376
1451
  return Boolean(route && (route.matched || routePathMatches(route.observed, route.expected_path, targetUrl)) && !route.error && (route.http_status == null || route.http_status < 400));
1377
1452
  }
1453
+ function parseEvidenceUrl(value, targetUrl) {
1454
+ if (!value) return null;
1455
+ try { return targetUrl ? new URL(value, targetUrl) : new URL(value); } catch {}
1456
+ try { return new URL(value); } catch {}
1457
+ return null;
1458
+ }
1459
+ function observedUrlForViewport(viewport, targetUrl) {
1460
+ return parseEvidenceUrl(viewport && viewport.url, targetUrl)
1461
+ || parseEvidenceUrl(viewport && viewport.route && viewport.route.observed, targetUrl)
1462
+ || parseEvidenceUrl(viewport && viewport.route && viewport.route.requested, targetUrl)
1463
+ || parseEvidenceUrl(targetUrl, null);
1464
+ }
1465
+ function searchParamObservation(viewport, targetUrl, param) {
1466
+ const url = observedUrlForViewport(viewport, targetUrl);
1467
+ const values = url ? url.searchParams.getAll(param) : [];
1468
+ return {
1469
+ viewport: viewport && viewport.name,
1470
+ url: url ? url.href : null,
1471
+ value: values.length ? values[0] : null,
1472
+ values,
1473
+ present: values.length > 0,
1474
+ };
1475
+ }
1378
1476
  function textMatches(sample, check) {
1379
1477
  if (check.pattern) {
1380
1478
  try { return new RegExp(check.pattern, check.flags || "").test(sample || ""); } catch { return false; }
@@ -1679,6 +1777,46 @@ function assessProfile(profile, evidence) {
1679
1777
  });
1680
1778
  continue;
1681
1779
  }
1780
+ if (check.type === "url_search_param_equals") {
1781
+ const param = check.param || "";
1782
+ const expectedValue = check.expected_value == null ? "" : String(check.expected_value);
1783
+ const observations = checkViewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
1784
+ const failed = observations.filter((observation) => observation.value !== expectedValue);
1785
+ checks.push({
1786
+ type: check.type,
1787
+ label: check.label || check.type,
1788
+ status: failed.length ? "failed" : "passed",
1789
+ evidence: {
1790
+ param,
1791
+ expected_value: expectedValue,
1792
+ observed_values: observations.map((observation) => observation.value),
1793
+ observed_all_values: observations.map((observation) => observation.values),
1794
+ observed_urls: observations.map((observation) => observation.url),
1795
+ viewports: observations,
1796
+ },
1797
+ message: failed.length ? "URL search param " + param + " did not equal " + JSON.stringify(expectedValue) + " in " + failed.length + " viewport(s)." : undefined,
1798
+ });
1799
+ continue;
1800
+ }
1801
+ if (check.type === "url_search_param_absent") {
1802
+ const param = check.param || "";
1803
+ const observations = checkViewports.map((viewport) => searchParamObservation(viewport, evidence.target_url, param));
1804
+ const failed = observations.filter((observation) => observation.present);
1805
+ checks.push({
1806
+ type: check.type,
1807
+ label: check.label || check.type,
1808
+ status: failed.length ? "failed" : "passed",
1809
+ evidence: {
1810
+ param,
1811
+ observed_values: observations.map((observation) => observation.value),
1812
+ observed_all_values: observations.map((observation) => observation.values),
1813
+ observed_urls: observations.map((observation) => observation.url),
1814
+ viewports: observations,
1815
+ },
1816
+ message: failed.length ? "URL search param " + param + " was present in " + failed.length + " viewport(s)." : undefined,
1817
+ });
1818
+ continue;
1819
+ }
1682
1820
  if (check.type === "selector_visible") {
1683
1821
  const selector = check.selector || "";
1684
1822
  const failed = checkViewports.filter((viewport) => !viewport.selectors || !viewport.selectors[selector] || viewport.selectors[selector].visible_count < 1);
@@ -2559,7 +2697,7 @@ async function frameEvidence(selector) {
2559
2697
  const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
2560
2698
  const viewportWidth = clientWidth || window.innerWidth;
2561
2699
  const overflowOffenders = [];
2562
- function isContainedByHorizontalScroller(element) {
2700
+ function isHandledByHorizontalOverflowAncestor(element, rect) {
2563
2701
  let current = element.parentElement;
2564
2702
  while (current && current !== body && current !== documentElement) {
2565
2703
  const style = window.getComputedStyle(current);
@@ -2569,6 +2707,11 @@ async function frameEvidence(selector) {
2569
2707
  const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
2570
2708
  if (contained) return true;
2571
2709
  }
2710
+ if (overflowX === "hidden" || overflowX === "clip" || style.overflow === "hidden" || style.overflow === "clip") {
2711
+ const currentRect = current.getBoundingClientRect();
2712
+ const clippedByAncestor = rect.left < currentRect.left - 0.5 || rect.right > currentRect.right + 0.5;
2713
+ if (clippedByAncestor) return true;
2714
+ }
2572
2715
  current = current.parentElement;
2573
2716
  }
2574
2717
  return false;
@@ -2582,7 +2725,7 @@ async function frameEvidence(selector) {
2582
2725
  const rightOverflow = Math.max(0, rect.right - viewportWidth);
2583
2726
  const overflow = Math.max(leftOverflow, rightOverflow);
2584
2727
  if (overflow <= 0.5) continue;
2585
- if (isContainedByHorizontalScroller(element)) continue;
2728
+ if (isHandledByHorizontalOverflowAncestor(element, rect)) continue;
2586
2729
  const tag = element.tagName ? element.tagName.toLowerCase() : "element";
2587
2730
  const id = element.id ? "#" + element.id : "";
2588
2731
  const className = typeof element.className === "string"
@@ -2962,7 +3105,7 @@ async function captureViewport(viewport) {
2962
3105
  const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
2963
3106
  const viewportWidth = clientWidth || window.innerWidth;
2964
3107
  const overflowOffenders = [];
2965
- function isContainedByHorizontalScroller(element) {
3108
+ function isHandledByHorizontalOverflowAncestor(element, rect) {
2966
3109
  let current = element.parentElement;
2967
3110
  while (current && current !== body && current !== documentElement) {
2968
3111
  const style = window.getComputedStyle(current);
@@ -2972,6 +3115,11 @@ async function captureViewport(viewport) {
2972
3115
  const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
2973
3116
  if (contained) return true;
2974
3117
  }
3118
+ if (overflowX === "hidden" || overflowX === "clip" || style.overflow === "hidden" || style.overflow === "clip") {
3119
+ const currentRect = current.getBoundingClientRect();
3120
+ const clippedByAncestor = rect.left < currentRect.left - 0.5 || rect.right > currentRect.right + 0.5;
3121
+ if (clippedByAncestor) return true;
3122
+ }
2975
3123
  current = current.parentElement;
2976
3124
  }
2977
3125
  return false;
@@ -2985,7 +3133,7 @@ async function captureViewport(viewport) {
2985
3133
  const rightOverflow = Math.max(0, rect.right - viewportWidth);
2986
3134
  const overflow = Math.max(leftOverflow, rightOverflow);
2987
3135
  if (overflow <= 0.5) continue;
2988
- if (isContainedByHorizontalScroller(element)) continue;
3136
+ if (isHandledByHorizontalOverflowAncestor(element, rect)) continue;
2989
3137
  const tag = element.tagName ? element.tagName.toLowerCase() : "element";
2990
3138
  const id = element.id ? "#" + element.id : "";
2991
3139
  const className = typeof element.className === "string"
@@ -4,7 +4,7 @@ declare const RIDDLE_PROOF_PROFILE_VERSION: "riddle-proof.profile.v1";
4
4
  declare const RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION: "riddle-proof.profile-evidence.v1";
5
5
  declare const RIDDLE_PROOF_PROFILE_RESULT_VERSION: "riddle-proof.profile-result.v1";
6
6
  declare const RIDDLE_PROOF_PROFILE_STATUSES: readonly ["passed", "product_regression", "proof_insufficient", "environment_blocked", "configuration_error", "needs_human_review"];
7
- declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_absent", "selector_count_at_least", "selector_count_equals", "selector_count_equal", "selector_count_eq", "selector_text_order", "frame_text_visible", "frame_no_horizontal_overflow", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
7
+ declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "url_search_param_equals", "url_search_param_absent", "selector_visible", "selector_absent", "selector_count_at_least", "selector_count_equals", "selector_count_equal", "selector_count_eq", "selector_text_order", "frame_text_visible", "frame_no_horizontal_overflow", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
8
8
  declare const RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES: readonly ["click", "fill", "set_input_value", "assert_text_visible", "assert_text_absent", "assert_selector_count", "assert_window_value", "local_storage", "session_storage", "clear_storage", "wait", "wait_for_selector", "wait_for_text", "window_call"];
9
9
  type RiddleProofProfileStatus = typeof RIDDLE_PROOF_PROFILE_STATUSES[number];
10
10
  type RiddleProofProfileCheckType = typeof RIDDLE_PROOF_PROFILE_CHECK_TYPES[number];
@@ -88,6 +88,8 @@ interface RiddleProofProfileCheck {
88
88
  type: RiddleProofProfileCheckType;
89
89
  label?: string;
90
90
  expected_path?: string;
91
+ param?: string;
92
+ expected_value?: string;
91
93
  expected_routes?: RiddleProofProfileRouteInventoryRoute[];
92
94
  selector?: string;
93
95
  expected_texts?: string[];
package/dist/profile.d.ts CHANGED
@@ -4,7 +4,7 @@ declare const RIDDLE_PROOF_PROFILE_VERSION: "riddle-proof.profile.v1";
4
4
  declare const RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION: "riddle-proof.profile-evidence.v1";
5
5
  declare const RIDDLE_PROOF_PROFILE_RESULT_VERSION: "riddle-proof.profile-result.v1";
6
6
  declare const RIDDLE_PROOF_PROFILE_STATUSES: readonly ["passed", "product_regression", "proof_insufficient", "environment_blocked", "configuration_error", "needs_human_review"];
7
- declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_absent", "selector_count_at_least", "selector_count_equals", "selector_count_equal", "selector_count_eq", "selector_text_order", "frame_text_visible", "frame_no_horizontal_overflow", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
7
+ declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "url_search_param_equals", "url_search_param_absent", "selector_visible", "selector_absent", "selector_count_at_least", "selector_count_equals", "selector_count_equal", "selector_count_eq", "selector_text_order", "frame_text_visible", "frame_no_horizontal_overflow", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
8
8
  declare const RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES: readonly ["click", "fill", "set_input_value", "assert_text_visible", "assert_text_absent", "assert_selector_count", "assert_window_value", "local_storage", "session_storage", "clear_storage", "wait", "wait_for_selector", "wait_for_text", "window_call"];
9
9
  type RiddleProofProfileStatus = typeof RIDDLE_PROOF_PROFILE_STATUSES[number];
10
10
  type RiddleProofProfileCheckType = typeof RIDDLE_PROOF_PROFILE_CHECK_TYPES[number];
@@ -88,6 +88,8 @@ interface RiddleProofProfileCheck {
88
88
  type: RiddleProofProfileCheckType;
89
89
  label?: string;
90
90
  expected_path?: string;
91
+ param?: string;
92
+ expected_value?: string;
91
93
  expected_routes?: RiddleProofProfileRouteInventoryRoute[];
92
94
  selector?: string;
93
95
  expected_texts?: string[];
package/dist/profile.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  resolveRiddleProofProfileTimeoutSec,
20
20
  slugifyRiddleProofProfileName,
21
21
  summarizeRiddleProofProfileResult
22
- } from "./chunk-IKT7AKZN.js";
22
+ } from "./chunk-2JFDTCIC.js";
23
23
  export {
24
24
  RIDDLE_PROOF_PROFILE_CHECK_TYPES,
25
25
  RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.7.53",
3
+ "version": "0.7.55",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",