cbrowser 19.3.7 → 19.3.9

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.
Files changed (159) hide show
  1. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  2. package/dist/analysis/accessibility-empathy.js +10 -1
  3. package/dist/analysis/accessibility-empathy.js.map +1 -1
  4. package/dist/analysis/agent-ready-audit.d.ts +9 -0
  5. package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
  6. package/dist/analysis/agent-ready-audit.js +79 -6
  7. package/dist/analysis/agent-ready-audit.js.map +1 -1
  8. package/dist/analysis/bug-hunter.d.ts.map +1 -1
  9. package/dist/analysis/bug-hunter.js +27 -10
  10. package/dist/analysis/bug-hunter.js.map +1 -1
  11. package/dist/analysis/chaos-testing.d.ts.map +1 -1
  12. package/dist/analysis/chaos-testing.js +12 -3
  13. package/dist/analysis/chaos-testing.js.map +1 -1
  14. package/dist/analysis/competitive-benchmark.d.ts +50 -7
  15. package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
  16. package/dist/analysis/competitive-benchmark.js +226 -106
  17. package/dist/analysis/competitive-benchmark.js.map +1 -1
  18. package/dist/analysis/page-understanding.d.ts +2 -0
  19. package/dist/analysis/page-understanding.d.ts.map +1 -1
  20. package/dist/analysis/page-understanding.js +104 -17
  21. package/dist/analysis/page-understanding.js.map +1 -1
  22. package/dist/artifact-store.d.ts +24 -2
  23. package/dist/artifact-store.d.ts.map +1 -1
  24. package/dist/artifact-store.js +34 -4
  25. package/dist/artifact-store.js.map +1 -1
  26. package/dist/browser/session-manager.d.ts.map +1 -1
  27. package/dist/browser/session-manager.js +3 -0
  28. package/dist/browser/session-manager.js.map +1 -1
  29. package/dist/browser/site-profile-manager.d.ts +17 -1
  30. package/dist/browser/site-profile-manager.d.ts.map +1 -1
  31. package/dist/browser/site-profile-manager.js +49 -12
  32. package/dist/browser/site-profile-manager.js.map +1 -1
  33. package/dist/browser.d.ts +58 -1
  34. package/dist/browser.d.ts.map +1 -1
  35. package/dist/browser.js +377 -50
  36. package/dist/browser.js.map +1 -1
  37. package/dist/charge-scope.d.ts.map +1 -1
  38. package/dist/charge-scope.js +6 -4
  39. package/dist/charge-scope.js.map +1 -1
  40. package/dist/cli.js +3 -0
  41. package/dist/cli.js.map +1 -1
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +8 -2
  44. package/dist/config.js.map +1 -1
  45. package/dist/mcp-server-remote.d.ts +27 -1
  46. package/dist/mcp-server-remote.d.ts.map +1 -1
  47. package/dist/mcp-server-remote.js +160 -87
  48. package/dist/mcp-server-remote.js.map +1 -1
  49. package/dist/mcp-server.d.ts.map +1 -1
  50. package/dist/mcp-server.js +4 -0
  51. package/dist/mcp-server.js.map +1 -1
  52. package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
  53. package/dist/mcp-tools/base/analysis-tools.js +5 -0
  54. package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
  55. package/dist/mcp-tools/base/audit-tools.d.ts +14 -0
  56. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  57. package/dist/mcp-tools/base/audit-tools.js +17 -8
  58. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  59. package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
  60. package/dist/mcp-tools/base/browser-state-tools.js +18 -27
  61. package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
  62. package/dist/mcp-tools/base/capture-tools.d.ts +18 -0
  63. package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
  64. package/dist/mcp-tools/base/capture-tools.js +109 -55
  65. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  66. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
  67. package/dist/mcp-tools/base/cognitive-tools.js +43 -16
  68. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
  69. package/dist/mcp-tools/base/marketing-tools.d.ts.map +1 -1
  70. package/dist/mcp-tools/base/marketing-tools.js +8 -4
  71. package/dist/mcp-tools/base/marketing-tools.js.map +1 -1
  72. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
  73. package/dist/mcp-tools/base/persona-comparison-tools.js +14 -3
  74. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
  75. package/dist/mcp-tools/base/remediation-tools.d.ts.map +1 -1
  76. package/dist/mcp-tools/base/remediation-tools.js +10 -2
  77. package/dist/mcp-tools/base/remediation-tools.js.map +1 -1
  78. package/dist/mcp-tools/base/session-tools.d.ts.map +1 -1
  79. package/dist/mcp-tools/base/session-tools.js +2 -0
  80. package/dist/mcp-tools/base/session-tools.js.map +1 -1
  81. package/dist/mcp-tools/base/visual-testing-tools.d.ts +13 -0
  82. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  83. package/dist/mcp-tools/base/visual-testing-tools.js +45 -16
  84. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  85. package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -1
  86. package/dist/mcp-tools/persona-creation-tools.js +59 -13
  87. package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
  88. package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -1
  89. package/dist/mcp-tools/persona-lifecycle-tools.js +4 -2
  90. package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -1
  91. package/dist/mcp-tools/tier-gate.d.ts +4 -4
  92. package/dist/mcp-tools/tier-gate.d.ts.map +1 -1
  93. package/dist/mcp-tools/tier-gate.js +41 -19
  94. package/dist/mcp-tools/tier-gate.js.map +1 -1
  95. package/dist/mcp-tools/tool-categories.d.ts +11 -0
  96. package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
  97. package/dist/mcp-tools/tool-categories.js +27 -1
  98. package/dist/mcp-tools/tool-categories.js.map +1 -1
  99. package/dist/mcp-tools/ui-resources.d.ts.map +1 -1
  100. package/dist/mcp-tools/ui-resources.js +32 -10
  101. package/dist/mcp-tools/ui-resources.js.map +1 -1
  102. package/dist/personas.d.ts +14 -0
  103. package/dist/personas.d.ts.map +1 -1
  104. package/dist/personas.js +26 -0
  105. package/dist/personas.js.map +1 -1
  106. package/dist/recording/engine.d.ts.map +1 -1
  107. package/dist/recording/engine.js +2 -1
  108. package/dist/recording/engine.js.map +1 -1
  109. package/dist/remediation/patch-generator.d.ts.map +1 -1
  110. package/dist/remediation/patch-generator.js +7 -1
  111. package/dist/remediation/patch-generator.js.map +1 -1
  112. package/dist/remediation/structured-data.d.ts +73 -1
  113. package/dist/remediation/structured-data.d.ts.map +1 -1
  114. package/dist/remediation/structured-data.js +239 -63
  115. package/dist/remediation/structured-data.js.map +1 -1
  116. package/dist/request-context.d.ts +18 -0
  117. package/dist/request-context.d.ts.map +1 -0
  118. package/dist/request-context.js +56 -0
  119. package/dist/request-context.js.map +1 -0
  120. package/dist/site-model/manager.d.ts +14 -1
  121. package/dist/site-model/manager.d.ts.map +1 -1
  122. package/dist/site-model/manager.js +24 -1
  123. package/dist/site-model/manager.js.map +1 -1
  124. package/dist/types.d.ts +90 -2
  125. package/dist/types.d.ts.map +1 -1
  126. package/dist/types.js.map +1 -1
  127. package/dist/values/index.d.ts +1 -1
  128. package/dist/values/index.d.ts.map +1 -1
  129. package/dist/values/index.js +1 -1
  130. package/dist/values/index.js.map +1 -1
  131. package/dist/values/persona-values.d.ts +16 -0
  132. package/dist/values/persona-values.d.ts.map +1 -1
  133. package/dist/values/persona-values.js +56 -4
  134. package/dist/values/persona-values.js.map +1 -1
  135. package/dist/visual/ab-comparison.d.ts +22 -0
  136. package/dist/visual/ab-comparison.d.ts.map +1 -1
  137. package/dist/visual/ab-comparison.js +36 -9
  138. package/dist/visual/ab-comparison.js.map +1 -1
  139. package/dist/visual/cognitive-transport.d.ts +15 -1
  140. package/dist/visual/cognitive-transport.d.ts.map +1 -1
  141. package/dist/visual/cognitive-transport.js +42 -12
  142. package/dist/visual/cognitive-transport.js.map +1 -1
  143. package/dist/visual/cross-browser.d.ts +8 -0
  144. package/dist/visual/cross-browser.d.ts.map +1 -1
  145. package/dist/visual/cross-browser.js +8 -4
  146. package/dist/visual/cross-browser.js.map +1 -1
  147. package/dist/visual/distance-metrics.d.ts +11 -2
  148. package/dist/visual/distance-metrics.d.ts.map +1 -1
  149. package/dist/visual/distance-metrics.js +44 -5
  150. package/dist/visual/distance-metrics.js.map +1 -1
  151. package/dist/visual/regression.d.ts +61 -0
  152. package/dist/visual/regression.d.ts.map +1 -1
  153. package/dist/visual/regression.js +123 -50
  154. package/dist/visual/regression.js.map +1 -1
  155. package/dist/visual/responsive.d.ts +8 -0
  156. package/dist/visual/responsive.d.ts.map +1 -1
  157. package/dist/visual/responsive.js +95 -17
  158. package/dist/visual/responsive.js.map +1 -1
  159. package/package.json +1 -1
@@ -1171,7 +1171,7 @@ export async function runCompetitiveBenchmark(options) {
1171
1171
  recommendations,
1172
1172
  };
1173
1173
  }
1174
- import { runAgentReadyAudit } from "./agent-ready-audit.js";
1174
+ import { runAgentReadyAudit, summarizeIssueGroup } from "./agent-ready-audit.js";
1175
1175
  // ============================================================================
1176
1176
  // Error Categorization & Retry Logic (v18.15.0)
1177
1177
  // ============================================================================
@@ -1318,57 +1318,7 @@ export async function runAIReadinessBenchmark(options) {
1318
1318
  };
1319
1319
  }
1320
1320
  const audit = retryResult.result;
1321
- // Determine strengths based on high category scores
1322
- const strengths = [];
1323
- if (audit.score.findability >= 80) {
1324
- strengths.push("Easy for agents to locate elements");
1325
- }
1326
- if (audit.score.stability >= 80) {
1327
- strengths.push("Stable selectors that won't break");
1328
- }
1329
- if (audit.score.agentPerceivability >= 80) {
1330
- strengths.push("Good ARIA labels for identification");
1331
- }
1332
- if (audit.score.semantics >= 80) {
1333
- strengths.push("Clear semantic HTML structure");
1334
- }
1335
- // Determine weaknesses based on low category scores
1336
- const weaknesses = [];
1337
- if (audit.score.findability < 60) {
1338
- weaknesses.push("Agents struggle to find elements");
1339
- }
1340
- if (audit.score.stability < 60) {
1341
- weaknesses.push("Dynamic selectors break automation");
1342
- }
1343
- if (audit.score.agentPerceivability < 60) {
1344
- weaknesses.push("Missing labels for element identification");
1345
- }
1346
- if (audit.score.semantics < 60) {
1347
- weaknesses.push("Poor semantic structure");
1348
- }
1349
- // Add issue-based weaknesses
1350
- const topThree = audit.issues.slice(0, 3);
1351
- const issueDescriptions = topThree.map((issue) => issue.description);
1352
- // Carried alongside the descriptions because reconciliation needs the
1353
- // CATEGORY, and mapping to description strings threw it away — which is
1354
- // why a strength and its contradiction could not be matched up.
1355
- const issueCategories = topThree.map((issue) => String(issue.category));
1356
- return {
1357
- url,
1358
- siteName,
1359
- grade: audit.grade,
1360
- score: audit.score.overall,
1361
- scoreBreakdown: audit.score,
1362
- topIssues: issueDescriptions,
1363
- topIssueCategories: issueCategories,
1364
- strengths: strengths.length > 0
1365
- ? reconcileStrengths(strengths, issueCategories, audit.score)
1366
- : ["No major strengths detected"],
1367
- weaknesses: weaknesses.length > 0 ? weaknesses : issueDescriptions,
1368
- duration: audit.duration,
1369
- auditStatus: "complete",
1370
- retryAttempts: retryResult.attempts,
1371
- };
1321
+ return siteResultFromAudit(url, siteName, audit, retryResult.attempts);
1372
1322
  }));
1373
1323
  results.push(...batchResults);
1374
1324
  }
@@ -1403,10 +1353,59 @@ export async function runAIReadinessBenchmark(options) {
1403
1353
  recommendations,
1404
1354
  };
1405
1355
  }
1356
+ /**
1357
+ * One completed audit as a benchmark site result. A pure function, so the
1358
+ * fields the comparison and the competitor references read (checks,
1359
+ * topIssueChecks, weaknessKeys) are testable without a browser.
1360
+ * (report v8 B28, 2026-10-10)
1361
+ */
1362
+ export function siteResultFromAudit(url, siteName, audit, retryAttempts) {
1363
+ // Strengths and weaknesses from the axis scores, phrased per axis by
1364
+ // AXIS_PHRASES so a phrase cannot drift from the axis it claims.
1365
+ const strengths = AXES
1366
+ .filter((axis) => audit.score[axis] >= 80)
1367
+ .map((axis) => AXIS_PHRASES[axis].strength);
1368
+ const weakAxes = AXES.filter((axis) => audit.score[axis] < 60);
1369
+ // One entry per check, each summarized over all of its issues. Was
1370
+ // audit.issues.slice(0, 3): ucdenver.edu's topIssues read "Hidden select
1371
+ // with custom UI..." three times and nothing else. (report v8 B28,
1372
+ // 2026-10-10)
1373
+ const top = topIssueGroups(audit.issues, 3);
1374
+ const issueDescriptions = top.map((g) => g.summary);
1375
+ // Carried alongside the descriptions because reconciliation needs the
1376
+ // CATEGORY, and mapping to description strings threw it away — which is
1377
+ // why a strength and its contradiction could not be matched up.
1378
+ const issueCategories = top.map((g) => g.category);
1379
+ return {
1380
+ url,
1381
+ siteName,
1382
+ grade: audit.grade,
1383
+ score: audit.score.overall,
1384
+ scoreBreakdown: audit.score,
1385
+ topIssues: issueDescriptions,
1386
+ topIssueCategories: issueCategories,
1387
+ topIssueChecks: top.map((g) => g.check),
1388
+ checks: siteChecks(audit.issues),
1389
+ // Every issue's category, not the top three's: a top entry is now a
1390
+ // whole check, so counting entries would undercount the elements.
1391
+ strengths: strengths.length > 0
1392
+ ? reconcileStrengths(strengths, audit.issues.map((i) => String(i.category)), audit.score)
1393
+ : ["No major strengths detected"],
1394
+ weaknesses: weakAxes.length > 0
1395
+ ? weakAxes.map((axis) => AXIS_PHRASES[axis].weakness)
1396
+ : issueDescriptions,
1397
+ weaknessKeys: weakAxes.length > 0
1398
+ ? weakAxes.map((axis) => ({ axis }))
1399
+ : top.map((g) => ({ check: g.check })),
1400
+ duration: audit.duration,
1401
+ auditStatus: "complete",
1402
+ retryAttempts,
1403
+ };
1404
+ }
1406
1405
  /**
1407
1406
  * Generate comparative analysis between sites
1408
1407
  */
1409
- function generateAIComparison(results) {
1408
+ export function generateAIComparison(results) {
1410
1409
  // Only compare successfully audited sites
1411
1410
  const successfulResults = results.filter((r) => r.score !== null && r.scoreBreakdown !== null);
1412
1411
  // Find best in each category (from successful audits only)
@@ -1415,16 +1414,13 @@ function generateAIComparison(results) {
1415
1414
  const byStability = [...successfulResults].sort((a, b) => b.scoreBreakdown.stability - a.scoreBreakdown.stability);
1416
1415
  const byAgentPerceivability = [...successfulResults].sort((a, b) => b.scoreBreakdown.agentPerceivability - a.scoreBreakdown.agentPerceivability);
1417
1416
  const bySemantics = [...successfulResults].sort((a, b) => b.scoreBreakdown.semantics - a.scoreBreakdown.semantics);
1418
- // Find common issues
1419
- const issueCounts = new Map();
1420
- for (const result of results) {
1421
- for (const issue of result.topIssues) {
1422
- issueCounts.set(issue, (issueCounts.get(issue) || 0) + 1);
1423
- }
1424
- }
1425
- const commonIssues = Array.from(issueCounts.entries())
1426
- .filter(([_, count]) => count >= Math.ceil(results.length / 2))
1427
- .map(([issue]) => issue);
1417
+ // A check is common when at least two audited sites, and at least half of
1418
+ // them, fail it. The old count keyed on description text from each site's top
1419
+ // three, counted repeats within one site, and used ceil(n/2), which is 1 for
1420
+ // two sites. Measured 2026-10-10 on cbrowser.ai vs ucdenver.edu: all three
1421
+ // "common" issues came from cbrowser.ai alone, two of them the same check
1422
+ // ("score: 0/10" and "score: 2/10"). (report v8 B28)
1423
+ const commonIssues = commonChecks(successfulResults);
1428
1424
  // Build site advantages
1429
1425
  const siteAdvantages = {};
1430
1426
  for (const result of results) {
@@ -1434,16 +1430,16 @@ function generateAIComparison(results) {
1434
1430
  advantages.push("Best overall AI readiness");
1435
1431
  }
1436
1432
  if (result.siteName === byFindability[0]?.siteName && result.siteName !== byScore[0]?.siteName) {
1437
- advantages.push("Best element findability");
1433
+ advantages.push(AXIS_PHRASES.findability.best);
1438
1434
  }
1439
1435
  if (result.siteName === byStability[0]?.siteName && result.siteName !== byScore[0]?.siteName) {
1440
- advantages.push("Most stable selectors");
1436
+ advantages.push(AXIS_PHRASES.stability.best);
1441
1437
  }
1442
1438
  if (result.siteName === byAgentPerceivability[0]?.siteName && result.siteName !== byScore[0]?.siteName) {
1443
- advantages.push("Best ARIA/accessibility labels");
1439
+ advantages.push(AXIS_PHRASES.agentPerceivability.best);
1444
1440
  }
1445
1441
  if (result.siteName === bySemantics[0]?.siteName && result.siteName !== byScore[0]?.siteName) {
1446
- advantages.push("Best semantic structure");
1442
+ advantages.push(AXIS_PHRASES.semantics.best);
1447
1443
  }
1448
1444
  // Add strength-based advantages
1449
1445
  advantages.push(...result.strengths.filter(s => s !== "No major strengths detected"));
@@ -1459,39 +1455,75 @@ function generateAIComparison(results) {
1459
1455
  siteAdvantages,
1460
1456
  };
1461
1457
  }
1458
+ /** The order strengths, weaknesses and superlatives are listed in. */
1459
+ const AXES = ["findability", "stability", "agentPerceivability", "semantics"];
1462
1460
  /**
1463
- * Which scoring axis a strength phrase is a claim about.
1461
+ * What each scoring axis is called in benchmark output, keyed by the axis.
1462
+ *
1463
+ * Every phrase is generated from this table and mapped back through its
1464
+ * inverses below, so renaming one cannot leave reconciliation or deduplication
1465
+ * matching the old wording. A new strength gets its axis by construction; it
1466
+ * used to need a hand-kept entry, and without one it was silently exempt from
1467
+ * reconciliation, which is how a claim and its contradiction shipped side by
1468
+ * side.
1464
1469
  *
1465
- * The phrases are generated from axis thresholds a few lines below, so this map
1466
- * is their inverse. Kept beside them deliberately: a new strength added without
1467
- * an entry here is silently exempt from reconciliation, which is how a claim and
1468
- * its contradiction came to ship side by side.
1470
+ * The stability phrases said "Stable selectors that won't break", "Dynamic
1471
+ * selectors break automation" and "Most stable selectors". Selector stability
1472
+ * is scored under FINDABILITY (findability-score-check). The stability axis is
1473
+ * scored from hidden native controls, sticky elements that intercept clicks,
1474
+ * session state, infinite scroll and loading indicators, so a page could be
1475
+ * praised for stable selectors beside "N elements lack stable selectors".
1476
+ * (report v8 B28, 2026-10-10)
1469
1477
  */
1470
- const STRENGTH_AXIS = {
1471
- "Easy for agents to locate elements": "findability",
1472
- "Stable selectors that won't break": "stability",
1473
- "Good ARIA labels for identification": "agentPerceivability",
1474
- "Clear semantic HTML structure": "semantics",
1478
+ const AXIS_PHRASES = {
1479
+ findability: {
1480
+ strength: "Easy for agents to locate elements",
1481
+ weakness: "Agents struggle to find elements",
1482
+ best: "Best element findability",
1483
+ },
1484
+ stability: {
1485
+ strength: "Few hidden controls, click-blocking overlays or loading hazards",
1486
+ weakness: "Hidden controls, click-blocking overlays or dynamic loading disrupt agents",
1487
+ best: "Most stable interactions",
1488
+ },
1489
+ agentPerceivability: {
1490
+ strength: "Good ARIA labels for identification",
1491
+ weakness: "Missing labels for element identification",
1492
+ best: "Best ARIA/accessibility labels",
1493
+ },
1494
+ semantics: {
1495
+ strength: "Clear semantic HTML structure",
1496
+ weakness: "Poor semantic structure",
1497
+ best: "Best semantic structure",
1498
+ },
1475
1499
  };
1500
+ const phraseToAxis = (field) => Object.fromEntries(AXES.map((axis) => [AXIS_PHRASES[axis][field], axis]));
1501
+ /** Which scoring axis a strength phrase is a claim about. */
1502
+ const STRENGTH_AXIS = phraseToAxis("strength");
1476
1503
  /** Superlatives and plain strengths make the same claim at different scopes. */
1477
- const ADVANTAGE_AXIS = {
1478
- "Best element findability": "findability",
1479
- "Most stable selectors": "stability",
1480
- "Best ARIA/accessibility labels": "agentPerceivability",
1481
- "Best semantic structure": "semantics",
1482
- };
1504
+ const ADVANTAGE_AXIS = phraseToAxis("best");
1505
+ /** An issue category as an axis. The deprecated "accessibility" is agentPerceivability. */
1506
+ function categoryAxis(category) {
1507
+ return category === "accessibility" ? "agentPerceivability" : category;
1508
+ }
1483
1509
  /**
1484
- * Qualify a strength that the site's own top issues contradict.
1510
+ * Qualify a strength that the site's own issues contradict.
1485
1511
  *
1486
1512
  * `/pricing` shipped `strengths: ["Stable selectors that won't break"]` beside
1487
1513
  * `topIssues: ["Element lacks stable selectors (score: 0/10)"]`. Both are
1488
1514
  * arithmetically correct -- the first is an AXIS score at 92, the second is an
1489
1515
  * ELEMENT scoring 0 -- and they are not publishable together unreconciled.
1516
+ * (The strength's wording was also wrong: that axis does not score selectors.
1517
+ * Renamed in report v8 B28, 2026-10-10; see AXIS_PHRASES.)
1490
1518
  *
1491
1519
  * Qualified rather than suppressed. Stability 92 is real and useful; deleting
1492
1520
  * the strength would lose it to hide an inconsistency that is actually
1493
1521
  * information. The exception count is what a reader needs, and it is what the
1494
1522
  * old output withheld.
1523
+ *
1524
+ * `issueCategories` is one entry per issue. The benchmark passes every issue's
1525
+ * category; it passed the top three's, which stopped being a count of elements
1526
+ * once a top entry became a whole check.
1495
1527
  */
1496
1528
  export function reconcileStrengths(strengths, issueCategories, breakdown) {
1497
1529
  return strengths.map((strength) => {
@@ -1500,8 +1532,7 @@ export function reconcileStrengths(strengths, issueCategories, breakdown) {
1500
1532
  return strength;
1501
1533
  // The deprecated "accessibility" category is out of the type but can still
1502
1534
  // arrive at runtime from an older caller across a JSON boundary.
1503
- const exceptions = issueCategories.filter((c) => c === axis
1504
- || (axis === "agentPerceivability" && c === "accessibility")).length;
1535
+ const exceptions = issueCategories.filter((c) => categoryAxis(c) === axis).length;
1505
1536
  if (exceptions === 0)
1506
1537
  return strength;
1507
1538
  const score = breakdown[axis];
@@ -1510,8 +1541,8 @@ export function reconcileStrengths(strengths, issueCategories, breakdown) {
1510
1541
  }
1511
1542
  /**
1512
1543
  * Drop a plain strength when the superlative for the same axis is already
1513
- * present. "Most stable selectors" and "Stable selectors that won't break" are
1514
- * one claim written twice, and the list shipped both.
1544
+ * present. "Most stable interactions" and the stability strength are one claim
1545
+ * written twice, and the list shipped both.
1515
1546
  */
1516
1547
  export function dedupeAdvantagesByAxis(advantages) {
1517
1548
  const superlativeAxes = new Set(advantages.map((a) => ADVANTAGE_AXIS[a]).filter(Boolean));
@@ -1528,27 +1559,109 @@ export function dedupeAdvantagesByAxis(advantages) {
1528
1559
  return true;
1529
1560
  });
1530
1561
  }
1562
+ /** A page's issues grouped by check (`detectionMethod`), in first-seen order. */
1563
+ function groupByCheck(issues) {
1564
+ const groups = new Map();
1565
+ for (const issue of issues) {
1566
+ let group = groups.get(issue.detectionMethod);
1567
+ if (!group) {
1568
+ group = { check: issue.detectionMethod, category: String(issue.category), issues: [] };
1569
+ groups.set(issue.detectionMethod, group);
1570
+ }
1571
+ group.issues.push(issue);
1572
+ }
1573
+ return [...groups.values()];
1574
+ }
1575
+ /** Every check that fired on a page, once each, with every issue's description. */
1576
+ export function siteChecks(issues) {
1577
+ return groupByCheck(issues).map((g) => ({
1578
+ id: g.check,
1579
+ category: g.category,
1580
+ descriptions: g.issues.map((i) => i.description),
1581
+ }));
1582
+ }
1531
1583
  /**
1532
- * Everything a site is known to be bad at.
1584
+ * A page's top issues: one entry per check, summarized over all of that
1585
+ * check's issues, worst severity first (ties keep detection order).
1533
1586
  *
1534
- * `weaknesses` alone is not that set. It is `weaknesses.length > 0 ? weaknesses
1535
- * : issueDescriptions` -- so a site with a low AXIS score gets only its axis
1536
- * weaknesses, and its element-level issues never appear there at all. That is
1537
- * the gap the reference check fell through: cbrowser.ai/pricing carried a sticky
1538
- * -element issue in `topIssues` and not in `weaknesses`, so a check reading only
1539
- * `weaknesses` concluded it was clean and recommended it as the example to
1540
- * follow -- for the identical issue.
1587
+ * Was `audit.issues.slice(0, 3)` -- raw issues in detection order, so one check
1588
+ * could fill all three slots (ucdenver.edu: "Hidden select with custom UI..."
1589
+ * x3, measured 2026-10-10), and severity played no part in which three showed.
1590
+ * (report v8 B28)
1541
1591
  */
1542
- function issueSurface(r) {
1543
- return new Set([...(r.weaknesses ?? []), ...(r.topIssues ?? [])]);
1592
+ export function topIssueGroups(issues, limit = 3) {
1593
+ const rank = (sev) => ({ critical: 0, high: 1, medium: 2, low: 3 }[String(sev).toLowerCase()] ?? 4);
1594
+ return groupByCheck(issues)
1595
+ .map((g, i) => ({ g, i, worst: Math.min(...g.issues.map((x) => rank(x.severity))) }))
1596
+ .sort((a, b) => a.worst - b.worst || a.i - b.i)
1597
+ .slice(0, limit)
1598
+ .map(({ g }) => ({ check: g.check, category: g.category, summary: summarizeIssueGroup(g.issues) }));
1599
+ }
1600
+ /**
1601
+ * Checks shared by at least two audited sites and at least half of them, most
1602
+ * widely shared first, one line each: "k of n sites: <summary>".
1603
+ *
1604
+ * Keyed on the check id across every issue each site has, counted once per
1605
+ * site. The summary covers every matching issue on every sharing site, so it
1606
+ * is true of all of them (summarizeIssueGroup's contract).
1607
+ */
1608
+ export function commonChecks(audited) {
1609
+ const n = audited.length;
1610
+ const threshold = Math.max(2, Math.ceil(n / 2));
1611
+ const byCheck = new Map();
1612
+ for (const site of audited) {
1613
+ const counted = new Set();
1614
+ for (const check of site.checks ?? []) {
1615
+ let entry = byCheck.get(check.id);
1616
+ if (!entry) {
1617
+ entry = { sites: 0, issues: [] };
1618
+ byCheck.set(check.id, entry);
1619
+ }
1620
+ if (!counted.has(check.id)) {
1621
+ counted.add(check.id);
1622
+ entry.sites++;
1623
+ }
1624
+ entry.issues.push(...check.descriptions.map((description) => ({ description, detectionMethod: check.id })));
1625
+ }
1626
+ }
1627
+ // Map iteration is first-seen order, and sort is stable, so ties keep it.
1628
+ return [...byCheck.values()]
1629
+ .filter((e) => e.sites >= threshold)
1630
+ .sort((a, b) => b.sites - a.sites)
1631
+ .map((e) => `${e.sites} of ${n} sites: ${summarizeIssueGroup(e.issues)}`);
1632
+ }
1633
+ /**
1634
+ * Whether a candidate site has the issue a weakness is about.
1635
+ *
1636
+ * Matched on keys. The old check compared description strings, which carry
1637
+ * per-element figures ("score: 0/10" vs "score: 2/10", "1280x57px"), so the
1638
+ * same check on a candidate read as a different issue, and an axis weakness
1639
+ * matched only a candidate printing the same weakness line, never one with
1640
+ * issues on that axis: ucdenver.edu (stability under 60) was told cbrowser.ai
1641
+ * "does not have this issue" while cbrowser.ai had a stability-axis
1642
+ * sticky-element issue. Measured 2026-10-10. (report v8 B28)
1643
+ *
1644
+ * A candidate whose checks are unknown is treated as having it: no reference
1645
+ * beats a wrong one.
1646
+ */
1647
+ function hasIssue(candidate, key) {
1648
+ if (!candidate.checks)
1649
+ return true;
1650
+ if ("check" in key)
1651
+ return candidate.checks.some((c) => c.id === key.check);
1652
+ if (candidate.checks.some((c) => categoryAxis(c.category) === key.axis))
1653
+ return true;
1654
+ const score = candidate.scoreBreakdown?.[key.axis];
1655
+ return score === undefined || score < 60;
1544
1656
  }
1545
1657
  /**
1546
1658
  * A site worth holding up as an example for THIS issue, or nothing.
1547
1659
  *
1548
1660
  * Three conditions, and the tool previously enforced none of them:
1549
1661
  *
1550
- * 1. It must not share the issue. Checked against the full issue surface above,
1551
- * not just `weaknesses`.
1662
+ * 1. It must not share the issue: not the same check, or for an axis
1663
+ * weakness, no issue on that axis and the axis not itself under 60. See
1664
+ * hasIssue above.
1552
1665
  * 2. It must actually score BETTER than the site being advised. The old code
1553
1666
  * took the first array match, so cbrowser.ai/pricing (83) was told that
1554
1667
  * cbrowser.ai/docs/home (80) "handles this better" -- advice to copy a page
@@ -1561,13 +1674,13 @@ function issueSurface(r) {
1561
1674
  * the failure most likely to be screenshotted. No reference is strictly better
1562
1675
  * than a wrong one.
1563
1676
  */
1564
- export function pickBetterReference(candidates, subject, issue) {
1677
+ export function pickBetterReference(candidates, subject, key) {
1565
1678
  if (subject.score === null)
1566
1679
  return undefined;
1567
1680
  return candidates
1568
1681
  .filter((r) => r.siteName !== subject.siteName)
1569
1682
  .filter((r) => r.score !== null && r.score > subject.score)
1570
- .filter((r) => !issueSurface(r).has(issue))
1683
+ .filter((r) => !hasIssue(r, key))
1571
1684
  .sort((a, b) => (b.score ?? 0) - (a.score ?? 0))[0];
1572
1685
  }
1573
1686
  /**
@@ -1598,9 +1711,13 @@ function generateAIRecommendations(results, ranking) {
1598
1711
  if (result.siteName === bestSite?.site && successfulRankings.length > 1) {
1599
1712
  continue;
1600
1713
  }
1601
- // Add weakness-based recommendations
1602
- for (const weakness of result.weaknesses) {
1603
- const betterSite = pickBetterReference(successfulResults, { siteName: result.siteName, score: result.score }, weakness);
1714
+ // Add weakness-based recommendations. The reference is matched on the
1715
+ // weakness's key, not its text; a weakness with no key gets no reference.
1716
+ for (const [i, weakness] of result.weaknesses.entries()) {
1717
+ const key = result.weaknessKeys?.[i];
1718
+ const betterSite = key
1719
+ ? pickBetterReference(successfulResults, { siteName: result.siteName, score: result.score }, key)
1720
+ : undefined;
1604
1721
  recommendations.push({
1605
1722
  site: result.siteName,
1606
1723
  priority: priority++,
@@ -1623,7 +1740,10 @@ function generateAIRecommendations(results, ranking) {
1623
1740
  {
1624
1741
  name: "stability",
1625
1742
  score: result.scoreBreakdown.stability,
1626
- fix: "Use data-testid attributes and semantic selectors",
1743
+ // Was "Use data-testid attributes and semantic selectors", a
1744
+ // findability fix: this axis scores hidden controls, sticky overlays
1745
+ // and dynamic loading. (report v8 B28, 2026-10-10)
1746
+ fix: "Expose native form controls, keep sticky elements off click targets, and mark loading with aria-busy",
1627
1747
  },
1628
1748
  {
1629
1749
  name: "agentPerceivability",