@riddledc/riddle-proof 0.7.89 → 0.7.91

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/index.cjs CHANGED
@@ -8755,6 +8755,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
8755
8755
  "frame_no_horizontal_overflow",
8756
8756
  "text_visible",
8757
8757
  "text_absent",
8758
+ "link_status",
8759
+ "artifact_link_status",
8758
8760
  "route_inventory",
8759
8761
  "no_horizontal_overflow",
8760
8762
  "no_mobile_horizontal_overflow",
@@ -9419,6 +9421,29 @@ function normalizeStringList(value, label) {
9419
9421
  if (!values.length) throw new Error(`${label} must contain non-empty strings.`);
9420
9422
  return values;
9421
9423
  }
9424
+ function normalizeHttpStatus(value, label) {
9425
+ if (value === void 0) return void 0;
9426
+ const status = numberValue3(value);
9427
+ if (status === void 0 || !Number.isInteger(status) || status < 100 || status > 599) {
9428
+ throw new Error(`${label} must be an HTTP status code.`);
9429
+ }
9430
+ return status;
9431
+ }
9432
+ function normalizeHttpStatuses(value, label) {
9433
+ if (value === void 0) return void 0;
9434
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
9435
+ if (!value.length) throw new Error(`${label} must not be empty.`);
9436
+ const statuses = value.map((item, index) => normalizeHttpStatus(item, `${label}[${index}]`));
9437
+ return Array.from(new Set(statuses));
9438
+ }
9439
+ function normalizePositiveInteger(value, label, max) {
9440
+ if (value === void 0) return void 0;
9441
+ const number = numberValue3(value);
9442
+ if (number === void 0 || !Number.isInteger(number) || number < 1 || max !== void 0 && number > max) {
9443
+ throw new Error(`${label} must be an integer from 1 to ${max ?? "unbounded"}.`);
9444
+ }
9445
+ return number;
9446
+ }
9422
9447
  function validateRegexPatterns(patterns, label) {
9423
9448
  for (const pattern of patterns || []) {
9424
9449
  try {
@@ -9462,7 +9487,8 @@ function normalizeCheck(input, index) {
9462
9487
  if (type === "url_search_param_equals" && expectedValue === void 0) {
9463
9488
  throw new Error(`checks[${index}] url_search_param_equals requires expected_value.`);
9464
9489
  }
9465
- if (type === "selector_count_at_least" && numberValue3(input.min_count) === void 0) {
9490
+ const minCount = numberValue3(input.min_count) ?? numberValue3(input.minCount);
9491
+ if (type === "selector_count_at_least" && minCount === void 0) {
9466
9492
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
9467
9493
  }
9468
9494
  const expectedCount = numberValue3(input.expected_count) ?? numberValue3(input.expectedCount) ?? numberValue3(input.count);
@@ -9478,6 +9504,21 @@ function normalizeCheck(input, index) {
9478
9504
  if (type === "route_inventory" && !expectedRoutes?.length) {
9479
9505
  throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
9480
9506
  }
9507
+ const isLinkStatusCheck = type === "link_status" || type === "artifact_link_status";
9508
+ const expectedStatus = isLinkStatusCheck ? normalizeHttpStatus(input.expected_status ?? input.expectedStatus ?? input.status, `checks[${index}] expected_status`) : void 0;
9509
+ const allowedStatuses = isLinkStatusCheck ? normalizeHttpStatuses(
9510
+ input.allowed_statuses ?? input.allowedStatuses ?? input.expected_statuses ?? input.expectedStatuses,
9511
+ `checks[${index}] allowed_statuses`
9512
+ ) : void 0;
9513
+ const maxLinks = isLinkStatusCheck ? normalizePositiveInteger(input.max_links ?? input.maxLinks ?? input.limit, `checks[${index}] max_links`, 500) : void 0;
9514
+ if (isLinkStatusCheck) {
9515
+ if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
9516
+ throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
9517
+ }
9518
+ if (expectedCount !== void 0 && (!Number.isInteger(expectedCount) || expectedCount < 0)) {
9519
+ throw new Error(`checks[${index}] ${type} expected_count must be a non-negative integer.`);
9520
+ }
9521
+ }
9481
9522
  return {
9482
9523
  type,
9483
9524
  label: stringValue5(input.label),
@@ -9503,8 +9544,15 @@ function normalizeCheck(input, index) {
9503
9544
  allowed_console_patterns: normalizeStringList(input.allowed_console_patterns ?? input.allowedConsolePatterns ?? input.allow_console_patterns ?? input.allowConsolePatterns, `checks[${index}] allowed_console_patterns`),
9504
9545
  allowed_page_error_texts: normalizeStringList(input.allowed_page_error_texts ?? input.allowedPageErrorTexts ?? input.allow_page_error_texts ?? input.allowPageErrorTexts, `checks[${index}] allowed_page_error_texts`),
9505
9546
  allowed_page_error_patterns: normalizeStringList(input.allowed_page_error_patterns ?? input.allowedPageErrorPatterns ?? input.allow_page_error_patterns ?? input.allowPageErrorPatterns, `checks[${index}] allowed_page_error_patterns`),
9506
- min_count: numberValue3(input.min_count),
9547
+ min_count: minCount,
9507
9548
  expected_count: expectedCount,
9549
+ expected_status: expectedStatus,
9550
+ allowed_statuses: allowedStatuses,
9551
+ max_links: maxLinks,
9552
+ same_origin_only: isLinkStatusCheck ? input.same_origin_only === true || input.sameOriginOnly === true : void 0,
9553
+ dedupe: isLinkStatusCheck ? input.dedupe === false ? false : true : void 0,
9554
+ require_nonzero_bytes: isLinkStatusCheck ? input.require_nonzero_bytes === true || input.requireNonzeroBytes === true : void 0,
9555
+ allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
9508
9556
  max_overflow_px: numberValue3(input.max_overflow_px),
9509
9557
  timeout_ms: numberValue3(input.timeout_ms) ?? numberValue3(input.timeoutMs),
9510
9558
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
@@ -9597,6 +9645,97 @@ function checkLabel(check) {
9597
9645
  function selectorKey(check) {
9598
9646
  return check.selector || "";
9599
9647
  }
9648
+ function linkStatusSelector(check) {
9649
+ return check.selector || check.link_selector || "a[href]";
9650
+ }
9651
+ function linkStatusAllowedStatuses(check) {
9652
+ if (check.allowed_statuses?.length) return check.allowed_statuses;
9653
+ if (check.expected_status !== void 0) return [check.expected_status];
9654
+ return void 0;
9655
+ }
9656
+ function linkStatusIsAllowed(status, check) {
9657
+ if (status === void 0) return false;
9658
+ const allowed = linkStatusAllowedStatuses(check);
9659
+ return allowed?.length ? allowed.includes(status) : status >= 200 && status < 400;
9660
+ }
9661
+ function linkStatusResultOk(result, check) {
9662
+ const status = numberValue3(result.status);
9663
+ if (!linkStatusIsAllowed(status, check)) return false;
9664
+ if (stringValue5(result.error)) return false;
9665
+ if (result.ok === false) return false;
9666
+ if (check.require_nonzero_bytes) {
9667
+ const bytes = numberValue3(result.bytes);
9668
+ const contentLength = numberValue3(result.content_length);
9669
+ return bytes !== void 0 && bytes > 0 || contentLength !== void 0 && contentLength > 0;
9670
+ }
9671
+ return true;
9672
+ }
9673
+ function linkStatusEvidenceForCheck(viewport, check) {
9674
+ const evidence = viewport.link_statuses?.[linkStatusSelector(check)];
9675
+ return isRecord2(evidence) ? evidence : void 0;
9676
+ }
9677
+ function summarizeLinkStatusEvidence(viewport, check) {
9678
+ const linkEvidence = linkStatusEvidenceForCheck(viewport, check);
9679
+ if (!linkEvidence) {
9680
+ return {
9681
+ viewport: viewport.name,
9682
+ selector: linkStatusSelector(check),
9683
+ total_count: 0,
9684
+ ok_count: 0,
9685
+ failed_count: 1,
9686
+ failures: [{ code: "link_status_evidence_missing" }]
9687
+ };
9688
+ }
9689
+ const results = Array.isArray(linkEvidence.results) ? linkEvidence.results.filter(isRecord2) : [];
9690
+ const totalCount = numberValue3(linkEvidence.total_count) ?? results.length;
9691
+ const okCount = results.filter((result) => linkStatusResultOk(result, check)).length;
9692
+ const failures = results.filter((result) => !linkStatusResultOk(result, check)).map((result) => ({
9693
+ code: "link_status_failed",
9694
+ url: stringValue5(result.url) ?? null,
9695
+ status: numberValue3(result.status) ?? null,
9696
+ method: stringValue5(result.method) ?? null,
9697
+ error: stringValue5(result.error) ?? null,
9698
+ bytes: numberValue3(result.bytes) ?? numberValue3(result.content_length) ?? null
9699
+ }));
9700
+ if (stringValue5(linkEvidence.error)) {
9701
+ failures.push({ code: "link_status_capture_failed", error: stringValue5(linkEvidence.error) ?? "" });
9702
+ }
9703
+ const recordedFailedCount = numberValue3(linkEvidence.failed_count) ?? 0;
9704
+ if (!failures.length && recordedFailedCount > 0) {
9705
+ const recordedFailures = Array.isArray(linkEvidence.failures) ? linkEvidence.failures.slice(0, 20) : [];
9706
+ failures.push({
9707
+ code: "link_status_recorded_failures",
9708
+ failed_count: recordedFailedCount,
9709
+ failures: toJsonValue(recordedFailures)
9710
+ });
9711
+ }
9712
+ if (linkEvidence.truncated === true) {
9713
+ failures.push({
9714
+ code: "link_status_probe_truncated",
9715
+ discovered_count: numberValue3(linkEvidence.discovered_count) ?? totalCount,
9716
+ max_links: numberValue3(linkEvidence.max_links) ?? check.max_links ?? 100
9717
+ });
9718
+ }
9719
+ if (check.expected_count !== void 0 && totalCount !== check.expected_count) {
9720
+ failures.push({ code: "link_status_count_mismatch", expected: check.expected_count, actual: totalCount });
9721
+ }
9722
+ if (check.min_count !== void 0 && totalCount < check.min_count) {
9723
+ failures.push({ code: "link_status_count_below_minimum", min_count: check.min_count, actual: totalCount });
9724
+ }
9725
+ const statusCounts = isRecord2(linkEvidence.status_counts) ? linkEvidence.status_counts : {};
9726
+ return {
9727
+ viewport: viewport.name,
9728
+ selector: linkStatusSelector(check),
9729
+ total_count: totalCount,
9730
+ discovered_count: numberValue3(linkEvidence.discovered_count) ?? totalCount,
9731
+ ok_count: numberValue3(linkEvidence.ok_count) ?? okCount,
9732
+ failed_count: failures.length,
9733
+ truncated: linkEvidence.truncated === true,
9734
+ max_links: numberValue3(linkEvidence.max_links) ?? check.max_links ?? 100,
9735
+ status_counts: statusCounts,
9736
+ failures: failures.slice(0, 20)
9737
+ };
9738
+ }
9600
9739
  function textKey(check) {
9601
9740
  return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
9602
9741
  }
@@ -10076,6 +10215,26 @@ function assessCheckFromEvidence(check, evidence) {
10076
10215
  message: failed ? `Text assertion failed in ${failed} viewport(s).` : void 0
10077
10216
  };
10078
10217
  }
10218
+ if (check.type === "link_status" || check.type === "artifact_link_status") {
10219
+ const selector = linkStatusSelector(check);
10220
+ const summaries = viewports.map((viewport) => summarizeLinkStatusEvidence(viewport, check));
10221
+ const failed = summaries.filter((summary) => (numberValue3(summary.failed_count) ?? 0) > 0);
10222
+ return {
10223
+ type: check.type,
10224
+ label: checkLabel(check),
10225
+ status: failed.length ? "failed" : "passed",
10226
+ evidence: {
10227
+ selector,
10228
+ expected_count: check.expected_count ?? null,
10229
+ min_count: check.min_count ?? null,
10230
+ allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
10231
+ require_nonzero_bytes: check.require_nonzero_bytes === true,
10232
+ viewports: summaries.map((summary) => toJsonValue(summary)),
10233
+ failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue5(summary.viewport) ?? null, failure })) : [])
10234
+ },
10235
+ message: failed.length ? `Link status failed in ${failed.length} viewport(s).` : void 0
10236
+ };
10237
+ }
10079
10238
  if (check.type === "route_inventory") {
10080
10239
  const inventories = viewports.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory })).filter((item) => isRecord2(item.inventory));
10081
10240
  if (!inventories.length) {
@@ -10658,6 +10817,98 @@ function textOrderMatch(texts, expectedTexts) {
10658
10817
  }
10659
10818
  return { matched: true, positions };
10660
10819
  }
10820
+ function linkStatusSelector(check) {
10821
+ return check.selector || check.link_selector || "a[href]";
10822
+ }
10823
+ function linkStatusAllowedStatuses(check) {
10824
+ if (Array.isArray(check.allowed_statuses) && check.allowed_statuses.length) return check.allowed_statuses;
10825
+ if (typeof check.expected_status === "number" && Number.isFinite(check.expected_status)) return [check.expected_status];
10826
+ return undefined;
10827
+ }
10828
+ function linkStatusIsAllowed(status, check) {
10829
+ if (typeof status !== "number" || !Number.isFinite(status)) return false;
10830
+ const allowed = linkStatusAllowedStatuses(check);
10831
+ return Array.isArray(allowed) && allowed.length ? allowed.includes(status) : status >= 200 && status < 400;
10832
+ }
10833
+ function linkStatusResultOk(result, check) {
10834
+ if (!result || typeof result !== "object" || Array.isArray(result)) return false;
10835
+ if (!linkStatusIsAllowed(result.status, check)) return false;
10836
+ if (typeof result.error === "string" && result.error.trim()) return false;
10837
+ if (result.ok === false) return false;
10838
+ if (check.require_nonzero_bytes === true) {
10839
+ const bytes = typeof result.bytes === "number" && Number.isFinite(result.bytes) ? result.bytes : undefined;
10840
+ const contentLength = typeof result.content_length === "number" && Number.isFinite(result.content_length) ? result.content_length : undefined;
10841
+ return (bytes !== undefined && bytes > 0) || (contentLength !== undefined && contentLength > 0);
10842
+ }
10843
+ return true;
10844
+ }
10845
+ function summarizeLinkStatusEvidence(viewport, check) {
10846
+ const selector = linkStatusSelector(check);
10847
+ const linkEvidence = viewport && viewport.link_statuses && viewport.link_statuses[selector];
10848
+ if (!linkEvidence || typeof linkEvidence !== "object" || Array.isArray(linkEvidence)) {
10849
+ return {
10850
+ viewport: viewport && viewport.name,
10851
+ selector,
10852
+ total_count: 0,
10853
+ ok_count: 0,
10854
+ failed_count: 1,
10855
+ failures: [{ code: "link_status_evidence_missing" }],
10856
+ };
10857
+ }
10858
+ const results = Array.isArray(linkEvidence.results) ? linkEvidence.results.filter((result) => result && typeof result === "object" && !Array.isArray(result)) : [];
10859
+ const totalCount = typeof linkEvidence.total_count === "number" && Number.isFinite(linkEvidence.total_count) ? linkEvidence.total_count : results.length;
10860
+ const okCount = results.filter((result) => linkStatusResultOk(result, check)).length;
10861
+ const failures = results
10862
+ .filter((result) => !linkStatusResultOk(result, check))
10863
+ .map((result) => ({
10864
+ code: "link_status_failed",
10865
+ url: typeof result.url === "string" ? result.url : null,
10866
+ status: typeof result.status === "number" && Number.isFinite(result.status) ? result.status : null,
10867
+ method: typeof result.method === "string" ? result.method : null,
10868
+ error: typeof result.error === "string" ? result.error : null,
10869
+ bytes: typeof result.bytes === "number" && Number.isFinite(result.bytes)
10870
+ ? result.bytes
10871
+ : typeof result.content_length === "number" && Number.isFinite(result.content_length)
10872
+ ? result.content_length
10873
+ : null,
10874
+ }));
10875
+ if (typeof linkEvidence.error === "string" && linkEvidence.error.trim()) {
10876
+ failures.push({ code: "link_status_capture_failed", error: linkEvidence.error });
10877
+ }
10878
+ const recordedFailedCount = typeof linkEvidence.failed_count === "number" && Number.isFinite(linkEvidence.failed_count) ? linkEvidence.failed_count : 0;
10879
+ if (!failures.length && recordedFailedCount > 0) {
10880
+ failures.push({
10881
+ code: "link_status_recorded_failures",
10882
+ failed_count: recordedFailedCount,
10883
+ failures: Array.isArray(linkEvidence.failures) ? linkEvidence.failures.slice(0, 20) : [],
10884
+ });
10885
+ }
10886
+ if (linkEvidence.truncated === true) {
10887
+ failures.push({
10888
+ code: "link_status_probe_truncated",
10889
+ discovered_count: typeof linkEvidence.discovered_count === "number" ? linkEvidence.discovered_count : totalCount,
10890
+ max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
10891
+ });
10892
+ }
10893
+ if (typeof check.expected_count === "number" && Number.isFinite(check.expected_count) && totalCount !== check.expected_count) {
10894
+ failures.push({ code: "link_status_count_mismatch", expected: check.expected_count, actual: totalCount });
10895
+ }
10896
+ if (typeof check.min_count === "number" && Number.isFinite(check.min_count) && totalCount < check.min_count) {
10897
+ failures.push({ code: "link_status_count_below_minimum", min_count: check.min_count, actual: totalCount });
10898
+ }
10899
+ return {
10900
+ viewport: viewport && viewport.name,
10901
+ selector,
10902
+ total_count: totalCount,
10903
+ discovered_count: typeof linkEvidence.discovered_count === "number" ? linkEvidence.discovered_count : totalCount,
10904
+ ok_count: typeof linkEvidence.ok_count === "number" ? linkEvidence.ok_count : okCount,
10905
+ failed_count: failures.length,
10906
+ truncated: linkEvidence.truncated === true,
10907
+ max_links: typeof linkEvidence.max_links === "number" ? linkEvidence.max_links : check.max_links || 100,
10908
+ status_counts: linkEvidence.status_counts && typeof linkEvidence.status_counts === "object" && !Array.isArray(linkEvidence.status_counts) ? linkEvidence.status_counts : {},
10909
+ failures: failures.slice(0, 20),
10910
+ };
10911
+ }
10661
10912
  function frameEvidenceForSelector(viewport, selector) {
10662
10913
  const container = viewport.frames && viewport.frames[selector || ""];
10663
10914
  if (!container || typeof container !== "object" || Array.isArray(container)) return [];
@@ -11311,6 +11562,29 @@ function assessProfile(profile, evidence) {
11311
11562
  });
11312
11563
  continue;
11313
11564
  }
11565
+ if (check.type === "link_status" || check.type === "artifact_link_status") {
11566
+ const selector = linkStatusSelector(check);
11567
+ const summaries = checkViewports.map((viewport) => summarizeLinkStatusEvidence(viewport, check));
11568
+ const failed = summaries.filter((summary) => summary.failed_count > 0);
11569
+ checks.push({
11570
+ type: check.type,
11571
+ label: check.label || check.type,
11572
+ status: failed.length ? "failed" : "passed",
11573
+ evidence: {
11574
+ selector,
11575
+ expected_count: check.expected_count ?? null,
11576
+ min_count: check.min_count ?? null,
11577
+ allowed_statuses: linkStatusAllowedStatuses(check) || ["2xx", "3xx"],
11578
+ require_nonzero_bytes: check.require_nonzero_bytes === true,
11579
+ viewports: summaries,
11580
+ failures: failed.flatMap((summary) => Array.isArray(summary.failures)
11581
+ ? summary.failures.map((failure) => ({ viewport: summary.viewport || null, failure }))
11582
+ : []),
11583
+ },
11584
+ message: failed.length ? "Link status failed in " + failed.length + " viewport(s)." : undefined,
11585
+ });
11586
+ continue;
11587
+ }
11314
11588
  if (check.type === "route_inventory") {
11315
11589
  const inventories = checkViewports
11316
11590
  .map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory }))
@@ -11551,6 +11825,10 @@ function textMatches(sample, check) {
11551
11825
  }
11552
11826
  return String(sample || "").includes(check.text || "");
11553
11827
  }
11828
+ function profileCheckAppliesToViewport(check, viewport) {
11829
+ if (!Array.isArray(check.viewports) || !check.viewports.length) return true;
11830
+ return Boolean(viewport && viewport.name && check.viewports.includes(viewport.name));
11831
+ }
11554
11832
  function setupActionType(action) {
11555
11833
  return String(action && action.type ? action.type : "").replace(/-/g, "_");
11556
11834
  }
@@ -11568,6 +11846,30 @@ function setupTextMatches(sample, action) {
11568
11846
  }
11569
11847
  return String(sample || "").includes(action.text || "");
11570
11848
  }
11849
+ async function waitForAnyVisibleSelector(context, selector, timeout) {
11850
+ const deadline = Date.now() + setupNumber(timeout, 15000);
11851
+ let lastReason = "selector_not_found";
11852
+ while (Date.now() <= deadline) {
11853
+ try {
11854
+ const locator = context.locator(selector);
11855
+ const count = await locator.count();
11856
+ if (!count) {
11857
+ lastReason = "selector_not_found";
11858
+ } else {
11859
+ lastReason = "no_visible_match";
11860
+ for (let index = 0; index < count; index += 1) {
11861
+ if (await locator.nth(index).isVisible().catch(() => false)) {
11862
+ return { ok: true, count, index };
11863
+ }
11864
+ }
11865
+ }
11866
+ } catch (error) {
11867
+ lastReason = String(error && error.message ? error.message : error).slice(0, 500);
11868
+ }
11869
+ await page.waitForTimeout(200);
11870
+ }
11871
+ throw new Error("No visible match for selector " + selector + ": " + lastReason);
11872
+ }
11571
11873
  function setupHasOwn(action, key) {
11572
11874
  return Boolean(action) && Object.keys(action).includes(key);
11573
11875
  }
@@ -11901,7 +12203,7 @@ async function executeSetupAction(action, ordinal, viewport) {
11901
12203
  if (type === "wait_for_selector") {
11902
12204
  const scope = await setupActionScope(action, timeout);
11903
12205
  if (!scope.ok) return setupScopeFailure(base, scope);
11904
- await scope.context.waitForSelector(action.selector, { state: "visible", timeout });
12206
+ await waitForAnyVisibleSelector(scope.context, action.selector, timeout);
11905
12207
  return { ...base, ...setupScopeEvidence(scope), ok: true, timeout_ms: timeout };
11906
12208
  }
11907
12209
  if (type === "drag") {
@@ -12369,6 +12671,196 @@ async function selectorTextSequence(selector) {
12369
12671
  };
12370
12672
  }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
12371
12673
  }
12674
+ function linkProbeMaxLinks(check) {
12675
+ const value = Number(check.max_links || check.maxLinks || check.limit || 100);
12676
+ return Number.isInteger(value) && value > 0 ? Math.min(value, 500) : 100;
12677
+ }
12678
+ function linkProbeDedupe(check) {
12679
+ return check.dedupe !== false;
12680
+ }
12681
+ function linkProbeSameOriginOnly(check) {
12682
+ return check.same_origin_only === true;
12683
+ }
12684
+ async function collectLinkCandidates(selector) {
12685
+ return page.locator(selector).evaluateAll((elements) => {
12686
+ const compact = (value, limit) => String(value || "").replace(/\s+/g, " ").trim().slice(0, limit || 160);
12687
+ return elements.map((element, index) => {
12688
+ const tag = element && element.tagName ? element.tagName.toLowerCase() : "element";
12689
+ const href = element && typeof element.href === "string" ? element.href : "";
12690
+ const src = element && typeof element.src === "string" ? element.src : "";
12691
+ const currentSrc = element && typeof element.currentSrc === "string" ? element.currentSrc : "";
12692
+ const attrHref = element && typeof element.getAttribute === "function" ? element.getAttribute("href") || "" : "";
12693
+ const attrSrc = element && typeof element.getAttribute === "function" ? element.getAttribute("src") || "" : "";
12694
+ const attrPoster = element && typeof element.getAttribute === "function" ? element.getAttribute("poster") || "" : "";
12695
+ const raw = href || currentSrc || src || attrHref || attrSrc || attrPoster;
12696
+ if (!raw) return null;
12697
+ let url = "";
12698
+ try {
12699
+ url = new URL(raw, location.href).href;
12700
+ } catch {}
12701
+ if (!url) return null;
12702
+ return {
12703
+ index,
12704
+ tag,
12705
+ url,
12706
+ raw,
12707
+ text: compact(element.innerText || element.textContent || "", 160),
12708
+ alt: compact(element.getAttribute && element.getAttribute("alt"), 80),
12709
+ };
12710
+ }).filter(Boolean);
12711
+ }).catch((error) => ({ error: String(error && error.message ? error.message : error).slice(0, 500) }));
12712
+ }
12713
+ function linkProbeAllowed(status, check) {
12714
+ if (typeof status !== "number" || !Number.isFinite(status)) return false;
12715
+ const allowed = Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
12716
+ ? check.allowed_statuses
12717
+ : typeof check.expected_status === "number" && Number.isFinite(check.expected_status)
12718
+ ? [check.expected_status]
12719
+ : null;
12720
+ return allowed ? allowed.includes(status) : status >= 200 && status < 400;
12721
+ }
12722
+ function linkProbeResponseFields(response, method) {
12723
+ const contentLengthHeader = response.headers && typeof response.headers.get === "function" ? response.headers.get("content-length") : null;
12724
+ const contentLength = contentLengthHeader && /^\d+$/.test(contentLengthHeader) ? Number(contentLengthHeader) : null;
12725
+ return {
12726
+ method,
12727
+ status: response.status,
12728
+ redirected: Boolean(response.redirected),
12729
+ final_url: response.url || null,
12730
+ content_type: response.headers && typeof response.headers.get === "function" ? response.headers.get("content-type") : null,
12731
+ content_length: contentLength,
12732
+ };
12733
+ }
12734
+ async function probeLinkStatus(candidate, check) {
12735
+ const requireNonzeroBytes = check.require_nonzero_bytes === true;
12736
+ const allowGetFallback = check.allow_get_fallback !== false;
12737
+ const result = {
12738
+ url: candidate.url,
12739
+ tag: candidate.tag,
12740
+ text: candidate.text || null,
12741
+ status: null,
12742
+ method: null,
12743
+ ok: false,
12744
+ content_type: null,
12745
+ content_length: null,
12746
+ bytes: null,
12747
+ redirected: false,
12748
+ final_url: null,
12749
+ error: null,
12750
+ };
12751
+ const applyResponse = async (response, method, readBytes) => {
12752
+ Object.assign(result, linkProbeResponseFields(response, method));
12753
+ if (readBytes || requireNonzeroBytes) {
12754
+ try {
12755
+ const buffer = await response.arrayBuffer();
12756
+ result.bytes = buffer.byteLength;
12757
+ } catch (error) {
12758
+ result.error = String(error && error.message ? error.message : error).slice(0, 500);
12759
+ }
12760
+ }
12761
+ result.ok = linkProbeAllowed(result.status, check)
12762
+ && (!requireNonzeroBytes || (typeof result.bytes === "number" ? result.bytes > 0 : typeof result.content_length === "number" && result.content_length > 0))
12763
+ && !result.error;
12764
+ };
12765
+ try {
12766
+ const response = await fetch(candidate.url, { method: "HEAD", redirect: "follow", cache: "no-store" });
12767
+ await applyResponse(response, "HEAD", false);
12768
+ if (result.ok || !allowGetFallback) return result;
12769
+ } catch (error) {
12770
+ result.error = String(error && error.message ? error.message : error).slice(0, 500);
12771
+ if (!allowGetFallback) return result;
12772
+ }
12773
+ try {
12774
+ result.error = null;
12775
+ const response = await fetch(candidate.url, {
12776
+ method: "GET",
12777
+ redirect: "follow",
12778
+ cache: "no-store",
12779
+ headers: { Range: "bytes=0-0" },
12780
+ });
12781
+ await applyResponse(response, "GET", requireNonzeroBytes);
12782
+ return result;
12783
+ } catch (error) {
12784
+ result.error = String(error && error.message ? error.message : error).slice(0, 500);
12785
+ result.ok = false;
12786
+ return result;
12787
+ }
12788
+ }
12789
+ async function collectLinkStatus(check) {
12790
+ const selector = linkStatusSelector(check);
12791
+ const candidateResult = await collectLinkCandidates(selector);
12792
+ if (candidateResult && candidateResult.error) {
12793
+ return {
12794
+ version: "riddle-proof.link-status.v1",
12795
+ selector,
12796
+ total_count: 0,
12797
+ discovered_count: 0,
12798
+ ok_count: 0,
12799
+ failed_count: 1,
12800
+ failures: [{ code: "link_status_capture_failed", error: candidateResult.error }],
12801
+ results: [],
12802
+ status_counts: {},
12803
+ error: candidateResult.error,
12804
+ };
12805
+ }
12806
+ const baseUrl = page.url() || targetUrl;
12807
+ let candidates = Array.isArray(candidateResult) ? candidateResult : [];
12808
+ if (linkProbeSameOriginOnly(check)) {
12809
+ const origin = new URL(baseUrl).origin;
12810
+ candidates = candidates.filter((candidate) => {
12811
+ try { return new URL(candidate.url).origin === origin; } catch { return false; }
12812
+ });
12813
+ }
12814
+ const discoveredCount = candidates.length;
12815
+ if (linkProbeDedupe(check)) {
12816
+ const seen = new Set();
12817
+ candidates = candidates.filter((candidate) => {
12818
+ if (seen.has(candidate.url)) return false;
12819
+ seen.add(candidate.url);
12820
+ return true;
12821
+ });
12822
+ }
12823
+ const maxLinks = linkProbeMaxLinks(check);
12824
+ const selected = candidates.slice(0, maxLinks);
12825
+ const results = [];
12826
+ for (const candidate of selected) {
12827
+ results.push(await probeLinkStatus(candidate, check));
12828
+ }
12829
+ const failures = results.filter((result) => !result.ok).map((result) => ({
12830
+ code: "link_status_failed",
12831
+ url: result.url,
12832
+ status: result.status,
12833
+ method: result.method,
12834
+ error: result.error,
12835
+ bytes: result.bytes == null ? result.content_length : result.bytes,
12836
+ }));
12837
+ const statusCounts = {};
12838
+ for (const result of results) {
12839
+ const key = result.status == null ? "error" : String(result.status);
12840
+ statusCounts[key] = (statusCounts[key] || 0) + 1;
12841
+ }
12842
+ return {
12843
+ version: "riddle-proof.link-status.v1",
12844
+ selector,
12845
+ max_links: maxLinks,
12846
+ same_origin_only: linkProbeSameOriginOnly(check),
12847
+ dedupe: linkProbeDedupe(check),
12848
+ require_nonzero_bytes: check.require_nonzero_bytes === true,
12849
+ allowed_statuses: Array.isArray(check.allowed_statuses) && check.allowed_statuses.length
12850
+ ? check.allowed_statuses
12851
+ : typeof check.expected_status === "number"
12852
+ ? [check.expected_status]
12853
+ : ["2xx", "3xx"],
12854
+ discovered_count: discoveredCount,
12855
+ total_count: selected.length,
12856
+ truncated: candidates.length > selected.length,
12857
+ ok_count: results.filter((result) => result.ok).length,
12858
+ failed_count: failures.length,
12859
+ status_counts: statusCounts,
12860
+ failures: failures.slice(0, 20),
12861
+ results,
12862
+ };
12863
+ }
12372
12864
  async function frameEvidence(selector) {
12373
12865
  const result = { selector, count: 0, frame_count: 0, frames: [], errors: [] };
12374
12866
  let handles = [];
@@ -12727,7 +13219,7 @@ async function collectRouteInventory(check, viewport) {
12727
13219
  try {
12728
13220
  await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 90000 });
12729
13221
  if (profile.target.wait_for_selector) {
12730
- await page.waitForSelector(profile.target.wait_for_selector, { state: "visible", timeout: 15000 }).catch(() => {});
13222
+ await waitForAnyVisibleSelector(page, profile.target.wait_for_selector, 15000).catch(() => {});
12731
13223
  }
12732
13224
  const index = await findInventoryLinkIndex(check, expectedRoute.path);
12733
13225
  if (index < 0) {
@@ -12786,7 +13278,7 @@ async function captureViewport(viewport) {
12786
13278
  }
12787
13279
  if (!navigationError && profile.target.wait_for_selector) {
12788
13280
  try {
12789
- await page.waitForSelector(profile.target.wait_for_selector, { state: "visible", timeout: 15000 });
13281
+ await waitForAnyVisibleSelector(page, profile.target.wait_for_selector, 15000);
12790
13282
  } catch (error) {
12791
13283
  waitError = String(error && error.message ? error.message : error).slice(0, 1000);
12792
13284
  }
@@ -12888,7 +13380,9 @@ async function captureViewport(viewport) {
12888
13380
  const frames = {};
12889
13381
  const text_sequences = {};
12890
13382
  const text_matches = {};
13383
+ const link_statuses = {};
12891
13384
  for (const check of profile.checks || []) {
13385
+ if (!profileCheckAppliesToViewport(check, viewport)) continue;
12892
13386
  if (
12893
13387
  (
12894
13388
  check.type === "selector_visible"
@@ -12912,6 +13406,10 @@ async function captureViewport(viewport) {
12912
13406
  selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
12913
13407
  frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
12914
13408
  }
13409
+ if (check.type === "link_status" || check.type === "artifact_link_status") {
13410
+ const selector = linkStatusSelector(check);
13411
+ link_statuses[selector] = link_statuses[selector] || await collectLinkStatus(check);
13412
+ }
12915
13413
  }
12916
13414
  const screenshotLabel = profileSlug + "-" + viewport.name;
12917
13415
  try {
@@ -12920,7 +13418,7 @@ async function captureViewport(viewport) {
12920
13418
  pageErrors.push({ message: "saveScreenshot failed: " + String(error && error.message ? error.message : error).slice(0, 500) });
12921
13419
  }
12922
13420
  let routeInventory;
12923
- const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
13421
+ const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory" && profileCheckAppliesToViewport(check, viewport));
12924
13422
  const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
12925
13423
  if (routeInventoryCheck && (routeInventoryCheck.run_all_viewports || !firstViewportName || viewport.name === firstViewportName)) {
12926
13424
  try {
@@ -12975,6 +13473,7 @@ async function captureViewport(viewport) {
12975
13473
  frames,
12976
13474
  text_sequences,
12977
13475
  text_matches,
13476
+ link_statuses,
12978
13477
  route_inventory: routeInventory,
12979
13478
  setup_action_results: setupActionResults,
12980
13479
  screenshot_label: screenshotLabel,
@@ -13023,6 +13522,18 @@ function buildProfileEvidence(currentViewports) {
13023
13522
  ),
13024
13523
  })),
13025
13524
  })),
13525
+ link_status: currentViewports
13526
+ .filter((viewport) => viewport.link_statuses && Object.keys(viewport.link_statuses).length)
13527
+ .map((viewport) => ({
13528
+ viewport: viewport.name,
13529
+ selectors: Object.entries(viewport.link_statuses || {}).map(([selector, statusSet]) => ({
13530
+ selector,
13531
+ total_count: statusSet && statusSet.total_count,
13532
+ ok_count: statusSet && statusSet.ok_count,
13533
+ failed_count: statusSet && statusSet.failed_count,
13534
+ truncated: statusSet && statusSet.truncated === true,
13535
+ })),
13536
+ })),
13026
13537
  route_inventory: currentViewports
13027
13538
  .filter((viewport) => viewport.route_inventory)
13028
13539
  .map((viewport) => ({
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  resolveRiddleProofProfileTimeoutSec,
59
59
  slugifyRiddleProofProfileName,
60
60
  summarizeRiddleProofProfileResult
61
- } from "./chunk-CEBHV23V.js";
61
+ } from "./chunk-2SGLFPFX.js";
62
62
  import {
63
63
  DEFAULT_RIDDLE_API_BASE_URL,
64
64
  DEFAULT_RIDDLE_API_KEY_FILE,