@riddledc/riddle-proof 0.7.31 → 0.7.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/profile.cjs CHANGED
@@ -59,6 +59,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
59
59
  "selector_count_at_least",
60
60
  "text_visible",
61
61
  "text_absent",
62
+ "route_inventory",
62
63
  "no_horizontal_overflow",
63
64
  "no_mobile_horizontal_overflow",
64
65
  "no_fatal_console_errors"
@@ -308,6 +309,33 @@ function normalizeNetworkMocks(value) {
308
309
  if (!Array.isArray(value)) throw new Error("target.network_mocks must be an array.");
309
310
  return value.map(normalizeNetworkMock);
310
311
  }
312
+ function normalizeRouteInventoryPath(value, label) {
313
+ const path = stringValue(value);
314
+ if (!path) throw new Error(`${label} requires path.`);
315
+ if (!path.startsWith("/")) throw new Error(`${label}.path must start with /.`);
316
+ return normalizeRoutePath(path);
317
+ }
318
+ function normalizeRouteInventoryRoute(input, index) {
319
+ if (typeof input === "string") return { path: normalizeRouteInventoryPath(input, `checks route_inventory expected_routes[${index}]`) };
320
+ if (!isRecord(input)) throw new Error(`checks route_inventory expected_routes[${index}] must be a string or object.`);
321
+ return {
322
+ name: stringValue(input.name),
323
+ path: normalizeRouteInventoryPath(input.path, `checks route_inventory expected_routes[${index}]`)
324
+ };
325
+ }
326
+ function normalizeRouteInventoryRoutes(value, index) {
327
+ if (value === void 0) return void 0;
328
+ if (!Array.isArray(value) || value.length === 0) {
329
+ throw new Error(`checks[${index}] route_inventory expected_routes must be a non-empty array.`);
330
+ }
331
+ const routes = value.map(normalizeRouteInventoryRoute);
332
+ const seen = /* @__PURE__ */ new Set();
333
+ for (const route of routes) {
334
+ if (seen.has(route.path)) throw new Error(`checks[${index}] route_inventory expected_routes contains duplicate path ${route.path}.`);
335
+ seen.add(route.path);
336
+ }
337
+ return routes;
338
+ }
311
339
  function normalizeCheck(input, index) {
312
340
  if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
313
341
  const type = stringValue(input.type);
@@ -324,16 +352,34 @@ function normalizeCheck(input, index) {
324
352
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
325
353
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
326
354
  }
355
+ const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
356
+ if (type === "route_inventory" && !expectedRoutes?.length) {
357
+ throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
358
+ }
327
359
  return {
328
360
  type,
329
361
  label: stringValue(input.label),
330
362
  expected_path: stringValue(input.expected_path),
363
+ expected_routes: expectedRoutes,
331
364
  selector: stringValue(input.selector),
365
+ link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
366
+ source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
367
+ route_path_prefix: stringValue(input.route_path_prefix) || stringValue(input.routePathPrefix),
368
+ route_ready_selector: stringValue(input.route_ready_selector) || stringValue(input.routeReadySelector),
369
+ route_ready_text: stringValue(input.route_ready_text) || stringValue(input.routeReadyText),
370
+ route_ready_pattern: stringValue(input.route_ready_pattern) || stringValue(input.routeReadyPattern),
371
+ route_ready_flags: stringValue(input.route_ready_flags) || stringValue(input.routeReadyFlags),
332
372
  text: stringValue(input.text),
333
373
  pattern: stringValue(input.pattern),
334
374
  flags: stringValue(input.flags),
335
375
  min_count: numberValue(input.min_count),
336
- max_overflow_px: numberValue(input.max_overflow_px)
376
+ max_overflow_px: numberValue(input.max_overflow_px),
377
+ timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
378
+ run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
379
+ run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
380
+ require_unique_routes: input.require_unique_routes === false || input.requireUniqueRoutes === false ? false : true,
381
+ allow_unexpected_routes: input.allow_unexpected_routes === true || input.allowUnexpectedRoutes === true,
382
+ save_route_screenshots: input.save_route_screenshots === true || input.saveRouteScreenshots === true
337
383
  };
338
384
  }
339
385
  function normalizeFailurePolicy(input) {
@@ -560,6 +606,36 @@ function assessCheckFromEvidence(check, evidence) {
560
606
  message: failed ? `Text assertion failed in ${failed} viewport(s).` : void 0
561
607
  };
562
608
  }
609
+ if (check.type === "route_inventory") {
610
+ const inventories = viewports.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory })).filter((item) => isRecord(item.inventory));
611
+ if (!inventories.length) {
612
+ return {
613
+ type: check.type,
614
+ label: checkLabel(check),
615
+ status: "failed",
616
+ evidence: { expected_count: check.expected_routes?.length || 0 },
617
+ message: "No route inventory evidence was captured."
618
+ };
619
+ }
620
+ const failures = inventories.flatMap((item) => Array.isArray(item.inventory?.failures) ? item.inventory.failures.map((failure) => toJsonValue({ viewport: item.viewport, failure })) : []);
621
+ const first = inventories[0]?.inventory;
622
+ const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
623
+ const clickthroughs = Array.isArray(first?.clickthroughs) ? first.clickthroughs : [];
624
+ return {
625
+ type: check.type,
626
+ label: checkLabel(check),
627
+ status: failures.length ? "failed" : "passed",
628
+ evidence: {
629
+ 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,
632
+ direct_route_count: directRoutes.length,
633
+ clickthrough_count: clickthroughs.length,
634
+ failures
635
+ },
636
+ message: failures.length ? `Route inventory failed with ${failures.length} issue(s).` : void 0
637
+ };
638
+ }
563
639
  if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
564
640
  const maxOverflow = check.max_overflow_px ?? 4;
565
641
  const applicable = check.type === "no_mobile_horizontal_overflow" ? viewports.filter((viewport) => viewport.width <= 820) : viewports;
@@ -1069,6 +1145,45 @@ function assessProfile(profile, evidence) {
1069
1145
  });
1070
1146
  continue;
1071
1147
  }
1148
+ if (check.type === "route_inventory") {
1149
+ const inventories = viewports
1150
+ .map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory }))
1151
+ .filter((item) => item.inventory && typeof item.inventory === "object");
1152
+ if (!inventories.length) {
1153
+ checks.push({
1154
+ type: check.type,
1155
+ label: check.label || check.type,
1156
+ status: "failed",
1157
+ evidence: { expected_count: (check.expected_routes || []).length },
1158
+ message: "No route inventory evidence was captured.",
1159
+ });
1160
+ continue;
1161
+ }
1162
+ const failures = [];
1163
+ for (const item of inventories) {
1164
+ for (const failure of Array.isArray(item.inventory.failures) ? item.inventory.failures : []) {
1165
+ failures.push({ viewport: item.viewport, failure });
1166
+ }
1167
+ }
1168
+ const first = inventories[0].inventory || {};
1169
+ const directRoutes = Array.isArray(first.direct_routes) ? first.direct_routes : [];
1170
+ const clickthroughs = Array.isArray(first.clickthroughs) ? first.clickthroughs : [];
1171
+ checks.push({
1172
+ type: check.type,
1173
+ label: check.label || check.type,
1174
+ status: failures.length ? "failed" : "passed",
1175
+ evidence: {
1176
+ 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,
1179
+ direct_route_count: directRoutes.length,
1180
+ clickthrough_count: clickthroughs.length,
1181
+ failures,
1182
+ },
1183
+ message: failures.length ? "Route inventory failed with " + failures.length + " issue(s)." : undefined,
1184
+ });
1185
+ continue;
1186
+ }
1072
1187
  if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
1073
1188
  const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
1074
1189
  const applicable = check.type === "no_mobile_horizontal_overflow" ? viewports.filter((viewport) => viewport.width <= 820) : viewports;
@@ -1380,6 +1495,290 @@ async function selectorStats(selector) {
1380
1495
  return { count: elements.length, visible_count: elements.filter(isVisible).length };
1381
1496
  }).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
1382
1497
  }
1498
+ function inventoryAppPathFromUrl(urlLike) {
1499
+ const url = new URL(String(urlLike), targetUrl);
1500
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
1501
+ let pathname = url.pathname || "/";
1502
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
1503
+ pathname = pathname.slice(mountPrefix.length) || "/";
1504
+ }
1505
+ return normalizeRoutePath(pathname);
1506
+ }
1507
+ function inventoryRouteUrl(expectedPath) {
1508
+ const base = new URL(targetUrl);
1509
+ const route = new URL(expectedPath, base.origin);
1510
+ const mountPrefix = previewMountPrefix(base.pathname);
1511
+ base.pathname = mountPrefix ? joinMountedRoutePath(mountPrefix, route.pathname) : route.pathname;
1512
+ base.search = route.search;
1513
+ base.hash = route.hash;
1514
+ return base.href;
1515
+ }
1516
+ function inventorySlugFromPath(path) {
1517
+ return String(path || "route").split("/").filter(Boolean).pop()?.replace(/[^a-z0-9]+/gi, "-").toLowerCase() || "route";
1518
+ }
1519
+ function inventoryCheckTimeout(check) {
1520
+ const timeout = Number(check && check.timeout_ms);
1521
+ return Number.isFinite(timeout) && timeout > 0 ? timeout : 45000;
1522
+ }
1523
+ async function collectInventoryHomeLinks(check) {
1524
+ const linkSelector = check.link_selector || "a[href]";
1525
+ const expectedPaths = (check.expected_routes || []).map((route) => route.path);
1526
+ const routePathPrefix = check.route_path_prefix || "";
1527
+ return await page.evaluate(({ linkSelector, expectedPaths, routePathPrefix, targetUrl }) => {
1528
+ const previewMountPrefix = (pathname) => {
1529
+ const value = pathname || "/";
1530
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
1531
+ if (apiPreview) return apiPreview[1];
1532
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
1533
+ return internalPreview ? internalPreview[1] : "";
1534
+ };
1535
+ const normalizeRoutePath = (path) => {
1536
+ const value = path || "/";
1537
+ if (value === "/") return "/";
1538
+ return value.replace(/\/+$/, "") || "/";
1539
+ };
1540
+ const appPathFromHref = (href) => {
1541
+ const url = new URL(href, location.href);
1542
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
1543
+ let pathname = url.pathname || "/";
1544
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
1545
+ pathname = pathname.slice(mountPrefix.length) || "/";
1546
+ }
1547
+ return normalizeRoutePath(pathname);
1548
+ };
1549
+ const expectedSet = new Set(expectedPaths);
1550
+ const links = Array.from(document.querySelectorAll(linkSelector)).map((anchor) => {
1551
+ const href = anchor.getAttribute("href") || "";
1552
+ const appPath = appPathFromHref(href || anchor.href || location.href);
1553
+ return {
1554
+ text: (anchor.textContent || "").replace(/\s+/g, " ").trim().slice(0, 240),
1555
+ href,
1556
+ pathname: new URL(href || anchor.href || location.href, location.href).pathname,
1557
+ app_path: appPath,
1558
+ };
1559
+ });
1560
+ return links.filter((link) => (
1561
+ routePathPrefix ? link.app_path.startsWith(routePathPrefix) : expectedSet.has(link.app_path)
1562
+ ));
1563
+ }, { linkSelector, expectedPaths, routePathPrefix, targetUrl });
1564
+ }
1565
+ async function waitForInventoryRouteHealth(check, expectedPath) {
1566
+ const timeout = inventoryCheckTimeout(check);
1567
+ await page.waitForURL((url) => inventoryAppPathFromUrl(url.href) === normalizeRoutePath(expectedPath), { timeout: Math.min(timeout, 20000) });
1568
+ if (check.route_ready_selector) {
1569
+ await page.waitForSelector(check.route_ready_selector, { state: "visible", timeout });
1570
+ return;
1571
+ }
1572
+ await page.waitForFunction(({ expectedPath, sourceSelector, routeReadyText, routeReadyPattern, routeReadyFlags, targetUrl }) => {
1573
+ const previewMountPrefix = (pathname) => {
1574
+ const value = pathname || "/";
1575
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
1576
+ if (apiPreview) return apiPreview[1];
1577
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
1578
+ return internalPreview ? internalPreview[1] : "";
1579
+ };
1580
+ const normalizeRoutePath = (path) => {
1581
+ const value = path || "/";
1582
+ if (value === "/") return "/";
1583
+ return value.replace(/\/+$/, "") || "/";
1584
+ };
1585
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
1586
+ let pathname = location.pathname || "/";
1587
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
1588
+ pathname = pathname.slice(mountPrefix.length) || "/";
1589
+ }
1590
+ if (normalizeRoutePath(pathname) !== normalizeRoutePath(expectedPath)) return false;
1591
+ if (sourceSelector && document.querySelector(sourceSelector)) return false;
1592
+ const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
1593
+ if (routeReadyPattern) {
1594
+ try {
1595
+ if (!new RegExp(routeReadyPattern, routeReadyFlags || "").test(text)) return false;
1596
+ } catch {
1597
+ return false;
1598
+ }
1599
+ }
1600
+ if (routeReadyText && !text.includes(routeReadyText)) return false;
1601
+ const loadingOnly = /^loading\.?$/i.test(text) || (/^loading/i.test(text) && text.length < 40);
1602
+ if (document.querySelectorAll("canvas").length > 0) return true;
1603
+ if (document.querySelectorAll("button").length > 0 && !loadingOnly) return true;
1604
+ return text.length > 30 && !loadingOnly && !/not found|cannot get/i.test(text);
1605
+ }, {
1606
+ expectedPath,
1607
+ sourceSelector: check.source_selector || "",
1608
+ routeReadyText: check.route_ready_text || "",
1609
+ routeReadyPattern: check.route_ready_pattern || "",
1610
+ routeReadyFlags: check.route_ready_flags || "",
1611
+ targetUrl,
1612
+ }, { timeout });
1613
+ }
1614
+ async function collectInventoryRouteSnapshot(expectedRoute, phase, check, error) {
1615
+ return await page.evaluate(({ expectedRoute, phase, sourceSelector, error, targetUrl }) => {
1616
+ const previewMountPrefix = (pathname) => {
1617
+ const value = pathname || "/";
1618
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
1619
+ if (apiPreview) return apiPreview[1];
1620
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
1621
+ return internalPreview ? internalPreview[1] : "";
1622
+ };
1623
+ const normalizeRoutePath = (path) => {
1624
+ const value = path || "/";
1625
+ if (value === "/") return "/";
1626
+ return value.replace(/\/+$/, "") || "/";
1627
+ };
1628
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
1629
+ let pathname = location.pathname || "/";
1630
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
1631
+ pathname = pathname.slice(mountPrefix.length) || "/";
1632
+ }
1633
+ const sourceCount = sourceSelector ? document.querySelectorAll(sourceSelector).length : 0;
1634
+ const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
1635
+ return {
1636
+ phase,
1637
+ name: expectedRoute.name || null,
1638
+ path: expectedRoute.path,
1639
+ loaded: !error,
1640
+ error: error || null,
1641
+ actual_path: location.pathname,
1642
+ actual_app_path: normalizeRoutePath(pathname),
1643
+ source_selector: sourceSelector || null,
1644
+ source_count: sourceCount,
1645
+ source_visible: sourceCount > 0,
1646
+ title: document.title,
1647
+ body_text_sample: text.slice(0, 900),
1648
+ canvas_count: document.querySelectorAll("canvas").length,
1649
+ button_texts: Array.from(document.querySelectorAll("button")).slice(0, 12).map((button) => (
1650
+ (button.textContent || "").replace(/\s+/g, " ").trim()
1651
+ )),
1652
+ heading_texts: Array.from(document.querySelectorAll("h1,h2,h3")).slice(0, 8).map((heading) => (
1653
+ (heading.textContent || "").replace(/\s+/g, " ").trim()
1654
+ )),
1655
+ };
1656
+ }, { expectedRoute, phase, sourceSelector: check.source_selector || "", error: error || "", targetUrl });
1657
+ }
1658
+ async function findInventoryLinkIndex(check, expectedPath) {
1659
+ const locator = page.locator(check.link_selector || "a[href]");
1660
+ const count = await locator.count();
1661
+ for (let index = 0; index < count; index += 1) {
1662
+ const appPath = await locator.nth(index).evaluate((anchor, targetUrl) => {
1663
+ const previewMountPrefix = (pathname) => {
1664
+ const value = pathname || "/";
1665
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
1666
+ if (apiPreview) return apiPreview[1];
1667
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
1668
+ return internalPreview ? internalPreview[1] : "";
1669
+ };
1670
+ const normalizeRoutePath = (path) => {
1671
+ const value = path || "/";
1672
+ if (value === "/") return "/";
1673
+ return value.replace(/\/+$/, "") || "/";
1674
+ };
1675
+ const href = anchor.getAttribute("href") || anchor.href || location.href;
1676
+ const url = new URL(href, location.href);
1677
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
1678
+ let pathname = url.pathname || "/";
1679
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
1680
+ pathname = pathname.slice(mountPrefix.length) || "/";
1681
+ }
1682
+ return normalizeRoutePath(pathname);
1683
+ }, targetUrl).catch(() => "");
1684
+ if (appPath === normalizeRoutePath(expectedPath)) return index;
1685
+ }
1686
+ return -1;
1687
+ }
1688
+ async function collectRouteInventory(check, viewport) {
1689
+ const expectedRoutes = check.expected_routes || [];
1690
+ const expectedPaths = expectedRoutes.map((route) => normalizeRoutePath(route.path));
1691
+ const expectedSet = new Set(expectedPaths);
1692
+ const failures = [];
1693
+ const homeLinks = await collectInventoryHomeLinks(check);
1694
+ const homeLinkPaths = homeLinks.map((link) => link.app_path);
1695
+ const uniqueHomeLinkPaths = Array.from(new Set(homeLinkPaths));
1696
+ const duplicateHomeLinkPaths = homeLinkPaths.filter((path, index) => homeLinkPaths.indexOf(path) !== index);
1697
+ if (check.require_unique_routes !== false && duplicateHomeLinkPaths.length) {
1698
+ failures.push({ code: "duplicate_source_links", paths: Array.from(new Set(duplicateHomeLinkPaths)) });
1699
+ }
1700
+ for (const route of expectedRoutes) {
1701
+ if (!homeLinkPaths.includes(normalizeRoutePath(route.path))) {
1702
+ failures.push({ code: "expected_route_missing_from_source", name: route.name || null, path: route.path });
1703
+ }
1704
+ }
1705
+ const unexpectedRoutes = uniqueHomeLinkPaths.filter((path) => !expectedSet.has(path));
1706
+ if (!check.allow_unexpected_routes && unexpectedRoutes.length) {
1707
+ failures.push({ code: "unexpected_source_links", paths: unexpectedRoutes });
1708
+ }
1709
+ if (!check.allow_unexpected_routes && uniqueHomeLinkPaths.length !== expectedRoutes.length) {
1710
+ failures.push({ code: "source_link_count_mismatch", expected: expectedRoutes.length, actual_unique: uniqueHomeLinkPaths.length, actual_total: homeLinkPaths.length });
1711
+ }
1712
+
1713
+ const directRoutes = [];
1714
+ if (check.run_direct_routes !== false) {
1715
+ for (const expectedRoute of expectedRoutes) {
1716
+ let error = "";
1717
+ try {
1718
+ await page.goto(inventoryRouteUrl(expectedRoute.path), { waitUntil: "domcontentloaded", timeout: 90000 });
1719
+ await waitForInventoryRouteHealth(check, expectedRoute.path);
1720
+ } catch (caught) {
1721
+ error = String(caught && caught.message ? caught.message : caught).slice(0, 1000);
1722
+ }
1723
+ const snapshot = await collectInventoryRouteSnapshot(expectedRoute, "direct", check, error);
1724
+ directRoutes.push(snapshot);
1725
+ if (check.save_route_screenshots) {
1726
+ await saveScreenshot(profileSlug + "-direct-" + inventorySlugFromPath(expectedRoute.path)).catch(() => {});
1727
+ }
1728
+ if (error) failures.push({ code: "direct_route_unhealthy", name: expectedRoute.name || null, path: expectedRoute.path, error });
1729
+ else if (snapshot.actual_app_path !== normalizeRoutePath(expectedRoute.path)) failures.push({ code: "direct_route_wrong_path", name: expectedRoute.name || null, path: expectedRoute.path, actual_app_path: snapshot.actual_app_path });
1730
+ else if (check.source_selector && snapshot.source_visible) failures.push({ code: "direct_route_kept_source_surface", name: expectedRoute.name || null, path: expectedRoute.path, source_selector: check.source_selector });
1731
+ }
1732
+ }
1733
+
1734
+ const clickthroughs = [];
1735
+ if (check.run_clickthroughs !== false) {
1736
+ for (const expectedRoute of expectedRoutes) {
1737
+ const result = { name: expectedRoute.name || null, path: expectedRoute.path, clicked: false, snapshot: null, error: null };
1738
+ try {
1739
+ await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 90000 });
1740
+ if (profile.target.wait_for_selector) {
1741
+ await page.waitForSelector(profile.target.wait_for_selector, { state: "visible", timeout: 15000 }).catch(() => {});
1742
+ }
1743
+ const index = await findInventoryLinkIndex(check, expectedRoute.path);
1744
+ if (index < 0) {
1745
+ result.error = "source_link_not_found";
1746
+ failures.push({ code: "source_link_clickthrough_missing", name: expectedRoute.name || null, path: expectedRoute.path });
1747
+ } else {
1748
+ const locator = page.locator(check.link_selector || "a[href]").nth(index);
1749
+ await locator.scrollIntoViewIfNeeded();
1750
+ await locator.click({ timeout: inventoryCheckTimeout(check), noWaitAfter: true });
1751
+ result.clicked = true;
1752
+ await waitForInventoryRouteHealth(check, expectedRoute.path);
1753
+ }
1754
+ } catch (caught) {
1755
+ result.error = String(caught && caught.message ? caught.message : caught).slice(0, 1000);
1756
+ }
1757
+ result.snapshot = await collectInventoryRouteSnapshot(expectedRoute, "clickthrough", check, result.error || "");
1758
+ clickthroughs.push(result);
1759
+ if (check.save_route_screenshots) {
1760
+ await saveScreenshot(profileSlug + "-click-" + inventorySlugFromPath(expectedRoute.path)).catch(() => {});
1761
+ }
1762
+ if (result.error && result.error !== "source_link_not_found") failures.push({ code: "source_link_clickthrough_unhealthy", name: expectedRoute.name || null, path: expectedRoute.path, error: result.error });
1763
+ else if (result.snapshot.actual_app_path !== normalizeRoutePath(expectedRoute.path)) failures.push({ code: "source_link_clickthrough_wrong_path", name: expectedRoute.name || null, path: expectedRoute.path, actual_app_path: result.snapshot.actual_app_path });
1764
+ else if (check.source_selector && result.snapshot.source_visible) failures.push({ code: "source_link_clickthrough_kept_source_surface", name: expectedRoute.name || null, path: expectedRoute.path, source_selector: check.source_selector });
1765
+ }
1766
+ }
1767
+
1768
+ return {
1769
+ version: "riddle-proof.route-inventory.v1",
1770
+ viewport: viewport.name,
1771
+ expected_routes: expectedRoutes,
1772
+ link_selector: check.link_selector || "a[href]",
1773
+ source_selector: check.source_selector || null,
1774
+ home_game_link_count: homeLinkPaths.length,
1775
+ home_unique_game_link_count: uniqueHomeLinkPaths.length,
1776
+ home_links: homeLinks,
1777
+ direct_routes: directRoutes,
1778
+ clickthroughs,
1779
+ failures,
1780
+ };
1781
+ }
1383
1782
  async function captureViewport(viewport) {
1384
1783
  await page.setViewportSize({ width: viewport.width, height: viewport.height });
1385
1784
  let httpStatus = null;
@@ -1502,6 +1901,31 @@ async function captureViewport(viewport) {
1502
1901
  } catch (error) {
1503
1902
  pageErrors.push({ message: "saveScreenshot failed: " + String(error && error.message ? error.message : error).slice(0, 500) });
1504
1903
  }
1904
+ let routeInventory;
1905
+ const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
1906
+ const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
1907
+ if (routeInventoryCheck && (!firstViewportName || viewport.name === firstViewportName)) {
1908
+ try {
1909
+ routeInventory = await collectRouteInventory(routeInventoryCheck, viewport);
1910
+ } catch (error) {
1911
+ routeInventory = {
1912
+ version: "riddle-proof.route-inventory.v1",
1913
+ viewport: viewport.name,
1914
+ expected_routes: routeInventoryCheck.expected_routes || [],
1915
+ link_selector: routeInventoryCheck.link_selector || "a[href]",
1916
+ source_selector: routeInventoryCheck.source_selector || null,
1917
+ home_game_link_count: 0,
1918
+ home_unique_game_link_count: 0,
1919
+ home_links: [],
1920
+ direct_routes: [],
1921
+ clickthroughs: [],
1922
+ failures: [{
1923
+ code: "route_inventory_capture_failed",
1924
+ error: String(error && error.message ? error.message : error).slice(0, 1000),
1925
+ }],
1926
+ };
1927
+ }
1928
+ }
1505
1929
  const expectedPath = profile.checks.find((check) => check.type === "route_loaded" && check.expected_path)?.expected_path || new URL(targetUrl).pathname || "/";
1506
1930
  return {
1507
1931
  name: viewport.name,
@@ -1526,6 +1950,7 @@ async function captureViewport(viewport) {
1526
1950
  overflow_offenders: dom.overflow_offenders || [],
1527
1951
  selectors,
1528
1952
  text_matches,
1953
+ route_inventory: routeInventory,
1529
1954
  setup_action_results: setupActionResults,
1530
1955
  screenshot_label: screenshotLabel,
1531
1956
  navigation_error: navigationError,
@@ -1558,6 +1983,16 @@ function buildProfileEvidence(currentViewports) {
1558
1983
  overflow_px: currentViewports.map((viewport) => viewport.overflow_px),
1559
1984
  bounds_overflow_px: currentViewports.map((viewport) => viewport.bounds_overflow_px),
1560
1985
  overflow_offender_counts: currentViewports.map((viewport) => (viewport.overflow_offenders || []).length),
1986
+ route_inventory: currentViewports
1987
+ .filter((viewport) => viewport.route_inventory)
1988
+ .map((viewport) => ({
1989
+ viewport: viewport.name,
1990
+ expected_count: (viewport.route_inventory.expected_routes || []).length,
1991
+ home_unique_game_link_count: viewport.route_inventory.home_unique_game_link_count,
1992
+ direct_route_count: (viewport.route_inventory.direct_routes || []).length,
1993
+ clickthrough_count: (viewport.route_inventory.clickthroughs || []).length,
1994
+ failure_count: (viewport.route_inventory.failures || []).length,
1995
+ })),
1561
1996
  network_mock_count: (profile.target.network_mocks || []).length,
1562
1997
  network_mock_hit_count: networkMockEvents.filter((event) => event.ok !== false).length,
1563
1998
  },
@@ -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", "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", "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];
@@ -45,6 +45,10 @@ interface RiddleProofProfileNetworkMock {
45
45
  body_json?: JsonValue;
46
46
  required?: boolean;
47
47
  }
48
+ interface RiddleProofProfileRouteInventoryRoute {
49
+ name?: string;
50
+ path: string;
51
+ }
48
52
  interface RiddleProofProfileTarget {
49
53
  url?: string;
50
54
  route?: string;
@@ -60,12 +64,26 @@ interface RiddleProofProfileCheck {
60
64
  type: RiddleProofProfileCheckType;
61
65
  label?: string;
62
66
  expected_path?: string;
67
+ expected_routes?: RiddleProofProfileRouteInventoryRoute[];
63
68
  selector?: string;
69
+ link_selector?: string;
70
+ source_selector?: string;
71
+ route_path_prefix?: string;
72
+ route_ready_selector?: string;
73
+ route_ready_text?: string;
74
+ route_ready_pattern?: string;
75
+ route_ready_flags?: string;
64
76
  text?: string;
65
77
  pattern?: string;
66
78
  flags?: string;
67
79
  min_count?: number;
68
80
  max_overflow_px?: number;
81
+ timeout_ms?: number;
82
+ run_direct_routes?: boolean;
83
+ run_clickthroughs?: boolean;
84
+ require_unique_routes?: boolean;
85
+ allow_unexpected_routes?: boolean;
86
+ save_route_screenshots?: boolean;
69
87
  }
70
88
  interface RiddleProofProfile {
71
89
  version: typeof RIDDLE_PROOF_PROFILE_VERSION;
@@ -120,6 +138,7 @@ interface RiddleProofProfileViewportEvidence {
120
138
  visible_count: number;
121
139
  }>;
122
140
  text_matches?: Record<string, boolean>;
141
+ route_inventory?: Record<string, JsonValue>;
123
142
  setup_action_results?: Array<Record<string, JsonValue>>;
124
143
  screenshot_label?: string;
125
144
  navigation_error?: string;
@@ -227,4 +246,4 @@ declare function buildRiddleProofProfileScript(profile: RiddleProofProfile): str
227
246
  declare function collectRiddleProfileArtifactRefs(input: unknown): RiddleProofProfileArtifactRef[];
228
247
  declare function extractRiddleProofProfileResult(input: unknown): RiddleProofProfileResult | undefined;
229
248
 
230
- export { type NormalizeRiddleProofProfileOptions, RIDDLE_PROOF_PROFILE_CHECK_TYPES, RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION, RIDDLE_PROOF_PROFILE_RESULT_VERSION, RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES, RIDDLE_PROOF_PROFILE_STATUSES, RIDDLE_PROOF_PROFILE_VERSION, type RiddleProofProfile, type RiddleProofProfileArtifactRef, type RiddleProofProfileBaselinePolicy, type RiddleProofProfileBoundsOffender, type RiddleProofProfileCheck, type RiddleProofProfileCheckResult, type RiddleProofProfileCheckType, type RiddleProofProfileEvidence, type RiddleProofProfileFailureAction, type RiddleProofProfileNetworkMock, type RiddleProofProfileResult, type RiddleProofProfileRouteEvidence, type RiddleProofProfileRunner, type RiddleProofProfileSetupAction, type RiddleProofProfileSetupActionType, type RiddleProofProfileStatus, type RiddleProofProfileTarget, type RiddleProofProfileViewport, type RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, extractRiddleProofProfileResult, normalizeRiddleProofProfile, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult };
249
+ export { type NormalizeRiddleProofProfileOptions, RIDDLE_PROOF_PROFILE_CHECK_TYPES, RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION, RIDDLE_PROOF_PROFILE_RESULT_VERSION, RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES, RIDDLE_PROOF_PROFILE_STATUSES, RIDDLE_PROOF_PROFILE_VERSION, type RiddleProofProfile, type RiddleProofProfileArtifactRef, type RiddleProofProfileBaselinePolicy, type RiddleProofProfileBoundsOffender, type RiddleProofProfileCheck, type RiddleProofProfileCheckResult, type RiddleProofProfileCheckType, type RiddleProofProfileEvidence, type RiddleProofProfileFailureAction, type RiddleProofProfileNetworkMock, type RiddleProofProfileResult, type RiddleProofProfileRouteEvidence, type RiddleProofProfileRouteInventoryRoute, type RiddleProofProfileRunner, type RiddleProofProfileSetupAction, type RiddleProofProfileSetupActionType, type RiddleProofProfileStatus, type RiddleProofProfileTarget, type RiddleProofProfileViewport, type RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, extractRiddleProofProfileResult, normalizeRiddleProofProfile, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult };
package/dist/profile.d.ts CHANGED
@@ -4,7 +4,7 @@ declare const RIDDLE_PROOF_PROFILE_VERSION: "riddle-proof.profile.v1";
4
4
  declare const RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION: "riddle-proof.profile-evidence.v1";
5
5
  declare const RIDDLE_PROOF_PROFILE_RESULT_VERSION: "riddle-proof.profile-result.v1";
6
6
  declare const RIDDLE_PROOF_PROFILE_STATUSES: readonly ["passed", "product_regression", "proof_insufficient", "environment_blocked", "configuration_error", "needs_human_review"];
7
- declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_count_at_least", "text_visible", "text_absent", "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", "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];
@@ -45,6 +45,10 @@ interface RiddleProofProfileNetworkMock {
45
45
  body_json?: JsonValue;
46
46
  required?: boolean;
47
47
  }
48
+ interface RiddleProofProfileRouteInventoryRoute {
49
+ name?: string;
50
+ path: string;
51
+ }
48
52
  interface RiddleProofProfileTarget {
49
53
  url?: string;
50
54
  route?: string;
@@ -60,12 +64,26 @@ interface RiddleProofProfileCheck {
60
64
  type: RiddleProofProfileCheckType;
61
65
  label?: string;
62
66
  expected_path?: string;
67
+ expected_routes?: RiddleProofProfileRouteInventoryRoute[];
63
68
  selector?: string;
69
+ link_selector?: string;
70
+ source_selector?: string;
71
+ route_path_prefix?: string;
72
+ route_ready_selector?: string;
73
+ route_ready_text?: string;
74
+ route_ready_pattern?: string;
75
+ route_ready_flags?: string;
64
76
  text?: string;
65
77
  pattern?: string;
66
78
  flags?: string;
67
79
  min_count?: number;
68
80
  max_overflow_px?: number;
81
+ timeout_ms?: number;
82
+ run_direct_routes?: boolean;
83
+ run_clickthroughs?: boolean;
84
+ require_unique_routes?: boolean;
85
+ allow_unexpected_routes?: boolean;
86
+ save_route_screenshots?: boolean;
69
87
  }
70
88
  interface RiddleProofProfile {
71
89
  version: typeof RIDDLE_PROOF_PROFILE_VERSION;
@@ -120,6 +138,7 @@ interface RiddleProofProfileViewportEvidence {
120
138
  visible_count: number;
121
139
  }>;
122
140
  text_matches?: Record<string, boolean>;
141
+ route_inventory?: Record<string, JsonValue>;
123
142
  setup_action_results?: Array<Record<string, JsonValue>>;
124
143
  screenshot_label?: string;
125
144
  navigation_error?: string;
@@ -227,4 +246,4 @@ declare function buildRiddleProofProfileScript(profile: RiddleProofProfile): str
227
246
  declare function collectRiddleProfileArtifactRefs(input: unknown): RiddleProofProfileArtifactRef[];
228
247
  declare function extractRiddleProofProfileResult(input: unknown): RiddleProofProfileResult | undefined;
229
248
 
230
- export { type NormalizeRiddleProofProfileOptions, RIDDLE_PROOF_PROFILE_CHECK_TYPES, RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION, RIDDLE_PROOF_PROFILE_RESULT_VERSION, RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES, RIDDLE_PROOF_PROFILE_STATUSES, RIDDLE_PROOF_PROFILE_VERSION, type RiddleProofProfile, type RiddleProofProfileArtifactRef, type RiddleProofProfileBaselinePolicy, type RiddleProofProfileBoundsOffender, type RiddleProofProfileCheck, type RiddleProofProfileCheckResult, type RiddleProofProfileCheckType, type RiddleProofProfileEvidence, type RiddleProofProfileFailureAction, type RiddleProofProfileNetworkMock, type RiddleProofProfileResult, type RiddleProofProfileRouteEvidence, type RiddleProofProfileRunner, type RiddleProofProfileSetupAction, type RiddleProofProfileSetupActionType, type RiddleProofProfileStatus, type RiddleProofProfileTarget, type RiddleProofProfileViewport, type RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, extractRiddleProofProfileResult, normalizeRiddleProofProfile, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult };
249
+ export { type NormalizeRiddleProofProfileOptions, RIDDLE_PROOF_PROFILE_CHECK_TYPES, RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION, RIDDLE_PROOF_PROFILE_RESULT_VERSION, RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES, RIDDLE_PROOF_PROFILE_STATUSES, RIDDLE_PROOF_PROFILE_VERSION, type RiddleProofProfile, type RiddleProofProfileArtifactRef, type RiddleProofProfileBaselinePolicy, type RiddleProofProfileBoundsOffender, type RiddleProofProfileCheck, type RiddleProofProfileCheckResult, type RiddleProofProfileCheckType, type RiddleProofProfileEvidence, type RiddleProofProfileFailureAction, type RiddleProofProfileNetworkMock, type RiddleProofProfileResult, type RiddleProofProfileRouteEvidence, type RiddleProofProfileRouteInventoryRoute, type RiddleProofProfileRunner, type RiddleProofProfileSetupAction, type RiddleProofProfileSetupActionType, type RiddleProofProfileStatus, type RiddleProofProfileTarget, type RiddleProofProfileViewport, type RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, extractRiddleProofProfileResult, normalizeRiddleProofProfile, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult };
package/dist/profile.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  resolveRiddleProofProfileTimeoutSec,
20
20
  slugifyRiddleProofProfileName,
21
21
  summarizeRiddleProofProfileResult
22
- } from "./chunk-2NWVXT4Q.js";
22
+ } from "./chunk-COUZXKGM.js";
23
23
  export {
24
24
  RIDDLE_PROOF_PROFILE_CHECK_TYPES,
25
25
  RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION,