cbrowser 19.3.8 → 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.
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +10 -1
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/agent-ready-audit.d.ts +9 -0
- package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
- package/dist/analysis/agent-ready-audit.js +79 -6
- package/dist/analysis/agent-ready-audit.js.map +1 -1
- package/dist/analysis/bug-hunter.d.ts.map +1 -1
- package/dist/analysis/bug-hunter.js +27 -10
- package/dist/analysis/bug-hunter.js.map +1 -1
- package/dist/analysis/chaos-testing.d.ts.map +1 -1
- package/dist/analysis/chaos-testing.js +12 -3
- package/dist/analysis/chaos-testing.js.map +1 -1
- package/dist/analysis/competitive-benchmark.d.ts +50 -7
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
- package/dist/analysis/competitive-benchmark.js +226 -106
- package/dist/analysis/competitive-benchmark.js.map +1 -1
- package/dist/analysis/page-understanding.d.ts +2 -0
- package/dist/analysis/page-understanding.d.ts.map +1 -1
- package/dist/analysis/page-understanding.js +104 -17
- package/dist/analysis/page-understanding.js.map +1 -1
- package/dist/artifact-store.d.ts +24 -2
- package/dist/artifact-store.d.ts.map +1 -1
- package/dist/artifact-store.js +34 -4
- package/dist/artifact-store.js.map +1 -1
- package/dist/browser/session-manager.d.ts.map +1 -1
- package/dist/browser/session-manager.js +3 -0
- package/dist/browser/session-manager.js.map +1 -1
- package/dist/browser/site-profile-manager.d.ts +17 -1
- package/dist/browser/site-profile-manager.d.ts.map +1 -1
- package/dist/browser/site-profile-manager.js +49 -12
- package/dist/browser/site-profile-manager.js.map +1 -1
- package/dist/browser.d.ts +58 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +377 -50
- package/dist/browser.js.map +1 -1
- package/dist/charge-scope.d.ts.map +1 -1
- package/dist/charge-scope.js +6 -4
- package/dist/charge-scope.js.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -2
- package/dist/config.js.map +1 -1
- package/dist/mcp-server-remote.d.ts +7 -1
- package/dist/mcp-server-remote.d.ts.map +1 -1
- package/dist/mcp-server-remote.js +127 -83
- package/dist/mcp-server-remote.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +4 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/analysis-tools.js +5 -0
- package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
- package/dist/mcp-tools/base/audit-tools.d.ts +14 -0
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +17 -8
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.js +18 -27
- package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
- package/dist/mcp-tools/base/capture-tools.d.ts +18 -0
- package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/capture-tools.js +109 -55
- package/dist/mcp-tools/base/capture-tools.js.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.js +43 -16
- package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
- package/dist/mcp-tools/base/marketing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/marketing-tools.js +8 -4
- package/dist/mcp-tools/base/marketing-tools.js.map +1 -1
- package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/persona-comparison-tools.js +14 -3
- package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
- package/dist/mcp-tools/base/remediation-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/remediation-tools.js +10 -2
- package/dist/mcp-tools/base/remediation-tools.js.map +1 -1
- package/dist/mcp-tools/base/session-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/session-tools.js +2 -0
- package/dist/mcp-tools/base/session-tools.js.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.d.ts +13 -0
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +45 -16
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -1
- package/dist/mcp-tools/persona-creation-tools.js +59 -13
- package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
- package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -1
- package/dist/mcp-tools/persona-lifecycle-tools.js +4 -2
- package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -1
- package/dist/mcp-tools/tier-gate.d.ts +4 -4
- package/dist/mcp-tools/tier-gate.d.ts.map +1 -1
- package/dist/mcp-tools/tier-gate.js +41 -19
- package/dist/mcp-tools/tier-gate.js.map +1 -1
- package/dist/mcp-tools/tool-categories.d.ts +11 -0
- package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
- package/dist/mcp-tools/tool-categories.js +27 -1
- package/dist/mcp-tools/tool-categories.js.map +1 -1
- package/dist/mcp-tools/ui-resources.d.ts.map +1 -1
- package/dist/mcp-tools/ui-resources.js +32 -10
- package/dist/mcp-tools/ui-resources.js.map +1 -1
- package/dist/personas.d.ts +14 -0
- package/dist/personas.d.ts.map +1 -1
- package/dist/personas.js +26 -0
- package/dist/personas.js.map +1 -1
- package/dist/recording/engine.d.ts.map +1 -1
- package/dist/recording/engine.js +2 -1
- package/dist/recording/engine.js.map +1 -1
- package/dist/remediation/patch-generator.d.ts.map +1 -1
- package/dist/remediation/patch-generator.js +7 -1
- package/dist/remediation/patch-generator.js.map +1 -1
- package/dist/remediation/structured-data.d.ts +73 -1
- package/dist/remediation/structured-data.d.ts.map +1 -1
- package/dist/remediation/structured-data.js +239 -63
- package/dist/remediation/structured-data.js.map +1 -1
- package/dist/request-context.d.ts +18 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +56 -0
- package/dist/request-context.js.map +1 -0
- package/dist/site-model/manager.d.ts +14 -1
- package/dist/site-model/manager.d.ts.map +1 -1
- package/dist/site-model/manager.js +24 -1
- package/dist/site-model/manager.js.map +1 -1
- package/dist/types.d.ts +90 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/values/index.d.ts +1 -1
- package/dist/values/index.d.ts.map +1 -1
- package/dist/values/index.js +1 -1
- package/dist/values/index.js.map +1 -1
- package/dist/values/persona-values.d.ts +16 -0
- package/dist/values/persona-values.d.ts.map +1 -1
- package/dist/values/persona-values.js +56 -4
- package/dist/values/persona-values.js.map +1 -1
- package/dist/visual/ab-comparison.d.ts +22 -0
- package/dist/visual/ab-comparison.d.ts.map +1 -1
- package/dist/visual/ab-comparison.js +36 -9
- package/dist/visual/ab-comparison.js.map +1 -1
- package/dist/visual/cognitive-transport.d.ts +15 -1
- package/dist/visual/cognitive-transport.d.ts.map +1 -1
- package/dist/visual/cognitive-transport.js +42 -12
- package/dist/visual/cognitive-transport.js.map +1 -1
- package/dist/visual/cross-browser.d.ts +8 -0
- package/dist/visual/cross-browser.d.ts.map +1 -1
- package/dist/visual/cross-browser.js +8 -4
- package/dist/visual/cross-browser.js.map +1 -1
- package/dist/visual/distance-metrics.d.ts +11 -2
- package/dist/visual/distance-metrics.d.ts.map +1 -1
- package/dist/visual/distance-metrics.js +44 -5
- package/dist/visual/distance-metrics.js.map +1 -1
- package/dist/visual/regression.d.ts +61 -0
- package/dist/visual/regression.d.ts.map +1 -1
- package/dist/visual/regression.js +123 -50
- package/dist/visual/regression.js.map +1 -1
- package/dist/visual/responsive.d.ts +8 -0
- package/dist/visual/responsive.d.ts.map +1 -1
- package/dist/visual/responsive.js +95 -17
- package/dist/visual/responsive.js.map +1 -1
- 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
|
-
|
|
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
|
-
//
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
*
|
|
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
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1468
|
-
*
|
|
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
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
|
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
|
|
1514
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* `
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
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
|
|
1543
|
-
|
|
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
|
|
1551
|
-
* not
|
|
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,
|
|
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) => !
|
|
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
|
-
|
|
1603
|
-
|
|
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
|
-
|
|
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",
|