eaa-kit 0.4.0 → 0.6.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 (64) hide show
  1. package/README.md +66 -11
  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-DK2StT1_.js +2 -0
  6. package/dist/{audit-CPoZMXGM.js → audit-e_uixhPG.js} +123 -54
  7. package/dist/baseline-BscHfFSW.js +2 -0
  8. package/dist/{baseline-DB9CZGnV.js → baseline-D2qWmUC6.js} +40 -12
  9. package/dist/checklist-BtgkxMEa.js +44 -0
  10. package/dist/cli/index.js +153 -25
  11. package/dist/{collect-CFM8gEVv.js → collect-Cu1q9NTB.js} +34 -3
  12. package/dist/command-CZKp9Wi_.js +251 -0
  13. package/dist/{remediation-Dtowi2EC.js → completeness-Dwq3CPB3.js} +61 -43
  14. package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
  15. package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
  16. package/dist/coverage-7bQqWNfN.js +2 -0
  17. package/dist/{coverage-B4IBKMO8.js → coverage-BD2_CYns.js} +46 -8
  18. package/dist/{crawl-BiI1Lau_.js → crawl-BHkJmUkA.js} +119 -11
  19. package/dist/eleventy/index.d.ts +1 -1
  20. package/dist/eleventy/index.js +1 -1
  21. package/dist/fingerprint-BjYV_0F7.js +67 -0
  22. package/dist/{html-BGTO3ypW.js → html-Bmh7X_sM.js} +52 -30
  23. package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
  24. package/dist/index.d.ts +187 -5
  25. package/dist/index.js +3 -3
  26. package/dist/{init-DIWDE35F.js → init-BjKYttxc.js} +10 -4
  27. package/dist/{jsdom-DCpGSLfW.js → jsdom-CQar6OQS.js} +12 -4
  28. package/dist/jsdom-DMvU2h-f.js +3 -0
  29. package/dist/{json-QQuFIw1W.js → json-HJDgJp09.js} +1 -1
  30. package/dist/{json-DjEvy1nX.js → json-PNg6BeXp.js} +13 -18
  31. package/dist/load-Bc_xDGAh.js +2 -0
  32. package/dist/{load-5wRGLvub.js → load-vZdV_fD1.js} +137 -8
  33. package/dist/nuxt/index.d.ts +1 -1
  34. package/dist/nuxt/index.js +1 -1
  35. package/dist/{playwright-BWniOain.js → playwright-C-miJP2b.js} +32 -7
  36. package/dist/{pool-BMevaLWD.js → pool-dkexq5xo.js} +52 -2
  37. package/dist/{render-DrvXRCEn.js → render-CtsqR0k-.js} +150 -11
  38. package/dist/{result-DoamKFsp.js → result-DfUru6y_.js} +60 -3
  39. package/dist/review-CdMK2GpQ.js +161 -0
  40. package/dist/review-DIbOTmOM.js +2 -0
  41. package/dist/{run-DB34BSOZ.js → run-D7hdVXLA.js} +1 -1
  42. package/dist/{run-BMASMmwO.d.ts → run-YiqyQA2b.d.ts} +17 -0
  43. package/dist/{sarif-SR3_lLYd.js → sarif-BKvq_AUm.js} +25 -14
  44. package/dist/{schema-is6CGX2D.js → schema-DJSF4K05.js} +15 -1
  45. package/dist/statement/templates/es.en.md +125 -0
  46. package/dist/statement/templates/es.es.md +127 -0
  47. package/dist/statement/templates/fr.en.md +128 -0
  48. package/dist/statement/templates/fr.fr.md +131 -0
  49. package/dist/statement/templates/it.en.md +127 -0
  50. package/dist/statement/templates/it.it.md +130 -0
  51. package/dist/statement/templates/nl.en.md +125 -0
  52. package/dist/statement/templates/nl.nl.md +127 -0
  53. package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
  54. package/dist/vite/index.d.ts +1 -1
  55. package/dist/vite/index.js +1 -1
  56. package/dist/webpack/index.d.ts +1 -1
  57. package/dist/webpack/index.js +1 -1
  58. package/dist/worksheet-eUjwYLxC.js +63 -0
  59. package/package.json +7 -3
  60. package/dist/audit-CpXH2Mk8.js +0 -2
  61. package/dist/baseline-22Y1NWxM.js +0 -2
  62. package/dist/command-D8l_oYbV.js +0 -77
  63. package/dist/fingerprint-DRoneAjj.js +0 -20
  64. package/dist/jsdom-4IMzv0eE.js +0 -3
package/README.md CHANGED
@@ -3,15 +3,24 @@
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.
11
+
12
+ 0.6.0 adds the half no engine can do. Of the 55 WCAG 2.2 A and AA success criteria, 34 have
13
+ no automated rule at all: `checklist` writes those down as a review somebody works through,
14
+ `audit --review` reads the answers back beside what the run measured, and `statement` now
15
+ refuses to publish a claim of full conformance that either one contradicts. Sites behind a
16
+ login or a preview protection are auditable too.
9
17
 
10
18
  ```bash
11
19
  npx eaa-kit audit # WCAG 2.2 AA report; finds your build itself
12
20
  npx eaa-kit diff a.json b.json # what a change made worse, and what it fixed
13
21
  npx eaa-kit init # write an eaa.config.json
14
- npx eaa-kit statement # Barrierefreiheitserklärung from that config
22
+ npx eaa-kit statement # accessibility statement, in one of seven countries
23
+ npx eaa-kit checklist # the manual review no engine can do for you
15
24
  ```
16
25
 
17
26
  > **Not legal advice.** eaa-kit reports what an automated engine can and cannot determine
@@ -33,7 +42,9 @@ supported here either.
33
42
  **Audits your build.** Globs the HTML out of `./dist`, parses it with jsdom and runs
34
43
  axe-core against it. No Chromium download, fast enough for CI, and it never fetches
35
44
  anything or executes your site's JavaScript. `--browser` swaps in real Chromium for the
36
- rules that need layout and CSS.
45
+ rules that need layout and CSS; `--fast` goes the other way and skips the rules the
46
+ browserless engine cannot decide anyway, rather than running them and discarding the
47
+ answer.
37
48
 
38
49
  ```bash
39
50
  eaa-kit audit ./dist --fail-on serious
@@ -46,14 +57,21 @@ export, Nuxt, SvelteKit, anything behind a CMS — are audited running instead:
46
57
  eaa-kit audit --url http://localhost:3000
47
58
  ```
48
59
 
49
- **Writes the statement.** A Barrierefreiheitserklärung from one config file, in German or
50
- English, as Markdown or HTML, naming the statute and supervisory body of Austria,
51
- Switzerland or Germany and optionally listing the barriers a real audit found.
60
+ **Writes the statement.** A Barrierefreiheitserklärung, a déclaration d'accessibilité, a
61
+ dichiarazione di accessibilità whatever the country calls it from one config file, as
62
+ Markdown or HTML, naming that country's statute and supervisory body, and optionally
63
+ listing the barriers a real audit found.
52
64
 
53
65
  ```bash
54
66
  eaa-kit statement --output src/content/a11y.md
67
+ eaa-kit statement --country FR --lang fr # or ES, IT, NL, AT, DE, CH
55
68
  ```
56
69
 
70
+ Each country's statement is a document under its own law rather than a translation of
71
+ another's, so where a national regime asks for more than this — France's RGAA declaration,
72
+ Italy's filing with AgID — the text says so instead of letting a generated file look like
73
+ it settles the matter.
74
+
57
75
  **Fails only on what is new.** The first run on a site that already exists finds
58
76
  everything at once. A baseline records what is already wrong so the build fails on
59
77
  regressions instead, without ever letting an accepted barrier look like a passing one.
@@ -84,6 +102,17 @@ could invent a fix that looks right and is not.
84
102
  written in src/components/Header.astro:12
85
103
  ```
86
104
 
105
+ **Records the part no engine can do.** 34 of the 55 criteria have no automated rule at
106
+ all. `eaa-kit checklist` writes them out as a worksheet and a record; `audit --review`
107
+ reads the record back and reports what a person checked *beside* what the run measured,
108
+ never folded into it. A review never overrules the engine, never counts an entry nobody
109
+ answered, and never counts one that has aged out.
110
+
111
+ ```bash
112
+ eaa-kit checklist --output a11y-review.md
113
+ eaa-kit audit --review eaa-review.json --coverage
114
+ ```
115
+
87
116
  **Says how much of WCAG it could reach.** WCAG 2.2 has 55 success criteria at Levels A and
88
117
  AA. axe-core has rules touching 23 of them. Every run says so, and `--coverage` lists all
89
118
  55 with what this run reached on each — never as a percentage, because most of WCAG cannot
@@ -96,6 +125,16 @@ fetch forty URLs, no longer produces a report that looks like a complete one.
96
125
  SARIF for GitHub code scanning, and a self-contained HTML page for the client whose site
97
126
  it is.
98
127
 
128
+ **Says it once.** The flags a project runs on every build belong in the project, not in the
129
+ build script that repeats them. `eaa.config` — the same file the statement reads — takes an
130
+ [`audit` block](docs/audit.md#defaults-from-eaaconfig) of defaults, and `baseline` reads the
131
+ keys that mean the same thing to it. A flag you type still wins, so a one-off `--browser`
132
+ check needs no edit to a committed file.
133
+
134
+ ```jsonc
135
+ { "audit": { "dir": "build", "failOn": "critical", "browser": true } }
136
+ ```
137
+
99
138
  **Runs in your build**: a [Vite plugin](docs/integrations.md#vite-plugin) covering SvelteKit
100
139
  and Remix too, and integrations for
101
140
  [Astro](docs/integrations.md#astro-integration),
@@ -112,15 +151,27 @@ that instead — following the site's own sitemap, wherever it keeps it.
112
151
  eaa-kit audit --url http://localhost:8000 --sitemap /sitemap_index.xml
113
152
  ```
114
153
 
154
+ **Reaches a site behind a login.** A preview deployment or a staging CMS refuses anonymous
155
+ requests, which used to put both out of reach. `--basic-auth` and a repeatable `--header`
156
+ cover them, on the crawl and in the browser, and the credentials are never written into a
157
+ report, a baseline or a SARIF log — nor accepted from `eaa.config`, which is a committed
158
+ file.
159
+
160
+ ```bash
161
+ eaa-kit audit --url https://preview.example.com --basic-auth user:password
162
+ ```
163
+
115
164
  ## Documentation
116
165
 
117
166
  | | |
118
167
  | --- | --- |
119
168
  | [Auditing a build](docs/audit.md) | The `audit` command, both engines, exit codes, and what an automated run can and cannot tell you |
120
- | [The statement command](docs/statement.md) | The config file, the three countries, and filling a statement from audit results |
169
+ | [Defaults from eaa.config](docs/audit.md#defaults-from-eaaconfig) | Writing the flags down once, and what still overrides them |
170
+ | [The statement command](docs/statement.md) | The config file, the seven countries, and filling a statement from audit results |
121
171
  | [Baselines](docs/baseline.md) | Adopting the tool on a site that already has violations |
122
172
  | [Comparing two runs](docs/reports.md#comparing-two-runs) | The `diff` command, and what it refuses to call fixed |
123
173
  | [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 |
174
+ | [Recording a manual review](docs/review.md) | The `checklist` command, the record, and what a review is not allowed to do |
124
175
  | [Report formats](docs/reports.md) | The JSON contract, SARIF, and the HTML report |
125
176
  | [Integrations](docs/integrations.md) | The build plugins, the GitHub Action, and the two builders that get a command instead |
126
177
 
@@ -147,11 +198,15 @@ you](docs/audit.md#what-the-browserless-engine-can-and-cannot-tell-you).
147
198
 
148
199
  The same applies to the statement: it says what you told it. eaa-kit cannot check whether
149
200
  those claims are true, and a statement claiming full conformance for a site that is not
150
- conformant is worse than no statement at all.
201
+ conformant is worse than no statement at all — which is why, given evidence, it now refuses
202
+ to write that particular one. `statement --audit` will not claim full conformance over a
203
+ report listing barriers, and `--review` will not claim it over criteria somebody recorded as
204
+ not met.
151
205
 
152
206
  ## Changelog
153
207
 
154
- See [CHANGELOG.md](CHANGELOG.md).
208
+ See [CHANGELOG.md](CHANGELOG.md), and [ROADMAP.md](ROADMAP.md) for what 0.6.0 is meant to
209
+ add and what it deliberately will not.
155
210
 
156
211
  ## License
157
212
 
@@ -1,4 +1,4 @@
1
- import { n as IntegrationOptions } from "../run-BMASMmwO.js";
1
+ import { n as IntegrationOptions } from "../run-YiqyQA2b.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-DB34BSOZ.js";
1
+ import { n as auditBuild, t as BuildAuditError } from "../run-D7hdVXLA.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 { t as auditPage } from "../../jsdom-DCpGSLfW.js";
1
+ import { t as auditPage } from "../../jsdom-CQar6OQS.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 ?? {};
@@ -0,0 +1,2 @@
1
+ import { n as runAuditCommand } from "./audit-e_uixhPG.js";
2
+ export { runAuditCommand };
@@ -1,10 +1,13 @@
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-Dtowi2EC.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-B4IBKMO8.js";
6
- import { a as runEngine, i as note, n as emitDocument, o as warn, r as fail, t as advise } from "./command-D8l_oYbV.js";
7
- import { n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-CFM8gEVv.js";
1
+ import { a as impactLabel, i as countAtOrAbove, r as byImpactThenRule } from "./impact-luGCnN8F.js";
2
+ import { n as count, o as plural, t as collapse } from "./text-BF1LHMiV.js";
3
+ import { c as runEngine } from "./result-DfUru6y_.js";
4
+ import { a as emitDocument, c as runEngine$1, l as warn, o as fail, s as note, t as advise } from "./command-CZKp9Wi_.js";
5
+ import { l as reviewSentence } from "./review-CdMK2GpQ.js";
6
+ import { r as componentPath } from "./component-ChisoFyY.js";
7
+ import { i as reviewSummary, n as buildCoverage, o as manualCheckFor, r as coverageSummary, s as understandingUrl } from "./coverage-BD2_CYns.js";
8
+ import { a as blindRules, c as isShared, i as remediationFor, l as issueTotals, o as coverageParts, r as runCompleteness, s as groupIssues } from "./completeness-Dwq3CPB3.js";
9
+ import { n as buildSummary } from "./json-PNg6BeXp.js";
10
+ import { i as holdsHtml, n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-Cu1q9NTB.js";
8
11
  import path from "node:path";
9
12
  import pc from "picocolors";
10
13
  //#region src/audit/report/console.ts
@@ -51,6 +54,7 @@ function context(options) {
51
54
  manual: options.manual ?? false,
52
55
  coverage: options.coverage ?? false,
53
56
  framework: options.framework,
57
+ review: options.review,
54
58
  completeness: options.completeness,
55
59
  c,
56
60
  symbol: (kind) => {
@@ -92,7 +96,7 @@ function line(ctx, text, paint) {
92
96
  }]);
93
97
  }
94
98
  function headerLines(audits, ctx) {
95
- const engineLabel = (audits[0]?.engine ?? "jsdom") === "browser" ? "chromium" : "jsdom (browserless)";
99
+ const engineLabel = runEngine(audits) === "browser" ? "chromium" : "jsdom (browserless)";
96
100
  return [render(ctx, [{
97
101
  text: "eaa-kit audit",
98
102
  paint: ctx.c.bold
@@ -120,7 +124,7 @@ function pageSection(audit, ctx) {
120
124
  }, { text: audit.error }]), "");
121
125
  return lines;
122
126
  }
123
- for (const finding of sortByImpact(audit.violations)) lines.push(...violationLines(finding, ctx));
127
+ for (const finding of [...audit.violations].sort(byImpactThenRule)) lines.push(...violationLines(finding, ctx));
124
128
  for (const finding of audit.incomplete.filter((item) => item.reason === "needs-review")) lines.push(render(ctx, [
125
129
  {
126
130
  text: ` ${ctx.symbol("review")} `,
@@ -145,7 +149,7 @@ function pageSection(audit, ctx) {
145
149
  paint: clean ? ctx.c.green : ctx.c.dim
146
150
  }]));
147
151
  }
148
- for (const finding of sortByImpact(audit.accepted ?? [])) {
152
+ for (const finding of [...audit.accepted ?? []].sort(byImpactThenRule)) {
149
153
  const elements = count(finding.nodes.length, "element");
150
154
  lines.push(line(ctx, ` · ${finding.ruleId} accepted by the baseline (${elements})`, ctx.c.dim));
151
155
  }
@@ -157,7 +161,7 @@ function coverageLine(audit, ctx) {
157
161
  return line(ctx, ` ${coverageParts(audit).join(" · ")}`, ctx.c.dim);
158
162
  }
159
163
  function violationLines(finding, ctx) {
160
- const impact = finding.impact ?? "unknown";
164
+ const impact = impactLabel(finding.impact);
161
165
  const lines = [render(ctx, [
162
166
  {
163
167
  text: ` ${ctx.symbol("violation")} `,
@@ -181,33 +185,31 @@ function violationLines(finding, ctx) {
181
185
  return lines;
182
186
  }
183
187
  function summary(audits, ctx) {
184
- const withViolations = audits.filter((audit) => audit.violations.length > 0);
185
- const errored = audits.filter((audit) => audit.error);
186
- const ruleCount = audits.reduce((total, audit) => total + audit.violations.length, 0);
187
- const elementCount = audits.reduce((total, audit) => total + audit.violations.reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
188
+ const totals = buildSummary(audits, ctx.failOn);
188
189
  const reviewCount = countRules(audits, "needs-review");
189
190
  const pages = count(audits.length, "page");
191
+ const audited = totals.pages - totals.pagesNotAudited;
190
192
  const lines = [line(ctx, "Summary", ctx.c.bold), ...completenessLines(ctx)];
191
- if (ruleCount === 0) {
192
- const clean = ctx.completeness && !ctx.completeness.complete ? ` No violations across the ${pages} that were audited.` : ` No violations across ${pages}.`;
193
+ if (totals.violations === 0 && audited === 0 && totals.pagesNotAudited > 0) lines.push(line(ctx, " Nothing was audited: no page could be read.", ctx.c.red));
194
+ else if (totals.violations === 0) {
195
+ const clean = totals.pagesNotAudited > 0 || ctx.completeness && !ctx.completeness.complete ? ` No violations across the ${count(audited, "page")} that were audited.` : ` No violations across ${pages}.`;
193
196
  lines.push(line(ctx, clean, ctx.c.green));
194
197
  } else {
195
198
  lines.push(render(ctx, [{
196
- text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${pages}`,
199
+ text: ` ${count(totals.violations, "violation")} on ${totals.pagesWithViolations} of ${count(audited, "page")}`,
197
200
  paint: ctx.c.red
198
201
  }, {
199
- text: ` (${count(elementCount, "element")})`,
202
+ text: ` (${count(totals.violatingElements, "element")})`,
200
203
  paint: ctx.c.dim
201
204
  }]));
202
- lines.push(thresholdLine(audits, ctx));
205
+ lines.push(thresholdLine(totals.failing, ctx));
203
206
  }
204
207
  if (reviewCount > 0) {
205
208
  const verb = reviewCount === 1 ? "needs" : "need";
206
209
  lines.push(line(ctx, ` ${count(reviewCount, "rule")} ${verb} manual review`, ctx.c.yellow));
207
210
  }
208
- if (errored.length > 0) lines.push(line(ctx, ` ${count(errored.length, "page")} could not be audited`, ctx.c.red));
209
- const accepted = audits.reduce((total, audit) => total + (audit.accepted ?? []).reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
210
- if (accepted > 0) lines.push(line(ctx, ` ${count(accepted, "element")} accepted by the baseline, not counted above`, ctx.c.dim));
211
+ if (totals.pagesNotAudited > 0) lines.push(line(ctx, ` ${count(totals.pagesNotAudited, "page")} could not be audited`, ctx.c.red));
212
+ if (totals.accepted > 0) lines.push(line(ctx, ` ${count(totals.accepted, "element")} accepted by the baseline, not counted above`, ctx.c.dim));
211
213
  lines.push(...blindSection(audits, ctx));
212
214
  lines.push(...coverageSection(audits, ctx));
213
215
  return lines;
@@ -221,12 +223,14 @@ function summary(audits, ctx) {
221
223
  * it could be read as boilerplate.
222
224
  */
223
225
  function coverageSection(audits, ctx) {
224
- const coverage = buildCoverage(audits);
226
+ const coverage = buildCoverage(audits, void 0, ctx.review);
225
227
  const lines = ["", ...wrap(coverageSummary(coverage), ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim))];
226
228
  if (coverage.browserWouldAnswer > 0) {
227
229
  const verb = coverage.browserWouldAnswer === 1 ? "criterion" : "criteria";
228
230
  lines.push(line(ctx, ` --browser would answer ${coverage.browserWouldAnswer} more ${verb}.`, ctx.c.dim));
229
231
  }
232
+ const review = reviewSummary(coverage);
233
+ if (review !== void 0) lines.push(...wrap(review, ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim)));
230
234
  if (!ctx.coverage) {
231
235
  lines.push(line(ctx, " Run with --coverage for the criterion-by-criterion breakdown.", ctx.c.dim));
232
236
  return lines;
@@ -246,6 +250,8 @@ function coverageSection(audits, ctx) {
246
250
  paint
247
251
  }
248
252
  ]));
253
+ const recorded = criterion.review;
254
+ if (recorded !== void 0) lines.push(line(ctx, ` ${reviewSentence(recorded)}`, ctx.c.dim));
249
255
  }
250
256
  return lines;
251
257
  }
@@ -272,7 +278,8 @@ function completenessLines(ctx) {
272
278
  const lines = [];
273
279
  if (completeness.unreachable.length > 0) {
274
280
  const noun = plural(completeness.unreachable.length, "page");
275
- lines.push(line(ctx, ` ${completeness.unreachable.length} ${noun} could not be reached, and were not audited`, ctx.c.yellow));
281
+ const verb = completeness.unreachable.length === 1 ? "was" : "were";
282
+ lines.push(line(ctx, ` ${completeness.unreachable.length} ${noun} could not be reached, and ${verb} not audited`, ctx.c.yellow));
276
283
  }
277
284
  if (completeness.truncated) lines.push(line(ctx, " The run stopped at its page limit; the site has more", ctx.c.yellow));
278
285
  if (lines.length > 0) lines.push(line(ctx, " This report describes what was audited, not the whole site.", ctx.c.dim));
@@ -282,9 +289,8 @@ function completenessLines(ctx) {
282
289
  * Why the run passed or failed. Without this, a build that exits 0 while the
283
290
  * report lists violations looks like a bug rather than a threshold choice.
284
291
  */
285
- function thresholdLine(audits, ctx) {
292
+ function thresholdLine(failing, ctx) {
286
293
  const failOn = ctx.failOn;
287
- const failing = countAtOrAbove(audits, failOn);
288
294
  if (failing === 0) return line(ctx, ` none at or above ${failOn} (--fail-on ${failOn}), so this run passes`, ctx.c.green);
289
295
  return line(ctx, ` ${failing} at or above ${failOn} (--fail-on ${failOn})`, ctx.c.red);
290
296
  }
@@ -329,9 +335,6 @@ function countRules(audits, reason) {
329
335
  for (const audit of audits) for (const finding of audit.incomplete) if (finding.reason === reason) ruleIds.add(finding.ruleId);
330
336
  return ruleIds.size;
331
337
  }
332
- function sortByImpact(findings) {
333
- return [...findings].sort((a, b) => impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId));
334
- }
335
338
  function criteria(finding) {
336
339
  return finding.successCriteria.length > 0 ? `, WCAG ${finding.successCriteria.join(" ")}` : "";
337
340
  }
@@ -347,8 +350,7 @@ function criteria(finding) {
347
350
  function issuesSection(audits, ctx) {
348
351
  const issues = groupIssues(audits);
349
352
  if (issues.length === 0) return [];
350
- const elements = issues.reduce((total, issue) => total + issue.elements.length, 0);
351
- const occurrences = issues.reduce((total, issue) => total + issue.occurrences, 0);
353
+ const { elements, occurrences } = issueTotals(issues);
352
354
  const lines = [line(ctx, "Issues", ctx.c.bold)];
353
355
  const intro = occurrences === elements ? ` ${count(elements, "distinct element")} to fix.` : ` ${count(occurrences, "violation")} across the site come from ${count(elements, "distinct element")}.`;
354
356
  lines.push(line(ctx, intro, ctx.c.dim));
@@ -484,6 +486,13 @@ async function resolvePages(directory, options = {}) {
484
486
  return;
485
487
  }
486
488
  if (pages.length === 0) {
489
+ if ((options.include !== void 0 || options.exclude !== void 0) && await holdsHtml(directory)) {
490
+ warn(`No page in ${shown} matched the filters, so nothing was audited.`);
491
+ if (options.include !== void 0) note(` --include ${options.include.join(" ")}`);
492
+ if (options.exclude !== void 0) note(` --exclude ${options.exclude.join(" ")}`);
493
+ note(" Patterns are relative to the audited directory, with POSIX separators.");
494
+ return;
495
+ }
487
496
  warn(await emptyDirectoryHint(shown, cwd));
488
497
  return;
489
498
  }
@@ -495,6 +504,7 @@ async function resolvePages(directory, options = {}) {
495
504
  }
496
505
  return {
497
506
  pages,
507
+ directory,
498
508
  label: shown,
499
509
  completeness: {
500
510
  discovery: "directory",
@@ -511,7 +521,7 @@ async function resolvePages(directory, options = {}) {
511
521
  * exit 2 — a run that reached no verdict, not a clean one.
512
522
  */
513
523
  async function crawlPages(url, options) {
514
- const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BiI1Lau_.js");
524
+ const { collapsedOnto, crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BHkJmUkA.js");
515
525
  let entry;
516
526
  try {
517
527
  entry = parseEntryUrl(url, options.allowRemote ?? false);
@@ -529,7 +539,8 @@ async function crawlPages(url, options) {
529
539
  ...options.sitemap === void 0 ? {} : { sitemap: options.sitemap },
530
540
  ...options.maxPages === void 0 ? {} : { maxPages: options.maxPages },
531
541
  ...options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth },
532
- ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
542
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
543
+ ...options.headers === void 0 ? {} : { headers: options.headers }
533
544
  });
534
545
  if (result.pages.length === 0 && result.failures.length > 0) {
535
546
  fail(`Could not fetch ${entry.href} (${result.failures[0]?.reason})`);
@@ -545,16 +556,26 @@ async function crawlPages(url, options) {
545
556
  if (result.failures.length > 10) note(` …and ${result.failures.length - 10} more`);
546
557
  }
547
558
  if (result.truncated) warn(`Stopped at ${count(result.pages.length, "page")}; the site has more. Raise --max-pages to go further.`);
559
+ const collapsed = collapsedOnto(result);
560
+ if (collapsed.length > 0) {
561
+ const landedOn = collapsed[0]?.landedOn ?? "";
562
+ warn(`Every page requested answered at ${landedOn}, which is not where it was asked.`);
563
+ note(" A sign-in page in front of the site looks like this.");
564
+ note(" If it is behind one, pass --basic-auth user:password or --header \"Cookie: …\".");
565
+ }
548
566
  return {
549
567
  pages: result.pages,
550
568
  origin: result.origin,
551
569
  completeness: {
552
570
  discovery: result.discovery,
553
571
  collected: result.pages.length,
554
- unreachable: result.failures.map((failure) => ({
572
+ unreachable: [...result.failures.map((failure) => ({
555
573
  location: failure.url,
556
574
  reason: failure.reason
557
- })),
575
+ })), ...collapsed.map((redirect) => ({
576
+ location: redirect.requested,
577
+ reason: `answered at ${redirect.landedOn} instead, so this page was not audited`
578
+ }))],
558
579
  truncated: result.truncated
559
580
  }
560
581
  };
@@ -609,22 +630,26 @@ const OUTPUT_FORMATS = [
609
630
  * piped somewhere without the chatter coming along.
610
631
  */
611
632
  async function runAuditCommand(dir, options = {}) {
633
+ if (options.fast && options.browser) warn("--fast has no effect with --browser: a real browser can decide those rules.");
612
634
  const resolved = await resolvePages(dir, options);
613
635
  if (!resolved) return {
614
636
  audits: [],
615
637
  exitCode: 2
616
638
  };
617
- const { pages, origin, label, cleanup, completeness: collection } = resolved;
639
+ const { pages, origin, label, cleanup, directory, completeness: collection } = resolved;
640
+ if (options.headers !== void 0 && directory !== void 0 && !options.browser) warn("--header and --basic-auth apply to pages that are fetched; this run read files.");
618
641
  try {
619
642
  note(`Auditing ${count(pages.length, "page")} in ${label}${await describeEngine(pages, options)}…`);
620
643
  const baseUrl = options.baseUrl ?? origin;
621
- let audits = await runEngine(pages, {
644
+ let audits = await runEngine$1(pages, {
622
645
  cwd: options.cwd ?? process.cwd(),
646
+ ...options.headers === void 0 ? {} : { headers: options.headers },
623
647
  ...baseUrl === void 0 ? {} : { baseUrl },
624
648
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
625
649
  ...options.browser ? { browser: true } : {},
650
+ ...options.fast && !options.browser ? { fast: true } : {},
626
651
  ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
627
- ...options.url === void 0 && dir !== void 0 ? { directory: dir } : {}
652
+ ...directory === void 0 ? {} : { directory }
628
653
  });
629
654
  if (!audits) return {
630
655
  audits: [],
@@ -640,7 +665,12 @@ async function runAuditCommand(dir, options = {}) {
640
665
  audits = applied;
641
666
  }
642
667
  const completeness = runCompleteness(audits, collection);
643
- await emit(audits, label, failOn, completeness, options);
668
+ const review = await loadReview(options);
669
+ if (review === FAILED) return {
670
+ audits,
671
+ exitCode: 2
672
+ };
673
+ await emit(audits, label, failOn, completeness, options, review);
644
674
  const unaudited = audits.filter((audit) => audit.error);
645
675
  if (unaudited.length > 0) {
646
676
  fail(`${unaudited.length} of ${audits.length} pages could not be audited`);
@@ -666,7 +696,7 @@ async function runAuditCommand(dir, options = {}) {
666
696
  * would be as wrong as silently passing.
667
697
  */
668
698
  async function acceptBaseline(audits, options) {
669
- const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-22Y1NWxM.js");
699
+ const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-BscHfFSW.js");
670
700
  try {
671
701
  const outcome = applyBaseline(audits, await readBaseline(options.baseline, options.cwd ?? process.cwd()));
672
702
  if (outcome.accepted > 0) note(`Baseline accepted ${outcome.accepted} violating elements`);
@@ -684,6 +714,32 @@ async function acceptBaseline(audits, options) {
684
714
  throw cause;
685
715
  }
686
716
  }
717
+ /** Distinguishes "no review asked for" from "the review could not be read". */
718
+ const FAILED = Symbol("review-failed");
719
+ /**
720
+ * Read the review record, when one was asked for.
721
+ *
722
+ * Returns undefined when no `--review` was given, which is the ordinary case,
723
+ * and the sentinel when one was given and could not be read.
724
+ */
725
+ async function loadReview(options) {
726
+ if (options.review === void 0) return void 0;
727
+ const { answeredCount, readReview, ReviewError } = await import("./review-DIbOTmOM.js");
728
+ try {
729
+ const record = await readReview(options.review, options.cwd ?? process.cwd());
730
+ note(`Review record: ${count(answeredCount(record), "criterion")} answered`);
731
+ return {
732
+ record,
733
+ ...options.reviewMaxAge === void 0 ? {} : { maxAgeDays: options.reviewMaxAge }
734
+ };
735
+ } catch (cause) {
736
+ if (cause instanceof ReviewError) {
737
+ fail(cause.message);
738
+ return FAILED;
739
+ }
740
+ throw cause;
741
+ }
742
+ }
687
743
  /**
688
744
  * What the progress line says about the engine.
689
745
  *
@@ -693,47 +749,51 @@ async function acceptBaseline(audits, options) {
693
749
  */
694
750
  async function describeEngine(pages, options) {
695
751
  if (options.browser) return " with Chromium";
696
- const { plannedWorkers } = await import("./pool-BMevaLWD.js");
752
+ const { plannedWorkers } = await import("./pool-dkexq5xo.js");
697
753
  const workers = options.concurrency ?? plannedWorkers(pages);
698
- return workers > 1 ? ` across ${workers} threads` : "";
754
+ const threads = workers > 1 ? ` across ${workers} threads` : "";
755
+ return options.fast ? `${threads}, skipping what this engine cannot decide` : threads;
699
756
  }
700
757
  /**
701
758
  * Emit the chosen format, to a file when --output is given and to stdout
702
759
  * otherwise. Colour is dropped when writing to a file, since escape codes in a
703
760
  * saved report are noise.
704
761
  */
705
- async function emit(audits, dir, failOn, completeness, options) {
706
- const body = await renderReport(audits, dir, failOn, completeness, options.format ?? "console", typeof options.output === "string", options);
762
+ async function emit(audits, dir, failOn, completeness, options, review) {
763
+ const body = await renderReport(audits, dir, failOn, completeness, options.format ?? "console", typeof options.output === "string", options, review);
707
764
  await emitDocument(body, options.output, options.cwd ?? process.cwd());
708
765
  if (options.output !== void 0) note(`Report written to ${options.output}`);
709
766
  }
710
- async function renderReport(audits, dir, failOn, completeness, format, toFile, options) {
767
+ async function renderReport(audits, dir, failOn, completeness, format, toFile, options, review) {
711
768
  switch (format) {
712
769
  case "json": {
713
- const { buildJsonReport, serialiseJsonReport } = await import("./json-QQuFIw1W.js");
770
+ const { buildJsonReport, serialiseJsonReport } = await import("./json-HJDgJp09.js");
714
771
  return serialiseJsonReport(buildJsonReport(audits, {
715
772
  directory: dir,
716
773
  ...options.url === void 0 ? {} : { sourceKind: "url" },
717
774
  failOn,
718
775
  completeness,
776
+ ...review === void 0 ? {} : { review },
719
777
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
720
778
  }));
721
779
  }
722
780
  case "sarif": {
723
- const { buildSarifReport, serialiseSarifReport } = await import("./sarif-SR3_lLYd.js");
781
+ const { buildSarifReport, serialiseSarifReport } = await import("./sarif-BKvq_AUm.js");
724
782
  return serialiseSarifReport(buildSarifReport(audits, {
725
783
  directory: dir,
726
- completeness
784
+ completeness,
785
+ ...review === void 0 ? {} : { review }
727
786
  }));
728
787
  }
729
788
  case "html": {
730
- const { buildHtmlReport } = await import("./html-BGTO3ypW.js");
789
+ const { buildHtmlReport } = await import("./html-Bmh7X_sM.js");
731
790
  const framework = await detectedFramework(options.cwd ?? process.cwd());
732
791
  return buildHtmlReport(audits, {
733
- ...await sourceLookups(options.cwd ?? process.cwd()),
792
+ ...await sourceLookups(options.cwd ?? process.cwd(), audits),
734
793
  directory: dir,
735
794
  failOn,
736
795
  completeness,
796
+ ...review === void 0 ? {} : { review },
737
797
  ...framework === void 0 ? {} : { framework },
738
798
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
739
799
  });
@@ -741,10 +801,11 @@ async function renderReport(audits, dir, failOn, completeness, format, toFile, o
741
801
  case "console": {
742
802
  const framework = await detectedFramework(options.cwd ?? process.cwd());
743
803
  return `${formatConsoleReport(audits, {
744
- ...await sourceLookups(options.cwd ?? process.cwd()),
804
+ ...await sourceLookups(options.cwd ?? process.cwd(), audits),
745
805
  dir,
746
806
  failOn,
747
807
  completeness,
808
+ ...review === void 0 ? {} : { review },
748
809
  ...options.perPage ? { perPage: true } : {},
749
810
  ...options.manual ? { manual: true } : {},
750
811
  ...options.coverage ? { coverage: true } : {},
@@ -765,15 +826,23 @@ async function detectedFramework(cwd) {
765
826
  const { readPackageJson } = await import("./project-MFrXcw1M.js");
766
827
  return (await detectFramework(cwd, await readPackageJson(cwd)))?.framework.id;
767
828
  }
768
- async function sourceLookups(cwd) {
829
+ async function sourceLookups(cwd, audits) {
769
830
  const { buildRouteMap, sourceFor } = await import("./routes-CmdRUuOs.js");
770
- const { buildComponentIndex, componentFor } = await import("./component-DKd3EHOg.js");
771
831
  const routes = await buildRouteMap(cwd);
832
+ if (!hasElementsToAttribute(audits)) return {
833
+ sourceFor: (page) => sourceFor(routes, page),
834
+ componentFor: () => void 0
835
+ };
836
+ const { buildComponentIndex, componentFor } = await import("./component-DcCn3BJS.js");
772
837
  const components = await buildComponentIndex(cwd);
773
838
  return {
774
839
  sourceFor: (page) => sourceFor(routes, page),
775
840
  componentFor: (html) => componentFor(components, html)
776
841
  };
777
842
  }
843
+ /** Whether any element will be listed, and so looked up against the source. */
844
+ function hasElementsToAttribute(audits) {
845
+ return audits.some((audit) => audit.violations.length > 0 || (audit.accepted?.length ?? 0) > 0);
846
+ }
778
847
  //#endregion
779
848
  export { runAuditCommand as n, resolvePages as r, OUTPUT_FORMATS as t };
@@ -0,0 +1,2 @@
1
+ import { o as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-D2qWmUC6.js";
2
+ export { BaselineError, applyBaseline, readBaseline };