eaa-kit 0.5.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 (52) hide show
  1. package/README.md +35 -2
  2. package/dist/astro/index.d.ts +1 -1
  3. package/dist/astro/index.js +1 -1
  4. package/dist/audit/runners/worker.js +1 -1
  5. package/dist/audit-DK2StT1_.js +2 -0
  6. package/dist/{audit-B282A3EA.js → audit-e_uixhPG.js} +95 -48
  7. package/dist/baseline-BscHfFSW.js +2 -0
  8. package/dist/{baseline-CuAS2lMk.js → baseline-D2qWmUC6.js} +36 -9
  9. package/dist/checklist-BtgkxMEa.js +44 -0
  10. package/dist/cli/index.js +111 -14
  11. package/dist/{command-C3D7JWn6.js → command-CZKp9Wi_.js} +101 -10
  12. package/dist/{remediation-CMBIrnpN.js → completeness-Dwq3CPB3.js} +60 -42
  13. package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
  14. package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
  15. package/dist/coverage-7bQqWNfN.js +2 -0
  16. package/dist/{coverage-B_Y6l-Ra.js → coverage-BD2_CYns.js} +44 -7
  17. package/dist/{crawl-BUWPgaGW.js → crawl-BHkJmUkA.js} +42 -8
  18. package/dist/eleventy/index.d.ts +1 -1
  19. package/dist/eleventy/index.js +1 -1
  20. package/dist/{html-C4vktg0A.js → html-Bmh7X_sM.js} +52 -30
  21. package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
  22. package/dist/index.d.ts +68 -1
  23. package/dist/index.js +3 -3
  24. package/dist/{init-CW7LfGT5.js → init-BjKYttxc.js} +3 -3
  25. package/dist/{jsdom-B--cEH-G.js → jsdom-CQar6OQS.js} +1 -1
  26. package/dist/jsdom-DMvU2h-f.js +3 -0
  27. package/dist/{json-D_Mnnft5.js → json-HJDgJp09.js} +1 -1
  28. package/dist/{json-DROX33kh.js → json-PNg6BeXp.js} +11 -16
  29. package/dist/load-Bc_xDGAh.js +2 -0
  30. package/dist/{load-yAR4wzez.js → load-vZdV_fD1.js} +5 -1
  31. package/dist/nuxt/index.d.ts +1 -1
  32. package/dist/nuxt/index.js +1 -1
  33. package/dist/{playwright-BojtYVUa.js → playwright-C-miJP2b.js} +3 -2
  34. package/dist/{pool-BO25OIez.js → pool-dkexq5xo.js} +2 -2
  35. package/dist/{render-DbGOVmhx.js → render-CtsqR0k-.js} +105 -4
  36. package/dist/{result-BWcYXeRs.js → result-DfUru6y_.js} +24 -1
  37. package/dist/review-CdMK2GpQ.js +161 -0
  38. package/dist/review-DIbOTmOM.js +2 -0
  39. package/dist/{run-C2nKFcb-.js → run-D7hdVXLA.js} +1 -1
  40. package/dist/{run-CtcEUhbe.d.ts → run-YiqyQA2b.d.ts} +10 -0
  41. package/dist/{sarif-B-UBcVu8.js → sarif-BKvq_AUm.js} +23 -12
  42. package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
  43. package/dist/vite/index.d.ts +1 -1
  44. package/dist/vite/index.js +1 -1
  45. package/dist/webpack/index.d.ts +1 -1
  46. package/dist/webpack/index.js +1 -1
  47. package/dist/worksheet-eUjwYLxC.js +63 -0
  48. package/package.json +2 -2
  49. package/dist/audit-DyHPmGeD.js +0 -2
  50. package/dist/baseline-0u4df12v.js +0 -2
  51. package/dist/jsdom-22Bkt65v.js +0 -3
  52. package/dist/load-CFq2VQtT.js +0 -2
package/README.md CHANGED
@@ -9,11 +9,18 @@ in the DACH region — the BFSG in Germany, the BaFG in Austria — and the stat
9
9
  the statute and supervisory body of **seven countries**: Austria, Germany, Switzerland,
10
10
  Spain, France, Italy and the Netherlands, each in its own language as well as English.
11
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.
17
+
12
18
  ```bash
13
19
  npx eaa-kit audit # WCAG 2.2 AA report; finds your build itself
14
20
  npx eaa-kit diff a.json b.json # what a change made worse, and what it fixed
15
21
  npx eaa-kit init # write an eaa.config.json
16
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
17
24
  ```
18
25
 
19
26
  > **Not legal advice.** eaa-kit reports what an automated engine can and cannot determine
@@ -95,6 +102,17 @@ could invent a fix that looks right and is not.
95
102
  written in src/components/Header.astro:12
96
103
  ```
97
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
+
98
116
  **Says how much of WCAG it could reach.** WCAG 2.2 has 55 success criteria at Levels A and
99
117
  AA. axe-core has rules touching 23 of them. Every run says so, and `--coverage` lists all
100
118
  55 with what this run reached on each — never as a percentage, because most of WCAG cannot
@@ -133,6 +151,16 @@ that instead — following the site's own sitemap, wherever it keeps it.
133
151
  eaa-kit audit --url http://localhost:8000 --sitemap /sitemap_index.xml
134
152
  ```
135
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
+
136
164
  ## Documentation
137
165
 
138
166
  | | |
@@ -143,6 +171,7 @@ eaa-kit audit --url http://localhost:8000 --sitemap /sitemap_index.xml
143
171
  | [Baselines](docs/baseline.md) | Adopting the tool on a site that already has violations |
144
172
  | [Comparing two runs](docs/reports.md#comparing-two-runs) | The `diff` command, and what it refuses to call fixed |
145
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 |
146
175
  | [Report formats](docs/reports.md) | The JSON contract, SARIF, and the HTML report |
147
176
  | [Integrations](docs/integrations.md) | The build plugins, the GitHub Action, and the two builders that get a command instead |
148
177
 
@@ -169,11 +198,15 @@ you](docs/audit.md#what-the-browserless-engine-can-and-cannot-tell-you).
169
198
 
170
199
  The same applies to the statement: it says what you told it. eaa-kit cannot check whether
171
200
  those claims are true, and a statement claiming full conformance for a site that is not
172
- 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.
173
205
 
174
206
  ## Changelog
175
207
 
176
- 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.
177
210
 
178
211
  ## License
179
212
 
@@ -1,4 +1,4 @@
1
- import { n as IntegrationOptions } from "../run-CtcEUhbe.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-C2nKFcb-.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,4 @@
1
- import { t as auditPage } from "../../jsdom-B--cEH-G.js";
1
+ import { t as auditPage } from "../../jsdom-CQar6OQS.js";
2
2
  import { enableCompileCache } from "node:module";
3
3
  import { parentPort, workerData } from "node:worker_threads";
4
4
  //#region src/audit/runners/worker.ts
@@ -0,0 +1,2 @@
1
+ import { n as runAuditCommand } from "./audit-e_uixhPG.js";
2
+ export { runAuditCommand };
@@ -1,9 +1,12 @@
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";
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";
7
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";
@@ -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,35 +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");
190
- const audited = audits.length - errored.length;
191
+ const audited = totals.pages - totals.pagesNotAudited;
191
192
  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}.`;
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}.`;
195
196
  lines.push(line(ctx, clean, ctx.c.green));
196
197
  } else {
197
198
  lines.push(render(ctx, [{
198
- text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${count(audited, "page")}`,
199
+ text: ` ${count(totals.violations, "violation")} on ${totals.pagesWithViolations} of ${count(audited, "page")}`,
199
200
  paint: ctx.c.red
200
201
  }, {
201
- text: ` (${count(elementCount, "element")})`,
202
+ text: ` (${count(totals.violatingElements, "element")})`,
202
203
  paint: ctx.c.dim
203
204
  }]));
204
- lines.push(thresholdLine(audits, ctx));
205
+ lines.push(thresholdLine(totals.failing, ctx));
205
206
  }
206
207
  if (reviewCount > 0) {
207
208
  const verb = reviewCount === 1 ? "needs" : "need";
208
209
  lines.push(line(ctx, ` ${count(reviewCount, "rule")} ${verb} manual review`, ctx.c.yellow));
209
210
  }
210
- if (errored.length > 0) lines.push(line(ctx, ` ${count(errored.length, "page")} could not be audited`, ctx.c.red));
211
- const accepted = audits.reduce((total, audit) => total + (audit.accepted ?? []).reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
212
- 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));
213
213
  lines.push(...blindSection(audits, ctx));
214
214
  lines.push(...coverageSection(audits, ctx));
215
215
  return lines;
@@ -223,12 +223,14 @@ function summary(audits, ctx) {
223
223
  * it could be read as boilerplate.
224
224
  */
225
225
  function coverageSection(audits, ctx) {
226
- const coverage = buildCoverage(audits);
226
+ const coverage = buildCoverage(audits, void 0, ctx.review);
227
227
  const lines = ["", ...wrap(coverageSummary(coverage), ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim))];
228
228
  if (coverage.browserWouldAnswer > 0) {
229
229
  const verb = coverage.browserWouldAnswer === 1 ? "criterion" : "criteria";
230
230
  lines.push(line(ctx, ` --browser would answer ${coverage.browserWouldAnswer} more ${verb}.`, ctx.c.dim));
231
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)));
232
234
  if (!ctx.coverage) {
233
235
  lines.push(line(ctx, " Run with --coverage for the criterion-by-criterion breakdown.", ctx.c.dim));
234
236
  return lines;
@@ -248,6 +250,8 @@ function coverageSection(audits, ctx) {
248
250
  paint
249
251
  }
250
252
  ]));
253
+ const recorded = criterion.review;
254
+ if (recorded !== void 0) lines.push(line(ctx, ` ${reviewSentence(recorded)}`, ctx.c.dim));
251
255
  }
252
256
  return lines;
253
257
  }
@@ -285,9 +289,8 @@ function completenessLines(ctx) {
285
289
  * Why the run passed or failed. Without this, a build that exits 0 while the
286
290
  * report lists violations looks like a bug rather than a threshold choice.
287
291
  */
288
- function thresholdLine(audits, ctx) {
292
+ function thresholdLine(failing, ctx) {
289
293
  const failOn = ctx.failOn;
290
- const failing = countAtOrAbove(audits, failOn);
291
294
  if (failing === 0) return line(ctx, ` none at or above ${failOn} (--fail-on ${failOn}), so this run passes`, ctx.c.green);
292
295
  return line(ctx, ` ${failing} at or above ${failOn} (--fail-on ${failOn})`, ctx.c.red);
293
296
  }
@@ -332,9 +335,6 @@ function countRules(audits, reason) {
332
335
  for (const audit of audits) for (const finding of audit.incomplete) if (finding.reason === reason) ruleIds.add(finding.ruleId);
333
336
  return ruleIds.size;
334
337
  }
335
- function sortByImpact(findings) {
336
- return [...findings].sort((a, b) => impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId));
337
- }
338
338
  function criteria(finding) {
339
339
  return finding.successCriteria.length > 0 ? `, WCAG ${finding.successCriteria.join(" ")}` : "";
340
340
  }
@@ -350,8 +350,7 @@ function criteria(finding) {
350
350
  function issuesSection(audits, ctx) {
351
351
  const issues = groupIssues(audits);
352
352
  if (issues.length === 0) return [];
353
- const elements = issues.reduce((total, issue) => total + issue.elements.length, 0);
354
- const occurrences = issues.reduce((total, issue) => total + issue.occurrences, 0);
353
+ const { elements, occurrences } = issueTotals(issues);
355
354
  const lines = [line(ctx, "Issues", ctx.c.bold)];
356
355
  const intro = occurrences === elements ? ` ${count(elements, "distinct element")} to fix.` : ` ${count(occurrences, "violation")} across the site come from ${count(elements, "distinct element")}.`;
357
356
  lines.push(line(ctx, intro, ctx.c.dim));
@@ -522,7 +521,7 @@ async function resolvePages(directory, options = {}) {
522
521
  * exit 2 — a run that reached no verdict, not a clean one.
523
522
  */
524
523
  async function crawlPages(url, options) {
525
- const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BUWPgaGW.js");
524
+ const { collapsedOnto, crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BHkJmUkA.js");
526
525
  let entry;
527
526
  try {
528
527
  entry = parseEntryUrl(url, options.allowRemote ?? false);
@@ -540,7 +539,8 @@ async function crawlPages(url, options) {
540
539
  ...options.sitemap === void 0 ? {} : { sitemap: options.sitemap },
541
540
  ...options.maxPages === void 0 ? {} : { maxPages: options.maxPages },
542
541
  ...options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth },
543
- ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
542
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
543
+ ...options.headers === void 0 ? {} : { headers: options.headers }
544
544
  });
545
545
  if (result.pages.length === 0 && result.failures.length > 0) {
546
546
  fail(`Could not fetch ${entry.href} (${result.failures[0]?.reason})`);
@@ -556,16 +556,26 @@ async function crawlPages(url, options) {
556
556
  if (result.failures.length > 10) note(` …and ${result.failures.length - 10} more`);
557
557
  }
558
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
+ }
559
566
  return {
560
567
  pages: result.pages,
561
568
  origin: result.origin,
562
569
  completeness: {
563
570
  discovery: result.discovery,
564
571
  collected: result.pages.length,
565
- unreachable: result.failures.map((failure) => ({
572
+ unreachable: [...result.failures.map((failure) => ({
566
573
  location: failure.url,
567
574
  reason: failure.reason
568
- })),
575
+ })), ...collapsed.map((redirect) => ({
576
+ location: redirect.requested,
577
+ reason: `answered at ${redirect.landedOn} instead, so this page was not audited`
578
+ }))],
569
579
  truncated: result.truncated
570
580
  }
571
581
  };
@@ -627,11 +637,13 @@ async function runAuditCommand(dir, options = {}) {
627
637
  exitCode: 2
628
638
  };
629
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.");
630
641
  try {
631
642
  note(`Auditing ${count(pages.length, "page")} in ${label}${await describeEngine(pages, options)}…`);
632
643
  const baseUrl = options.baseUrl ?? origin;
633
- let audits = await runEngine(pages, {
644
+ let audits = await runEngine$1(pages, {
634
645
  cwd: options.cwd ?? process.cwd(),
646
+ ...options.headers === void 0 ? {} : { headers: options.headers },
635
647
  ...baseUrl === void 0 ? {} : { baseUrl },
636
648
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
637
649
  ...options.browser ? { browser: true } : {},
@@ -653,7 +665,12 @@ async function runAuditCommand(dir, options = {}) {
653
665
  audits = applied;
654
666
  }
655
667
  const completeness = runCompleteness(audits, collection);
656
- 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);
657
674
  const unaudited = audits.filter((audit) => audit.error);
658
675
  if (unaudited.length > 0) {
659
676
  fail(`${unaudited.length} of ${audits.length} pages could not be audited`);
@@ -679,7 +696,7 @@ async function runAuditCommand(dir, options = {}) {
679
696
  * would be as wrong as silently passing.
680
697
  */
681
698
  async function acceptBaseline(audits, options) {
682
- const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-0u4df12v.js");
699
+ const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-BscHfFSW.js");
683
700
  try {
684
701
  const outcome = applyBaseline(audits, await readBaseline(options.baseline, options.cwd ?? process.cwd()));
685
702
  if (outcome.accepted > 0) note(`Baseline accepted ${outcome.accepted} violating elements`);
@@ -697,6 +714,32 @@ async function acceptBaseline(audits, options) {
697
714
  throw cause;
698
715
  }
699
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
+ }
700
743
  /**
701
744
  * What the progress line says about the engine.
702
745
  *
@@ -706,7 +749,7 @@ async function acceptBaseline(audits, options) {
706
749
  */
707
750
  async function describeEngine(pages, options) {
708
751
  if (options.browser) return " with Chromium";
709
- const { plannedWorkers } = await import("./pool-BO25OIez.js");
752
+ const { plannedWorkers } = await import("./pool-dkexq5xo.js");
710
753
  const workers = options.concurrency ?? plannedWorkers(pages);
711
754
  const threads = workers > 1 ? ` across ${workers} threads` : "";
712
755
  return options.fast ? `${threads}, skipping what this engine cannot decide` : threads;
@@ -716,38 +759,41 @@ async function describeEngine(pages, options) {
716
759
  * otherwise. Colour is dropped when writing to a file, since escape codes in a
717
760
  * saved report are noise.
718
761
  */
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);
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);
721
764
  await emitDocument(body, options.output, options.cwd ?? process.cwd());
722
765
  if (options.output !== void 0) note(`Report written to ${options.output}`);
723
766
  }
724
- async function renderReport(audits, dir, failOn, completeness, format, toFile, options) {
767
+ async function renderReport(audits, dir, failOn, completeness, format, toFile, options, review) {
725
768
  switch (format) {
726
769
  case "json": {
727
- const { buildJsonReport, serialiseJsonReport } = await import("./json-D_Mnnft5.js");
770
+ const { buildJsonReport, serialiseJsonReport } = await import("./json-HJDgJp09.js");
728
771
  return serialiseJsonReport(buildJsonReport(audits, {
729
772
  directory: dir,
730
773
  ...options.url === void 0 ? {} : { sourceKind: "url" },
731
774
  failOn,
732
775
  completeness,
776
+ ...review === void 0 ? {} : { review },
733
777
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
734
778
  }));
735
779
  }
736
780
  case "sarif": {
737
- const { buildSarifReport, serialiseSarifReport } = await import("./sarif-B-UBcVu8.js");
781
+ const { buildSarifReport, serialiseSarifReport } = await import("./sarif-BKvq_AUm.js");
738
782
  return serialiseSarifReport(buildSarifReport(audits, {
739
783
  directory: dir,
740
- completeness
784
+ completeness,
785
+ ...review === void 0 ? {} : { review }
741
786
  }));
742
787
  }
743
788
  case "html": {
744
- const { buildHtmlReport } = await import("./html-C4vktg0A.js");
789
+ const { buildHtmlReport } = await import("./html-Bmh7X_sM.js");
745
790
  const framework = await detectedFramework(options.cwd ?? process.cwd());
746
791
  return buildHtmlReport(audits, {
747
792
  ...await sourceLookups(options.cwd ?? process.cwd(), audits),
748
793
  directory: dir,
749
794
  failOn,
750
795
  completeness,
796
+ ...review === void 0 ? {} : { review },
751
797
  ...framework === void 0 ? {} : { framework },
752
798
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
753
799
  });
@@ -759,6 +805,7 @@ async function renderReport(audits, dir, failOn, completeness, format, toFile, o
759
805
  dir,
760
806
  failOn,
761
807
  completeness,
808
+ ...review === void 0 ? {} : { review },
762
809
  ...options.perPage ? { perPage: true } : {},
763
810
  ...options.manual ? { manual: true } : {},
764
811
  ...options.coverage ? { coverage: true } : {},
@@ -786,7 +833,7 @@ async function sourceLookups(cwd, audits) {
786
833
  sourceFor: (page) => sourceFor(routes, page),
787
834
  componentFor: () => void 0
788
835
  };
789
- const { buildComponentIndex, componentFor } = await import("./component-DKd3EHOg.js");
836
+ const { buildComponentIndex, componentFor } = await import("./component-DcCn3BJS.js");
790
837
  const components = await buildComponentIndex(cwd);
791
838
  return {
792
839
  sourceFor: (page) => sourceFor(routes, page),
@@ -0,0 +1,2 @@
1
+ import { o as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-D2qWmUC6.js";
2
+ export { BaselineError, applyBaseline, readBaseline };
@@ -1,6 +1,7 @@
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";
1
+ import { c as nullable, d as optional, h as string, l as number, m as safeParse, o as isoDate, t as array, u as object, y as withDefault } from "./schema-DJSF4K05.js";
2
+ import { a as isoDate$1 } from "./text-BF1LHMiV.js";
2
3
  import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
3
- import { o as findingElements } from "./result-BWcYXeRs.js";
4
+ import { o as findingElements } from "./result-DfUru6y_.js";
4
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
6
  import path from "node:path";
6
7
  /** Default filename, used by the CLI when no path is given. */
@@ -18,7 +19,7 @@ const entrySchema = object({
18
19
  /** ISO date the entry was written. */
19
20
  acceptedOn: withDefault(string(), () => ""),
20
21
  /** ISO date after which this entry stops suppressing anything. */
21
- expiresOn: optional(isoDate$1()),
22
+ expiresOn: optional(isoDate()),
22
23
  /** Why this is being lived with. Free text, for whoever reads the file. */
23
24
  note: optional(string())
24
25
  });
@@ -37,7 +38,7 @@ var BaselineError = class extends Error {
37
38
  * can still report on what the run actually found.
38
39
  */
39
40
  function applyBaseline(audits, baseline, options = {}) {
40
- const today = isoDate(options.today ?? /* @__PURE__ */ new Date());
41
+ const today = isoDate$1(options.today ?? /* @__PURE__ */ new Date());
41
42
  const expired = [];
42
43
  const live = /* @__PURE__ */ new Map();
43
44
  for (const entry of baseline.entries) if (entry.expiresOn !== void 0 && entry.expiresOn < today) expired.push(entry);
@@ -93,9 +94,38 @@ function applyBaseline(audits, baseline, options = {}) {
93
94
  accepted
94
95
  };
95
96
  }
97
+ /**
98
+ * The baseline without the entries this run showed are gone.
99
+ *
100
+ * `applyBaseline` already decides which those are, and does it carefully: an
101
+ * entry counts as stale only when the page it names was audited and the element
102
+ * was not found. Entries for pages a narrowed run never looked at are not stale
103
+ * and are kept here, because from the outside a page nobody audited and a page
104
+ * that no longer exists are the same thing, and guessing wrong empties the file
105
+ * that is holding the rest of the site's build together.
106
+ *
107
+ * Nothing is added. That is the whole difference between this and recording a
108
+ * baseline again: re-recording accepts whatever the site is failing today,
109
+ * which on a bad day is a new barrier nobody agreed to.
110
+ */
111
+ function pruneBaseline(baseline, stale) {
112
+ const gone = new Set(stale.map((entry) => key(entry.page, entry.ruleId, entry.fingerprint)));
113
+ if (gone.size === 0) return {
114
+ baseline,
115
+ removed: []
116
+ };
117
+ const kept = baseline.entries.filter((entry) => !gone.has(key(entry.page, entry.ruleId, entry.fingerprint)));
118
+ return {
119
+ baseline: {
120
+ ...baseline,
121
+ entries: kept
122
+ },
123
+ removed: baseline.entries.filter((entry) => gone.has(key(entry.page, entry.ruleId, entry.fingerprint)))
124
+ };
125
+ }
96
126
  /** Record every violation a run found, so a later run can fail only on new ones. */
97
127
  function buildBaseline(audits, options = {}) {
98
- const acceptedOn = isoDate(options.today ?? /* @__PURE__ */ new Date());
128
+ const acceptedOn = isoDate$1(options.today ?? /* @__PURE__ */ new Date());
99
129
  const entries = [];
100
130
  for (const audit of audits) for (const finding of audit.violations) for (const node of findingElements(finding)) entries.push({
101
131
  page: audit.relativePath,
@@ -153,8 +183,5 @@ function key(page, ruleId, fingerprint) {
153
183
  function byEntry(a, b) {
154
184
  return a.page.localeCompare(b.page) || a.ruleId.localeCompare(b.ruleId) || a.fingerprint.localeCompare(b.fingerprint);
155
185
  }
156
- function isoDate(date) {
157
- return date.toISOString().slice(0, 10);
158
- }
159
186
  //#endregion
160
- export { readBaseline as a, buildBaseline as i, DEFAULT_BASELINE_FILE as n, serialiseBaseline as o, applyBaseline as r, writeBaseline as s, BaselineError as t };
187
+ export { pruneBaseline as a, writeBaseline as c, buildBaseline as i, DEFAULT_BASELINE_FILE as n, readBaseline as o, applyBaseline as r, serialiseBaseline as s, BaselineError as t };
@@ -0,0 +1,44 @@
1
+ import { n as count } from "./text-BF1LHMiV.js";
2
+ import { a as emitDocument, o as fail, s as note } from "./command-CZKp9Wi_.js";
3
+ import "./review-CdMK2GpQ.js";
4
+ import { access } from "node:fs/promises";
5
+ import path from "node:path";
6
+ //#region src/cli/checklist.ts
7
+ async function runChecklistCommand(options = {}) {
8
+ const cwd = options.cwd ?? process.cwd();
9
+ const recordPath = options.record ?? "eaa-review.json";
10
+ const { WCAG22_AA_CRITERIA } = await import("./coverage-7bQqWNfN.js");
11
+ const { answeredCount, blankReview, readReview, ReviewError, writeReview } = await import("./review-DIbOTmOM.js");
12
+ let existing;
13
+ try {
14
+ existing = await readReview(recordPath, cwd);
15
+ } catch (cause) {
16
+ if (!(cause instanceof ReviewError)) throw cause;
17
+ if (await exists(path.resolve(cwd, recordPath))) {
18
+ fail(cause.message);
19
+ return { exitCode: 2 };
20
+ }
21
+ }
22
+ const record = blankReview(WCAG22_AA_CRITERIA, existing);
23
+ if (options.reviewedBy !== void 0) record.reviewedBy = options.reviewedBy;
24
+ await writeReview(recordPath, record, cwd);
25
+ note(existing === void 0 ? `Review record written to ${recordPath}` : `Review record updated at ${recordPath}: ${count(answeredCount(record), "criterion")} already answered`);
26
+ const { buildWorksheet } = await import("./worksheet-eUjwYLxC.js");
27
+ await emitDocument(buildWorksheet({
28
+ recordPath,
29
+ review: record
30
+ }), options.output, cwd);
31
+ if (options.output !== void 0) note(`Worksheet written to ${options.output}`);
32
+ note("Answers go in the record; the worksheet is generated and never read back.");
33
+ return { exitCode: 0 };
34
+ }
35
+ async function exists(target) {
36
+ try {
37
+ await access(target);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ //#endregion
44
+ export { runChecklistCommand };