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
@@ -1,8 +1,8 @@
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-vZdV_fD1.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
7
  import { readFile, readdir } from "node:fs/promises";
8
8
  import path from "node:path";
@@ -20,6 +20,107 @@ import { fileURLToPath } from "node:url";
20
20
  var StatementError = class extends Error {
21
21
  name = "StatementError";
22
22
  };
23
+ //#endregion
24
+ //#region src/statement/evidence.ts
25
+ /**
26
+ * How old a claim or a report may be before the document says so.
27
+ *
28
+ * A year, because a statement is a document about a site that changes, and one
29
+ * dated further back than that is describing a site nobody has checked since.
30
+ * It is a warning and never a refusal: how often a statement must be revisited
31
+ * is a question about a legal regime and a rate of change, and this tool is in
32
+ * no position to answer it for somebody.
33
+ */
34
+ const STALE_DAYS = 365;
35
+ function checkStatementEvidence(input) {
36
+ const today = input.today ?? /* @__PURE__ */ new Date();
37
+ return [
38
+ ...claimAgainstAudit(input),
39
+ ...claimAgainstReview(input),
40
+ ...dates(input, today)
41
+ ];
42
+ }
43
+ /** Whether any problem is bad enough to stop the document being written. */
44
+ function refuses(problems) {
45
+ return problems.some((problem) => problem.severity === "refuses");
46
+ }
47
+ /**
48
+ * A claim of full conformance, against barriers the audit found.
49
+ *
50
+ * Refused rather than warned about. Every other output of this tool is a report
51
+ * somebody reads; this one is a legal document published under their name, and
52
+ * the failure mode is not a wrong number in a terminal but a false statement on
53
+ * a website. The fix is in the message because it is not obvious to somebody
54
+ * filling in a config file for the first time that `partially-compliant` is the
55
+ * ordinary answer rather than an admission of defeat.
56
+ */
57
+ function claimAgainstAudit(input) {
58
+ const audit = input.audit;
59
+ if (audit === void 0 || input.config.compliance.status !== "compliant") return [];
60
+ if (audit.findings.length === 0) return [];
61
+ const pages = new Set(audit.findings.flatMap((finding) => finding.pages)).size;
62
+ return [{
63
+ severity: "refuses",
64
+ 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.`
65
+ }];
66
+ }
67
+ /**
68
+ * A claim of full conformance, against criteria a person recorded as not met.
69
+ *
70
+ * The review record is the only evidence this tool has about the 34 criteria no
71
+ * engine can reach, and a `not-met` in it is somebody's own finding. Claiming
72
+ * full conformance over the top of it is the same false statement as claiming
73
+ * it over the top of a violation, so it gets the same refusal.
74
+ */
75
+ function claimAgainstReview(input) {
76
+ const review = input.review;
77
+ if (review === void 0 || input.config.compliance.status !== "compliant") return [];
78
+ const notMet = Object.entries(review.criteria).filter(([, entry]) => entry.result === "not-met").map(([number]) => number);
79
+ if (notMet.length === 0) return [];
80
+ return [{
81
+ severity: "refuses",
82
+ 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.`
83
+ }];
84
+ }
85
+ /**
86
+ * Whether the document's dates still describe the site.
87
+ *
88
+ * All warnings. A date being old is not a false claim — it is a document nobody
89
+ * has revisited, which is a different problem with a different fix, and the
90
+ * person publishing it is the one who knows whether the site has moved.
91
+ */
92
+ function dates(input, today) {
93
+ const problems = [];
94
+ const assessedOn = input.config.compliance.assessedOn;
95
+ const now = isoDate(today);
96
+ if (assessedOn > now) problems.push({
97
+ severity: "warns",
98
+ message: `compliance.assessedOn is ${assessedOn}, which is in the future. A statement dated ahead of its own assessment is a typo somebody will notice.`
99
+ });
100
+ else if (daysBetween(assessedOn, now) > STALE_DAYS) problems.push({
101
+ severity: "warns",
102
+ 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.`
103
+ });
104
+ const audit = input.audit;
105
+ if (audit === void 0) return problems;
106
+ const auditDay = audit.generatedAt.slice(0, 10);
107
+ if (auditDay > assessedOn) problems.push({
108
+ severity: "warns",
109
+ 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.`
110
+ });
111
+ if (daysBetween(auditDay, now) > STALE_DAYS) problems.push({
112
+ severity: "warns",
113
+ message: `The audit report is from ${auditDay}, ${count(daysBetween(auditDay, now), "day")} ago. The barriers it lists are the ones the site had then.`
114
+ });
115
+ return problems;
116
+ }
117
+ /** Whole days between two ISO dates, both UTC, without a date library. */
118
+ function daysBetween(from, to) {
119
+ const start = Date.parse(`${from}T00:00:00Z`);
120
+ const end = Date.parse(`${to}T00:00:00Z`);
121
+ if (Number.isNaN(start) || Number.isNaN(end)) return 0;
122
+ return Math.round((end - start) / 864e5);
123
+ }
23
124
  /**
24
125
  * Only the fields the statement reads. Everything else in the report — node
25
126
  * markup, selectors, passes, inapplicable — is audit detail with no place in a
@@ -614,4 +715,4 @@ async function findTemplateDirectory() {
614
715
  throw new StatementError(`Could not locate the statement templates. Looked in: ${candidates.join(", ")}`);
615
716
  }
616
717
  //#endregion
617
- export { summariseAuditReport as a, readAuditReport as i, toHtmlBody as n, StatementError as o, toHtmlDocument as r, renderStatement as t };
718
+ 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 { failedPage as a, runEngine as c, successCriteria as d, uniqueRuleOutcomes as f, 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 };
@@ -0,0 +1,161 @@
1
+ import { d as optional, h as string, i as enumeration, l as number, m as safeParse, o as isoDate, p as record, u as object, y as withDefault } from "./schema-DJSF4K05.js";
2
+ import { a as isoDate$1 } from "./text-BF1LHMiV.js";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ /** Default filename, used by the CLI when no path is given. */
6
+ const DEFAULT_REVIEW_FILE = "eaa-review.json";
7
+ /**
8
+ * What a person concluded about one criterion.
9
+ *
10
+ * `not-applicable` is kept apart from `met` for the reason `inapplicable` is
11
+ * kept apart from `passes` everywhere else in this tool: a criterion with
12
+ * nothing on the site to which it applies has not been met, it has been ruled
13
+ * out, and folding the two together would let an empty site look conformant.
14
+ */
15
+ const REVIEW_RESULTS = [
16
+ "met",
17
+ "not-met",
18
+ "not-applicable",
19
+ "unreviewed"
20
+ ];
21
+ const entrySchema = object({
22
+ result: withDefault(enumeration(REVIEW_RESULTS), () => "unreviewed"),
23
+ /** ISO date the check was done. Without it the entry cannot be aged. */
24
+ reviewedOn: optional(isoDate()),
25
+ /** Who did it, when that is not the person named on the record. */
26
+ reviewedBy: optional(string()),
27
+ /** What was checked and what was found. Free text, for whoever reads it. */
28
+ note: optional(string())
29
+ });
30
+ const reviewSchema = object({
31
+ schemaVersion: number(),
32
+ /** Who carried out the review, for the record as a whole. */
33
+ reviewedBy: optional(string()),
34
+ /** Keyed by success criterion number, e.g. '1.2.1'. */
35
+ criteria: withDefault(record(entrySchema), () => ({}))
36
+ });
37
+ var ReviewError = class extends Error {
38
+ name = "ReviewError";
39
+ };
40
+ /**
41
+ * What the record says about one criterion, and whether it counts.
42
+ *
43
+ * Returns undefined when there is nothing recorded, which is the ordinary case
44
+ * for most criteria of most sites: a missing entry and an `unreviewed` one mean
45
+ * the same thing and are reported the same way.
46
+ */
47
+ function criterionReview(criterionNumber, options, engineReachedVerdict) {
48
+ const entry = options.record.criteria[criterionNumber];
49
+ if (entry === void 0 || entry.result === "unreviewed") return void 0;
50
+ const attributed = {
51
+ ...entry,
52
+ ...entry.reviewedBy === void 0 && options.record.reviewedBy !== void 0 ? { reviewedBy: options.record.reviewedBy } : {}
53
+ };
54
+ const ignored = ignoredReason(entry, options, engineReachedVerdict);
55
+ return ignored === void 0 ? {
56
+ ...attributed,
57
+ counts: true
58
+ } : {
59
+ ...attributed,
60
+ counts: false,
61
+ ignored
62
+ };
63
+ }
64
+ function ignoredReason(entry, options, engineReachedVerdict) {
65
+ if (engineReachedVerdict) return "engine-reached-a-verdict";
66
+ if (options.maxAgeDays === void 0) return void 0;
67
+ if (entry.reviewedOn === void 0) return "undated";
68
+ return olderThan(entry.reviewedOn, options.maxAgeDays, options.today ?? /* @__PURE__ */ new Date()) ? "stale" : void 0;
69
+ }
70
+ /** Whole days between an ISO date and today, without pulling in a date library. */
71
+ function olderThan(reviewedOn, maxAgeDays, today) {
72
+ const then = Date.parse(`${reviewedOn}T00:00:00Z`);
73
+ if (Number.isNaN(then)) return true;
74
+ return (Date.parse(`${isoDate$1(today)}T00:00:00Z`) - then) / 864e5 > maxAgeDays;
75
+ }
76
+ /**
77
+ * What a person recorded, as the sentence both reports print under the criterion.
78
+ *
79
+ * One sentence rather than one per format: the console report and the HTML one
80
+ * were composing the same four parts in the same order, with two tables of
81
+ * reasons that had already drifted apart in wording. A reader comparing the two
82
+ * documents of one run should not have to work out whether they mean the same
83
+ * thing.
84
+ *
85
+ * An entry that was not counted still gets a sentence, with the reason. Dropping
86
+ * it would hide the one thing a reader has to act on: a review that has aged
87
+ * out, or one recorded against a criterion the run decided for itself.
88
+ */
89
+ function reviewSentence(review) {
90
+ const by = review.reviewedBy === void 0 ? "" : ` by ${review.reviewedBy}`;
91
+ const on = review.reviewedOn ?? "no date recorded";
92
+ const because = review.counts ? "" : `, not counted: ${IGNORED_REASONS[review.ignored ?? "stale"]}`;
93
+ return `checked by hand${by} (${on}): ${review.result}${because}`;
94
+ }
95
+ /** Why an entry was read and not counted, in words rather than a code. */
96
+ const IGNORED_REASONS = {
97
+ "engine-reached-a-verdict": "this run reached its own verdict here",
98
+ stale: "older than the maximum age this run was given",
99
+ undated: "no date recorded, and a maximum age was set"
100
+ };
101
+ /** A record with an entry for every criterion, keeping any answers already given. */
102
+ function blankReview(criteria, existing) {
103
+ const entries = {};
104
+ for (const criterion of criteria) entries[criterion.number] = existing?.criteria[criterion.number] ?? { result: "unreviewed" };
105
+ for (const [number, entry] of Object.entries(existing?.criteria ?? {})) entries[number] ??= entry;
106
+ return {
107
+ schemaVersion: 1,
108
+ ...existing?.reviewedBy ? { reviewedBy: existing.reviewedBy } : {},
109
+ criteria: sortByCriterion(entries)
110
+ };
111
+ }
112
+ /** How many criteria the record has a usable answer for, before any run is considered. */
113
+ function answeredCount(record) {
114
+ return Object.values(record.criteria).filter((entry) => entry.result !== "unreviewed").length;
115
+ }
116
+ /** Numerically by section, so 1.4.10 sorts after 1.4.5 rather than before it. */
117
+ function sortByCriterion(entries) {
118
+ return Object.fromEntries(Object.entries(entries).sort(([a], [b]) => compareCriteria(a, b)));
119
+ }
120
+ function compareCriteria(a, b) {
121
+ const left = a.split(".").map(Number);
122
+ const right = b.split(".").map(Number);
123
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
124
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
125
+ if (difference !== 0) return difference;
126
+ }
127
+ return 0;
128
+ }
129
+ function serialiseReview(record) {
130
+ return `${JSON.stringify(record, null, 2)}\n`;
131
+ }
132
+ async function readReview(file, cwd = process.cwd()) {
133
+ const target = path.resolve(cwd, file);
134
+ let raw;
135
+ try {
136
+ raw = await readFile(target, "utf8");
137
+ } catch {
138
+ throw new ReviewError(`Could not read the review record at ${file}. Create one with: eaa-kit checklist`);
139
+ }
140
+ let value;
141
+ try {
142
+ value = JSON.parse(raw);
143
+ } catch (cause) {
144
+ throw new ReviewError(`${path.basename(target)} is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`);
145
+ }
146
+ const result = safeParse(reviewSchema, value);
147
+ if (!result.success) {
148
+ const issues = result.error.issues.map((issue) => `${issue.path.join(".") || "document"}: ${issue.message}`).slice(0, 5);
149
+ throw new ReviewError(`${path.basename(target)} is not an eaa-kit review record (${issues.join("; ")})`);
150
+ }
151
+ if (result.data.schemaVersion !== 1) throw new ReviewError(`${path.basename(target)} has schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 1.`);
152
+ return result.data;
153
+ }
154
+ async function writeReview(file, record, cwd = process.cwd()) {
155
+ const target = path.resolve(cwd, file);
156
+ await mkdir(path.dirname(target), { recursive: true });
157
+ await writeFile(target, serialiseReview(record), "utf8");
158
+ return target;
159
+ }
160
+ //#endregion
161
+ export { blankReview as a, readReview as c, writeReview as d, answeredCount as i, reviewSentence as l, REVIEW_RESULTS as n, compareCriteria as o, ReviewError as r, criterionReview as s, DEFAULT_REVIEW_FILE as t, serialiseReview as u };
@@ -0,0 +1,2 @@
1
+ import { a as blankReview, c as readReview, d as writeReview, i as answeredCount, r as ReviewError } from "./review-CdMK2GpQ.js";
2
+ export { ReviewError, answeredCount, blankReview, readReview, writeReview };
@@ -35,7 +35,7 @@ async function auditBuild(directory, options, logger) {
35
35
  logger.info("skipped (enabled: false)");
36
36
  return;
37
37
  }
38
- const { runAuditCommand } = await import("./audit-DyHPmGeD.js");
38
+ const { runAuditCommand } = await import("./audit-DK2StT1_.js");
39
39
  const { exitCode } = await runAuditCommand(directory, auditOptions);
40
40
  if (exitCode === 0) {
41
41
  logger.info("no violations at or above the threshold");
@@ -29,6 +29,16 @@ interface IntegrationOptions {
29
29
  fast?: boolean;
30
30
  concurrency?: number;
31
31
  baseline?: string;
32
+ /**
33
+ * Path to a review record: what a person checked, for the criteria no engine
34
+ * can reach. It changes nothing about whether the build fails — a review is a
35
+ * claim beside the run's own findings, never a verdict on them — but it is
36
+ * what makes the coverage in a build log describe the whole standard rather
37
+ * than only the automatable fraction of it.
38
+ */
39
+ review?: string;
40
+ /** Days after which a recorded review stops counting. */
41
+ reviewMaxAge?: number;
32
42
  format?: OutputFormat;
33
43
  /** Write the report here instead of the build log. */
34
44
  output?: string;
@@ -1,8 +1,9 @@
1
- import { o as isImpactLevel } from "./impact-DZt2oBCP.js";
2
- import { o as standardsReference } from "./text-CKKpzkYM.js";
1
+ import { s as isImpactLevel } from "./impact-luGCnN8F.js";
2
+ import { s as standardsReference } from "./text-BF1LHMiV.js";
3
3
  import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
4
4
  import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
5
- import { c as ruleOutcomes, o as findingElements } from "./result-BWcYXeRs.js";
5
+ import { c as runEngine, f as uniqueRuleOutcomes, o as findingElements } from "./result-DfUru6y_.js";
6
+ import { n as buildCoverage } from "./coverage-BD2_CYns.js";
6
7
  import path from "node:path";
7
8
  //#region src/audit/report/sarif.ts
8
9
  const SARIF_VERSION = "2.1.0";
@@ -75,7 +76,7 @@ function buildSarifReport(audits, options) {
75
76
  toolExecutionNotifications: notifications
76
77
  }],
77
78
  results,
78
- properties: summaryProperties(audits, options.completeness)
79
+ properties: summaryProperties(audits, options.completeness, options.review)
79
80
  }]
80
81
  };
81
82
  }
@@ -115,12 +116,7 @@ function fingerprint(ruleId, selector, html) {
115
116
  }
116
117
  /** Every rule the run knows about, so the catalogue is complete in GitHub. */
117
118
  function buildRules(audits) {
118
- const rules = /* @__PURE__ */ new Map();
119
- for (const audit of audits) for (const outcome of ruleOutcomes(audit)) {
120
- if (rules.has(outcome.ruleId)) continue;
121
- rules.set(outcome.ruleId, toSarifRule(outcome));
122
- }
123
- return [...rules.values()].sort((a, b) => a.id.localeCompare(b.id));
119
+ return uniqueRuleOutcomes(audits).map(toSarifRule);
124
120
  }
125
121
  function toSarifRule(outcome) {
126
122
  const references = standardsReference(outcome.successCriteria, outcome.enClauses);
@@ -140,7 +136,7 @@ function toSarifRule(outcome) {
140
136
  * Coverage that has no place in `results` but should not vanish: a SARIF log
141
137
  * with no results must not be mistaken for "everything was checked".
142
138
  */
143
- function summaryProperties(audits, completeness) {
139
+ function summaryProperties(audits, completeness, review) {
144
140
  let needsReview = 0;
145
141
  let notEvaluated = 0;
146
142
  const notEvaluatedRules = /* @__PURE__ */ new Set();
@@ -149,11 +145,12 @@ function summaryProperties(audits, completeness) {
149
145
  notEvaluatedRules.add(finding.ruleId);
150
146
  } else needsReview += 1;
151
147
  return {
152
- engine: audits[0]?.engine ?? "jsdom",
148
+ engine: runEngine(audits),
153
149
  pages: audits.length,
154
150
  needsReview,
155
151
  notEvaluated,
156
152
  notEvaluatedRules: [...notEvaluatedRules].sort(),
153
+ ...review ? reviewProperties(audits, review) : {},
157
154
  ...completeness ? {
158
155
  complete: completeness.complete,
159
156
  discovery: completeness.discovery,
@@ -165,6 +162,20 @@ function summaryProperties(audits, completeness) {
165
162
  };
166
163
  }
167
164
  /**
165
+ * The review as three counts, in the same shape the JSON report carries them.
166
+ *
167
+ * Counts only: this log's job is alerts, and the criterion-by-criterion detail
168
+ * belongs in the JSON report, which carries it in full.
169
+ */
170
+ function reviewProperties(audits, review) {
171
+ const coverage = buildCoverage(audits, void 0, review);
172
+ return {
173
+ reviewedCriteria: coverage.reviewed,
174
+ reviewedNotMet: coverage.reviewedNotMet,
175
+ reviewNotCounted: coverage.reviewNotCounted
176
+ };
177
+ }
178
+ /**
168
179
  * Artifact URIs are relative to the working directory and POSIX-separated, so
169
180
  * GitHub can line them up with files in the repository.
170
181
  */
@@ -18,9 +18,21 @@ function escapeText(value) {
18
18
  function escapeAttribute(value) {
19
19
  return escapeText(value).replace(/"/g, "&quot;").replace(/'/g, "&#39;");
20
20
  }
21
+ /**
22
+ * The plurals this package's own output actually uses, where an `s` is wrong.
23
+ *
24
+ * Deliberately a short list rather than a pluralisation library: this is the
25
+ * vocabulary of a handful of report lines, and every entry here was added
26
+ * because something printed "2 entrys" or "4 criterions" at somebody.
27
+ */
28
+ const IRREGULAR = {
29
+ entry: "entries",
30
+ criterion: "criteria"
31
+ };
21
32
  /** `plural(1, 'page')` is `page`, `plural(2, 'page')` is `pages`. */
22
33
  function plural(value, noun) {
23
- return value === 1 ? noun : `${noun}s`;
34
+ if (value === 1) return noun;
35
+ return IRREGULAR[noun] ?? `${noun}s`;
24
36
  }
25
37
  /** `count(2, 'page')` is `2 pages`. */
26
38
  function count(value, noun) {
@@ -39,5 +51,13 @@ function collapse(html, max) {
39
51
  function standardsReference(successCriteria, enClauses) {
40
52
  return [...successCriteria.map((criterion) => `WCAG ${criterion}`), ...enClauses.map((clause) => `EN 301 549 ${clause}`)].join(", ");
41
53
  }
54
+ /**
55
+ * A date as the ISO day this package records everywhere it writes one: the day
56
+ * a baseline entry was accepted, and the day a criterion was reviewed. Always
57
+ * UTC, so two people on either side of a date line write the same file.
58
+ */
59
+ function isoDate(date) {
60
+ return date.toISOString().slice(0, 10);
61
+ }
42
62
  //#endregion
43
- export { plural as a, escapeText as i, count as n, standardsReference as o, escapeAttribute as r, collapse as t };
63
+ export { isoDate as a, escapeText as i, count as n, plural as o, escapeAttribute as r, standardsReference as s, collapse as t };
@@ -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-YiqyQA2b.js";
2
2
  //#region src/vite/index.d.ts
3
3
  interface EaaKitPluginOptions extends IntegrationOptions {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { n as auditBuild, t as BuildAuditError } from "../run-C2nKFcb-.js";
1
+ import { n as auditBuild, t as BuildAuditError } from "../run-D7hdVXLA.js";
2
2
  import path from "node:path";
3
3
  //#region src/vite/index.ts
4
4
  function eaaKit(options = {}) {
@@ -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-YiqyQA2b.js";
2
2
  //#region src/webpack/index.d.ts
3
3
  /** The part of a webpack compilation this reads. */
4
4
  interface CompilationLike {
@@ -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-D7hdVXLA.js";
2
2
  import path from "node:path";
3
3
  //#region src/webpack/index.ts
4
4
  var EaaKitWebpackPlugin = class {
@@ -0,0 +1,63 @@
1
+ import { n as DEFAULT_TAGS } from "./result-DfUru6y_.js";
2
+ import { a as rulesByCriterion, o as manualCheckFor, s as understandingUrl, t as WCAG22_AA_CRITERIA } from "./coverage-BD2_CYns.js";
3
+ //#region src/audit/worksheet.ts
4
+ function buildWorksheet(options) {
5
+ const rules = rulesByCriterion(options.tags ?? DEFAULT_TAGS);
6
+ const unautomatable = WCAG22_AA_CRITERIA.filter((criterion) => (rules.get(criterion.number) ?? []).length === 0);
7
+ const lines = [
8
+ "# Manual accessibility review",
9
+ "",
10
+ `Of the ${WCAG22_AA_CRITERIA.length} WCAG 2.2 success criteria at Levels A and AA,`,
11
+ `**${unautomatable.length} have no automated rule at all**: no engine, this one included, can`,
12
+ "reach a verdict on them. They are the part of conformance a person has to do, and this is",
13
+ "the list of them.",
14
+ "",
15
+ `Record what you find in \`${options.recordPath}\`, which is the file \`eaa-kit audit --review\``,
16
+ "reads. Ticking a box below records nothing: this document is generated from the record and",
17
+ "never read back into it.",
18
+ "",
19
+ "For each criterion, set `result` to one of:",
20
+ "",
21
+ "- `met` — you checked it and the site meets it.",
22
+ "- `not-met` — you checked it and it does not. Say what is wrong in `note`.",
23
+ "- `not-applicable` — there is nothing on this site the criterion applies to. Not the same",
24
+ " as `met`, and never counted as though it were.",
25
+ "- `unreviewed` — nobody has looked yet. What every entry starts as.",
26
+ "",
27
+ "Set `reviewedOn` to the day you checked it. An entry with no date cannot be shown to still",
28
+ "hold, and an audit run with `--review-max-age` will not count it.",
29
+ ""
30
+ ];
31
+ lines.push("## Criteria no automated rule can reach", "", "These are the review. Nothing in any eaa-kit report says anything about them.", "");
32
+ for (const criterion of unautomatable) lines.push(...criterionEntry(criterion, [], options.review));
33
+ lines.push("## Criteria an audit can reach", "", "An audit reaches these when the rules below actually match something on the site. A run", "that found nothing to check has not shown the criterion is met, so they are worth a look", "too — and where this engine could not decide a rule, the check to do by hand is named.", "");
34
+ for (const criterion of WCAG22_AA_CRITERIA) {
35
+ const forCriterion = rules.get(criterion.number) ?? [];
36
+ if (forCriterion.length === 0) continue;
37
+ lines.push(...criterionEntry(criterion, forCriterion, options.review));
38
+ }
39
+ lines.push("---", "", "A completed review is not a compliance statement, and this tool cannot check that anything", "recorded here is true. It records what somebody says they checked, which is the same", "standing as the claims in the statement it generates.", "");
40
+ return lines.join("\n");
41
+ }
42
+ function criterionEntry(criterion, rules, review) {
43
+ const entry = review?.criteria[criterion.number];
44
+ const lines = [`- [${entry !== void 0 && entry.result !== "unreviewed" ? "x" : " "}] **${criterion.number} ${criterion.title}** (Level ${criterion.level})`];
45
+ const url = understandingUrl(criterion.number);
46
+ if (url !== void 0) lines.push(` What it requires: ${url}`);
47
+ if (rules.length > 0) {
48
+ lines.push(` Rules that touch it: ${rules.join(", ")}`);
49
+ for (const rule of rules) {
50
+ const manual = manualCheckFor(rule);
51
+ if (manual !== void 0) lines.push(` ${rule}: ${manual.check}`);
52
+ }
53
+ }
54
+ if (entry !== void 0 && entry.result !== "unreviewed") {
55
+ const on = entry.reviewedOn === void 0 ? "no date recorded" : `on ${entry.reviewedOn}`;
56
+ lines.push(` Recorded: ${entry.result} (${on})`);
57
+ if (entry.note !== void 0) lines.push(` Note: ${entry.note}`);
58
+ }
59
+ lines.push("");
60
+ return lines;
61
+ }
62
+ //#endregion
63
+ export { buildWorksheet };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eaa-kit",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "WCAG 2.2 AA auditor and EU accessibility statement generator for seven countries (AT, DE, CH, ES, FR, IT, NL). Audits a static build or a running site from the command line.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -79,7 +79,7 @@
79
79
  "typecheck": "tsc --noEmit",
80
80
  "lint": "biome check .",
81
81
  "format": "biome check --write .",
82
- "smoke": "pnpm build && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json && node dist/cli/index.js audit tests/fixtures/site --include \"about/**\" \"blog/**\" \"drafts/**\" \"legacy.htm\" --concurrency 2 && node dist/cli/index.js audit tests/fixtures/site --include about/** --format html && node dist/cli/index.js audit tests/fixtures/site --baseline examples/baseline.json && node -e \"import('./dist/astro/index.js').then(m => { const i = m.default(); if (i.name !== 'eaa-kit' || typeof i.hooks['astro:build:done'] !== 'function') { throw new Error('astro entry point is not an integration') } console.log('astro entry ok') })\" && node dist/cli/index.js statement --config examples/eaa.config.json && node dist/cli/index.js statement --config examples/eaa.config.json --audit examples/report.json --format html && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json --output .eaa-kit/smoke-base.json && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json --output .eaa-kit/smoke-head.json && node dist/cli/index.js diff .eaa-kit/smoke-base.json .eaa-kit/smoke-head.json --format json",
82
+ "smoke": "pnpm build && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json && node dist/cli/index.js audit tests/fixtures/site --include \"about/**\" \"blog/**\" \"drafts/**\" \"legacy.htm\" --concurrency 2 && node dist/cli/index.js audit tests/fixtures/site --include about/** --format html && node dist/cli/index.js audit tests/fixtures/site --baseline examples/baseline.json && node -e \"import('./dist/astro/index.js').then(m => { const i = m.default(); if (i.name !== 'eaa-kit' || typeof i.hooks['astro:build:done'] !== 'function') { throw new Error('astro entry point is not an integration') } console.log('astro entry ok') })\" && node dist/cli/index.js baseline tests/fixtures/site --output .eaa-kit/smoke-baseline.json && node dist/cli/index.js baseline tests/fixtures/site --output .eaa-kit/smoke-baseline.json --prune && node dist/cli/index.js checklist --record .eaa-kit/smoke-review.json --output .eaa-kit/smoke-review.md && node dist/cli/index.js audit tests/fixtures/site --include about/** --review .eaa-kit/smoke-review.json --coverage && node dist/cli/index.js statement --config examples/eaa.config.json && node dist/cli/index.js statement --config examples/eaa.config.json --audit examples/report.json --format html && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json --output .eaa-kit/smoke-base.json && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json --output .eaa-kit/smoke-head.json && node dist/cli/index.js diff .eaa-kit/smoke-base.json .eaa-kit/smoke-head.json --format json",
83
83
  "examples": "pnpm build && node scripts/generate-examples.mjs",
84
84
  "test:packaged": "pnpm build && node scripts/test-packaged.mjs"
85
85
  },
@@ -1,2 +0,0 @@
1
- import { n as runAuditCommand } from "./audit-B282A3EA.js";
2
- export { runAuditCommand };
@@ -1,2 +0,0 @@
1
- import { a as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-CuAS2lMk.js";
2
- export { BaselineError, applyBaseline, readBaseline };
@@ -1,3 +0,0 @@
1
- import "./result-BWcYXeRs.js";
2
- import { n as runJsdomAudit } from "./jsdom-B--cEH-G.js";
3
- export { runJsdomAudit };
@@ -1,2 +0,0 @@
1
- import { r as loadAuditConfig } from "./load-yAR4wzez.js";
2
- export { loadAuditConfig };