eaa-kit 0.5.0 → 0.7.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 (69) hide show
  1. package/README.md +53 -5
  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-Bwm1xBdJ.js +2 -0
  6. package/dist/{audit-B282A3EA.js → audit-DRjAY2iY.js} +243 -107
  7. package/dist/{baseline-CuAS2lMk.js → baseline-BpE7DbDq.js} +36 -9
  8. package/dist/baseline-rtnj3RHB.js +2 -0
  9. package/dist/cache-D4dOIuOi.js +219 -0
  10. package/dist/checklist-DDd9jewi.js +44 -0
  11. package/dist/cli/index.js +130 -17
  12. package/dist/{command-C3D7JWn6.js → command-3r4fht5f.js} +102 -10
  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-B_Y6l-Ra.js → coverage-cb5kPp48.js} +45 -290
  16. package/dist/coverage-n1X9CkiG.js +3 -0
  17. package/dist/{crawl-BUWPgaGW.js → crawl-BHkJmUkA.js} +42 -8
  18. package/dist/criteria-CnPJKGeb.js +286 -0
  19. package/dist/eleventy/index.d.ts +1 -1
  20. package/dist/eleventy/index.js +1 -1
  21. package/dist/{html-C4vktg0A.js → html-BIxxF3FC.js} +54 -30
  22. package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
  23. package/dist/index.d.ts +87 -1
  24. package/dist/index.js +3 -3
  25. package/dist/{init-CW7LfGT5.js → init-DrFfzjvm.js} +3 -3
  26. package/dist/jsdom-BUYuVJJm.js +3 -0
  27. package/dist/{jsdom-B--cEH-G.js → jsdom-CsW_xVcA.js} +11 -12
  28. package/dist/{json-DROX33kh.js → json-Bus6YhxB.js} +13 -17
  29. package/dist/{json-D_Mnnft5.js → json-CrQmoTj6.js} +1 -1
  30. package/dist/{load-yAR4wzez.js → load-BaWMa-G_.js} +11 -2
  31. package/dist/load-n62iXDq_.js +2 -0
  32. package/dist/nuxt/index.d.ts +1 -1
  33. package/dist/nuxt/index.js +1 -1
  34. package/dist/{playwright-BojtYVUa.js → playwright-ukCr_5o3.js} +3 -2
  35. package/dist/{pool-BO25OIez.js → pool-B4tEUa2S.js} +6 -2
  36. package/dist/{remediation-CMBIrnpN.js → remediation-C0klJblu.js} +35 -4
  37. package/dist/{render-DbGOVmhx.js → render-C1sUZZI_.js} +145 -6
  38. package/dist/{result-BWcYXeRs.js → result-B7_zVxAX.js} +24 -1
  39. package/dist/result-CRmjjoAA.js +2 -0
  40. package/dist/review-CdMK2GpQ.js +161 -0
  41. package/dist/review-DIbOTmOM.js +2 -0
  42. package/dist/{run-CtcEUhbe.d.ts → run-BK2blxMI.d.ts} +18 -0
  43. package/dist/{run-C2nKFcb-.js → run-DrS0Komf.js} +6 -2
  44. package/dist/{sarif-B-UBcVu8.js → sarif-BNCE0PNT.js} +24 -12
  45. package/dist/statement/templates/at.de.md +14 -0
  46. package/dist/statement/templates/at.en.md +14 -0
  47. package/dist/statement/templates/ch.de.md +14 -0
  48. package/dist/statement/templates/ch.en.md +14 -0
  49. package/dist/statement/templates/de.de.md +14 -0
  50. package/dist/statement/templates/de.en.md +14 -0
  51. package/dist/statement/templates/es.en.md +14 -0
  52. package/dist/statement/templates/es.es.md +15 -0
  53. package/dist/statement/templates/fr.en.md +14 -0
  54. package/dist/statement/templates/fr.fr.md +15 -0
  55. package/dist/statement/templates/it.en.md +14 -0
  56. package/dist/statement/templates/it.it.md +15 -0
  57. package/dist/statement/templates/nl.en.md +14 -0
  58. package/dist/statement/templates/nl.nl.md +15 -0
  59. package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
  60. package/dist/vite/index.d.ts +1 -1
  61. package/dist/vite/index.js +1 -1
  62. package/dist/webpack/index.d.ts +1 -1
  63. package/dist/webpack/index.js +1 -1
  64. package/dist/worksheet-BIxMtbUd.js +64 -0
  65. package/package.json +3 -2
  66. package/dist/audit-DyHPmGeD.js +0 -2
  67. package/dist/baseline-0u4df12v.js +0 -2
  68. package/dist/jsdom-22Bkt65v.js +0 -3
  69. package/dist/load-CFq2VQtT.js +0 -2
@@ -1,6 +1,86 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import pc from "picocolors";
4
+ //#region src/audit/headers.ts
5
+ /**
6
+ * Request headers for the pages this tool fetches.
7
+ *
8
+ * The two places a small agency stages work are the two this tool could not
9
+ * reach: a preview deployment, which every host protects by default, and a CMS
10
+ * staging site behind basic auth or a session cookie. `--url` sent one fixed
11
+ * pair of headers and had no way to add to it, so auditing either meant putting
12
+ * the site on the public internet first.
13
+ *
14
+ * These values are credentials. They are handled on the principle that the tool
15
+ * never writes one down: nothing here reaches a report, a baseline, a SARIF log
16
+ * or the completeness record, and the errors below name the header rather than
17
+ * echoing what was typed, because a malformed `--header` is exactly the case
18
+ * where the value is a token and the terminal is a CI log.
19
+ *
20
+ * For the same reason there is no `headers` key in `eaa.config`. That file is
21
+ * committed; a token in it is a token in the repository, and a tool that
22
+ * offered the field would be inviting that. Pass them as flags — a shell
23
+ * expands `--header "Authorization: Bearer $TOKEN"` — or as the GitHub Action's
24
+ * input, which reads from `secrets`.
25
+ */
26
+ /** Thrown for a header this cannot parse. Never carries the value. */
27
+ var HeaderError = class extends Error {
28
+ name = "HeaderError";
29
+ };
30
+ /**
31
+ * `Name: value` into a pair.
32
+ *
33
+ * Lenient about the space after the colon and about a value containing colons,
34
+ * which `Authorization: Basic …` and a `Cookie` both do. Strict about the name,
35
+ * because a header name is a token by RFC 9110 and anything else is a typo that
36
+ * a server would reject in a way nobody could read.
37
+ */
38
+ function parseHeader(input) {
39
+ const at = input.indexOf(":");
40
+ if (at <= 0) throw new HeaderError("a header must be written \"Name: value\"");
41
+ const name = input.slice(0, at).trim();
42
+ const value = input.slice(at + 1).trim();
43
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name)) throw new HeaderError(`"${name}" is not a valid header name`);
44
+ if (value === "") throw new HeaderError(`the ${name} header was given no value`);
45
+ if (/[\r\n]/.test(value)) throw new HeaderError(`the ${name} header contains a line break`);
46
+ return [name, value];
47
+ }
48
+ /** Several `Name: value` strings into the record the fetchers take. */
49
+ function parseHeaders(inputs) {
50
+ const headers = {};
51
+ for (const input of inputs) {
52
+ const [name, value] = parseHeader(input);
53
+ headers[name] = value;
54
+ }
55
+ return headers;
56
+ }
57
+ /**
58
+ * `user:password` into an `Authorization` header.
59
+ *
60
+ * Sugar for the header somebody would otherwise have to base64 by hand, which
61
+ * is the one credential a staging site is most likely to be behind. A password
62
+ * may contain colons; a username may not, which is basic auth's own rule.
63
+ */
64
+ function basicAuth(input) {
65
+ if (input.indexOf(":") <= 0) throw new HeaderError("basic auth must be written \"user:password\"");
66
+ return ["Authorization", `Basic ${Buffer.from(input, "utf8").toString("base64")}`];
67
+ }
68
+ /**
69
+ * The headers a run will send, from whichever flags were given.
70
+ *
71
+ * `--basic-auth` first, so an explicit `--header "Authorization: …"` beats it:
72
+ * a typed header is the more specific thing to have asked for.
73
+ */
74
+ function requestHeaders(options) {
75
+ const headers = {};
76
+ if (options.basicAuth !== void 0) {
77
+ const [name, value] = basicAuth(options.basicAuth);
78
+ headers[name] = value;
79
+ }
80
+ Object.assign(headers, parseHeaders(options.header ?? []));
81
+ return Object.keys(headers).length === 0 ? void 0 : headers;
82
+ }
83
+ //#endregion
4
84
  //#region src/cli/command.ts
5
85
  /**
6
86
  * What every command does around the audit itself: say what is happening, run
@@ -42,18 +122,19 @@ async function runEngine(pages, options) {
42
122
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
43
123
  };
44
124
  if (!options.browser) {
45
- const { runPooledAudit } = await import("./pool-BO25OIez.js");
125
+ const { runPooledAudit } = await import("./pool-B4tEUa2S.js");
46
126
  return runPooledAudit(pages, {
47
127
  ...runnerOptions,
48
128
  ...options.fast ? { fast: true } : {},
49
129
  ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
50
130
  });
51
131
  }
52
- const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-BojtYVUa.js");
132
+ const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-ukCr_5o3.js");
53
133
  try {
54
134
  return await runBrowserAudit(options.directory, pages, {
55
135
  ...runnerOptions,
56
- ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
136
+ ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
137
+ ...options.headers === void 0 ? {} : { headers: options.headers }
57
138
  });
58
139
  } catch (cause) {
59
140
  if (cause instanceof BrowserUnavailableError) {
@@ -95,7 +176,7 @@ async function emitDocument(body, output, cwd) {
95
176
  * on different settings than it names would be worse than stopping.
96
177
  */
97
178
  async function auditDefaults(options = {}) {
98
- const { loadAuditConfig } = await import("./load-CFq2VQtT.js");
179
+ const { loadAuditConfig } = await import("./load-n62iXDq_.js");
99
180
  const loaded = await loadAuditConfig({
100
181
  ...options.cwd ? { cwd: options.cwd } : {},
101
182
  ...options.config ? { path: options.config } : {}
@@ -105,14 +186,20 @@ async function auditDefaults(options = {}) {
105
186
  return loaded.audit;
106
187
  }
107
188
  function auditInvocation(dir, defaults, flags) {
108
- const { build, config: _config, ...typed } = flags;
109
- const { dir: configDir, build: configBuild, ...fromConfig } = defaults;
189
+ const { build, cache, config: _config, header, basicAuth, ...typed } = flags;
190
+ const { dir: configDir, build: configBuild, cache: configCache, headers: _fromFile, ...fromConfig } = defaults;
191
+ const headers = requestHeaders({
192
+ ...header ? { header } : {},
193
+ ...basicAuth ? { basicAuth } : {}
194
+ });
110
195
  return {
111
196
  dir: dir ?? configDir,
112
197
  options: {
113
198
  ...fromConfig,
114
199
  ...typed,
115
- ...build === false || configBuild === false ? { noBuild: true } : {}
200
+ ...build === false || configBuild === false ? { noBuild: true } : {},
201
+ ...cache === false || configCache === false ? { noCache: true } : {},
202
+ ...headers === void 0 ? {} : { headers }
116
203
  }
117
204
  };
118
205
  }
@@ -126,12 +213,17 @@ function auditInvocation(dir, defaults, flags) {
126
213
  * and `baseline` all describe a verdict this command does not reach.
127
214
  */
128
215
  function baselineInvocation(dir, defaults, flags) {
129
- const { config: _config, ...typed } = flags;
216
+ const { config: _config, header, basicAuth, ...typed } = flags;
217
+ const headers = requestHeaders({
218
+ ...header ? { header } : {},
219
+ ...basicAuth ? { basicAuth } : {}
220
+ });
130
221
  return {
131
222
  dir: dir ?? defaults.dir ?? "./dist",
132
223
  options: {
133
224
  ...baselineDefaults(defaults),
134
- ...typed
225
+ ...typed,
226
+ ...headers === void 0 ? {} : { headers }
135
227
  }
136
228
  };
137
229
  }
@@ -157,4 +249,4 @@ function pick(source, keys) {
157
249
  return out;
158
250
  }
159
251
  //#endregion
160
- export { emitDocument as a, runEngine as c, baselineInvocation as i, warn as l, auditDefaults as n, fail as o, auditInvocation as r, note as s, advise as t };
252
+ export { emitDocument as a, runEngine as c, parseHeader as d, baselineInvocation as i, warn as l, auditDefaults as n, fail as o, auditInvocation as r, note as s, advise as t, basicAuth as u };
@@ -1,5 +1,5 @@
1
1
  import { i as toPosix } from "./fs-BmPtmFke.js";
2
- import { t as collapse } from "./text-CKKpzkYM.js";
2
+ import { t as collapse } from "./text-BF1LHMiV.js";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { glob } from "tinyglobby";
@@ -1,2 +1,2 @@
1
- import { n as componentFor, t as buildComponentIndex } from "./component-C3GL1Mnu.js";
1
+ import { n as componentFor, t as buildComponentIndex } from "./component-ChisoFyY.js";
2
2
  export { buildComponentIndex, componentFor };
@@ -1,4 +1,6 @@
1
- import { d as successCriteria, n as DEFAULT_TAGS, r as ENGINE_BLIND_RULES } from "./result-BWcYXeRs.js";
1
+ import { t as WCAG22_AA_CRITERIA } from "./criteria-CnPJKGeb.js";
2
+ import { n as DEFAULT_TAGS, p as successCriteria, r as ENGINE_BLIND_RULES } from "./result-B7_zVxAX.js";
3
+ import { s as criterionReview } from "./review-CdMK2GpQ.js";
2
4
  import axe from "axe-core";
3
5
  //#region src/audit/manual.ts
4
6
  /**
@@ -137,289 +139,6 @@ function manualCheckFor(ruleId) {
137
139
  //#endregion
138
140
  //#region src/audit/coverage.ts
139
141
  /**
140
- * WCAG 2.2 at Levels A and AA, which is what this tool audits against.
141
- *
142
- * 4.1.1 Parsing is deliberately absent: it was removed in WCAG 2.2 and counting
143
- * it would inflate the denominator with a criterion nobody has to meet.
144
- */
145
- const WCAG22_AA_CRITERIA = [
146
- {
147
- number: "1.1.1",
148
- title: "Non-text Content",
149
- level: "A"
150
- },
151
- {
152
- number: "1.2.1",
153
- title: "Audio-only and Video-only (Prerecorded)",
154
- level: "A"
155
- },
156
- {
157
- number: "1.2.2",
158
- title: "Captions (Prerecorded)",
159
- level: "A"
160
- },
161
- {
162
- number: "1.2.3",
163
- title: "Audio Description or Media Alternative (Prerecorded)",
164
- level: "A"
165
- },
166
- {
167
- number: "1.2.4",
168
- title: "Captions (Live)",
169
- level: "AA"
170
- },
171
- {
172
- number: "1.2.5",
173
- title: "Audio Description (Prerecorded)",
174
- level: "AA"
175
- },
176
- {
177
- number: "1.3.1",
178
- title: "Info and Relationships",
179
- level: "A"
180
- },
181
- {
182
- number: "1.3.2",
183
- title: "Meaningful Sequence",
184
- level: "A"
185
- },
186
- {
187
- number: "1.3.3",
188
- title: "Sensory Characteristics",
189
- level: "A"
190
- },
191
- {
192
- number: "1.3.4",
193
- title: "Orientation",
194
- level: "AA"
195
- },
196
- {
197
- number: "1.3.5",
198
- title: "Identify Input Purpose",
199
- level: "AA"
200
- },
201
- {
202
- number: "1.4.1",
203
- title: "Use of Color",
204
- level: "A"
205
- },
206
- {
207
- number: "1.4.2",
208
- title: "Audio Control",
209
- level: "A"
210
- },
211
- {
212
- number: "1.4.3",
213
- title: "Contrast (Minimum)",
214
- level: "AA"
215
- },
216
- {
217
- number: "1.4.4",
218
- title: "Resize Text",
219
- level: "AA"
220
- },
221
- {
222
- number: "1.4.5",
223
- title: "Images of Text",
224
- level: "AA"
225
- },
226
- {
227
- number: "1.4.10",
228
- title: "Reflow",
229
- level: "AA"
230
- },
231
- {
232
- number: "1.4.11",
233
- title: "Non-text Contrast",
234
- level: "AA"
235
- },
236
- {
237
- number: "1.4.12",
238
- title: "Text Spacing",
239
- level: "AA"
240
- },
241
- {
242
- number: "1.4.13",
243
- title: "Content on Hover or Focus",
244
- level: "AA"
245
- },
246
- {
247
- number: "2.1.1",
248
- title: "Keyboard",
249
- level: "A"
250
- },
251
- {
252
- number: "2.1.2",
253
- title: "No Keyboard Trap",
254
- level: "A"
255
- },
256
- {
257
- number: "2.1.4",
258
- title: "Character Key Shortcuts",
259
- level: "A"
260
- },
261
- {
262
- number: "2.2.1",
263
- title: "Timing Adjustable",
264
- level: "A"
265
- },
266
- {
267
- number: "2.2.2",
268
- title: "Pause, Stop, Hide",
269
- level: "A"
270
- },
271
- {
272
- number: "2.3.1",
273
- title: "Three Flashes or Below Threshold",
274
- level: "A"
275
- },
276
- {
277
- number: "2.4.1",
278
- title: "Bypass Blocks",
279
- level: "A"
280
- },
281
- {
282
- number: "2.4.2",
283
- title: "Page Titled",
284
- level: "A"
285
- },
286
- {
287
- number: "2.4.3",
288
- title: "Focus Order",
289
- level: "A"
290
- },
291
- {
292
- number: "2.4.4",
293
- title: "Link Purpose (In Context)",
294
- level: "A"
295
- },
296
- {
297
- number: "2.4.5",
298
- title: "Multiple Ways",
299
- level: "AA"
300
- },
301
- {
302
- number: "2.4.6",
303
- title: "Headings and Labels",
304
- level: "AA"
305
- },
306
- {
307
- number: "2.4.7",
308
- title: "Focus Visible",
309
- level: "AA"
310
- },
311
- {
312
- number: "2.4.11",
313
- title: "Focus Not Obscured (Minimum)",
314
- level: "AA"
315
- },
316
- {
317
- number: "2.5.1",
318
- title: "Pointer Gestures",
319
- level: "A"
320
- },
321
- {
322
- number: "2.5.2",
323
- title: "Pointer Cancellation",
324
- level: "A"
325
- },
326
- {
327
- number: "2.5.3",
328
- title: "Label in Name",
329
- level: "A"
330
- },
331
- {
332
- number: "2.5.4",
333
- title: "Motion Actuation",
334
- level: "A"
335
- },
336
- {
337
- number: "2.5.7",
338
- title: "Dragging Movements",
339
- level: "AA"
340
- },
341
- {
342
- number: "2.5.8",
343
- title: "Target Size (Minimum)",
344
- level: "AA"
345
- },
346
- {
347
- number: "3.1.1",
348
- title: "Language of Page",
349
- level: "A"
350
- },
351
- {
352
- number: "3.1.2",
353
- title: "Language of Parts",
354
- level: "AA"
355
- },
356
- {
357
- number: "3.2.1",
358
- title: "On Focus",
359
- level: "A"
360
- },
361
- {
362
- number: "3.2.2",
363
- title: "On Input",
364
- level: "A"
365
- },
366
- {
367
- number: "3.2.3",
368
- title: "Consistent Navigation",
369
- level: "AA"
370
- },
371
- {
372
- number: "3.2.4",
373
- title: "Consistent Identification",
374
- level: "AA"
375
- },
376
- {
377
- number: "3.2.6",
378
- title: "Consistent Help",
379
- level: "A"
380
- },
381
- {
382
- number: "3.3.1",
383
- title: "Error Identification",
384
- level: "A"
385
- },
386
- {
387
- number: "3.3.2",
388
- title: "Labels or Instructions",
389
- level: "A"
390
- },
391
- {
392
- number: "3.3.3",
393
- title: "Error Suggestion",
394
- level: "AA"
395
- },
396
- {
397
- number: "3.3.4",
398
- title: "Error Prevention (Legal, Financial, Data)",
399
- level: "AA"
400
- },
401
- {
402
- number: "3.3.7",
403
- title: "Redundant Entry",
404
- level: "A"
405
- },
406
- {
407
- number: "3.3.8",
408
- title: "Accessible Authentication (Minimum)",
409
- level: "AA"
410
- },
411
- {
412
- number: "4.1.2",
413
- title: "Name, Role, Value",
414
- level: "A"
415
- },
416
- {
417
- number: "4.1.3",
418
- title: "Status Messages",
419
- level: "AA"
420
- }
421
- ];
422
- /**
423
142
  * Rules axe-core would run under these tags, mapped to the criteria they cover.
424
143
  *
425
144
  * Experimental rules are excluded, matching `blindRulesInScope`: axe-core leaves
@@ -463,7 +182,7 @@ function blindedRules(audits) {
463
182
  * the criterion is met on this site, and saying otherwise would be a coverage
464
183
  * claim resting on an empty page.
465
184
  */
466
- function buildCoverage(audits, tags = DEFAULT_TAGS) {
185
+ function buildCoverage(audits, tags = DEFAULT_TAGS, review) {
467
186
  const byCriterion = rulesByCriterion(tags);
468
187
  const decided = decidedRules(audits);
469
188
  const blinded = blindedRules(audits);
@@ -496,15 +215,36 @@ function buildCoverage(audits, tags = DEFAULT_TAGS) {
496
215
  browserWouldAnswer: false
497
216
  };
498
217
  });
499
- const count = (status) => criteria.filter((criterion) => criterion.status === status).length;
218
+ const reviewed = review === void 0 ? criteria : criteria.map(withReview(review));
219
+ const count = (status) => reviewed.filter((criterion) => criterion.status === status).length;
220
+ const counted = reviewed.filter((criterion) => criterion.review?.counts === true);
500
221
  return {
501
- criteria,
222
+ criteria: reviewed,
502
223
  evaluated: count("evaluated"),
503
224
  notEvaluated: count("not-evaluated"),
504
225
  nothingToCheck: count("nothing-to-check"),
505
226
  noAutomatedRule: count("no-automated-rule"),
506
- browserWouldAnswer: criteria.filter((criterion) => criterion.browserWouldAnswer).length,
507
- total: WCAG22_AA_CRITERIA.length
227
+ browserWouldAnswer: reviewed.filter((criterion) => criterion.browserWouldAnswer).length,
228
+ total: WCAG22_AA_CRITERIA.length,
229
+ reviewed: counted.length,
230
+ reviewedNotMet: counted.filter((criterion) => criterion.review?.result === "not-met").length,
231
+ reviewNotCounted: reviewed.filter((criterion) => criterion.review?.counts === false).length
232
+ };
233
+ }
234
+ /**
235
+ * Attach what a person recorded, without letting it move what the engine found.
236
+ *
237
+ * `status` is untouched here, deliberately and in every branch: a review adds a
238
+ * second kind of claim beside the run's own, and the moment it could rewrite
239
+ * one the report would stop being a record of what was measured.
240
+ */
241
+ function withReview(review) {
242
+ return (criterion) => {
243
+ const recorded = criterionReview(criterion.number, review, criterion.status === "evaluated");
244
+ return recorded === void 0 ? criterion : {
245
+ ...criterion,
246
+ review: recorded
247
+ };
508
248
  };
509
249
  }
510
250
  /**
@@ -518,5 +258,20 @@ function coverageSummary(coverage) {
518
258
  const unautomatable = coverage.noAutomatedRule;
519
259
  return `Of the ${coverage.total} WCAG 2.2 A and AA success criteria, ${unautomatable} cannot be checked by any automated engine and need a person. This run reached a verdict on ${coverage.evaluated}.`;
520
260
  }
261
+ /**
262
+ * What a person added to the run, in one sentence, or nothing when nobody did.
263
+ *
264
+ * Kept out of `coverageSummary` so the sentence about the engine's reach does
265
+ * not change shape depending on whether a review was supplied — and because a
266
+ * reader has to be able to tell the two claims apart at a glance: one is what
267
+ * an engine measured, the other is what somebody says they checked.
268
+ */
269
+ function reviewSummary(coverage) {
270
+ if (coverage.reviewed === 0 && coverage.reviewNotCounted === 0) return void 0;
271
+ const parts = [`A person recorded a result for ${coverage.reviewed} of the criteria this run did not reach`];
272
+ if (coverage.reviewedNotMet > 0) parts.push(`${coverage.reviewedNotMet} of them as not met`);
273
+ if (coverage.reviewNotCounted > 0) parts.push(`${coverage.reviewNotCounted} further entries were not counted`);
274
+ return `${parts.join("; ")}. This is a claim by a person, not a measurement.`;
275
+ }
521
276
  //#endregion
522
- export { understandingUrl as i, coverageSummary as n, manualCheckFor as r, buildCoverage as t };
277
+ export { manualCheckFor as a, rulesByCriterion as i, coverageSummary as n, understandingUrl as o, reviewSummary as r, buildCoverage as t };
@@ -0,0 +1,3 @@
1
+ import { t as WCAG22_AA_CRITERIA } from "./criteria-CnPJKGeb.js";
2
+ import "./coverage-cb5kPp48.js";
3
+ export { WCAG22_AA_CRITERIA };
@@ -93,7 +93,7 @@ function urlsFromSitemap(xml, origin) {
93
93
  return urls;
94
94
  }
95
95
  /** One request, with a timeout, returning HTML or a reason it is not a page. */
96
- async function fetchPage(url, impl, timeoutMs, origin, maxBodyBytes) {
96
+ async function fetchPage(url, impl, timeoutMs, origin, maxBodyBytes, headers) {
97
97
  const controller = new AbortController();
98
98
  const timer = setTimeout(() => controller.abort(), timeoutMs);
99
99
  try {
@@ -102,7 +102,8 @@ async function fetchPage(url, impl, timeoutMs, origin, maxBodyBytes) {
102
102
  redirect: "follow",
103
103
  headers: {
104
104
  accept: "text/html,application/xhtml+xml",
105
- "user-agent": "eaa-kit"
105
+ "user-agent": "eaa-kit",
106
+ ...headers
106
107
  }
107
108
  });
108
109
  if (!response.ok) return {
@@ -213,13 +214,14 @@ function disallowedPaths(robots) {
213
214
  * that is not a page. A site that does not publish one is the ordinary case,
214
215
  * not a failure, so it comes back empty either way.
215
216
  */
216
- async function fetchSiteFile(entry, impl, name, timeoutMs, maxBodyBytes) {
217
+ async function fetchSiteFile(entry, impl, name, timeoutMs, maxBodyBytes, headers) {
217
218
  const controller = new AbortController();
218
219
  const timer = setTimeout(() => controller.abort(), timeoutMs);
219
220
  try {
220
221
  const response = await impl(new URL(name, entry).href, {
221
222
  redirect: "follow",
222
- signal: controller.signal
223
+ signal: controller.signal,
224
+ ...headers === void 0 ? {} : { headers }
223
225
  });
224
226
  if (!response.ok) return void 0;
225
227
  if (new URL(response.url || new URL(name, entry).href).origin !== entry.origin) return void 0;
@@ -244,7 +246,7 @@ async function crawlSite(entry, options = {}) {
244
246
  const timeoutMs = options.timeoutMs ?? 15e3;
245
247
  const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
246
248
  const failures = [];
247
- const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt", timeoutMs, maxBodyBytes);
249
+ const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt", timeoutMs, maxBodyBytes, options.headers);
248
250
  const blocked = robots === void 0 ? [] : disallowedPaths(robots);
249
251
  const allowed = (url) => !blocked.some((path) => url.pathname.startsWith(path));
250
252
  let discovery = "links";
@@ -258,7 +260,7 @@ async function crawlSite(entry, options = {}) {
258
260
  depth
259
261
  });
260
262
  };
261
- const sitemap = await fetchSiteFile(entry, impl, options.sitemap ?? "/sitemap.xml", timeoutMs, maxBodyBytes);
263
+ const sitemap = await fetchSiteFile(entry, impl, options.sitemap ?? "/sitemap.xml", timeoutMs, maxBodyBytes, options.headers);
262
264
  const listed = sitemap === void 0 ? [] : urlsFromSitemap(sitemap, entry);
263
265
  if (listed.length > 0) {
264
266
  discovery = "sitemap";
@@ -266,11 +268,12 @@ async function crawlSite(entry, options = {}) {
266
268
  }
267
269
  enqueue(entry, 0);
268
270
  const pages = [];
271
+ const redirects = [];
269
272
  while (queue.length > 0 && pages.length < maxPages) {
270
273
  const batch = queue.splice(0, Math.min(REQUEST_CONCURRENCY, maxPages - pages.length));
271
274
  const results = await Promise.all(batch.map(async (item) => ({
272
275
  item,
273
- result: await fetchPage(item.url, impl, timeoutMs, entry.origin, maxBodyBytes)
276
+ result: await fetchPage(item.url, impl, timeoutMs, entry.origin, maxBodyBytes, options.headers)
274
277
  })));
275
278
  for (const { item, result } of results) {
276
279
  if (!result.ok) {
@@ -281,6 +284,10 @@ async function crawlSite(entry, options = {}) {
281
284
  continue;
282
285
  }
283
286
  const { url, html } = result.value;
287
+ if (url.href !== item.url.href) redirects.push({
288
+ requested: item.url.href,
289
+ landedOn: url.href
290
+ });
284
291
  pages.push({
285
292
  absolutePath: url.href,
286
293
  relativePath: pageIdentity(url),
@@ -294,11 +301,38 @@ async function crawlSite(entry, options = {}) {
294
301
  pages: byIdentity(pages),
295
302
  origin: entry.origin,
296
303
  failures,
304
+ redirects,
297
305
  truncated: queue.length > 0,
298
306
  discovery
299
307
  };
300
308
  }
301
309
  /**
310
+ * The requested URLs that a login wall swallowed, or none.
311
+ *
312
+ * A redirect on its own says nothing: sites normalise trailing slashes and send
313
+ * `/` to `/en/` all day. What is not ordinary is *several* different URLs
314
+ * answering at one address, or a whole crawl coming back as the single page the
315
+ * entry was redirected to. Both are the shape of a sign-in page standing in
316
+ * front of the site, and both otherwise produce a report about a page nobody
317
+ * asked for that says it audited everything it set out to.
318
+ *
319
+ * Returns what was asked for and never reached, which is the one thing the run
320
+ * has to say out loud. Deliberately shy of naming the cause: a single-page site
321
+ * that redirects its entry looks identical from here, and being told where the
322
+ * run actually landed is useful either way.
323
+ */
324
+ function collapsedOnto(result) {
325
+ if (result.redirects.length === 0) return [];
326
+ const byDestination = /* @__PURE__ */ new Map();
327
+ for (const redirect of result.redirects) byDestination.set(redirect.landedOn, [...byDestination.get(redirect.landedOn) ?? [], redirect.requested]);
328
+ const collapsed = [];
329
+ for (const [landedOn, requested] of byDestination) if (requested.length > 1 || result.pages.length === 1) for (const from of requested) collapsed.push({
330
+ requested: from,
331
+ landedOn
332
+ });
333
+ return collapsed;
334
+ }
335
+ /**
302
336
  * One page per identity, in a stable order.
303
337
  *
304
338
  * The queue already refuses a URL it has seen, so this catches the case the
@@ -315,4 +349,4 @@ function byIdentity(pages) {
315
349
  }).sort((a, b) => a.relativePath.localeCompare(b.relativePath));
316
350
  }
317
351
  //#endregion
318
- export { CrawlError, crawlSite, parseEntryUrl };
352
+ export { CrawlError, collapsedOnto, crawlSite, parseEntryUrl };