@riddledc/riddle-proof 0.7.34 → 0.7.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -202,6 +202,21 @@ clears `local`, `session`, or `both` browser storage scopes, defaults to
202
202
  profile carries its own hosted Riddle worker budget; an explicit CLI `--timeout`
203
203
  still overrides the profile value for one-off runs.
204
204
 
205
+ Use `selector_text_order` when a table, list, or card group must show visible
206
+ items in a specific order after setup actions such as sorting or filtering:
207
+
208
+ ```json
209
+ {
210
+ "type": "selector_text_order",
211
+ "selector": ".game-table tbody tr",
212
+ "expected_texts": ["AAA Alpha", "MMM Middle", "ZZZ Omega"]
213
+ }
214
+ ```
215
+
216
+ The check records the visible text sequence for the selector and passes when
217
+ the expected texts appear in that order as a subsequence. This is less brittle
218
+ than matching one large body-text regex when only row or card order matters.
219
+
205
220
  Use the `route_inventory` check for source-page route coverage audits where a
206
221
  navigation surface must expose a known set of routes and each route must load
207
222
  both directly and through real link clicks:
@@ -225,7 +240,9 @@ source-link counts, missing/unexpected routes, direct route health, real
225
240
  clickthrough health, wrong-path failures, and stale source-surface failures. It
226
241
  runs direct/clickthrough sweeps on the first viewport by default and leaves
227
242
  ordinary profile overflow checks to cover the source page across all configured
228
- viewports. Set `run_direct_routes: false`, `run_clickthroughs: false`,
243
+ viewports. Set `run_all_viewports: true` when desktop and mobile navigation
244
+ surfaces both need direct/clickthrough inventory evidence in one profile result.
245
+ Set `run_direct_routes: false`, `run_clickthroughs: false`,
229
246
  `allow_unexpected_routes: true`, `require_unique_routes: false`, or
230
247
  `save_route_screenshots: true` when a profile needs a narrower or more
231
248
  artifact-heavy audit. `require_unique_routes: false` is useful when a navigation
@@ -14,6 +14,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
14
14
  "route_loaded",
15
15
  "selector_visible",
16
16
  "selector_count_at_least",
17
+ "selector_text_order",
17
18
  "text_visible",
18
19
  "text_absent",
19
20
  "route_inventory",
@@ -293,6 +294,15 @@ function normalizeRouteInventoryRoutes(value, index) {
293
294
  }
294
295
  return routes;
295
296
  }
297
+ function normalizeExpectedTexts(value, index) {
298
+ if (value === void 0) return void 0;
299
+ if (!Array.isArray(value) || value.length === 0) {
300
+ throw new Error(`checks[${index}] selector_text_order expected_texts must be a non-empty array.`);
301
+ }
302
+ const texts = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
303
+ if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
304
+ return texts;
305
+ }
296
306
  function normalizeCheck(input, index) {
297
307
  if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
298
308
  const type = stringValue(input.type);
@@ -309,6 +319,11 @@ function normalizeCheck(input, index) {
309
319
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
310
320
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
311
321
  }
322
+ const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
323
+ if (type === "selector_text_order") {
324
+ if (!stringValue(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
325
+ if (!expectedTexts?.length) throw new Error(`checks[${index}] selector_text_order requires expected_texts.`);
326
+ }
312
327
  const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
313
328
  if (type === "route_inventory" && !expectedRoutes?.length) {
314
329
  throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
@@ -319,6 +334,7 @@ function normalizeCheck(input, index) {
319
334
  expected_path: stringValue(input.expected_path),
320
335
  expected_routes: expectedRoutes,
321
336
  selector: stringValue(input.selector),
337
+ expected_texts: expectedTexts,
322
338
  link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
323
339
  source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
324
340
  route_path_prefix: stringValue(input.route_path_prefix) || stringValue(input.routePathPrefix),
@@ -334,6 +350,7 @@ function normalizeCheck(input, index) {
334
350
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
335
351
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
336
352
  run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
353
+ run_all_viewports: input.run_all_viewports === true || input.runAllViewports === true,
337
354
  require_unique_routes: input.require_unique_routes === false || input.requireUniqueRoutes === false ? false : true,
338
355
  allow_unexpected_routes: input.allow_unexpected_routes === true || input.allowUnexpectedRoutes === true,
339
356
  save_route_screenshots: input.save_route_screenshots === true || input.saveRouteScreenshots === true
@@ -424,6 +441,47 @@ function selectorKey(check) {
424
441
  function textKey(check) {
425
442
  return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
426
443
  }
444
+ function textSequenceForCheck(viewport, check) {
445
+ const key = selectorKey(check);
446
+ const sequence = viewport.text_sequences?.[key];
447
+ if (isRecord(sequence)) {
448
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
449
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
450
+ const candidates = visibleTexts.length ? visibleTexts : texts;
451
+ return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
452
+ }
453
+ return [];
454
+ }
455
+ function textOrderMatch(texts, expectedTexts) {
456
+ const positions = [];
457
+ let startAt = 0;
458
+ for (const expected of expectedTexts) {
459
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
460
+ if (index < 0) return { matched: false, positions };
461
+ positions.push(index);
462
+ startAt = index + 1;
463
+ }
464
+ return { matched: true, positions };
465
+ }
466
+ function summarizeRouteInventory(viewport, inventory) {
467
+ const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
468
+ const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
469
+ const sourceLinkCount = numberValue(inventory.source_link_count) ?? numberValue(inventory.home_game_link_count) ?? null;
470
+ const sourceUniqueLinkCount = numberValue(inventory.source_unique_link_count) ?? numberValue(inventory.home_unique_game_link_count) ?? null;
471
+ const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path) => String(path)) : [];
472
+ const duplicateSourceLinkCount = numberValue(inventory.duplicate_source_link_count) ?? (sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null);
473
+ const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
474
+ return {
475
+ viewport,
476
+ source_link_count: sourceLinkCount,
477
+ source_unique_link_count: sourceUniqueLinkCount,
478
+ duplicate_source_link_count: duplicateSourceLinkCount,
479
+ duplicate_source_links: duplicateSourceLinks,
480
+ direct_route_count: directRoutes.length,
481
+ clickthrough_count: clickthroughs.length,
482
+ failure_count: failures.length
483
+ };
484
+ }
427
485
  function matchText(sample, check) {
428
486
  if (check.pattern) {
429
487
  try {
@@ -543,6 +601,32 @@ function assessCheckFromEvidence(check, evidence) {
543
601
  message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
544
602
  };
545
603
  }
604
+ if (check.type === "selector_text_order") {
605
+ const key = selectorKey(check);
606
+ const expectedTexts = check.expected_texts || [];
607
+ const results = viewports.map((viewport) => {
608
+ const texts = textSequenceForCheck(viewport, check);
609
+ const match = textOrderMatch(texts, expectedTexts);
610
+ return {
611
+ viewport: viewport.name,
612
+ matched: match.matched,
613
+ matched_positions: match.positions,
614
+ visible_texts: texts.slice(0, 20)
615
+ };
616
+ });
617
+ const failed = results.filter((result) => !result.matched).length;
618
+ return {
619
+ type: check.type,
620
+ label: checkLabel(check),
621
+ status: failed ? "failed" : "passed",
622
+ evidence: {
623
+ selector: key,
624
+ expected_texts: expectedTexts,
625
+ viewports: results.map((result) => toJsonValue(result))
626
+ },
627
+ message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
628
+ };
629
+ }
546
630
  if (check.type === "text_visible" || check.type === "text_absent") {
547
631
  const key = textKey(check);
548
632
  const expectedVisible = check.type === "text_visible";
@@ -574,6 +658,7 @@ function assessCheckFromEvidence(check, evidence) {
574
658
  message: "No route inventory evidence was captured."
575
659
  };
576
660
  }
661
+ const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory));
577
662
  const failures = inventories.flatMap((item) => Array.isArray(item.inventory?.failures) ? item.inventory.failures.map((failure) => toJsonValue({ viewport: item.viewport, failure })) : []);
578
663
  const first = inventories[0]?.inventory;
579
664
  const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
@@ -597,6 +682,8 @@ function assessCheckFromEvidence(check, evidence) {
597
682
  homepage_unique_link_count: numberValue(first?.home_unique_game_link_count) ?? sourceUniqueLinkCount,
598
683
  direct_route_count: directRoutes.length,
599
684
  clickthrough_count: clickthroughs.length,
685
+ viewport_count: inventories.length,
686
+ viewports: viewportSummaries.map((summary) => toJsonValue(summary)),
600
687
  failures
601
688
  },
602
689
  message: failures.length ? `Route inventory failed with ${failures.length} issue(s).` : void 0
@@ -893,6 +980,47 @@ function textMatches(sample, check) {
893
980
  }
894
981
  return String(sample || "").includes(check.text || "");
895
982
  }
983
+ function textSequenceForCheck(viewport, check) {
984
+ const key = check.selector || "";
985
+ const sequence = viewport.text_sequences && viewport.text_sequences[key];
986
+ if (sequence && typeof sequence === "object") {
987
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
988
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
989
+ const candidates = visibleTexts.length ? visibleTexts : texts;
990
+ return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
991
+ }
992
+ return [];
993
+ }
994
+ function textOrderMatch(texts, expectedTexts) {
995
+ const positions = [];
996
+ let startAt = 0;
997
+ for (const expected of expectedTexts || []) {
998
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
999
+ if (index < 0) return { matched: false, positions };
1000
+ positions.push(index);
1001
+ startAt = index + 1;
1002
+ }
1003
+ return { matched: true, positions };
1004
+ }
1005
+ function summarizeRouteInventory(viewport, inventory) {
1006
+ const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
1007
+ const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
1008
+ const sourceLinkCount = typeof inventory.source_link_count === "number" ? inventory.source_link_count : typeof inventory.home_game_link_count === "number" ? inventory.home_game_link_count : null;
1009
+ const sourceUniqueLinkCount = typeof inventory.source_unique_link_count === "number" ? inventory.source_unique_link_count : typeof inventory.home_unique_game_link_count === "number" ? inventory.home_unique_game_link_count : null;
1010
+ const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path) => String(path)) : [];
1011
+ const duplicateSourceLinkCount = typeof inventory.duplicate_source_link_count === "number" ? inventory.duplicate_source_link_count : sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null;
1012
+ const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
1013
+ return {
1014
+ viewport,
1015
+ source_link_count: sourceLinkCount,
1016
+ source_unique_link_count: sourceUniqueLinkCount,
1017
+ duplicate_source_link_count: duplicateSourceLinkCount,
1018
+ duplicate_source_links: duplicateSourceLinks,
1019
+ direct_route_count: directRoutes.length,
1020
+ clickthrough_count: clickthroughs.length,
1021
+ failure_count: failures.length,
1022
+ };
1023
+ }
896
1024
  function numberValue(value) {
897
1025
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
898
1026
  }
@@ -1097,6 +1225,29 @@ function assessProfile(profile, evidence) {
1097
1225
  });
1098
1226
  continue;
1099
1227
  }
1228
+ if (check.type === "selector_text_order") {
1229
+ const selector = check.selector || "";
1230
+ const expectedTexts = check.expected_texts || [];
1231
+ const results = viewports.map((viewport) => {
1232
+ const texts = textSequenceForCheck(viewport, check);
1233
+ const match = textOrderMatch(texts, expectedTexts);
1234
+ return {
1235
+ viewport: viewport.name,
1236
+ matched: match.matched,
1237
+ matched_positions: match.positions,
1238
+ visible_texts: texts.slice(0, 20),
1239
+ };
1240
+ });
1241
+ const failed = results.filter((result) => !result.matched).length;
1242
+ checks.push({
1243
+ type: check.type,
1244
+ label: check.label || check.type,
1245
+ status: failed ? "failed" : "passed",
1246
+ evidence: { selector, expected_texts: expectedTexts, viewports: results },
1247
+ message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
1248
+ });
1249
+ continue;
1250
+ }
1100
1251
  if (check.type === "text_visible" || check.type === "text_absent") {
1101
1252
  const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
1102
1253
  const expectedVisible = check.type === "text_visible";
@@ -1125,6 +1276,7 @@ function assessProfile(profile, evidence) {
1125
1276
  });
1126
1277
  continue;
1127
1278
  }
1279
+ const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory || {}));
1128
1280
  const failures = [];
1129
1281
  for (const item of inventories) {
1130
1282
  for (const failure of Array.isArray(item.inventory.failures) ? item.inventory.failures : []) {
@@ -1153,6 +1305,8 @@ function assessProfile(profile, evidence) {
1153
1305
  homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : sourceUniqueLinkCount,
1154
1306
  direct_route_count: directRoutes.length,
1155
1307
  clickthrough_count: clickthroughs.length,
1308
+ viewport_count: inventories.length,
1309
+ viewports: viewportSummaries,
1156
1310
  failures,
1157
1311
  },
1158
1312
  message: failures.length ? "Route inventory failed with " + failures.length + " issue(s)." : undefined,
@@ -1470,6 +1624,27 @@ async function selectorStats(selector) {
1470
1624
  return { count: elements.length, visible_count: elements.filter(isVisible).length };
1471
1625
  }).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
1472
1626
  }
1627
+ async function selectorTextSequence(selector) {
1628
+ return page.locator(selector).evaluateAll((elements) => {
1629
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
1630
+ const isVisible = (element) => {
1631
+ const style = window.getComputedStyle(element);
1632
+ const rect = element.getBoundingClientRect();
1633
+ return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
1634
+ };
1635
+ const rows = elements.map((element, index) => ({
1636
+ index,
1637
+ text: compact(element.innerText || element.textContent || ""),
1638
+ visible: isVisible(element),
1639
+ }));
1640
+ return {
1641
+ count: rows.length,
1642
+ visible_count: rows.filter((row) => row.visible).length,
1643
+ texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
1644
+ visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
1645
+ };
1646
+ }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
1647
+ }
1473
1648
  function inventoryAppPathFromUrl(urlLike) {
1474
1649
  const url = new URL(String(urlLike), targetUrl);
1475
1650
  const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
@@ -1867,11 +2042,16 @@ async function captureViewport(viewport) {
1867
2042
  evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
1868
2043
  }));
1869
2044
  const selectors = {};
2045
+ const text_sequences = {};
1870
2046
  const text_matches = {};
1871
2047
  for (const check of profile.checks || []) {
1872
2048
  if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
1873
2049
  selectors[check.selector] = await selectorStats(check.selector);
1874
2050
  }
2051
+ if (check.type === "selector_text_order" && check.selector) {
2052
+ selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
2053
+ text_sequences[check.selector] = await selectorTextSequence(check.selector);
2054
+ }
1875
2055
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
1876
2056
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
1877
2057
  }
@@ -1885,7 +2065,7 @@ async function captureViewport(viewport) {
1885
2065
  let routeInventory;
1886
2066
  const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
1887
2067
  const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
1888
- if (routeInventoryCheck && (!firstViewportName || viewport.name === firstViewportName)) {
2068
+ if (routeInventoryCheck && (routeInventoryCheck.run_all_viewports || !firstViewportName || viewport.name === firstViewportName)) {
1889
2069
  try {
1890
2070
  routeInventory = await collectRouteInventory(routeInventoryCheck, viewport);
1891
2071
  } catch (error) {
@@ -1935,6 +2115,7 @@ async function captureViewport(viewport) {
1935
2115
  bounds_overflow_px: dom.bounds_overflow_px,
1936
2116
  overflow_offenders: dom.overflow_offenders || [],
1937
2117
  selectors,
2118
+ text_sequences,
1938
2119
  text_matches,
1939
2120
  route_inventory: routeInventory,
1940
2121
  setup_action_results: setupActionResults,
package/dist/cli.cjs CHANGED
@@ -6887,6 +6887,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
6887
6887
  "route_loaded",
6888
6888
  "selector_visible",
6889
6889
  "selector_count_at_least",
6890
+ "selector_text_order",
6890
6891
  "text_visible",
6891
6892
  "text_absent",
6892
6893
  "route_inventory",
@@ -7166,6 +7167,15 @@ function normalizeRouteInventoryRoutes(value, index) {
7166
7167
  }
7167
7168
  return routes;
7168
7169
  }
7170
+ function normalizeExpectedTexts(value, index) {
7171
+ if (value === void 0) return void 0;
7172
+ if (!Array.isArray(value) || value.length === 0) {
7173
+ throw new Error(`checks[${index}] selector_text_order expected_texts must be a non-empty array.`);
7174
+ }
7175
+ const texts = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
7176
+ if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
7177
+ return texts;
7178
+ }
7169
7179
  function normalizeCheck(input, index) {
7170
7180
  if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
7171
7181
  const type = stringValue2(input.type);
@@ -7182,6 +7192,11 @@ function normalizeCheck(input, index) {
7182
7192
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
7183
7193
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
7184
7194
  }
7195
+ const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
7196
+ if (type === "selector_text_order") {
7197
+ if (!stringValue2(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
7198
+ if (!expectedTexts?.length) throw new Error(`checks[${index}] selector_text_order requires expected_texts.`);
7199
+ }
7185
7200
  const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
7186
7201
  if (type === "route_inventory" && !expectedRoutes?.length) {
7187
7202
  throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
@@ -7192,6 +7207,7 @@ function normalizeCheck(input, index) {
7192
7207
  expected_path: stringValue2(input.expected_path),
7193
7208
  expected_routes: expectedRoutes,
7194
7209
  selector: stringValue2(input.selector),
7210
+ expected_texts: expectedTexts,
7195
7211
  link_selector: stringValue2(input.link_selector) || stringValue2(input.linkSelector),
7196
7212
  source_selector: stringValue2(input.source_selector) || stringValue2(input.sourceSelector),
7197
7213
  route_path_prefix: stringValue2(input.route_path_prefix) || stringValue2(input.routePathPrefix),
@@ -7207,6 +7223,7 @@ function normalizeCheck(input, index) {
7207
7223
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
7208
7224
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
7209
7225
  run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
7226
+ run_all_viewports: input.run_all_viewports === true || input.runAllViewports === true,
7210
7227
  require_unique_routes: input.require_unique_routes === false || input.requireUniqueRoutes === false ? false : true,
7211
7228
  allow_unexpected_routes: input.allow_unexpected_routes === true || input.allowUnexpectedRoutes === true,
7212
7229
  save_route_screenshots: input.save_route_screenshots === true || input.saveRouteScreenshots === true
@@ -7297,6 +7314,47 @@ function selectorKey(check) {
7297
7314
  function textKey(check) {
7298
7315
  return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
7299
7316
  }
7317
+ function textSequenceForCheck(viewport, check) {
7318
+ const key = selectorKey(check);
7319
+ const sequence = viewport.text_sequences?.[key];
7320
+ if (isRecord(sequence)) {
7321
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
7322
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
7323
+ const candidates = visibleTexts.length ? visibleTexts : texts;
7324
+ return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
7325
+ }
7326
+ return [];
7327
+ }
7328
+ function textOrderMatch(texts, expectedTexts) {
7329
+ const positions = [];
7330
+ let startAt = 0;
7331
+ for (const expected of expectedTexts) {
7332
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
7333
+ if (index < 0) return { matched: false, positions };
7334
+ positions.push(index);
7335
+ startAt = index + 1;
7336
+ }
7337
+ return { matched: true, positions };
7338
+ }
7339
+ function summarizeRouteInventory(viewport, inventory) {
7340
+ const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
7341
+ const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
7342
+ const sourceLinkCount = numberValue(inventory.source_link_count) ?? numberValue(inventory.home_game_link_count) ?? null;
7343
+ const sourceUniqueLinkCount = numberValue(inventory.source_unique_link_count) ?? numberValue(inventory.home_unique_game_link_count) ?? null;
7344
+ const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path7) => String(path7)) : [];
7345
+ const duplicateSourceLinkCount = numberValue(inventory.duplicate_source_link_count) ?? (sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null);
7346
+ const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
7347
+ return {
7348
+ viewport,
7349
+ source_link_count: sourceLinkCount,
7350
+ source_unique_link_count: sourceUniqueLinkCount,
7351
+ duplicate_source_link_count: duplicateSourceLinkCount,
7352
+ duplicate_source_links: duplicateSourceLinks,
7353
+ direct_route_count: directRoutes.length,
7354
+ clickthrough_count: clickthroughs.length,
7355
+ failure_count: failures.length
7356
+ };
7357
+ }
7300
7358
  function matchText(sample, check) {
7301
7359
  if (check.pattern) {
7302
7360
  try {
@@ -7416,6 +7474,32 @@ function assessCheckFromEvidence(check, evidence) {
7416
7474
  message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
7417
7475
  };
7418
7476
  }
7477
+ if (check.type === "selector_text_order") {
7478
+ const key = selectorKey(check);
7479
+ const expectedTexts = check.expected_texts || [];
7480
+ const results = viewports.map((viewport) => {
7481
+ const texts = textSequenceForCheck(viewport, check);
7482
+ const match = textOrderMatch(texts, expectedTexts);
7483
+ return {
7484
+ viewport: viewport.name,
7485
+ matched: match.matched,
7486
+ matched_positions: match.positions,
7487
+ visible_texts: texts.slice(0, 20)
7488
+ };
7489
+ });
7490
+ const failed = results.filter((result) => !result.matched).length;
7491
+ return {
7492
+ type: check.type,
7493
+ label: checkLabel(check),
7494
+ status: failed ? "failed" : "passed",
7495
+ evidence: {
7496
+ selector: key,
7497
+ expected_texts: expectedTexts,
7498
+ viewports: results.map((result) => toJsonValue(result))
7499
+ },
7500
+ message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
7501
+ };
7502
+ }
7419
7503
  if (check.type === "text_visible" || check.type === "text_absent") {
7420
7504
  const key = textKey(check);
7421
7505
  const expectedVisible = check.type === "text_visible";
@@ -7447,6 +7531,7 @@ function assessCheckFromEvidence(check, evidence) {
7447
7531
  message: "No route inventory evidence was captured."
7448
7532
  };
7449
7533
  }
7534
+ const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory));
7450
7535
  const failures = inventories.flatMap((item) => Array.isArray(item.inventory?.failures) ? item.inventory.failures.map((failure) => toJsonValue({ viewport: item.viewport, failure })) : []);
7451
7536
  const first = inventories[0]?.inventory;
7452
7537
  const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
@@ -7470,6 +7555,8 @@ function assessCheckFromEvidence(check, evidence) {
7470
7555
  homepage_unique_link_count: numberValue(first?.home_unique_game_link_count) ?? sourceUniqueLinkCount,
7471
7556
  direct_route_count: directRoutes.length,
7472
7557
  clickthrough_count: clickthroughs.length,
7558
+ viewport_count: inventories.length,
7559
+ viewports: viewportSummaries.map((summary) => toJsonValue(summary)),
7473
7560
  failures
7474
7561
  },
7475
7562
  message: failures.length ? `Route inventory failed with ${failures.length} issue(s).` : void 0
@@ -7750,6 +7837,47 @@ function textMatches(sample, check) {
7750
7837
  }
7751
7838
  return String(sample || "").includes(check.text || "");
7752
7839
  }
7840
+ function textSequenceForCheck(viewport, check) {
7841
+ const key = check.selector || "";
7842
+ const sequence = viewport.text_sequences && viewport.text_sequences[key];
7843
+ if (sequence && typeof sequence === "object") {
7844
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
7845
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
7846
+ const candidates = visibleTexts.length ? visibleTexts : texts;
7847
+ return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
7848
+ }
7849
+ return [];
7850
+ }
7851
+ function textOrderMatch(texts, expectedTexts) {
7852
+ const positions = [];
7853
+ let startAt = 0;
7854
+ for (const expected of expectedTexts || []) {
7855
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
7856
+ if (index < 0) return { matched: false, positions };
7857
+ positions.push(index);
7858
+ startAt = index + 1;
7859
+ }
7860
+ return { matched: true, positions };
7861
+ }
7862
+ function summarizeRouteInventory(viewport, inventory) {
7863
+ const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
7864
+ const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
7865
+ const sourceLinkCount = typeof inventory.source_link_count === "number" ? inventory.source_link_count : typeof inventory.home_game_link_count === "number" ? inventory.home_game_link_count : null;
7866
+ const sourceUniqueLinkCount = typeof inventory.source_unique_link_count === "number" ? inventory.source_unique_link_count : typeof inventory.home_unique_game_link_count === "number" ? inventory.home_unique_game_link_count : null;
7867
+ const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path) => String(path)) : [];
7868
+ const duplicateSourceLinkCount = typeof inventory.duplicate_source_link_count === "number" ? inventory.duplicate_source_link_count : sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null;
7869
+ const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
7870
+ return {
7871
+ viewport,
7872
+ source_link_count: sourceLinkCount,
7873
+ source_unique_link_count: sourceUniqueLinkCount,
7874
+ duplicate_source_link_count: duplicateSourceLinkCount,
7875
+ duplicate_source_links: duplicateSourceLinks,
7876
+ direct_route_count: directRoutes.length,
7877
+ clickthrough_count: clickthroughs.length,
7878
+ failure_count: failures.length,
7879
+ };
7880
+ }
7753
7881
  function numberValue(value) {
7754
7882
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
7755
7883
  }
@@ -7954,6 +8082,29 @@ function assessProfile(profile, evidence) {
7954
8082
  });
7955
8083
  continue;
7956
8084
  }
8085
+ if (check.type === "selector_text_order") {
8086
+ const selector = check.selector || "";
8087
+ const expectedTexts = check.expected_texts || [];
8088
+ const results = viewports.map((viewport) => {
8089
+ const texts = textSequenceForCheck(viewport, check);
8090
+ const match = textOrderMatch(texts, expectedTexts);
8091
+ return {
8092
+ viewport: viewport.name,
8093
+ matched: match.matched,
8094
+ matched_positions: match.positions,
8095
+ visible_texts: texts.slice(0, 20),
8096
+ };
8097
+ });
8098
+ const failed = results.filter((result) => !result.matched).length;
8099
+ checks.push({
8100
+ type: check.type,
8101
+ label: check.label || check.type,
8102
+ status: failed ? "failed" : "passed",
8103
+ evidence: { selector, expected_texts: expectedTexts, viewports: results },
8104
+ message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
8105
+ });
8106
+ continue;
8107
+ }
7957
8108
  if (check.type === "text_visible" || check.type === "text_absent") {
7958
8109
  const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
7959
8110
  const expectedVisible = check.type === "text_visible";
@@ -7982,6 +8133,7 @@ function assessProfile(profile, evidence) {
7982
8133
  });
7983
8134
  continue;
7984
8135
  }
8136
+ const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory || {}));
7985
8137
  const failures = [];
7986
8138
  for (const item of inventories) {
7987
8139
  for (const failure of Array.isArray(item.inventory.failures) ? item.inventory.failures : []) {
@@ -8010,6 +8162,8 @@ function assessProfile(profile, evidence) {
8010
8162
  homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : sourceUniqueLinkCount,
8011
8163
  direct_route_count: directRoutes.length,
8012
8164
  clickthrough_count: clickthroughs.length,
8165
+ viewport_count: inventories.length,
8166
+ viewports: viewportSummaries,
8013
8167
  failures,
8014
8168
  },
8015
8169
  message: failures.length ? "Route inventory failed with " + failures.length + " issue(s)." : undefined,
@@ -8327,6 +8481,27 @@ async function selectorStats(selector) {
8327
8481
  return { count: elements.length, visible_count: elements.filter(isVisible).length };
8328
8482
  }).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
8329
8483
  }
8484
+ async function selectorTextSequence(selector) {
8485
+ return page.locator(selector).evaluateAll((elements) => {
8486
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
8487
+ const isVisible = (element) => {
8488
+ const style = window.getComputedStyle(element);
8489
+ const rect = element.getBoundingClientRect();
8490
+ return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
8491
+ };
8492
+ const rows = elements.map((element, index) => ({
8493
+ index,
8494
+ text: compact(element.innerText || element.textContent || ""),
8495
+ visible: isVisible(element),
8496
+ }));
8497
+ return {
8498
+ count: rows.length,
8499
+ visible_count: rows.filter((row) => row.visible).length,
8500
+ texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
8501
+ visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
8502
+ };
8503
+ }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
8504
+ }
8330
8505
  function inventoryAppPathFromUrl(urlLike) {
8331
8506
  const url = new URL(String(urlLike), targetUrl);
8332
8507
  const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
@@ -8724,11 +8899,16 @@ async function captureViewport(viewport) {
8724
8899
  evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
8725
8900
  }));
8726
8901
  const selectors = {};
8902
+ const text_sequences = {};
8727
8903
  const text_matches = {};
8728
8904
  for (const check of profile.checks || []) {
8729
8905
  if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
8730
8906
  selectors[check.selector] = await selectorStats(check.selector);
8731
8907
  }
8908
+ if (check.type === "selector_text_order" && check.selector) {
8909
+ selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
8910
+ text_sequences[check.selector] = await selectorTextSequence(check.selector);
8911
+ }
8732
8912
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
8733
8913
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
8734
8914
  }
@@ -8742,7 +8922,7 @@ async function captureViewport(viewport) {
8742
8922
  let routeInventory;
8743
8923
  const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
8744
8924
  const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
8745
- if (routeInventoryCheck && (!firstViewportName || viewport.name === firstViewportName)) {
8925
+ if (routeInventoryCheck && (routeInventoryCheck.run_all_viewports || !firstViewportName || viewport.name === firstViewportName)) {
8746
8926
  try {
8747
8927
  routeInventory = await collectRouteInventory(routeInventoryCheck, viewport);
8748
8928
  } catch (error) {
@@ -8792,6 +8972,7 @@ async function captureViewport(viewport) {
8792
8972
  bounds_overflow_px: dom.bounds_overflow_px,
8793
8973
  overflow_offenders: dom.overflow_offenders || [],
8794
8974
  selectors,
8975
+ text_sequences,
8795
8976
  text_matches,
8796
8977
  route_inventory: routeInventory,
8797
8978
  setup_action_results: setupActionResults,
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  profileStatusExitCode,
11
11
  resolveRiddleProofProfileTargetUrl,
12
12
  resolveRiddleProofProfileTimeoutSec
13
- } from "./chunk-PWGJQLOS.js";
13
+ } from "./chunk-VEK6QHPE.js";
14
14
  import {
15
15
  createRiddleApiClient,
16
16
  parseRiddleViewport