ultra11y 5.12.0 → 5.13.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +19 -3
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +19 -3
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +19 -3
- package/src/audit.ts +8 -1
- package/src/axe-map.ts +21 -0
- package/src/pages.ts +15 -2
- package/src/types.ts +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "ultra11y",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
|
|
13
|
-
"version": "5.
|
|
13
|
+
"version": "5.13.1",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
16
16
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.13.1",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.13.1",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
// Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
|
|
28
28
|
// does not carry it, so a user's own ultra11y.js is never clobbered.
|
|
29
29
|
const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
|
|
30
|
-
const ULTRA11Y_PLUGIN_VERSION = "5.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.13.1";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.1",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.
|
|
9
|
+
var VERSION = "5.13.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -39283,6 +39283,12 @@ var AXE_WCAG = {
|
|
|
39283
39283
|
bypass: "2.4.1",
|
|
39284
39284
|
accesskeys: "2.1.1"
|
|
39285
39285
|
};
|
|
39286
|
+
var AXE_DECIDES = {
|
|
39287
|
+
// Text contrast. `color-contrast` is on by default, runs on every text node, and computes
|
|
39288
|
+
// the ratio the criterion states — the one criterion where the automated answer IS the
|
|
39289
|
+
// answer, which is why the local scan tier has always mapped it here.
|
|
39290
|
+
"1.4.3": ["color-contrast"]
|
|
39291
|
+
};
|
|
39286
39292
|
var FALLBACK_SC = "4.1.2";
|
|
39287
39293
|
function scFromWcagTags(tags) {
|
|
39288
39294
|
if (!tags) return null;
|
|
@@ -48561,6 +48567,7 @@ function foldDoc(acc, doc, graph) {
|
|
|
48561
48567
|
const axe = doc.signals?.axe;
|
|
48562
48568
|
if (axe?.ran) {
|
|
48563
48569
|
acc.axeRan.add(pageId);
|
|
48570
|
+
for (const sc of Object.keys(AXE_DECIDES)) acc.renderedScs.add(sc);
|
|
48564
48571
|
for (const f of axeFindings(axe.violations ?? [], doc.file, pageId)) {
|
|
48565
48572
|
const list = acc.byCriterion.get(f.criteriaId) ?? [];
|
|
48566
48573
|
list.push(f);
|
|
@@ -48589,6 +48596,7 @@ function renderedProves(sc, acc) {
|
|
|
48589
48596
|
if (acc.pageIds.size === 0) return false;
|
|
48590
48597
|
const probed = acc.probedScs.get(sc);
|
|
48591
48598
|
if (probed && probed.size === acc.pageIds.size) return true;
|
|
48599
|
+
if (AXE_DECIDES[sc] && acc.axeRan.size === acc.pageIds.size) return true;
|
|
48592
48600
|
const rules = renderedRulesFor(sc);
|
|
48593
48601
|
if (!rules.length) return false;
|
|
48594
48602
|
return rules.every((ruleId) => {
|
|
@@ -48605,6 +48613,9 @@ function partialProbeReason(sc, acc) {
|
|
|
48605
48613
|
return `Probed on ${probed.size} of the ${acc.pageIds.size} pages in scope, so this criterion stays open: conformity here is measured on EVERY page or on none, and the page nobody probed is exactly where the failure would be. Not probed: ${shown.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}.`;
|
|
48606
48614
|
}
|
|
48607
48615
|
function renderedProvesReason(sc, acc) {
|
|
48616
|
+
if (AXE_DECIDES[sc] && acc.axeRan.size === acc.pageIds.size) {
|
|
48617
|
+
return `Measured by axe-core on all ${acc.pageIds.size} page(s) in scope \u2014 ${AXE_DECIDES[sc]?.join(", ")} ran in the browser and reported nothing. A page axe had not run on would have kept this criterion open.`;
|
|
48618
|
+
}
|
|
48608
48619
|
const probed = acc.probedScs.get(sc);
|
|
48609
48620
|
if (probed && probed.size === acc.pageIds.size) {
|
|
48610
48621
|
return `Measured in a real browser on all ${acc.pageIds.size} page(s) in scope \u2014 the probe acted on the page (zoom, 320px viewport, text-spacing override, Tab, hover) and observed nothing. A page the probe had not run on would have kept this criterion open.`;
|
|
@@ -54362,6 +54373,7 @@ function pageStatus(c2, pageFindings, basis) {
|
|
|
54362
54373
|
if (pageFindings.some((f) => !f.advisory)) return "NC";
|
|
54363
54374
|
if (c2.status === "manual") return "manual";
|
|
54364
54375
|
if (c2.status === "NA") return "NA";
|
|
54376
|
+
if (c2.inapplicable) return c2.status;
|
|
54365
54377
|
if (c2.decidedBy === "agent" || c2.decidedBy === "scan") return c2.status;
|
|
54366
54378
|
if (automatability(c2.id) !== "static") return "manual";
|
|
54367
54379
|
return basis === "snapshot" ? "C" : "manual";
|
|
@@ -54379,13 +54391,17 @@ function derivePages(result, pages) {
|
|
|
54379
54391
|
const own = result.findings.filter((f) => f.page === p.id);
|
|
54380
54392
|
const criteria = result.criteria.map((c2) => {
|
|
54381
54393
|
const pf = own.filter((f) => f.criteriaId === c2.id);
|
|
54394
|
+
const status = pageStatus(c2, pf, p.basis);
|
|
54382
54395
|
return {
|
|
54383
54396
|
id: c2.id,
|
|
54384
54397
|
guideline: c2.guideline,
|
|
54385
|
-
status
|
|
54398
|
+
status,
|
|
54386
54399
|
findings: pf,
|
|
54387
54400
|
...c2.justification ? { justification: c2.justification } : {},
|
|
54388
|
-
...c2.decidedBy ? { decidedBy: c2.decidedBy } : {}
|
|
54401
|
+
...c2.decidedBy ? { decidedBy: c2.decidedBy } : {},
|
|
54402
|
+
// Carried, not recomputed: a finding on THIS page proves the subject exists after all,
|
|
54403
|
+
// and `pageStatus` has already turned that into an NC above.
|
|
54404
|
+
...c2.inapplicable && status === c2.status ? { inapplicable: true } : {}
|
|
54389
54405
|
};
|
|
54390
54406
|
});
|
|
54391
54407
|
const { rate, decided, total } = pct(criteria);
|
|
@@ -5,7 +5,7 @@ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should
|
|
|
5
5
|
allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
|
|
6
6
|
license: MIT
|
|
7
7
|
metadata:
|
|
8
|
-
version: 5.
|
|
8
|
+
version: 5.13.1
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.
|
|
9
|
+
var VERSION = "5.13.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -39283,6 +39283,12 @@ var AXE_WCAG = {
|
|
|
39283
39283
|
bypass: "2.4.1",
|
|
39284
39284
|
accesskeys: "2.1.1"
|
|
39285
39285
|
};
|
|
39286
|
+
var AXE_DECIDES = {
|
|
39287
|
+
// Text contrast. `color-contrast` is on by default, runs on every text node, and computes
|
|
39288
|
+
// the ratio the criterion states — the one criterion where the automated answer IS the
|
|
39289
|
+
// answer, which is why the local scan tier has always mapped it here.
|
|
39290
|
+
"1.4.3": ["color-contrast"]
|
|
39291
|
+
};
|
|
39286
39292
|
var FALLBACK_SC = "4.1.2";
|
|
39287
39293
|
function scFromWcagTags(tags) {
|
|
39288
39294
|
if (!tags) return null;
|
|
@@ -48561,6 +48567,7 @@ function foldDoc(acc, doc, graph) {
|
|
|
48561
48567
|
const axe = doc.signals?.axe;
|
|
48562
48568
|
if (axe?.ran) {
|
|
48563
48569
|
acc.axeRan.add(pageId);
|
|
48570
|
+
for (const sc of Object.keys(AXE_DECIDES)) acc.renderedScs.add(sc);
|
|
48564
48571
|
for (const f of axeFindings(axe.violations ?? [], doc.file, pageId)) {
|
|
48565
48572
|
const list = acc.byCriterion.get(f.criteriaId) ?? [];
|
|
48566
48573
|
list.push(f);
|
|
@@ -48589,6 +48596,7 @@ function renderedProves(sc, acc) {
|
|
|
48589
48596
|
if (acc.pageIds.size === 0) return false;
|
|
48590
48597
|
const probed = acc.probedScs.get(sc);
|
|
48591
48598
|
if (probed && probed.size === acc.pageIds.size) return true;
|
|
48599
|
+
if (AXE_DECIDES[sc] && acc.axeRan.size === acc.pageIds.size) return true;
|
|
48592
48600
|
const rules = renderedRulesFor(sc);
|
|
48593
48601
|
if (!rules.length) return false;
|
|
48594
48602
|
return rules.every((ruleId) => {
|
|
@@ -48605,6 +48613,9 @@ function partialProbeReason(sc, acc) {
|
|
|
48605
48613
|
return `Probed on ${probed.size} of the ${acc.pageIds.size} pages in scope, so this criterion stays open: conformity here is measured on EVERY page or on none, and the page nobody probed is exactly where the failure would be. Not probed: ${shown.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}.`;
|
|
48606
48614
|
}
|
|
48607
48615
|
function renderedProvesReason(sc, acc) {
|
|
48616
|
+
if (AXE_DECIDES[sc] && acc.axeRan.size === acc.pageIds.size) {
|
|
48617
|
+
return `Measured by axe-core on all ${acc.pageIds.size} page(s) in scope \u2014 ${AXE_DECIDES[sc]?.join(", ")} ran in the browser and reported nothing. A page axe had not run on would have kept this criterion open.`;
|
|
48618
|
+
}
|
|
48608
48619
|
const probed = acc.probedScs.get(sc);
|
|
48609
48620
|
if (probed && probed.size === acc.pageIds.size) {
|
|
48610
48621
|
return `Measured in a real browser on all ${acc.pageIds.size} page(s) in scope \u2014 the probe acted on the page (zoom, 320px viewport, text-spacing override, Tab, hover) and observed nothing. A page the probe had not run on would have kept this criterion open.`;
|
|
@@ -54362,6 +54373,7 @@ function pageStatus(c2, pageFindings, basis) {
|
|
|
54362
54373
|
if (pageFindings.some((f) => !f.advisory)) return "NC";
|
|
54363
54374
|
if (c2.status === "manual") return "manual";
|
|
54364
54375
|
if (c2.status === "NA") return "NA";
|
|
54376
|
+
if (c2.inapplicable) return c2.status;
|
|
54365
54377
|
if (c2.decidedBy === "agent" || c2.decidedBy === "scan") return c2.status;
|
|
54366
54378
|
if (automatability(c2.id) !== "static") return "manual";
|
|
54367
54379
|
return basis === "snapshot" ? "C" : "manual";
|
|
@@ -54379,13 +54391,17 @@ function derivePages(result, pages) {
|
|
|
54379
54391
|
const own = result.findings.filter((f) => f.page === p.id);
|
|
54380
54392
|
const criteria = result.criteria.map((c2) => {
|
|
54381
54393
|
const pf = own.filter((f) => f.criteriaId === c2.id);
|
|
54394
|
+
const status = pageStatus(c2, pf, p.basis);
|
|
54382
54395
|
return {
|
|
54383
54396
|
id: c2.id,
|
|
54384
54397
|
guideline: c2.guideline,
|
|
54385
|
-
status
|
|
54398
|
+
status,
|
|
54386
54399
|
findings: pf,
|
|
54387
54400
|
...c2.justification ? { justification: c2.justification } : {},
|
|
54388
|
-
...c2.decidedBy ? { decidedBy: c2.decidedBy } : {}
|
|
54401
|
+
...c2.decidedBy ? { decidedBy: c2.decidedBy } : {},
|
|
54402
|
+
// Carried, not recomputed: a finding on THIS page proves the subject exists after all,
|
|
54403
|
+
// and `pageStatus` has already turned that into an NC above.
|
|
54404
|
+
...c2.inapplicable && status === c2.status ? { inapplicable: true } : {}
|
|
54389
54405
|
};
|
|
54390
54406
|
});
|
|
54391
54407
|
const { rate, decided, total } = pct(criteria);
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibili
|
|
|
4
4
|
when_to_use: "Invoke when the user asks for an accessibility AUDIT or a formal deliverable: audit a repo, site or page against WCAG 2.2 AA or a country standard (RGAA, Section 508, EN 301 549); produce a dated conformance report, a criterion grid, a PRD backlog, or tickets (GitHub/GitLab/Jira); author accessible markup from scratch; wire the repo gate (init --hook/--ci) or a rendered-DOM capture pipeline. For a review of the code UNDER CHANGE (diff, branch, PR), use the `review-a11y` skill instead."
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: 5.
|
|
7
|
+
version: 5.13.1
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.
|
|
9
|
+
var VERSION = "5.13.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -39283,6 +39283,12 @@ var AXE_WCAG = {
|
|
|
39283
39283
|
bypass: "2.4.1",
|
|
39284
39284
|
accesskeys: "2.1.1"
|
|
39285
39285
|
};
|
|
39286
|
+
var AXE_DECIDES = {
|
|
39287
|
+
// Text contrast. `color-contrast` is on by default, runs on every text node, and computes
|
|
39288
|
+
// the ratio the criterion states — the one criterion where the automated answer IS the
|
|
39289
|
+
// answer, which is why the local scan tier has always mapped it here.
|
|
39290
|
+
"1.4.3": ["color-contrast"]
|
|
39291
|
+
};
|
|
39286
39292
|
var FALLBACK_SC = "4.1.2";
|
|
39287
39293
|
function scFromWcagTags(tags) {
|
|
39288
39294
|
if (!tags) return null;
|
|
@@ -48561,6 +48567,7 @@ function foldDoc(acc, doc, graph) {
|
|
|
48561
48567
|
const axe = doc.signals?.axe;
|
|
48562
48568
|
if (axe?.ran) {
|
|
48563
48569
|
acc.axeRan.add(pageId);
|
|
48570
|
+
for (const sc of Object.keys(AXE_DECIDES)) acc.renderedScs.add(sc);
|
|
48564
48571
|
for (const f of axeFindings(axe.violations ?? [], doc.file, pageId)) {
|
|
48565
48572
|
const list = acc.byCriterion.get(f.criteriaId) ?? [];
|
|
48566
48573
|
list.push(f);
|
|
@@ -48589,6 +48596,7 @@ function renderedProves(sc, acc) {
|
|
|
48589
48596
|
if (acc.pageIds.size === 0) return false;
|
|
48590
48597
|
const probed = acc.probedScs.get(sc);
|
|
48591
48598
|
if (probed && probed.size === acc.pageIds.size) return true;
|
|
48599
|
+
if (AXE_DECIDES[sc] && acc.axeRan.size === acc.pageIds.size) return true;
|
|
48592
48600
|
const rules = renderedRulesFor(sc);
|
|
48593
48601
|
if (!rules.length) return false;
|
|
48594
48602
|
return rules.every((ruleId) => {
|
|
@@ -48605,6 +48613,9 @@ function partialProbeReason(sc, acc) {
|
|
|
48605
48613
|
return `Probed on ${probed.size} of the ${acc.pageIds.size} pages in scope, so this criterion stays open: conformity here is measured on EVERY page or on none, and the page nobody probed is exactly where the failure would be. Not probed: ${shown.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}.`;
|
|
48606
48614
|
}
|
|
48607
48615
|
function renderedProvesReason(sc, acc) {
|
|
48616
|
+
if (AXE_DECIDES[sc] && acc.axeRan.size === acc.pageIds.size) {
|
|
48617
|
+
return `Measured by axe-core on all ${acc.pageIds.size} page(s) in scope \u2014 ${AXE_DECIDES[sc]?.join(", ")} ran in the browser and reported nothing. A page axe had not run on would have kept this criterion open.`;
|
|
48618
|
+
}
|
|
48608
48619
|
const probed = acc.probedScs.get(sc);
|
|
48609
48620
|
if (probed && probed.size === acc.pageIds.size) {
|
|
48610
48621
|
return `Measured in a real browser on all ${acc.pageIds.size} page(s) in scope \u2014 the probe acted on the page (zoom, 320px viewport, text-spacing override, Tab, hover) and observed nothing. A page the probe had not run on would have kept this criterion open.`;
|
|
@@ -54362,6 +54373,7 @@ function pageStatus(c2, pageFindings, basis) {
|
|
|
54362
54373
|
if (pageFindings.some((f) => !f.advisory)) return "NC";
|
|
54363
54374
|
if (c2.status === "manual") return "manual";
|
|
54364
54375
|
if (c2.status === "NA") return "NA";
|
|
54376
|
+
if (c2.inapplicable) return c2.status;
|
|
54365
54377
|
if (c2.decidedBy === "agent" || c2.decidedBy === "scan") return c2.status;
|
|
54366
54378
|
if (automatability(c2.id) !== "static") return "manual";
|
|
54367
54379
|
return basis === "snapshot" ? "C" : "manual";
|
|
@@ -54379,13 +54391,17 @@ function derivePages(result, pages) {
|
|
|
54379
54391
|
const own = result.findings.filter((f) => f.page === p.id);
|
|
54380
54392
|
const criteria = result.criteria.map((c2) => {
|
|
54381
54393
|
const pf = own.filter((f) => f.criteriaId === c2.id);
|
|
54394
|
+
const status = pageStatus(c2, pf, p.basis);
|
|
54382
54395
|
return {
|
|
54383
54396
|
id: c2.id,
|
|
54384
54397
|
guideline: c2.guideline,
|
|
54385
|
-
status
|
|
54398
|
+
status,
|
|
54386
54399
|
findings: pf,
|
|
54387
54400
|
...c2.justification ? { justification: c2.justification } : {},
|
|
54388
|
-
...c2.decidedBy ? { decidedBy: c2.decidedBy } : {}
|
|
54401
|
+
...c2.decidedBy ? { decidedBy: c2.decidedBy } : {},
|
|
54402
|
+
// Carried, not recomputed: a finding on THIS page proves the subject exists after all,
|
|
54403
|
+
// and `pageStatus` has already turned that into an NC above.
|
|
54404
|
+
...c2.inapplicable && status === c2.status ? { inapplicable: true } : {}
|
|
54389
54405
|
};
|
|
54390
54406
|
});
|
|
54391
54407
|
const { rate, decided, total } = pct(criteria);
|
package/src/audit.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { attr, elementsByTag, type Doc, type CaptureProvenance } from "./parse/h
|
|
|
13
13
|
import { CAPTURES_DIR, computeCaptureCoverage, enrichCaptureOrigins, isUnderDir, readCaptureDir, capturesForSources } from "./capture.js";
|
|
14
14
|
import { isFullDocument } from "./rules/rule.js";
|
|
15
15
|
import { renderedRulesFor, renderedRulesRan, renderedTestedScs } from "./rules/rendered.js";
|
|
16
|
-
import { PROBE_SEVERITY, PROBE_WCAG, isAxeAdvisory, scForAxe, severityFromImpact } from "./axe-map.js";
|
|
16
|
+
import { AXE_DECIDES, PROBE_SEVERITY, PROBE_WCAG, isAxeAdvisory, scForAxe, severityFromImpact } from "./axe-map.js";
|
|
17
17
|
import { subjectsAbsent, subjectsForSc, subjectsPresentIn } from "./adjudicate-subjects.js";
|
|
18
18
|
import { runRules } from "./rules/registry.js";
|
|
19
19
|
import { runCrossRules } from "./rules/cross-registry.js";
|
|
@@ -526,6 +526,7 @@ export function foldDoc(acc: Accum, doc: Doc, graph?: DepGraph): void {
|
|
|
526
526
|
const axe = doc.signals?.axe;
|
|
527
527
|
if (axe?.ran) {
|
|
528
528
|
acc.axeRan.add(pageId);
|
|
529
|
+
for (const sc of Object.keys(AXE_DECIDES)) acc.renderedScs.add(sc);
|
|
529
530
|
for (const f of axeFindings(axe.violations ?? [], doc.file, pageId)) {
|
|
530
531
|
const list = acc.byCriterion.get(f.criteriaId) ?? [];
|
|
531
532
|
list.push(f);
|
|
@@ -585,6 +586,9 @@ function renderedProves(sc: string, acc: Accum): boolean {
|
|
|
585
586
|
// in scope, or nothing.
|
|
586
587
|
const probed = acc.probedScs.get(sc);
|
|
587
588
|
if (probed && probed.size === acc.pageIds.size) return true;
|
|
589
|
+
// AXE, on every page in scope, for the handful of criteria it is the canonical decider of
|
|
590
|
+
// (AXE_DECIDES). Same rule as the probes and the digest tier: every page, or nothing.
|
|
591
|
+
if (AXE_DECIDES[sc] && acc.axeRan.size === acc.pageIds.size) return true;
|
|
588
592
|
const rules = renderedRulesFor(sc);
|
|
589
593
|
if (!rules.length) return false;
|
|
590
594
|
return rules.every((ruleId) => {
|
|
@@ -610,6 +614,9 @@ function partialProbeReason(sc: string, acc: Accum): string | undefined {
|
|
|
610
614
|
}
|
|
611
615
|
|
|
612
616
|
function renderedProvesReason(sc: string, acc: Accum): string {
|
|
617
|
+
if (AXE_DECIDES[sc] && acc.axeRan.size === acc.pageIds.size) {
|
|
618
|
+
return `Measured by axe-core on all ${acc.pageIds.size} page(s) in scope — ${AXE_DECIDES[sc]?.join(", ")} ran in the browser and reported nothing. A page axe had not run on would have kept this criterion open.`;
|
|
619
|
+
}
|
|
613
620
|
const probed = acc.probedScs.get(sc);
|
|
614
621
|
if (probed && probed.size === acc.pageIds.size) {
|
|
615
622
|
return `Measured in a real browser on all ${acc.pageIds.size} page(s) in scope — the probe acted on the page (zoom, 320px viewport, text-spacing override, Tab, hover) and observed nothing. A page the probe had not run on would have kept this criterion open.`;
|
package/src/axe-map.ts
CHANGED
|
@@ -128,6 +128,27 @@ export const AXE_WCAG: Record<string, string> = {
|
|
|
128
128
|
accesskeys: "2.1.1",
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
+
/** THE SUCCESS CRITERIA AN AXE PASS THAT FOUND NOTHING IS ALLOWED TO CLOSE.
|
|
132
|
+
*
|
|
133
|
+
* Deliberately tiny, and deliberately a list rather than a rule. axe REPORTS on dozens of
|
|
134
|
+
* success criteria (that is what AXE_WCAG above is for), but reporting on one is not the same
|
|
135
|
+
* as being its canonical decider: `color-contrast` computes the very ratio 1.4.3 is written
|
|
136
|
+
* in, while axe's ARIA rules cover a slice of 4.1.2 that no clean run can close. Reading the
|
|
137
|
+
* second as the first is how a grid fills up with conformities nobody verified.
|
|
138
|
+
*
|
|
139
|
+
* The guardrail around it is in src/audit.ts and is the same one the probes get: the pass must
|
|
140
|
+
* have run on EVERY page in scope. A criterion measured on some pages is not a criterion
|
|
141
|
+
* measured.
|
|
142
|
+
*
|
|
143
|
+
* Entries are added one at a time, each with the rule that carries it and a reason to trust
|
|
144
|
+
* it — never in bulk from axe's tag list. */
|
|
145
|
+
export const AXE_DECIDES: Record<string, readonly string[]> = {
|
|
146
|
+
// Text contrast. `color-contrast` is on by default, runs on every text node, and computes
|
|
147
|
+
// the ratio the criterion states — the one criterion where the automated answer IS the
|
|
148
|
+
// answer, which is why the local scan tier has always mapped it here.
|
|
149
|
+
"1.4.3": ["color-contrast"],
|
|
150
|
+
};
|
|
151
|
+
|
|
131
152
|
export const FALLBACK_SC = "4.1.2";
|
|
132
153
|
|
|
133
154
|
/** Read a WCAG SC from axe's native rule tags. axe tags each rule with `wcag<abc>`
|
package/src/pages.ts
CHANGED
|
@@ -165,7 +165,16 @@ function pageStatus(c: CriterionResult, pageFindings: Finding[], basis: PageScop
|
|
|
165
165
|
// A non-normative recommendation can never flip a criterion to NC — same rule as core.
|
|
166
166
|
if (pageFindings.some((f) => !f.advisory)) return "NC";
|
|
167
167
|
if (c.status === "manual") return "manual"; // the engine cannot decide it anywhere
|
|
168
|
-
if (c.status === "NA") return "NA"; //
|
|
168
|
+
if (c.status === "NA") return "NA"; // legacy audits only — the engine no longer emits it
|
|
169
|
+
// NOTHING OF THAT KIND EXISTS ANYWHERE IN SCOPE, so there is none on this page either.
|
|
170
|
+
//
|
|
171
|
+
// Checked BEFORE rule 3 below, and it is not an exception to it: rule 3 refuses to conclude
|
|
172
|
+
// from silence, and this is not silence — it is a scope-wide absence the engine can point at,
|
|
173
|
+
// carrying the justification that names the subject it looked for. Left to fall through, every
|
|
174
|
+
// media, table and form criterion came back « à évaluer » on each page while the run-wide grid
|
|
175
|
+
// had them settled: measured on a three-page sample, 75 of 106 RGAA criteria open per page
|
|
176
|
+
// against 26 for the run.
|
|
177
|
+
if (c.inapplicable) return c.status;
|
|
169
178
|
|
|
170
179
|
// A verdict SOMEBODY RECORDED is not silence, and rule 3 below is only about silence.
|
|
171
180
|
//
|
|
@@ -225,13 +234,17 @@ export function derivePages(result: AuditResult, pages: PageScope[]): PageResult
|
|
|
225
234
|
const own = result.findings.filter((f) => f.page === p.id);
|
|
226
235
|
const criteria: CriterionResult[] = result.criteria.map((c) => {
|
|
227
236
|
const pf = own.filter((f) => f.criteriaId === c.id);
|
|
237
|
+
const status = pageStatus(c, pf, p.basis);
|
|
228
238
|
return {
|
|
229
239
|
id: c.id,
|
|
230
240
|
guideline: c.guideline,
|
|
231
|
-
status
|
|
241
|
+
status,
|
|
232
242
|
findings: pf,
|
|
233
243
|
...(c.justification ? { justification: c.justification } : {}),
|
|
234
244
|
...(c.decidedBy ? { decidedBy: c.decidedBy } : {}),
|
|
245
|
+
// Carried, not recomputed: a finding on THIS page proves the subject exists after all,
|
|
246
|
+
// and `pageStatus` has already turned that into an NC above.
|
|
247
|
+
...(c.inapplicable && status === c.status ? { inapplicable: true } : {}),
|
|
235
248
|
};
|
|
236
249
|
});
|
|
237
250
|
const { rate, decided, total } = pct(criteria);
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Single source of truth for shared types + the embedded version. sync-version.mjs
|
|
2
2
|
// rewrites VERSION at release time (kept in lockstep with package.json + SKILL.md).
|
|
3
|
-
export const VERSION = "5.
|
|
3
|
+
export const VERSION = "5.13.1";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|