eaa-kit 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +66 -11
  2. package/dist/astro/index.d.ts +1 -1
  3. package/dist/astro/index.js +1 -1
  4. package/dist/audit/runners/worker.js +5 -1
  5. package/dist/audit-DK2StT1_.js +2 -0
  6. package/dist/{audit-CPoZMXGM.js → audit-e_uixhPG.js} +123 -54
  7. package/dist/baseline-BscHfFSW.js +2 -0
  8. package/dist/{baseline-DB9CZGnV.js → baseline-D2qWmUC6.js} +40 -12
  9. package/dist/checklist-BtgkxMEa.js +44 -0
  10. package/dist/cli/index.js +153 -25
  11. package/dist/{collect-CFM8gEVv.js → collect-Cu1q9NTB.js} +34 -3
  12. package/dist/command-CZKp9Wi_.js +251 -0
  13. package/dist/{remediation-Dtowi2EC.js → completeness-Dwq3CPB3.js} +61 -43
  14. package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
  15. package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
  16. package/dist/coverage-7bQqWNfN.js +2 -0
  17. package/dist/{coverage-B4IBKMO8.js → coverage-BD2_CYns.js} +46 -8
  18. package/dist/{crawl-BiI1Lau_.js → crawl-BHkJmUkA.js} +119 -11
  19. package/dist/eleventy/index.d.ts +1 -1
  20. package/dist/eleventy/index.js +1 -1
  21. package/dist/fingerprint-BjYV_0F7.js +67 -0
  22. package/dist/{html-BGTO3ypW.js → html-Bmh7X_sM.js} +52 -30
  23. package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
  24. package/dist/index.d.ts +187 -5
  25. package/dist/index.js +3 -3
  26. package/dist/{init-DIWDE35F.js → init-BjKYttxc.js} +10 -4
  27. package/dist/{jsdom-DCpGSLfW.js → jsdom-CQar6OQS.js} +12 -4
  28. package/dist/jsdom-DMvU2h-f.js +3 -0
  29. package/dist/{json-QQuFIw1W.js → json-HJDgJp09.js} +1 -1
  30. package/dist/{json-DjEvy1nX.js → json-PNg6BeXp.js} +13 -18
  31. package/dist/load-Bc_xDGAh.js +2 -0
  32. package/dist/{load-5wRGLvub.js → load-vZdV_fD1.js} +137 -8
  33. package/dist/nuxt/index.d.ts +1 -1
  34. package/dist/nuxt/index.js +1 -1
  35. package/dist/{playwright-BWniOain.js → playwright-C-miJP2b.js} +32 -7
  36. package/dist/{pool-BMevaLWD.js → pool-dkexq5xo.js} +52 -2
  37. package/dist/{render-DrvXRCEn.js → render-CtsqR0k-.js} +150 -11
  38. package/dist/{result-DoamKFsp.js → result-DfUru6y_.js} +60 -3
  39. package/dist/review-CdMK2GpQ.js +161 -0
  40. package/dist/review-DIbOTmOM.js +2 -0
  41. package/dist/{run-DB34BSOZ.js → run-D7hdVXLA.js} +1 -1
  42. package/dist/{run-BMASMmwO.d.ts → run-YiqyQA2b.d.ts} +17 -0
  43. package/dist/{sarif-SR3_lLYd.js → sarif-BKvq_AUm.js} +25 -14
  44. package/dist/{schema-is6CGX2D.js → schema-DJSF4K05.js} +15 -1
  45. package/dist/statement/templates/es.en.md +125 -0
  46. package/dist/statement/templates/es.es.md +127 -0
  47. package/dist/statement/templates/fr.en.md +128 -0
  48. package/dist/statement/templates/fr.fr.md +131 -0
  49. package/dist/statement/templates/it.en.md +127 -0
  50. package/dist/statement/templates/it.it.md +130 -0
  51. package/dist/statement/templates/nl.en.md +125 -0
  52. package/dist/statement/templates/nl.nl.md +127 -0
  53. package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
  54. package/dist/vite/index.d.ts +1 -1
  55. package/dist/vite/index.js +1 -1
  56. package/dist/webpack/index.d.ts +1 -1
  57. package/dist/webpack/index.js +1 -1
  58. package/dist/worksheet-eUjwYLxC.js +63 -0
  59. package/package.json +7 -3
  60. package/dist/audit-CpXH2Mk8.js +0 -2
  61. package/dist/baseline-22Y1NWxM.js +0 -2
  62. package/dist/command-D8l_oYbV.js +0 -77
  63. package/dist/fingerprint-DRoneAjj.js +0 -20
  64. package/dist/jsdom-4IMzv0eE.js +0 -3
@@ -0,0 +1,251 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
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
84
+ //#region src/cli/command.ts
85
+ /**
86
+ * What every command does around the audit itself: say what is happening, run
87
+ * the engine, and put the document somewhere.
88
+ *
89
+ * `audit` and `baseline` are siblings — one reports what a run found and the
90
+ * other writes it down — so they take the same flags, choose between the same
91
+ * two engines and fail on the same setup problems. Keeping that in one place is
92
+ * what stops the two commands drifting into disagreeing about what a run is.
93
+ */
94
+ /** Progress and diagnostics go to stderr, so the report can be piped away. */
95
+ function note(message) {
96
+ process.stderr.write(pc.dim(`${message}\n`));
97
+ }
98
+ function warn(message) {
99
+ process.stderr.write(`${pc.yellow("warning")} ${message}\n`);
100
+ }
101
+ function fail(message) {
102
+ process.stderr.write(`${pc.red("error")} ${message}\n`);
103
+ }
104
+ /**
105
+ * Advice the reader should not miss, without the `warning` prefix: nothing has
106
+ * gone wrong, but what happens next is theirs to get right.
107
+ */
108
+ function advise(message) {
109
+ process.stderr.write(pc.yellow(`${message}\n`));
110
+ }
111
+ /**
112
+ * Audit the pages with whichever engine was asked for.
113
+ *
114
+ * Returns undefined when the browser was asked for and is not usable, having
115
+ * already said so: Playwright missing is a setup problem with a specific fix,
116
+ * not a crash, and both commands turn it into exit 2.
117
+ */
118
+ async function runEngine(pages, options) {
119
+ const runnerOptions = {
120
+ cwd: options.cwd,
121
+ ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl },
122
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
123
+ };
124
+ if (!options.browser) {
125
+ const { runPooledAudit } = await import("./pool-dkexq5xo.js");
126
+ return runPooledAudit(pages, {
127
+ ...runnerOptions,
128
+ ...options.fast ? { fast: true } : {},
129
+ ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
130
+ });
131
+ }
132
+ const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-C-miJP2b.js");
133
+ try {
134
+ return await runBrowserAudit(options.directory, pages, {
135
+ ...runnerOptions,
136
+ ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
137
+ ...options.headers === void 0 ? {} : { headers: options.headers }
138
+ });
139
+ } catch (cause) {
140
+ if (cause instanceof BrowserUnavailableError) {
141
+ fail(cause.message);
142
+ return;
143
+ }
144
+ throw cause;
145
+ }
146
+ }
147
+ /**
148
+ * Write a document to `output`, or to stdout when there is none. Parent
149
+ * directories are created, since a report path in CI usually names one that is
150
+ * not there yet.
151
+ */
152
+ async function emitDocument(body, output, cwd) {
153
+ if (output === void 0) {
154
+ process.stdout.write(body);
155
+ return;
156
+ }
157
+ const target = path.resolve(cwd, output);
158
+ await mkdir(path.dirname(target), { recursive: true });
159
+ await writeFile(target, body, "utf8");
160
+ }
161
+ /**
162
+ * Audit defaults from the project's config file.
163
+ *
164
+ * `audit` and `baseline` are run from a build script over and over with the
165
+ * same six flags, and the flags are the only place to say them: the config file
166
+ * has existed since 0.2 and served the statement alone. An `audit` block there
167
+ * is that list written once.
168
+ *
169
+ * Everything it returns is a default. The flags are merged over it by the
170
+ * caller, because the file is the project's usual answer and a flag is somebody
171
+ * asking for something else on this run.
172
+ *
173
+ * No config file at all is not an error, unlike for `statement`: this command
174
+ * has always run against projects that have never heard of one. A file that
175
+ * exists and cannot be read is exit 2 — it was written to be used, and running
176
+ * on different settings than it names would be worse than stopping.
177
+ */
178
+ async function auditDefaults(options = {}) {
179
+ const { loadAuditConfig } = await import("./load-Bc_xDGAh.js");
180
+ const loaded = await loadAuditConfig({
181
+ ...options.cwd ? { cwd: options.cwd } : {},
182
+ ...options.config ? { path: options.config } : {}
183
+ });
184
+ if (!loaded?.audit) return {};
185
+ note(`Defaults from ${path.basename(loaded.path)}`);
186
+ return loaded.audit;
187
+ }
188
+ function auditInvocation(dir, defaults, flags) {
189
+ const { build, config: _config, header, basicAuth, ...typed } = flags;
190
+ const { dir: configDir, build: configBuild, headers: _fromFile, ...fromConfig } = defaults;
191
+ const headers = requestHeaders({
192
+ ...header ? { header } : {},
193
+ ...basicAuth ? { basicAuth } : {}
194
+ });
195
+ return {
196
+ dir: dir ?? configDir,
197
+ options: {
198
+ ...fromConfig,
199
+ ...typed,
200
+ ...build === false || configBuild === false ? { noBuild: true } : {},
201
+ ...headers === void 0 ? {} : { headers }
202
+ }
203
+ };
204
+ }
205
+ /**
206
+ * The same, for `baseline`, which reads the defaults that mean the same thing
207
+ * to it.
208
+ *
209
+ * Deliberately a subset. `output` names where the report goes for one command
210
+ * and where the baseline goes for the other, so carrying it across would write
211
+ * a baseline over the path somebody set aside for a report; `format`, `failOn`
212
+ * and `baseline` all describe a verdict this command does not reach.
213
+ */
214
+ function baselineInvocation(dir, defaults, flags) {
215
+ const { config: _config, header, basicAuth, ...typed } = flags;
216
+ const headers = requestHeaders({
217
+ ...header ? { header } : {},
218
+ ...basicAuth ? { basicAuth } : {}
219
+ });
220
+ return {
221
+ dir: dir ?? defaults.dir ?? "./dist",
222
+ options: {
223
+ ...baselineDefaults(defaults),
224
+ ...typed,
225
+ ...headers === void 0 ? {} : { headers }
226
+ }
227
+ };
228
+ }
229
+ function baselineDefaults(config) {
230
+ return pick(config, [
231
+ "include",
232
+ "exclude",
233
+ "baseUrl",
234
+ "url",
235
+ "allowRemote",
236
+ "ignoreRobots",
237
+ "sitemap",
238
+ "maxPages",
239
+ "maxDepth",
240
+ "browser",
241
+ "concurrency"
242
+ ]);
243
+ }
244
+ /** Copies the keys that are actually set, so nothing spreads an undefined over a real value. */
245
+ function pick(source, keys) {
246
+ const out = {};
247
+ for (const key of keys) if (source[key] !== void 0) out[key] = source[key];
248
+ return out;
249
+ }
250
+ //#endregion
251
+ 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,45 +1,6 @@
1
- import { a as impactRank } from "./impact-DZt2oBCP.js";
2
- import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
3
- import { a as findingElements } from "./result-DoamKFsp.js";
4
- //#region src/audit/completeness.ts
5
- /**
6
- * Fold what the collector knew together with what the engine managed.
7
- *
8
- * The audits carry the second half: a page with an `error` was collected and
9
- * then not audited, and is as unmeasured as one that was never fetched.
10
- */
11
- function runCompleteness(audits, collection) {
12
- const errored = audits.filter((audit) => audit.error).length;
13
- return {
14
- ...collection,
15
- audited: audits.length - errored,
16
- errored,
17
- complete: collection.unreachable.length === 0 && !collection.truncated && errored === 0
18
- };
19
- }
20
- /**
21
- * What was missed, as phrases both reports print.
22
- *
23
- * Separate clauses rather than one total, for the same reason the coverage
24
- * parts are: a page that could not be fetched and a page that could not be
25
- * parsed are different problems with different fixes, and summing them would
26
- * name neither.
27
- */
28
- function missedParts(completeness) {
29
- const parts = [];
30
- if (completeness.unreachable.length > 0) parts.push(`${completeness.unreachable.length} could not be reached`);
31
- if (completeness.errored > 0) parts.push(`${completeness.errored} could not be audited`);
32
- if (completeness.truncated) parts.push("the run stopped at its page limit");
33
- return parts;
34
- }
35
- /** How the pages were found, in words, for the run details both reports show. */
36
- function discoveryLabel(discovery) {
37
- switch (discovery) {
38
- case "directory": return "files in the build directory";
39
- case "sitemap": return "sitemap.xml and links";
40
- case "links": return "links from the entry page";
41
- }
42
- }
1
+ import { o as impactRank } from "./impact-luGCnN8F.js";
2
+ import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
3
+ import { o as findingElements } from "./result-DfUru6y_.js";
43
4
  /** Whether this element looks like one component rendered on many pages. */
44
5
  function isShared(element) {
45
6
  return element.pages.length >= 3;
@@ -95,6 +56,23 @@ function groupIssues(audits) {
95
56
  return issues;
96
57
  }
97
58
  /**
59
+ * What the grouped issues add up to: distinct elements, and the violations they
60
+ * account for. Both reports open their issues section with these two numbers,
61
+ * and say something different when they are equal.
62
+ */
63
+ function issueTotals(issues) {
64
+ let elements = 0;
65
+ let occurrences = 0;
66
+ for (const issue of issues) {
67
+ elements += issue.elements.length;
68
+ occurrences += issue.occurrences;
69
+ }
70
+ return {
71
+ elements,
72
+ occurrences
73
+ };
74
+ }
75
+ /**
98
76
  * Unevaluated rules folded across the site, sorted by rule id.
99
77
  *
100
78
  * Both reports list these once at the end rather than under every page: on a
@@ -318,4 +296,44 @@ function remediationFor(ruleId, framework) {
318
296
  };
319
297
  }
320
298
  //#endregion
321
- export { isShared as a, runCompleteness as c, groupIssues as i, blindRules as n, discoveryLabel as o, coverageParts as r, missedParts as s, remediationFor as t };
299
+ //#region src/audit/completeness.ts
300
+ /**
301
+ * Fold what the collector knew together with what the engine managed.
302
+ *
303
+ * The audits carry the second half: a page with an `error` was collected and
304
+ * then not audited, and is as unmeasured as one that was never fetched.
305
+ */
306
+ function runCompleteness(audits, collection) {
307
+ const errored = audits.filter((audit) => audit.error).length;
308
+ return {
309
+ ...collection,
310
+ audited: audits.length - errored,
311
+ errored,
312
+ complete: collection.unreachable.length === 0 && !collection.truncated && errored === 0
313
+ };
314
+ }
315
+ /**
316
+ * What was missed, as phrases both reports print.
317
+ *
318
+ * Separate clauses rather than one total, for the same reason the coverage
319
+ * parts are: a page that could not be fetched and a page that could not be
320
+ * parsed are different problems with different fixes, and summing them would
321
+ * name neither.
322
+ */
323
+ function missedParts(completeness) {
324
+ const parts = [];
325
+ if (completeness.unreachable.length > 0) parts.push(`${completeness.unreachable.length} could not be reached`);
326
+ if (completeness.errored > 0) parts.push(`${completeness.errored} could not be audited`);
327
+ if (completeness.truncated) parts.push("the run stopped at its page limit");
328
+ return parts;
329
+ }
330
+ /** How the pages were found, in words, for the run details both reports show. */
331
+ function discoveryLabel(discovery) {
332
+ switch (discovery) {
333
+ case "directory": return "files in the build directory";
334
+ case "sitemap": return "sitemap.xml and links";
335
+ case "links": return "links from the entry page";
336
+ }
337
+ }
338
+ //#endregion
339
+ export { blindRules as a, isShared as c, remediationFor as i, issueTotals as l, missedParts as n, coverageParts as o, runCompleteness as r, groupIssues as s, discoveryLabel as t };
@@ -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 };
@@ -0,0 +1,2 @@
1
+ import { t as WCAG22_AA_CRITERIA } from "./coverage-BD2_CYns.js";
2
+ export { WCAG22_AA_CRITERIA };
@@ -1,4 +1,5 @@
1
- import { n as ENGINE_BLIND_RULES, t as DEFAULT_TAGS, u as successCriteria } from "./result-DoamKFsp.js";
1
+ import { d as successCriteria, n as DEFAULT_TAGS, r as ENGINE_BLIND_RULES } from "./result-DfUru6y_.js";
2
+ import { s as criterionReview } from "./review-CdMK2GpQ.js";
2
3
  import axe from "axe-core";
3
4
  //#region src/audit/manual.ts
4
5
  /**
@@ -463,10 +464,11 @@ function blindedRules(audits) {
463
464
  * the criterion is met on this site, and saying otherwise would be a coverage
464
465
  * claim resting on an empty page.
465
466
  */
466
- function buildCoverage(audits, tags = DEFAULT_TAGS) {
467
+ function buildCoverage(audits, tags = DEFAULT_TAGS, review) {
467
468
  const byCriterion = rulesByCriterion(tags);
468
469
  const decided = decidedRules(audits);
469
470
  const blinded = blindedRules(audits);
471
+ const jsdomBlindApplies = audits.length === 0 || audits.some((audit) => audit.engine !== "browser");
470
472
  const criteria = WCAG22_AA_CRITERIA.map((criterion) => {
471
473
  const rules = byCriterion.get(criterion.number) ?? [];
472
474
  if (rules.length === 0) return {
@@ -481,7 +483,7 @@ function buildCoverage(audits, tags = DEFAULT_TAGS) {
481
483
  rules,
482
484
  browserWouldAnswer: false
483
485
  };
484
- const engineBlind = rules.filter((rule) => blinded.has(rule) || ENGINE_BLIND_RULES[rule] !== void 0);
486
+ const engineBlind = rules.filter((rule) => blinded.has(rule) || jsdomBlindApplies && ENGINE_BLIND_RULES[rule] !== void 0);
485
487
  if (engineBlind.length > 0) return {
486
488
  ...criterion,
487
489
  status: "not-evaluated",
@@ -495,15 +497,36 @@ function buildCoverage(audits, tags = DEFAULT_TAGS) {
495
497
  browserWouldAnswer: false
496
498
  };
497
499
  });
498
- const count = (status) => criteria.filter((criterion) => criterion.status === status).length;
500
+ const reviewed = review === void 0 ? criteria : criteria.map(withReview(review));
501
+ const count = (status) => reviewed.filter((criterion) => criterion.status === status).length;
502
+ const counted = reviewed.filter((criterion) => criterion.review?.counts === true);
499
503
  return {
500
- criteria,
504
+ criteria: reviewed,
501
505
  evaluated: count("evaluated"),
502
506
  notEvaluated: count("not-evaluated"),
503
507
  nothingToCheck: count("nothing-to-check"),
504
508
  noAutomatedRule: count("no-automated-rule"),
505
- browserWouldAnswer: criteria.filter((criterion) => criterion.browserWouldAnswer).length,
506
- total: WCAG22_AA_CRITERIA.length
509
+ browserWouldAnswer: reviewed.filter((criterion) => criterion.browserWouldAnswer).length,
510
+ total: WCAG22_AA_CRITERIA.length,
511
+ reviewed: counted.length,
512
+ reviewedNotMet: counted.filter((criterion) => criterion.review?.result === "not-met").length,
513
+ reviewNotCounted: reviewed.filter((criterion) => criterion.review?.counts === false).length
514
+ };
515
+ }
516
+ /**
517
+ * Attach what a person recorded, without letting it move what the engine found.
518
+ *
519
+ * `status` is untouched here, deliberately and in every branch: a review adds a
520
+ * second kind of claim beside the run's own, and the moment it could rewrite
521
+ * one the report would stop being a record of what was measured.
522
+ */
523
+ function withReview(review) {
524
+ return (criterion) => {
525
+ const recorded = criterionReview(criterion.number, review, criterion.status === "evaluated");
526
+ return recorded === void 0 ? criterion : {
527
+ ...criterion,
528
+ review: recorded
529
+ };
507
530
  };
508
531
  }
509
532
  /**
@@ -517,5 +540,20 @@ function coverageSummary(coverage) {
517
540
  const unautomatable = coverage.noAutomatedRule;
518
541
  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}.`;
519
542
  }
543
+ /**
544
+ * What a person added to the run, in one sentence, or nothing when nobody did.
545
+ *
546
+ * Kept out of `coverageSummary` so the sentence about the engine's reach does
547
+ * not change shape depending on whether a review was supplied — and because a
548
+ * reader has to be able to tell the two claims apart at a glance: one is what
549
+ * an engine measured, the other is what somebody says they checked.
550
+ */
551
+ function reviewSummary(coverage) {
552
+ if (coverage.reviewed === 0 && coverage.reviewNotCounted === 0) return void 0;
553
+ const parts = [`A person recorded a result for ${coverage.reviewed} of the criteria this run did not reach`];
554
+ if (coverage.reviewedNotMet > 0) parts.push(`${coverage.reviewedNotMet} of them as not met`);
555
+ if (coverage.reviewNotCounted > 0) parts.push(`${coverage.reviewNotCounted} further entries were not counted`);
556
+ return `${parts.join("; ")}. This is a claim by a person, not a measurement.`;
557
+ }
520
558
  //#endregion
521
- export { understandingUrl as i, coverageSummary as n, manualCheckFor as r, buildCoverage as t };
559
+ export { rulesByCriterion as a, reviewSummary as i, buildCoverage as n, manualCheckFor as o, coverageSummary as r, understandingUrl as s, WCAG22_AA_CRITERIA as t };