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,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 { s as findingElements } from "./result-B7_zVxAX.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,2 @@
1
+ import { o as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-BpE7DbDq.js";
2
+ export { BaselineError, applyBaseline, readBaseline };
@@ -0,0 +1,219 @@
1
+ import { c as nullable, d as optional, h as string, i as enumeration, l as number, m as safeParse, t as array, u as object, y as withDefault } from "./schema-DJSF4K05.js";
2
+ import { a as isoDate } from "./text-BF1LHMiV.js";
3
+ import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { createHash } from "node:crypto";
6
+ const MANIFEST_FILE = "manifest.json";
7
+ const outcomeSchema = object({
8
+ ruleId: string(),
9
+ help: withDefault(string(), () => ""),
10
+ helpUrl: withDefault(string(), () => ""),
11
+ successCriteria: withDefault(array(string()), () => []),
12
+ enClauses: withDefault(array(string()), () => []),
13
+ tags: withDefault(array(string()), () => [])
14
+ });
15
+ const nodeSchema = object({
16
+ html: string(),
17
+ target: withDefault(array(string()), () => []),
18
+ failureSummary: optional(string())
19
+ });
20
+ const findingSchema = object({
21
+ ruleId: string(),
22
+ help: withDefault(string(), () => ""),
23
+ helpUrl: withDefault(string(), () => ""),
24
+ successCriteria: withDefault(array(string()), () => []),
25
+ enClauses: withDefault(array(string()), () => []),
26
+ tags: withDefault(array(string()), () => []),
27
+ impact: withDefault(nullable(string()), () => null),
28
+ nodes: withDefault(array(nodeSchema), () => [])
29
+ });
30
+ const incompleteSchema = object({
31
+ ruleId: string(),
32
+ help: withDefault(string(), () => ""),
33
+ helpUrl: withDefault(string(), () => ""),
34
+ successCriteria: withDefault(array(string()), () => []),
35
+ enClauses: withDefault(array(string()), () => []),
36
+ tags: withDefault(array(string()), () => []),
37
+ impact: withDefault(nullable(string()), () => null),
38
+ nodes: withDefault(array(nodeSchema), () => []),
39
+ reason: enumeration(["needs-review", "engine-limitation"]),
40
+ reasonDetail: withDefault(string(), () => "")
41
+ });
42
+ const entrySchema = object({
43
+ schemaVersion: number(),
44
+ /** The page this describes, as the run names it. */
45
+ page: string(),
46
+ /** ISO date the result was produced, printed by the reports. */
47
+ cachedOn: string(),
48
+ violations: withDefault(array(findingSchema), () => []),
49
+ incomplete: withDefault(array(incompleteSchema), () => []),
50
+ passes: withDefault(array(outcomeSchema), () => []),
51
+ inapplicable: withDefault(array(outcomeSchema), () => [])
52
+ });
53
+ const manifestSchema = object({
54
+ schemaVersion: number(),
55
+ /** Hash of everything in `CacheInputs`. A mismatch discards the cache. */
56
+ key: string(),
57
+ createdOn: withDefault(string(), () => "")
58
+ });
59
+ /**
60
+ * The identity of a page's content.
61
+ *
62
+ * Path and HTML together: the same markup at two paths is two pages, because
63
+ * the path is what a baseline, a diff and every report key on. A full digest
64
+ * rather than the sixteen characters `elementFingerprint` uses — that one is
65
+ * read by people in a committed file, and this one is read only by the tool,
66
+ * where a collision would serve one page's verdicts for another's.
67
+ */
68
+ function pageKey(page) {
69
+ return createHash("sha256").update(`${page.relativePath}\n${page.html}`).digest("hex");
70
+ }
71
+ /**
72
+ * The identity of everything else the verdict depends on.
73
+ *
74
+ * Headers are hashed with the rest and never recorded: what a run sent can
75
+ * change what it was served, so it belongs in the key, and a credential does
76
+ * not belong on disk.
77
+ */
78
+ function inputsKey(inputs) {
79
+ const headers = Object.entries(inputs.headers ?? {}).map(([name, value]) => `${name.toLowerCase()}=${value}`).sort().join("\n");
80
+ return createHash("sha256").update([
81
+ `tool=${inputs.toolVersion}`,
82
+ `axe=${inputs.axeVersion}`,
83
+ `tags=${[...inputs.tags].join(",")}`,
84
+ `engine=${inputs.engine}`,
85
+ `fast=${inputs.fast}`,
86
+ `viewport=${inputs.viewport ? `${inputs.viewport.width}x${inputs.viewport.height}` : ""}`,
87
+ `baseUrl=${inputs.baseUrl ?? ""}`,
88
+ `timeout=${inputs.timeoutMs ?? ""}`,
89
+ `headers=${createHash("sha256").update(headers).digest("hex")}`
90
+ ].join("\n")).digest("hex");
91
+ }
92
+ /**
93
+ * Open the cache for a run, discarding it if anything about the run changed.
94
+ *
95
+ * Everything is read up front and held in memory: an audit asks about every
96
+ * page before it audits any, the entries are small, and a synchronous answer
97
+ * is what lets the caller decide whether to import an engine at all.
98
+ */
99
+ async function openCache(inputs, options = {}) {
100
+ const root = path.resolve(options.cwd ?? process.cwd(), options.dir ?? ".eaa-kit/cache");
101
+ const key = inputsKey(inputs);
102
+ const today = isoDate(options.today ?? /* @__PURE__ */ new Date());
103
+ const entries = await manifestMatches(root, key) ? await readEntries(root) : /* @__PURE__ */ new Map();
104
+ const written = /* @__PURE__ */ new Map();
105
+ return {
106
+ get(page) {
107
+ return entries.get(pageKey(page));
108
+ },
109
+ put(page, audit) {
110
+ if (audit.error !== void 0) return;
111
+ written.set(pageKey(page), toEntry(page, audit, today));
112
+ },
113
+ async flush() {
114
+ if (written.size === 0) return;
115
+ try {
116
+ await mkdir(root, { recursive: true });
117
+ await writeFile(path.join(root, MANIFEST_FILE), `${JSON.stringify({
118
+ schemaVersion: 1,
119
+ key,
120
+ createdOn: today
121
+ }, null, 2)}\n`, "utf8");
122
+ await Promise.all([...written].map(([hash, entry]) => writeFile(path.join(root, `${hash}.json`), `${JSON.stringify(entry)}\n`, "utf8")));
123
+ } catch {}
124
+ }
125
+ };
126
+ }
127
+ /**
128
+ * Whether the stored cache was produced under the same conditions as this run.
129
+ *
130
+ * A mismatch empties the directory rather than leaving entries to be stepped
131
+ * over. They can never be read again — the key that would reach them is gone —
132
+ * and leaving them would grow `.eaa-kit/` without limit.
133
+ */
134
+ async function manifestMatches(root, key) {
135
+ let raw;
136
+ try {
137
+ raw = await readFile(path.join(root, MANIFEST_FILE), "utf8");
138
+ } catch {
139
+ return false;
140
+ }
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(raw);
144
+ } catch {
145
+ await discard(root);
146
+ return false;
147
+ }
148
+ const result = safeParse(manifestSchema, parsed);
149
+ if (!result.success || result.data.schemaVersion !== 1 || result.data.key !== key) {
150
+ await discard(root);
151
+ return false;
152
+ }
153
+ return true;
154
+ }
155
+ async function discard(root) {
156
+ try {
157
+ await rm(root, {
158
+ recursive: true,
159
+ force: true
160
+ });
161
+ } catch {}
162
+ }
163
+ /** Every entry in the directory, keyed by page hash. A bad file is skipped. */
164
+ async function readEntries(root) {
165
+ let files;
166
+ try {
167
+ files = await readdir(root);
168
+ } catch {
169
+ return /* @__PURE__ */ new Map();
170
+ }
171
+ const entries = /* @__PURE__ */ new Map();
172
+ await Promise.all(files.filter((file) => file.endsWith(".json") && file !== MANIFEST_FILE).map(async (file) => {
173
+ try {
174
+ const parsed = JSON.parse(await readFile(path.join(root, file), "utf8"));
175
+ const result = safeParse(entrySchema, parsed);
176
+ if (!result.success || result.data.schemaVersion !== 1) return;
177
+ entries.set(path.basename(file, ".json"), result.data);
178
+ } catch {}
179
+ }));
180
+ return entries;
181
+ }
182
+ /** The parts of a result that describe the page rather than the run. */
183
+ function toEntry(page, audit, today) {
184
+ return {
185
+ schemaVersion: 1,
186
+ page: page.relativePath,
187
+ cachedOn: today,
188
+ violations: audit.violations,
189
+ incomplete: audit.incomplete,
190
+ passes: audit.passes,
191
+ inapplicable: audit.inapplicable
192
+ };
193
+ }
194
+ /**
195
+ * A stored result, put back together as this run's audit.
196
+ *
197
+ * The page's own facts come from the cache; everything about *this* run — where
198
+ * the file is, what URL it is audited under, which engine is running — comes
199
+ * from the run, because those are not properties of the result. `durationMs` is
200
+ * zero rather than the duration of a run that happened on another day, and
201
+ * `reused` is what stops every consumer downstream from mistaking this for work
202
+ * that was done just now.
203
+ */
204
+ function fromEntry(entry, page, context) {
205
+ return {
206
+ relativePath: page.relativePath,
207
+ absolutePath: page.absolutePath,
208
+ url: context.url,
209
+ engine: context.engine,
210
+ violations: entry.violations,
211
+ incomplete: entry.incomplete,
212
+ passes: entry.passes,
213
+ inapplicable: entry.inapplicable,
214
+ durationMs: 0,
215
+ reused: { on: entry.cachedOn }
216
+ };
217
+ }
218
+ //#endregion
219
+ export { fromEntry, openCache };
@@ -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-3r4fht5f.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-n1X9CkiG.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-BIxMtbUd.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 };
package/dist/cli/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { a as impactRank, n as IMPACT_LEVELS, o as isImpactLevel, s as meetsThreshold, t as DEFAULT_FAIL_ON } from "../impact-DZt2oBCP.js";
2
+ import { c as meetsThreshold, n as IMPACT_LEVELS, o as impactRank, s as isImpactLevel, t as DEFAULT_FAIL_ON } from "../impact-luGCnN8F.js";
3
3
  import { c as nullable, d as optional, h as string, l as number, m as safeParse, n as boolean, p as record, t as array, u as object, y as withDefault } from "../schema-DJSF4K05.js";
4
- import { c as ConfigError, i as loadConfig, s as COUNTRIES, u as STATEMENT_LOCALES } from "../load-yAR4wzez.js";
5
- import { i as readAuditReport, o as StatementError, t as renderStatement } from "../render-DbGOVmhx.js";
6
- import { n as count, t as collapse } from "../text-CKKpzkYM.js";
4
+ import { c as ConfigError, i as loadConfig, s as COUNTRIES, u as STATEMENT_LOCALES } from "../load-BaWMa-G_.js";
5
+ import { c as StatementError, i as readAuditReport, o as checkStatementEvidence, s as refuses, t as renderStatement } from "../render-C1sUZZI_.js";
6
+ import { n as count, t as collapse } from "../text-BF1LHMiV.js";
7
7
  import { t as TOOL_VERSION } from "../version-B3v4rNoG.js";
8
8
  import { t as elementFingerprint } from "../fingerprint-BjYV_0F7.js";
9
- import { i as buildBaseline, n as DEFAULT_BASELINE_FILE, s as writeBaseline, t as BaselineError } from "../baseline-CuAS2lMk.js";
10
- import { n as runAuditCommand, r as resolvePages, t as OUTPUT_FORMATS } from "../audit-B282A3EA.js";
11
- import { a as emitDocument, c as runEngine, i as baselineInvocation, l as warn, n as auditDefaults, o as fail, r as auditInvocation, s as note, t as advise } from "../command-C3D7JWn6.js";
9
+ import { a as pruneBaseline, c as writeBaseline, i as buildBaseline, n as DEFAULT_BASELINE_FILE, o as readBaseline, r as applyBaseline, t as BaselineError } from "../baseline-BpE7DbDq.js";
10
+ import { a as emitDocument, c as runEngine, d as parseHeader, i as baselineInvocation, l as warn, n as auditDefaults, o as fail, r as auditInvocation, s as note, t as advise, u as basicAuth } from "../command-3r4fht5f.js";
11
+ import { t as DEFAULT_REVIEW_FILE } from "../review-CdMK2GpQ.js";
12
+ import { n as runAuditCommand, r as resolvePages, t as OUTPUT_FORMATS } from "../audit-DRjAY2iY.js";
12
13
  import { enableCompileCache } from "node:module";
13
14
  import { readFile } from "node:fs/promises";
14
15
  import path from "node:path";
@@ -42,6 +43,7 @@ async function runBaselineCommand(dir, options = {}) {
42
43
  const baseUrl = options.baseUrl ?? origin;
43
44
  const audits = await runEngine(pages, {
44
45
  cwd,
46
+ ...options.headers === void 0 ? {} : { headers: options.headers },
45
47
  ...baseUrl === void 0 ? {} : { baseUrl },
46
48
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
47
49
  ...options.browser ? { browser: true } : {},
@@ -60,11 +62,12 @@ async function runBaselineCommand(dir, options = {}) {
60
62
  exitCode: 2
61
63
  };
62
64
  }
65
+ const target = options.output ?? "eaa-baseline.json";
66
+ if (options.prune) return await prune(audits, target, cwd);
63
67
  const baseline = buildBaseline(audits, {
64
68
  ...options.note ? { note: options.note } : {},
65
69
  ...options.expiresOn ? { expiresOn: options.expiresOn } : {}
66
70
  });
67
- const target = options.output ?? "eaa-baseline.json";
68
71
  try {
69
72
  await writeBaseline(target, baseline, cwd);
70
73
  } catch (cause) {
@@ -78,13 +81,60 @@ async function runBaselineCommand(dir, options = {}) {
78
81
  throw cause;
79
82
  }
80
83
  const entries = baseline.entries.length;
81
- note(`Wrote ${entries} ${entries === 1 ? "entry" : "entries"} to ${target}`);
84
+ note(`Wrote ${count(entries, "entry")} to ${target}`);
82
85
  if (entries > 0) advise("These are barriers, not exceptions. Commit the file, then work the list down.");
83
86
  return {
84
87
  entries,
85
88
  exitCode: 0
86
89
  };
87
90
  }
91
+ /**
92
+ * `--prune`: take out what this run shows is gone, and add nothing.
93
+ *
94
+ * The audit already tells anybody running it that entries no longer match and
95
+ * can be removed. Acting on that advice meant editing JSON by hand, entry by
96
+ * entry, against a file whose whole purpose is that nobody has to remember what
97
+ * is in it — so most people did not, and a baseline accumulated barriers that
98
+ * were fixed years ago while looking exactly like one that had not been read.
99
+ *
100
+ * Recording a new baseline is not the same operation and is not a substitute:
101
+ * it accepts whatever the site fails today, which on a bad day quietly adopts a
102
+ * barrier nobody agreed to.
103
+ */
104
+ async function prune(audits, target, cwd) {
105
+ let existing;
106
+ try {
107
+ existing = await readBaseline(target, cwd);
108
+ } catch (cause) {
109
+ if (cause instanceof BaselineError) {
110
+ fail(cause.message);
111
+ return {
112
+ entries: 0,
113
+ exitCode: 2
114
+ };
115
+ }
116
+ throw cause;
117
+ }
118
+ const outcome = applyBaseline(audits, existing);
119
+ const { baseline, removed } = pruneBaseline(existing, outcome.stale);
120
+ if (removed.length === 0) {
121
+ note(`Nothing to remove from ${target}: every entry still matches something on this run`);
122
+ return {
123
+ entries: existing.entries.length,
124
+ removed: 0,
125
+ exitCode: 0
126
+ };
127
+ }
128
+ await writeBaseline(target, baseline, cwd);
129
+ note(`Removed ${count(removed.length, "entry")} from ${target}; ${count(baseline.entries.length, "entry")} left`);
130
+ for (const entry of removed) note(` ${entry.page} ${entry.ruleId}`);
131
+ if (outcome.expired.length > 0) advise(`${count(outcome.expired.length, "entry")} have expired and suppress nothing; --prune leaves them for you to decide about`);
132
+ return {
133
+ entries: baseline.entries.length,
134
+ removed: removed.length,
135
+ exitCode: 0
136
+ };
137
+ }
88
138
  var DiffError = class extends Error {
89
139
  name = "DiffError";
90
140
  };
@@ -105,6 +155,8 @@ const reportSchema = object({
105
155
  pages: withDefault(array(object({
106
156
  path: string(),
107
157
  error: withDefault(nullable(string()), () => null),
158
+ /** Present when the later run reused this page rather than auditing it. */
159
+ reusedFrom: optional(string()),
108
160
  violations: withDefault(array(object({
109
161
  ruleId: string(),
110
162
  impact: withDefault(nullable(string()), () => null),
@@ -155,9 +207,22 @@ function entriesOf(report) {
155
207
  }
156
208
  return entries;
157
209
  }
158
- /** Pages a run reached a verdict on. A page that errored is not one of them. */
210
+ /**
211
+ * Pages a run reached a verdict on *itself*.
212
+ *
213
+ * A page that errored is not one of them, and neither is a page whose result
214
+ * was reused from a cache: this comparison exists to say what changed between
215
+ * two runs, and a run that did not look at a page learned nothing about it. The
216
+ * difference matters in one direction only, and it is the direction that lies —
217
+ * a violation present in the earlier report and absent from a reused page would
218
+ * otherwise be announced as fixed, which is a changelog of work nobody did.
219
+ *
220
+ * A report written before reuse existed carries no marker, and is read exactly
221
+ * as it was meant when it was written: those runs audited every page they
222
+ * listed.
223
+ */
159
224
  function auditedPages(report) {
160
- return new Set(report.pages.filter((page) => page.error === null).map((page) => page.path));
225
+ return new Set(report.pages.filter((page) => page.error === null && page.reusedFrom === void 0).map((page) => page.path));
161
226
  }
162
227
  function sideOf(report) {
163
228
  return {
@@ -353,10 +418,28 @@ async function runStatementCommand(options = {}) {
353
418
  });
354
419
  let audit;
355
420
  if (options.audit) audit = await readAuditReport(options.audit, options.cwd ?? process.cwd());
421
+ let review;
422
+ if (options.review) {
423
+ const { readReview } = await import("../review-DIbOTmOM.js");
424
+ review = await readReview(options.review, options.cwd ?? process.cwd());
425
+ }
426
+ const problems = checkStatementEvidence({
427
+ config,
428
+ ...audit ? { audit } : {},
429
+ ...review ? { review } : {}
430
+ });
431
+ for (const problem of problems) if (problem.severity === "refuses") fail(problem.message);
432
+ else advise(problem.message);
433
+ if (refuses(problems)) return {
434
+ document: "",
435
+ format,
436
+ exitCode: 2
437
+ };
356
438
  const statement = await renderStatement(config, {
357
439
  ...options.locale ? { locale: options.locale } : {},
358
440
  ...options.country ? { country: options.country } : {},
359
- ...audit ? { audit } : {}
441
+ ...audit ? { audit } : {},
442
+ ...review ? { review } : {}
360
443
  });
361
444
  note(`Statement for ${config.site.url} from ${path.basename(configPath)} (${statement.template}, ${format})`);
362
445
  if (audit) {
@@ -374,7 +457,8 @@ async function runStatementCommand(options = {}) {
374
457
  exitCode: 0
375
458
  };
376
459
  } catch (cause) {
377
- if (cause instanceof ConfigError || cause instanceof StatementError) {
460
+ const { ReviewError } = await import("../review-DIbOTmOM.js");
461
+ if (cause instanceof ConfigError || cause instanceof StatementError || cause instanceof ReviewError) {
378
462
  fail(cause.message);
379
463
  if (cause instanceof ConfigError) for (const issue of cause.issues) note(` ${issue}`);
380
464
  return {
@@ -453,31 +537,60 @@ const parseFormat = oneOf(OUTPUT_FORMATS);
453
537
  const parsePositive = wholeNumber(1);
454
538
  const parseDepth = wholeNumber(0);
455
539
  const parseConcurrency = wholeNumber(1);
540
+ /**
541
+ * `--header` repeated, collected into a list the command turns into a record.
542
+ *
543
+ * Validated here so a typo stops the run before a site is crawled with a
544
+ * malformed credential — and the error names the header rather than repeating
545
+ * the value, because a bad `--header` is exactly the case where the value is a
546
+ * token and the terminal is a CI log.
547
+ */
548
+ function collectHeader(value, previous = []) {
549
+ try {
550
+ parseHeader(value);
551
+ } catch (cause) {
552
+ throw new InvalidArgumentError(cause instanceof Error ? cause.message : String(cause));
553
+ }
554
+ return [...previous, value];
555
+ }
556
+ function parseBasicAuth(value) {
557
+ try {
558
+ basicAuth(value);
559
+ } catch (cause) {
560
+ throw new InvalidArgumentError(cause instanceof Error ? cause.message : String(cause));
561
+ }
562
+ return value;
563
+ }
456
564
  const program = new Command();
457
565
  program.exitOverride();
458
566
  program.name("eaa-kit").description("WCAG 2.2 AA auditor and EU accessibility statement generator for static sites.\nNot legal advice.").version(TOOL_VERSION, "-v, --version");
459
- program.command("audit").description("Audit built HTML against WCAG 2.2 AA").argument("[dir]", "directory holding the built site (default: found automatically)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "audit a running site instead of a build directory").option("--no-build", "never run the project build or start its server").option("--per-page", "also list every page and its result").option("--manual", "what to check by hand for the rules this engine cannot evaluate").option("--coverage", "list every WCAG 2.2 A/AA criterion and what this run reached on it").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--fail-on <impact>", `exit 1 on violations at or above this impact (${IMPACT_LEVELS.join("|")}; default: ${DEFAULT_FAIL_ON})`, parseImpact).option("--format <format>", `output format (${OUTPUT_FORMATS.join("|")}; default: console)`, parseFormat).option("--output <path>", "write the report to a file instead of stdout").option("--browser", "audit in real Chromium, covering the rules jsdom cannot evaluate").option("--fast", "skip the rules the browserless engine cannot decide anyway, losing their element lists").option("--concurrency <n>", "pages to audit at once, or 1 for none (default: from the page and core count)", parseConcurrency).option("--baseline <path>", "accept the violations recorded in this file; fail only on new ones").option("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
567
+ program.command("audit").description("Audit built HTML against WCAG 2.2 AA").argument("[dir]", "directory holding the built site (default: found automatically)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "audit a running site instead of a build directory").option("--no-build", "never run the project build or start its server").option("--per-page", "also list every page and its result").option("--manual", "what to check by hand for the rules this engine cannot evaluate").option("--coverage", "list every WCAG 2.2 A/AA criterion and what this run reached on it").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--header <header>", "send this header with every request, e.g. \"Authorization: Bearer …\". Repeatable", collectHeader).option("--basic-auth <user:password>", "send an Authorization header for basic auth", parseBasicAuth).option("--fail-on <impact>", `exit 1 on violations at or above this impact (${IMPACT_LEVELS.join("|")}; default: ${DEFAULT_FAIL_ON})`, parseImpact).option("--format <format>", `output format (${OUTPUT_FORMATS.join("|")}; default: console)`, parseFormat).option("--output <path>", "write the report to a file instead of stdout").option("--browser", "audit in real Chromium, covering the rules jsdom cannot evaluate").option("--fast", "skip the rules the browserless engine cannot decide anyway, losing their element lists").option("--concurrency <n>", "pages to audit at once, or 1 for none (default: from the page and core count)", parseConcurrency).option("--baseline <path>", "accept the violations recorded in this file; fail only on new ones").option("--no-cache", "audit every page, reusing no result from an earlier run").option("--review <path>", "what a person checked, from eaa-kit checklist").option("--review-max-age <days>", "stop counting review entries older than this", parseDepth).option("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
460
568
  const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
461
569
  const invocation = auditInvocation(dir, defaults, flags);
462
570
  const { exitCode } = await runAuditCommand(invocation.dir, invocation.options);
463
571
  process.exitCode = exitCode;
464
572
  });
465
- program.command("baseline").description("Record the violations a build already has, so later runs fail only on new ones").argument("[dir]", "directory holding the built site (default: ./dist)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "record a baseline from a running site instead of a directory").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--output <path>", `where to write it (default: ${DEFAULT_BASELINE_FILE})`).option("--note <text>", "recorded on every entry, for whoever reads the file").option("--expires-on <date>", "ISO date after which the entries stop suppressing", parseDate).option("--browser", "audit in real Chromium instead of jsdom").option("--concurrency <n>", "pages to audit at once, or 1 for none", parseConcurrency).option("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
573
+ program.command("baseline").description("Record the violations a build already has, so later runs fail only on new ones").argument("[dir]", "directory holding the built site (default: ./dist)").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--url <url>", "record a baseline from a running site instead of a directory").option("--allow-remote", "allow --url to crawl a host that is not localhost").option("--ignore-robots", "crawl paths robots.txt disallows").option("--sitemap <path>", "where the site lists its pages, if not /sitemap.xml").option("--max-pages <n>", "stop the crawl after this many pages", parsePositive).option("--max-depth <n>", "how far from the entry URL to follow links", parseDepth).option("--header <header>", "send this header with every request, e.g. \"Authorization: Bearer …\". Repeatable", collectHeader).option("--basic-auth <user:password>", "send an Authorization header for basic auth", parseBasicAuth).option("--output <path>", `where to write it (default: ${DEFAULT_BASELINE_FILE})`).option("--prune", "remove the entries this run shows are gone, instead of recording a new baseline").option("--note <text>", "recorded on every entry, for whoever reads the file").option("--expires-on <date>", "ISO date after which the entries stop suppressing", parseDate).option("--browser", "audit in real Chromium instead of jsdom").option("--concurrency <n>", "pages to audit at once, or 1 for none", parseConcurrency).option("--config <path>", "take defaults from this config file, otherwise it is searched for").action(async (dir, flags) => {
466
574
  const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
467
575
  const invocation = baselineInvocation(dir, defaults, flags);
468
576
  const { exitCode } = await runBaselineCommand(invocation.dir, invocation.options);
469
577
  process.exitCode = exitCode;
470
578
  });
579
+ program.command("checklist").description("Write the manual review: the 34 WCAG criteria no automated rule can reach").option("--record <path>", `where the answers live (default: ${DEFAULT_REVIEW_FILE})`).option("--output <path>", "write the worksheet here instead of stdout").option("--reviewed-by <name>", "who is carrying out the review").action(async (flags) => {
580
+ const { runChecklistCommand } = await import("../checklist-DDd9jewi.js");
581
+ const { exitCode } = await runChecklistCommand(flags);
582
+ process.exitCode = exitCode;
583
+ });
471
584
  program.command("diff").description("Compare two JSON reports: what a change made worse, and what it fixed").argument("<before>", "JSON report from before the change").argument("<after>", "JSON report from after it").option("--format <format>", `output format (${DIFF_FORMATS.join("|")})`, oneOf(DIFF_FORMATS)).option("--output <path>", "write the diff here instead of stdout").option("--fail-on <impact>", `exit 1 on NEW violations at or above this impact (${IMPACT_LEVELS.join("|")})`, parseImpact, DEFAULT_FAIL_ON).action(async (before, after, flags) => {
472
585
  const { exitCode } = await runDiffCommand(before, after, flags);
473
586
  process.exitCode = exitCode;
474
587
  });
475
588
  program.command("init").description("Write an eaa.config.json to generate statements from").option("--output <path>", "write here instead of eaa.config.json").option("--force", "overwrite a config that is already there").option("-y, --yes", "take every default without asking").action(async (flags) => {
476
- const { runInitCommand } = await import("../init-CW7LfGT5.js");
589
+ const { runInitCommand } = await import("../init-DrFfzjvm.js");
477
590
  const { exitCode } = await runInitCommand(flags);
478
591
  process.exitCode = exitCode;
479
592
  });
480
- program.command("statement").description("Generate an EU accessibility statement from eaa.config").option("--config <path>", "path to the config file, otherwise it is searched for").option("--lang <locale>", `statement language (${STATEMENT_LOCALES.join("|")})`, parseLocale).option("--country <code>", `override the country template (${COUNTRIES.join("|")})`, parseCountry).option("--audit <path>", "list the barriers from an eaa-kit audit --format json report").option("--format <format>", `output format (${STATEMENT_FORMATS.join("|")}), otherwise from the --output extension`, parseStatementFormat).option("--output <path>", "write the statement to a file instead of stdout").action(async (flags) => {
593
+ program.command("statement").description("Generate an EU accessibility statement from eaa.config").option("--config <path>", "path to the config file, otherwise it is searched for").option("--lang <locale>", `statement language (${STATEMENT_LOCALES.join("|")})`, parseLocale).option("--country <code>", `override the country template (${COUNTRIES.join("|")})`, parseCountry).option("--audit <path>", "list the barriers from an eaa-kit audit --format json report").option("--review <path>", "say how many criteria a person checked, and check the conformance claim against them").option("--format <format>", `output format (${STATEMENT_FORMATS.join("|")}), otherwise from the --output extension`, parseStatementFormat).option("--output <path>", "write the statement to a file instead of stdout").action(async (flags) => {
481
594
  const { lang, ...options } = flags;
482
595
  const { exitCode } = await runStatementCommand({
483
596
  ...options,