nfunc-mcp 0.2.0 → 0.3.0
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/README.md +47 -5
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/dist/mappers/a11yDedupe.d.ts +25 -0
- package/dist/mappers/a11yDedupe.js +95 -0
- package/dist/mappers/a11yDedupe.js.map +1 -0
- package/dist/mappers/compositeScore.d.ts +23 -0
- package/dist/mappers/compositeScore.js +107 -0
- package/dist/mappers/compositeScore.js.map +1 -0
- package/dist/mappers/correlator.js +94 -33
- package/dist/mappers/correlator.js.map +1 -1
- package/dist/mappers/defectFormatter.js +129 -15
- package/dist/mappers/defectFormatter.js.map +1 -1
- package/dist/mappers/priorityMapper.d.ts +38 -0
- package/dist/mappers/priorityMapper.js +126 -0
- package/dist/mappers/priorityMapper.js.map +1 -1
- package/dist/tools/accessibility.js +72 -31
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/lighthouse.d.ts +14 -0
- package/dist/tools/lighthouse.js +134 -34
- package/dist/tools/lighthouse.js.map +1 -1
- package/dist/tools/qaGate.js +109 -34
- package/dist/tools/qaGate.js.map +1 -1
- package/dist/utils/outputParsers.d.ts +25 -0
- package/dist/utils/outputParsers.js +42 -0
- package/dist/utils/outputParsers.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ A local MCP server that gives Claude (or any MCP client) a full non-functional Q
|
|
|
22
22
|
- [HTML report](#html-report)
|
|
23
23
|
- [Output shape](#output-shape)
|
|
24
24
|
7. [Individual tools](#individual-tools)
|
|
25
|
+
- [Mobile vs desktop](#mobile-vs-desktop)
|
|
26
|
+
- [Choosing an accessibility engine](#choosing-an-accessibility-engine)
|
|
25
27
|
8. [Priority system](#priority-system)
|
|
26
28
|
9. [Project layout](#project-layout)
|
|
27
29
|
10. [How to prompt](#how-to-prompt)
|
|
@@ -128,10 +130,10 @@ claude mcp add nfunc-mcp -- node /absolute/path/to/NFunc_MCP/dist/index.js
|
|
|
128
130
|
| Tool | Description | Inputs |
|
|
129
131
|
|---|---|---|
|
|
130
132
|
| `ping` | Health check — confirms the server is up | — |
|
|
131
|
-
| `run_lighthouse` | Full Lighthouse audit for a URL | `url` |
|
|
132
|
-
| `run_accessibility_check` | pa11y WCAG audit for a URL | `url`, `standard` (optional), `ignore` (optional) |
|
|
133
|
+
| `run_lighthouse` | Full Lighthouse audit for a URL | `url`, `form_factor` (optional), `categories` (optional), `thresholds` (optional) |
|
|
134
|
+
| `run_accessibility_check` | pa11y WCAG audit for a URL | `url`, `runner` (optional), `standard` (optional), `ignore` (optional) |
|
|
133
135
|
| `run_static_analysis` | ESLint + Semgrep scan for a local codebase | `path` |
|
|
134
|
-
| `run_qa_gate` | All tools in parallel + correlation + HTML report | `url` and/or `path` |
|
|
136
|
+
| `run_qa_gate` | All tools in parallel + correlation + HTML report | `url` and/or `path`, `form_factor` (optional), `a11y_runner` (optional) |
|
|
135
137
|
|
|
136
138
|
---
|
|
137
139
|
|
|
@@ -148,6 +150,8 @@ Both inputs are **optional** — provide whichever you have. At least one is req
|
|
|
148
150
|
| `url` | string (URL) | You have a running page — production, staging, preview URL, or localhost. Enables Lighthouse and pa11y. |
|
|
149
151
|
| `path` | string (path) | You have a local codebase. Enables ESLint and Semgrep. |
|
|
150
152
|
| `context` | string | Optional. Free-text description of the project (e.g. `"React e-commerce checkout"`). Helps Claude interpret results. |
|
|
153
|
+
| `form_factor` | `mobile` \| `desktop` \| `both` | Optional, default `mobile`. Lighthouse device profile — see [Mobile vs desktop](#mobile-vs-desktop). |
|
|
154
|
+
| `a11y_runner` | `htmlcs` \| `axe` \| `both` | Optional, default `htmlcs`. pa11y engine — see [Choosing an accessibility engine](#choosing-an-accessibility-engine). |
|
|
151
155
|
|
|
152
156
|
**URL only** — browser-based checks, static analysis skipped:
|
|
153
157
|
```
|
|
@@ -300,9 +304,28 @@ Runs a full Lighthouse audit against a URL.
|
|
|
300
304
|
|
|
301
305
|
```
|
|
302
306
|
Run Lighthouse on https://myapp.com
|
|
307
|
+
Run Lighthouse on https://myapp.com for mobile and desktop
|
|
303
308
|
```
|
|
304
309
|
|
|
305
|
-
Returns: `url`, `scores` (per category), `ttfb_ms`, `findings` (priority-ordered).
|
|
310
|
+
Returns: `url`, `form_factor`, `scores` (per category), `ttfb_ms`, `findings` (priority-ordered).
|
|
311
|
+
|
|
312
|
+
#### Mobile vs desktop
|
|
313
|
+
|
|
314
|
+
`form_factor` accepts `desktop` (default), `mobile`, or `both`.
|
|
315
|
+
|
|
316
|
+
**This default deliberately differs from the Lighthouse CLI's**, which is mobile: a 412×823 screen, a mid-range Android user agent, simulated slow 4G, and a **4× CPU slowdown**. That profile is much harsher and reports substantially lower performance scores for the same page, so passing `mobile` here is not a like-for-like comparison with a default run — check `form_factor` in the response before comparing two reports.
|
|
317
|
+
|
|
318
|
+
**The two are not interchangeable.** They render different DOM, so they find different defects — not just different performance numbers. Measured against one commerce category page:
|
|
319
|
+
|
|
320
|
+
| | Mobile | Desktop |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| performance | 54 | 62 |
|
|
323
|
+
| accessibility | **87** | **73** |
|
|
324
|
+
| seo | 77 | 69 |
|
|
325
|
+
|
|
326
|
+
Five accessibility audits failed on desktop that mobile never reported — `image-alt`, `aria-required-children`, `aria-required-parent`, `aria-allowed-attr`, `aria-valid-attr-value` — while three others failed only on mobile. Neither profile is a superset of the other.
|
|
327
|
+
|
|
328
|
+
With `form_factor: "both"`, the two run concurrently (so it costs little more wall time than one), `scores` is keyed by form factor, and each finding carries `affects_form_factors` and `form_factor_specific` so device-only regressions are obvious at a glance.
|
|
306
329
|
|
|
307
330
|
### run_accessibility_check
|
|
308
331
|
|
|
@@ -311,9 +334,28 @@ Runs pa11y against a URL at WCAG 2 AA by default. Returns only violations (error
|
|
|
311
334
|
```
|
|
312
335
|
Run an accessibility check on https://myapp.com
|
|
313
336
|
Run accessibility check at AAA standard on https://myapp.com
|
|
337
|
+
Run an accessibility check on https://myapp.com using the axe runner
|
|
314
338
|
```
|
|
315
339
|
|
|
316
|
-
Returns: `url`, `standard`, `violation_count`, `findings`.
|
|
340
|
+
Returns: `url`, `standard`, `runners`, `violation_count`, `raw_violation_count`, `findings`.
|
|
341
|
+
|
|
342
|
+
#### Choosing an accessibility engine
|
|
343
|
+
|
|
344
|
+
`runner` accepts `htmlcs` (default), `axe`, or `both`.
|
|
345
|
+
|
|
346
|
+
| Engine | Strongest at | Severity source |
|
|
347
|
+
|---|---|---|
|
|
348
|
+
| `htmlcs` | WCAG techniques, document structure, form labelling, duplicate ids | WCAG technique class |
|
|
349
|
+
| `axe` | **ARIA** — invalid roles, missing required parent/child relationships, prohibited and unsupported attributes — and computed colour contrast | axe's own `impact` rating |
|
|
350
|
+
|
|
351
|
+
**Reach for `axe` whenever the work under test involves ARIA, a component library, or a design system.** The overlap between the engines is smaller than you would expect. On the same page:
|
|
352
|
+
|
|
353
|
+
- htmlcs found unlabelled inputs, forms with no submit mechanism, and ten duplicate ids that axe did not report.
|
|
354
|
+
- axe found `aria-allowed-attr`, `aria-prohibited-attr`, `aria-required-parent`, `aria-required-children` and `image-alt` failures that htmlcs missed entirely.
|
|
355
|
+
|
|
356
|
+
`both` runs them concurrently and merges the results. Note that an element flagged by both engines appears twice, because they emit different rule codes for the same defect — that is deliberate, since two independent engines agreeing is corroboration worth seeing.
|
|
357
|
+
|
|
358
|
+
Findings from axe carry `axe_impact` in evidence, and `needs_manual_review: true` where axe wants a human to confirm (those are demoted one priority tier — a maybe should not gate a release as hard as a certainty).
|
|
317
359
|
|
|
318
360
|
### run_static_analysis
|
|
319
361
|
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
2
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
6
|
import { registerLighthouseTool } from "./tools/lighthouse.js";
|
|
5
7
|
import { registerAccessibilityTool } from "./tools/accessibility.js";
|
|
6
8
|
import { registerStaticAnalysisTool } from "./tools/staticAnalysis.js";
|
|
7
9
|
import { registerQaGateTool } from "./tools/qaGate.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Identity comes from package.json rather than being written out here, so the
|
|
12
|
+
* two cannot drift — they already had, declaring qa-mcp/0.1.0 against
|
|
13
|
+
* nfunc-mcp/0.2.0 after the npm rename, and `npm version` would reintroduce
|
|
14
|
+
* the gap on every release if these were hardcoded.
|
|
15
|
+
*
|
|
16
|
+
* "../package.json" resolves to the repo root from both src/index.ts and
|
|
17
|
+
* dist/index.js, and npm always ships package.json, so the same path works in
|
|
18
|
+
* dev, in a local build, and in an installed package. Falls back rather than
|
|
19
|
+
* failing to boot if it is ever unreadable.
|
|
20
|
+
*/
|
|
21
|
+
function readIdentity() {
|
|
22
|
+
try {
|
|
23
|
+
const pkg = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
|
|
24
|
+
return { name: pkg.name ?? "nfunc-mcp", version: pkg.version ?? "0.0.0" };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { name: "nfunc-mcp", version: "0.0.0" };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const server = new McpServer(readIdentity());
|
|
12
31
|
server.registerTool("ping", {
|
|
13
32
|
description: "Health-check tool. Returns ok status and current ISO timestamp.",
|
|
14
33
|
inputSchema: {},
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,SAAS,YAAY;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CACpB,YAAY,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAC1C,CAAC;QACzC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,WAAW,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IACjD,CAAC;AACH,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;AAE7C,MAAM,CAAC,YAAY,CACjB,MAAM,EACN;IACE,WAAW,EAAE,iEAAiE;IAC9E,WAAW,EAAE,EAAE;CAChB,EACD,KAAK,IAAI,EAAE;IACT,MAAM,OAAO,GAAG;QACd,MAAM,EAAE,IAAI;QACZ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IACF,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;KAC3D,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,sBAAsB,CAAC,MAAM,CAAC,CAAC;AAC/B,yBAAyB,CAAC,MAAM,CAAC,CAAC;AAClC,0BAA0B,CAAC,MAAM,CAAC,CAAC;AACnC,kBAAkB,CAAC,MAAM,CAAC,CAAC;AAE3B,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Finding } from "../types.js";
|
|
2
|
+
export interface A11yDedupeResult {
|
|
3
|
+
findings: Finding[];
|
|
4
|
+
rawCount: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Collapses the two ways pa11y over-reports a single defect.
|
|
8
|
+
*
|
|
9
|
+
* Pass 1 — exact repeats. pa11y emits one violation per occurrence, and for
|
|
10
|
+
* page-wide rules (F77, duplicate id, above all) every occurrence resolves
|
|
11
|
+
* back to the same selector. A real commerce page produced 70 of 85 findings
|
|
12
|
+
* from two defects repeated 35 times each, burying the genuine issues and
|
|
13
|
+
* flooring the qa_gate composite score. Collapsing on (rule_code, selector)
|
|
14
|
+
* removes those while keeping two different elements failing the same rule
|
|
15
|
+
* separate, since they are separate fixes.
|
|
16
|
+
*
|
|
17
|
+
* Pass 2 — systemic defects. One rule failing across many *different*
|
|
18
|
+
* elements, which (rule_code, selector) cannot catch. The axe runner produces
|
|
19
|
+
* these routinely: one mis-authored component reused across a page gave 41
|
|
20
|
+
* distinct aria-allowed-attr findings. See SYSTEMIC_THRESHOLD.
|
|
21
|
+
*
|
|
22
|
+
* Counts survive as evidence.occurrences (written only when > 1), so the
|
|
23
|
+
* volume signal is preserved without N copies of the finding.
|
|
24
|
+
*/
|
|
25
|
+
export declare function dedupeA11yFindings(findings: Finding[]): A11yDedupeResult;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const PRIORITY_ORDER = { P1: 0, P2: 1, P3: 2 };
|
|
2
|
+
/**
|
|
3
|
+
* Above this many distinct elements failing one rule, the individual findings
|
|
4
|
+
* stop being separately actionable and start being one systemic defect — a
|
|
5
|
+
* component used everywhere, not N unrelated bugs. At or below it, each
|
|
6
|
+
* element keeps its own line.
|
|
7
|
+
*
|
|
8
|
+
* Set at 10 rather than lower because distinct elements failing the same rule
|
|
9
|
+
* are often still separate fixes: ten duplicate ids on a page are ten ids to
|
|
10
|
+
* rename, and collapsing them hides the list a developer needs. It takes a
|
|
11
|
+
* genuine flood — axe reported 41 aria-allowed-attr failures from one reused
|
|
12
|
+
* component — before the group is more useful than its members.
|
|
13
|
+
*/
|
|
14
|
+
const SYSTEMIC_THRESHOLD = 10;
|
|
15
|
+
/**
|
|
16
|
+
* Collapses the two ways pa11y over-reports a single defect.
|
|
17
|
+
*
|
|
18
|
+
* Pass 1 — exact repeats. pa11y emits one violation per occurrence, and for
|
|
19
|
+
* page-wide rules (F77, duplicate id, above all) every occurrence resolves
|
|
20
|
+
* back to the same selector. A real commerce page produced 70 of 85 findings
|
|
21
|
+
* from two defects repeated 35 times each, burying the genuine issues and
|
|
22
|
+
* flooring the qa_gate composite score. Collapsing on (rule_code, selector)
|
|
23
|
+
* removes those while keeping two different elements failing the same rule
|
|
24
|
+
* separate, since they are separate fixes.
|
|
25
|
+
*
|
|
26
|
+
* Pass 2 — systemic defects. One rule failing across many *different*
|
|
27
|
+
* elements, which (rule_code, selector) cannot catch. The axe runner produces
|
|
28
|
+
* these routinely: one mis-authored component reused across a page gave 41
|
|
29
|
+
* distinct aria-allowed-attr findings. See SYSTEMIC_THRESHOLD.
|
|
30
|
+
*
|
|
31
|
+
* Counts survive as evidence.occurrences (written only when > 1), so the
|
|
32
|
+
* volume signal is preserved without N copies of the finding.
|
|
33
|
+
*/
|
|
34
|
+
export function dedupeA11yFindings(findings) {
|
|
35
|
+
// Pass 1 — exact repeats: same rule on the same selector.
|
|
36
|
+
const byKey = new Map();
|
|
37
|
+
for (const f of findings) {
|
|
38
|
+
const key = `${String(f.evidence["rule_code"] ?? "")}|` +
|
|
39
|
+
`${String(f.evidence["selector"] ?? "")}`;
|
|
40
|
+
const existing = byKey.get(key);
|
|
41
|
+
if (!existing) {
|
|
42
|
+
byKey.set(key, { finding: f, count: 1 });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
existing.count += 1;
|
|
46
|
+
// pa11y can grade occurrences of one rule differently (error vs warning);
|
|
47
|
+
// keep the most severe so dedup never downgrades a defect.
|
|
48
|
+
if (PRIORITY_ORDER[f.priority] < PRIORITY_ORDER[existing.finding.priority]) {
|
|
49
|
+
existing.finding = f;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const exact = Array.from(byKey.values()).map(({ finding, count }) => count > 1
|
|
53
|
+
? { ...finding, evidence: { ...finding.evidence, occurrences: count } }
|
|
54
|
+
: finding);
|
|
55
|
+
// Pass 2 — systemic defects: one rule failing across many *different*
|
|
56
|
+
// elements. htmlcs rarely does this (its repeats share a selector, which
|
|
57
|
+
// pass 1 already caught) but axe routinely does: a single mis-authored
|
|
58
|
+
// component reused across a page produced 41 separate aria-allowed-attr
|
|
59
|
+
// findings, which drowned everything else and inflated the P1 count.
|
|
60
|
+
const byRule = new Map();
|
|
61
|
+
for (const f of exact) {
|
|
62
|
+
const rule = String(f.evidence["rule_code"] ?? "");
|
|
63
|
+
const group = byRule.get(rule);
|
|
64
|
+
if (group)
|
|
65
|
+
group.push(f);
|
|
66
|
+
else
|
|
67
|
+
byRule.set(rule, [f]);
|
|
68
|
+
}
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const [, group] of byRule) {
|
|
71
|
+
if (group.length <= SYSTEMIC_THRESHOLD) {
|
|
72
|
+
out.push(...group);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const worst = group.reduce((a, b) => PRIORITY_ORDER[a.priority] <= PRIORITY_ORDER[b.priority] ? a : b);
|
|
76
|
+
const elements = group.reduce((n, f) => n + Number(f.evidence["occurrences"] ?? 1), 0);
|
|
77
|
+
out.push({
|
|
78
|
+
...worst,
|
|
79
|
+
description: `${worst.description} This rule fails on ${elements} elements across the page, ` +
|
|
80
|
+
`which usually means one shared component rather than ${elements} separate defects — ` +
|
|
81
|
+
`fix the component and all of them clear.`,
|
|
82
|
+
evidence: {
|
|
83
|
+
...worst.evidence,
|
|
84
|
+
occurrences: elements,
|
|
85
|
+
distinct_elements: group.length,
|
|
86
|
+
systemic: true,
|
|
87
|
+
sample_selectors: group
|
|
88
|
+
.slice(0, 10)
|
|
89
|
+
.map((f) => String(f.evidence["selector"] ?? "")),
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return { findings: out, rawCount: findings.length };
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=a11yDedupe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a11yDedupe.js","sourceRoot":"","sources":["../../src/mappers/a11yDedupe.ts"],"names":[],"mappings":"AAEA,MAAM,cAAc,GAA6B,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;AAOzE;;;;;;;;;;;GAWG;AACH,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAE9B;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAmB;IACpD,0DAA0D;IAC1D,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+C,CAAC;IAErE,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,GAAG,GACP,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,GAAG;YAC3C,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QAE5C,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;YACzC,SAAS;QACX,CAAC;QAED,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC;QACpB,0EAA0E;QAC1E,2DAA2D;QAC3D,IAAI,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,cAAc,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3E,QAAQ,CAAC,OAAO,GAAG,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAClE,KAAK,GAAG,CAAC;QACP,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE;QACvE,CAAC,CAAC,OAAO,CACZ,CAAC;IAEF,sEAAsE;IACtE,yEAAyE;IACzE,uEAAuE;IACvE,wEAAwE;IACxE,qEAAqE;IACrE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC5C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,KAAK;YAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;YACpB,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;QAC/B,IAAI,KAAK,CAAC,MAAM,IAAI,kBAAkB,EAAE,CAAC;YACvC,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;YACnB,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAClC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACjE,CAAC;QACF,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAC3B,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,EACpD,CAAC,CACF,CAAC;QACF,GAAG,CAAC,IAAI,CAAC;YACP,GAAG,KAAK;YACR,WAAW,EACT,GAAG,KAAK,CAAC,WAAW,uBAAuB,QAAQ,6BAA6B;gBAChF,wDAAwD,QAAQ,sBAAsB;gBACtF,0CAA0C;YAC5C,QAAQ,EAAE;gBACR,GAAG,KAAK,CAAC,QAAQ;gBACjB,WAAW,EAAE,QAAQ;gBACrB,iBAAiB,EAAE,KAAK,CAAC,MAAM;gBAC/B,QAAQ,EAAE,IAAI;gBACd,gBAAgB,EAAE,KAAK;qBACpB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;qBACZ,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;aACpD;SACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Finding } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Lighthouse's own category scores ARE a calibrated 0–100 health signal, with
|
|
4
|
+
* per-audit weighting Google already tuned. Re-deriving one by counting
|
|
5
|
+
* findings throws that away and double-counts besides: total-blocking-time,
|
|
6
|
+
* bootup-time, mainthread-work-breakdown, interactive and max-potential-fid are
|
|
7
|
+
* five findings describing one overloaded main thread, and the old formula
|
|
8
|
+
* charged 75 points for it. So use the category scores directly.
|
|
9
|
+
*/
|
|
10
|
+
export declare function lighthouseSubScore(categoryScores: Record<string, number>): number | null;
|
|
11
|
+
export declare function a11ySubScore(findings: Finding[]): number;
|
|
12
|
+
export declare function staticSubScore(findings: Finding[]): number;
|
|
13
|
+
export interface SubScores {
|
|
14
|
+
lighthouse: number | null;
|
|
15
|
+
pa11y: number | null;
|
|
16
|
+
static: number | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Weighted mean over the tools that produced a score. Weights are renormalised
|
|
20
|
+
* across whatever ran, so a url-only run and a url+path run are on the same
|
|
21
|
+
* scale. Returns null when nothing scored.
|
|
22
|
+
*/
|
|
23
|
+
export declare function compositeScore(sub: SubScores): number | null;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composite health score.
|
|
3
|
+
*
|
|
4
|
+
* The previous formula was a single global subtraction — 100 minus 15 per P1,
|
|
5
|
+
* 7 per P2, 3 per P3 — which had two structural faults:
|
|
6
|
+
*
|
|
7
|
+
* 1. No resolution. Seven P1 findings floored it at 0, so a mediocre page and
|
|
8
|
+
* a catastrophic one were indistinguishable. Real pages routinely exceed
|
|
9
|
+
* that within a single tool.
|
|
10
|
+
* 2. Running more tools lowered the score, because every tool fed the same
|
|
11
|
+
* running subtraction. A url-only run was not comparable to a url+path run,
|
|
12
|
+
* which is exactly backwards: broader checking should not look like worse
|
|
13
|
+
* health.
|
|
14
|
+
*
|
|
15
|
+
* Both are fixed by scoring each tool to its own 0–100 sub-score and taking a
|
|
16
|
+
* weighted mean over only the tools that actually ran.
|
|
17
|
+
*/
|
|
18
|
+
const SEVERITY_DAMAGE = { P1: 10, P2: 3, P3: 1 };
|
|
19
|
+
/**
|
|
20
|
+
* Saturating decay. Never reaches 0, always preserves ordering, and each
|
|
21
|
+
* additional finding costs less than the one before — which matches how the
|
|
22
|
+
* information actually behaves: the twentieth P1 tells you far less about the
|
|
23
|
+
* page than the first did.
|
|
24
|
+
*
|
|
25
|
+
* K sets where the curve has resolution. At K = 90 a single P1 scores 89, five
|
|
26
|
+
* P1s score 57, and fifteen P1s score 19 — spread across the range real pages
|
|
27
|
+
* occupy instead of collapsing onto the floor.
|
|
28
|
+
*/
|
|
29
|
+
function decay(damage, k) {
|
|
30
|
+
return Math.round(100 * Math.exp(-damage / k));
|
|
31
|
+
}
|
|
32
|
+
function damageOf(findings) {
|
|
33
|
+
return findings.reduce((sum, f) => sum + SEVERITY_DAMAGE[f.priority], 0);
|
|
34
|
+
}
|
|
35
|
+
// Tuned so a page's sub-score lands where a human triager would put it. Static
|
|
36
|
+
// analysis decays faster because code defects are fewer and more directly
|
|
37
|
+
// actionable than page-level accessibility violations.
|
|
38
|
+
const A11Y_K = 90;
|
|
39
|
+
const STATIC_K = 60;
|
|
40
|
+
/**
|
|
41
|
+
* Relative weights of the Lighthouse categories inside the Lighthouse
|
|
42
|
+
* sub-score. Lighthouse publishes per-audit weights but no cross-category
|
|
43
|
+
* weighting, so this is our judgement: performance and accessibility carry the
|
|
44
|
+
* most user impact, SEO and best-practices matter but less directly.
|
|
45
|
+
*
|
|
46
|
+
* Categories not listed here — currently the experimental "agentic-browsing" —
|
|
47
|
+
* get UNKNOWN_CATEGORY_WEIGHT so a new or volatile category cannot dominate the
|
|
48
|
+
* score, while still being visible in the breakdown.
|
|
49
|
+
*/
|
|
50
|
+
const CATEGORY_WEIGHTS = {
|
|
51
|
+
performance: 0.3,
|
|
52
|
+
accessibility: 0.3,
|
|
53
|
+
"best-practices": 0.2,
|
|
54
|
+
seo: 0.2,
|
|
55
|
+
};
|
|
56
|
+
const UNKNOWN_CATEGORY_WEIGHT = 0.05;
|
|
57
|
+
/**
|
|
58
|
+
* Lighthouse's own category scores ARE a calibrated 0–100 health signal, with
|
|
59
|
+
* per-audit weighting Google already tuned. Re-deriving one by counting
|
|
60
|
+
* findings throws that away and double-counts besides: total-blocking-time,
|
|
61
|
+
* bootup-time, mainthread-work-breakdown, interactive and max-potential-fid are
|
|
62
|
+
* five findings describing one overloaded main thread, and the old formula
|
|
63
|
+
* charged 75 points for it. So use the category scores directly.
|
|
64
|
+
*/
|
|
65
|
+
export function lighthouseSubScore(categoryScores) {
|
|
66
|
+
const entries = Object.entries(categoryScores);
|
|
67
|
+
if (entries.length === 0)
|
|
68
|
+
return null;
|
|
69
|
+
let weighted = 0;
|
|
70
|
+
let totalWeight = 0;
|
|
71
|
+
for (const [name, score] of entries) {
|
|
72
|
+
const w = CATEGORY_WEIGHTS[name] ?? UNKNOWN_CATEGORY_WEIGHT;
|
|
73
|
+
weighted += w * score;
|
|
74
|
+
totalWeight += w;
|
|
75
|
+
}
|
|
76
|
+
return totalWeight > 0 ? Math.round(weighted / totalWeight) : null;
|
|
77
|
+
}
|
|
78
|
+
export function a11ySubScore(findings) {
|
|
79
|
+
return decay(damageOf(findings), A11Y_K);
|
|
80
|
+
}
|
|
81
|
+
export function staticSubScore(findings) {
|
|
82
|
+
return decay(damageOf(findings), STATIC_K);
|
|
83
|
+
}
|
|
84
|
+
// How much each tool contributes to the composite. Static analysis is weighted
|
|
85
|
+
// lowest because a clean scan of a small directory returns 100 and would
|
|
86
|
+
// otherwise flatter a run whose real problems are all in the browser.
|
|
87
|
+
const TOOL_WEIGHTS = { lighthouse: 0.4, pa11y: 0.4, static: 0.2 };
|
|
88
|
+
/**
|
|
89
|
+
* Weighted mean over the tools that produced a score. Weights are renormalised
|
|
90
|
+
* across whatever ran, so a url-only run and a url+path run are on the same
|
|
91
|
+
* scale. Returns null when nothing scored.
|
|
92
|
+
*/
|
|
93
|
+
export function compositeScore(sub) {
|
|
94
|
+
const parts = [];
|
|
95
|
+
if (sub.lighthouse !== null)
|
|
96
|
+
parts.push([sub.lighthouse, TOOL_WEIGHTS.lighthouse]);
|
|
97
|
+
if (sub.pa11y !== null)
|
|
98
|
+
parts.push([sub.pa11y, TOOL_WEIGHTS.pa11y]);
|
|
99
|
+
if (sub.static !== null)
|
|
100
|
+
parts.push([sub.static, TOOL_WEIGHTS.static]);
|
|
101
|
+
if (parts.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
const totalWeight = parts.reduce((s, [, w]) => s + w, 0);
|
|
104
|
+
const weighted = parts.reduce((s, [v, w]) => s + v * w, 0);
|
|
105
|
+
return Math.round(weighted / totalWeight);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=compositeScore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compositeScore.js","sourceRoot":"","sources":["../../src/mappers/compositeScore.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,17 +9,49 @@ function promotePriority(p) {
|
|
|
9
9
|
// Maps Lighthouse accessibility audit IDs to substrings that appear inside the
|
|
10
10
|
// pa11y rule_code for the same class of issue. A leading "." prevents matching
|
|
11
11
|
// partial segment names (e.g. ".H44" won't accidentally match ".1H44").
|
|
12
|
+
//
|
|
13
|
+
// Keep this keyed on defects the two tools genuinely detect in common. A wrong
|
|
14
|
+
// entry is worse than a missing one: a bogus match promotes a finding a whole
|
|
15
|
+
// priority tier and stamps it "confirmed by two tools". Where Lighthouse and
|
|
16
|
+
// pa11y test adjacent-but-different criteria (e.g. label-content-name-mismatch
|
|
17
|
+
// under WCAG 2.5.3 vs F68 under 1.3.1) they are deliberately left unmapped.
|
|
12
18
|
const LH_TO_PA11Y_SUBSTRINGS = {
|
|
19
|
+
// Colour / contrast
|
|
13
20
|
"color-contrast": [".G18", ".G145", ".G174"],
|
|
21
|
+
// Images
|
|
14
22
|
"image-alt": [".H37", ".H67", ".F65"],
|
|
23
|
+
"input-image-alt": [".H36"],
|
|
24
|
+
"object-alt": [".H53"],
|
|
25
|
+
// Accessible names on controls
|
|
15
26
|
label: [".H44", ".F68", ".H91.Input", ".H91.Select", ".H91.Textarea"],
|
|
27
|
+
"form-field-multiple-labels": [".H44", ".F68"],
|
|
28
|
+
"button-name": [".H91.Button"],
|
|
29
|
+
"select-name": [".H91.Select"],
|
|
30
|
+
"aria-input-field-name": [".ARIA6", ".ARIA9", ".H91"],
|
|
31
|
+
"aria-toggle-field-name": [".ARIA6", ".ARIA9", ".H91"],
|
|
32
|
+
"aria-command-name": [".ARIA6", ".ARIA9", ".H91"],
|
|
33
|
+
"aria-dialog-name": [".ARIA6", ".ARIA9"],
|
|
34
|
+
// ARIA misuse
|
|
35
|
+
"aria-prohibited-attr": [".ARIA6", ".ARIA4"],
|
|
36
|
+
"aria-valid-attr-value": [".ARIA9"],
|
|
37
|
+
// Links
|
|
16
38
|
"link-name": [".H30", ".H91.A."],
|
|
39
|
+
"link-text": [".H30", ".H91.A."],
|
|
40
|
+
"crawlable-anchors": [".G1,G123,G124", ".H30"],
|
|
41
|
+
// Document / language
|
|
17
42
|
"document-title": [".H25", ".F89"],
|
|
18
43
|
"html-has-lang": [".H57"],
|
|
44
|
+
"html-lang-valid": [".H57"],
|
|
45
|
+
"valid-lang": [".H58"],
|
|
46
|
+
// Structure
|
|
47
|
+
"heading-order": [".G141", ".H42"],
|
|
19
48
|
"frame-title": [".H64"],
|
|
20
|
-
|
|
21
|
-
|
|
49
|
+
// Duplicate identifiers. Lighthouse 12 removed duplicate-id-aria altogether,
|
|
50
|
+
// which is why a page throwing 35 pa11y F77 violations correlated with
|
|
51
|
+
// nothing under Lighthouse 13. Both ids are kept so older Lighthouse output
|
|
52
|
+
// still matches; there is currently no LH 13 equivalent to pair F77 with.
|
|
22
53
|
"duplicate-id-aria": [".H93", ".F77"],
|
|
54
|
+
"duplicate-id-active": [".H93", ".F77"],
|
|
23
55
|
};
|
|
24
56
|
// Lighthouse performance audit IDs where Rule 2 (code linkage) is relevant.
|
|
25
57
|
const PERFORMANCE_AUDIT_IDS = new Set([
|
|
@@ -30,14 +62,27 @@ const PERFORMANCE_AUDIT_IDS = new Set([
|
|
|
30
62
|
"mainthread-work-breakdown",
|
|
31
63
|
"total-blocking-time",
|
|
32
64
|
]);
|
|
65
|
+
// Deterministic non-crypto string hash (djb2), base36-encoded.
|
|
66
|
+
// Selectors are long and routinely share long prefixes — e.g.
|
|
67
|
+
// "#accordion-panel-:rn: > div > div > input:nth-child(1)" and the same
|
|
68
|
+
// selector ending ":nth-child(3)" are identical for their first 40 characters.
|
|
69
|
+
// The previous id truncated the selector to 16 chars, so those two distinct
|
|
70
|
+
// findings collapsed to one id and consumedA11yIds claimed the wrong entry
|
|
71
|
+
// during correlation. Hashing the whole selector keeps ids short and unique.
|
|
72
|
+
function shortHash(input) {
|
|
73
|
+
let h = 5381;
|
|
74
|
+
for (let i = 0; i < input.length; i++) {
|
|
75
|
+
h = ((h << 5) + h + input.charCodeAt(i)) | 0;
|
|
76
|
+
}
|
|
77
|
+
return (h >>> 0).toString(36);
|
|
78
|
+
}
|
|
33
79
|
function makeId(tool, f) {
|
|
34
80
|
const e = f.evidence;
|
|
35
81
|
if (tool === "lighthouse")
|
|
36
82
|
return `lh:${String(e["audit_id"] ?? f.title)}`;
|
|
37
83
|
if (tool === "a11y") {
|
|
38
84
|
const code = String(e["rule_code"] ?? "").split(".").slice(-2).join(".");
|
|
39
|
-
|
|
40
|
-
return `a11y:${code}:${sel}`;
|
|
85
|
+
return `a11y:${code}:${shortHash(String(e["selector"] ?? ""))}`;
|
|
41
86
|
}
|
|
42
87
|
const basename = String(e["file"] ?? "").split("/").pop() ?? "";
|
|
43
88
|
return `static:${String(e["source"] ?? "")}:${basename}:${String(e["line"] ?? "")}`;
|
|
@@ -58,37 +103,53 @@ function applyRule1(lhFindings, a11yFindings) {
|
|
|
58
103
|
const substrings = LH_TO_PA11Y_SUBSTRINGS[auditId];
|
|
59
104
|
if (!substrings)
|
|
60
105
|
continue;
|
|
61
|
-
|
|
106
|
+
// Claim every unconsumed pa11y finding covering the same technique, not
|
|
107
|
+
// just the first. One Lighthouse audit routinely corresponds to several
|
|
108
|
+
// pa11y violations — one per offending element — and stopping at the first
|
|
109
|
+
// left the rest to reappear as uncorroborated copies of the same defect.
|
|
110
|
+
const matches = a11yFindings.filter((a11yF) => {
|
|
62
111
|
if (consumedA11yIds.has(a11yF.id))
|
|
63
|
-
|
|
112
|
+
return false;
|
|
64
113
|
const ruleCode = String(a11yF.evidence["rule_code"] ?? "");
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
114
|
+
return substrings.some((s) => ruleCode.includes(s));
|
|
115
|
+
});
|
|
116
|
+
if (matches.length === 0)
|
|
117
|
+
continue;
|
|
118
|
+
// Use the most severe of the matched pa11y findings, then take the better
|
|
119
|
+
// (lower index = higher priority) of that and the Lighthouse finding as the
|
|
120
|
+
// base, and promote it one tier.
|
|
121
|
+
const worstA11y = matches.reduce((a, b) => PRIORITY_ORDER[a.priority] <= PRIORITY_ORDER[b.priority] ? a : b);
|
|
122
|
+
const basePriority = PRIORITY_ORDER[lhF.priority] <= PRIORITY_ORDER[worstA11y.priority]
|
|
123
|
+
? lhF.priority
|
|
124
|
+
: worstA11y.priority;
|
|
125
|
+
// Findings arrive here already deduplicated, so fold the collapsed
|
|
126
|
+
// occurrence counts back in to report how many elements are affected.
|
|
127
|
+
const elementCount = matches.reduce((n, m) => n + Number(m.evidence["occurrences"] ?? 1), 0);
|
|
128
|
+
correlated.push({
|
|
129
|
+
id: `corr:lh+a11y:${auditId}`,
|
|
130
|
+
source_tool: "lighthouse+pa11y",
|
|
131
|
+
priority: promotePriority(basePriority),
|
|
132
|
+
title: lhF.title,
|
|
133
|
+
description: `[Lighthouse] ${lhF.description} ` +
|
|
134
|
+
`[pa11y] ${worstA11y.description} ` +
|
|
135
|
+
`Two independent tools flagged the same accessibility gap across ` +
|
|
136
|
+
`${elementCount} element${elementCount !== 1 ? "s" : ""} — ` +
|
|
137
|
+
`this cross-tool confirmation increases confidence that the issue is real and affects real users.`,
|
|
138
|
+
evidence: {
|
|
139
|
+
...lhF.evidence,
|
|
140
|
+
pa11y_rule_codes: [
|
|
141
|
+
...new Set(matches.map((m) => String(m.evidence["rule_code"] ?? ""))),
|
|
142
|
+
],
|
|
143
|
+
pa11y_selectors: matches
|
|
144
|
+
.slice(0, 5)
|
|
145
|
+
.map((m) => String(m.evidence["selector"] ?? "")),
|
|
146
|
+
pa11y_elements_affected: elementCount,
|
|
147
|
+
},
|
|
148
|
+
confirmed_by: ["lighthouse", "pa11y"],
|
|
149
|
+
});
|
|
150
|
+
consumedLhIds.add(lhF.id);
|
|
151
|
+
for (const m of matches)
|
|
152
|
+
consumedA11yIds.add(m.id);
|
|
92
153
|
}
|
|
93
154
|
return { correlated, consumedLhIds, consumedA11yIds };
|
|
94
155
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"correlator.js","sourceRoot":"","sources":["../../src/mappers/correlator.ts"],"names":[],"mappings":"AA+BA,MAAM,cAAc,GAA6B,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;AAEzE,SAAS,eAAe,CAAC,CAAW;IAClC,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5B,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5B,OAAO,IAAI,CAAC;AACd,CAAC;AAED,+EAA+E;AAC/E,gFAAgF;AAChF,wEAAwE;AACxE,MAAM,sBAAsB,GAA6B;IACvD,gBAAgB,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"correlator.js","sourceRoot":"","sources":["../../src/mappers/correlator.ts"],"names":[],"mappings":"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"}
|