eaa-kit 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +35 -2
  2. package/dist/astro/index.d.ts +1 -1
  3. package/dist/astro/index.js +1 -1
  4. package/dist/audit/runners/worker.js +1 -1
  5. package/dist/audit-DK2StT1_.js +2 -0
  6. package/dist/{audit-B282A3EA.js → audit-e_uixhPG.js} +95 -48
  7. package/dist/baseline-BscHfFSW.js +2 -0
  8. package/dist/{baseline-CuAS2lMk.js → baseline-D2qWmUC6.js} +36 -9
  9. package/dist/checklist-BtgkxMEa.js +44 -0
  10. package/dist/cli/index.js +111 -14
  11. package/dist/{command-C3D7JWn6.js → command-CZKp9Wi_.js} +101 -10
  12. package/dist/{remediation-CMBIrnpN.js → completeness-Dwq3CPB3.js} +60 -42
  13. package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
  14. package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
  15. package/dist/coverage-7bQqWNfN.js +2 -0
  16. package/dist/{coverage-B_Y6l-Ra.js → coverage-BD2_CYns.js} +44 -7
  17. package/dist/{crawl-BUWPgaGW.js → crawl-BHkJmUkA.js} +42 -8
  18. package/dist/eleventy/index.d.ts +1 -1
  19. package/dist/eleventy/index.js +1 -1
  20. package/dist/{html-C4vktg0A.js → html-Bmh7X_sM.js} +52 -30
  21. package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
  22. package/dist/index.d.ts +68 -1
  23. package/dist/index.js +3 -3
  24. package/dist/{init-CW7LfGT5.js → init-BjKYttxc.js} +3 -3
  25. package/dist/{jsdom-B--cEH-G.js → jsdom-CQar6OQS.js} +1 -1
  26. package/dist/jsdom-DMvU2h-f.js +3 -0
  27. package/dist/{json-D_Mnnft5.js → json-HJDgJp09.js} +1 -1
  28. package/dist/{json-DROX33kh.js → json-PNg6BeXp.js} +11 -16
  29. package/dist/load-Bc_xDGAh.js +2 -0
  30. package/dist/{load-yAR4wzez.js → load-vZdV_fD1.js} +5 -1
  31. package/dist/nuxt/index.d.ts +1 -1
  32. package/dist/nuxt/index.js +1 -1
  33. package/dist/{playwright-BojtYVUa.js → playwright-C-miJP2b.js} +3 -2
  34. package/dist/{pool-BO25OIez.js → pool-dkexq5xo.js} +2 -2
  35. package/dist/{render-DbGOVmhx.js → render-CtsqR0k-.js} +105 -4
  36. package/dist/{result-BWcYXeRs.js → result-DfUru6y_.js} +24 -1
  37. package/dist/review-CdMK2GpQ.js +161 -0
  38. package/dist/review-DIbOTmOM.js +2 -0
  39. package/dist/{run-C2nKFcb-.js → run-D7hdVXLA.js} +1 -1
  40. package/dist/{run-CtcEUhbe.d.ts → run-YiqyQA2b.d.ts} +10 -0
  41. package/dist/{sarif-B-UBcVu8.js → sarif-BKvq_AUm.js} +23 -12
  42. package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
  43. package/dist/vite/index.d.ts +1 -1
  44. package/dist/vite/index.js +1 -1
  45. package/dist/webpack/index.d.ts +1 -1
  46. package/dist/webpack/index.js +1 -1
  47. package/dist/worksheet-eUjwYLxC.js +63 -0
  48. package/package.json +2 -2
  49. package/dist/audit-DyHPmGeD.js +0 -2
  50. package/dist/baseline-0u4df12v.js +0 -2
  51. package/dist/jsdom-22Bkt65v.js +0 -3
  52. package/dist/load-CFq2VQtT.js +0 -2
package/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-vZdV_fD1.js";
5
+ import { c as StatementError, i as readAuditReport, o as checkStatementEvidence, s as refuses, t as renderStatement } from "../render-CtsqR0k-.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-D2qWmUC6.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-CZKp9Wi_.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-e_uixhPG.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
  };
@@ -353,6 +403,23 @@ async function runStatementCommand(options = {}) {
353
403
  });
354
404
  let audit;
355
405
  if (options.audit) audit = await readAuditReport(options.audit, options.cwd ?? process.cwd());
406
+ let review;
407
+ if (options.review) {
408
+ const { readReview } = await import("../review-DIbOTmOM.js");
409
+ review = await readReview(options.review, options.cwd ?? process.cwd());
410
+ }
411
+ const problems = checkStatementEvidence({
412
+ config,
413
+ ...audit ? { audit } : {},
414
+ ...review ? { review } : {}
415
+ });
416
+ for (const problem of problems) if (problem.severity === "refuses") fail(problem.message);
417
+ else advise(problem.message);
418
+ if (refuses(problems)) return {
419
+ document: "",
420
+ format,
421
+ exitCode: 2
422
+ };
356
423
  const statement = await renderStatement(config, {
357
424
  ...options.locale ? { locale: options.locale } : {},
358
425
  ...options.country ? { country: options.country } : {},
@@ -374,7 +441,8 @@ async function runStatementCommand(options = {}) {
374
441
  exitCode: 0
375
442
  };
376
443
  } catch (cause) {
377
- if (cause instanceof ConfigError || cause instanceof StatementError) {
444
+ const { ReviewError } = await import("../review-DIbOTmOM.js");
445
+ if (cause instanceof ConfigError || cause instanceof StatementError || cause instanceof ReviewError) {
378
446
  fail(cause.message);
379
447
  if (cause instanceof ConfigError) for (const issue of cause.issues) note(` ${issue}`);
380
448
  return {
@@ -453,31 +521,60 @@ const parseFormat = oneOf(OUTPUT_FORMATS);
453
521
  const parsePositive = wholeNumber(1);
454
522
  const parseDepth = wholeNumber(0);
455
523
  const parseConcurrency = wholeNumber(1);
524
+ /**
525
+ * `--header` repeated, collected into a list the command turns into a record.
526
+ *
527
+ * Validated here so a typo stops the run before a site is crawled with a
528
+ * malformed credential — and the error names the header rather than repeating
529
+ * the value, because a bad `--header` is exactly the case where the value is a
530
+ * token and the terminal is a CI log.
531
+ */
532
+ function collectHeader(value, previous = []) {
533
+ try {
534
+ parseHeader(value);
535
+ } catch (cause) {
536
+ throw new InvalidArgumentError(cause instanceof Error ? cause.message : String(cause));
537
+ }
538
+ return [...previous, value];
539
+ }
540
+ function parseBasicAuth(value) {
541
+ try {
542
+ basicAuth(value);
543
+ } catch (cause) {
544
+ throw new InvalidArgumentError(cause instanceof Error ? cause.message : String(cause));
545
+ }
546
+ return value;
547
+ }
456
548
  const program = new Command();
457
549
  program.exitOverride();
458
550
  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) => {
551
+ 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("--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
552
  const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
461
553
  const invocation = auditInvocation(dir, defaults, flags);
462
554
  const { exitCode } = await runAuditCommand(invocation.dir, invocation.options);
463
555
  process.exitCode = exitCode;
464
556
  });
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) => {
557
+ 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
558
  const defaults = await auditDefaults({ ...flags.config ? { config: flags.config } : {} });
467
559
  const invocation = baselineInvocation(dir, defaults, flags);
468
560
  const { exitCode } = await runBaselineCommand(invocation.dir, invocation.options);
469
561
  process.exitCode = exitCode;
470
562
  });
563
+ 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) => {
564
+ const { runChecklistCommand } = await import("../checklist-BtgkxMEa.js");
565
+ const { exitCode } = await runChecklistCommand(flags);
566
+ process.exitCode = exitCode;
567
+ });
471
568
  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
569
  const { exitCode } = await runDiffCommand(before, after, flags);
473
570
  process.exitCode = exitCode;
474
571
  });
475
572
  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");
573
+ const { runInitCommand } = await import("../init-BjKYttxc.js");
477
574
  const { exitCode } = await runInitCommand(flags);
478
575
  process.exitCode = exitCode;
479
576
  });
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) => {
577
+ 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>", "check the conformance claim against a review record; nothing from it is published").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
578
  const { lang, ...options } = flags;
482
579
  const { exitCode } = await runStatementCommand({
483
580
  ...options,
@@ -1,6 +1,86 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import pc from "picocolors";
4
+ //#region src/audit/headers.ts
5
+ /**
6
+ * Request headers for the pages this tool fetches.
7
+ *
8
+ * The two places a small agency stages work are the two this tool could not
9
+ * reach: a preview deployment, which every host protects by default, and a CMS
10
+ * staging site behind basic auth or a session cookie. `--url` sent one fixed
11
+ * pair of headers and had no way to add to it, so auditing either meant putting
12
+ * the site on the public internet first.
13
+ *
14
+ * These values are credentials. They are handled on the principle that the tool
15
+ * never writes one down: nothing here reaches a report, a baseline, a SARIF log
16
+ * or the completeness record, and the errors below name the header rather than
17
+ * echoing what was typed, because a malformed `--header` is exactly the case
18
+ * where the value is a token and the terminal is a CI log.
19
+ *
20
+ * For the same reason there is no `headers` key in `eaa.config`. That file is
21
+ * committed; a token in it is a token in the repository, and a tool that
22
+ * offered the field would be inviting that. Pass them as flags — a shell
23
+ * expands `--header "Authorization: Bearer $TOKEN"` — or as the GitHub Action's
24
+ * input, which reads from `secrets`.
25
+ */
26
+ /** Thrown for a header this cannot parse. Never carries the value. */
27
+ var HeaderError = class extends Error {
28
+ name = "HeaderError";
29
+ };
30
+ /**
31
+ * `Name: value` into a pair.
32
+ *
33
+ * Lenient about the space after the colon and about a value containing colons,
34
+ * which `Authorization: Basic …` and a `Cookie` both do. Strict about the name,
35
+ * because a header name is a token by RFC 9110 and anything else is a typo that
36
+ * a server would reject in a way nobody could read.
37
+ */
38
+ function parseHeader(input) {
39
+ const at = input.indexOf(":");
40
+ if (at <= 0) throw new HeaderError("a header must be written \"Name: value\"");
41
+ const name = input.slice(0, at).trim();
42
+ const value = input.slice(at + 1).trim();
43
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name)) throw new HeaderError(`"${name}" is not a valid header name`);
44
+ if (value === "") throw new HeaderError(`the ${name} header was given no value`);
45
+ if (/[\r\n]/.test(value)) throw new HeaderError(`the ${name} header contains a line break`);
46
+ return [name, value];
47
+ }
48
+ /** Several `Name: value` strings into the record the fetchers take. */
49
+ function parseHeaders(inputs) {
50
+ const headers = {};
51
+ for (const input of inputs) {
52
+ const [name, value] = parseHeader(input);
53
+ headers[name] = value;
54
+ }
55
+ return headers;
56
+ }
57
+ /**
58
+ * `user:password` into an `Authorization` header.
59
+ *
60
+ * Sugar for the header somebody would otherwise have to base64 by hand, which
61
+ * is the one credential a staging site is most likely to be behind. A password
62
+ * may contain colons; a username may not, which is basic auth's own rule.
63
+ */
64
+ function basicAuth(input) {
65
+ if (input.indexOf(":") <= 0) throw new HeaderError("basic auth must be written \"user:password\"");
66
+ return ["Authorization", `Basic ${Buffer.from(input, "utf8").toString("base64")}`];
67
+ }
68
+ /**
69
+ * The headers a run will send, from whichever flags were given.
70
+ *
71
+ * `--basic-auth` first, so an explicit `--header "Authorization: …"` beats it:
72
+ * a typed header is the more specific thing to have asked for.
73
+ */
74
+ function requestHeaders(options) {
75
+ const headers = {};
76
+ if (options.basicAuth !== void 0) {
77
+ const [name, value] = basicAuth(options.basicAuth);
78
+ headers[name] = value;
79
+ }
80
+ Object.assign(headers, parseHeaders(options.header ?? []));
81
+ return Object.keys(headers).length === 0 ? void 0 : headers;
82
+ }
83
+ //#endregion
4
84
  //#region src/cli/command.ts
5
85
  /**
6
86
  * What every command does around the audit itself: say what is happening, run
@@ -42,18 +122,19 @@ async function runEngine(pages, options) {
42
122
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
43
123
  };
44
124
  if (!options.browser) {
45
- const { runPooledAudit } = await import("./pool-BO25OIez.js");
125
+ const { runPooledAudit } = await import("./pool-dkexq5xo.js");
46
126
  return runPooledAudit(pages, {
47
127
  ...runnerOptions,
48
128
  ...options.fast ? { fast: true } : {},
49
129
  ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
50
130
  });
51
131
  }
52
- const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-BojtYVUa.js");
132
+ const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-C-miJP2b.js");
53
133
  try {
54
134
  return await runBrowserAudit(options.directory, pages, {
55
135
  ...runnerOptions,
56
- ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
136
+ ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency },
137
+ ...options.headers === void 0 ? {} : { headers: options.headers }
57
138
  });
58
139
  } catch (cause) {
59
140
  if (cause instanceof BrowserUnavailableError) {
@@ -95,7 +176,7 @@ async function emitDocument(body, output, cwd) {
95
176
  * on different settings than it names would be worse than stopping.
96
177
  */
97
178
  async function auditDefaults(options = {}) {
98
- const { loadAuditConfig } = await import("./load-CFq2VQtT.js");
179
+ const { loadAuditConfig } = await import("./load-Bc_xDGAh.js");
99
180
  const loaded = await loadAuditConfig({
100
181
  ...options.cwd ? { cwd: options.cwd } : {},
101
182
  ...options.config ? { path: options.config } : {}
@@ -105,14 +186,19 @@ async function auditDefaults(options = {}) {
105
186
  return loaded.audit;
106
187
  }
107
188
  function auditInvocation(dir, defaults, flags) {
108
- const { build, config: _config, ...typed } = flags;
109
- const { dir: configDir, build: configBuild, ...fromConfig } = defaults;
189
+ const { build, 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
+ });
110
195
  return {
111
196
  dir: dir ?? configDir,
112
197
  options: {
113
198
  ...fromConfig,
114
199
  ...typed,
115
- ...build === false || configBuild === false ? { noBuild: true } : {}
200
+ ...build === false || configBuild === false ? { noBuild: true } : {},
201
+ ...headers === void 0 ? {} : { headers }
116
202
  }
117
203
  };
118
204
  }
@@ -126,12 +212,17 @@ function auditInvocation(dir, defaults, flags) {
126
212
  * and `baseline` all describe a verdict this command does not reach.
127
213
  */
128
214
  function baselineInvocation(dir, defaults, flags) {
129
- const { config: _config, ...typed } = flags;
215
+ const { config: _config, header, basicAuth, ...typed } = flags;
216
+ const headers = requestHeaders({
217
+ ...header ? { header } : {},
218
+ ...basicAuth ? { basicAuth } : {}
219
+ });
130
220
  return {
131
221
  dir: dir ?? defaults.dir ?? "./dist",
132
222
  options: {
133
223
  ...baselineDefaults(defaults),
134
- ...typed
224
+ ...typed,
225
+ ...headers === void 0 ? {} : { headers }
135
226
  }
136
227
  };
137
228
  }
@@ -157,4 +248,4 @@ function pick(source, keys) {
157
248
  return out;
158
249
  }
159
250
  //#endregion
160
- export { emitDocument as a, runEngine as c, baselineInvocation as i, warn as l, auditDefaults as n, fail as o, auditInvocation as r, note as s, advise as t };
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";
1
+ import { o as impactRank } from "./impact-luGCnN8F.js";
2
2
  import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
3
- import { o as findingElements } from "./result-BWcYXeRs.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
- }
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 { d as successCriteria, n as DEFAULT_TAGS, r as ENGINE_BLIND_RULES } from "./result-BWcYXeRs.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,7 +464,7 @@ 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);
@@ -496,15 +497,36 @@ function buildCoverage(audits, tags = DEFAULT_TAGS) {
496
497
  browserWouldAnswer: false
497
498
  };
498
499
  });
499
- 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);
500
503
  return {
501
- criteria,
504
+ criteria: reviewed,
502
505
  evaluated: count("evaluated"),
503
506
  notEvaluated: count("not-evaluated"),
504
507
  nothingToCheck: count("nothing-to-check"),
505
508
  noAutomatedRule: count("no-automated-rule"),
506
- browserWouldAnswer: criteria.filter((criterion) => criterion.browserWouldAnswer).length,
507
- 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
+ };
508
530
  };
509
531
  }
510
532
  /**
@@ -518,5 +540,20 @@ function coverageSummary(coverage) {
518
540
  const unautomatable = coverage.noAutomatedRule;
519
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}.`;
520
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
+ }
521
558
  //#endregion
522
- 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 };