@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/dist/profile.cjs CHANGED
@@ -63,6 +63,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
63
63
  "selector_count_equals",
64
64
  "selector_count_equal",
65
65
  "selector_count_eq",
66
+ "selector_text_visible",
67
+ "selector_text_absent",
66
68
  "selector_text_order",
67
69
  "frame_text_visible",
68
70
  "frame_url_equals",
@@ -776,7 +778,7 @@ function normalizeCheck(input, index) {
776
778
  if (!isSupportedCheckType(type)) {
777
779
  throw new Error(`checks[${index}].type ${type} is not supported. Supported checks: ${RIDDLE_PROOF_PROFILE_CHECK_TYPES.join(", ")}`);
778
780
  }
779
- 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)) {
781
+ 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)) {
780
782
  throw new Error(`checks[${index}] ${type} requires selector.`);
781
783
  }
782
784
  if ((type === "frame_text_visible" || type === "frame_url_equals" || type === "frame_url_matches" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
@@ -792,7 +794,7 @@ function normalizeCheck(input, index) {
792
794
  if (type === "frame_url_matches" && !stringValue(input.pattern)) {
793
795
  throw new Error(`checks[${index}] frame_url_matches requires pattern.`);
794
796
  }
795
- if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
797
+ if ((type === "text_visible" || type === "text_absent" || type === "selector_text_visible" || type === "selector_text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
796
798
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
797
799
  }
798
800
  if ((type === "url_search_param_equals" || type === "url_search_param_absent") && !stringValue(input.param) && !stringValue(input.search_param) && !stringValue(input.searchParam) && !stringValue(input.key)) {
@@ -826,6 +828,12 @@ function normalizeCheck(input, index) {
826
828
  `checks[${index}] allowed_statuses`
827
829
  ) : void 0;
828
830
  const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
831
+ const minBytes = isLinkStatusCheck ? normalizePositiveInteger(input.min_bytes ?? input.minBytes ?? input.min_response_bytes ?? input.minResponseBytes, `checks[${index}] min_bytes`) : void 0;
832
+ const expectedContentType = isLinkStatusCheck ? stringValue(input.expected_content_type) || stringValue(input.expectedContentType) || stringValue(input.content_type) || stringValue(input.contentType) : void 0;
833
+ const allowedContentTypes = isLinkStatusCheck ? normalizeStringList(
834
+ input.allowed_content_types ?? input.allowedContentTypes ?? input.expected_content_types ?? input.expectedContentTypes,
835
+ `checks[${index}] allowed_content_types`
836
+ ) ?? (expectedContentType ? [expectedContentType] : void 0) : void 0;
829
837
  if (isLinkStatusCheck) {
830
838
  if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
831
839
  throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
@@ -867,6 +875,8 @@ function normalizeCheck(input, index) {
867
875
  same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
868
876
  dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
869
877
  require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
878
+ min_bytes: minBytes,
879
+ allowed_content_types: allowedContentTypes,
870
880
  allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
871
881
  max_overflow_px: numberValue(input.max_overflow_px),
872
882
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
@@ -973,15 +983,40 @@ function linkStatusIsAllowed(status, check) {
973
983
  const allowed = linkStatusAllowedStatuses(check);
974
984
  return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
975
985
  }
986
+ function linkStatusObservedBytes(result) {
987
+ const bytes = numberValue(result.bytes);
988
+ const contentLength = numberValue(result.content_length);
989
+ return Math.max(bytes ?? 0, contentLength ?? 0) || void 0;
990
+ }
991
+ function normalizeLinkStatusContentType(value) {
992
+ const normalized = value?.split(";")[0]?.trim().toLowerCase();
993
+ return normalized || void 0;
994
+ }
995
+ function linkStatusContentTypeOk(result, check) {
996
+ const expected = check.allowed_content_types;
997
+ if (!expected?.length) return true;
998
+ const actual = normalizeLinkStatusContentType(stringValue(result.content_type));
999
+ if (!actual) return false;
1000
+ return expected.some((contentType) => {
1001
+ const normalized = normalizeLinkStatusContentType(contentType);
1002
+ if (!normalized) return false;
1003
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
1004
+ return actual === normalized;
1005
+ });
1006
+ }
976
1007
  function linkStatusResultOk(result, check) {
977
1008
  const status = numberValue(result.status);
978
1009
  if (!linkStatusIsAllowed(status, check)) return false;
979
1010
  if (stringValue(result.error)) return false;
980
1011
  if (result.ok === false) return false;
1012
+ if (!linkStatusContentTypeOk(result, check)) return false;
981
1013
  if (check.require_nonzero_bytes) {
982
- const bytes = numberValue(result.bytes);
983
- const contentLength = numberValue(result.content_length);
984
- return bytes !== void 0 && bytes > 0 || contentLength !== void 0 && contentLength > 0;
1014
+ const observedBytes = linkStatusObservedBytes(result);
1015
+ if (observedBytes === void 0 || observedBytes <= 0) return false;
1016
+ }
1017
+ if (check.min_bytes !== void 0) {
1018
+ const observedBytes = linkStatusObservedBytes(result);
1019
+ if (observedBytes === void 0 || observedBytes < check.min_bytes) return false;
985
1020
  }
986
1021
  return true;
987
1022
  }
@@ -1010,7 +1045,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
1010
1045
  status: numberValue(result.status) ?? null,
1011
1046
  method: stringValue(result.method) ?? null,
1012
1047
  error: stringValue(result.error) ?? null,
1013
- bytes: numberValue(result.bytes) ?? numberValue(result.content_length) ?? null
1048
+ content_type: stringValue(result.content_type) ?? null,
1049
+ bytes: linkStatusObservedBytes(result) ?? null,
1050
+ min_bytes: check.min_bytes ?? null,
1051
+ allowed_content_types: check.allowed_content_types ?? null
1014
1052
  }));
1015
1053
  if (stringValue(linkEvidence.error)) {
1016
1054
  failures.push({ code: "link_status_capture_failed", error: stringValue(linkEvidence.error) ?? "" });
@@ -1047,6 +1085,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
1047
1085
  failed_count: failures.length,
1048
1086
  truncated: linkEvidence.truncated === true,
1049
1087
  max_links: numberValue(linkEvidence.max_links) ?? check.max_links ?? 100,
1088
+ min_bytes: check.min_bytes ?? null,
1089
+ allowed_content_types: check.allowed_content_types ?? null,
1050
1090
  status_counts: statusCounts,
1051
1091
  failures: failures.slice(0, 20)
1052
1092
  };
@@ -1058,9 +1098,11 @@ function textSequenceForCheck(viewport, check) {
1058
1098
  const key = selectorKey(check);
1059
1099
  const sequence = viewport.text_sequences?.[key];
1060
1100
  if (isRecord(sequence)) {
1101
+ const visibleMatchTexts = Array.isArray(sequence.visible_match_texts) ? sequence.visible_match_texts : [];
1102
+ const matchTexts = Array.isArray(sequence.match_texts) ? sequence.match_texts : [];
1061
1103
  const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
1062
1104
  const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
1063
- const candidates = visibleTexts.length ? visibleTexts : texts;
1105
+ const candidates = visibleMatchTexts.length ? visibleMatchTexts : matchTexts.length ? matchTexts : visibleTexts.length ? visibleTexts : texts;
1064
1106
  return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
1065
1107
  }
1066
1108
  return [];
@@ -1394,6 +1436,35 @@ function assessCheckFromEvidence(check, evidence) {
1394
1436
  message: failed.length ? `Selector ${key} count did not equal ${expectedCount} in ${failed.length} viewport(s).` : void 0
1395
1437
  };
1396
1438
  }
1439
+ if (check.type === "selector_text_visible" || check.type === "selector_text_absent") {
1440
+ const key = selectorKey(check);
1441
+ const expectedVisible = check.type === "selector_text_visible";
1442
+ const results = viewports.map((viewport) => {
1443
+ const texts = textSequenceForCheck(viewport, check);
1444
+ const matches = texts.filter((text) => matchText(text, check));
1445
+ return {
1446
+ viewport: viewport.name,
1447
+ selector_count: viewport.selectors?.[key]?.count || 0,
1448
+ visible_count: viewport.selectors?.[key]?.visible_count || 0,
1449
+ matched_count: matches.length,
1450
+ matched: matches.length > 0,
1451
+ samples: matches.slice(0, 3).map((text) => text.slice(0, 240))
1452
+ };
1453
+ });
1454
+ const failed = results.filter((result) => result.matched !== expectedVisible).length;
1455
+ return {
1456
+ type: check.type,
1457
+ label: checkLabel(check),
1458
+ status: failed ? "failed" : "passed",
1459
+ evidence: {
1460
+ selector: key,
1461
+ text: check.text || null,
1462
+ pattern: check.pattern || null,
1463
+ viewports: results.map((result) => toJsonValue(result))
1464
+ },
1465
+ message: failed ? `Selector ${key} text assertion failed in ${failed} viewport(s).` : void 0
1466
+ };
1467
+ }
1397
1468
  if (check.type === "selector_text_order") {
1398
1469
  const key = selectorKey(check);
1399
1470
  const expectedTexts = check.expected_texts || [];
@@ -1404,7 +1475,7 @@ function assessCheckFromEvidence(check, evidence) {
1404
1475
  viewport: viewport.name,
1405
1476
  matched: match.matched,
1406
1477
  matched_positions: match.positions,
1407
- visible_texts: texts.slice(0, 20)
1478
+ visible_texts: texts.slice(0, 20).map((text) => text.slice(0, 240))
1408
1479
  };
1409
1480
  });
1410
1481
  const failed = results.filter((result) => !result.matched).length;
@@ -1544,6 +1615,8 @@ function assessCheckFromEvidence(check, evidence) {
1544
1615
  min_count: check.min_count ?? null,
1545
1616
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
1546
1617
  require_nonzero_bytes: check.require_nonzero_bytes === true,
1618
+ min_bytes: check.min_bytes ?? null,
1619
+ allowed_content_types: check.allowed_content_types ?? null,
1547
1620
  viewports: summaries.map((summary) => toJsonValue(summary)),
1548
1621
  failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
1549
1622
  },
@@ -2114,9 +2187,11 @@ function textSequenceForCheck(viewport, check) {
2114
2187
  const key = check.selector || "";
2115
2188
  const sequence = viewport.text_sequences && viewport.text_sequences[key];
2116
2189
  if (sequence && typeof sequence === "object") {
2190
+ const visibleMatchTexts = Array.isArray(sequence.visible_match_texts) ? sequence.visible_match_texts : [];
2191
+ const matchTexts = Array.isArray(sequence.match_texts) ? sequence.match_texts : [];
2117
2192
  const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
2118
2193
  const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
2119
- const candidates = visibleTexts.length ? visibleTexts : texts;
2194
+ const candidates = visibleMatchTexts.length ? visibleMatchTexts : matchTexts.length ? matchTexts : visibleTexts.length ? visibleTexts : texts;
2120
2195
  return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
2121
2196
  }
2122
2197
  return [];
@@ -2145,15 +2220,41 @@ function linkStatusIsAllowed(status, check) {
2145
2220
  const allowed = linkStatusAllowedStatuses(check);
2146
2221
  return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
2147
2222
  }
2223
+ function linkStatusObservedBytes(result) {
2224
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
2225
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
2226
+ const observed = Math.max(bytes || 0, contentLength || 0);
2227
+ return observed > 0 ? observed : undefined;
2228
+ }
2229
+ function normalizeLinkStatusContentType(value) {
2230
+ if (typeof value !== "string") return undefined;
2231
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
2232
+ return normalized || undefined;
2233
+ }
2234
+ function linkStatusContentTypeOk(result, check) {
2235
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
2236
+ const actual = normalizeLinkStatusContentType(result.content_type);
2237
+ if (!actual) return false;
2238
+ return check.allowed_content_types.some((contentType) => {
2239
+ const normalized = normalizeLinkStatusContentType(contentType);
2240
+ if (!normalized) return false;
2241
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
2242
+ return actual === normalized;
2243
+ });
2244
+ }
2148
2245
  function linkStatusResultOk(result, check) {
2149
2246
  if (!result || typeof result !== "object" || Array.isArray(result)) return false;
2150
2247
  if (!linkStatusIsAllowed(result.status, check)) return false;
2151
2248
  if (typeof result.error === "string" && result.error.trim()) return false;
2152
2249
  if (result.ok === false) return false;
2250
+ if (!linkStatusContentTypeOk(result, check)) return false;
2153
2251
  if (check.require_nonzero_bytes === true) {
2154
- const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
2155
- const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
2156
- return (bytes !== undefined && bytes > 0) || (contentLength !== undefined && contentLength > 0);
2252
+ const observedBytes = linkStatusObservedBytes(result);
2253
+ if (observedBytes === undefined || observedBytes <= 0) return false;
2254
+ }
2255
+ if (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) {
2256
+ const observedBytes = linkStatusObservedBytes(result);
2257
+ if (observedBytes === undefined || observedBytes < check.min_bytes) return false;
2157
2258
  }
2158
2259
  return true;
2159
2260
  }
@@ -2181,11 +2282,10 @@ function summarizeLinkStatusEvidence(viewport, check) {
2181
2282
  status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
2182
2283
  method: typeof result.method === "string" ? result.method : null,
2183
2284
  error: typeof result.error === "string" ? result.error : null,
2184
- bytes: typeof result.bytes === "number" && Number.isFinite(result.bytes)
2185
- ? result.bytes
2186
- : typeof result.content_length === "number" && Number.isFinite(result.content_length)
2187
- ? result.content_length
2188
- : null,
2285
+ content_type: typeof result.content_type === "string" ? result.content_type : null,
2286
+ bytes: linkStatusObservedBytes(result) ?? null,
2287
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
2288
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
2189
2289
  }));
2190
2290
  if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
2191
2291
  failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
@@ -2220,6 +2320,8 @@ function summarizeLinkStatusEvidence(viewport, check) {
2220
2320
  failed_count: failures.length,
2221
2321
  truncated: linkEvidence.truncated === true,
2222
2322
  max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
2323
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
2324
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
2223
2325
  status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
2224
2326
  failures: failures.slice(0, 20),
2225
2327
  };
@@ -2754,6 +2856,31 @@ function assessProfile(profile, evidence) {
2754
2856
  });
2755
2857
  continue;
2756
2858
  }
2859
+ if (check.type === "selector_text_visible" || check.type === "selector_text_absent") {
2860
+ const selector = check.selector || "";
2861
+ const expectedVisible = check.type === "selector_text_visible";
2862
+ const results = checkViewports.map((viewport) => {
2863
+ const texts = textSequenceForCheck(viewport, check);
2864
+ const matches = texts.filter((text) => textMatches(text, check));
2865
+ return {
2866
+ viewport: viewport.name,
2867
+ selector_count: viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].count : 0,
2868
+ visible_count: viewport.selectors && viewport.selectors[selector] ? viewport.selectors[selector].visible_count : 0,
2869
+ matched_count: matches.length,
2870
+ matched: matches.length > 0,
2871
+ samples: matches.slice(0, 3).map((text) => text.slice(0, 240)),
2872
+ };
2873
+ });
2874
+ const failed = results.filter((result) => result.matched !== expectedVisible).length;
2875
+ checks.push({
2876
+ type: check.type,
2877
+ label: check.label || check.type,
2878
+ status: failed ? "failed" : "passed",
2879
+ evidence: { selector, text: check.text || null, pattern: check.pattern || null, viewports: results },
2880
+ message: failed ? "Selector " + selector + " text assertion failed in " + failed + " viewport(s)." : undefined,
2881
+ });
2882
+ continue;
2883
+ }
2757
2884
  if (check.type === "selector_text_order") {
2758
2885
  const selector = check.selector || "";
2759
2886
  const expectedTexts = check.expected_texts || [];
@@ -2764,7 +2891,7 @@ function assessProfile(profile, evidence) {
2764
2891
  viewport: viewport.name,
2765
2892
  matched: match.matched,
2766
2893
  matched_positions: match.positions,
2767
- visible_texts: texts.slice(0, 20),
2894
+ visible_texts: texts.slice(0, 20).map((text) => text.slice(0, 240)),
2768
2895
  };
2769
2896
  });
2770
2897
  const failed = results.filter((result) => !result.matched).length;
@@ -2891,6 +3018,8 @@ function assessProfile(profile, evidence) {
2891
3018
  min_count: check.min_count ?? null,
2892
3019
  allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
2893
3020
  require_nonzero_bytes: check.require_nonzero_bytes === true,
3021
+ min_bytes: check.min_bytes ?? null,
3022
+ allowed_content_types: check.allowed_content_types ?? null,
2894
3023
  viewports: summaries,
2895
3024
  failures: failed.flatMap((summary) => Array.isArray(summary.failures)
2896
3025
  ? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
@@ -3968,6 +4097,7 @@ async function selectorStats(selector) {
3968
4097
  async function selectorTextSequence(selector) {
3969
4098
  return page.locator(selector).evaluateAll((elements) => {
3970
4099
  const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
4100
+ const matchText = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 8000);
3971
4101
  const isVisible = (element) => {
3972
4102
  const style = window.getComputedStyle(element);
3973
4103
  const rect = element.getBoundingClientRect();
@@ -3976,6 +4106,7 @@ async function selectorTextSequence(selector) {
3976
4106
  const rows = elements.map((element, index) => ({
3977
4107
  index,
3978
4108
  text: compact(element.innerText || element.textContent || ""),
4109
+ match_text: matchText(element.innerText || element.textContent || ""),
3979
4110
  visible: isVisible(element),
3980
4111
  }));
3981
4112
  return {
@@ -3983,8 +4114,10 @@ async function selectorTextSequence(selector) {
3983
4114
  visible_count: rows.filter((row) => row.visible).length,
3984
4115
  texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
3985
4116
  visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
4117
+ match_texts: rows.map((row) => row.match_text).filter(Boolean).slice(0, 40),
4118
+ visible_match_texts: rows.filter((row) => row.visible).map((row) => row.match_text).filter(Boolean).slice(0, 40),
3986
4119
  };
3987
- }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
4120
+ }).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) }));
3988
4121
  }
3989
4122
  function linkProbeMaxLinks(check) {
3990
4123
  const value = Number(check.max_links || check.maxLinks || check.limit || 100);
@@ -4034,6 +4167,28 @@ function linkProbeAllowed(status, check) {
4034
4167
  : null;
4035
4168
  return allowed ? allowed.includes(status) : status >= 200 && status < 400;
4036
4169
  }
4170
+ function linkProbeObservedBytes(result) {
4171
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
4172
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
4173
+ const observed = Math.max(bytes || 0, contentLength || 0);
4174
+ return observed > 0 ? observed : undefined;
4175
+ }
4176
+ function normalizeLinkProbeContentType(value) {
4177
+ if (typeof value !== "string") return undefined;
4178
+ const normalized = (value.split(";")[0] || "").trim().toLowerCase();
4179
+ return normalized || undefined;
4180
+ }
4181
+ function linkProbeContentTypeAllowed(result, check) {
4182
+ if (!Array.isArray(check.allowed_content_types) || !check.allowed_content_types.length) return true;
4183
+ const actual = normalizeLinkProbeContentType(result.content_type);
4184
+ if (!actual) return false;
4185
+ return check.allowed_content_types.some((contentType) => {
4186
+ const normalized = normalizeLinkProbeContentType(contentType);
4187
+ if (!normalized) return false;
4188
+ if (normalized.endsWith("/*")) return actual.startsWith(normalized.slice(0, -1));
4189
+ return actual === normalized;
4190
+ });
4191
+ }
4037
4192
  function linkProbeResponseFields(response, method) {
4038
4193
  const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
4039
4194
  const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
@@ -4048,6 +4203,7 @@ function linkProbeResponseFields(response, method) {
4048
4203
  }
4049
4204
  async function probeLinkStatus(candidate, check) {
4050
4205
  const requireNonzeroBytes = check.require_nonzero_bytes === true;
4206
+ const minBytes = typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? Math.max(1, Math.floor(check.min_bytes)) : null;
4051
4207
  const allowGetFallback = check.allow_get_fallback !== false;
4052
4208
  const result = {
4053
4209
  url: candidate.url,
@@ -4065,7 +4221,7 @@ async function probeLinkStatus(candidate, check) {
4065
4221
  };
4066
4222
  const applyResponse = async (response, method, readBytes) => {
4067
4223
  Object.assign(result, linkProbeResponseFields(response, method));
4068
- if (readBytes || requireNonzeroBytes) {
4224
+ if (readBytes) {
4069
4225
  try {
4070
4226
  const buffer = await response.arrayBuffer();
4071
4227
  result.bytes = buffer.byteLength;
@@ -4074,7 +4230,9 @@ async function probeLinkStatus(candidate, check) {
4074
4230
  }
4075
4231
  }
4076
4232
  result.ok = linkProbeAllowed(result.status, check)
4077
- && (!requireNonzeroBytes || (typeof result.bytes === "number" ? result.bytes > 0 : typeof result.content_length === "number" && result.content_length > 0))
4233
+ && linkProbeContentTypeAllowed(result, check)
4234
+ && (!requireNonzeroBytes || ((linkProbeObservedBytes(result) || 0) > 0))
4235
+ && (minBytes === null || ((linkProbeObservedBytes(result) || 0) >= minBytes))
4078
4236
  && !result.error;
4079
4237
  };
4080
4238
  try {
@@ -4091,9 +4249,9 @@ async function probeLinkStatus(candidate, check) {
4091
4249
  method: "GET",
4092
4250
  redirect: "follow",
4093
4251
  cache: "no-store",
4094
- headers: { Range: "bytes=0-0" },
4252
+ headers: { Range: "bytes=0-" + String((minBytes || 1) - 1) },
4095
4253
  });
4096
- await applyResponse(response, "GET", requireNonzeroBytes);
4254
+ await applyResponse(response, "GET", requireNonzeroBytes || minBytes !== null);
4097
4255
  return result;
4098
4256
  } catch (error) {
4099
4257
  result.error = String(error && error.message ? error.message : error).slice(0, 500);
@@ -4147,7 +4305,10 @@ async function collectLinkStatus(check) {
4147
4305
  status: result.status,
4148
4306
  method: result.method,
4149
4307
  error: result.error,
4150
- bytes: result.bytes == null ? result.content_length : result.bytes,
4308
+ content_type: result.content_type,
4309
+ bytes: linkProbeObservedBytes(result) || null,
4310
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
4311
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
4151
4312
  }));
4152
4313
  const statusCounts = {};
4153
4314
  for (const result of results) {
@@ -4161,6 +4322,8 @@ async function collectLinkStatus(check) {
4161
4322
  same_origin_only: linkProbeSameOriginOnly(check),
4162
4323
  dedupe: linkProbeDedupe(check),
4163
4324
  require_nonzero_bytes: check.require_nonzero_bytes === true,
4325
+ min_bytes: typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes) ? check.min_bytes : null,
4326
+ allowed_content_types: Array.isArray(check.allowed_content_types) ? check.allowed_content_types : null,
4164
4327
  allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
4165
4328
  ? check.allowed_statuses
4166
4329
  : typeof check.expected_status === "number"
@@ -4720,7 +4883,7 @@ async function captureViewport(viewport) {
4720
4883
  ) {
4721
4884
  selectors[check.selector] = await selectorStats(check.selector);
4722
4885
  }
4723
- if (check.type === "selector_text_order" && check.selector) {
4886
+ if ((check.type === "selector_text_order" || check.type === "selector_text_visible" || check.type === "selector_text_absent") && check.selector) {
4724
4887
  selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
4725
4888
  text_sequences[check.selector] = await selectorTextSequence(check.selector);
4726
4889
  }
@@ -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", "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_url_equals", "frame_url_matches", "frame_no_horizontal_overflow", "text_visible", "text_absent", "link_status", "artifact_link_status", "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_visible", "selector_text_absent", "selector_text_order", "frame_text_visible", "frame_url_equals", "frame_url_matches", "frame_no_horizontal_overflow", "text_visible", "text_absent", "link_status", "artifact_link_status", "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", "drag", "press", "fill", "set_input_value", "assert_text_visible", "assert_text_absent", "assert_selector_count", "assert_window_value", "assert_window_number", "local_storage", "session_storage", "clear_storage", "clear_console", "dialog_response", "screenshot", "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];
@@ -138,6 +138,8 @@ interface RiddleProofProfileCheck {
138
138
  same_origin_only?: boolean;
139
139
  dedupe?: boolean;
140
140
  require_nonzero_bytes?: boolean;
141
+ min_bytes?: number;
142
+ allowed_content_types?: string[];
141
143
  allow_get_fallback?: boolean;
142
144
  max_overflow_px?: number;
143
145
  timeout_ms?: number;
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", "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_url_equals", "frame_url_matches", "frame_no_horizontal_overflow", "text_visible", "text_absent", "link_status", "artifact_link_status", "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_visible", "selector_text_absent", "selector_text_order", "frame_text_visible", "frame_url_equals", "frame_url_matches", "frame_no_horizontal_overflow", "text_visible", "text_absent", "link_status", "artifact_link_status", "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", "drag", "press", "fill", "set_input_value", "assert_text_visible", "assert_text_absent", "assert_selector_count", "assert_window_value", "assert_window_number", "local_storage", "session_storage", "clear_storage", "clear_console", "dialog_response", "screenshot", "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];
@@ -138,6 +138,8 @@ interface RiddleProofProfileCheck {
138
138
  same_origin_only?: boolean;
139
139
  dedupe?: boolean;
140
140
  require_nonzero_bytes?: boolean;
141
+ min_bytes?: number;
142
+ allowed_content_types?: string[];
141
143
  allow_get_fallback?: boolean;
142
144
  max_overflow_px?: number;
143
145
  timeout_ms?: number;
package/dist/profile.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  resolveRiddleProofProfileTimeoutSec,
20
20
  slugifyRiddleProofProfileName,
21
21
  summarizeRiddleProofProfileResult
22
- } from "./chunk-QLBOFUON.js";
22
+ } from "./chunk-5OCJKEHE.js";
23
23
  export {
24
24
  RIDDLE_PROOF_PROFILE_CHECK_TYPES,
25
25
  RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION,
@@ -292,7 +292,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
292
292
  blocking?: boolean;
293
293
  details?: Record<string, unknown>;
294
294
  ok: boolean;
295
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
295
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
296
296
  state_path: string;
297
297
  stage: any;
298
298
  summary: string;
@@ -382,7 +382,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
382
382
  continueWithStage?: WorkflowStage | null;
383
383
  blocking?: boolean;
384
384
  details?: Record<string, unknown>;
385
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
385
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
386
386
  state_path: string;
387
387
  stage: any;
388
388
  checkpoint: string;
@@ -659,7 +659,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
659
659
  error?: undefined;
660
660
  } | {
661
661
  ok: boolean;
662
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
662
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
663
663
  state_path: string;
664
664
  stage: any;
665
665
  summary: string;
@@ -292,7 +292,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
292
292
  blocking?: boolean;
293
293
  details?: Record<string, unknown>;
294
294
  ok: boolean;
295
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
295
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
296
296
  state_path: string;
297
297
  stage: any;
298
298
  summary: string;
@@ -382,7 +382,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
382
382
  continueWithStage?: WorkflowStage | null;
383
383
  blocking?: boolean;
384
384
  details?: Record<string, unknown>;
385
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
385
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
386
386
  state_path: string;
387
387
  stage: any;
388
388
  checkpoint: string;
@@ -659,7 +659,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
659
659
  error?: undefined;
660
660
  } | {
661
661
  ok: boolean;
662
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
662
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
663
663
  state_path: string;
664
664
  stage: any;
665
665
  summary: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.7.93",
3
+ "version": "0.7.95",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",