@riddledc/riddle-proof 0.7.33 → 0.7.35

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/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  profileStatusExitCode,
11
11
  resolveRiddleProofProfileTargetUrl,
12
12
  resolveRiddleProofProfileTimeoutSec
13
- } from "./chunk-COUZXKGM.js";
13
+ } from "./chunk-VCINQE5O.js";
14
14
  import {
15
15
  createRiddleApiClient,
16
16
  parseRiddleViewport
package/dist/index.cjs CHANGED
@@ -8728,6 +8728,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
8728
8728
  "route_loaded",
8729
8729
  "selector_visible",
8730
8730
  "selector_count_at_least",
8731
+ "selector_text_order",
8731
8732
  "text_visible",
8732
8733
  "text_absent",
8733
8734
  "route_inventory",
@@ -9007,6 +9008,15 @@ function normalizeRouteInventoryRoutes(value, index) {
9007
9008
  }
9008
9009
  return routes;
9009
9010
  }
9011
+ function normalizeExpectedTexts(value, index) {
9012
+ if (value === void 0) return void 0;
9013
+ if (!Array.isArray(value) || value.length === 0) {
9014
+ throw new Error(`checks[${index}] selector_text_order expected_texts must be a non-empty array.`);
9015
+ }
9016
+ const texts = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
9017
+ if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
9018
+ return texts;
9019
+ }
9010
9020
  function normalizeCheck(input, index) {
9011
9021
  if (!isRecord2(input)) throw new Error(`checks[${index}] must be an object.`);
9012
9022
  const type = stringValue5(input.type);
@@ -9023,6 +9033,11 @@ function normalizeCheck(input, index) {
9023
9033
  if (type === "selector_count_at_least" && numberValue3(input.min_count) === void 0) {
9024
9034
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
9025
9035
  }
9036
+ const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
9037
+ if (type === "selector_text_order") {
9038
+ if (!stringValue5(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
9039
+ if (!expectedTexts?.length) throw new Error(`checks[${index}] selector_text_order requires expected_texts.`);
9040
+ }
9026
9041
  const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
9027
9042
  if (type === "route_inventory" && !expectedRoutes?.length) {
9028
9043
  throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
@@ -9033,6 +9048,7 @@ function normalizeCheck(input, index) {
9033
9048
  expected_path: stringValue5(input.expected_path),
9034
9049
  expected_routes: expectedRoutes,
9035
9050
  selector: stringValue5(input.selector),
9051
+ expected_texts: expectedTexts,
9036
9052
  link_selector: stringValue5(input.link_selector) || stringValue5(input.linkSelector),
9037
9053
  source_selector: stringValue5(input.source_selector) || stringValue5(input.sourceSelector),
9038
9054
  route_path_prefix: stringValue5(input.route_path_prefix) || stringValue5(input.routePathPrefix),
@@ -9138,6 +9154,28 @@ function selectorKey(check) {
9138
9154
  function textKey(check) {
9139
9155
  return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
9140
9156
  }
9157
+ function textSequenceForCheck(viewport, check) {
9158
+ const key = selectorKey(check);
9159
+ const sequence = viewport.text_sequences?.[key];
9160
+ if (isRecord2(sequence)) {
9161
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
9162
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
9163
+ const candidates = visibleTexts.length ? visibleTexts : texts;
9164
+ return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
9165
+ }
9166
+ return [];
9167
+ }
9168
+ function textOrderMatch(texts, expectedTexts) {
9169
+ const positions = [];
9170
+ let startAt = 0;
9171
+ for (const expected of expectedTexts) {
9172
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
9173
+ if (index < 0) return { matched: false, positions };
9174
+ positions.push(index);
9175
+ startAt = index + 1;
9176
+ }
9177
+ return { matched: true, positions };
9178
+ }
9141
9179
  function matchText(sample, check) {
9142
9180
  if (check.pattern) {
9143
9181
  try {
@@ -9257,6 +9295,32 @@ function assessCheckFromEvidence(check, evidence) {
9257
9295
  message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
9258
9296
  };
9259
9297
  }
9298
+ if (check.type === "selector_text_order") {
9299
+ const key = selectorKey(check);
9300
+ const expectedTexts = check.expected_texts || [];
9301
+ const results = viewports.map((viewport) => {
9302
+ const texts = textSequenceForCheck(viewport, check);
9303
+ const match = textOrderMatch(texts, expectedTexts);
9304
+ return {
9305
+ viewport: viewport.name,
9306
+ matched: match.matched,
9307
+ matched_positions: match.positions,
9308
+ visible_texts: texts.slice(0, 20)
9309
+ };
9310
+ });
9311
+ const failed = results.filter((result) => !result.matched).length;
9312
+ return {
9313
+ type: check.type,
9314
+ label: checkLabel(check),
9315
+ status: failed ? "failed" : "passed",
9316
+ evidence: {
9317
+ selector: key,
9318
+ expected_texts: expectedTexts,
9319
+ viewports: results.map((result) => toJsonValue(result))
9320
+ },
9321
+ message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
9322
+ };
9323
+ }
9260
9324
  if (check.type === "text_visible" || check.type === "text_absent") {
9261
9325
  const key = textKey(check);
9262
9326
  const expectedVisible = check.type === "text_visible";
@@ -9292,14 +9356,23 @@ function assessCheckFromEvidence(check, evidence) {
9292
9356
  const first = inventories[0]?.inventory;
9293
9357
  const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
9294
9358
  const clickthroughs = Array.isArray(first?.clickthroughs) ? first.clickthroughs : [];
9359
+ const sourceLinkCount = numberValue3(first?.source_link_count) ?? numberValue3(first?.home_game_link_count) ?? null;
9360
+ const sourceUniqueLinkCount = numberValue3(first?.source_unique_link_count) ?? numberValue3(first?.home_unique_game_link_count) ?? null;
9361
+ const duplicateSourceLinks = Array.isArray(first?.duplicate_source_link_paths) ? first.duplicate_source_link_paths.map((path6) => String(path6)) : [];
9362
+ const duplicateSourceLinkCount = numberValue3(first?.duplicate_source_link_count) ?? (sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null);
9295
9363
  return {
9296
9364
  type: check.type,
9297
9365
  label: checkLabel(check),
9298
9366
  status: failures.length ? "failed" : "passed",
9299
9367
  evidence: {
9300
9368
  expected_count: check.expected_routes?.length || 0,
9301
- homepage_link_count: numberValue3(first?.home_game_link_count) ?? null,
9302
- homepage_unique_link_count: numberValue3(first?.home_unique_game_link_count) ?? null,
9369
+ source_link_count: sourceLinkCount,
9370
+ source_unique_link_count: sourceUniqueLinkCount,
9371
+ duplicate_source_link_count: duplicateSourceLinkCount,
9372
+ duplicate_source_links: duplicateSourceLinks,
9373
+ duplicates_allowed: check.require_unique_routes === false,
9374
+ homepage_link_count: numberValue3(first?.home_game_link_count) ?? sourceLinkCount,
9375
+ homepage_unique_link_count: numberValue3(first?.home_unique_game_link_count) ?? sourceUniqueLinkCount,
9303
9376
  direct_route_count: directRoutes.length,
9304
9377
  clickthrough_count: clickthroughs.length,
9305
9378
  failures
@@ -9598,6 +9671,28 @@ function textMatches(sample, check) {
9598
9671
  }
9599
9672
  return String(sample || "").includes(check.text || "");
9600
9673
  }
9674
+ function textSequenceForCheck(viewport, check) {
9675
+ const key = check.selector || "";
9676
+ const sequence = viewport.text_sequences && viewport.text_sequences[key];
9677
+ if (sequence && typeof sequence === "object") {
9678
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
9679
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
9680
+ const candidates = visibleTexts.length ? visibleTexts : texts;
9681
+ return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
9682
+ }
9683
+ return [];
9684
+ }
9685
+ function textOrderMatch(texts, expectedTexts) {
9686
+ const positions = [];
9687
+ let startAt = 0;
9688
+ for (const expected of expectedTexts || []) {
9689
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
9690
+ if (index < 0) return { matched: false, positions };
9691
+ positions.push(index);
9692
+ startAt = index + 1;
9693
+ }
9694
+ return { matched: true, positions };
9695
+ }
9601
9696
  function numberValue(value) {
9602
9697
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
9603
9698
  }
@@ -9802,6 +9897,29 @@ function assessProfile(profile, evidence) {
9802
9897
  });
9803
9898
  continue;
9804
9899
  }
9900
+ if (check.type === "selector_text_order") {
9901
+ const selector = check.selector || "";
9902
+ const expectedTexts = check.expected_texts || [];
9903
+ const results = viewports.map((viewport) => {
9904
+ const texts = textSequenceForCheck(viewport, check);
9905
+ const match = textOrderMatch(texts, expectedTexts);
9906
+ return {
9907
+ viewport: viewport.name,
9908
+ matched: match.matched,
9909
+ matched_positions: match.positions,
9910
+ visible_texts: texts.slice(0, 20),
9911
+ };
9912
+ });
9913
+ const failed = results.filter((result) => !result.matched).length;
9914
+ checks.push({
9915
+ type: check.type,
9916
+ label: check.label || check.type,
9917
+ status: failed ? "failed" : "passed",
9918
+ evidence: { selector, expected_texts: expectedTexts, viewports: results },
9919
+ message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
9920
+ });
9921
+ continue;
9922
+ }
9805
9923
  if (check.type === "text_visible" || check.type === "text_absent") {
9806
9924
  const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
9807
9925
  const expectedVisible = check.type === "text_visible";
@@ -9839,14 +9957,23 @@ function assessProfile(profile, evidence) {
9839
9957
  const first = inventories[0].inventory || {};
9840
9958
  const directRoutes = Array.isArray(first.direct_routes) ? first.direct_routes : [];
9841
9959
  const clickthroughs = Array.isArray(first.clickthroughs) ? first.clickthroughs : [];
9960
+ const sourceLinkCount = typeof first.source_link_count === "number" ? first.source_link_count : typeof first.home_game_link_count === "number" ? first.home_game_link_count : null;
9961
+ const sourceUniqueLinkCount = typeof first.source_unique_link_count === "number" ? first.source_unique_link_count : typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : null;
9962
+ const duplicateSourceLinks = Array.isArray(first.duplicate_source_link_paths) ? first.duplicate_source_link_paths.map((path) => String(path)) : [];
9963
+ const duplicateSourceLinkCount = typeof first.duplicate_source_link_count === "number" ? first.duplicate_source_link_count : sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null;
9842
9964
  checks.push({
9843
9965
  type: check.type,
9844
9966
  label: check.label || check.type,
9845
9967
  status: failures.length ? "failed" : "passed",
9846
9968
  evidence: {
9847
9969
  expected_count: (check.expected_routes || []).length,
9848
- homepage_link_count: typeof first.home_game_link_count === "number" ? first.home_game_link_count : null,
9849
- homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : null,
9970
+ source_link_count: sourceLinkCount,
9971
+ source_unique_link_count: sourceUniqueLinkCount,
9972
+ duplicate_source_link_count: duplicateSourceLinkCount,
9973
+ duplicate_source_links: duplicateSourceLinks,
9974
+ duplicates_allowed: check.require_unique_routes === false,
9975
+ homepage_link_count: typeof first.home_game_link_count === "number" ? first.home_game_link_count : sourceLinkCount,
9976
+ homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : sourceUniqueLinkCount,
9850
9977
  direct_route_count: directRoutes.length,
9851
9978
  clickthrough_count: clickthroughs.length,
9852
9979
  failures,
@@ -10166,6 +10293,27 @@ async function selectorStats(selector) {
10166
10293
  return { count: elements.length, visible_count: elements.filter(isVisible).length };
10167
10294
  }).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
10168
10295
  }
10296
+ async function selectorTextSequence(selector) {
10297
+ return page.locator(selector).evaluateAll((elements) => {
10298
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
10299
+ const isVisible = (element) => {
10300
+ const style = window.getComputedStyle(element);
10301
+ const rect = element.getBoundingClientRect();
10302
+ return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
10303
+ };
10304
+ const rows = elements.map((element, index) => ({
10305
+ index,
10306
+ text: compact(element.innerText || element.textContent || ""),
10307
+ visible: isVisible(element),
10308
+ }));
10309
+ return {
10310
+ count: rows.length,
10311
+ visible_count: rows.filter((row) => row.visible).length,
10312
+ texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
10313
+ visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
10314
+ };
10315
+ }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
10316
+ }
10169
10317
  function inventoryAppPathFromUrl(urlLike) {
10170
10318
  const url = new URL(String(urlLike), targetUrl);
10171
10319
  const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
@@ -10365,8 +10513,9 @@ async function collectRouteInventory(check, viewport) {
10365
10513
  const homeLinkPaths = homeLinks.map((link) => link.app_path);
10366
10514
  const uniqueHomeLinkPaths = Array.from(new Set(homeLinkPaths));
10367
10515
  const duplicateHomeLinkPaths = homeLinkPaths.filter((path, index) => homeLinkPaths.indexOf(path) !== index);
10516
+ const duplicateHomeLinkPathSet = Array.from(new Set(duplicateHomeLinkPaths));
10368
10517
  if (check.require_unique_routes !== false && duplicateHomeLinkPaths.length) {
10369
- failures.push({ code: "duplicate_source_links", paths: Array.from(new Set(duplicateHomeLinkPaths)) });
10518
+ failures.push({ code: "duplicate_source_links", paths: duplicateHomeLinkPathSet });
10370
10519
  }
10371
10520
  for (const route of expectedRoutes) {
10372
10521
  if (!homeLinkPaths.includes(normalizeRoutePath(route.path))) {
@@ -10442,6 +10591,11 @@ async function collectRouteInventory(check, viewport) {
10442
10591
  expected_routes: expectedRoutes,
10443
10592
  link_selector: check.link_selector || "a[href]",
10444
10593
  source_selector: check.source_selector || null,
10594
+ source_link_count: homeLinkPaths.length,
10595
+ source_unique_link_count: uniqueHomeLinkPaths.length,
10596
+ duplicate_source_link_count: duplicateHomeLinkPaths.length,
10597
+ duplicate_source_link_paths: duplicateHomeLinkPathSet,
10598
+ duplicates_allowed: check.require_unique_routes === false,
10445
10599
  home_game_link_count: homeLinkPaths.length,
10446
10600
  home_unique_game_link_count: uniqueHomeLinkPaths.length,
10447
10601
  home_links: homeLinks,
@@ -10557,11 +10711,16 @@ async function captureViewport(viewport) {
10557
10711
  evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
10558
10712
  }));
10559
10713
  const selectors = {};
10714
+ const text_sequences = {};
10560
10715
  const text_matches = {};
10561
10716
  for (const check of profile.checks || []) {
10562
10717
  if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
10563
10718
  selectors[check.selector] = await selectorStats(check.selector);
10564
10719
  }
10720
+ if (check.type === "selector_text_order" && check.selector) {
10721
+ selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
10722
+ text_sequences[check.selector] = await selectorTextSequence(check.selector);
10723
+ }
10565
10724
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
10566
10725
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
10567
10726
  }
@@ -10585,6 +10744,11 @@ async function captureViewport(viewport) {
10585
10744
  expected_routes: routeInventoryCheck.expected_routes || [],
10586
10745
  link_selector: routeInventoryCheck.link_selector || "a[href]",
10587
10746
  source_selector: routeInventoryCheck.source_selector || null,
10747
+ source_link_count: 0,
10748
+ source_unique_link_count: 0,
10749
+ duplicate_source_link_count: 0,
10750
+ duplicate_source_link_paths: [],
10751
+ duplicates_allowed: routeInventoryCheck.require_unique_routes === false,
10588
10752
  home_game_link_count: 0,
10589
10753
  home_unique_game_link_count: 0,
10590
10754
  home_links: [],
@@ -10620,6 +10784,7 @@ async function captureViewport(viewport) {
10620
10784
  bounds_overflow_px: dom.bounds_overflow_px,
10621
10785
  overflow_offenders: dom.overflow_offenders || [],
10622
10786
  selectors,
10787
+ text_sequences,
10623
10788
  text_matches,
10624
10789
  route_inventory: routeInventory,
10625
10790
  setup_action_results: setupActionResults,
@@ -10659,6 +10824,9 @@ function buildProfileEvidence(currentViewports) {
10659
10824
  .map((viewport) => ({
10660
10825
  viewport: viewport.name,
10661
10826
  expected_count: (viewport.route_inventory.expected_routes || []).length,
10827
+ source_link_count: viewport.route_inventory.source_link_count == null ? viewport.route_inventory.home_game_link_count : viewport.route_inventory.source_link_count,
10828
+ source_unique_link_count: viewport.route_inventory.source_unique_link_count == null ? viewport.route_inventory.home_unique_game_link_count : viewport.route_inventory.source_unique_link_count,
10829
+ duplicate_source_link_count: viewport.route_inventory.duplicate_source_link_count,
10662
10830
  home_unique_game_link_count: viewport.route_inventory.home_unique_game_link_count,
10663
10831
  direct_route_count: (viewport.route_inventory.direct_routes || []).length,
10664
10832
  clickthrough_count: (viewport.route_inventory.clickthroughs || []).length,
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  resolveRiddleProofProfileTimeoutSec,
59
59
  slugifyRiddleProofProfileName,
60
60
  summarizeRiddleProofProfileResult
61
- } from "./chunk-COUZXKGM.js";
61
+ } from "./chunk-VCINQE5O.js";
62
62
  import {
63
63
  DEFAULT_RIDDLE_API_BASE_URL,
64
64
  DEFAULT_RIDDLE_API_KEY_FILE,
package/dist/profile.cjs CHANGED
@@ -57,6 +57,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
57
57
  "route_loaded",
58
58
  "selector_visible",
59
59
  "selector_count_at_least",
60
+ "selector_text_order",
60
61
  "text_visible",
61
62
  "text_absent",
62
63
  "route_inventory",
@@ -336,6 +337,15 @@ function normalizeRouteInventoryRoutes(value, index) {
336
337
  }
337
338
  return routes;
338
339
  }
340
+ function normalizeExpectedTexts(value, index) {
341
+ if (value === void 0) return void 0;
342
+ if (!Array.isArray(value) || value.length === 0) {
343
+ throw new Error(`checks[${index}] selector_text_order expected_texts must be a non-empty array.`);
344
+ }
345
+ const texts = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
346
+ if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
347
+ return texts;
348
+ }
339
349
  function normalizeCheck(input, index) {
340
350
  if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
341
351
  const type = stringValue(input.type);
@@ -352,6 +362,11 @@ function normalizeCheck(input, index) {
352
362
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
353
363
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
354
364
  }
365
+ const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
366
+ if (type === "selector_text_order") {
367
+ if (!stringValue(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
368
+ if (!expectedTexts?.length) throw new Error(`checks[${index}] selector_text_order requires expected_texts.`);
369
+ }
355
370
  const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
356
371
  if (type === "route_inventory" && !expectedRoutes?.length) {
357
372
  throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
@@ -362,6 +377,7 @@ function normalizeCheck(input, index) {
362
377
  expected_path: stringValue(input.expected_path),
363
378
  expected_routes: expectedRoutes,
364
379
  selector: stringValue(input.selector),
380
+ expected_texts: expectedTexts,
365
381
  link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
366
382
  source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
367
383
  route_path_prefix: stringValue(input.route_path_prefix) || stringValue(input.routePathPrefix),
@@ -467,6 +483,28 @@ function selectorKey(check) {
467
483
  function textKey(check) {
468
484
  return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
469
485
  }
486
+ function textSequenceForCheck(viewport, check) {
487
+ const key = selectorKey(check);
488
+ const sequence = viewport.text_sequences?.[key];
489
+ if (isRecord(sequence)) {
490
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
491
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
492
+ const candidates = visibleTexts.length ? visibleTexts : texts;
493
+ return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
494
+ }
495
+ return [];
496
+ }
497
+ function textOrderMatch(texts, expectedTexts) {
498
+ const positions = [];
499
+ let startAt = 0;
500
+ for (const expected of expectedTexts) {
501
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
502
+ if (index < 0) return { matched: false, positions };
503
+ positions.push(index);
504
+ startAt = index + 1;
505
+ }
506
+ return { matched: true, positions };
507
+ }
470
508
  function matchText(sample, check) {
471
509
  if (check.pattern) {
472
510
  try {
@@ -586,6 +624,32 @@ function assessCheckFromEvidence(check, evidence) {
586
624
  message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
587
625
  };
588
626
  }
627
+ if (check.type === "selector_text_order") {
628
+ const key = selectorKey(check);
629
+ const expectedTexts = check.expected_texts || [];
630
+ const results = viewports.map((viewport) => {
631
+ const texts = textSequenceForCheck(viewport, check);
632
+ const match = textOrderMatch(texts, expectedTexts);
633
+ return {
634
+ viewport: viewport.name,
635
+ matched: match.matched,
636
+ matched_positions: match.positions,
637
+ visible_texts: texts.slice(0, 20)
638
+ };
639
+ });
640
+ const failed = results.filter((result) => !result.matched).length;
641
+ return {
642
+ type: check.type,
643
+ label: checkLabel(check),
644
+ status: failed ? "failed" : "passed",
645
+ evidence: {
646
+ selector: key,
647
+ expected_texts: expectedTexts,
648
+ viewports: results.map((result) => toJsonValue(result))
649
+ },
650
+ message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
651
+ };
652
+ }
589
653
  if (check.type === "text_visible" || check.type === "text_absent") {
590
654
  const key = textKey(check);
591
655
  const expectedVisible = check.type === "text_visible";
@@ -621,14 +685,23 @@ function assessCheckFromEvidence(check, evidence) {
621
685
  const first = inventories[0]?.inventory;
622
686
  const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
623
687
  const clickthroughs = Array.isArray(first?.clickthroughs) ? first.clickthroughs : [];
688
+ const sourceLinkCount = numberValue(first?.source_link_count) ?? numberValue(first?.home_game_link_count) ?? null;
689
+ const sourceUniqueLinkCount = numberValue(first?.source_unique_link_count) ?? numberValue(first?.home_unique_game_link_count) ?? null;
690
+ const duplicateSourceLinks = Array.isArray(first?.duplicate_source_link_paths) ? first.duplicate_source_link_paths.map((path) => String(path)) : [];
691
+ const duplicateSourceLinkCount = numberValue(first?.duplicate_source_link_count) ?? (sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null);
624
692
  return {
625
693
  type: check.type,
626
694
  label: checkLabel(check),
627
695
  status: failures.length ? "failed" : "passed",
628
696
  evidence: {
629
697
  expected_count: check.expected_routes?.length || 0,
630
- homepage_link_count: numberValue(first?.home_game_link_count) ?? null,
631
- homepage_unique_link_count: numberValue(first?.home_unique_game_link_count) ?? null,
698
+ source_link_count: sourceLinkCount,
699
+ source_unique_link_count: sourceUniqueLinkCount,
700
+ duplicate_source_link_count: duplicateSourceLinkCount,
701
+ duplicate_source_links: duplicateSourceLinks,
702
+ duplicates_allowed: check.require_unique_routes === false,
703
+ homepage_link_count: numberValue(first?.home_game_link_count) ?? sourceLinkCount,
704
+ homepage_unique_link_count: numberValue(first?.home_unique_game_link_count) ?? sourceUniqueLinkCount,
632
705
  direct_route_count: directRoutes.length,
633
706
  clickthrough_count: clickthroughs.length,
634
707
  failures
@@ -927,6 +1000,28 @@ function textMatches(sample, check) {
927
1000
  }
928
1001
  return String(sample || "").includes(check.text || "");
929
1002
  }
1003
+ function textSequenceForCheck(viewport, check) {
1004
+ const key = check.selector || "";
1005
+ const sequence = viewport.text_sequences && viewport.text_sequences[key];
1006
+ if (sequence && typeof sequence === "object") {
1007
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
1008
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
1009
+ const candidates = visibleTexts.length ? visibleTexts : texts;
1010
+ return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
1011
+ }
1012
+ return [];
1013
+ }
1014
+ function textOrderMatch(texts, expectedTexts) {
1015
+ const positions = [];
1016
+ let startAt = 0;
1017
+ for (const expected of expectedTexts || []) {
1018
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
1019
+ if (index < 0) return { matched: false, positions };
1020
+ positions.push(index);
1021
+ startAt = index + 1;
1022
+ }
1023
+ return { matched: true, positions };
1024
+ }
930
1025
  function numberValue(value) {
931
1026
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
932
1027
  }
@@ -1131,6 +1226,29 @@ function assessProfile(profile, evidence) {
1131
1226
  });
1132
1227
  continue;
1133
1228
  }
1229
+ if (check.type === "selector_text_order") {
1230
+ const selector = check.selector || "";
1231
+ const expectedTexts = check.expected_texts || [];
1232
+ const results = viewports.map((viewport) => {
1233
+ const texts = textSequenceForCheck(viewport, check);
1234
+ const match = textOrderMatch(texts, expectedTexts);
1235
+ return {
1236
+ viewport: viewport.name,
1237
+ matched: match.matched,
1238
+ matched_positions: match.positions,
1239
+ visible_texts: texts.slice(0, 20),
1240
+ };
1241
+ });
1242
+ const failed = results.filter((result) => !result.matched).length;
1243
+ checks.push({
1244
+ type: check.type,
1245
+ label: check.label || check.type,
1246
+ status: failed ? "failed" : "passed",
1247
+ evidence: { selector, expected_texts: expectedTexts, viewports: results },
1248
+ message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
1249
+ });
1250
+ continue;
1251
+ }
1134
1252
  if (check.type === "text_visible" || check.type === "text_absent") {
1135
1253
  const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
1136
1254
  const expectedVisible = check.type === "text_visible";
@@ -1168,14 +1286,23 @@ function assessProfile(profile, evidence) {
1168
1286
  const first = inventories[0].inventory || {};
1169
1287
  const directRoutes = Array.isArray(first.direct_routes) ? first.direct_routes : [];
1170
1288
  const clickthroughs = Array.isArray(first.clickthroughs) ? first.clickthroughs : [];
1289
+ const sourceLinkCount = typeof first.source_link_count === "number" ? first.source_link_count : typeof first.home_game_link_count === "number" ? first.home_game_link_count : null;
1290
+ const sourceUniqueLinkCount = typeof first.source_unique_link_count === "number" ? first.source_unique_link_count : typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : null;
1291
+ const duplicateSourceLinks = Array.isArray(first.duplicate_source_link_paths) ? first.duplicate_source_link_paths.map((path) => String(path)) : [];
1292
+ const duplicateSourceLinkCount = typeof first.duplicate_source_link_count === "number" ? first.duplicate_source_link_count : sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null;
1171
1293
  checks.push({
1172
1294
  type: check.type,
1173
1295
  label: check.label || check.type,
1174
1296
  status: failures.length ? "failed" : "passed",
1175
1297
  evidence: {
1176
1298
  expected_count: (check.expected_routes || []).length,
1177
- homepage_link_count: typeof first.home_game_link_count === "number" ? first.home_game_link_count : null,
1178
- homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : null,
1299
+ source_link_count: sourceLinkCount,
1300
+ source_unique_link_count: sourceUniqueLinkCount,
1301
+ duplicate_source_link_count: duplicateSourceLinkCount,
1302
+ duplicate_source_links: duplicateSourceLinks,
1303
+ duplicates_allowed: check.require_unique_routes === false,
1304
+ homepage_link_count: typeof first.home_game_link_count === "number" ? first.home_game_link_count : sourceLinkCount,
1305
+ homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : sourceUniqueLinkCount,
1179
1306
  direct_route_count: directRoutes.length,
1180
1307
  clickthrough_count: clickthroughs.length,
1181
1308
  failures,
@@ -1495,6 +1622,27 @@ async function selectorStats(selector) {
1495
1622
  return { count: elements.length, visible_count: elements.filter(isVisible).length };
1496
1623
  }).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
1497
1624
  }
1625
+ async function selectorTextSequence(selector) {
1626
+ return page.locator(selector).evaluateAll((elements) => {
1627
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
1628
+ const isVisible = (element) => {
1629
+ const style = window.getComputedStyle(element);
1630
+ const rect = element.getBoundingClientRect();
1631
+ return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
1632
+ };
1633
+ const rows = elements.map((element, index) => ({
1634
+ index,
1635
+ text: compact(element.innerText || element.textContent || ""),
1636
+ visible: isVisible(element),
1637
+ }));
1638
+ return {
1639
+ count: rows.length,
1640
+ visible_count: rows.filter((row) => row.visible).length,
1641
+ texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
1642
+ visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
1643
+ };
1644
+ }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
1645
+ }
1498
1646
  function inventoryAppPathFromUrl(urlLike) {
1499
1647
  const url = new URL(String(urlLike), targetUrl);
1500
1648
  const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
@@ -1694,8 +1842,9 @@ async function collectRouteInventory(check, viewport) {
1694
1842
  const homeLinkPaths = homeLinks.map((link) => link.app_path);
1695
1843
  const uniqueHomeLinkPaths = Array.from(new Set(homeLinkPaths));
1696
1844
  const duplicateHomeLinkPaths = homeLinkPaths.filter((path, index) => homeLinkPaths.indexOf(path) !== index);
1845
+ const duplicateHomeLinkPathSet = Array.from(new Set(duplicateHomeLinkPaths));
1697
1846
  if (check.require_unique_routes !== false && duplicateHomeLinkPaths.length) {
1698
- failures.push({ code: "duplicate_source_links", paths: Array.from(new Set(duplicateHomeLinkPaths)) });
1847
+ failures.push({ code: "duplicate_source_links", paths: duplicateHomeLinkPathSet });
1699
1848
  }
1700
1849
  for (const route of expectedRoutes) {
1701
1850
  if (!homeLinkPaths.includes(normalizeRoutePath(route.path))) {
@@ -1771,6 +1920,11 @@ async function collectRouteInventory(check, viewport) {
1771
1920
  expected_routes: expectedRoutes,
1772
1921
  link_selector: check.link_selector || "a[href]",
1773
1922
  source_selector: check.source_selector || null,
1923
+ source_link_count: homeLinkPaths.length,
1924
+ source_unique_link_count: uniqueHomeLinkPaths.length,
1925
+ duplicate_source_link_count: duplicateHomeLinkPaths.length,
1926
+ duplicate_source_link_paths: duplicateHomeLinkPathSet,
1927
+ duplicates_allowed: check.require_unique_routes === false,
1774
1928
  home_game_link_count: homeLinkPaths.length,
1775
1929
  home_unique_game_link_count: uniqueHomeLinkPaths.length,
1776
1930
  home_links: homeLinks,
@@ -1886,11 +2040,16 @@ async function captureViewport(viewport) {
1886
2040
  evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
1887
2041
  }));
1888
2042
  const selectors = {};
2043
+ const text_sequences = {};
1889
2044
  const text_matches = {};
1890
2045
  for (const check of profile.checks || []) {
1891
2046
  if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
1892
2047
  selectors[check.selector] = await selectorStats(check.selector);
1893
2048
  }
2049
+ if (check.type === "selector_text_order" && check.selector) {
2050
+ selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
2051
+ text_sequences[check.selector] = await selectorTextSequence(check.selector);
2052
+ }
1894
2053
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
1895
2054
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
1896
2055
  }
@@ -1914,6 +2073,11 @@ async function captureViewport(viewport) {
1914
2073
  expected_routes: routeInventoryCheck.expected_routes || [],
1915
2074
  link_selector: routeInventoryCheck.link_selector || "a[href]",
1916
2075
  source_selector: routeInventoryCheck.source_selector || null,
2076
+ source_link_count: 0,
2077
+ source_unique_link_count: 0,
2078
+ duplicate_source_link_count: 0,
2079
+ duplicate_source_link_paths: [],
2080
+ duplicates_allowed: routeInventoryCheck.require_unique_routes === false,
1917
2081
  home_game_link_count: 0,
1918
2082
  home_unique_game_link_count: 0,
1919
2083
  home_links: [],
@@ -1949,6 +2113,7 @@ async function captureViewport(viewport) {
1949
2113
  bounds_overflow_px: dom.bounds_overflow_px,
1950
2114
  overflow_offenders: dom.overflow_offenders || [],
1951
2115
  selectors,
2116
+ text_sequences,
1952
2117
  text_matches,
1953
2118
  route_inventory: routeInventory,
1954
2119
  setup_action_results: setupActionResults,
@@ -1988,6 +2153,9 @@ function buildProfileEvidence(currentViewports) {
1988
2153
  .map((viewport) => ({
1989
2154
  viewport: viewport.name,
1990
2155
  expected_count: (viewport.route_inventory.expected_routes || []).length,
2156
+ source_link_count: viewport.route_inventory.source_link_count == null ? viewport.route_inventory.home_game_link_count : viewport.route_inventory.source_link_count,
2157
+ source_unique_link_count: viewport.route_inventory.source_unique_link_count == null ? viewport.route_inventory.home_unique_game_link_count : viewport.route_inventory.source_unique_link_count,
2158
+ duplicate_source_link_count: viewport.route_inventory.duplicate_source_link_count,
1991
2159
  home_unique_game_link_count: viewport.route_inventory.home_unique_game_link_count,
1992
2160
  direct_route_count: (viewport.route_inventory.direct_routes || []).length,
1993
2161
  clickthrough_count: (viewport.route_inventory.clickthroughs || []).length,
@@ -4,7 +4,7 @@ declare const RIDDLE_PROOF_PROFILE_VERSION: "riddle-proof.profile.v1";
4
4
  declare const RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION: "riddle-proof.profile-evidence.v1";
5
5
  declare const RIDDLE_PROOF_PROFILE_RESULT_VERSION: "riddle-proof.profile-result.v1";
6
6
  declare const RIDDLE_PROOF_PROFILE_STATUSES: readonly ["passed", "product_regression", "proof_insufficient", "environment_blocked", "configuration_error", "needs_human_review"];
7
- declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_count_at_least", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
7
+ declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_count_at_least", "selector_text_order", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
8
8
  declare const RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES: readonly ["click", "fill", "set_input_value", "local_storage", "session_storage", "clear_storage", "wait", "wait_for_selector", "wait_for_text"];
9
9
  type RiddleProofProfileStatus = typeof RIDDLE_PROOF_PROFILE_STATUSES[number];
10
10
  type RiddleProofProfileCheckType = typeof RIDDLE_PROOF_PROFILE_CHECK_TYPES[number];
@@ -66,6 +66,7 @@ interface RiddleProofProfileCheck {
66
66
  expected_path?: string;
67
67
  expected_routes?: RiddleProofProfileRouteInventoryRoute[];
68
68
  selector?: string;
69
+ expected_texts?: string[];
69
70
  link_selector?: string;
70
71
  source_selector?: string;
71
72
  route_path_prefix?: string;
@@ -137,6 +138,7 @@ interface RiddleProofProfileViewportEvidence {
137
138
  count: number;
138
139
  visible_count: number;
139
140
  }>;
141
+ text_sequences?: Record<string, Record<string, JsonValue>>;
140
142
  text_matches?: Record<string, boolean>;
141
143
  route_inventory?: Record<string, JsonValue>;
142
144
  setup_action_results?: Array<Record<string, JsonValue>>;