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,19 +1,9 @@
1
- import { a as failedPage, i as blindRulesInScope, l as runOptions, n as DEFAULT_TAGS, s as pageUrl, t as DEFAULT_PAGE_TIMEOUT_MS, u as shapeResults } from "./result-BWcYXeRs.js";
1
+ import { c as pageUrl, d as runOptions, f as shapeResults, i as blindRulesInScope, n as DEFAULT_TAGS, o as failedPage } from "./result-B7_zVxAX.js";
2
2
  import axe from "axe-core";
3
3
  import { Script } from "node:vm";
4
4
  import { JSDOM, VirtualConsole } from "jsdom";
5
5
  //#region src/audit/runners/jsdom.ts
6
6
  /**
7
- * Per-page ceiling.
8
- *
9
- * A soft one, and the limit is worth stating: this races axe-core against a
10
- * timer, so it only fires where the work yields to the event loop. Neither
11
- * jsdom's parse nor axe-core's walk of the tree does, so a document pathological
12
- * enough to hold the thread runs past this unimpeded. The hard ceiling is the
13
- * worker pool's, which terminates the thread; see the note on `runWorkers`.
14
- */
15
- const DEFAULT_TIMEOUT_MS = DEFAULT_PAGE_TIMEOUT_MS;
16
- /**
17
7
  * Audit collected pages with axe-core inside jsdom.
18
8
  *
19
9
  * Pages are processed sequentially: jsdom parsing and axe-core are both
@@ -40,7 +30,7 @@ async function auditPage(page, options = {}) {
40
30
  dom = createDom(page.html, url);
41
31
  injectAxe(dom);
42
32
  const { axe: pageAxe } = dom.window;
43
- const results = await withTimeout(pageAxe.run(dom.window.document, runOptions(tags, { skipBlindRules: options.fast === true })), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
33
+ const results = await withTimeout(pageAxe.run(dom.window.document, runOptions(tags, { skipBlindRules: options.fast === true })), options.timeoutMs ?? 3e4);
44
34
  return shapeResults(results, {
45
35
  ...identity,
46
36
  durationMs: Date.now() - startedAt,
@@ -74,6 +64,15 @@ function injectAxe(dom) {
74
64
  axeScript ??= new Script(axe.source, { filename: "axe-core.js" });
75
65
  axeScript.runInContext(dom.getInternalVMContext());
76
66
  }
67
+ /**
68
+ * The per-page ceiling, and it is a soft one.
69
+ *
70
+ * This races axe-core against a timer, so it only fires where the work yields to
71
+ * the event loop. Neither jsdom's parse nor axe-core's walk of the tree does, so
72
+ * a document pathological enough to hold the thread runs past this unimpeded.
73
+ * The hard ceiling is the worker pool's, which terminates the thread; see the
74
+ * note on `runWorkers`.
75
+ */
77
76
  async function withTimeout(promise, ms) {
78
77
  promise.catch(() => {});
79
78
  let timer;
@@ -1,8 +1,8 @@
1
- import { i as impactLabel, o as isImpactLevel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
1
+ import { a as impactLabel, i as countAtOrAbove, s as isImpactLevel } from "./impact-luGCnN8F.js";
2
2
  import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
3
3
  import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
4
- import { c as ruleOutcomes } from "./result-BWcYXeRs.js";
5
- import { t as buildCoverage } from "./coverage-B_Y6l-Ra.js";
4
+ import { m as uniqueRuleOutcomes, u as runEngine } from "./result-B7_zVxAX.js";
5
+ import { t as buildCoverage } from "./coverage-cb5kPp48.js";
6
6
  import axe from "axe-core";
7
7
  /**
8
8
  * Build the machine-readable report.
@@ -24,7 +24,7 @@ function buildJsonReport(audits, options) {
24
24
  axeCore: axe.version
25
25
  },
26
26
  generatedAt,
27
- engine: audits[0]?.engine ?? "jsdom",
27
+ engine: runEngine(audits),
28
28
  target: {
29
29
  source: options.directory,
30
30
  kind: options.sourceKind ?? "directory",
@@ -33,24 +33,19 @@ function buildJsonReport(audits, options) {
33
33
  },
34
34
  summary: buildSummary(audits, options.failOn),
35
35
  completeness: options.completeness,
36
- coverage: buildCoverage(audits),
36
+ coverage: buildCoverage(audits, void 0, options.review),
37
37
  rules: buildRuleIndex(audits),
38
38
  pages: audits.map(toJsonPage)
39
39
  };
40
40
  }
41
41
  /** Every rule mentioned by any page, keyed by id and sorted for stable diffs. */
42
42
  function buildRuleIndex(audits) {
43
- const index = /* @__PURE__ */ new Map();
44
- for (const audit of audits) for (const outcome of ruleOutcomes(audit)) {
45
- if (index.has(outcome.ruleId)) continue;
46
- index.set(outcome.ruleId, {
47
- help: outcome.help,
48
- helpUrl: outcome.helpUrl,
49
- successCriteria: outcome.successCriteria,
50
- en301549: outcome.enClauses
51
- });
52
- }
53
- return Object.fromEntries([...index].sort(([a], [b]) => a.localeCompare(b)));
43
+ return Object.fromEntries(uniqueRuleOutcomes(audits).map((outcome) => [outcome.ruleId, {
44
+ help: outcome.help,
45
+ helpUrl: outcome.helpUrl,
46
+ successCriteria: outcome.successCriteria,
47
+ en301549: outcome.enClauses
48
+ }]));
54
49
  }
55
50
  /** Serialised form written to stdout or to --output, with a trailing newline. */
56
51
  function serialiseJsonReport(report) {
@@ -109,7 +104,8 @@ function toJsonPage(audit) {
109
104
  incomplete: [...audit.incomplete].sort(byRuleId).map(toJsonIncomplete),
110
105
  passes: [...audit.passes].sort(byRuleId).map((outcome) => outcome.ruleId),
111
106
  inapplicable: [...audit.inapplicable].sort(byRuleId).map((outcome) => outcome.ruleId),
112
- error: audit.error ?? null
107
+ error: audit.error ?? null,
108
+ ...audit.reused === void 0 ? {} : { reusedFrom: audit.reused.on }
113
109
  };
114
110
  }
115
111
  function toJsonFinding(finding) {
@@ -1,2 +1,2 @@
1
- import { r as serialiseJsonReport, t as buildJsonReport } from "./json-DROX33kh.js";
1
+ import { r as serialiseJsonReport, t as buildJsonReport } from "./json-Bus6YhxB.js";
2
2
  export { buildJsonReport, serialiseJsonReport };
@@ -1,4 +1,4 @@
1
- import { n as IMPACT_LEVELS } from "./impact-DZt2oBCP.js";
1
+ import { n as IMPACT_LEVELS } from "./impact-luGCnN8F.js";
2
2
  import { _ as union, a as integer, d as optional, f as pipe, g as transform, h as string, i as enumeration, m as safeParse, n as boolean, o as isoDate, r as email, t as array, u as object, v as url, y as withDefault } from "./schema-DJSF4K05.js";
3
3
  import { r as isFile } from "./fs-BmPtmFke.js";
4
4
  import { readFile } from "node:fs/promises";
@@ -122,12 +122,21 @@ const auditSchema = object({
122
122
  manual: optional(boolean()),
123
123
  /** List every WCAG 2.2 A/AA criterion and what the run reached on it. */
124
124
  coverage: optional(boolean()),
125
+ /** Path to a review record: what a person checked, for the criteria no engine reaches. */
126
+ review: optional(string({ min: 1 })),
127
+ /** Days after which a recorded review stops counting. */
128
+ reviewMaxAge: optional(integer({ min: 0 })),
125
129
  /**
126
130
  * False is `--no-build`: never run the project's build or start its server to
127
131
  * find something to audit. Written in the positive because that is the state
128
132
  * being described, and because a config file has no flags to negate.
129
133
  */
130
- build: optional(boolean())
134
+ build: optional(boolean()),
135
+ /**
136
+ * False is `--no-cache`: audit every page, reusing nothing an earlier run
137
+ * measured. Written in the positive for the same reason `build` is.
138
+ */
139
+ cache: optional(boolean())
131
140
  });
132
141
  const configSchema = object({
133
142
  site: object({
@@ -0,0 +1,2 @@
1
+ import { r as loadAuditConfig } from "./load-BaWMa-G_.js";
2
+ export { loadAuditConfig };
@@ -1,4 +1,4 @@
1
- import { n as IntegrationOptions, t as BuildAuditError } from "../run-CtcEUhbe.js";
1
+ import { n as IntegrationOptions, t as BuildAuditError } from "../run-BK2blxMI.js";
2
2
  //#region src/nuxt/index.d.ts
3
3
  /** The part of the Nitro instance this reads, handed over by `nitro:init`. */
4
4
  interface NitroLike {
@@ -1,4 +1,4 @@
1
- import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-C2nKFcb-.js";
1
+ import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-DrS0Komf.js";
2
2
  import path from "node:path";
3
3
  //#region src/nuxt/index.ts
4
4
  function eaaKitModule(options = {}, nuxt) {
@@ -1,5 +1,5 @@
1
1
  import { r as isFile } from "./fs-BmPtmFke.js";
2
- import { a as failedPage, l as runOptions, n as DEFAULT_TAGS, s as pageUrl, u as shapeResults } from "./result-BWcYXeRs.js";
2
+ import { c as pageUrl, d as runOptions, f as shapeResults, n as DEFAULT_TAGS, o as failedPage } from "./result-B7_zVxAX.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { stat } from "node:fs/promises";
5
5
  import path from "node:path";
@@ -165,7 +165,8 @@ async function runBrowserAudit(directory, pages, options = {}) {
165
165
  try {
166
166
  const context = await browser.newContext({
167
167
  viewport,
168
- bypassCSP: true
168
+ bypassCSP: true,
169
+ ...options.headers === void 0 ? {} : { extraHTTPHeaders: options.headers }
169
170
  });
170
171
  const audits = Array.from({ length: pages.length });
171
172
  const lanes = Math.max(1, Math.min(options.concurrency ?? DEFAULT_CONCURRENCY, pages.length));
@@ -1,5 +1,5 @@
1
1
  import { r as isFile } from "./fs-BmPtmFke.js";
2
- import { a as failedPage, s as pageUrl } from "./result-BWcYXeRs.js";
2
+ import { c as pageUrl, o as failedPage } from "./result-B7_zVxAX.js";
3
3
  import { Worker } from "node:worker_threads";
4
4
  import { availableParallelism } from "node:os";
5
5
  //#region src/audit/runners/pool.ts
@@ -105,8 +105,12 @@ async function runPooledAudit(pages, options = {}) {
105
105
  if (!entry) return auditHere(pages, runnerOptions);
106
106
  return runWorkers(pages, runnerOptions, Math.min(workers, pages.length), entry);
107
107
  }
108
+ /**
109
+ * Audit in this process: the fallback for a run with no threads, and the sweep
110
+ * for pages the threads never reported on.
111
+ */
108
112
  async function auditHere(pages, options) {
109
- const { runJsdomAudit } = await import("./jsdom-22Bkt65v.js");
113
+ const { runJsdomAudit } = await import("./jsdom-BUYuVJJm.js");
110
114
  return runJsdomAudit(pages, options);
111
115
  }
112
116
  /**
@@ -1,6 +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";
3
+ import { s as findingElements } from "./result-B7_zVxAX.js";
4
4
  //#region src/audit/completeness.ts
5
5
  /**
6
6
  * Fold what the collector knew together with what the engine managed.
@@ -10,14 +10,28 @@ import { o as findingElements } from "./result-BWcYXeRs.js";
10
10
  */
11
11
  function runCompleteness(audits, collection) {
12
12
  const errored = audits.filter((audit) => audit.error).length;
13
+ const reused = audits.filter((audit) => audit.reused !== void 0).length;
13
14
  return {
14
15
  ...collection,
15
- audited: audits.length - errored,
16
+ audited: audits.length - errored - reused,
17
+ reused,
16
18
  errored,
17
19
  complete: collection.unreachable.length === 0 && !collection.truncated && errored === 0
18
20
  };
19
21
  }
20
22
  /**
23
+ * What a run reused, in one phrase, or nothing when it audited everything.
24
+ *
25
+ * Its own sentence rather than a clause in `missedParts`: nothing was missed
26
+ * here. The pages have verdicts; they were reached on an earlier day, on markup
27
+ * identical to today's, and a reader is entitled to know which.
28
+ */
29
+ function reusedPart(completeness) {
30
+ if (completeness.reused === 0) return void 0;
31
+ const pages = completeness.reused === 1 ? "page" : "pages";
32
+ return `${completeness.reused} ${pages} unchanged since the last run, so the earlier result was reused`;
33
+ }
34
+ /**
21
35
  * What was missed, as phrases both reports print.
22
36
  *
23
37
  * Separate clauses rather than one total, for the same reason the coverage
@@ -95,6 +109,23 @@ function groupIssues(audits) {
95
109
  return issues;
96
110
  }
97
111
  /**
112
+ * What the grouped issues add up to: distinct elements, and the violations they
113
+ * account for. Both reports open their issues section with these two numbers,
114
+ * and say something different when they are equal.
115
+ */
116
+ function issueTotals(issues) {
117
+ let elements = 0;
118
+ let occurrences = 0;
119
+ for (const issue of issues) {
120
+ elements += issue.elements.length;
121
+ occurrences += issue.occurrences;
122
+ }
123
+ return {
124
+ elements,
125
+ occurrences
126
+ };
127
+ }
128
+ /**
98
129
  * Unevaluated rules folded across the site, sorted by rule id.
99
130
  *
100
131
  * Both reports list these once at the end rather than under every page: on a
@@ -318,4 +349,4 @@ function remediationFor(ruleId, framework) {
318
349
  };
319
350
  }
320
351
  //#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 };
352
+ export { isShared as a, missedParts as c, groupIssues as i, reusedPart as l, blindRules as n, issueTotals as o, coverageParts as r, discoveryLabel as s, remediationFor as t, runCompleteness as u };
@@ -1,9 +1,10 @@
1
- import { a as impactRank, n as IMPACT_LEVELS } from "./impact-DZt2oBCP.js";
1
+ import { n as IMPACT_LEVELS, o as impactRank } from "./impact-luGCnN8F.js";
2
2
  import { c as nullable, h as string, l as number, m as safeParse, p as record, s as isoDateTime, t as array, u as object, y as withDefault } from "./schema-DJSF4K05.js";
3
- import { u as STATEMENT_LOCALES } from "./load-yAR4wzez.js";
3
+ import { u as STATEMENT_LOCALES } from "./load-BaWMa-G_.js";
4
4
  import { n as isDirectory } from "./fs-BmPtmFke.js";
5
- import { i as escapeText, o as standardsReference, r as escapeAttribute } from "./text-CKKpzkYM.js";
5
+ import { a as isoDate, i as escapeText, n as count, r as escapeAttribute, s as standardsReference } from "./text-BF1LHMiV.js";
6
6
  import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
7
+ import { t as WCAG22_AA_CRITERIA } from "./criteria-CnPJKGeb.js";
7
8
  import { readFile, readdir } from "node:fs/promises";
8
9
  import path from "node:path";
9
10
  import { fileURLToPath } from "node:url";
@@ -20,6 +21,107 @@ import { fileURLToPath } from "node:url";
20
21
  var StatementError = class extends Error {
21
22
  name = "StatementError";
22
23
  };
24
+ //#endregion
25
+ //#region src/statement/evidence.ts
26
+ /**
27
+ * How old a claim or a report may be before the document says so.
28
+ *
29
+ * A year, because a statement is a document about a site that changes, and one
30
+ * dated further back than that is describing a site nobody has checked since.
31
+ * It is a warning and never a refusal: how often a statement must be revisited
32
+ * is a question about a legal regime and a rate of change, and this tool is in
33
+ * no position to answer it for somebody.
34
+ */
35
+ const STALE_DAYS = 365;
36
+ function checkStatementEvidence(input) {
37
+ const today = input.today ?? /* @__PURE__ */ new Date();
38
+ return [
39
+ ...claimAgainstAudit(input),
40
+ ...claimAgainstReview(input),
41
+ ...dates(input, today)
42
+ ];
43
+ }
44
+ /** Whether any problem is bad enough to stop the document being written. */
45
+ function refuses(problems) {
46
+ return problems.some((problem) => problem.severity === "refuses");
47
+ }
48
+ /**
49
+ * A claim of full conformance, against barriers the audit found.
50
+ *
51
+ * Refused rather than warned about. Every other output of this tool is a report
52
+ * somebody reads; this one is a legal document published under their name, and
53
+ * the failure mode is not a wrong number in a terminal but a false statement on
54
+ * a website. The fix is in the message because it is not obvious to somebody
55
+ * filling in a config file for the first time that `partially-compliant` is the
56
+ * ordinary answer rather than an admission of defeat.
57
+ */
58
+ function claimAgainstAudit(input) {
59
+ const audit = input.audit;
60
+ if (audit === void 0 || input.config.compliance.status !== "compliant") return [];
61
+ if (audit.findings.length === 0) return [];
62
+ const pages = new Set(audit.findings.flatMap((finding) => finding.pages)).size;
63
+ return [{
64
+ severity: "refuses",
65
+ message: `The config claims full conformance, and the audit report lists ${count(audit.findings.length, "barrier")} on ${count(pages, "page")}.\n A statement claiming full conformance for a site that is not conformant is worse\n than no statement at all, so this one was not written.\n Either fix the barriers and audit again, or set compliance.status to\n "partially-compliant", which is the honest answer for most sites and the one that\n carries the obligation to list what is missing.`
66
+ }];
67
+ }
68
+ /**
69
+ * A claim of full conformance, against criteria a person recorded as not met.
70
+ *
71
+ * The review record is the only evidence this tool has about the 34 criteria no
72
+ * engine can reach, and a `not-met` in it is somebody's own finding. Claiming
73
+ * full conformance over the top of it is the same false statement as claiming
74
+ * it over the top of a violation, so it gets the same refusal.
75
+ */
76
+ function claimAgainstReview(input) {
77
+ const review = input.review;
78
+ if (review === void 0 || input.config.compliance.status !== "compliant") return [];
79
+ const notMet = Object.entries(review.criteria).filter(([, entry]) => entry.result === "not-met").map(([number]) => number);
80
+ if (notMet.length === 0) return [];
81
+ return [{
82
+ severity: "refuses",
83
+ message: `The config claims full conformance, and the review record has ${count(notMet.length, "criterion")} recorded as not met: ${notMet.join(", ")}.\n Somebody checked those and wrote down that the site does not meet them, so this\n statement was not written. Fix them and record the result, or set\n compliance.status to "partially-compliant" and describe them in\n compliance.knownIssues.`
84
+ }];
85
+ }
86
+ /**
87
+ * Whether the document's dates still describe the site.
88
+ *
89
+ * All warnings. A date being old is not a false claim — it is a document nobody
90
+ * has revisited, which is a different problem with a different fix, and the
91
+ * person publishing it is the one who knows whether the site has moved.
92
+ */
93
+ function dates(input, today) {
94
+ const problems = [];
95
+ const assessedOn = input.config.compliance.assessedOn;
96
+ const now = isoDate(today);
97
+ if (assessedOn > now) problems.push({
98
+ severity: "warns",
99
+ message: `compliance.assessedOn is ${assessedOn}, which is in the future. A statement dated ahead of its own assessment is a typo somebody will notice.`
100
+ });
101
+ else if (daysBetween(assessedOn, now) > STALE_DAYS) problems.push({
102
+ severity: "warns",
103
+ message: `compliance.assessedOn is ${assessedOn}, ${count(daysBetween(assessedOn, now), "day")} ago. The statement will say so, and it describes a site nobody has assessed since.`
104
+ });
105
+ const audit = input.audit;
106
+ if (audit === void 0) return problems;
107
+ const auditDay = audit.generatedAt.slice(0, 10);
108
+ if (auditDay > assessedOn) problems.push({
109
+ severity: "warns",
110
+ message: `The audit ran on ${auditDay}, after the ${assessedOn} this statement gives as its assessment date. Move compliance.assessedOn forward, or cite the report from that day.`
111
+ });
112
+ if (daysBetween(auditDay, now) > STALE_DAYS) problems.push({
113
+ severity: "warns",
114
+ message: `The audit report is from ${auditDay}, ${count(daysBetween(auditDay, now), "day")} ago. The barriers it lists are the ones the site had then.`
115
+ });
116
+ return problems;
117
+ }
118
+ /** Whole days between two ISO dates, both UTC, without a date library. */
119
+ function daysBetween(from, to) {
120
+ const start = Date.parse(`${from}T00:00:00Z`);
121
+ const end = Date.parse(`${to}T00:00:00Z`);
122
+ if (Number.isNaN(start) || Number.isNaN(end)) return 0;
123
+ return Math.round((end - start) / 864e5);
124
+ }
23
125
  /**
24
126
  * Only the fields the statement reads. Everything else in the report — node
25
127
  * markup, selectors, passes, inapplicable — is audit detail with no place in a
@@ -412,7 +514,7 @@ async function renderStatement(config, options = {}) {
412
514
  const country = options.country ?? config.enforcement.country;
413
515
  const locale = options.locale ?? defaultLocale(config);
414
516
  const template = `${country.toLowerCase()}.${locale}`;
415
- const markdown = tidy(renderTemplate(await loadTemplate(country, locale), buildScope(config, locale, options.audit)));
517
+ const markdown = tidy(renderTemplate(await loadTemplate(country, locale), buildScope(config, locale, options)));
416
518
  return {
417
519
  markdown,
418
520
  html: toHtmlDocument(markdown, {
@@ -442,7 +544,9 @@ function defaultLocale(config) {
442
544
  * template, which keeps the template language trivial and puts the mapping
443
545
  * somewhere that can be typechecked.
444
546
  */
445
- function buildScope(config, locale, audit) {
547
+ function buildScope(config, locale, options) {
548
+ const audit = options.audit;
549
+ const review = options.review ? toReviewScope(options.review, locale) : void 0;
446
550
  const issues = [...config.compliance.knownIssues.map((issue) => toIssueScope(issue, locale)), ...(audit?.findings ?? []).map((finding) => toFindingScope(finding, config.compliance.auditReason))];
447
551
  return {
448
552
  site: { ...config.site },
@@ -459,6 +563,8 @@ function buildScope(config, locale, audit) {
459
563
  },
460
564
  audit: audit ? toAuditScope(audit, locale) : void 0,
461
565
  hasAudit: audit !== void 0,
566
+ review,
567
+ hasReview: review !== void 0,
462
568
  hasKnownIssues: issues.length > 0,
463
569
  hasNoKnownIssues: issues.length === 0
464
570
  };
@@ -484,6 +590,39 @@ function toAuditScope(audit, locale) {
484
590
  checkedOnFormatted: formatDate(audit.generatedAt.slice(0, 10), locale)
485
591
  };
486
592
  }
593
+ /**
594
+ * What a person's review contributes to the "preparation" section.
595
+ *
596
+ * One fact: how many of WCAG 2.2's success criteria at A and AA somebody
597
+ * checked by hand, and the last day one of those checks was recorded. It is
598
+ * there because the automated sentences beside it describe a run that cannot
599
+ * reach 34 of those 55 criteria, and a reader with no way to tell an unchecked
600
+ * criterion from an unchecked-by-machine one is being told less than the truth.
601
+ *
602
+ * What it never says is what the review concluded. A recorded result is a claim
603
+ * by a person, and the place for a claim in this document is the barrier list,
604
+ * where a person writes it in their own words. Turning `not-met` into a
605
+ * conformance sentence here would be this tool putting a legal position in
606
+ * somebody's mouth.
607
+ *
608
+ * Returns undefined for a record nobody has answered yet — the ordinary state
609
+ * of the file `eaa-kit checklist` has just written. Generating a worksheet is
610
+ * not doing the review, and the audit report refuses to count it for the same
611
+ * reason.
612
+ */
613
+ function toReviewScope(record, locale) {
614
+ const answered = Object.values(record.criteria).filter((entry) => entry.result !== "unreviewed");
615
+ if (answered.length === 0) return void 0;
616
+ const latest = answered.map((entry) => entry.reviewedOn).filter((on) => on !== void 0).sort().at(-1);
617
+ return {
618
+ answered: answered.length,
619
+ total: WCAG22_AA_CRITERIA.length,
620
+ isSingle: answered.length === 1,
621
+ isPlural: answered.length > 1,
622
+ hasDate: latest !== void 0,
623
+ checkedOnFormatted: latest === void 0 ? "" : formatDate(latest, locale)
624
+ };
625
+ }
487
626
  function toIssueScope(issue, locale) {
488
627
  return {
489
628
  ...reasonScope(issue.reason),
@@ -614,4 +753,4 @@ async function findTemplateDirectory() {
614
753
  throw new StatementError(`Could not locate the statement templates. Looked in: ${candidates.join(", ")}`);
615
754
  }
616
755
  //#endregion
617
- export { summariseAuditReport as a, readAuditReport as i, toHtmlBody as n, StatementError as o, toHtmlDocument as r, renderStatement as t };
756
+ export { summariseAuditReport as a, StatementError as c, readAuditReport as i, toHtmlBody as n, checkStatementEvidence as o, toHtmlDocument as r, refuses as s, renderStatement as t };
@@ -131,6 +131,29 @@ function ruleOutcomes(audit) {
131
131
  ...audit.inapplicable
132
132
  ];
133
133
  }
134
+ /**
135
+ * Which engine produced this run, as every report format has to label it.
136
+ *
137
+ * Taken from the first page rather than tracked separately: a run uses one
138
+ * engine for all of its pages, and an empty run has none to report, which is
139
+ * the jsdom default the CLI would have used.
140
+ */
141
+ function runEngine(audits) {
142
+ return audits[0]?.engine ?? "jsdom";
143
+ }
144
+ /**
145
+ * Every rule the run mentions anywhere, once each, sorted by id.
146
+ *
147
+ * Both machine-readable formats need this and neither can take it from the
148
+ * other: the JSON report keys its `rules` map by id, and SARIF needs the same
149
+ * catalogue as a list its results index into. First mention wins because a
150
+ * rule's help text and criteria do not vary between the pages it appears on.
151
+ */
152
+ function uniqueRuleOutcomes(audits) {
153
+ const byId = /* @__PURE__ */ new Map();
154
+ for (const audit of audits) for (const outcome of ruleOutcomes(audit)) if (!byId.has(outcome.ruleId)) byId.set(outcome.ruleId, outcome);
155
+ return [...byId.values()].sort((a, b) => a.ruleId.localeCompare(b.ruleId));
156
+ }
134
157
  function runOptions(tags, options = {}) {
135
158
  return {
136
159
  runOnly: {
@@ -313,4 +336,4 @@ function enClauses(tags) {
313
336
  return [...clauses].sort();
314
337
  }
315
338
  //#endregion
316
- export { failedPage as a, ruleOutcomes as c, successCriteria as d, blindRulesInScope as i, runOptions as l, DEFAULT_TAGS as n, findingElements as o, ENGINE_BLIND_RULES as r, pageUrl as s, DEFAULT_PAGE_TIMEOUT_MS as t, shapeResults as u };
339
+ export { enClauses as a, pageUrl as c, runOptions as d, shapeResults as f, blindRulesInScope as i, ruleOutcomes as l, uniqueRuleOutcomes as m, DEFAULT_TAGS as n, failedPage as o, successCriteria as p, ENGINE_BLIND_RULES as r, findingElements as s, DEFAULT_PAGE_TIMEOUT_MS as t, runEngine as u };
@@ -0,0 +1,2 @@
1
+ import { c as pageUrl } from "./result-B7_zVxAX.js";
2
+ export { pageUrl };