@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/cli.cjs CHANGED
@@ -6889,6 +6889,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
6889
6889
  "selector_count_at_least",
6890
6890
  "text_visible",
6891
6891
  "text_absent",
6892
+ "route_inventory",
6892
6893
  "no_horizontal_overflow",
6893
6894
  "no_mobile_horizontal_overflow",
6894
6895
  "no_fatal_console_errors"
@@ -7138,6 +7139,33 @@ function normalizeNetworkMocks(value) {
7138
7139
  if (!Array.isArray(value)) throw new Error("target.network_mocks must be an array.");
7139
7140
  return value.map(normalizeNetworkMock);
7140
7141
  }
7142
+ function normalizeRouteInventoryPath(value, label) {
7143
+ const path7 = stringValue2(value);
7144
+ if (!path7) throw new Error(`${label} requires path.`);
7145
+ if (!path7.startsWith("/")) throw new Error(`${label}.path must start with /.`);
7146
+ return normalizeRoutePath(path7);
7147
+ }
7148
+ function normalizeRouteInventoryRoute(input, index) {
7149
+ if (typeof input === "string") return { path: normalizeRouteInventoryPath(input, `checks route_inventory expected_routes[${index}]`) };
7150
+ if (!isRecord(input)) throw new Error(`checks route_inventory expected_routes[${index}] must be a string or object.`);
7151
+ return {
7152
+ name: stringValue2(input.name),
7153
+ path: normalizeRouteInventoryPath(input.path, `checks route_inventory expected_routes[${index}]`)
7154
+ };
7155
+ }
7156
+ function normalizeRouteInventoryRoutes(value, index) {
7157
+ if (value === void 0) return void 0;
7158
+ if (!Array.isArray(value) || value.length === 0) {
7159
+ throw new Error(`checks[${index}] route_inventory expected_routes must be a non-empty array.`);
7160
+ }
7161
+ const routes = value.map(normalizeRouteInventoryRoute);
7162
+ const seen = /* @__PURE__ */ new Set();
7163
+ for (const route of routes) {
7164
+ if (seen.has(route.path)) throw new Error(`checks[${index}] route_inventory expected_routes contains duplicate path ${route.path}.`);
7165
+ seen.add(route.path);
7166
+ }
7167
+ return routes;
7168
+ }
7141
7169
  function normalizeCheck(input, index) {
7142
7170
  if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
7143
7171
  const type = stringValue2(input.type);
@@ -7154,16 +7182,34 @@ function normalizeCheck(input, index) {
7154
7182
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
7155
7183
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
7156
7184
  }
7185
+ const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
7186
+ if (type === "route_inventory" && !expectedRoutes?.length) {
7187
+ throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
7188
+ }
7157
7189
  return {
7158
7190
  type,
7159
7191
  label: stringValue2(input.label),
7160
7192
  expected_path: stringValue2(input.expected_path),
7193
+ expected_routes: expectedRoutes,
7161
7194
  selector: stringValue2(input.selector),
7195
+ link_selector: stringValue2(input.link_selector) || stringValue2(input.linkSelector),
7196
+ source_selector: stringValue2(input.source_selector) || stringValue2(input.sourceSelector),
7197
+ route_path_prefix: stringValue2(input.route_path_prefix) || stringValue2(input.routePathPrefix),
7198
+ route_ready_selector: stringValue2(input.route_ready_selector) || stringValue2(input.routeReadySelector),
7199
+ route_ready_text: stringValue2(input.route_ready_text) || stringValue2(input.routeReadyText),
7200
+ route_ready_pattern: stringValue2(input.route_ready_pattern) || stringValue2(input.routeReadyPattern),
7201
+ route_ready_flags: stringValue2(input.route_ready_flags) || stringValue2(input.routeReadyFlags),
7162
7202
  text: stringValue2(input.text),
7163
7203
  pattern: stringValue2(input.pattern),
7164
7204
  flags: stringValue2(input.flags),
7165
7205
  min_count: numberValue(input.min_count),
7166
- max_overflow_px: numberValue(input.max_overflow_px)
7206
+ max_overflow_px: numberValue(input.max_overflow_px),
7207
+ timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
7208
+ run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
7209
+ run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
7210
+ require_unique_routes: input.require_unique_routes === false || input.requireUniqueRoutes === false ? false : true,
7211
+ allow_unexpected_routes: input.allow_unexpected_routes === true || input.allowUnexpectedRoutes === true,
7212
+ save_route_screenshots: input.save_route_screenshots === true || input.saveRouteScreenshots === true
7167
7213
  };
7168
7214
  }
7169
7215
  function normalizeFailurePolicy(input) {
@@ -7390,6 +7436,36 @@ function assessCheckFromEvidence(check, evidence) {
7390
7436
  message: failed ? `Text assertion failed in ${failed} viewport(s).` : void 0
7391
7437
  };
7392
7438
  }
7439
+ if (check.type === "route_inventory") {
7440
+ const inventories = viewports.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory })).filter((item) => isRecord(item.inventory));
7441
+ if (!inventories.length) {
7442
+ return {
7443
+ type: check.type,
7444
+ label: checkLabel(check),
7445
+ status: "failed",
7446
+ evidence: { expected_count: check.expected_routes?.length || 0 },
7447
+ message: "No route inventory evidence was captured."
7448
+ };
7449
+ }
7450
+ const failures = inventories.flatMap((item) => Array.isArray(item.inventory?.failures) ? item.inventory.failures.map((failure) => toJsonValue({ viewport: item.viewport, failure })) : []);
7451
+ const first = inventories[0]?.inventory;
7452
+ const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
7453
+ const clickthroughs = Array.isArray(first?.clickthroughs) ? first.clickthroughs : [];
7454
+ return {
7455
+ type: check.type,
7456
+ label: checkLabel(check),
7457
+ status: failures.length ? "failed" : "passed",
7458
+ evidence: {
7459
+ expected_count: check.expected_routes?.length || 0,
7460
+ homepage_link_count: numberValue(first?.home_game_link_count) ?? null,
7461
+ homepage_unique_link_count: numberValue(first?.home_unique_game_link_count) ?? null,
7462
+ direct_route_count: directRoutes.length,
7463
+ clickthrough_count: clickthroughs.length,
7464
+ failures
7465
+ },
7466
+ message: failures.length ? `Route inventory failed with ${failures.length} issue(s).` : void 0
7467
+ };
7468
+ }
7393
7469
  if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
7394
7470
  const maxOverflow = check.max_overflow_px ?? 4;
7395
7471
  const applicable = check.type === "no_mobile_horizontal_overflow" ? viewports.filter((viewport) => viewport.width <= 820) : viewports;
@@ -7883,6 +7959,45 @@ function assessProfile(profile, evidence) {
7883
7959
  });
7884
7960
  continue;
7885
7961
  }
7962
+ if (check.type === "route_inventory") {
7963
+ const inventories = viewports
7964
+ .map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory }))
7965
+ .filter((item) => item.inventory && typeof item.inventory === "object");
7966
+ if (!inventories.length) {
7967
+ checks.push({
7968
+ type: check.type,
7969
+ label: check.label || check.type,
7970
+ status: "failed",
7971
+ evidence: { expected_count: (check.expected_routes || []).length },
7972
+ message: "No route inventory evidence was captured.",
7973
+ });
7974
+ continue;
7975
+ }
7976
+ const failures = [];
7977
+ for (const item of inventories) {
7978
+ for (const failure of Array.isArray(item.inventory.failures) ? item.inventory.failures : []) {
7979
+ failures.push({ viewport: item.viewport, failure });
7980
+ }
7981
+ }
7982
+ const first = inventories[0].inventory || {};
7983
+ const directRoutes = Array.isArray(first.direct_routes) ? first.direct_routes : [];
7984
+ const clickthroughs = Array.isArray(first.clickthroughs) ? first.clickthroughs : [];
7985
+ checks.push({
7986
+ type: check.type,
7987
+ label: check.label || check.type,
7988
+ status: failures.length ? "failed" : "passed",
7989
+ evidence: {
7990
+ expected_count: (check.expected_routes || []).length,
7991
+ homepage_link_count: typeof first.home_game_link_count === "number" ? first.home_game_link_count : null,
7992
+ homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : null,
7993
+ direct_route_count: directRoutes.length,
7994
+ clickthrough_count: clickthroughs.length,
7995
+ failures,
7996
+ },
7997
+ message: failures.length ? "Route inventory failed with " + failures.length + " issue(s)." : undefined,
7998
+ });
7999
+ continue;
8000
+ }
7886
8001
  if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
7887
8002
  const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
7888
8003
  const applicable = check.type === "no_mobile_horizontal_overflow" ? viewports.filter((viewport) => viewport.width <= 820) : viewports;
@@ -8194,6 +8309,290 @@ async function selectorStats(selector) {
8194
8309
  return { count: elements.length, visible_count: elements.filter(isVisible).length };
8195
8310
  }).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
8196
8311
  }
8312
+ function inventoryAppPathFromUrl(urlLike) {
8313
+ const url = new URL(String(urlLike), targetUrl);
8314
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
8315
+ let pathname = url.pathname || "/";
8316
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
8317
+ pathname = pathname.slice(mountPrefix.length) || "/";
8318
+ }
8319
+ return normalizeRoutePath(pathname);
8320
+ }
8321
+ function inventoryRouteUrl(expectedPath) {
8322
+ const base = new URL(targetUrl);
8323
+ const route = new URL(expectedPath, base.origin);
8324
+ const mountPrefix = previewMountPrefix(base.pathname);
8325
+ base.pathname = mountPrefix ? joinMountedRoutePath(mountPrefix, route.pathname) : route.pathname;
8326
+ base.search = route.search;
8327
+ base.hash = route.hash;
8328
+ return base.href;
8329
+ }
8330
+ function inventorySlugFromPath(path) {
8331
+ return String(path || "route").split("/").filter(Boolean).pop()?.replace(/[^a-z0-9]+/gi, "-").toLowerCase() || "route";
8332
+ }
8333
+ function inventoryCheckTimeout(check) {
8334
+ const timeout = Number(check && check.timeout_ms);
8335
+ return Number.isFinite(timeout) && timeout > 0 ? timeout : 45000;
8336
+ }
8337
+ async function collectInventoryHomeLinks(check) {
8338
+ const linkSelector = check.link_selector || "a[href]";
8339
+ const expectedPaths = (check.expected_routes || []).map((route) => route.path);
8340
+ const routePathPrefix = check.route_path_prefix || "";
8341
+ return await page.evaluate(({ linkSelector, expectedPaths, routePathPrefix, targetUrl }) => {
8342
+ const previewMountPrefix = (pathname) => {
8343
+ const value = pathname || "/";
8344
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
8345
+ if (apiPreview) return apiPreview[1];
8346
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
8347
+ return internalPreview ? internalPreview[1] : "";
8348
+ };
8349
+ const normalizeRoutePath = (path) => {
8350
+ const value = path || "/";
8351
+ if (value === "/") return "/";
8352
+ return value.replace(/\/+$/, "") || "/";
8353
+ };
8354
+ const appPathFromHref = (href) => {
8355
+ const url = new URL(href, location.href);
8356
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
8357
+ let pathname = url.pathname || "/";
8358
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
8359
+ pathname = pathname.slice(mountPrefix.length) || "/";
8360
+ }
8361
+ return normalizeRoutePath(pathname);
8362
+ };
8363
+ const expectedSet = new Set(expectedPaths);
8364
+ const links = Array.from(document.querySelectorAll(linkSelector)).map((anchor) => {
8365
+ const href = anchor.getAttribute("href") || "";
8366
+ const appPath = appPathFromHref(href || anchor.href || location.href);
8367
+ return {
8368
+ text: (anchor.textContent || "").replace(/\s+/g, " ").trim().slice(0, 240),
8369
+ href,
8370
+ pathname: new URL(href || anchor.href || location.href, location.href).pathname,
8371
+ app_path: appPath,
8372
+ };
8373
+ });
8374
+ return links.filter((link) => (
8375
+ routePathPrefix ? link.app_path.startsWith(routePathPrefix) : expectedSet.has(link.app_path)
8376
+ ));
8377
+ }, { linkSelector, expectedPaths, routePathPrefix, targetUrl });
8378
+ }
8379
+ async function waitForInventoryRouteHealth(check, expectedPath) {
8380
+ const timeout = inventoryCheckTimeout(check);
8381
+ await page.waitForURL((url) => inventoryAppPathFromUrl(url.href) === normalizeRoutePath(expectedPath), { timeout: Math.min(timeout, 20000) });
8382
+ if (check.route_ready_selector) {
8383
+ await page.waitForSelector(check.route_ready_selector, { state: "visible", timeout });
8384
+ return;
8385
+ }
8386
+ await page.waitForFunction(({ expectedPath, sourceSelector, routeReadyText, routeReadyPattern, routeReadyFlags, targetUrl }) => {
8387
+ const previewMountPrefix = (pathname) => {
8388
+ const value = pathname || "/";
8389
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
8390
+ if (apiPreview) return apiPreview[1];
8391
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
8392
+ return internalPreview ? internalPreview[1] : "";
8393
+ };
8394
+ const normalizeRoutePath = (path) => {
8395
+ const value = path || "/";
8396
+ if (value === "/") return "/";
8397
+ return value.replace(/\/+$/, "") || "/";
8398
+ };
8399
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
8400
+ let pathname = location.pathname || "/";
8401
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
8402
+ pathname = pathname.slice(mountPrefix.length) || "/";
8403
+ }
8404
+ if (normalizeRoutePath(pathname) !== normalizeRoutePath(expectedPath)) return false;
8405
+ if (sourceSelector && document.querySelector(sourceSelector)) return false;
8406
+ const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
8407
+ if (routeReadyPattern) {
8408
+ try {
8409
+ if (!new RegExp(routeReadyPattern, routeReadyFlags || "").test(text)) return false;
8410
+ } catch {
8411
+ return false;
8412
+ }
8413
+ }
8414
+ if (routeReadyText && !text.includes(routeReadyText)) return false;
8415
+ const loadingOnly = /^loading\.?$/i.test(text) || (/^loading/i.test(text) && text.length < 40);
8416
+ if (document.querySelectorAll("canvas").length > 0) return true;
8417
+ if (document.querySelectorAll("button").length > 0 && !loadingOnly) return true;
8418
+ return text.length > 30 && !loadingOnly && !/not found|cannot get/i.test(text);
8419
+ }, {
8420
+ expectedPath,
8421
+ sourceSelector: check.source_selector || "",
8422
+ routeReadyText: check.route_ready_text || "",
8423
+ routeReadyPattern: check.route_ready_pattern || "",
8424
+ routeReadyFlags: check.route_ready_flags || "",
8425
+ targetUrl,
8426
+ }, { timeout });
8427
+ }
8428
+ async function collectInventoryRouteSnapshot(expectedRoute, phase, check, error) {
8429
+ return await page.evaluate(({ expectedRoute, phase, sourceSelector, error, targetUrl }) => {
8430
+ const previewMountPrefix = (pathname) => {
8431
+ const value = pathname || "/";
8432
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
8433
+ if (apiPreview) return apiPreview[1];
8434
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
8435
+ return internalPreview ? internalPreview[1] : "";
8436
+ };
8437
+ const normalizeRoutePath = (path) => {
8438
+ const value = path || "/";
8439
+ if (value === "/") return "/";
8440
+ return value.replace(/\/+$/, "") || "/";
8441
+ };
8442
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
8443
+ let pathname = location.pathname || "/";
8444
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
8445
+ pathname = pathname.slice(mountPrefix.length) || "/";
8446
+ }
8447
+ const sourceCount = sourceSelector ? document.querySelectorAll(sourceSelector).length : 0;
8448
+ const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
8449
+ return {
8450
+ phase,
8451
+ name: expectedRoute.name || null,
8452
+ path: expectedRoute.path,
8453
+ loaded: !error,
8454
+ error: error || null,
8455
+ actual_path: location.pathname,
8456
+ actual_app_path: normalizeRoutePath(pathname),
8457
+ source_selector: sourceSelector || null,
8458
+ source_count: sourceCount,
8459
+ source_visible: sourceCount > 0,
8460
+ title: document.title,
8461
+ body_text_sample: text.slice(0, 900),
8462
+ canvas_count: document.querySelectorAll("canvas").length,
8463
+ button_texts: Array.from(document.querySelectorAll("button")).slice(0, 12).map((button) => (
8464
+ (button.textContent || "").replace(/\s+/g, " ").trim()
8465
+ )),
8466
+ heading_texts: Array.from(document.querySelectorAll("h1,h2,h3")).slice(0, 8).map((heading) => (
8467
+ (heading.textContent || "").replace(/\s+/g, " ").trim()
8468
+ )),
8469
+ };
8470
+ }, { expectedRoute, phase, sourceSelector: check.source_selector || "", error: error || "", targetUrl });
8471
+ }
8472
+ async function findInventoryLinkIndex(check, expectedPath) {
8473
+ const locator = page.locator(check.link_selector || "a[href]");
8474
+ const count = await locator.count();
8475
+ for (let index = 0; index < count; index += 1) {
8476
+ const appPath = await locator.nth(index).evaluate((anchor, targetUrl) => {
8477
+ const previewMountPrefix = (pathname) => {
8478
+ const value = pathname || "/";
8479
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
8480
+ if (apiPreview) return apiPreview[1];
8481
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
8482
+ return internalPreview ? internalPreview[1] : "";
8483
+ };
8484
+ const normalizeRoutePath = (path) => {
8485
+ const value = path || "/";
8486
+ if (value === "/") return "/";
8487
+ return value.replace(/\/+$/, "") || "/";
8488
+ };
8489
+ const href = anchor.getAttribute("href") || anchor.href || location.href;
8490
+ const url = new URL(href, location.href);
8491
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
8492
+ let pathname = url.pathname || "/";
8493
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
8494
+ pathname = pathname.slice(mountPrefix.length) || "/";
8495
+ }
8496
+ return normalizeRoutePath(pathname);
8497
+ }, targetUrl).catch(() => "");
8498
+ if (appPath === normalizeRoutePath(expectedPath)) return index;
8499
+ }
8500
+ return -1;
8501
+ }
8502
+ async function collectRouteInventory(check, viewport) {
8503
+ const expectedRoutes = check.expected_routes || [];
8504
+ const expectedPaths = expectedRoutes.map((route) => normalizeRoutePath(route.path));
8505
+ const expectedSet = new Set(expectedPaths);
8506
+ const failures = [];
8507
+ const homeLinks = await collectInventoryHomeLinks(check);
8508
+ const homeLinkPaths = homeLinks.map((link) => link.app_path);
8509
+ const uniqueHomeLinkPaths = Array.from(new Set(homeLinkPaths));
8510
+ const duplicateHomeLinkPaths = homeLinkPaths.filter((path, index) => homeLinkPaths.indexOf(path) !== index);
8511
+ if (check.require_unique_routes !== false && duplicateHomeLinkPaths.length) {
8512
+ failures.push({ code: "duplicate_source_links", paths: Array.from(new Set(duplicateHomeLinkPaths)) });
8513
+ }
8514
+ for (const route of expectedRoutes) {
8515
+ if (!homeLinkPaths.includes(normalizeRoutePath(route.path))) {
8516
+ failures.push({ code: "expected_route_missing_from_source", name: route.name || null, path: route.path });
8517
+ }
8518
+ }
8519
+ const unexpectedRoutes = uniqueHomeLinkPaths.filter((path) => !expectedSet.has(path));
8520
+ if (!check.allow_unexpected_routes && unexpectedRoutes.length) {
8521
+ failures.push({ code: "unexpected_source_links", paths: unexpectedRoutes });
8522
+ }
8523
+ if (!check.allow_unexpected_routes && uniqueHomeLinkPaths.length !== expectedRoutes.length) {
8524
+ failures.push({ code: "source_link_count_mismatch", expected: expectedRoutes.length, actual_unique: uniqueHomeLinkPaths.length, actual_total: homeLinkPaths.length });
8525
+ }
8526
+
8527
+ const directRoutes = [];
8528
+ if (check.run_direct_routes !== false) {
8529
+ for (const expectedRoute of expectedRoutes) {
8530
+ let error = "";
8531
+ try {
8532
+ await page.goto(inventoryRouteUrl(expectedRoute.path), { waitUntil: "domcontentloaded", timeout: 90000 });
8533
+ await waitForInventoryRouteHealth(check, expectedRoute.path);
8534
+ } catch (caught) {
8535
+ error = String(caught && caught.message ? caught.message : caught).slice(0, 1000);
8536
+ }
8537
+ const snapshot = await collectInventoryRouteSnapshot(expectedRoute, "direct", check, error);
8538
+ directRoutes.push(snapshot);
8539
+ if (check.save_route_screenshots) {
8540
+ await saveScreenshot(profileSlug + "-direct-" + inventorySlugFromPath(expectedRoute.path)).catch(() => {});
8541
+ }
8542
+ if (error) failures.push({ code: "direct_route_unhealthy", name: expectedRoute.name || null, path: expectedRoute.path, error });
8543
+ 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 });
8544
+ 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 });
8545
+ }
8546
+ }
8547
+
8548
+ const clickthroughs = [];
8549
+ if (check.run_clickthroughs !== false) {
8550
+ for (const expectedRoute of expectedRoutes) {
8551
+ const result = { name: expectedRoute.name || null, path: expectedRoute.path, clicked: false, snapshot: null, error: null };
8552
+ try {
8553
+ await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 90000 });
8554
+ if (profile.target.wait_for_selector) {
8555
+ await page.waitForSelector(profile.target.wait_for_selector, { state: "visible", timeout: 15000 }).catch(() => {});
8556
+ }
8557
+ const index = await findInventoryLinkIndex(check, expectedRoute.path);
8558
+ if (index < 0) {
8559
+ result.error = "source_link_not_found";
8560
+ failures.push({ code: "source_link_clickthrough_missing", name: expectedRoute.name || null, path: expectedRoute.path });
8561
+ } else {
8562
+ const locator = page.locator(check.link_selector || "a[href]").nth(index);
8563
+ await locator.scrollIntoViewIfNeeded();
8564
+ await locator.click({ timeout: inventoryCheckTimeout(check), noWaitAfter: true });
8565
+ result.clicked = true;
8566
+ await waitForInventoryRouteHealth(check, expectedRoute.path);
8567
+ }
8568
+ } catch (caught) {
8569
+ result.error = String(caught && caught.message ? caught.message : caught).slice(0, 1000);
8570
+ }
8571
+ result.snapshot = await collectInventoryRouteSnapshot(expectedRoute, "clickthrough", check, result.error || "");
8572
+ clickthroughs.push(result);
8573
+ if (check.save_route_screenshots) {
8574
+ await saveScreenshot(profileSlug + "-click-" + inventorySlugFromPath(expectedRoute.path)).catch(() => {});
8575
+ }
8576
+ 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 });
8577
+ 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 });
8578
+ 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 });
8579
+ }
8580
+ }
8581
+
8582
+ return {
8583
+ version: "riddle-proof.route-inventory.v1",
8584
+ viewport: viewport.name,
8585
+ expected_routes: expectedRoutes,
8586
+ link_selector: check.link_selector || "a[href]",
8587
+ source_selector: check.source_selector || null,
8588
+ home_game_link_count: homeLinkPaths.length,
8589
+ home_unique_game_link_count: uniqueHomeLinkPaths.length,
8590
+ home_links: homeLinks,
8591
+ direct_routes: directRoutes,
8592
+ clickthroughs,
8593
+ failures,
8594
+ };
8595
+ }
8197
8596
  async function captureViewport(viewport) {
8198
8597
  await page.setViewportSize({ width: viewport.width, height: viewport.height });
8199
8598
  let httpStatus = null;
@@ -8316,6 +8715,31 @@ async function captureViewport(viewport) {
8316
8715
  } catch (error) {
8317
8716
  pageErrors.push({ message: "saveScreenshot failed: " + String(error && error.message ? error.message : error).slice(0, 500) });
8318
8717
  }
8718
+ let routeInventory;
8719
+ const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
8720
+ const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
8721
+ if (routeInventoryCheck && (!firstViewportName || viewport.name === firstViewportName)) {
8722
+ try {
8723
+ routeInventory = await collectRouteInventory(routeInventoryCheck, viewport);
8724
+ } catch (error) {
8725
+ routeInventory = {
8726
+ version: "riddle-proof.route-inventory.v1",
8727
+ viewport: viewport.name,
8728
+ expected_routes: routeInventoryCheck.expected_routes || [],
8729
+ link_selector: routeInventoryCheck.link_selector || "a[href]",
8730
+ source_selector: routeInventoryCheck.source_selector || null,
8731
+ home_game_link_count: 0,
8732
+ home_unique_game_link_count: 0,
8733
+ home_links: [],
8734
+ direct_routes: [],
8735
+ clickthroughs: [],
8736
+ failures: [{
8737
+ code: "route_inventory_capture_failed",
8738
+ error: String(error && error.message ? error.message : error).slice(0, 1000),
8739
+ }],
8740
+ };
8741
+ }
8742
+ }
8319
8743
  const expectedPath = profile.checks.find((check) => check.type === "route_loaded" && check.expected_path)?.expected_path || new URL(targetUrl).pathname || "/";
8320
8744
  return {
8321
8745
  name: viewport.name,
@@ -8340,6 +8764,7 @@ async function captureViewport(viewport) {
8340
8764
  overflow_offenders: dom.overflow_offenders || [],
8341
8765
  selectors,
8342
8766
  text_matches,
8767
+ route_inventory: routeInventory,
8343
8768
  setup_action_results: setupActionResults,
8344
8769
  screenshot_label: screenshotLabel,
8345
8770
  navigation_error: navigationError,
@@ -8372,6 +8797,16 @@ function buildProfileEvidence(currentViewports) {
8372
8797
  overflow_px: currentViewports.map((viewport) => viewport.overflow_px),
8373
8798
  bounds_overflow_px: currentViewports.map((viewport) => viewport.bounds_overflow_px),
8374
8799
  overflow_offender_counts: currentViewports.map((viewport) => (viewport.overflow_offenders || []).length),
8800
+ route_inventory: currentViewports
8801
+ .filter((viewport) => viewport.route_inventory)
8802
+ .map((viewport) => ({
8803
+ viewport: viewport.name,
8804
+ expected_count: (viewport.route_inventory.expected_routes || []).length,
8805
+ home_unique_game_link_count: viewport.route_inventory.home_unique_game_link_count,
8806
+ direct_route_count: (viewport.route_inventory.direct_routes || []).length,
8807
+ clickthrough_count: (viewport.route_inventory.clickthroughs || []).length,
8808
+ failure_count: (viewport.route_inventory.failures || []).length,
8809
+ })),
8375
8810
  network_mock_count: (profile.target.network_mocks || []).length,
8376
8811
  network_mock_hit_count: networkMockEvents.filter((event) => event.ok !== false).length,
8377
8812
  },
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  profileStatusExitCode,
11
11
  resolveRiddleProofProfileTargetUrl,
12
12
  resolveRiddleProofProfileTimeoutSec
13
- } from "./chunk-2NWVXT4Q.js";
13
+ } from "./chunk-COUZXKGM.js";
14
14
  import {
15
15
  createRiddleApiClient,
16
16
  parseRiddleViewport