eaa-kit 0.3.0 → 0.5.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.
Files changed (66) hide show
  1. package/README.md +82 -14
  2. package/dist/astro/index.d.ts +1 -1
  3. package/dist/astro/index.js +1 -1
  4. package/dist/audit/runners/worker.js +5 -1
  5. package/dist/{audit-DcL73mOC.js → audit-B282A3EA.js} +207 -41
  6. package/dist/audit-DyHPmGeD.js +2 -0
  7. package/dist/{baseline-CuKFq4IF.js → baseline-0u4df12v.js} +1 -1
  8. package/dist/{baseline-s9F3fXTN.js → baseline-CuAS2lMk.js} +6 -5
  9. package/dist/cli/index.js +306 -18
  10. package/dist/{collect-BkAQ0viT.js → collect-Cu1q9NTB.js} +53 -11
  11. package/dist/command-C3D7JWn6.js +160 -0
  12. package/dist/{component-7kEBjv_y.js → component-C3GL1Mnu.js} +41 -6
  13. package/dist/component-DKd3EHOg.js +2 -0
  14. package/dist/coverage-B_Y6l-Ra.js +522 -0
  15. package/dist/{crawl-Oxt2Gaqo.js → crawl-BUWPgaGW.js} +83 -9
  16. package/dist/eleventy/index.d.ts +28 -0
  17. package/dist/eleventy/index.js +19 -0
  18. package/dist/fingerprint-BjYV_0F7.js +67 -0
  19. package/dist/{frameworks-BYa3tULg.js → frameworks-B4ClIJgE.js} +71 -0
  20. package/dist/{frameworks-DaDqrJOw.js → frameworks-etFg_O8K.js} +1 -1
  21. package/dist/{html-DKiI_3gs.js → html-C4vktg0A.js} +130 -5
  22. package/dist/{impact-YdoOtFqm.js → impact-DZt2oBCP.js} +1 -1
  23. package/dist/index.d.ts +119 -4
  24. package/dist/index.js +2 -2
  25. package/dist/{init-DiLiYvN1.js → init-CW7LfGT5.js} +10 -4
  26. package/dist/jsdom-22Bkt65v.js +3 -0
  27. package/dist/jsdom-B--cEH-G.js +89 -0
  28. package/dist/{json-Cnv9nd6U.js → json-DROX33kh.js} +9 -4
  29. package/dist/{json-B0Y7rNjt.js → json-D_Mnnft5.js} +1 -1
  30. package/dist/load-CFq2VQtT.js +2 -0
  31. package/dist/{load-UYXLqGV9.js → load-yAR4wzez.js} +133 -8
  32. package/dist/nuxt/index.d.ts +35 -0
  33. package/dist/nuxt/index.js +23 -0
  34. package/dist/{playwright-DYFsGUNd.js → playwright-BojtYVUa.js} +30 -6
  35. package/dist/{pool-BWkWZiJW.js → pool-BO25OIez.js} +52 -2
  36. package/dist/{project-DW08TseF.js → project-CzOnkLH6.js} +8 -2
  37. package/dist/project-MFrXcw1M.js +2 -0
  38. package/dist/remediation-CMBIrnpN.js +321 -0
  39. package/dist/{render-DI_aCnAZ.js → render-DbGOVmhx.js} +48 -10
  40. package/dist/{result-DLxd2Eip.js → result-BWcYXeRs.js} +114 -3
  41. package/dist/routes-CmdRUuOs.js +265 -0
  42. package/dist/{run-BW6CVuND.js → run-C2nKFcb-.js} +19 -2
  43. package/dist/{run-BMASMmwO.d.ts → run-CtcEUhbe.d.ts} +7 -0
  44. package/dist/{sarif-DB3WG7T9.js → sarif-B-UBcVu8.js} +16 -8
  45. package/dist/{schema-CMZ8ItGk.js → schema-DJSF4K05.js} +18 -1
  46. package/dist/statement/templates/es.en.md +125 -0
  47. package/dist/statement/templates/es.es.md +127 -0
  48. package/dist/statement/templates/fr.en.md +128 -0
  49. package/dist/statement/templates/fr.fr.md +131 -0
  50. package/dist/statement/templates/it.en.md +127 -0
  51. package/dist/statement/templates/it.it.md +130 -0
  52. package/dist/statement/templates/nl.en.md +125 -0
  53. package/dist/statement/templates/nl.nl.md +127 -0
  54. package/dist/{text-BFmNtMsV.js → text-CKKpzkYM.js} +1 -1
  55. package/dist/vite/index.d.ts +1 -1
  56. package/dist/vite/index.js +1 -1
  57. package/dist/webpack/index.d.ts +33 -0
  58. package/dist/webpack/index.js +20 -0
  59. package/package.json +35 -9
  60. package/dist/audit-CuG2hYyo.js +0 -2
  61. package/dist/command-Dxpa00Ha.js +0 -77
  62. package/dist/fingerprint-DRoneAjj.js +0 -20
  63. package/dist/jsdom-BjpF-2V-.js +0 -158
  64. package/dist/jsdom-X4KYfTp8.js +0 -3
  65. package/dist/manual-Vz-oX1I_.js +0 -239
  66. package/dist/routes-C2Cgf6Ko.js +0 -119
package/README.md CHANGED
@@ -3,14 +3,17 @@
3
3
  [![CI](https://github.com/likeBloodMoon/eaa-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/likeBloodMoon/eaa-kit/actions/workflows/ci.yml)
4
4
 
5
5
  Build-time WCAG 2.2 AA auditor and EU accessibility statement generator for static sites,
6
- aimed at freelancers and small agencies in the DACH region who have to comply with the
7
- European Accessibility Act (in force since 28 June 2025), the BFSG in Germany and the BaFG
8
- in Austria.
6
+ built for the freelancers and small agencies who have to comply with the European
7
+ Accessibility Act (in force since 28 June 2025) without an accessibility budget. It started
8
+ in the DACH region — the BFSG in Germany, the BaFG in Austria — and the statement now names
9
+ the statute and supervisory body of **seven countries**: Austria, Germany, Switzerland,
10
+ Spain, France, Italy and the Netherlands, each in its own language as well as English.
9
11
 
10
12
  ```bash
11
13
  npx eaa-kit audit # WCAG 2.2 AA report; finds your build itself
14
+ npx eaa-kit diff a.json b.json # what a change made worse, and what it fixed
12
15
  npx eaa-kit init # write an eaa.config.json
13
- npx eaa-kit statement # Barrierefreiheitserklärung from that config
16
+ npx eaa-kit statement # accessibility statement, in one of seven countries
14
17
  ```
15
18
 
16
19
  > **Not legal advice.** eaa-kit reports what an automated engine can and cannot determine
@@ -32,7 +35,9 @@ supported here either.
32
35
  **Audits your build.** Globs the HTML out of `./dist`, parses it with jsdom and runs
33
36
  axe-core against it. No Chromium download, fast enough for CI, and it never fetches
34
37
  anything or executes your site's JavaScript. `--browser` swaps in real Chromium for the
35
- rules that need layout and CSS.
38
+ rules that need layout and CSS; `--fast` goes the other way and skips the rules the
39
+ browserless engine cannot decide anyway, rather than running them and discarding the
40
+ answer.
36
41
 
37
42
  ```bash
38
43
  eaa-kit audit ./dist --fail-on serious
@@ -45,14 +50,21 @@ export, Nuxt, SvelteKit, anything behind a CMS — are audited running instead:
45
50
  eaa-kit audit --url http://localhost:3000
46
51
  ```
47
52
 
48
- **Writes the statement.** A Barrierefreiheitserklärung from one config file, in German or
49
- English, as Markdown or HTML, naming the statute and supervisory body of Austria,
50
- Switzerland or Germany and optionally listing the barriers a real audit found.
53
+ **Writes the statement.** A Barrierefreiheitserklärung, a déclaration d'accessibilité, a
54
+ dichiarazione di accessibilità whatever the country calls it from one config file, as
55
+ Markdown or HTML, naming that country's statute and supervisory body, and optionally
56
+ listing the barriers a real audit found.
51
57
 
52
58
  ```bash
53
59
  eaa-kit statement --output src/content/a11y.md
60
+ eaa-kit statement --country FR --lang fr # or ES, IT, NL, AT, DE, CH
54
61
  ```
55
62
 
63
+ Each country's statement is a document under its own law rather than a translation of
64
+ another's, so where a national regime asks for more than this — France's RGAA declaration,
65
+ Italy's filing with AgID — the text says so instead of letting a generated file look like
66
+ it settles the matter.
67
+
56
68
  **Fails only on what is new.** The first run on a site that already exists finds
57
69
  everything at once. A baseline records what is already wrong so the build fails on
58
70
  regressions instead, without ever letting an accepted barrier look like a passing one.
@@ -62,23 +74,77 @@ eaa-kit baseline ./dist
62
74
  eaa-kit audit ./dist --baseline eaa-baseline.json
63
75
  ```
64
76
 
77
+ **Says what a change did.** Two reports, and the difference between them: what is new, what
78
+ was fixed, and what the later run never looked at — which is kept apart, because a
79
+ violation missing from a run that stopped early was not fixed by anybody.
80
+
81
+ ```bash
82
+ eaa-kit diff before.json after.json
83
+ ```
84
+
85
+ **Says what to do about it.** Not "images must have alternative text" and a link — who the
86
+ barrier stops, what to change, and the corrected form of *your* markup, in your framework's
87
+ idiom where that differs. Deterministic and offline: no model, no API key, nothing that
88
+ could invent a fix that looks right and is not.
89
+
90
+ ```
91
+ ✗ image-alt critical, WCAG 1.1.1
92
+ A screen reader announces this image by its filename, or skips it entirely.
93
+ Fix: Add alt text describing what the image conveys…
94
+ → <img src="/assets/logo.svg" alt="What this image shows">
95
+ written in src/components/Header.astro:12
96
+ ```
97
+
98
+ **Says how much of WCAG it could reach.** WCAG 2.2 has 55 success criteria at Levels A and
99
+ AA. axe-core has rules touching 23 of them. Every run says so, and `--coverage` lists all
100
+ 55 with what this run reached on each — never as a percentage, because most of WCAG cannot
101
+ be automated and a score would present that as a fact about your site.
102
+
103
+ **Says what it did not measure.** A crawl that stopped at its page limit, or could not
104
+ fetch forty URLs, no longer produces a report that looks like a complete one.
105
+
65
106
  **Reports in four shapes**: a console report for whoever ran it, JSON for other tools,
66
107
  SARIF for GitHub code scanning, and a self-contained HTML page for the client whose site
67
108
  it is.
68
109
 
69
- **Runs in your build**, as a [Vite plugin](docs/integrations.md#vite-plugin) covering
70
- SvelteKit, Nuxt and Remix tooan [Astro integration](docs/integrations.md#astro-integration),
71
- or the bundled [GitHub Action](docs/integrations.md#github-actions).
110
+ **Says it once.** The flags a project runs on every build belong in the project, not in the
111
+ build script that repeats them. `eaa.config` the same file the statement reads — takes an
112
+ [`audit` block](docs/audit.md#defaults-from-eaaconfig) of defaults, and `baseline` reads the
113
+ keys that mean the same thing to it. A flag you type still wins, so a one-off `--browser`
114
+ check needs no edit to a committed file.
115
+
116
+ ```jsonc
117
+ { "audit": { "dir": "build", "failOn": "critical", "browser": true } }
118
+ ```
119
+
120
+ **Runs in your build**: a [Vite plugin](docs/integrations.md#vite-plugin) covering SvelteKit
121
+ and Remix too, and integrations for
122
+ [Astro](docs/integrations.md#astro-integration),
123
+ [Nuxt](docs/integrations.md#nuxt-module),
124
+ [Eleventy](docs/integrations.md#eleventy-plugin) and
125
+ [webpack](docs/integrations.md#webpack-plugin) — or the bundled
126
+ [GitHub Action](docs/integrations.md#github-actions).
127
+
128
+ **Audits what a CMS serves.** WordPress, TYPO3, Craft, Laravel, Symfony, Rails and Django
129
+ write no HTML to disk, so eaa-kit names the command that gets the site running and audits
130
+ that instead — following the site's own sitemap, wherever it keeps it.
131
+
132
+ ```bash
133
+ eaa-kit audit --url http://localhost:8000 --sitemap /sitemap_index.xml
134
+ ```
72
135
 
73
136
  ## Documentation
74
137
 
75
138
  | | |
76
139
  | --- | --- |
77
140
  | [Auditing a build](docs/audit.md) | The `audit` command, both engines, exit codes, and what an automated run can and cannot tell you |
78
- | [The statement command](docs/statement.md) | The config file, the three countries, and filling a statement from audit results |
141
+ | [Defaults from eaa.config](docs/audit.md#defaults-from-eaaconfig) | Writing the flags down once, and what still overrides them |
142
+ | [The statement command](docs/statement.md) | The config file, the seven countries, and filling a statement from audit results |
79
143
  | [Baselines](docs/baseline.md) | Adopting the tool on a site that already has violations |
144
+ | [Comparing two runs](docs/reports.md#comparing-two-runs) | The `diff` command, and what it refuses to call fixed |
145
+ | [Coverage of WCAG](docs/audit.md#how-much-of-wcag-a-run-reaches) | What an automated engine can reach at all, and what it cannot |
80
146
  | [Report formats](docs/reports.md) | The JSON contract, SARIF, and the HTML report |
81
- | [Integrations](docs/integrations.md) | Astro and GitHub Actions |
147
+ | [Integrations](docs/integrations.md) | The build plugins, the GitHub Action, and the two builders that get a command instead |
82
148
 
83
149
  Complete generated output for every format is checked in under [examples/](examples).
84
150
 
@@ -90,7 +156,9 @@ The two things worth knowing before you rely on any of it:
90
156
  text is accurate, whether a page makes sense in reading order, or whether a form can
91
157
  actually be completed with a screen reader. A clean report means nothing was found by this
92
158
  engine, which is not the same as a site being accessible — and none of the four output
93
- formats will say otherwise on your behalf.
159
+ formats will say otherwise on your behalf. This is not a disclaimer the tool leaves you to
160
+ take on faith: it counts it. Of the 55 WCAG 2.2 A and AA criteria, 34 have no automated
161
+ rule at all, and every report says so.
94
162
 
95
163
  **The browserless engine cannot decide everything.** jsdom has no layout, so rules that
96
164
  depend on rendering — colour contrast, target size, computed overflow — cannot be
@@ -1,4 +1,4 @@
1
- import { n as IntegrationOptions } from "../run-BMASMmwO.js";
1
+ import { n as IntegrationOptions } from "../run-CtcEUhbe.js";
2
2
  //#region src/astro/index.d.ts
3
3
  /**
4
4
  * An Astro integration that audits the build Astro just produced.
@@ -1,4 +1,4 @@
1
- import { n as auditBuild, t as BuildAuditError } from "../run-BW6CVuND.js";
1
+ import { n as auditBuild, t as BuildAuditError } from "../run-C2nKFcb-.js";
2
2
  import { fileURLToPath } from "node:url";
3
3
  //#region src/astro/index.ts
4
4
  var AstroAuditError = class extends Error {
@@ -1,4 +1,5 @@
1
- import { n as auditPage } from "../../jsdom-BjpF-2V-.js";
1
+ import { t as auditPage } from "../../jsdom-B--cEH-G.js";
2
+ import { enableCompileCache } from "node:module";
2
3
  import { parentPort, workerData } from "node:worker_threads";
3
4
  //#region src/audit/runners/worker.ts
4
5
  /**
@@ -17,6 +18,9 @@ import { parentPort, workerData } from "node:worker_threads";
17
18
  * something catastrophic — the thread running out of memory — reaches the
18
19
  * pool's error handler instead.
19
20
  */
21
+ try {
22
+ enableCompileCache();
23
+ } catch {}
20
24
  const port = parentPort;
21
25
  if (!port) throw new Error("eaa-kit audit worker was started outside a worker thread");
22
26
  const options = workerData ?? {};
@@ -1,8 +1,10 @@
1
- import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-YdoOtFqm.js";
2
- import { n as count, t as collapse } from "./text-BFmNtMsV.js";
3
- import { a as groupIssues, i as coverageParts, n as understandingUrl, o as isShared, r as blindRules, t as manualCheckFor } from "./manual-Vz-oX1I_.js";
4
- import { a as runEngine, i as note, n as emitDocument, o as warn, r as fail, t as advise } from "./command-Dxpa00Ha.js";
5
- import { n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-BkAQ0viT.js";
1
+ import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
2
+ import { a as plural, n as count, t as collapse } from "./text-CKKpzkYM.js";
3
+ import { a as isShared, c as runCompleteness, i as groupIssues, n as blindRules, r as coverageParts, t as remediationFor } from "./remediation-CMBIrnpN.js";
4
+ import { r as componentPath } from "./component-C3GL1Mnu.js";
5
+ import { i as understandingUrl, n as coverageSummary, r as manualCheckFor, t as buildCoverage } from "./coverage-B_Y6l-Ra.js";
6
+ import { a as emitDocument, c as runEngine, l as warn, o as fail, s as note, t as advise } from "./command-C3D7JWn6.js";
7
+ import { i as holdsHtml, n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-Cu1q9NTB.js";
6
8
  import path from "node:path";
7
9
  import pc from "picocolors";
8
10
  //#region src/audit/report/console.ts
@@ -47,6 +49,9 @@ function context(options) {
47
49
  sourceFor: options.sourceFor ?? (() => void 0),
48
50
  componentFor: options.componentFor ?? (() => void 0),
49
51
  manual: options.manual ?? false,
52
+ coverage: options.coverage ?? false,
53
+ framework: options.framework,
54
+ completeness: options.completeness,
50
55
  c,
51
56
  symbol: (kind) => {
52
57
  switch (kind) {
@@ -182,11 +187,15 @@ function summary(audits, ctx) {
182
187
  const elementCount = audits.reduce((total, audit) => total + audit.violations.reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
183
188
  const reviewCount = countRules(audits, "needs-review");
184
189
  const pages = count(audits.length, "page");
185
- const lines = [line(ctx, "Summary", ctx.c.bold)];
186
- if (ruleCount === 0) lines.push(line(ctx, ` No violations across ${pages}.`, ctx.c.green));
187
- else {
190
+ const audited = audits.length - errored.length;
191
+ const lines = [line(ctx, "Summary", ctx.c.bold), ...completenessLines(ctx)];
192
+ if (ruleCount === 0 && audited === 0 && errored.length > 0) lines.push(line(ctx, " Nothing was audited: no page could be read.", ctx.c.red));
193
+ else if (ruleCount === 0) {
194
+ const clean = errored.length > 0 || ctx.completeness && !ctx.completeness.complete ? ` No violations across the ${count(audited, "page")} that were audited.` : ` No violations across ${pages}.`;
195
+ lines.push(line(ctx, clean, ctx.c.green));
196
+ } else {
188
197
  lines.push(render(ctx, [{
189
- text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${pages}`,
198
+ text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${count(audited, "page")}`,
190
199
  paint: ctx.c.red
191
200
  }, {
192
201
  text: ` (${count(elementCount, "element")})`,
@@ -202,6 +211,74 @@ function summary(audits, ctx) {
202
211
  const accepted = audits.reduce((total, audit) => total + (audit.accepted ?? []).reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
203
212
  if (accepted > 0) lines.push(line(ctx, ` ${count(accepted, "element")} accepted by the baseline, not counted above`, ctx.c.dim));
204
213
  lines.push(...blindSection(audits, ctx));
214
+ lines.push(...coverageSection(audits, ctx));
215
+ return lines;
216
+ }
217
+ /**
218
+ * How much of the standard this run could reach.
219
+ *
220
+ * One line by default. It is the sentence that stops a clean report reading as
221
+ * a clean site: most of WCAG cannot be checked by any automated engine, and
222
+ * until this was printed the report said so only in prose, in the footer, where
223
+ * it could be read as boilerplate.
224
+ */
225
+ function coverageSection(audits, ctx) {
226
+ const coverage = buildCoverage(audits);
227
+ const lines = ["", ...wrap(coverageSummary(coverage), ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim))];
228
+ if (coverage.browserWouldAnswer > 0) {
229
+ const verb = coverage.browserWouldAnswer === 1 ? "criterion" : "criteria";
230
+ lines.push(line(ctx, ` --browser would answer ${coverage.browserWouldAnswer} more ${verb}.`, ctx.c.dim));
231
+ }
232
+ if (!ctx.coverage) {
233
+ lines.push(line(ctx, " Run with --coverage for the criterion-by-criterion breakdown.", ctx.c.dim));
234
+ return lines;
235
+ }
236
+ lines.push("", line(ctx, "Coverage", ctx.c.bold));
237
+ for (const criterion of coverage.criteria) {
238
+ const paint = criterion.status === "evaluated" ? ctx.c.green : ctx.c.dim;
239
+ const note = criterion.browserWouldAnswer ? " (--browser would answer this)" : "";
240
+ lines.push(render(ctx, [
241
+ {
242
+ text: ` ${criterion.number} `,
243
+ paint: ctx.c.bold
244
+ },
245
+ { text: `${criterion.title} (${criterion.level}) — ` },
246
+ {
247
+ text: `${STATUS_WORDS[criterion.status]}${note}`,
248
+ paint
249
+ }
250
+ ]));
251
+ }
252
+ return lines;
253
+ }
254
+ /** What each outcome is called, in words rather than a symbol. */
255
+ const STATUS_WORDS = {
256
+ evaluated: "evaluated here",
257
+ "not-evaluated": "this engine could not evaluate it",
258
+ "nothing-to-check": "rules ran and found nothing on this site to check",
259
+ "no-automated-rule": "no automated rule exists; a person must check it"
260
+ };
261
+ /**
262
+ * What the run never looked at.
263
+ *
264
+ * Printed before the counts rather than after them, because it changes how they
265
+ * read: "no violations" means one thing over a whole site and another over the
266
+ * twelve pages of it a crawl managed to fetch before it hit its limit.
267
+ *
268
+ * Pages that errored are left to the summary's own line, which already names
269
+ * them; repeating the number here would read as twice as many.
270
+ */
271
+ function completenessLines(ctx) {
272
+ const completeness = ctx.completeness;
273
+ if (completeness === void 0 || completeness.complete) return [];
274
+ const lines = [];
275
+ if (completeness.unreachable.length > 0) {
276
+ const noun = plural(completeness.unreachable.length, "page");
277
+ const verb = completeness.unreachable.length === 1 ? "was" : "were";
278
+ lines.push(line(ctx, ` ${completeness.unreachable.length} ${noun} could not be reached, and ${verb} not audited`, ctx.c.yellow));
279
+ }
280
+ if (completeness.truncated) lines.push(line(ctx, " The run stopped at its page limit; the site has more", ctx.c.yellow));
281
+ if (lines.length > 0) lines.push(line(ctx, " This report describes what was audited, not the whole site.", ctx.c.dim));
205
282
  return lines;
206
283
  }
207
284
  /**
@@ -296,6 +373,7 @@ function issuesSection(audits, ctx) {
296
373
  }] : []
297
374
  ]));
298
375
  lines.push(line(ctx, ` ${issue.help}`, ctx.c.dim));
376
+ lines.push(...remediationLines(issue.ruleId, issue.elements[0]?.html, ctx));
299
377
  for (const element of issue.elements.slice(0, ctx.maxNodes)) {
300
378
  lines.push(line(ctx, ` ${collapse(element.html)}`));
301
379
  lines.push(...whereLines(element, ctx));
@@ -305,6 +383,27 @@ function issuesSection(audits, ctx) {
305
383
  }
306
384
  return lines;
307
385
  }
386
+ /**
387
+ * What to do about the rule, under the finding rather than in a link.
388
+ *
389
+ * axe-core's help text says what is wrong; this says who it stops and what to
390
+ * change. The corrected line is built from the element that actually failed,
391
+ * because a textbook snippet is a second thing to translate before anybody can
392
+ * use it.
393
+ */
394
+ function remediationLines(ruleId, html, ctx) {
395
+ const remediation = remediationFor(ruleId, ctx.framework);
396
+ if (remediation === void 0) return [];
397
+ const indent = " ";
398
+ const width = ctx.width - 6 - 2;
399
+ const lines = [...wrap(remediation.why, width).map((text) => line(ctx, `${indent}${text}`, ctx.c.dim)), ...wrap(`Fix: ${remediation.fix}`, width).map((text) => line(ctx, `${indent}${text}`))];
400
+ const example = html === void 0 ? void 0 : remediation.example?.(html);
401
+ if (example !== void 0) lines.push(render(ctx, [{
402
+ text: `${indent}→ `,
403
+ paint: ctx.c.green
404
+ }, { text: collapse(example) }]));
405
+ return lines;
406
+ }
308
407
  /** Where one element appears, and what that says about where the fix goes. */
309
408
  function whereLines(element, ctx) {
310
409
  const shown = element.pages.slice(0, ctx.maxNodes);
@@ -313,7 +412,7 @@ function whereLines(element, ctx) {
313
412
  const lines = component === void 0 ? [] : [render(ctx, [{
314
413
  text: " written in ",
315
414
  paint: ctx.c.dim
316
- }, { text: component }])];
415
+ }, { text: componentPath(component) }])];
317
416
  lines.push(line(ctx, ` on ${count(element.pages.length, "page")}:`, ctx.c.dim));
318
417
  for (const page of shown) {
319
418
  const source = ctx.sourceFor(page);
@@ -364,16 +463,22 @@ async function resolvePages(directory, options = {}) {
364
463
  return {
365
464
  pages: crawled.pages,
366
465
  origin: crawled.origin,
367
- label: options.url
466
+ label: options.url,
467
+ completeness: crawled.completeness
368
468
  };
369
469
  }
370
470
  const cwd = options.cwd ?? process.cwd();
371
471
  const shown = options.label ?? directory;
472
+ const unreachable = [];
372
473
  let pages;
373
474
  try {
374
475
  pages = await collectPages(directory, {
375
476
  ...options.include ? { include: options.include } : {},
376
- ...options.exclude ? { exclude: options.exclude } : {}
477
+ ...options.exclude ? { exclude: options.exclude } : {},
478
+ onUnreadable: (relativePath, reason) => unreachable.push({
479
+ location: relativePath,
480
+ reason
481
+ })
377
482
  });
378
483
  } catch (cause) {
379
484
  if (!(cause instanceof BuildDirectoryError)) throw cause;
@@ -382,12 +487,32 @@ async function resolvePages(directory, options = {}) {
382
487
  return;
383
488
  }
384
489
  if (pages.length === 0) {
490
+ if ((options.include !== void 0 || options.exclude !== void 0) && await holdsHtml(directory)) {
491
+ warn(`No page in ${shown} matched the filters, so nothing was audited.`);
492
+ if (options.include !== void 0) note(` --include ${options.include.join(" ")}`);
493
+ if (options.exclude !== void 0) note(` --exclude ${options.exclude.join(" ")}`);
494
+ note(" Patterns are relative to the audited directory, with POSIX separators.");
495
+ return;
496
+ }
385
497
  warn(await emptyDirectoryHint(shown, cwd));
386
498
  return;
387
499
  }
500
+ if (unreachable.length > 0) {
501
+ const verb = unreachable.length === 1 ? "file was" : "files were";
502
+ warn(`${unreachable.length} ${verb} not readable, and so not audited:`);
503
+ for (const file of unreachable.slice(0, 10)) note(` ${file.location} — ${file.reason}`);
504
+ if (unreachable.length > 10) note(` …and ${unreachable.length - 10} more`);
505
+ }
388
506
  return {
389
507
  pages,
390
- label: shown
508
+ directory,
509
+ label: shown,
510
+ completeness: {
511
+ discovery: "directory",
512
+ collected: pages.length,
513
+ unreachable,
514
+ truncated: false
515
+ }
391
516
  };
392
517
  }
393
518
  /**
@@ -397,7 +522,7 @@ async function resolvePages(directory, options = {}) {
397
522
  * exit 2 — a run that reached no verdict, not a clean one.
398
523
  */
399
524
  async function crawlPages(url, options) {
400
- const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-Oxt2Gaqo.js");
525
+ const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BUWPgaGW.js");
401
526
  let entry;
402
527
  try {
403
528
  entry = parseEntryUrl(url, options.allowRemote ?? false);
@@ -412,6 +537,7 @@ async function crawlPages(url, options) {
412
537
  const result = await crawlSite(entry, {
413
538
  ...options.allowRemote ? { allowRemote: true } : {},
414
539
  ...options.ignoreRobots ? { ignoreRobots: true } : {},
540
+ ...options.sitemap === void 0 ? {} : { sitemap: options.sitemap },
415
541
  ...options.maxPages === void 0 ? {} : { maxPages: options.maxPages },
416
542
  ...options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth },
417
543
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
@@ -432,7 +558,16 @@ async function crawlPages(url, options) {
432
558
  if (result.truncated) warn(`Stopped at ${count(result.pages.length, "page")}; the site has more. Raise --max-pages to go further.`);
433
559
  return {
434
560
  pages: result.pages,
435
- origin: result.origin
561
+ origin: result.origin,
562
+ completeness: {
563
+ discovery: result.discovery,
564
+ collected: result.pages.length,
565
+ unreachable: result.failures.map((failure) => ({
566
+ location: failure.url,
567
+ reason: failure.reason
568
+ })),
569
+ truncated: result.truncated
570
+ }
436
571
  };
437
572
  }
438
573
  /**
@@ -444,7 +579,7 @@ async function crawlPages(url, options) {
444
579
  */
445
580
  async function resolveAutomatically(options) {
446
581
  const cwd = options.cwd ?? process.cwd();
447
- const { autoDetectSource } = await import("./project-DW08TseF.js");
582
+ const { autoDetectSource } = await import("./project-MFrXcw1M.js");
448
583
  const detected = await autoDetectSource(cwd, {
449
584
  ...options.noBuild ? { noBuild: true } : {},
450
585
  onStep: note
@@ -485,12 +620,13 @@ const OUTPUT_FORMATS = [
485
620
  * piped somewhere without the chatter coming along.
486
621
  */
487
622
  async function runAuditCommand(dir, options = {}) {
623
+ if (options.fast && options.browser) warn("--fast has no effect with --browser: a real browser can decide those rules.");
488
624
  const resolved = await resolvePages(dir, options);
489
625
  if (!resolved) return {
490
626
  audits: [],
491
627
  exitCode: 2
492
628
  };
493
- const { pages, origin, label, cleanup } = resolved;
629
+ const { pages, origin, label, cleanup, directory, completeness: collection } = resolved;
494
630
  try {
495
631
  note(`Auditing ${count(pages.length, "page")} in ${label}${await describeEngine(pages, options)}…`);
496
632
  const baseUrl = options.baseUrl ?? origin;
@@ -499,8 +635,9 @@ async function runAuditCommand(dir, options = {}) {
499
635
  ...baseUrl === void 0 ? {} : { baseUrl },
500
636
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
501
637
  ...options.browser ? { browser: true } : {},
638
+ ...options.fast && !options.browser ? { fast: true } : {},
502
639
  ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
503
- ...options.url === void 0 && dir !== void 0 ? { directory: dir } : {}
640
+ ...directory === void 0 ? {} : { directory }
504
641
  });
505
642
  if (!audits) return {
506
643
  audits: [],
@@ -515,7 +652,8 @@ async function runAuditCommand(dir, options = {}) {
515
652
  };
516
653
  audits = applied;
517
654
  }
518
- await emit(audits, label, failOn, options);
655
+ const completeness = runCompleteness(audits, collection);
656
+ await emit(audits, label, failOn, completeness, options);
519
657
  const unaudited = audits.filter((audit) => audit.error);
520
658
  if (unaudited.length > 0) {
521
659
  fail(`${unaudited.length} of ${audits.length} pages could not be audited`);
@@ -541,7 +679,7 @@ async function runAuditCommand(dir, options = {}) {
541
679
  * would be as wrong as silently passing.
542
680
  */
543
681
  async function acceptBaseline(audits, options) {
544
- const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-CuKFq4IF.js");
682
+ const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-0u4df12v.js");
545
683
  try {
546
684
  const outcome = applyBaseline(audits, await readBaseline(options.baseline, options.cwd ?? process.cwd()));
547
685
  if (outcome.accepted > 0) note(`Baseline accepted ${outcome.accepted} violating elements`);
@@ -568,52 +706,66 @@ async function acceptBaseline(audits, options) {
568
706
  */
569
707
  async function describeEngine(pages, options) {
570
708
  if (options.browser) return " with Chromium";
571
- const { plannedWorkers } = await import("./pool-BWkWZiJW.js");
709
+ const { plannedWorkers } = await import("./pool-BO25OIez.js");
572
710
  const workers = options.concurrency ?? plannedWorkers(pages);
573
- return workers > 1 ? ` across ${workers} threads` : "";
711
+ const threads = workers > 1 ? ` across ${workers} threads` : "";
712
+ return options.fast ? `${threads}, skipping what this engine cannot decide` : threads;
574
713
  }
575
714
  /**
576
715
  * Emit the chosen format, to a file when --output is given and to stdout
577
716
  * otherwise. Colour is dropped when writing to a file, since escape codes in a
578
717
  * saved report are noise.
579
718
  */
580
- async function emit(audits, dir, failOn, options) {
581
- const body = await renderReport(audits, dir, failOn, options.format ?? "console", typeof options.output === "string", options);
719
+ async function emit(audits, dir, failOn, completeness, options) {
720
+ const body = await renderReport(audits, dir, failOn, completeness, options.format ?? "console", typeof options.output === "string", options);
582
721
  await emitDocument(body, options.output, options.cwd ?? process.cwd());
583
722
  if (options.output !== void 0) note(`Report written to ${options.output}`);
584
723
  }
585
- async function renderReport(audits, dir, failOn, format, toFile, options) {
724
+ async function renderReport(audits, dir, failOn, completeness, format, toFile, options) {
586
725
  switch (format) {
587
726
  case "json": {
588
- const { buildJsonReport, serialiseJsonReport } = await import("./json-B0Y7rNjt.js");
727
+ const { buildJsonReport, serialiseJsonReport } = await import("./json-D_Mnnft5.js");
589
728
  return serialiseJsonReport(buildJsonReport(audits, {
590
729
  directory: dir,
591
730
  ...options.url === void 0 ? {} : { sourceKind: "url" },
592
731
  failOn,
732
+ completeness,
593
733
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
594
734
  }));
595
735
  }
596
736
  case "sarif": {
597
- const { buildSarifReport, serialiseSarifReport } = await import("./sarif-DB3WG7T9.js");
598
- return serialiseSarifReport(buildSarifReport(audits, { directory: dir }));
737
+ const { buildSarifReport, serialiseSarifReport } = await import("./sarif-B-UBcVu8.js");
738
+ return serialiseSarifReport(buildSarifReport(audits, {
739
+ directory: dir,
740
+ completeness
741
+ }));
599
742
  }
600
743
  case "html": {
601
- const { buildHtmlReport } = await import("./html-DKiI_3gs.js");
744
+ const { buildHtmlReport } = await import("./html-C4vktg0A.js");
745
+ const framework = await detectedFramework(options.cwd ?? process.cwd());
602
746
  return buildHtmlReport(audits, {
603
- ...await sourceLookups(options.cwd ?? process.cwd()),
747
+ ...await sourceLookups(options.cwd ?? process.cwd(), audits),
604
748
  directory: dir,
605
749
  failOn,
750
+ completeness,
751
+ ...framework === void 0 ? {} : { framework },
606
752
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
607
753
  });
608
754
  }
609
- case "console": return `${formatConsoleReport(audits, {
610
- ...await sourceLookups(options.cwd ?? process.cwd()),
611
- dir,
612
- failOn,
613
- ...options.perPage ? { perPage: true } : {},
614
- ...options.manual ? { manual: true } : {},
615
- ...toFile ? { color: false } : {}
616
- })}\n`;
755
+ case "console": {
756
+ const framework = await detectedFramework(options.cwd ?? process.cwd());
757
+ return `${formatConsoleReport(audits, {
758
+ ...await sourceLookups(options.cwd ?? process.cwd(), audits),
759
+ dir,
760
+ failOn,
761
+ completeness,
762
+ ...options.perPage ? { perPage: true } : {},
763
+ ...options.manual ? { manual: true } : {},
764
+ ...options.coverage ? { coverage: true } : {},
765
+ ...framework === void 0 ? {} : { framework },
766
+ ...toFile ? { color: false } : {}
767
+ })}\n`;
768
+ }
617
769
  }
618
770
  }
619
771
  /**
@@ -621,15 +773,29 @@ async function renderReport(audits, dir, failOn, format, toFile, options) {
621
773
  * so. Best-effort: a project using no convention this recognises gets the
622
774
  * report it always got, with no source named.
623
775
  */
624
- async function sourceLookups(cwd) {
625
- const { buildRouteMap, sourceFor } = await import("./routes-C2Cgf6Ko.js");
626
- const { buildComponentIndex, componentFor } = await import("./component-7kEBjv_y.js");
776
+ /** The registry id of whatever built this project, for framework-shaped advice. */
777
+ async function detectedFramework(cwd) {
778
+ const { detectFramework } = await import("./frameworks-etFg_O8K.js");
779
+ const { readPackageJson } = await import("./project-MFrXcw1M.js");
780
+ return (await detectFramework(cwd, await readPackageJson(cwd)))?.framework.id;
781
+ }
782
+ async function sourceLookups(cwd, audits) {
783
+ const { buildRouteMap, sourceFor } = await import("./routes-CmdRUuOs.js");
627
784
  const routes = await buildRouteMap(cwd);
785
+ if (!hasElementsToAttribute(audits)) return {
786
+ sourceFor: (page) => sourceFor(routes, page),
787
+ componentFor: () => void 0
788
+ };
789
+ const { buildComponentIndex, componentFor } = await import("./component-DKd3EHOg.js");
628
790
  const components = await buildComponentIndex(cwd);
629
791
  return {
630
792
  sourceFor: (page) => sourceFor(routes, page),
631
793
  componentFor: (html) => componentFor(components, html)
632
794
  };
633
795
  }
796
+ /** Whether any element will be listed, and so looked up against the source. */
797
+ function hasElementsToAttribute(audits) {
798
+ return audits.some((audit) => audit.violations.length > 0 || (audit.accepted?.length ?? 0) > 0);
799
+ }
634
800
  //#endregion
635
801
  export { runAuditCommand as n, resolvePages as r, OUTPUT_FORMATS as t };
@@ -0,0 +1,2 @@
1
+ import { n as runAuditCommand } from "./audit-B282A3EA.js";
2
+ export { runAuditCommand };
@@ -1,2 +1,2 @@
1
- import { a as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-s9F3fXTN.js";
1
+ import { a as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-CuAS2lMk.js";
2
2
  export { BaselineError, applyBaseline, readBaseline };
@@ -1,6 +1,6 @@
1
- import { _ as withDefault, c as object, f as safeParse, i as isoDate$1, l as optional, o as nullable, p as string, s as number, t as array } from "./schema-CMZ8ItGk.js";
2
- import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
3
- import { r as findingElements } from "./result-DLxd2Eip.js";
1
+ import { c as nullable, d as optional, h as string, l as number, m as safeParse, o as isoDate$1, t as array, u as object, y as withDefault } from "./schema-DJSF4K05.js";
2
+ import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
3
+ import { o as findingElements } from "./result-BWcYXeRs.js";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  /** Default filename, used by the CLI when no path is given. */
@@ -109,7 +109,7 @@ function buildBaseline(audits, options = {}) {
109
109
  ...options.note ? { note: options.note } : {}
110
110
  });
111
111
  return {
112
- schemaVersion: 1,
112
+ schemaVersion: 2,
113
113
  createdOn: acceptedOn,
114
114
  entries: entries.sort(byEntry)
115
115
  };
@@ -137,7 +137,8 @@ async function readBaseline(file, cwd = process.cwd()) {
137
137
  const issues = result.error.issues.map((issue) => `${issue.path.join(".") || "document"}: ${issue.message}`).slice(0, 5);
138
138
  throw new BaselineError(`${path.basename(target)} is not an eaa-kit baseline (${issues.join("; ")})`);
139
139
  }
140
- if (result.data.schemaVersion !== 1) throw new BaselineError(`${path.basename(target)} has schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 1`);
140
+ if (result.data.schemaVersion !== 2) throw new BaselineError(`${path.basename(target)} has schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 2.\n Record it again on the current build: eaa-kit baseline\n Read the new file before committing it: it lists what this run found, which is
141
+ not necessarily what the old one accepted.`);
141
142
  return result.data;
142
143
  }
143
144
  async function writeBaseline(file, baseline, cwd = process.cwd()) {