@riddledc/riddle-proof 0.7.31 → 0.7.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -8730,6 +8730,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
8730
8730
  "selector_count_at_least",
8731
8731
  "text_visible",
8732
8732
  "text_absent",
8733
+ "route_inventory",
8733
8734
  "no_horizontal_overflow",
8734
8735
  "no_mobile_horizontal_overflow",
8735
8736
  "no_fatal_console_errors"
@@ -8979,6 +8980,33 @@ function normalizeNetworkMocks(value) {
8979
8980
  if (!Array.isArray(value)) throw new Error("target.network_mocks must be an array.");
8980
8981
  return value.map(normalizeNetworkMock);
8981
8982
  }
8983
+ function normalizeRouteInventoryPath(value, label) {
8984
+ const path6 = stringValue5(value);
8985
+ if (!path6) throw new Error(`${label} requires path.`);
8986
+ if (!path6.startsWith("/")) throw new Error(`${label}.path must start with /.`);
8987
+ return normalizeRoutePath(path6);
8988
+ }
8989
+ function normalizeRouteInventoryRoute(input, index) {
8990
+ if (typeof input === "string") return { path: normalizeRouteInventoryPath(input, `checks route_inventory expected_routes[${index}]`) };
8991
+ if (!isRecord2(input)) throw new Error(`checks route_inventory expected_routes[${index}] must be a string or object.`);
8992
+ return {
8993
+ name: stringValue5(input.name),
8994
+ path: normalizeRouteInventoryPath(input.path, `checks route_inventory expected_routes[${index}]`)
8995
+ };
8996
+ }
8997
+ function normalizeRouteInventoryRoutes(value, index) {
8998
+ if (value === void 0) return void 0;
8999
+ if (!Array.isArray(value) || value.length === 0) {
9000
+ throw new Error(`checks[${index}] route_inventory expected_routes must be a non-empty array.`);
9001
+ }
9002
+ const routes = value.map(normalizeRouteInventoryRoute);
9003
+ const seen = /* @__PURE__ */ new Set();
9004
+ for (const route of routes) {
9005
+ if (seen.has(route.path)) throw new Error(`checks[${index}] route_inventory expected_routes contains duplicate path ${route.path}.`);
9006
+ seen.add(route.path);
9007
+ }
9008
+ return routes;
9009
+ }
8982
9010
  function normalizeCheck(input, index) {
8983
9011
  if (!isRecord2(input)) throw new Error(`checks[${index}] must be an object.`);
8984
9012
  const type = stringValue5(input.type);
@@ -8995,16 +9023,34 @@ function normalizeCheck(input, index) {
8995
9023
  if (type === "selector_count_at_least" && numberValue3(input.min_count) === void 0) {
8996
9024
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
8997
9025
  }
9026
+ const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
9027
+ if (type === "route_inventory" && !expectedRoutes?.length) {
9028
+ throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
9029
+ }
8998
9030
  return {
8999
9031
  type,
9000
9032
  label: stringValue5(input.label),
9001
9033
  expected_path: stringValue5(input.expected_path),
9034
+ expected_routes: expectedRoutes,
9002
9035
  selector: stringValue5(input.selector),
9036
+ link_selector: stringValue5(input.link_selector) || stringValue5(input.linkSelector),
9037
+ source_selector: stringValue5(input.source_selector) || stringValue5(input.sourceSelector),
9038
+ route_path_prefix: stringValue5(input.route_path_prefix) || stringValue5(input.routePathPrefix),
9039
+ route_ready_selector: stringValue5(input.route_ready_selector) || stringValue5(input.routeReadySelector),
9040
+ route_ready_text: stringValue5(input.route_ready_text) || stringValue5(input.routeReadyText),
9041
+ route_ready_pattern: stringValue5(input.route_ready_pattern) || stringValue5(input.routeReadyPattern),
9042
+ route_ready_flags: stringValue5(input.route_ready_flags) || stringValue5(input.routeReadyFlags),
9003
9043
  text: stringValue5(input.text),
9004
9044
  pattern: stringValue5(input.pattern),
9005
9045
  flags: stringValue5(input.flags),
9006
9046
  min_count: numberValue3(input.min_count),
9007
- max_overflow_px: numberValue3(input.max_overflow_px)
9047
+ max_overflow_px: numberValue3(input.max_overflow_px),
9048
+ timeout_ms: numberValue3(input.timeout_ms) ?? numberValue3(input.timeoutMs),
9049
+ run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
9050
+ run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
9051
+ require_unique_routes: input.require_unique_routes === false || input.requireUniqueRoutes === false ? false : true,
9052
+ allow_unexpected_routes: input.allow_unexpected_routes === true || input.allowUnexpectedRoutes === true,
9053
+ save_route_screenshots: input.save_route_screenshots === true || input.saveRouteScreenshots === true
9008
9054
  };
9009
9055
  }
9010
9056
  function normalizeFailurePolicy(input) {
@@ -9231,6 +9277,36 @@ function assessCheckFromEvidence(check, evidence) {
9231
9277
  message: failed ? `Text assertion failed in ${failed} viewport(s).` : void 0
9232
9278
  };
9233
9279
  }
9280
+ if (check.type === "route_inventory") {
9281
+ const inventories = viewports.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory })).filter((item) => isRecord2(item.inventory));
9282
+ if (!inventories.length) {
9283
+ return {
9284
+ type: check.type,
9285
+ label: checkLabel(check),
9286
+ status: "failed",
9287
+ evidence: { expected_count: check.expected_routes?.length || 0 },
9288
+ message: "No route inventory evidence was captured."
9289
+ };
9290
+ }
9291
+ const failures = inventories.flatMap((item) => Array.isArray(item.inventory?.failures) ? item.inventory.failures.map((failure) => toJsonValue({ viewport: item.viewport, failure })) : []);
9292
+ const first = inventories[0]?.inventory;
9293
+ const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
9294
+ const clickthroughs = Array.isArray(first?.clickthroughs) ? first.clickthroughs : [];
9295
+ return {
9296
+ type: check.type,
9297
+ label: checkLabel(check),
9298
+ status: failures.length ? "failed" : "passed",
9299
+ evidence: {
9300
+ expected_count: check.expected_routes?.length || 0,
9301
+ homepage_link_count: numberValue3(first?.home_game_link_count) ?? null,
9302
+ homepage_unique_link_count: numberValue3(first?.home_unique_game_link_count) ?? null,
9303
+ direct_route_count: directRoutes.length,
9304
+ clickthrough_count: clickthroughs.length,
9305
+ failures
9306
+ },
9307
+ message: failures.length ? `Route inventory failed with ${failures.length} issue(s).` : void 0
9308
+ };
9309
+ }
9234
9310
  if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
9235
9311
  const maxOverflow = check.max_overflow_px ?? 4;
9236
9312
  const applicable = check.type === "no_mobile_horizontal_overflow" ? viewports.filter((viewport) => viewport.width <= 820) : viewports;
@@ -9740,6 +9816,45 @@ function assessProfile(profile, evidence) {
9740
9816
  });
9741
9817
  continue;
9742
9818
  }
9819
+ if (check.type === "route_inventory") {
9820
+ const inventories = viewports
9821
+ .map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory }))
9822
+ .filter((item) => item.inventory && typeof item.inventory === "object");
9823
+ if (!inventories.length) {
9824
+ checks.push({
9825
+ type: check.type,
9826
+ label: check.label || check.type,
9827
+ status: "failed",
9828
+ evidence: { expected_count: (check.expected_routes || []).length },
9829
+ message: "No route inventory evidence was captured.",
9830
+ });
9831
+ continue;
9832
+ }
9833
+ const failures = [];
9834
+ for (const item of inventories) {
9835
+ for (const failure of Array.isArray(item.inventory.failures) ? item.inventory.failures : []) {
9836
+ failures.push({ viewport: item.viewport, failure });
9837
+ }
9838
+ }
9839
+ const first = inventories[0].inventory || {};
9840
+ const directRoutes = Array.isArray(first.direct_routes) ? first.direct_routes : [];
9841
+ const clickthroughs = Array.isArray(first.clickthroughs) ? first.clickthroughs : [];
9842
+ checks.push({
9843
+ type: check.type,
9844
+ label: check.label || check.type,
9845
+ status: failures.length ? "failed" : "passed",
9846
+ evidence: {
9847
+ expected_count: (check.expected_routes || []).length,
9848
+ homepage_link_count: typeof first.home_game_link_count === "number" ? first.home_game_link_count : null,
9849
+ homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : null,
9850
+ direct_route_count: directRoutes.length,
9851
+ clickthrough_count: clickthroughs.length,
9852
+ failures,
9853
+ },
9854
+ message: failures.length ? "Route inventory failed with " + failures.length + " issue(s)." : undefined,
9855
+ });
9856
+ continue;
9857
+ }
9743
9858
  if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
9744
9859
  const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
9745
9860
  const applicable = check.type === "no_mobile_horizontal_overflow" ? viewports.filter((viewport) => viewport.width <= 820) : viewports;
@@ -10051,6 +10166,290 @@ async function selectorStats(selector) {
10051
10166
  return { count: elements.length, visible_count: elements.filter(isVisible).length };
10052
10167
  }).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
10053
10168
  }
10169
+ function inventoryAppPathFromUrl(urlLike) {
10170
+ const url = new URL(String(urlLike), targetUrl);
10171
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
10172
+ let pathname = url.pathname || "/";
10173
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
10174
+ pathname = pathname.slice(mountPrefix.length) || "/";
10175
+ }
10176
+ return normalizeRoutePath(pathname);
10177
+ }
10178
+ function inventoryRouteUrl(expectedPath) {
10179
+ const base = new URL(targetUrl);
10180
+ const route = new URL(expectedPath, base.origin);
10181
+ const mountPrefix = previewMountPrefix(base.pathname);
10182
+ base.pathname = mountPrefix ? joinMountedRoutePath(mountPrefix, route.pathname) : route.pathname;
10183
+ base.search = route.search;
10184
+ base.hash = route.hash;
10185
+ return base.href;
10186
+ }
10187
+ function inventorySlugFromPath(path) {
10188
+ return String(path || "route").split("/").filter(Boolean).pop()?.replace(/[^a-z0-9]+/gi, "-").toLowerCase() || "route";
10189
+ }
10190
+ function inventoryCheckTimeout(check) {
10191
+ const timeout = Number(check && check.timeout_ms);
10192
+ return Number.isFinite(timeout) && timeout > 0 ? timeout : 45000;
10193
+ }
10194
+ async function collectInventoryHomeLinks(check) {
10195
+ const linkSelector = check.link_selector || "a[href]";
10196
+ const expectedPaths = (check.expected_routes || []).map((route) => route.path);
10197
+ const routePathPrefix = check.route_path_prefix || "";
10198
+ return await page.evaluate(({ linkSelector, expectedPaths, routePathPrefix, targetUrl }) => {
10199
+ const previewMountPrefix = (pathname) => {
10200
+ const value = pathname || "/";
10201
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
10202
+ if (apiPreview) return apiPreview[1];
10203
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
10204
+ return internalPreview ? internalPreview[1] : "";
10205
+ };
10206
+ const normalizeRoutePath = (path) => {
10207
+ const value = path || "/";
10208
+ if (value === "/") return "/";
10209
+ return value.replace(/\/+$/, "") || "/";
10210
+ };
10211
+ const appPathFromHref = (href) => {
10212
+ const url = new URL(href, location.href);
10213
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
10214
+ let pathname = url.pathname || "/";
10215
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
10216
+ pathname = pathname.slice(mountPrefix.length) || "/";
10217
+ }
10218
+ return normalizeRoutePath(pathname);
10219
+ };
10220
+ const expectedSet = new Set(expectedPaths);
10221
+ const links = Array.from(document.querySelectorAll(linkSelector)).map((anchor) => {
10222
+ const href = anchor.getAttribute("href") || "";
10223
+ const appPath = appPathFromHref(href || anchor.href || location.href);
10224
+ return {
10225
+ text: (anchor.textContent || "").replace(/\s+/g, " ").trim().slice(0, 240),
10226
+ href,
10227
+ pathname: new URL(href || anchor.href || location.href, location.href).pathname,
10228
+ app_path: appPath,
10229
+ };
10230
+ });
10231
+ return links.filter((link) => (
10232
+ routePathPrefix ? link.app_path.startsWith(routePathPrefix) : expectedSet.has(link.app_path)
10233
+ ));
10234
+ }, { linkSelector, expectedPaths, routePathPrefix, targetUrl });
10235
+ }
10236
+ async function waitForInventoryRouteHealth(check, expectedPath) {
10237
+ const timeout = inventoryCheckTimeout(check);
10238
+ await page.waitForURL((url) => inventoryAppPathFromUrl(url.href) === normalizeRoutePath(expectedPath), { timeout: Math.min(timeout, 20000) });
10239
+ if (check.route_ready_selector) {
10240
+ await page.waitForSelector(check.route_ready_selector, { state: "visible", timeout });
10241
+ return;
10242
+ }
10243
+ await page.waitForFunction(({ expectedPath, sourceSelector, routeReadyText, routeReadyPattern, routeReadyFlags, targetUrl }) => {
10244
+ const previewMountPrefix = (pathname) => {
10245
+ const value = pathname || "/";
10246
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
10247
+ if (apiPreview) return apiPreview[1];
10248
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
10249
+ return internalPreview ? internalPreview[1] : "";
10250
+ };
10251
+ const normalizeRoutePath = (path) => {
10252
+ const value = path || "/";
10253
+ if (value === "/") return "/";
10254
+ return value.replace(/\/+$/, "") || "/";
10255
+ };
10256
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
10257
+ let pathname = location.pathname || "/";
10258
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
10259
+ pathname = pathname.slice(mountPrefix.length) || "/";
10260
+ }
10261
+ if (normalizeRoutePath(pathname) !== normalizeRoutePath(expectedPath)) return false;
10262
+ if (sourceSelector && document.querySelector(sourceSelector)) return false;
10263
+ const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
10264
+ if (routeReadyPattern) {
10265
+ try {
10266
+ if (!new RegExp(routeReadyPattern, routeReadyFlags || "").test(text)) return false;
10267
+ } catch {
10268
+ return false;
10269
+ }
10270
+ }
10271
+ if (routeReadyText && !text.includes(routeReadyText)) return false;
10272
+ const loadingOnly = /^loading\.?$/i.test(text) || (/^loading/i.test(text) && text.length < 40);
10273
+ if (document.querySelectorAll("canvas").length > 0) return true;
10274
+ if (document.querySelectorAll("button").length > 0 && !loadingOnly) return true;
10275
+ return text.length > 30 && !loadingOnly && !/not found|cannot get/i.test(text);
10276
+ }, {
10277
+ expectedPath,
10278
+ sourceSelector: check.source_selector || "",
10279
+ routeReadyText: check.route_ready_text || "",
10280
+ routeReadyPattern: check.route_ready_pattern || "",
10281
+ routeReadyFlags: check.route_ready_flags || "",
10282
+ targetUrl,
10283
+ }, { timeout });
10284
+ }
10285
+ async function collectInventoryRouteSnapshot(expectedRoute, phase, check, error) {
10286
+ return await page.evaluate(({ expectedRoute, phase, sourceSelector, error, targetUrl }) => {
10287
+ const previewMountPrefix = (pathname) => {
10288
+ const value = pathname || "/";
10289
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
10290
+ if (apiPreview) return apiPreview[1];
10291
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
10292
+ return internalPreview ? internalPreview[1] : "";
10293
+ };
10294
+ const normalizeRoutePath = (path) => {
10295
+ const value = path || "/";
10296
+ if (value === "/") return "/";
10297
+ return value.replace(/\/+$/, "") || "/";
10298
+ };
10299
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
10300
+ let pathname = location.pathname || "/";
10301
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
10302
+ pathname = pathname.slice(mountPrefix.length) || "/";
10303
+ }
10304
+ const sourceCount = sourceSelector ? document.querySelectorAll(sourceSelector).length : 0;
10305
+ const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
10306
+ return {
10307
+ phase,
10308
+ name: expectedRoute.name || null,
10309
+ path: expectedRoute.path,
10310
+ loaded: !error,
10311
+ error: error || null,
10312
+ actual_path: location.pathname,
10313
+ actual_app_path: normalizeRoutePath(pathname),
10314
+ source_selector: sourceSelector || null,
10315
+ source_count: sourceCount,
10316
+ source_visible: sourceCount > 0,
10317
+ title: document.title,
10318
+ body_text_sample: text.slice(0, 900),
10319
+ canvas_count: document.querySelectorAll("canvas").length,
10320
+ button_texts: Array.from(document.querySelectorAll("button")).slice(0, 12).map((button) => (
10321
+ (button.textContent || "").replace(/\s+/g, " ").trim()
10322
+ )),
10323
+ heading_texts: Array.from(document.querySelectorAll("h1,h2,h3")).slice(0, 8).map((heading) => (
10324
+ (heading.textContent || "").replace(/\s+/g, " ").trim()
10325
+ )),
10326
+ };
10327
+ }, { expectedRoute, phase, sourceSelector: check.source_selector || "", error: error || "", targetUrl });
10328
+ }
10329
+ async function findInventoryLinkIndex(check, expectedPath) {
10330
+ const locator = page.locator(check.link_selector || "a[href]");
10331
+ const count = await locator.count();
10332
+ for (let index = 0; index < count; index += 1) {
10333
+ const appPath = await locator.nth(index).evaluate((anchor, targetUrl) => {
10334
+ const previewMountPrefix = (pathname) => {
10335
+ const value = pathname || "/";
10336
+ const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
10337
+ if (apiPreview) return apiPreview[1];
10338
+ const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
10339
+ return internalPreview ? internalPreview[1] : "";
10340
+ };
10341
+ const normalizeRoutePath = (path) => {
10342
+ const value = path || "/";
10343
+ if (value === "/") return "/";
10344
+ return value.replace(/\/+$/, "") || "/";
10345
+ };
10346
+ const href = anchor.getAttribute("href") || anchor.href || location.href;
10347
+ const url = new URL(href, location.href);
10348
+ const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
10349
+ let pathname = url.pathname || "/";
10350
+ if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
10351
+ pathname = pathname.slice(mountPrefix.length) || "/";
10352
+ }
10353
+ return normalizeRoutePath(pathname);
10354
+ }, targetUrl).catch(() => "");
10355
+ if (appPath === normalizeRoutePath(expectedPath)) return index;
10356
+ }
10357
+ return -1;
10358
+ }
10359
+ async function collectRouteInventory(check, viewport) {
10360
+ const expectedRoutes = check.expected_routes || [];
10361
+ const expectedPaths = expectedRoutes.map((route) => normalizeRoutePath(route.path));
10362
+ const expectedSet = new Set(expectedPaths);
10363
+ const failures = [];
10364
+ const homeLinks = await collectInventoryHomeLinks(check);
10365
+ const homeLinkPaths = homeLinks.map((link) => link.app_path);
10366
+ const uniqueHomeLinkPaths = Array.from(new Set(homeLinkPaths));
10367
+ const duplicateHomeLinkPaths = homeLinkPaths.filter((path, index) => homeLinkPaths.indexOf(path) !== index);
10368
+ if (check.require_unique_routes !== false && duplicateHomeLinkPaths.length) {
10369
+ failures.push({ code: "duplicate_source_links", paths: Array.from(new Set(duplicateHomeLinkPaths)) });
10370
+ }
10371
+ for (const route of expectedRoutes) {
10372
+ if (!homeLinkPaths.includes(normalizeRoutePath(route.path))) {
10373
+ failures.push({ code: "expected_route_missing_from_source", name: route.name || null, path: route.path });
10374
+ }
10375
+ }
10376
+ const unexpectedRoutes = uniqueHomeLinkPaths.filter((path) => !expectedSet.has(path));
10377
+ if (!check.allow_unexpected_routes && unexpectedRoutes.length) {
10378
+ failures.push({ code: "unexpected_source_links", paths: unexpectedRoutes });
10379
+ }
10380
+ if (!check.allow_unexpected_routes && uniqueHomeLinkPaths.length !== expectedRoutes.length) {
10381
+ failures.push({ code: "source_link_count_mismatch", expected: expectedRoutes.length, actual_unique: uniqueHomeLinkPaths.length, actual_total: homeLinkPaths.length });
10382
+ }
10383
+
10384
+ const directRoutes = [];
10385
+ if (check.run_direct_routes !== false) {
10386
+ for (const expectedRoute of expectedRoutes) {
10387
+ let error = "";
10388
+ try {
10389
+ await page.goto(inventoryRouteUrl(expectedRoute.path), { waitUntil: "domcontentloaded", timeout: 90000 });
10390
+ await waitForInventoryRouteHealth(check, expectedRoute.path);
10391
+ } catch (caught) {
10392
+ error = String(caught && caught.message ? caught.message : caught).slice(0, 1000);
10393
+ }
10394
+ const snapshot = await collectInventoryRouteSnapshot(expectedRoute, "direct", check, error);
10395
+ directRoutes.push(snapshot);
10396
+ if (check.save_route_screenshots) {
10397
+ await saveScreenshot(profileSlug + "-direct-" + inventorySlugFromPath(expectedRoute.path)).catch(() => {});
10398
+ }
10399
+ if (error) failures.push({ code: "direct_route_unhealthy", name: expectedRoute.name || null, path: expectedRoute.path, error });
10400
+ 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 });
10401
+ 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 });
10402
+ }
10403
+ }
10404
+
10405
+ const clickthroughs = [];
10406
+ if (check.run_clickthroughs !== false) {
10407
+ for (const expectedRoute of expectedRoutes) {
10408
+ const result = { name: expectedRoute.name || null, path: expectedRoute.path, clicked: false, snapshot: null, error: null };
10409
+ try {
10410
+ await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 90000 });
10411
+ if (profile.target.wait_for_selector) {
10412
+ await page.waitForSelector(profile.target.wait_for_selector, { state: "visible", timeout: 15000 }).catch(() => {});
10413
+ }
10414
+ const index = await findInventoryLinkIndex(check, expectedRoute.path);
10415
+ if (index < 0) {
10416
+ result.error = "source_link_not_found";
10417
+ failures.push({ code: "source_link_clickthrough_missing", name: expectedRoute.name || null, path: expectedRoute.path });
10418
+ } else {
10419
+ const locator = page.locator(check.link_selector || "a[href]").nth(index);
10420
+ await locator.scrollIntoViewIfNeeded();
10421
+ await locator.click({ timeout: inventoryCheckTimeout(check), noWaitAfter: true });
10422
+ result.clicked = true;
10423
+ await waitForInventoryRouteHealth(check, expectedRoute.path);
10424
+ }
10425
+ } catch (caught) {
10426
+ result.error = String(caught && caught.message ? caught.message : caught).slice(0, 1000);
10427
+ }
10428
+ result.snapshot = await collectInventoryRouteSnapshot(expectedRoute, "clickthrough", check, result.error || "");
10429
+ clickthroughs.push(result);
10430
+ if (check.save_route_screenshots) {
10431
+ await saveScreenshot(profileSlug + "-click-" + inventorySlugFromPath(expectedRoute.path)).catch(() => {});
10432
+ }
10433
+ 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 });
10434
+ 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 });
10435
+ 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 });
10436
+ }
10437
+ }
10438
+
10439
+ return {
10440
+ version: "riddle-proof.route-inventory.v1",
10441
+ viewport: viewport.name,
10442
+ expected_routes: expectedRoutes,
10443
+ link_selector: check.link_selector || "a[href]",
10444
+ source_selector: check.source_selector || null,
10445
+ home_game_link_count: homeLinkPaths.length,
10446
+ home_unique_game_link_count: uniqueHomeLinkPaths.length,
10447
+ home_links: homeLinks,
10448
+ direct_routes: directRoutes,
10449
+ clickthroughs,
10450
+ failures,
10451
+ };
10452
+ }
10054
10453
  async function captureViewport(viewport) {
10055
10454
  await page.setViewportSize({ width: viewport.width, height: viewport.height });
10056
10455
  let httpStatus = null;
@@ -10173,6 +10572,31 @@ async function captureViewport(viewport) {
10173
10572
  } catch (error) {
10174
10573
  pageErrors.push({ message: "saveScreenshot failed: " + String(error && error.message ? error.message : error).slice(0, 500) });
10175
10574
  }
10575
+ let routeInventory;
10576
+ const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
10577
+ const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
10578
+ if (routeInventoryCheck && (!firstViewportName || viewport.name === firstViewportName)) {
10579
+ try {
10580
+ routeInventory = await collectRouteInventory(routeInventoryCheck, viewport);
10581
+ } catch (error) {
10582
+ routeInventory = {
10583
+ version: "riddle-proof.route-inventory.v1",
10584
+ viewport: viewport.name,
10585
+ expected_routes: routeInventoryCheck.expected_routes || [],
10586
+ link_selector: routeInventoryCheck.link_selector || "a[href]",
10587
+ source_selector: routeInventoryCheck.source_selector || null,
10588
+ home_game_link_count: 0,
10589
+ home_unique_game_link_count: 0,
10590
+ home_links: [],
10591
+ direct_routes: [],
10592
+ clickthroughs: [],
10593
+ failures: [{
10594
+ code: "route_inventory_capture_failed",
10595
+ error: String(error && error.message ? error.message : error).slice(0, 1000),
10596
+ }],
10597
+ };
10598
+ }
10599
+ }
10176
10600
  const expectedPath = profile.checks.find((check) => check.type === "route_loaded" && check.expected_path)?.expected_path || new URL(targetUrl).pathname || "/";
10177
10601
  return {
10178
10602
  name: viewport.name,
@@ -10197,6 +10621,7 @@ async function captureViewport(viewport) {
10197
10621
  overflow_offenders: dom.overflow_offenders || [],
10198
10622
  selectors,
10199
10623
  text_matches,
10624
+ route_inventory: routeInventory,
10200
10625
  setup_action_results: setupActionResults,
10201
10626
  screenshot_label: screenshotLabel,
10202
10627
  navigation_error: navigationError,
@@ -10229,6 +10654,16 @@ function buildProfileEvidence(currentViewports) {
10229
10654
  overflow_px: currentViewports.map((viewport) => viewport.overflow_px),
10230
10655
  bounds_overflow_px: currentViewports.map((viewport) => viewport.bounds_overflow_px),
10231
10656
  overflow_offender_counts: currentViewports.map((viewport) => (viewport.overflow_offenders || []).length),
10657
+ route_inventory: currentViewports
10658
+ .filter((viewport) => viewport.route_inventory)
10659
+ .map((viewport) => ({
10660
+ viewport: viewport.name,
10661
+ expected_count: (viewport.route_inventory.expected_routes || []).length,
10662
+ home_unique_game_link_count: viewport.route_inventory.home_unique_game_link_count,
10663
+ direct_route_count: (viewport.route_inventory.direct_routes || []).length,
10664
+ clickthrough_count: (viewport.route_inventory.clickthroughs || []).length,
10665
+ failure_count: (viewport.route_inventory.failures || []).length,
10666
+ })),
10232
10667
  network_mock_count: (profile.target.network_mocks || []).length,
10233
10668
  network_mock_hit_count: networkMockEvents.filter((event) => event.ok !== false).length,
10234
10669
  },
package/dist/index.d.cts CHANGED
@@ -10,5 +10,5 @@ export { CreateCaptureDiagnosticInput, DEFAULT_DIAGNOSTIC_ARRAY_LIMIT, DEFAULT_D
10
10
  export { BuildVisualProofSessionInput, RIDDLE_PROOF_VISUAL_SESSION_FINGERPRINT_VERSION, RIDDLE_PROOF_VISUAL_SESSION_VERSION, VisualProofSessionMismatch, buildVisualProofSession, compareVisualProofSessionFingerprint, parseVisualProofSession, visualSessionFingerprint, visualSessionFingerprintBasis } from './proof-session.cjs';
11
11
  export { AssessPlayabilityOptions, RIDDLE_PROOF_PLAYABILITY_ASSESSMENT_VERSION, RIDDLE_PROOF_PLAYABILITY_VERSION, RiddleProofPlayabilityAssessment, RiddleProofPlayabilityEvidence, assessPlayabilityEvidence, extractPlayabilityEvidence, isRiddleProofPlayabilityMode } from './playability.cjs';
12
12
  export { AssessBasicGameplayOptions, AttachBasicGameplayArtifactOptions, BASIC_GAMEPLAY_ACTION_TYPES, BASIC_GAMEPLAY_PROGRESS_CHECK_TYPES, BasicGameplayActionResult, BasicGameplayActionType, BasicGameplayArtifactResolution, BasicGameplayAssessmentSummary, BasicGameplayBoundsOffender, BasicGameplayCanvasState, BasicGameplayCatchRecord, BasicGameplayChangeSummary, BasicGameplayFailureCode, BasicGameplayFixReference, BasicGameplayMetric, BasicGameplayMobileEvidence, BasicGameplayProgressCheckType, BasicGameplayProgressionCheck, BasicGameplayProofArtifact, BasicGameplayResponsiveViewportEvidence, BasicGameplayRouteReference, BasicGameplaySnapshot, BasicGameplaySuiteFailure, BasicGameplayWarningCode, CreateBasicGameplayCatchSummaryInput, RIDDLE_PROOF_BASIC_GAMEPLAY_ASSESSMENT_VERSION, RIDDLE_PROOF_BASIC_GAMEPLAY_CATCH_VERSION, RIDDLE_PROOF_BASIC_GAMEPLAY_VERSION, RiddleProofBasicGameplayAssessment, RiddleProofBasicGameplayCatchSummary, RiddleProofBasicGameplayEvidence, RiddleProofBasicGameplayRouteAssessment, RiddleProofBasicGameplayRouteEvidence, assessBasicGameplayEvidence, assessBasicGameplayProgressionCheck, assessBasicGameplayProgressionChecks, assessBasicGameplayRoute, attachBasicGameplayArtifactScreenshotHashes, augmentBasicGameplayAssessmentWithProgressionChecks, compactBasicGameplayText, createBasicGameplayCatchRecords, createBasicGameplayCatchSummary, extractBasicGameplayEvidence, resolveBasicGameplayProgressionCheckWithArtifactScreenshots, sanitizeBasicGameplayJsonString } from './basic-gameplay.cjs';
13
- export { 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, RiddleProofProfile, RiddleProofProfileArtifactRef, RiddleProofProfileBaselinePolicy, RiddleProofProfileBoundsOffender, RiddleProofProfileCheck, RiddleProofProfileCheckResult, RiddleProofProfileCheckType, RiddleProofProfileEvidence, RiddleProofProfileFailureAction, RiddleProofProfileNetworkMock, RiddleProofProfileResult, RiddleProofProfileRouteEvidence, RiddleProofProfileRunner, RiddleProofProfileSetupAction, RiddleProofProfileSetupActionType, RiddleProofProfileStatus, RiddleProofProfileTarget, RiddleProofProfileViewport, RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, extractRiddleProofProfileResult, normalizeRiddleProofProfile, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult } from './profile.cjs';
13
+ export { 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, RiddleProofProfile, RiddleProofProfileArtifactRef, RiddleProofProfileBaselinePolicy, RiddleProofProfileBoundsOffender, RiddleProofProfileCheck, RiddleProofProfileCheckResult, RiddleProofProfileCheckType, RiddleProofProfileEvidence, RiddleProofProfileFailureAction, RiddleProofProfileNetworkMock, RiddleProofProfileResult, RiddleProofProfileRouteEvidence, RiddleProofProfileRouteInventoryRoute, RiddleProofProfileRunner, RiddleProofProfileSetupAction, RiddleProofProfileSetupActionType, RiddleProofProfileStatus, RiddleProofProfileTarget, RiddleProofProfileViewport, RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, extractRiddleProofProfileResult, normalizeRiddleProofProfile, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult } from './profile.cjs';
14
14
  export { DEFAULT_RIDDLE_API_BASE_URL, DEFAULT_RIDDLE_API_KEY_FILE, RiddleApiError, RiddleClientConfig, RiddleFetch, RiddlePollJobOptions, RiddlePollJobResult, RiddlePollProgressSnapshot, RiddlePollSummary, RiddlePreviewDeployResult, RiddleRunScriptInput, RiddleServerPreviewInput, RiddleServerPreviewResult, createRiddleApiClient, deployRiddleStaticPreview, isTerminalRiddleJobStatus, parseRiddleViewport, pollRiddleJob, resolveRiddleApiKey, riddleRequestJson, runRiddleScript, runRiddleServerPreview } from './riddle-client.cjs';
package/dist/index.d.ts CHANGED
@@ -10,5 +10,5 @@ export { CreateCaptureDiagnosticInput, DEFAULT_DIAGNOSTIC_ARRAY_LIMIT, DEFAULT_D
10
10
  export { BuildVisualProofSessionInput, RIDDLE_PROOF_VISUAL_SESSION_FINGERPRINT_VERSION, RIDDLE_PROOF_VISUAL_SESSION_VERSION, VisualProofSessionMismatch, buildVisualProofSession, compareVisualProofSessionFingerprint, parseVisualProofSession, visualSessionFingerprint, visualSessionFingerprintBasis } from './proof-session.js';
11
11
  export { AssessPlayabilityOptions, RIDDLE_PROOF_PLAYABILITY_ASSESSMENT_VERSION, RIDDLE_PROOF_PLAYABILITY_VERSION, RiddleProofPlayabilityAssessment, RiddleProofPlayabilityEvidence, assessPlayabilityEvidence, extractPlayabilityEvidence, isRiddleProofPlayabilityMode } from './playability.js';
12
12
  export { AssessBasicGameplayOptions, AttachBasicGameplayArtifactOptions, BASIC_GAMEPLAY_ACTION_TYPES, BASIC_GAMEPLAY_PROGRESS_CHECK_TYPES, BasicGameplayActionResult, BasicGameplayActionType, BasicGameplayArtifactResolution, BasicGameplayAssessmentSummary, BasicGameplayBoundsOffender, BasicGameplayCanvasState, BasicGameplayCatchRecord, BasicGameplayChangeSummary, BasicGameplayFailureCode, BasicGameplayFixReference, BasicGameplayMetric, BasicGameplayMobileEvidence, BasicGameplayProgressCheckType, BasicGameplayProgressionCheck, BasicGameplayProofArtifact, BasicGameplayResponsiveViewportEvidence, BasicGameplayRouteReference, BasicGameplaySnapshot, BasicGameplaySuiteFailure, BasicGameplayWarningCode, CreateBasicGameplayCatchSummaryInput, RIDDLE_PROOF_BASIC_GAMEPLAY_ASSESSMENT_VERSION, RIDDLE_PROOF_BASIC_GAMEPLAY_CATCH_VERSION, RIDDLE_PROOF_BASIC_GAMEPLAY_VERSION, RiddleProofBasicGameplayAssessment, RiddleProofBasicGameplayCatchSummary, RiddleProofBasicGameplayEvidence, RiddleProofBasicGameplayRouteAssessment, RiddleProofBasicGameplayRouteEvidence, assessBasicGameplayEvidence, assessBasicGameplayProgressionCheck, assessBasicGameplayProgressionChecks, assessBasicGameplayRoute, attachBasicGameplayArtifactScreenshotHashes, augmentBasicGameplayAssessmentWithProgressionChecks, compactBasicGameplayText, createBasicGameplayCatchRecords, createBasicGameplayCatchSummary, extractBasicGameplayEvidence, resolveBasicGameplayProgressionCheckWithArtifactScreenshots, sanitizeBasicGameplayJsonString } from './basic-gameplay.js';
13
- export { 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, RiddleProofProfile, RiddleProofProfileArtifactRef, RiddleProofProfileBaselinePolicy, RiddleProofProfileBoundsOffender, RiddleProofProfileCheck, RiddleProofProfileCheckResult, RiddleProofProfileCheckType, RiddleProofProfileEvidence, RiddleProofProfileFailureAction, RiddleProofProfileNetworkMock, RiddleProofProfileResult, RiddleProofProfileRouteEvidence, RiddleProofProfileRunner, RiddleProofProfileSetupAction, RiddleProofProfileSetupActionType, RiddleProofProfileStatus, RiddleProofProfileTarget, RiddleProofProfileViewport, RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, extractRiddleProofProfileResult, normalizeRiddleProofProfile, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult } from './profile.js';
13
+ export { 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, RiddleProofProfile, RiddleProofProfileArtifactRef, RiddleProofProfileBaselinePolicy, RiddleProofProfileBoundsOffender, RiddleProofProfileCheck, RiddleProofProfileCheckResult, RiddleProofProfileCheckType, RiddleProofProfileEvidence, RiddleProofProfileFailureAction, RiddleProofProfileNetworkMock, RiddleProofProfileResult, RiddleProofProfileRouteEvidence, RiddleProofProfileRouteInventoryRoute, RiddleProofProfileRunner, RiddleProofProfileSetupAction, RiddleProofProfileSetupActionType, RiddleProofProfileStatus, RiddleProofProfileTarget, RiddleProofProfileViewport, RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, extractRiddleProofProfileResult, normalizeRiddleProofProfile, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult } from './profile.js';
14
14
  export { DEFAULT_RIDDLE_API_BASE_URL, DEFAULT_RIDDLE_API_KEY_FILE, RiddleApiError, RiddleClientConfig, RiddleFetch, RiddlePollJobOptions, RiddlePollJobResult, RiddlePollProgressSnapshot, RiddlePollSummary, RiddlePreviewDeployResult, RiddleRunScriptInput, RiddleServerPreviewInput, RiddleServerPreviewResult, createRiddleApiClient, deployRiddleStaticPreview, isTerminalRiddleJobStatus, parseRiddleViewport, pollRiddleJob, resolveRiddleApiKey, riddleRequestJson, runRiddleScript, runRiddleServerPreview } from './riddle-client.js';
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  resolveRiddleProofProfileTimeoutSec,
59
59
  slugifyRiddleProofProfileName,
60
60
  summarizeRiddleProofProfileResult
61
- } from "./chunk-2NWVXT4Q.js";
61
+ } from "./chunk-COUZXKGM.js";
62
62
  import {
63
63
  DEFAULT_RIDDLE_API_BASE_URL,
64
64
  DEFAULT_RIDDLE_API_KEY_FILE,