@riddledc/riddle-proof 0.7.93 → 0.7.95

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
@@ -163,6 +163,7 @@ or as a stronger proof base before a change loop.
163
163
  { "type": "selector_visible", "selector": "[data-testid='pricing-cards']" },
164
164
  { "type": "selector_absent", "selector": "[data-testid='loading-spinner']" },
165
165
  { "type": "selector_count_equals", "selector": "[data-testid='pricing-card']", "expected_count": 3 },
166
+ { "type": "selector_text_visible", "selector": "[data-testid='pricing-cards']", "text": "Pro" },
166
167
  { "type": "text_visible", "text": "Start building" },
167
168
  { "type": "text_visible", "text": "Compare plans", "viewports": ["desktop"] },
168
169
  { "type": "no_mobile_horizontal_overflow" },
@@ -417,6 +418,22 @@ The check records the visible text sequence for the selector and passes when
417
418
  the expected texts appear in that order as a subsequence. This is less brittle
418
419
  than matching one large body-text regex when only row or card order matters.
419
420
 
421
+ Use `selector_text_visible` and `selector_text_absent` when the durable
422
+ assertion belongs to one panel, code sample, result area, or card group rather
423
+ than the whole page:
424
+
425
+ ```json
426
+ {
427
+ "type": "selector_text_visible",
428
+ "selector": ".result-state",
429
+ "text": "\"sync\": false"
430
+ }
431
+ ```
432
+
433
+ The check records visible text for the selector in each viewport and reports
434
+ matched counts plus short samples, which makes generated-command and evidence
435
+ card audits easier to diagnose than global `text_visible` checks.
436
+
420
437
  Use `frame_text_visible` and `frame_no_horizontal_overflow` for embedded app,
421
438
  game, or preview surfaces that render inside iframes:
422
439
 
@@ -462,6 +479,8 @@ artifact links should fail the profile:
462
479
  "expected_count": 88,
463
480
  "same_origin_only": true,
464
481
  "require_nonzero_bytes": true,
482
+ "min_bytes": 32,
483
+ "allowed_content_types": ["image/*", "application/json"],
465
484
  "max_links": 150
466
485
  }
467
486
  ```
@@ -469,9 +488,12 @@ artifact links should fail the profile:
469
488
  The check defaults to `a[href]`, deduplicates URLs, probes up to 100 selected
470
489
  URLs, and treats HTTP `2xx` / `3xx` responses as healthy. Use
471
490
  `allowed_statuses` or `expected_status` when a narrower status contract is
472
- intentional, `min_count` for lower-bound audits, and `max_links` when the
473
- selected set is intentionally larger than 100. `artifact_link_status` is an
474
- alias with the same behavior for profiles that want artifact-specific wording.
491
+ intentional, `min_count` for lower-bound audits, `min_bytes` when a one-byte
492
+ range response is too weak, `allowed_content_types` for MIME checks, and
493
+ `max_links` when the selected set is intentionally larger than 100.
494
+ `allowed_content_types` accepts exact types and family wildcards such as
495
+ `image/*`. `artifact_link_status` is an alias with the same behavior for
496
+ profiles that want artifact-specific wording.
475
497
 
476
498
  Use the `route_inventory` check for source-page route coverage audits where a
477
499
  navigation surface must expose a known set of routes and each route must load
@@ -20,6 +20,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
20
20
  "selector_count_equals",
21
21
  "selector_count_equal",
22
22
  "selector_count_eq",
23
+ "selector_text_visible",
24
+ "selector_text_absent",
23
25
  "selector_text_order",
24
26
  "frame_text_visible",
25
27
  "frame_url_equals",
@@ -733,7 +735,7 @@ function normalizeCheck(input, index) {
733
735
  if (!isSupportedCheckType(type)) {
734
736
  throw new Error(`checks[${index}].type ${type} is not supported. Supported checks: ${RIDDLE_PROOF_PROFILE_CHECK_TYPES.join(", ")}`);
735
737
  }
736
- if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && !stringValue(input.selector)) {
738
+ if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq" || type === "selector_text_visible" || type === "selector_text_absent") && !stringValue(input.selector)) {
737
739
  throw new Error(`checks[${index}] ${type} requires selector.`);
738
740
  }
739
741
  if ((type === "frame_text_visible" || type === "frame_url_equals" || type === "frame_url_matches" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
@@ -749,7 +751,7 @@ function normalizeCheck(input, index) {
749
751
  if (type === "frame_url_matches" && !stringValue(input.pattern)) {
750
752
  throw new Error(`checks[${index}] frame_url_matches requires pattern.`);
751
753
  }
752
- if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
754
+ if ((type === "text_visible" || type === "text_absent" || type === "selector_text_visible" || type === "selector_text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
753
755
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
754
756
  }
755
757
  if ((type === "url_search_param_equals" || type === "url_search_param_absent") && !stringValue(input.param) && !stringValue(input.search_param) && !stringValue(input.searchParam) && !stringValue(input.key)) {
@@ -783,6 +785,12 @@ function normalizeCheck(input, index) {
783
785
  `checks[${index}] allowed_statuses`
784
786
  ) : void 0;
785
787
  const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
788
+ const minBytes = isLinkStatusCheck ? normalizePositiveInteger(input.min_bytes ?? input.minBytes ?? input.min_response_bytes ?? input.minResponseBytes, `checks[${index}] min_bytes`) : void 0;
789
+ const expectedContentType = isLinkStatusCheck ? stringValue(input.expected_content_type) || stringValue(input.expectedContentType) || stringValue(input.content_type) || stringValue(input.contentType) : void 0;
790
+ const allowedContentTypes = isLinkStatusCheck ? normalizeStringList(
791
+ input.allowed_content_types ?? input.allowedContentTypes ?? input.expected_content_types ?? input.expectedContentTypes,
792
+ `checks[${index}] allowed_content_types`
793
+ ) ?? (expectedContentType ? [expectedContentType] : void 0) : void 0;
786
794
  if (isLinkStatusCheck) {
787
795
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
788
796
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -824,6 +832,8 @@ function normalizeCheck(input, index) {
824
832
  same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
825
833
  dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
826
834
  require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
835
+ min_bytes: minBytes,
836
+ allowed_content_types: allowedContentTypes,
827
837
  allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
828
838
  max_overflow_px: numberValue(input.max_overflow_px),
829
839
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
@@ -930,15 +940,40 @@ function linkStatusIsAllowed(status, check) {
930
940
  const allowed = linkStatusAllowedStatuses(check);
931
941
  return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
932
942
  }
943
+ function linkStatusObservedBytes(result) {
944
+ const bytes = numberValue(result.bytes);
945
+ const contentLength = numberValue(result.content_length);
946
+ return Math.max(bytes ?? 0, contentLength ?? 0) || void 0;
947
+ }
948
+ function normalizeLinkStatusContentType(value) {
949
+ const normalized = value?.split(";")[0]?.trim().toLowerCase();
950
+ return normalized || void 0;
951
+ }
952
+ function linkStatusContentTypeOk(result, check) {
953
+ const expected = check.allowed_content_types;
954
+ if (!expected?.length) return true;
955
+ const actual = normalizeLinkStatusContentType(stringValue(result.content_type));
956
+ if (!actual) return false;
957
+ return expected.some((contentType) => {
958
+ const normalized = normalizeLinkStatusContentType(contentType);
959
+ if (!normalized) return false;
960
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
961
+ return actual === normalized;
962
+ });
963
+ }
933
964
  function linkStatusResultOk(result, check) {
934
965
  const status = numberValue(result.status);
935
966
  if (!linkStatusIsAllowed(status, check)) return false;
936
967
  if (stringValue(result.error)) return false;
937
968
  if (result.ok === false) return false;
969
+ if (!linkStatusContentTypeOk(result, check)) return false;
938
970
  if (check.require_nonzero_bytes) {
939
- const bytes = numberValue(result.bytes);
940
- const contentLength = numberValue(result.content_length);
941
- return bytes !== void 0 && bytes > 0 || contentLength !== void 0 && contentLength > 0;
971
+ const observedBytes = linkStatusObservedBytes(result);
972
+ if (observedBytes === void 0 || observedBytes <= 0) return false;
973
+ }
974
+ if (check.min_bytes !== void 0) {
975
+ const observedBytes = linkStatusObservedBytes(result);
976
+ if (observedBytes === void 0 || observedBytes < check.min_bytes) return false;
942
977
  }
943
978
  return true;
944
979
  }
@@ -967,7 +1002,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
967
1002
  status: numberValue(result.status) ?? null,
968
1003
  method: stringValue(result.method) ?? null,
969
1004
  error: stringValue(result.error) ?? null,
970
- bytes: numberValue(result.bytes) ?? numberValue(result.content_length) ?? null
1005
+ content_type: stringValue(result.content_type) ?? null,
1006
+ bytes: linkStatusObservedBytes(result) ?? null,
1007
+ min_bytes: check.min_bytes ?? null,
1008
+ allowed_content_types: check.allowed_content_types ?? null
971
1009
  }));
972
1010
  if (stringValue(linkEvidence.error)) {
973
1011
  failures.push({ code: "link_status_capture_failed", error: stringValue(linkEvidence.error) ?? "" });
@@ -1004,6 +1042,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
1004
1042
  failed_count: failures.length,
1005
1043
  truncated: linkEvidence.truncated === true,
1006
1044
  max_links: numberValue(linkEvidence.max_links) ?? check.max_links ?? 100,
1045
+ min_bytes: check.min_bytes ?? null,
1046
+ allowed_content_types: check.allowed_content_types ?? null,
1007
1047
  status_counts: statusCounts,
1008
1048
  failures: failures.slice(0, 20)
1009
1049
  };
@@ -1015,9 +1055,11 @@ function textSequenceForCheck(viewport, check) {
1015
1055
  const key = selectorKey(check);
1016
1056
  const sequence = viewport.text_sequences?.[key];
1017
1057
  if (isRecord(sequence)) {
1058
+ const visibleMatchTexts = Array.isArray(sequence.visible_match_texts) ? sequence.visible_match_texts : [];
1059
+ const matchTexts = Array.isArray(sequence.match_texts) ? sequence.match_texts : [];
1018
1060
  const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
1019
1061
  const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
1020
- const candidates = visibleTexts.length ? visibleTexts : texts;
1062
+ const candidates = visibleMatchTexts.length ? visibleMatchTexts : matchTexts.length ? matchTexts : visibleTexts.length ? visibleTexts : texts;
1021
1063
  return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
1022
1064
  }
1023
1065
  return [];
@@ -1351,6 +1393,35 @@ function assessCheckFromEvidence(check, evidence) {
1351
1393
  message: failed.length ? `Selector ${key} count did not equal ${expectedCount} in ${failed.length} viewport(s).` : void 0
1352
1394
  };
1353
1395
  }
1396
+ if (check.type === "selector_text_visible" || check.type === "selector_text_absent") {
1397
+ const key = selectorKey(check);
1398
+ const expectedVisible = check.type === "selector_text_visible";
1399
+ const results = viewports.map((viewport) => {
1400
+ const texts = textSequenceForCheck(viewport, check);
1401
+ const matches = texts.filter((text) => matchText(text, check));
1402
+ return {
1403
+ viewport: viewport.name,
1404
+ selector_count: viewport.selectors?.[key]?.count || 0,
1405
+ visible_count: viewport.selectors?.[key]?.visible_count || 0,
1406
+ matched_count: matches.length,
1407
+ matched: matches.length > 0,
1408
+ samples: matches.slice(0, 3).map((text) => text.slice(0, 240))
1409
+ };
1410
+ });
1411
+ const failed = results.filter((result) => result.matched !== expectedVisible).length;
1412
+ return {
1413
+ type: check.type,
1414
+ label: checkLabel(check),
1415
+ status: failed ? "failed" : "passed",
1416
+ evidence: {
1417
+ selector: key,
1418
+ text: check.text || null,
1419
+ pattern: check.pattern || null,
1420
+ viewports: results.map((result) => toJsonValue(result))
1421
+ },
1422
+ message: failed ? `Selector ${key} text assertion failed in ${failed} viewport(s).` : void 0
1423
+ };
1424
+ }
1354
1425
  if (check.type === "selector_text_order") {
1355
1426
  const key = selectorKey(check);
1356
1427
  const expectedTexts = check.expected_texts || [];
@@ -1361,7 +1432,7 @@ function assessCheckFromEvidence(check, evidence) {
1361
1432
  viewport: viewport.name,
1362
1433
  matched: match.matched,
1363
1434
  matched_positions: match.positions,
1364
- visible_texts: texts.slice(0, 20)
1435
+ visible_texts: texts.slice(0, 20).map((text) => text.slice(0, 240))
1365
1436
  };
1366
1437
  });
1367
1438
  const failed = results.filter((result) => !result.matched).length;
@@ -1501,6 +1572,8 @@ function assessCheckFromEvidence(check, evidence) {
1501
1572
  min_count: check.min_count ?? null,
1502
1573
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
1503
1574
  require_nonzero_bytes: check.require_nonzero_bytes === true,
1575
+ min_bytes: check.min_bytes ?? null,
1576
+ allowed_content_types: check.allowed_content_types ?? null,
1504
1577
  viewports: summaries.map((summary) => toJsonValue(summary)),
1505
1578
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
1506
1579
  },
@@ -2071,9 +2144,11 @@ function textSequenceForCheck(viewport, check) {
2071
2144
  const key = check.selector || "";
2072
2145
  const sequence = viewport.text_sequences && viewport.text_sequences[key];
2073
2146
  if (sequence && typeof sequence === "object") {
2147
+ const visibleMatchTexts = Array.isArray(sequence.visible_match_texts) ? sequence.visible_match_texts : [];
2148
+ const matchTexts = Array.isArray(sequence.match_texts) ? sequence.match_texts : [];
2074
2149
  const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
2075
2150
  const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
2076
- const candidates = visibleTexts.length ? visibleTexts : texts;
2151
+ const candidates = visibleMatchTexts.length ? visibleMatchTexts : matchTexts.length ? matchTexts : visibleTexts.length ? visibleTexts : texts;
2077
2152
  return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
2078
2153
  }
2079
2154
  return [];
@@ -2102,15 +2177,41 @@ function linkStatusIsAllowed(status, check) {
2102
2177
  const allowed = linkStatusAllowedStatuses(check);
2103
2178
  return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
2104
2179
  }
2180
+ function linkStatusObservedBytes(result) {
2181
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
2182
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
2183
+ const observed = Math.max(bytes || 0, contentLength || 0);
2184
+ return observed > 0 ? observed : undefined;
2185
+ }
2186
+ function normalizeLinkStatusContentType(value) {
2187
+ if (typeof value !== "string") return undefined;
2188
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
2189
+ return normalized || undefined;
2190
+ }
2191
+ function linkStatusContentTypeOk(result, check) {
2192
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
2193
+ const actual = normalizeLinkStatusContentType(result.content_type);
2194
+ if (!actual) return false;
2195
+ return check.allowed_content_types.some((contentType) => {
2196
+ const normalized = normalizeLinkStatusContentType(contentType);
2197
+ if (!normalized) return false;
2198
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
2199
+ return actual === normalized;
2200
+ });
2201
+ }
2105
2202
  function linkStatusResultOk(result, check) {
2106
2203
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
2107
2204
  if (!linkStatusIsAllowed(result.status, check)) return false;
2108
2205
  if (typeof result.error === "string" && result.error.trim()) return false;
2109
2206
  if (result.ok === false) return false;
2207
+ if (!linkStatusContentTypeOk(result, check)) return false;
2110
2208
  if (check.require_nonzero_bytes === true) {
2111
- const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
2112
- const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
2113
- return (bytes !== undefined && bytes > 0) || (contentLength !== undefined && contentLength > 0);
2209
+ const observedBytes = linkStatusObservedBytes(result);
2210
+ if (observedBytes === undefined || observedBytes <= 0) return false;
2211
+ }
2212
+ if (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) {
2213
+ const observedBytes = linkStatusObservedBytes(result);
2214
+ if (observedBytes === undefined || observedBytes < check.min_bytes) return false;
2114
2215
  }
2115
2216
  return true;
2116
2217
  }
@@ -2138,11 +2239,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
2138
2239
  status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
2139
2240
  method: typeof result.method === "string" ? result.method : null,
2140
2241
  error: typeof result.error === "string" ? result.error : null,
2141
- bytes: typeof result.bytes === "number" && Number.isFinite(result.bytes)
2142
- ? result.bytes
2143
- : typeof result.content_length === "number" && Number.isFinite(result.content_length)
2144
- ? result.content_length
2145
- : null,
2242
+ content_type: typeof result.content_type === "string" ? result.content_type : null,
2243
+ bytes: linkStatusObservedBytes(result) ?? null,
2244
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
2245
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
2146
2246
  }));
2147
2247
  if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
2148
2248
  failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
@@ -2177,6 +2277,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
2177
2277
  failed_count: failures.length,
2178
2278
  truncated: linkEvidence.truncated === true,
2179
2279
  max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
2280
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
2281
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
2180
2282
  status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
2181
2283
  failures: failures.slice(0, 20),
2182
2284
  };
@@ -2711,6 +2813,31 @@ function assessProfile(profile, evidence) {
2711
2813
  });
2712
2814
  continue;
2713
2815
  }
2816
+ if (check.type === "selector_text_visible" || check.type === "selector_text_absent") {
2817
+ const selector = check.selector || "";
2818
+ const expectedVisible = check.type === "selector_text_visible";
2819
+ const results = checkViewports.map((viewport) => {
2820
+ const texts = textSequenceForCheck(viewport, check);
2821
+ const matches = texts.filter((text) => textMatches(text, check));
2822
+ return {
2823
+ viewport: viewport.name,
2824
+ selector_count: viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0,
2825
+ visible_count: viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].visible_count : 0,
2826
+ matched_count: matches.length,
2827
+ matched: matches.length > 0,
2828
+ samples: matches.slice(0, 3).map((text) => text.slice(0, 240)),
2829
+ };
2830
+ });
2831
+ const failed = results.filter((result) => result.matched !== expectedVisible).length;
2832
+ checks.push({
2833
+ type: check.type,
2834
+ label: check.label || check.type,
2835
+ status: failed ? "failed" : "passed",
2836
+ evidence: { selector, text: check.text || null, pattern: check.pattern || null, viewports: results },
2837
+ message: failed ? "Selector " + selector + " text assertion failed in " + failed + " viewport(s)." : undefined,
2838
+ });
2839
+ continue;
2840
+ }
2714
2841
  if (check.type === "selector_text_order") {
2715
2842
  const selector = check.selector || "";
2716
2843
  const expectedTexts = check.expected_texts || [];
@@ -2721,7 +2848,7 @@ function assessProfile(profile, evidence) {
2721
2848
  viewport: viewport.name,
2722
2849
  matched: match.matched,
2723
2850
  matched_positions: match.positions,
2724
- visible_texts: texts.slice(0, 20),
2851
+ visible_texts: texts.slice(0, 20).map((text) => text.slice(0, 240)),
2725
2852
  };
2726
2853
  });
2727
2854
  const failed = results.filter((result) => !result.matched).length;
@@ -2848,6 +2975,8 @@ function assessProfile(profile, evidence) {
2848
2975
  min_count: check.min_count ?? null,
2849
2976
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
2850
2977
  require_nonzero_bytes: check.require_nonzero_bytes === true,
2978
+ min_bytes: check.min_bytes ?? null,
2979
+ allowed_content_types: check.allowed_content_types ?? null,
2851
2980
  viewports: summaries,
2852
2981
  failures: failed.flatMap((summary) => Array.isArray(summary.failures)
2853
2982
  ? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
@@ -3925,6 +4054,7 @@ async function selectorStats(selector) {
3925
4054
  async function selectorTextSequence(selector) {
3926
4055
  return page.locator(selector).evaluateAll((elements) => {
3927
4056
  const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
4057
+ const matchText = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 8000);
3928
4058
  const isVisible = (element) => {
3929
4059
  const style = window.getComputedStyle(element);
3930
4060
  const rect = element.getBoundingClientRect();
@@ -3933,6 +4063,7 @@ async function selectorTextSequence(selector) {
3933
4063
  const rows = elements.map((element, index) => ({
3934
4064
  index,
3935
4065
  text: compact(element.innerText || element.textContent || ""),
4066
+ match_text: matchText(element.innerText || element.textContent || ""),
3936
4067
  visible: isVisible(element),
3937
4068
  }));
3938
4069
  return {
@@ -3940,8 +4071,10 @@ async function selectorTextSequence(selector) {
3940
4071
  visible_count: rows.filter((row) => row.visible).length,
3941
4072
  texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
3942
4073
  visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
4074
+ match_texts: rows.map((row) => row.match_text).filter(Boolean).slice(0, 40),
4075
+ visible_match_texts: rows.filter((row) => row.visible).map((row) => row.match_text).filter(Boolean).slice(0, 40),
3943
4076
  };
3944
- }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
4077
+ }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], match_texts: [], visible_match_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
3945
4078
  }
3946
4079
  function linkProbeMaxLinks(check) {
3947
4080
  const value = Number(check.max_links || check.maxLinks || check.limit || 100);
@@ -3991,6 +4124,28 @@ function linkProbeAllowed(status, check) {
3991
4124
  : null;
3992
4125
  return allowed ? allowed.includes(status) : status >= 200 && status < 400;
3993
4126
  }
4127
+ function linkProbeObservedBytes(result) {
4128
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
4129
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
4130
+ const observed = Math.max(bytes || 0, contentLength || 0);
4131
+ return observed > 0 ? observed : undefined;
4132
+ }
4133
+ function normalizeLinkProbeContentType(value) {
4134
+ if (typeof value !== "string") return undefined;
4135
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
4136
+ return normalized || undefined;
4137
+ }
4138
+ function linkProbeContentTypeAllowed(result, check) {
4139
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
4140
+ const actual = normalizeLinkProbeContentType(result.content_type);
4141
+ if (!actual) return false;
4142
+ return check.allowed_content_types.some((contentType) => {
4143
+ const normalized = normalizeLinkProbeContentType(contentType);
4144
+ if (!normalized) return false;
4145
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
4146
+ return actual === normalized;
4147
+ });
4148
+ }
3994
4149
  function linkProbeResponseFields(response, method) {
3995
4150
  const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
3996
4151
  const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
@@ -4005,6 +4160,7 @@ function linkProbeResponseFields(response, method) {
4005
4160
  }
4006
4161
  async function probeLinkStatus(candidate, check) {
4007
4162
  const requireNonzeroBytes = check.require_nonzero_bytes === true;
4163
+ const minBytes = typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? Math.max(1, Math.floor(check.min_bytes)) : null;
4008
4164
  const allowGetFallback = check.allow_get_fallback !== false;
4009
4165
  const result = {
4010
4166
  url: candidate.url,
@@ -4022,7 +4178,7 @@ async function probeLinkStatus(candidate, check) {
4022
4178
  };
4023
4179
  const applyResponse = async (response, method, readBytes) => {
4024
4180
  Object.assign(result, linkProbeResponseFields(response, method));
4025
- if (readBytes || requireNonzeroBytes) {
4181
+ if (readBytes) {
4026
4182
  try {
4027
4183
  const buffer = await response.arrayBuffer();
4028
4184
  result.bytes = buffer.byteLength;
@@ -4031,7 +4187,9 @@ async function probeLinkStatus(candidate, check) {
4031
4187
  }
4032
4188
  }
4033
4189
  result.ok = linkProbeAllowed(result.status, check)
4034
- && (!requireNonzeroBytes || (typeof result.bytes === "number" ? result.bytes > 0 : typeof result.content_length === "number" && result.content_length > 0))
4190
+ && linkProbeContentTypeAllowed(result, check)
4191
+ && (!requireNonzeroBytes || ((linkProbeObservedBytes(result) || 0) > 0))
4192
+ && (minBytes === null || ((linkProbeObservedBytes(result) || 0) >= minBytes))
4035
4193
  && !result.error;
4036
4194
  };
4037
4195
  try {
@@ -4048,9 +4206,9 @@ async function probeLinkStatus(candidate, check) {
4048
4206
  method: "GET",
4049
4207
  redirect: "follow",
4050
4208
  cache: "no-store",
4051
- headers: { Range: "bytes=0-0" },
4209
+ headers: { Range: "bytes=0-" + String((minBytes || 1) - 1) },
4052
4210
  });
4053
- await applyResponse(response, "GET", requireNonzeroBytes);
4211
+ await applyResponse(response, "GET", requireNonzeroBytes || minBytes !== null);
4054
4212
  return result;
4055
4213
  } catch (error) {
4056
4214
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -4104,7 +4262,10 @@ async function collectLinkStatus(check) {
4104
4262
  status: result.status,
4105
4263
  method: result.method,
4106
4264
  error: result.error,
4107
- bytes: result.bytes == null ? result.content_length : result.bytes,
4265
+ content_type: result.content_type,
4266
+ bytes: linkProbeObservedBytes(result) || null,
4267
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
4268
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
4108
4269
  }));
4109
4270
  const statusCounts = {};
4110
4271
  for (const result of results) {
@@ -4118,6 +4279,8 @@ async function collectLinkStatus(check) {
4118
4279
  same_origin_only: linkProbeSameOriginOnly(check),
4119
4280
  dedupe: linkProbeDedupe(check),
4120
4281
  require_nonzero_bytes: check.require_nonzero_bytes === true,
4282
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
4283
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
4121
4284
  allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
4122
4285
  ? check.allowed_statuses
4123
4286
  : typeof check.expected_status === "number"
@@ -4677,7 +4840,7 @@ async function captureViewport(viewport) {
4677
4840
  ) {
4678
4841
  selectors[check.selector] = await selectorStats(check.selector);
4679
4842
  }
4680
- if (check.type === "selector_text_order" && check.selector) {
4843
+ if ((check.type === "selector_text_order" || check.type === "selector_text_visible" || check.type === "selector_text_absent") && check.selector) {
4681
4844
  selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
4682
4845
  text_sequences[check.selector] = await selectorTextSequence(check.selector);
4683
4846
  }