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.
- package/README.md +35 -2
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/audit/runners/worker.js +1 -1
- package/dist/audit-DK2StT1_.js +2 -0
- package/dist/{audit-B282A3EA.js → audit-e_uixhPG.js} +95 -48
- package/dist/baseline-BscHfFSW.js +2 -0
- package/dist/{baseline-CuAS2lMk.js → baseline-D2qWmUC6.js} +36 -9
- package/dist/checklist-BtgkxMEa.js +44 -0
- package/dist/cli/index.js +111 -14
- package/dist/{command-C3D7JWn6.js → command-CZKp9Wi_.js} +101 -10
- package/dist/{remediation-CMBIrnpN.js → completeness-Dwq3CPB3.js} +60 -42
- package/dist/{component-C3GL1Mnu.js → component-ChisoFyY.js} +1 -1
- package/dist/{component-DKd3EHOg.js → component-DcCn3BJS.js} +1 -1
- package/dist/coverage-7bQqWNfN.js +2 -0
- package/dist/{coverage-B_Y6l-Ra.js → coverage-BD2_CYns.js} +44 -7
- package/dist/{crawl-BUWPgaGW.js → crawl-BHkJmUkA.js} +42 -8
- package/dist/eleventy/index.d.ts +1 -1
- package/dist/eleventy/index.js +1 -1
- package/dist/{html-C4vktg0A.js → html-Bmh7X_sM.js} +52 -30
- package/dist/{impact-DZt2oBCP.js → impact-luGCnN8F.js} +11 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +3 -3
- package/dist/{init-CW7LfGT5.js → init-BjKYttxc.js} +3 -3
- package/dist/{jsdom-B--cEH-G.js → jsdom-CQar6OQS.js} +1 -1
- package/dist/jsdom-DMvU2h-f.js +3 -0
- package/dist/{json-D_Mnnft5.js → json-HJDgJp09.js} +1 -1
- package/dist/{json-DROX33kh.js → json-PNg6BeXp.js} +11 -16
- package/dist/load-Bc_xDGAh.js +2 -0
- package/dist/{load-yAR4wzez.js → load-vZdV_fD1.js} +5 -1
- package/dist/nuxt/index.d.ts +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/{playwright-BojtYVUa.js → playwright-C-miJP2b.js} +3 -2
- package/dist/{pool-BO25OIez.js → pool-dkexq5xo.js} +2 -2
- package/dist/{render-DbGOVmhx.js → render-CtsqR0k-.js} +105 -4
- package/dist/{result-BWcYXeRs.js → result-DfUru6y_.js} +24 -1
- package/dist/review-CdMK2GpQ.js +161 -0
- package/dist/review-DIbOTmOM.js +2 -0
- package/dist/{run-C2nKFcb-.js → run-D7hdVXLA.js} +1 -1
- package/dist/{run-CtcEUhbe.d.ts → run-YiqyQA2b.d.ts} +10 -0
- package/dist/{sarif-B-UBcVu8.js → sarif-BKvq_AUm.js} +23 -12
- package/dist/{text-CKKpzkYM.js → text-BF1LHMiV.js} +22 -2
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/webpack/index.d.ts +1 -1
- package/dist/webpack/index.js +1 -1
- package/dist/worksheet-eUjwYLxC.js +63 -0
- package/package.json +2 -2
- package/dist/audit-DyHPmGeD.js +0 -2
- package/dist/baseline-0u4df12v.js +0 -2
- package/dist/jsdom-22Bkt65v.js +0 -3
- package/dist/load-CFq2VQtT.js +0 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
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,
|
|
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,
|
|
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,
|
|
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 };
|
|
@@ -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-
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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, """).replace(/'/g, "'");
|
|
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
|
-
|
|
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 {
|
|
63
|
+
export { isoDate as a, escapeText as i, count as n, plural as o, escapeAttribute as r, standardsReference as s, collapse as t };
|
package/dist/vite/index.d.ts
CHANGED
package/dist/vite/index.js
CHANGED
package/dist/webpack/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as IntegrationOptions, t as BuildAuditError } from "../run-
|
|
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 {
|
package/dist/webpack/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-
|
|
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.
|
|
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
|
},
|
package/dist/audit-DyHPmGeD.js
DELETED
package/dist/jsdom-22Bkt65v.js
DELETED
package/dist/load-CFq2VQtT.js
DELETED