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
@@ -0,0 +1,286 @@
1
+ //#region src/audit/criteria.ts
2
+ /**
3
+ * WCAG 2.2 at Levels A and AA, which is what this tool audits against.
4
+ *
5
+ * 4.1.1 Parsing is deliberately absent: it was removed in WCAG 2.2 and counting
6
+ * it would inflate the denominator with a criterion nobody has to meet.
7
+ */
8
+ const WCAG22_AA_CRITERIA = [
9
+ {
10
+ number: "1.1.1",
11
+ title: "Non-text Content",
12
+ level: "A"
13
+ },
14
+ {
15
+ number: "1.2.1",
16
+ title: "Audio-only and Video-only (Prerecorded)",
17
+ level: "A"
18
+ },
19
+ {
20
+ number: "1.2.2",
21
+ title: "Captions (Prerecorded)",
22
+ level: "A"
23
+ },
24
+ {
25
+ number: "1.2.3",
26
+ title: "Audio Description or Media Alternative (Prerecorded)",
27
+ level: "A"
28
+ },
29
+ {
30
+ number: "1.2.4",
31
+ title: "Captions (Live)",
32
+ level: "AA"
33
+ },
34
+ {
35
+ number: "1.2.5",
36
+ title: "Audio Description (Prerecorded)",
37
+ level: "AA"
38
+ },
39
+ {
40
+ number: "1.3.1",
41
+ title: "Info and Relationships",
42
+ level: "A"
43
+ },
44
+ {
45
+ number: "1.3.2",
46
+ title: "Meaningful Sequence",
47
+ level: "A"
48
+ },
49
+ {
50
+ number: "1.3.3",
51
+ title: "Sensory Characteristics",
52
+ level: "A"
53
+ },
54
+ {
55
+ number: "1.3.4",
56
+ title: "Orientation",
57
+ level: "AA"
58
+ },
59
+ {
60
+ number: "1.3.5",
61
+ title: "Identify Input Purpose",
62
+ level: "AA"
63
+ },
64
+ {
65
+ number: "1.4.1",
66
+ title: "Use of Color",
67
+ level: "A"
68
+ },
69
+ {
70
+ number: "1.4.2",
71
+ title: "Audio Control",
72
+ level: "A"
73
+ },
74
+ {
75
+ number: "1.4.3",
76
+ title: "Contrast (Minimum)",
77
+ level: "AA"
78
+ },
79
+ {
80
+ number: "1.4.4",
81
+ title: "Resize Text",
82
+ level: "AA"
83
+ },
84
+ {
85
+ number: "1.4.5",
86
+ title: "Images of Text",
87
+ level: "AA"
88
+ },
89
+ {
90
+ number: "1.4.10",
91
+ title: "Reflow",
92
+ level: "AA"
93
+ },
94
+ {
95
+ number: "1.4.11",
96
+ title: "Non-text Contrast",
97
+ level: "AA"
98
+ },
99
+ {
100
+ number: "1.4.12",
101
+ title: "Text Spacing",
102
+ level: "AA"
103
+ },
104
+ {
105
+ number: "1.4.13",
106
+ title: "Content on Hover or Focus",
107
+ level: "AA"
108
+ },
109
+ {
110
+ number: "2.1.1",
111
+ title: "Keyboard",
112
+ level: "A"
113
+ },
114
+ {
115
+ number: "2.1.2",
116
+ title: "No Keyboard Trap",
117
+ level: "A"
118
+ },
119
+ {
120
+ number: "2.1.4",
121
+ title: "Character Key Shortcuts",
122
+ level: "A"
123
+ },
124
+ {
125
+ number: "2.2.1",
126
+ title: "Timing Adjustable",
127
+ level: "A"
128
+ },
129
+ {
130
+ number: "2.2.2",
131
+ title: "Pause, Stop, Hide",
132
+ level: "A"
133
+ },
134
+ {
135
+ number: "2.3.1",
136
+ title: "Three Flashes or Below Threshold",
137
+ level: "A"
138
+ },
139
+ {
140
+ number: "2.4.1",
141
+ title: "Bypass Blocks",
142
+ level: "A"
143
+ },
144
+ {
145
+ number: "2.4.2",
146
+ title: "Page Titled",
147
+ level: "A"
148
+ },
149
+ {
150
+ number: "2.4.3",
151
+ title: "Focus Order",
152
+ level: "A"
153
+ },
154
+ {
155
+ number: "2.4.4",
156
+ title: "Link Purpose (In Context)",
157
+ level: "A"
158
+ },
159
+ {
160
+ number: "2.4.5",
161
+ title: "Multiple Ways",
162
+ level: "AA"
163
+ },
164
+ {
165
+ number: "2.4.6",
166
+ title: "Headings and Labels",
167
+ level: "AA"
168
+ },
169
+ {
170
+ number: "2.4.7",
171
+ title: "Focus Visible",
172
+ level: "AA"
173
+ },
174
+ {
175
+ number: "2.4.11",
176
+ title: "Focus Not Obscured (Minimum)",
177
+ level: "AA"
178
+ },
179
+ {
180
+ number: "2.5.1",
181
+ title: "Pointer Gestures",
182
+ level: "A"
183
+ },
184
+ {
185
+ number: "2.5.2",
186
+ title: "Pointer Cancellation",
187
+ level: "A"
188
+ },
189
+ {
190
+ number: "2.5.3",
191
+ title: "Label in Name",
192
+ level: "A"
193
+ },
194
+ {
195
+ number: "2.5.4",
196
+ title: "Motion Actuation",
197
+ level: "A"
198
+ },
199
+ {
200
+ number: "2.5.7",
201
+ title: "Dragging Movements",
202
+ level: "AA"
203
+ },
204
+ {
205
+ number: "2.5.8",
206
+ title: "Target Size (Minimum)",
207
+ level: "AA"
208
+ },
209
+ {
210
+ number: "3.1.1",
211
+ title: "Language of Page",
212
+ level: "A"
213
+ },
214
+ {
215
+ number: "3.1.2",
216
+ title: "Language of Parts",
217
+ level: "AA"
218
+ },
219
+ {
220
+ number: "3.2.1",
221
+ title: "On Focus",
222
+ level: "A"
223
+ },
224
+ {
225
+ number: "3.2.2",
226
+ title: "On Input",
227
+ level: "A"
228
+ },
229
+ {
230
+ number: "3.2.3",
231
+ title: "Consistent Navigation",
232
+ level: "AA"
233
+ },
234
+ {
235
+ number: "3.2.4",
236
+ title: "Consistent Identification",
237
+ level: "AA"
238
+ },
239
+ {
240
+ number: "3.2.6",
241
+ title: "Consistent Help",
242
+ level: "A"
243
+ },
244
+ {
245
+ number: "3.3.1",
246
+ title: "Error Identification",
247
+ level: "A"
248
+ },
249
+ {
250
+ number: "3.3.2",
251
+ title: "Labels or Instructions",
252
+ level: "A"
253
+ },
254
+ {
255
+ number: "3.3.3",
256
+ title: "Error Suggestion",
257
+ level: "AA"
258
+ },
259
+ {
260
+ number: "3.3.4",
261
+ title: "Error Prevention (Legal, Financial, Data)",
262
+ level: "AA"
263
+ },
264
+ {
265
+ number: "3.3.7",
266
+ title: "Redundant Entry",
267
+ level: "A"
268
+ },
269
+ {
270
+ number: "3.3.8",
271
+ title: "Accessible Authentication (Minimum)",
272
+ level: "AA"
273
+ },
274
+ {
275
+ number: "4.1.2",
276
+ title: "Name, Role, Value",
277
+ level: "A"
278
+ },
279
+ {
280
+ number: "4.1.3",
281
+ title: "Status Messages",
282
+ level: "AA"
283
+ }
284
+ ];
285
+ //#endregion
286
+ export { WCAG22_AA_CRITERIA as t };
@@ -1,4 +1,4 @@
1
- import { n as IntegrationOptions, t as BuildAuditError } from "../run-CtcEUhbe.js";
1
+ import { n as IntegrationOptions, t as BuildAuditError } from "../run-BK2blxMI.js";
2
2
  //#region src/eleventy/index.d.ts
3
3
  /** What `eleventy.after` is given. Only two of its fields are read. */
4
4
  interface EleventyAfterEvent {
@@ -1,4 +1,4 @@
1
- import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-C2nKFcb-.js";
1
+ import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-DrS0Komf.js";
2
2
  import path from "node:path";
3
3
  //#region src/eleventy/index.ts
4
4
  /**
@@ -1,10 +1,12 @@
1
- import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
2
- import { i as escapeText, n as count, o as standardsReference, r as escapeAttribute, t as collapse } from "./text-CKKpzkYM.js";
1
+ import { a as impactLabel, r as byImpactThenRule } from "./impact-luGCnN8F.js";
2
+ import { i as escapeText, n as count, r as escapeAttribute, s as standardsReference, t as collapse } from "./text-BF1LHMiV.js";
3
3
  import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
4
- import { a as isShared, i as groupIssues, n as blindRules, o as discoveryLabel, r as coverageParts, s as missedParts, t as remediationFor } from "./remediation-CMBIrnpN.js";
5
- import { r as componentPath } from "./component-C3GL1Mnu.js";
6
- import { i as understandingUrl, r as manualCheckFor, t as buildCoverage } from "./coverage-B_Y6l-Ra.js";
7
- import { n as buildSummary } from "./json-DROX33kh.js";
4
+ import { u as runEngine } from "./result-B7_zVxAX.js";
5
+ import { l as reviewSentence } from "./review-CdMK2GpQ.js";
6
+ import { a as isShared, c as missedParts, i as groupIssues, l as reusedPart, n as blindRules, o as issueTotals, r as coverageParts, s as discoveryLabel, t as remediationFor } from "./remediation-C0klJblu.js";
7
+ import { r as componentPath } from "./component-ChisoFyY.js";
8
+ import { a as manualCheckFor, o as understandingUrl, r as reviewSummary, t as buildCoverage } from "./coverage-cb5kPp48.js";
9
+ import { n as buildSummary } from "./json-Bus6YhxB.js";
8
10
  import axe from "axe-core";
9
11
  //#region src/audit/report/html.ts
10
12
  /**
@@ -36,8 +38,8 @@ const MAX_NODES = 5;
36
38
  /** Longest element markup shown before it is truncated. */
37
39
  const MAX_SNIPPET = 200;
38
40
  function buildHtmlReport(audits, options) {
39
- const engine = audits[0]?.engine ?? "jsdom";
40
- const failing = countAtOrAbove(audits, options.failOn);
41
+ const engine = runEngine(audits);
42
+ const totals = buildSummary(audits, options.failOn);
41
43
  const generatedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
42
44
  const title = `Accessibility audit · ${options.directory}`;
43
45
  return `<!doctype html>
@@ -54,15 +56,15 @@ ${STYLES}
54
56
  <body>
55
57
  <main>
56
58
  <h1>Accessibility audit</h1>
57
- ${verdict(audits, failing, options)}
59
+ ${verdict(totals, options)}
58
60
  ${scoreboard(audits)}
59
61
  ${issues(audits, options)}
60
62
  ${runDetails(audits, engine, generatedAt, options)}
61
63
  ${notMeasured(options)}
62
- ${summary(audits, failing, options)}
64
+ ${summary(totals, options)}
63
65
  ${pages(audits)}
64
66
  ${notEvaluated(audits)}
65
- ${coverageSection(audits)}
67
+ ${coverageSection(audits, options)}
66
68
  ${footer()}
67
69
  </main>
68
70
  </body>
@@ -77,11 +79,11 @@ ${footer()}
77
79
  * which would be an embarrassing thing for this document in particular to get
78
80
  * wrong.
79
81
  */
80
- function verdict(audits, failing, options) {
81
- const unaudited = audits.filter((audit) => audit.error).length;
82
+ function verdict(totals, options) {
83
+ const unaudited = totals.pagesNotAudited;
82
84
  if (unaudited > 0) return banner("broken", "Could not finish", `${count(unaudited, "page")} could not be audited, so this run reached no verdict.`);
83
- if (failing > 0) return banner("fail", "Violations found", `${count(failing, "violation")} at or above ${escapeText(options.failOn)}.`);
84
- const below = totalViolations(audits) - failing;
85
+ if (totals.failing > 0) return banner("fail", "Violations found", `${count(totals.failing, "violation")} at or above ${escapeText(options.failOn)}.`);
86
+ const below = totals.violations - totals.failing;
85
87
  if (below > 0) return banner("pass", "No violations at the threshold", `${count(below, "violation")} below ${escapeText(options.failOn)}, which do not fail the run.`);
86
88
  const completeness = options.completeness;
87
89
  if (completeness && !completeness.complete) return banner("partial", "No violations found, but the whole site was not measured", `${escapeText(missedParts(completeness).join(", "))}. This report describes ${count(completeness.audited, "page")}, not the site.`);
@@ -101,6 +103,8 @@ function runDetails(audits, engine, generatedAt, options) {
101
103
  ];
102
104
  if (options.baseUrl) rows.splice(1, 0, ["Base URL", options.baseUrl]);
103
105
  if (options.completeness) rows.splice(2, 0, ["Pages found via", discoveryLabel(options.completeness.discovery)]);
106
+ const reused = options.completeness ? reusedPart(options.completeness) : void 0;
107
+ if (reused !== void 0) rows.push(["Reused", reused]);
104
108
  return `<h2>The run</h2>\n<dl class="run">\n${rows.map(([term, value]) => ` <div><dt>${escapeText(term)}</dt><dd>${escapeText(value)}</dd></div>`).join("\n")}\n</dl>`;
105
109
  }
106
110
  /** Pages named individually before the rest are counted. */
@@ -137,13 +141,12 @@ function notMeasured(options) {
137
141
  * the kind of bug nobody notices until a client does, so there is one place
138
142
  * that counts.
139
143
  */
140
- function summary(audits, failing, options) {
141
- const totals = buildSummary(audits, options.failOn);
144
+ function summary(totals, options) {
142
145
  const impacts = IMPACT_ORDER.filter((impact) => totals.byImpact[impact] > 0).map((impact) => ` <li><span class="badge ${escapeAttribute(impact)}">${escapeText(impact)}</span> ${totals.byImpact[impact]}</li>`).join("\n");
143
146
  return `<h2>Summary</h2>
144
147
  <ul class="counts">
145
148
  <li><strong>${count(totals.violations, "violation")}</strong> on ${totals.pagesWithViolations} of ${count(totals.pages, "page")}, across ${count(totals.violatingElements, "element")}</li>
146
- <li><strong>${failing}</strong> at or above ${escapeText(options.failOn)}</li>
149
+ <li><strong>${totals.failing}</strong> at or above ${escapeText(options.failOn)}</li>
147
150
  <li><strong>${totals.needsReview}</strong> ${totals.needsReview === 1 ? "rule needs" : "rules need"} manual review</li>
148
151
  <li><strong>${totals.notEvaluated}</strong> ${totals.notEvaluated === 1 ? "rule was" : "rules were"} not evaluated by this engine</li>
149
152
  ${totals.accepted > 0 ? ` <li><strong>${totals.accepted}</strong> ${totals.accepted === 1 ? "element is" : "elements are"} accepted by the baseline, and not counted above</li>` : ""}
@@ -236,8 +239,8 @@ ${manualBlock(ruleId, finding)}
236
239
  * majority of WCAG, and a percentage would present a limit of automated testing
237
240
  * as a property of this site.
238
241
  */
239
- function coverageSection(audits) {
240
- const coverage = buildCoverage(audits);
242
+ function coverageSection(audits, options) {
243
+ const coverage = buildCoverage(audits, void 0, options.review);
241
244
  const rows = coverage.criteria.map((criterion) => {
242
245
  const url = understandingUrl(criterion.number);
243
246
  const name = escapeText(`${criterion.number} ${criterion.title}`);
@@ -245,7 +248,7 @@ function coverageSection(audits) {
245
248
  return ` <tr class="${escapeAttribute(criterion.status)}">
246
249
  <td>${linked}</td>
247
250
  <td>${escapeText(criterion.level)}</td>
248
- <td>${statusText(criterion)}</td>
251
+ <td>${statusText(criterion)}${reviewCell(criterion)}</td>
249
252
  </tr>`;
250
253
  }).join("\n");
251
254
  return `<h2>Coverage of WCAG 2.2 AA</h2>
@@ -263,6 +266,7 @@ function coverageSection(audits) {
263
266
  of WCAG cannot be automated, and a percentage here would present that limit of automated
264
267
  testing as though it were a measurement of this site.
265
268
  </p>
269
+ ${reviewParagraph(coverage)}
266
270
  <div class="scroll">
267
271
  <table class="coverage">
268
272
  <thead><tr><th>Success criterion</th><th>Level</th><th>This run</th></tr></thead>
@@ -272,6 +276,20 @@ ${rows}
272
276
  </table>
273
277
  </div>`;
274
278
  }
279
+ /**
280
+ * What a person recorded, where a review was supplied.
281
+ *
282
+ * Its own paragraph, under the four counts and visibly apart from them: the
283
+ * counts are what an engine measured and this is what somebody says they
284
+ * checked. This document is the one that leaves the building, so the difference
285
+ * between the two has to survive being quoted.
286
+ */
287
+ function reviewParagraph(coverage) {
288
+ const summary = reviewSummary(coverage);
289
+ if (summary === void 0) return "";
290
+ return `<p class="note review">${escapeText(summary)} eaa-kit cannot check that anything
291
+ recorded in a review is true; it reports it as the claim it is.</p>`;
292
+ }
275
293
  function statusText(criterion) {
276
294
  const browser = criterion.browserWouldAnswer ? " <em>(a real browser would answer this)</em>" : "";
277
295
  switch (criterion.status) {
@@ -281,6 +299,16 @@ function statusText(criterion) {
281
299
  case "no-automated-rule": return "No automated rule exists; a person must check it";
282
300
  }
283
301
  }
302
+ /**
303
+ * What a person recorded about this criterion, in the cell that says what the
304
+ * run reached. The same sentence the console report prints, from the same
305
+ * function, so two documents of one run cannot word it differently.
306
+ */
307
+ function reviewCell(criterion) {
308
+ const review = criterion.review;
309
+ if (review === void 0) return "";
310
+ return `<br><span class="reviewed">${escapeText(reviewSentence(review))}</span>`;
311
+ }
284
312
  function footer() {
285
313
  return `<hr>
286
314
  <footer>
@@ -303,12 +331,6 @@ function standards(finding) {
303
331
  function standardsText(finding) {
304
332
  return escapeText(standardsReference(finding.successCriteria, finding.enClauses));
305
333
  }
306
- function totalViolations(audits) {
307
- return audits.reduce((total, audit) => total + audit.violations.length, 0);
308
- }
309
- function byImpactThenRule(a, b) {
310
- return impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId);
311
- }
312
334
  const STYLES = `:root { color-scheme: light dark; }
313
335
  body { margin: 0; background: #ffffff; color: #1a1a1a;
314
336
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; line-height: 1.6; }
@@ -362,6 +384,8 @@ table.coverage tr.evaluated td:last-child { color: #216e39; }
362
384
  table.coverage em { font-style: normal; opacity: 0.75; }
363
385
  ul.unreachable li { margin-bottom: 0.2rem; overflow-wrap: anywhere; }
364
386
  code.selector { color: #4a4a4a; }
387
+ .reviewed { color: #4a4a4a; font-size: 0.95em; }
388
+ p.note.review { border-left: 3px solid #c9c9c9; padding-left: 0.75rem; }
365
389
  p.clean { color: #216e39; }
366
390
  p.note { font-size: 0.95rem; }
367
391
  hr { border: 0; border-top: 1px solid #d4d4d4; margin: 3rem 0 1.5rem; }
@@ -401,7 +425,8 @@ ul.page-list { margin: 0.35rem 0 0; padding-left: 1.25rem; }
401
425
  pre { background: #1e1e1e; }
402
426
  li.finding { border-color: #3a3a3a; }
403
427
  p.standards, p.coverage, .reason, li.more, code.selector, footer,
404
- p.accepted-heading, ul.accepted { color: #b6b6b6; }
428
+ p.accepted-heading, ul.accepted, .reviewed { color: #b6b6b6; }
429
+ p.note.review { border-left-color: #555; }
405
430
  p.clean { color: #7ee2a8; }
406
431
  .verdict.pass { background: #10240f; border-color: #7ee2a8; }
407
432
  .verdict.fail { background: #2b1111; border-color: #ff9d9d; }
@@ -467,8 +492,7 @@ function scoreboard(audits) {
467
492
  function issues(audits, options) {
468
493
  const found = groupIssues(audits);
469
494
  if (found.length === 0) return "";
470
- const elements = found.reduce((total, issue) => total + issue.elements.length, 0);
471
- const occurrences = found.reduce((total, issue) => total + issue.occurrences, 0);
495
+ const { elements, occurrences } = issueTotals(found);
472
496
  const intro = occurrences === elements ? `${count(elements, "distinct element")} to fix.` : `${count(occurrences, "violation")} across the site, from ${count(elements, "distinct element")}.`;
473
497
  return `<h2 id="issues">What to fix</h2>
474
498
  <p class="intro">${escapeText(intro)}</p>
@@ -36,6 +36,16 @@ function impactRank(impact) {
36
36
  function impactLabel(impact) {
37
37
  return impact !== null && isImpactLevel(impact) ? impact : "unclassified";
38
38
  }
39
+ /**
40
+ * Worst first, then by rule id so two runs of the same build agree.
41
+ *
42
+ * The order every format lists a page's findings in. Issues are sorted by
43
+ * `bySeverityThenReach` instead, which adds the reach a grouped issue has and a
44
+ * single page's findings do not.
45
+ */
46
+ function byImpactThenRule(a, b) {
47
+ return impactRank(a.impact) - impactRank(b.impact) || a.ruleId.localeCompare(b.ruleId);
48
+ }
39
49
  /** Violations at or above `threshold`, counted per rule per page. */
40
50
  function countAtOrAbove(audits, threshold) {
41
51
  let total = 0;
@@ -43,4 +53,4 @@ function countAtOrAbove(audits, threshold) {
43
53
  return total;
44
54
  }
45
55
  //#endregion
46
- export { impactRank as a, impactLabel as i, IMPACT_LEVELS as n, isImpactLevel as o, countAtOrAbove as r, meetsThreshold as s, DEFAULT_FAIL_ON as t };
56
+ export { impactLabel as a, meetsThreshold as c, countAtOrAbove as i, IMPACT_LEVELS as n, impactRank as o, byImpactThenRule as r, isImpactLevel as s, DEFAULT_FAIL_ON as t };
package/dist/index.d.ts CHANGED
@@ -136,12 +136,21 @@ declare const auditSchema: Schema<ObjectOf<{
136
136
  manual: OptionalSchema<boolean>;
137
137
  /** List every WCAG 2.2 A/AA criterion and what the run reached on it. */
138
138
  coverage: OptionalSchema<boolean>;
139
+ /** Path to a review record: what a person checked, for the criteria no engine reaches. */
140
+ review: OptionalSchema<string>;
141
+ /** Days after which a recorded review stops counting. */
142
+ reviewMaxAge: OptionalSchema<number>;
139
143
  /**
140
144
  * False is `--no-build`: never run the project's build or start its server to
141
145
  * find something to audit. Written in the positive because that is the state
142
146
  * being described, and because a config file has no flags to negate.
143
147
  */
144
148
  build: OptionalSchema<boolean>;
149
+ /**
150
+ * False is `--no-cache`: audit every page, reusing nothing an earlier run
151
+ * measured. Written in the positive for the same reason `build` is.
152
+ */
153
+ cache: OptionalSchema<boolean>;
145
154
  }>>;
146
155
  declare const configSchema: Schema<ObjectOf<{
147
156
  site: Schema<ObjectOf<{
@@ -232,12 +241,21 @@ declare const configSchema: Schema<ObjectOf<{
232
241
  manual: OptionalSchema<boolean>;
233
242
  /** List every WCAG 2.2 A/AA criterion and what the run reached on it. */
234
243
  coverage: OptionalSchema<boolean>;
244
+ /** Path to a review record: what a person checked, for the criteria no engine reaches. */
245
+ review: OptionalSchema<string>;
246
+ /** Days after which a recorded review stops counting. */
247
+ reviewMaxAge: OptionalSchema<number>;
235
248
  /**
236
249
  * False is `--no-build`: never run the project's build or start its server to
237
250
  * find something to audit. Written in the positive because that is the state
238
251
  * being described, and because a config file has no flags to negate.
239
252
  */
240
253
  build: OptionalSchema<boolean>;
254
+ /**
255
+ * False is `--no-cache`: audit every page, reusing nothing an earlier run
256
+ * measured. Written in the positive for the same reason `build` is.
257
+ */
258
+ cache: OptionalSchema<boolean>;
241
259
  }>>;
242
260
  }>>;
243
261
  /**
@@ -349,6 +367,24 @@ declare class StatementError extends Error {
349
367
  readonly name = "StatementError";
350
368
  }
351
369
  //#endregion
370
+ //#region src/audit/review.d.ts
371
+ declare const reviewSchema: Schema<ObjectOf<{
372
+ schemaVersion: Schema<number>;
373
+ /** Who carried out the review, for the record as a whole. */
374
+ reviewedBy: OptionalSchema<string>;
375
+ /** Keyed by success criterion number, e.g. '1.2.1'. */
376
+ criteria: DefaultedSchema<Record<string, ObjectOf<{
377
+ result: DefaultedSchema<"met" | "not-applicable" | "not-met" | "unreviewed">;
378
+ /** ISO date the check was done. Without it the entry cannot be aged. */
379
+ reviewedOn: OptionalSchema<string>;
380
+ /** Who did it, when that is not the person named on the record. */
381
+ reviewedBy: OptionalSchema<string>;
382
+ /** What was checked and what was found. Free text, for whoever reads it. */
383
+ note: OptionalSchema<string>;
384
+ }>>>;
385
+ }>>;
386
+ type ReviewRecord = Infer<typeof reviewSchema>;
387
+ //#endregion
352
388
  //#region src/statement/findings.d.ts
353
389
  /** One rule that failed, folded across every page it failed on. */
354
390
  interface AuditFinding {
@@ -394,6 +430,47 @@ declare function summariseAuditReport(value: unknown, source?: string): AuditSum
394
430
  /** Read and summarise a report written by `eaa-kit audit --format json`. */
395
431
  declare function readAuditReport(file: string, cwd?: string): Promise<AuditSummary>;
396
432
  //#endregion
433
+ //#region src/statement/evidence.d.ts
434
+ /**
435
+ * Whether the statement's claim survives contact with the evidence beside it.
436
+ *
437
+ * The README has said since 0.1 that "a statement claiming full conformance for
438
+ * a site that is not conformant is worse than no statement at all", and until
439
+ * now the tool did nothing about it: `compliance.status` came out of the config
440
+ * file, the audit report came in through `--audit`, and nothing ever put the
441
+ * two in the same room. A run could print a document claiming full conformance
442
+ * directly above the list of barriers that disproves it.
443
+ *
444
+ * This is the check that was missing. It is deliberately narrow: it does not
445
+ * decide whether a site is accessible, and it cannot — most of WCAG is not
446
+ * automatable and the whole tool is built around saying so. It refuses only
447
+ * claims the evidence in hand already contradicts, and warns where a document
448
+ * is citing evidence that no longer describes the site.
449
+ *
450
+ * Nothing here needs an audit report. Given none, only the dates are checked,
451
+ * and a project that has never run one gets exactly the statement it always
452
+ * got — the tool cannot audit what it was not shown.
453
+ */
454
+ /** How much a problem matters. `refuses` stops the statement being written. */
455
+ type EvidenceSeverity = 'refuses' | 'warns';
456
+ interface EvidenceProblem {
457
+ severity: EvidenceSeverity;
458
+ /** One sentence stating the contradiction, and one naming the fix. */
459
+ message: string;
460
+ }
461
+ interface EvidenceInput {
462
+ config: EaaConfig;
463
+ /** From `--audit`. Without it only the dates can be checked. */
464
+ audit?: AuditSummary;
465
+ /** From `--review`. Read for this check alone; it does not reach the text. */
466
+ review?: ReviewRecord;
467
+ /** Injectable so the ageing checks can be tested without waiting. */
468
+ today?: Date;
469
+ }
470
+ declare function checkStatementEvidence(input: EvidenceInput): EvidenceProblem[];
471
+ /** Whether any problem is bad enough to stop the document being written. */
472
+ declare function refuses(problems: readonly EvidenceProblem[]): boolean;
473
+ //#endregion
397
474
  //#region src/statement/html.d.ts
398
475
  /**
399
476
  * Markdown-to-HTML for exactly the subset the statement templates emit, and
@@ -447,6 +524,15 @@ interface RenderStatementOptions {
447
524
  * config lists. Left out, the statement says only what the config says.
448
525
  */
449
526
  audit?: AuditSummary;
527
+ /**
528
+ * What a person checked, from `eaa-kit checklist`.
529
+ *
530
+ * Reaches the document as one fact — how many of WCAG's success criteria were
531
+ * checked by hand, and when — and never as a verdict. What a review concluded
532
+ * about a criterion is a claim by a person, and the place for a claim in a
533
+ * statement is the barrier list, which a person writes.
534
+ */
535
+ review?: ReviewRecord;
450
536
  }
451
537
  interface RenderedStatement {
452
538
  markdown: string;
@@ -466,4 +552,4 @@ interface RenderedStatement {
466
552
  */
467
553
  declare function renderStatement(config: EaaConfig, options?: RenderStatementOptions): Promise<RenderedStatement>;
468
554
  //#endregion
469
- export { ASSESSMENT_METHODS, type AssessmentMethod, type AuditFinding, type AuditSummary, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, type ComplianceStatus, ConfigError, type Country, type EaaConfig, type EaaConfigInput, type HtmlDocumentOptions, ISSUE_REASONS, type IssueReason, type KnownIssue, type LoadConfigOptions, type LoadedConfig, type RenderStatementOptions, type RenderedStatement, STATEMENT_LOCALES, StatementError, type StatementLocale, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
555
+ export { ASSESSMENT_METHODS, type AssessmentMethod, type AuditFinding, type AuditSummary, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, type ComplianceStatus, ConfigError, type Country, type EaaConfig, type EaaConfigInput, type EvidenceInput, type EvidenceProblem, type EvidenceSeverity, type HtmlDocumentOptions, ISSUE_REASONS, type IssueReason, type KnownIssue, type LoadConfigOptions, type LoadedConfig, type RenderStatementOptions, type RenderedStatement, STATEMENT_LOCALES, StatementError, type StatementLocale, checkStatementEvidence, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, refuses, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as ASSESSMENT_METHODS, c as ConfigError, d as configSchema, f as defineConfig, i as loadConfig, l as ISSUE_REASONS, n as findConfigFile, o as COMPLIANCE_STATUSES, p as parseConfig, s as COUNTRIES, t as CONFIG_FILENAMES, u as STATEMENT_LOCALES } from "./load-yAR4wzez.js";
2
- import { a as summariseAuditReport, i as readAuditReport, n as toHtmlBody, o as StatementError, r as toHtmlDocument, t as renderStatement } from "./render-DbGOVmhx.js";
3
- export { ASSESSMENT_METHODS, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, ConfigError, ISSUE_REASONS, STATEMENT_LOCALES, StatementError, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
1
+ import { a as ASSESSMENT_METHODS, c as ConfigError, d as configSchema, f as defineConfig, i as loadConfig, l as ISSUE_REASONS, n as findConfigFile, o as COMPLIANCE_STATUSES, p as parseConfig, s as COUNTRIES, t as CONFIG_FILENAMES, u as STATEMENT_LOCALES } from "./load-BaWMa-G_.js";
2
+ import { a as summariseAuditReport, c as StatementError, i as readAuditReport, n as toHtmlBody, o as checkStatementEvidence, r as toHtmlDocument, s as refuses, t as renderStatement } from "./render-C1sUZZI_.js";
3
+ export { ASSESSMENT_METHODS, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, ConfigError, ISSUE_REASONS, STATEMENT_LOCALES, StatementError, checkStatementEvidence, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, refuses, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
@@ -1,7 +1,7 @@
1
- import { t as DEFAULT_FAIL_ON } from "./impact-DZt2oBCP.js";
2
- import { s as COUNTRIES, t as CONFIG_FILENAMES } from "./load-yAR4wzez.js";
1
+ import { t as DEFAULT_FAIL_ON } from "./impact-luGCnN8F.js";
2
+ import { s as COUNTRIES, t as CONFIG_FILENAMES } from "./load-BaWMa-G_.js";
3
3
  import { t as exists } from "./fs-BmPtmFke.js";
4
- import { l as warn, o as fail, s as note } from "./command-C3D7JWn6.js";
4
+ import { l as warn, o as fail, s as note } from "./command-3r4fht5f.js";
5
5
  import { readFile, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import pc from "picocolors";
@@ -0,0 +1,3 @@
1
+ import "./result-B7_zVxAX.js";
2
+ import { n as runJsdomAudit } from "./jsdom-CsW_xVcA.js";
3
+ export { runJsdomAudit };