eaa-kit 0.3.0 → 0.4.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 (50) hide show
  1. package/README.md +51 -5
  2. package/dist/astro/index.js +1 -1
  3. package/dist/audit/runners/worker.js +1 -1
  4. package/dist/{audit-DcL73mOC.js → audit-CPoZMXGM.js} +180 -36
  5. package/dist/audit-CpXH2Mk8.js +2 -0
  6. package/dist/{baseline-CuKFq4IF.js → baseline-22Y1NWxM.js} +1 -1
  7. package/dist/{baseline-s9F3fXTN.js → baseline-DB9CZGnV.js} +2 -2
  8. package/dist/cli/index.js +267 -10
  9. package/dist/{collect-BkAQ0viT.js → collect-CFM8gEVv.js} +21 -10
  10. package/dist/{command-Dxpa00Ha.js → command-D8l_oYbV.js} +2 -2
  11. package/dist/{component-7kEBjv_y.js → component-C3GL1Mnu.js} +41 -6
  12. package/dist/component-DKd3EHOg.js +2 -0
  13. package/dist/coverage-B4IBKMO8.js +521 -0
  14. package/dist/{crawl-Oxt2Gaqo.js → crawl-BiI1Lau_.js} +2 -2
  15. package/dist/eleventy/index.d.ts +28 -0
  16. package/dist/eleventy/index.js +19 -0
  17. package/dist/{frameworks-BYa3tULg.js → frameworks-B4ClIJgE.js} +71 -0
  18. package/dist/{frameworks-DaDqrJOw.js → frameworks-etFg_O8K.js} +1 -1
  19. package/dist/{html-DKiI_3gs.js → html-BGTO3ypW.js} +130 -5
  20. package/dist/{impact-YdoOtFqm.js → impact-DZt2oBCP.js} +1 -1
  21. package/dist/index.js +2 -2
  22. package/dist/{init-DiLiYvN1.js → init-DIWDE35F.js} +2 -2
  23. package/dist/jsdom-4IMzv0eE.js +3 -0
  24. package/dist/jsdom-DCpGSLfW.js +81 -0
  25. package/dist/{json-Cnv9nd6U.js → json-DjEvy1nX.js} +8 -3
  26. package/dist/{json-B0Y7rNjt.js → json-QQuFIw1W.js} +1 -1
  27. package/dist/{load-UYXLqGV9.js → load-5wRGLvub.js} +1 -1
  28. package/dist/nuxt/index.d.ts +35 -0
  29. package/dist/nuxt/index.js +23 -0
  30. package/dist/{playwright-DYFsGUNd.js → playwright-BWniOain.js} +1 -1
  31. package/dist/{pool-BWkWZiJW.js → pool-BMevaLWD.js} +2 -2
  32. package/dist/{project-DW08TseF.js → project-CzOnkLH6.js} +8 -2
  33. package/dist/project-MFrXcw1M.js +2 -0
  34. package/dist/remediation-Dtowi2EC.js +321 -0
  35. package/dist/{render-DI_aCnAZ.js → render-DrvXRCEn.js} +3 -3
  36. package/dist/{result-DLxd2Eip.js → result-DoamKFsp.js} +78 -1
  37. package/dist/routes-CmdRUuOs.js +265 -0
  38. package/dist/{run-BW6CVuND.js → run-DB34BSOZ.js} +19 -2
  39. package/dist/{sarif-DB3WG7T9.js → sarif-SR3_lLYd.js} +14 -6
  40. package/dist/{schema-CMZ8ItGk.js → schema-is6CGX2D.js} +4 -1
  41. package/dist/{text-BFmNtMsV.js → text-CKKpzkYM.js} +1 -1
  42. package/dist/vite/index.js +1 -1
  43. package/dist/webpack/index.d.ts +33 -0
  44. package/dist/webpack/index.js +20 -0
  45. package/package.json +30 -8
  46. package/dist/audit-CuG2hYyo.js +0 -2
  47. package/dist/jsdom-BjpF-2V-.js +0 -158
  48. package/dist/jsdom-X4KYfTp8.js +0 -3
  49. package/dist/manual-Vz-oX1I_.js +0 -239
  50. package/dist/routes-C2Cgf6Ko.js +0 -119
package/README.md CHANGED
@@ -9,6 +9,7 @@ in Austria.
9
9
 
10
10
  ```bash
11
11
  npx eaa-kit audit # WCAG 2.2 AA report; finds your build itself
12
+ npx eaa-kit diff a.json b.json # what a change made worse, and what it fixed
12
13
  npx eaa-kit init # write an eaa.config.json
13
14
  npx eaa-kit statement # Barrierefreiheitserklärung from that config
14
15
  ```
@@ -62,13 +63,54 @@ eaa-kit baseline ./dist
62
63
  eaa-kit audit ./dist --baseline eaa-baseline.json
63
64
  ```
64
65
 
66
+ **Says what a change did.** Two reports, and the difference between them: what is new, what
67
+ was fixed, and what the later run never looked at — which is kept apart, because a
68
+ violation missing from a run that stopped early was not fixed by anybody.
69
+
70
+ ```bash
71
+ eaa-kit diff before.json after.json
72
+ ```
73
+
74
+ **Says what to do about it.** Not "images must have alternative text" and a link — who the
75
+ barrier stops, what to change, and the corrected form of *your* markup, in your framework's
76
+ idiom where that differs. Deterministic and offline: no model, no API key, nothing that
77
+ could invent a fix that looks right and is not.
78
+
79
+ ```
80
+ ✗ image-alt critical, WCAG 1.1.1
81
+ A screen reader announces this image by its filename, or skips it entirely.
82
+ Fix: Add alt text describing what the image conveys…
83
+ → <img src="/assets/logo.svg" alt="What this image shows">
84
+ written in src/components/Header.astro:12
85
+ ```
86
+
87
+ **Says how much of WCAG it could reach.** WCAG 2.2 has 55 success criteria at Levels A and
88
+ AA. axe-core has rules touching 23 of them. Every run says so, and `--coverage` lists all
89
+ 55 with what this run reached on each — never as a percentage, because most of WCAG cannot
90
+ be automated and a score would present that as a fact about your site.
91
+
92
+ **Says what it did not measure.** A crawl that stopped at its page limit, or could not
93
+ fetch forty URLs, no longer produces a report that looks like a complete one.
94
+
65
95
  **Reports in four shapes**: a console report for whoever ran it, JSON for other tools,
66
96
  SARIF for GitHub code scanning, and a self-contained HTML page for the client whose site
67
97
  it is.
68
98
 
69
- **Runs in your build**, as a [Vite plugin](docs/integrations.md#vite-plugin) covering
70
- SvelteKit, Nuxt and Remix too an [Astro integration](docs/integrations.md#astro-integration),
71
- or the bundled [GitHub Action](docs/integrations.md#github-actions).
99
+ **Runs in your build**: a [Vite plugin](docs/integrations.md#vite-plugin) covering SvelteKit
100
+ and Remix too, and integrations for
101
+ [Astro](docs/integrations.md#astro-integration),
102
+ [Nuxt](docs/integrations.md#nuxt-module),
103
+ [Eleventy](docs/integrations.md#eleventy-plugin) and
104
+ [webpack](docs/integrations.md#webpack-plugin) — or the bundled
105
+ [GitHub Action](docs/integrations.md#github-actions).
106
+
107
+ **Audits what a CMS serves.** WordPress, TYPO3, Craft, Laravel, Symfony, Rails and Django
108
+ write no HTML to disk, so eaa-kit names the command that gets the site running and audits
109
+ that instead — following the site's own sitemap, wherever it keeps it.
110
+
111
+ ```bash
112
+ eaa-kit audit --url http://localhost:8000 --sitemap /sitemap_index.xml
113
+ ```
72
114
 
73
115
  ## Documentation
74
116
 
@@ -77,8 +119,10 @@ or the bundled [GitHub Action](docs/integrations.md#github-actions).
77
119
  | [Auditing a build](docs/audit.md) | The `audit` command, both engines, exit codes, and what an automated run can and cannot tell you |
78
120
  | [The statement command](docs/statement.md) | The config file, the three countries, and filling a statement from audit results |
79
121
  | [Baselines](docs/baseline.md) | Adopting the tool on a site that already has violations |
122
+ | [Comparing two runs](docs/reports.md#comparing-two-runs) | The `diff` command, and what it refuses to call fixed |
123
+ | [Coverage of WCAG](docs/audit.md#how-much-of-wcag-a-run-reaches) | What an automated engine can reach at all, and what it cannot |
80
124
  | [Report formats](docs/reports.md) | The JSON contract, SARIF, and the HTML report |
81
- | [Integrations](docs/integrations.md) | Astro and GitHub Actions |
125
+ | [Integrations](docs/integrations.md) | The build plugins, the GitHub Action, and the two builders that get a command instead |
82
126
 
83
127
  Complete generated output for every format is checked in under [examples/](examples).
84
128
 
@@ -90,7 +134,9 @@ The two things worth knowing before you rely on any of it:
90
134
  text is accurate, whether a page makes sense in reading order, or whether a form can
91
135
  actually be completed with a screen reader. A clean report means nothing was found by this
92
136
  engine, which is not the same as a site being accessible — and none of the four output
93
- formats will say otherwise on your behalf.
137
+ formats will say otherwise on your behalf. This is not a disclaimer the tool leaves you to
138
+ take on faith: it counts it. Of the 55 WCAG 2.2 A and AA criteria, 34 have no automated
139
+ rule at all, and every report says so.
94
140
 
95
141
  **The browserless engine cannot decide everything.** jsdom has no layout, so rules that
96
142
  depend on rendering — colour contrast, target size, computed overflow — cannot be
@@ -1,4 +1,4 @@
1
- import { n as auditBuild, t as BuildAuditError } from "../run-BW6CVuND.js";
1
+ import { n as auditBuild, t as BuildAuditError } from "../run-DB34BSOZ.js";
2
2
  import { fileURLToPath } from "node:url";
3
3
  //#region src/astro/index.ts
4
4
  var AstroAuditError = class extends Error {
@@ -1,4 +1,4 @@
1
- import { n as auditPage } from "../../jsdom-BjpF-2V-.js";
1
+ import { t as auditPage } from "../../jsdom-DCpGSLfW.js";
2
2
  import { parentPort, workerData } from "node:worker_threads";
3
3
  //#region src/audit/runners/worker.ts
4
4
  /**
@@ -1,8 +1,10 @@
1
- import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-YdoOtFqm.js";
2
- import { n as count, t as collapse } from "./text-BFmNtMsV.js";
3
- import { a as groupIssues, i as coverageParts, n as understandingUrl, o as isShared, r as blindRules, t as manualCheckFor } from "./manual-Vz-oX1I_.js";
4
- import { a as runEngine, i as note, n as emitDocument, o as warn, r as fail, t as advise } from "./command-Dxpa00Ha.js";
5
- import { n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-BkAQ0viT.js";
1
+ import { a as impactRank, i as impactLabel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
2
+ import { a as plural, n as count, t as collapse } from "./text-CKKpzkYM.js";
3
+ import { a as isShared, c as runCompleteness, i as groupIssues, n as blindRules, r as coverageParts, t as remediationFor } from "./remediation-Dtowi2EC.js";
4
+ import { r as componentPath } from "./component-C3GL1Mnu.js";
5
+ import { i as understandingUrl, n as coverageSummary, r as manualCheckFor, t as buildCoverage } from "./coverage-B4IBKMO8.js";
6
+ import { a as runEngine, i as note, n as emitDocument, o as warn, r as fail, t as advise } from "./command-D8l_oYbV.js";
7
+ import { n as collectPages, r as emptyDirectoryHint, t as BuildDirectoryError } from "./collect-CFM8gEVv.js";
6
8
  import path from "node:path";
7
9
  import pc from "picocolors";
8
10
  //#region src/audit/report/console.ts
@@ -47,6 +49,9 @@ function context(options) {
47
49
  sourceFor: options.sourceFor ?? (() => void 0),
48
50
  componentFor: options.componentFor ?? (() => void 0),
49
51
  manual: options.manual ?? false,
52
+ coverage: options.coverage ?? false,
53
+ framework: options.framework,
54
+ completeness: options.completeness,
50
55
  c,
51
56
  symbol: (kind) => {
52
57
  switch (kind) {
@@ -182,9 +187,11 @@ function summary(audits, ctx) {
182
187
  const elementCount = audits.reduce((total, audit) => total + audit.violations.reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
183
188
  const reviewCount = countRules(audits, "needs-review");
184
189
  const pages = count(audits.length, "page");
185
- const lines = [line(ctx, "Summary", ctx.c.bold)];
186
- if (ruleCount === 0) lines.push(line(ctx, ` No violations across ${pages}.`, ctx.c.green));
187
- else {
190
+ const lines = [line(ctx, "Summary", ctx.c.bold), ...completenessLines(ctx)];
191
+ if (ruleCount === 0) {
192
+ const clean = ctx.completeness && !ctx.completeness.complete ? ` No violations across the ${pages} that were audited.` : ` No violations across ${pages}.`;
193
+ lines.push(line(ctx, clean, ctx.c.green));
194
+ } else {
188
195
  lines.push(render(ctx, [{
189
196
  text: ` ${count(ruleCount, "violation")} on ${withViolations.length} of ${pages}`,
190
197
  paint: ctx.c.red
@@ -202,6 +209,73 @@ function summary(audits, ctx) {
202
209
  const accepted = audits.reduce((total, audit) => total + (audit.accepted ?? []).reduce((sum, finding) => sum + finding.nodes.length, 0), 0);
203
210
  if (accepted > 0) lines.push(line(ctx, ` ${count(accepted, "element")} accepted by the baseline, not counted above`, ctx.c.dim));
204
211
  lines.push(...blindSection(audits, ctx));
212
+ lines.push(...coverageSection(audits, ctx));
213
+ return lines;
214
+ }
215
+ /**
216
+ * How much of the standard this run could reach.
217
+ *
218
+ * One line by default. It is the sentence that stops a clean report reading as
219
+ * a clean site: most of WCAG cannot be checked by any automated engine, and
220
+ * until this was printed the report said so only in prose, in the footer, where
221
+ * it could be read as boilerplate.
222
+ */
223
+ function coverageSection(audits, ctx) {
224
+ const coverage = buildCoverage(audits);
225
+ const lines = ["", ...wrap(coverageSummary(coverage), ctx.width - 2).map((text) => line(ctx, ` ${text}`, ctx.c.dim))];
226
+ if (coverage.browserWouldAnswer > 0) {
227
+ const verb = coverage.browserWouldAnswer === 1 ? "criterion" : "criteria";
228
+ lines.push(line(ctx, ` --browser would answer ${coverage.browserWouldAnswer} more ${verb}.`, ctx.c.dim));
229
+ }
230
+ if (!ctx.coverage) {
231
+ lines.push(line(ctx, " Run with --coverage for the criterion-by-criterion breakdown.", ctx.c.dim));
232
+ return lines;
233
+ }
234
+ lines.push("", line(ctx, "Coverage", ctx.c.bold));
235
+ for (const criterion of coverage.criteria) {
236
+ const paint = criterion.status === "evaluated" ? ctx.c.green : ctx.c.dim;
237
+ const note = criterion.browserWouldAnswer ? " (--browser would answer this)" : "";
238
+ lines.push(render(ctx, [
239
+ {
240
+ text: ` ${criterion.number} `,
241
+ paint: ctx.c.bold
242
+ },
243
+ { text: `${criterion.title} (${criterion.level}) — ` },
244
+ {
245
+ text: `${STATUS_WORDS[criterion.status]}${note}`,
246
+ paint
247
+ }
248
+ ]));
249
+ }
250
+ return lines;
251
+ }
252
+ /** What each outcome is called, in words rather than a symbol. */
253
+ const STATUS_WORDS = {
254
+ evaluated: "evaluated here",
255
+ "not-evaluated": "this engine could not evaluate it",
256
+ "nothing-to-check": "rules ran and found nothing on this site to check",
257
+ "no-automated-rule": "no automated rule exists; a person must check it"
258
+ };
259
+ /**
260
+ * What the run never looked at.
261
+ *
262
+ * Printed before the counts rather than after them, because it changes how they
263
+ * read: "no violations" means one thing over a whole site and another over the
264
+ * twelve pages of it a crawl managed to fetch before it hit its limit.
265
+ *
266
+ * Pages that errored are left to the summary's own line, which already names
267
+ * them; repeating the number here would read as twice as many.
268
+ */
269
+ function completenessLines(ctx) {
270
+ const completeness = ctx.completeness;
271
+ if (completeness === void 0 || completeness.complete) return [];
272
+ const lines = [];
273
+ if (completeness.unreachable.length > 0) {
274
+ const noun = plural(completeness.unreachable.length, "page");
275
+ lines.push(line(ctx, ` ${completeness.unreachable.length} ${noun} could not be reached, and were not audited`, ctx.c.yellow));
276
+ }
277
+ if (completeness.truncated) lines.push(line(ctx, " The run stopped at its page limit; the site has more", ctx.c.yellow));
278
+ if (lines.length > 0) lines.push(line(ctx, " This report describes what was audited, not the whole site.", ctx.c.dim));
205
279
  return lines;
206
280
  }
207
281
  /**
@@ -296,6 +370,7 @@ function issuesSection(audits, ctx) {
296
370
  }] : []
297
371
  ]));
298
372
  lines.push(line(ctx, ` ${issue.help}`, ctx.c.dim));
373
+ lines.push(...remediationLines(issue.ruleId, issue.elements[0]?.html, ctx));
299
374
  for (const element of issue.elements.slice(0, ctx.maxNodes)) {
300
375
  lines.push(line(ctx, ` ${collapse(element.html)}`));
301
376
  lines.push(...whereLines(element, ctx));
@@ -305,6 +380,27 @@ function issuesSection(audits, ctx) {
305
380
  }
306
381
  return lines;
307
382
  }
383
+ /**
384
+ * What to do about the rule, under the finding rather than in a link.
385
+ *
386
+ * axe-core's help text says what is wrong; this says who it stops and what to
387
+ * change. The corrected line is built from the element that actually failed,
388
+ * because a textbook snippet is a second thing to translate before anybody can
389
+ * use it.
390
+ */
391
+ function remediationLines(ruleId, html, ctx) {
392
+ const remediation = remediationFor(ruleId, ctx.framework);
393
+ if (remediation === void 0) return [];
394
+ const indent = " ";
395
+ const width = ctx.width - 6 - 2;
396
+ const lines = [...wrap(remediation.why, width).map((text) => line(ctx, `${indent}${text}`, ctx.c.dim)), ...wrap(`Fix: ${remediation.fix}`, width).map((text) => line(ctx, `${indent}${text}`))];
397
+ const example = html === void 0 ? void 0 : remediation.example?.(html);
398
+ if (example !== void 0) lines.push(render(ctx, [{
399
+ text: `${indent}→ `,
400
+ paint: ctx.c.green
401
+ }, { text: collapse(example) }]));
402
+ return lines;
403
+ }
308
404
  /** Where one element appears, and what that says about where the fix goes. */
309
405
  function whereLines(element, ctx) {
310
406
  const shown = element.pages.slice(0, ctx.maxNodes);
@@ -313,7 +409,7 @@ function whereLines(element, ctx) {
313
409
  const lines = component === void 0 ? [] : [render(ctx, [{
314
410
  text: " written in ",
315
411
  paint: ctx.c.dim
316
- }, { text: component }])];
412
+ }, { text: componentPath(component) }])];
317
413
  lines.push(line(ctx, ` on ${count(element.pages.length, "page")}:`, ctx.c.dim));
318
414
  for (const page of shown) {
319
415
  const source = ctx.sourceFor(page);
@@ -364,16 +460,22 @@ async function resolvePages(directory, options = {}) {
364
460
  return {
365
461
  pages: crawled.pages,
366
462
  origin: crawled.origin,
367
- label: options.url
463
+ label: options.url,
464
+ completeness: crawled.completeness
368
465
  };
369
466
  }
370
467
  const cwd = options.cwd ?? process.cwd();
371
468
  const shown = options.label ?? directory;
469
+ const unreachable = [];
372
470
  let pages;
373
471
  try {
374
472
  pages = await collectPages(directory, {
375
473
  ...options.include ? { include: options.include } : {},
376
- ...options.exclude ? { exclude: options.exclude } : {}
474
+ ...options.exclude ? { exclude: options.exclude } : {},
475
+ onUnreadable: (relativePath, reason) => unreachable.push({
476
+ location: relativePath,
477
+ reason
478
+ })
377
479
  });
378
480
  } catch (cause) {
379
481
  if (!(cause instanceof BuildDirectoryError)) throw cause;
@@ -385,9 +487,21 @@ async function resolvePages(directory, options = {}) {
385
487
  warn(await emptyDirectoryHint(shown, cwd));
386
488
  return;
387
489
  }
490
+ if (unreachable.length > 0) {
491
+ const verb = unreachable.length === 1 ? "file was" : "files were";
492
+ warn(`${unreachable.length} ${verb} not readable, and so not audited:`);
493
+ for (const file of unreachable.slice(0, 10)) note(` ${file.location} — ${file.reason}`);
494
+ if (unreachable.length > 10) note(` …and ${unreachable.length - 10} more`);
495
+ }
388
496
  return {
389
497
  pages,
390
- label: shown
498
+ label: shown,
499
+ completeness: {
500
+ discovery: "directory",
501
+ collected: pages.length,
502
+ unreachable,
503
+ truncated: false
504
+ }
391
505
  };
392
506
  }
393
507
  /**
@@ -397,7 +511,7 @@ async function resolvePages(directory, options = {}) {
397
511
  * exit 2 — a run that reached no verdict, not a clean one.
398
512
  */
399
513
  async function crawlPages(url, options) {
400
- const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-Oxt2Gaqo.js");
514
+ const { crawlSite, CrawlError, parseEntryUrl } = await import("./crawl-BiI1Lau_.js");
401
515
  let entry;
402
516
  try {
403
517
  entry = parseEntryUrl(url, options.allowRemote ?? false);
@@ -412,6 +526,7 @@ async function crawlPages(url, options) {
412
526
  const result = await crawlSite(entry, {
413
527
  ...options.allowRemote ? { allowRemote: true } : {},
414
528
  ...options.ignoreRobots ? { ignoreRobots: true } : {},
529
+ ...options.sitemap === void 0 ? {} : { sitemap: options.sitemap },
415
530
  ...options.maxPages === void 0 ? {} : { maxPages: options.maxPages },
416
531
  ...options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth },
417
532
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
@@ -432,7 +547,16 @@ async function crawlPages(url, options) {
432
547
  if (result.truncated) warn(`Stopped at ${count(result.pages.length, "page")}; the site has more. Raise --max-pages to go further.`);
433
548
  return {
434
549
  pages: result.pages,
435
- origin: result.origin
550
+ origin: result.origin,
551
+ completeness: {
552
+ discovery: result.discovery,
553
+ collected: result.pages.length,
554
+ unreachable: result.failures.map((failure) => ({
555
+ location: failure.url,
556
+ reason: failure.reason
557
+ })),
558
+ truncated: result.truncated
559
+ }
436
560
  };
437
561
  }
438
562
  /**
@@ -444,7 +568,7 @@ async function crawlPages(url, options) {
444
568
  */
445
569
  async function resolveAutomatically(options) {
446
570
  const cwd = options.cwd ?? process.cwd();
447
- const { autoDetectSource } = await import("./project-DW08TseF.js");
571
+ const { autoDetectSource } = await import("./project-MFrXcw1M.js");
448
572
  const detected = await autoDetectSource(cwd, {
449
573
  ...options.noBuild ? { noBuild: true } : {},
450
574
  onStep: note
@@ -490,7 +614,7 @@ async function runAuditCommand(dir, options = {}) {
490
614
  audits: [],
491
615
  exitCode: 2
492
616
  };
493
- const { pages, origin, label, cleanup } = resolved;
617
+ const { pages, origin, label, cleanup, completeness: collection } = resolved;
494
618
  try {
495
619
  note(`Auditing ${count(pages.length, "page")} in ${label}${await describeEngine(pages, options)}…`);
496
620
  const baseUrl = options.baseUrl ?? origin;
@@ -515,7 +639,8 @@ async function runAuditCommand(dir, options = {}) {
515
639
  };
516
640
  audits = applied;
517
641
  }
518
- await emit(audits, label, failOn, options);
642
+ const completeness = runCompleteness(audits, collection);
643
+ await emit(audits, label, failOn, completeness, options);
519
644
  const unaudited = audits.filter((audit) => audit.error);
520
645
  if (unaudited.length > 0) {
521
646
  fail(`${unaudited.length} of ${audits.length} pages could not be audited`);
@@ -541,7 +666,7 @@ async function runAuditCommand(dir, options = {}) {
541
666
  * would be as wrong as silently passing.
542
667
  */
543
668
  async function acceptBaseline(audits, options) {
544
- const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-CuKFq4IF.js");
669
+ const { applyBaseline, BaselineError, readBaseline } = await import("./baseline-22Y1NWxM.js");
545
670
  try {
546
671
  const outcome = applyBaseline(audits, await readBaseline(options.baseline, options.cwd ?? process.cwd()));
547
672
  if (outcome.accepted > 0) note(`Baseline accepted ${outcome.accepted} violating elements`);
@@ -568,7 +693,7 @@ async function acceptBaseline(audits, options) {
568
693
  */
569
694
  async function describeEngine(pages, options) {
570
695
  if (options.browser) return " with Chromium";
571
- const { plannedWorkers } = await import("./pool-BWkWZiJW.js");
696
+ const { plannedWorkers } = await import("./pool-BMevaLWD.js");
572
697
  const workers = options.concurrency ?? plannedWorkers(pages);
573
698
  return workers > 1 ? ` across ${workers} threads` : "";
574
699
  }
@@ -577,43 +702,56 @@ async function describeEngine(pages, options) {
577
702
  * otherwise. Colour is dropped when writing to a file, since escape codes in a
578
703
  * saved report are noise.
579
704
  */
580
- async function emit(audits, dir, failOn, options) {
581
- const body = await renderReport(audits, dir, failOn, options.format ?? "console", typeof options.output === "string", options);
705
+ async function emit(audits, dir, failOn, completeness, options) {
706
+ const body = await renderReport(audits, dir, failOn, completeness, options.format ?? "console", typeof options.output === "string", options);
582
707
  await emitDocument(body, options.output, options.cwd ?? process.cwd());
583
708
  if (options.output !== void 0) note(`Report written to ${options.output}`);
584
709
  }
585
- async function renderReport(audits, dir, failOn, format, toFile, options) {
710
+ async function renderReport(audits, dir, failOn, completeness, format, toFile, options) {
586
711
  switch (format) {
587
712
  case "json": {
588
- const { buildJsonReport, serialiseJsonReport } = await import("./json-B0Y7rNjt.js");
713
+ const { buildJsonReport, serialiseJsonReport } = await import("./json-QQuFIw1W.js");
589
714
  return serialiseJsonReport(buildJsonReport(audits, {
590
715
  directory: dir,
591
716
  ...options.url === void 0 ? {} : { sourceKind: "url" },
592
717
  failOn,
718
+ completeness,
593
719
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
594
720
  }));
595
721
  }
596
722
  case "sarif": {
597
- const { buildSarifReport, serialiseSarifReport } = await import("./sarif-DB3WG7T9.js");
598
- return serialiseSarifReport(buildSarifReport(audits, { directory: dir }));
723
+ const { buildSarifReport, serialiseSarifReport } = await import("./sarif-SR3_lLYd.js");
724
+ return serialiseSarifReport(buildSarifReport(audits, {
725
+ directory: dir,
726
+ completeness
727
+ }));
599
728
  }
600
729
  case "html": {
601
- const { buildHtmlReport } = await import("./html-DKiI_3gs.js");
730
+ const { buildHtmlReport } = await import("./html-BGTO3ypW.js");
731
+ const framework = await detectedFramework(options.cwd ?? process.cwd());
602
732
  return buildHtmlReport(audits, {
603
733
  ...await sourceLookups(options.cwd ?? process.cwd()),
604
734
  directory: dir,
605
735
  failOn,
736
+ completeness,
737
+ ...framework === void 0 ? {} : { framework },
606
738
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
607
739
  });
608
740
  }
609
- case "console": return `${formatConsoleReport(audits, {
610
- ...await sourceLookups(options.cwd ?? process.cwd()),
611
- dir,
612
- failOn,
613
- ...options.perPage ? { perPage: true } : {},
614
- ...options.manual ? { manual: true } : {},
615
- ...toFile ? { color: false } : {}
616
- })}\n`;
741
+ case "console": {
742
+ const framework = await detectedFramework(options.cwd ?? process.cwd());
743
+ return `${formatConsoleReport(audits, {
744
+ ...await sourceLookups(options.cwd ?? process.cwd()),
745
+ dir,
746
+ failOn,
747
+ completeness,
748
+ ...options.perPage ? { perPage: true } : {},
749
+ ...options.manual ? { manual: true } : {},
750
+ ...options.coverage ? { coverage: true } : {},
751
+ ...framework === void 0 ? {} : { framework },
752
+ ...toFile ? { color: false } : {}
753
+ })}\n`;
754
+ }
617
755
  }
618
756
  }
619
757
  /**
@@ -621,9 +759,15 @@ async function renderReport(audits, dir, failOn, format, toFile, options) {
621
759
  * so. Best-effort: a project using no convention this recognises gets the
622
760
  * report it always got, with no source named.
623
761
  */
762
+ /** The registry id of whatever built this project, for framework-shaped advice. */
763
+ async function detectedFramework(cwd) {
764
+ const { detectFramework } = await import("./frameworks-etFg_O8K.js");
765
+ const { readPackageJson } = await import("./project-MFrXcw1M.js");
766
+ return (await detectFramework(cwd, await readPackageJson(cwd)))?.framework.id;
767
+ }
624
768
  async function sourceLookups(cwd) {
625
- const { buildRouteMap, sourceFor } = await import("./routes-C2Cgf6Ko.js");
626
- const { buildComponentIndex, componentFor } = await import("./component-7kEBjv_y.js");
769
+ const { buildRouteMap, sourceFor } = await import("./routes-CmdRUuOs.js");
770
+ const { buildComponentIndex, componentFor } = await import("./component-DKd3EHOg.js");
627
771
  const routes = await buildRouteMap(cwd);
628
772
  const components = await buildComponentIndex(cwd);
629
773
  return {
@@ -0,0 +1,2 @@
1
+ import { n as runAuditCommand } from "./audit-CPoZMXGM.js";
2
+ export { runAuditCommand };
@@ -1,2 +1,2 @@
1
- import { a as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-s9F3fXTN.js";
1
+ import { a as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-DB9CZGnV.js";
2
2
  export { BaselineError, applyBaseline, readBaseline };
@@ -1,6 +1,6 @@
1
- import { _ as withDefault, c as object, f as safeParse, i as isoDate$1, l as optional, o as nullable, p as string, s as number, t as array } from "./schema-CMZ8ItGk.js";
1
+ import { a as isoDate$1, c as number, l as object, m as string, p as safeParse, s as nullable, t as array, u as optional, v as withDefault } from "./schema-is6CGX2D.js";
2
2
  import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
3
- import { r as findingElements } from "./result-DLxd2Eip.js";
3
+ import { a as findingElements } from "./result-DoamKFsp.js";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  /** Default filename, used by the CLI when no path is given. */