ultra11y 5.15.0 → 5.16.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/.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/README.md +1 -1
- package/package.json +3 -3
- package/scripts/ultra11y.mjs +45 -12
- package/scripts/vendor/wcag-2.2-fr.json +1 -1
- package/scripts/vendor/wcag-2.2-sc-universe.json +1 -1
- package/scripts/vendor/wcag-2.2-techniques.json +1 -1
- package/scripts/vendor/wcag-2.2-text.json +1 -1
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +45 -12
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/references/dynamic.md +12 -7
- package/skills/ultra11y/scripts/ultra11y.mjs +45 -12
- package/src/cli.ts +39 -1
- package/src/data/wcag-universe.json +1 -1
- package/src/ledger.ts +20 -0
- package/src/scan-local.ts +50 -13
- 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.16.0",
|
|
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.16.0",
|
|
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.16.0",
|
|
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.16.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -442,7 +442,7 @@ node scripts/ultra11y.mjs scan http://localhost:3000 --runtime local --cwd . --m
|
|
|
442
442
|
node scripts/ultra11y.mjs report --in audits/audit-latest.json --out audits
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
-
Only the Docker runtime needs Docker
|
|
445
|
+
Only the Docker runtime needs Docker. `--runtime local` needs a Chromium binary (`npx playwright install chromium`) and nothing else: `@playwright/test` and `@axe-core/playwright` are dependencies of the package, and the audited project's own copies take precedence when it has them. The engine bundle itself stays dependency-free. The Docker runner + Dockerfile are embedded in the engine and mirrored under `docker/` (with a `docker-compose.yml`). See `skills/ultra11y/references/dynamic.md`.
|
|
446
446
|
|
|
447
447
|
Typical audit flow:
|
|
448
448
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.16.0",
|
|
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",
|
|
@@ -77,14 +77,14 @@
|
|
|
77
77
|
"url": "https://github.com/maxgfr/ultra11y"
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
+
"@axe-core/playwright": "^4.13.0",
|
|
80
81
|
"@babel/parser": "^8.0.0",
|
|
82
|
+
"@playwright/test": "^1.62.1",
|
|
81
83
|
"domhandler": "^6.0.1",
|
|
82
84
|
"htmlparser2": "^12.0.0"
|
|
83
85
|
},
|
|
84
86
|
"devDependencies": {
|
|
85
|
-
"@axe-core/playwright": "^4.13.0",
|
|
86
87
|
"@biomejs/biome": "^2.5.1",
|
|
87
|
-
"@playwright/test": "^1.62.1",
|
|
88
88
|
"@semantic-release/changelog": "^6.0.3",
|
|
89
89
|
"@semantic-release/exec": "^7.1.0",
|
|
90
90
|
"@semantic-release/git": "^10.0.1",
|
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.16.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -2290,7 +2290,7 @@ var wcag_universe_default = {
|
|
|
2290
2290
|
wcagVersion: "2.2",
|
|
2291
2291
|
source: "https://www.w3.org/TR/WCAG22/",
|
|
2292
2292
|
criteriaSource: "https://github.com/w3c/wcag",
|
|
2293
|
-
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
2293
|
+
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-20 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
2294
2294
|
criteria: [
|
|
2295
2295
|
{
|
|
2296
2296
|
id: "1.1.1",
|
|
@@ -58678,6 +58678,9 @@ function readLedger(path) {
|
|
|
58678
58678
|
return void 0;
|
|
58679
58679
|
}
|
|
58680
58680
|
}
|
|
58681
|
+
function unreadableCaptures(audit2, cwd = ".") {
|
|
58682
|
+
return (audit2.scope.pagesAudited ?? []).filter((id) => !existsSync21(join36(cwd, PAGES_DIR, id, "dom.html")));
|
|
58683
|
+
}
|
|
58681
58684
|
function writeLedger(path, ledger) {
|
|
58682
58685
|
mkdirSync10(dirname10(path), { recursive: true });
|
|
58683
58686
|
const sorted = { ...ledger, entries: [...ledger.entries].sort((a, b) => a.criteriaId.localeCompare(b.criteriaId, "en", { numeric: true })) };
|
|
@@ -59891,17 +59894,38 @@ function localTestedScs(interact) {
|
|
|
59891
59894
|
}
|
|
59892
59895
|
var PW_SPEC = "@playwright/test";
|
|
59893
59896
|
var AXE_SPEC = "@axe-core/playwright";
|
|
59894
|
-
function
|
|
59895
|
-
|
|
59896
|
-
|
|
59897
|
+
function resolveAnchors(cwd) {
|
|
59898
|
+
return [resolve12(cwd, "package.json"), import.meta.url];
|
|
59899
|
+
}
|
|
59900
|
+
function requireRuntime(cwd, spec) {
|
|
59901
|
+
let last;
|
|
59902
|
+
for (const from of resolveAnchors(cwd)) {
|
|
59897
59903
|
try {
|
|
59898
|
-
|
|
59904
|
+
return createRequire(from)(spec);
|
|
59905
|
+
} catch (e) {
|
|
59906
|
+
last = e;
|
|
59907
|
+
}
|
|
59908
|
+
}
|
|
59909
|
+
throw last instanceof Error ? last : new Error(String(last));
|
|
59910
|
+
}
|
|
59911
|
+
function resolvesRuntime(cwd, spec) {
|
|
59912
|
+
return resolveAnchors(cwd).some((from) => {
|
|
59913
|
+
try {
|
|
59914
|
+
createRequire(from).resolve(spec);
|
|
59915
|
+
return true;
|
|
59899
59916
|
} catch {
|
|
59900
|
-
return
|
|
59917
|
+
return false;
|
|
59918
|
+
}
|
|
59919
|
+
});
|
|
59920
|
+
}
|
|
59921
|
+
function localTierStatus(cwd) {
|
|
59922
|
+
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
59923
|
+
if (!resolvesRuntime(cwd, spec)) {
|
|
59924
|
+
return { ok: false, reason: `${spec} resolves neither from ${cwd} nor from ultra11y itself` };
|
|
59901
59925
|
}
|
|
59902
59926
|
}
|
|
59903
59927
|
try {
|
|
59904
|
-
const pw =
|
|
59928
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59905
59929
|
const bin = pw.chromium?.executablePath?.();
|
|
59906
59930
|
if (typeof bin === "string" && bin.length > 0 && !existsSync24(bin)) {
|
|
59907
59931
|
return { ok: false, reason: `no browser binary at ${bin} \u2014 run \`npx playwright install chromium\`` };
|
|
@@ -59918,15 +59942,14 @@ function resolveLocalDeps(cwd) {
|
|
|
59918
59942
|
let chromium;
|
|
59919
59943
|
let AxeBuilder;
|
|
59920
59944
|
try {
|
|
59921
|
-
const
|
|
59922
|
-
const
|
|
59923
|
-
const axeMod = req(AXE_SPEC);
|
|
59945
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59946
|
+
const axeMod = requireRuntime(cwd, AXE_SPEC);
|
|
59924
59947
|
chromium = pw.chromium;
|
|
59925
59948
|
AxeBuilder = axeMod.default ?? axeMod;
|
|
59926
59949
|
} catch (e) {
|
|
59927
59950
|
const msg = e instanceof Error ? e.message : String(e);
|
|
59928
59951
|
throw new Error(
|
|
59929
|
-
`Playwright
|
|
59952
|
+
`Playwright resolves neither from "${cwd}" nor from ultra11y itself. Both are dependencies of this package, so the usual cause is the standalone engine bundle running with no node_modules beside it: pass --cwd <dir> at a project that has @playwright/test + @axe-core/playwright (e.g. --cwd packages/app), or use --runtime docker. (${msg})`
|
|
59930
59953
|
);
|
|
59931
59954
|
}
|
|
59932
59955
|
if (!chromium || typeof AxeBuilder !== "function") {
|
|
@@ -68713,6 +68736,11 @@ function applyAdjudicationFile(p, adj, lang) {
|
|
|
68713
68736
|
writeFileSync20(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
|
|
68714
68737
|
const ledgerOut = ledgerTarget(p, adj.standard);
|
|
68715
68738
|
if (ledgerOut) {
|
|
68739
|
+
const blind = unreadableCaptures(r.audit);
|
|
68740
|
+
if (blind.length)
|
|
68741
|
+
console.error(
|
|
68742
|
+
lang === "fr" ? `\u26A0 Registre \xE9crit sans les instantan\xE9s de ${blind.length} page(s) que cet audit dit avoir lues (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). L'\xE9vidence moissonn\xE9e ici est plus petite que celle qu'un run reconstruira, donc ces verdicts seront d\xE9clar\xE9s P\xC9RIM\xC9S au rejeu. Adjugez l\xE0 o\xF9 \`.ultra11y/pages/\` existe.` : `\u26A0 Ledger written without the snapshots of ${blind.length} page(s) this audit says it read (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). The evidence harvested here is smaller than the one a run will rebuild, so these verdicts will be dropped as STALE on replay. Adjudicate where \`.ultra11y/pages/\` exists.`
|
|
68743
|
+
);
|
|
68716
68744
|
const refused = new Set(r.rejectedCriteria);
|
|
68717
68745
|
const accepted = new Set(adj.items.map((it) => it.criteriaId).filter((id) => !refused.has(id)));
|
|
68718
68746
|
const fresh = entriesFrom(adj, accepted, r.audit.date);
|
|
@@ -68845,6 +68873,11 @@ async function cmdJudge(p) {
|
|
|
68845
68873
|
`);
|
|
68846
68874
|
const ledgerOut = ledgerTarget(p, adj.standard);
|
|
68847
68875
|
if (ledgerOut) {
|
|
68876
|
+
const blind = unreadableCaptures(r.audit);
|
|
68877
|
+
if (blind.length)
|
|
68878
|
+
console.error(
|
|
68879
|
+
lang === "fr" ? `\u26A0 Registre \xE9crit sans les instantan\xE9s de ${blind.length} page(s) que cet audit dit avoir lues (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). L'\xE9vidence moissonn\xE9e ici est plus petite que celle qu'un run reconstruira, donc ces verdicts seront d\xE9clar\xE9s P\xC9RIM\xC9S au rejeu. Adjugez l\xE0 o\xF9 \`.ultra11y/pages/\` existe.` : `\u26A0 Ledger written without the snapshots of ${blind.length} page(s) this audit says it read (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). The evidence harvested here is smaller than the one a run will rebuild, so these verdicts will be dropped as STALE on replay. Adjudicate where \`.ultra11y/pages/\` exists.`
|
|
68880
|
+
);
|
|
68848
68881
|
const refused = new Set(r.rejectedCriteria);
|
|
68849
68882
|
const accepted = new Set(adj.items.map((it) => it.criteriaId).filter((id) => !refused.has(id)));
|
|
68850
68883
|
const fresh = entriesFrom(adj, accepted, r.audit.date);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"wcagVersion": "2.2",
|
|
3
3
|
"source": "https://www.w3.org/TR/WCAG22/",
|
|
4
4
|
"criteriaSource": "https://github.com/w3c/wcag",
|
|
5
|
-
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
5
|
+
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-20 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
6
6
|
"principles": [
|
|
7
7
|
{
|
|
8
8
|
"number": 1,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"source": "https://github.com/w3c/wcag",
|
|
3
3
|
"understanding": "https://www.w3.org/WAI/WCAG22/Understanding/",
|
|
4
4
|
"license": "W3C Document License (code samples: W3C Software License)",
|
|
5
|
-
"fetchedAt": "2026-08-
|
|
5
|
+
"fetchedAt": "2026-08-20",
|
|
6
6
|
"criteria": {
|
|
7
7
|
"1.1.1": {
|
|
8
8
|
"slug": "non-text-content",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "https://github.com/w3c/wcag",
|
|
3
3
|
"license": "W3C Document License",
|
|
4
|
-
"fetchedAt": "2026-08-
|
|
4
|
+
"fetchedAt": "2026-08-20",
|
|
5
5
|
"criteria": {
|
|
6
6
|
"1.1.1": "All non-text content that is presented to the user has a text alternative that serves the equivalent purpose, except for the situations listed below.\nControls, Input:\nIf non-text content is a control or accepts user input, then it has a name that describes its purpose. (Refer to Success Criterion 4.1.2 Name, Role, Value for additional requirements for controls and content that accepts user input.)\nTime-Based Media:\nIf non-text content is time-based media, then text alternatives at least provide descriptive identification of the non-text content. (Refer to Guideline 1.2 for additional requirements for media.)\nTest:\nIf non-text content is a test or exercise that would be invalid if presented in text, then text alternatives at least provide descriptive identification of the non-text content.\nSensory:\nIf non-text content is primarily intended to create a specific sensory experience, then text alternatives at least provide descriptive identification of the non-text content.\nCAPTCHA:\nIf the purpose of non-text content is to confirm that content is being accessed by a person rather than a computer, then text alternatives that identify and describe the purpose of the non-text content are provided, and alternative forms of CAPTCHA using output modes for different types of sensory perception are provided to accommodate different disabilities.\nDecoration, Formatting, Invisible:\nIf non-text content is pure decoration, is used only for visual formatting, or is not presented to users, then it is implemented in a way that it can be ignored by assistive technology.",
|
|
7
7
|
"1.2.1": "For prerecorded audio-only and prerecorded video-only media, the following are true, except when the audio or video is a media alternative for text and is clearly labeled as such:\nPrerecorded Audio-only:\nAn alternative for time-based media is provided that presents equivalent information for prerecorded audio-only content.\nPrerecorded Video-only:\nEither an alternative for time-based media or an audio track is provided that presents equivalent information for prerecorded video-only content.",
|
|
@@ -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.16.0
|
|
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.16.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -2290,7 +2290,7 @@ var wcag_universe_default = {
|
|
|
2290
2290
|
wcagVersion: "2.2",
|
|
2291
2291
|
source: "https://www.w3.org/TR/WCAG22/",
|
|
2292
2292
|
criteriaSource: "https://github.com/w3c/wcag",
|
|
2293
|
-
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
2293
|
+
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-20 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
2294
2294
|
criteria: [
|
|
2295
2295
|
{
|
|
2296
2296
|
id: "1.1.1",
|
|
@@ -58678,6 +58678,9 @@ function readLedger(path) {
|
|
|
58678
58678
|
return void 0;
|
|
58679
58679
|
}
|
|
58680
58680
|
}
|
|
58681
|
+
function unreadableCaptures(audit2, cwd = ".") {
|
|
58682
|
+
return (audit2.scope.pagesAudited ?? []).filter((id) => !existsSync21(join36(cwd, PAGES_DIR, id, "dom.html")));
|
|
58683
|
+
}
|
|
58681
58684
|
function writeLedger(path, ledger) {
|
|
58682
58685
|
mkdirSync10(dirname10(path), { recursive: true });
|
|
58683
58686
|
const sorted = { ...ledger, entries: [...ledger.entries].sort((a, b) => a.criteriaId.localeCompare(b.criteriaId, "en", { numeric: true })) };
|
|
@@ -59891,17 +59894,38 @@ function localTestedScs(interact) {
|
|
|
59891
59894
|
}
|
|
59892
59895
|
var PW_SPEC = "@playwright/test";
|
|
59893
59896
|
var AXE_SPEC = "@axe-core/playwright";
|
|
59894
|
-
function
|
|
59895
|
-
|
|
59896
|
-
|
|
59897
|
+
function resolveAnchors(cwd) {
|
|
59898
|
+
return [resolve12(cwd, "package.json"), import.meta.url];
|
|
59899
|
+
}
|
|
59900
|
+
function requireRuntime(cwd, spec) {
|
|
59901
|
+
let last;
|
|
59902
|
+
for (const from of resolveAnchors(cwd)) {
|
|
59897
59903
|
try {
|
|
59898
|
-
|
|
59904
|
+
return createRequire(from)(spec);
|
|
59905
|
+
} catch (e) {
|
|
59906
|
+
last = e;
|
|
59907
|
+
}
|
|
59908
|
+
}
|
|
59909
|
+
throw last instanceof Error ? last : new Error(String(last));
|
|
59910
|
+
}
|
|
59911
|
+
function resolvesRuntime(cwd, spec) {
|
|
59912
|
+
return resolveAnchors(cwd).some((from) => {
|
|
59913
|
+
try {
|
|
59914
|
+
createRequire(from).resolve(spec);
|
|
59915
|
+
return true;
|
|
59899
59916
|
} catch {
|
|
59900
|
-
return
|
|
59917
|
+
return false;
|
|
59918
|
+
}
|
|
59919
|
+
});
|
|
59920
|
+
}
|
|
59921
|
+
function localTierStatus(cwd) {
|
|
59922
|
+
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
59923
|
+
if (!resolvesRuntime(cwd, spec)) {
|
|
59924
|
+
return { ok: false, reason: `${spec} resolves neither from ${cwd} nor from ultra11y itself` };
|
|
59901
59925
|
}
|
|
59902
59926
|
}
|
|
59903
59927
|
try {
|
|
59904
|
-
const pw =
|
|
59928
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59905
59929
|
const bin = pw.chromium?.executablePath?.();
|
|
59906
59930
|
if (typeof bin === "string" && bin.length > 0 && !existsSync24(bin)) {
|
|
59907
59931
|
return { ok: false, reason: `no browser binary at ${bin} \u2014 run \`npx playwright install chromium\`` };
|
|
@@ -59918,15 +59942,14 @@ function resolveLocalDeps(cwd) {
|
|
|
59918
59942
|
let chromium;
|
|
59919
59943
|
let AxeBuilder;
|
|
59920
59944
|
try {
|
|
59921
|
-
const
|
|
59922
|
-
const
|
|
59923
|
-
const axeMod = req(AXE_SPEC);
|
|
59945
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59946
|
+
const axeMod = requireRuntime(cwd, AXE_SPEC);
|
|
59924
59947
|
chromium = pw.chromium;
|
|
59925
59948
|
AxeBuilder = axeMod.default ?? axeMod;
|
|
59926
59949
|
} catch (e) {
|
|
59927
59950
|
const msg = e instanceof Error ? e.message : String(e);
|
|
59928
59951
|
throw new Error(
|
|
59929
|
-
`Playwright
|
|
59952
|
+
`Playwright resolves neither from "${cwd}" nor from ultra11y itself. Both are dependencies of this package, so the usual cause is the standalone engine bundle running with no node_modules beside it: pass --cwd <dir> at a project that has @playwright/test + @axe-core/playwright (e.g. --cwd packages/app), or use --runtime docker. (${msg})`
|
|
59930
59953
|
);
|
|
59931
59954
|
}
|
|
59932
59955
|
if (!chromium || typeof AxeBuilder !== "function") {
|
|
@@ -68713,6 +68736,11 @@ function applyAdjudicationFile(p, adj, lang) {
|
|
|
68713
68736
|
writeFileSync20(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
|
|
68714
68737
|
const ledgerOut = ledgerTarget(p, adj.standard);
|
|
68715
68738
|
if (ledgerOut) {
|
|
68739
|
+
const blind = unreadableCaptures(r.audit);
|
|
68740
|
+
if (blind.length)
|
|
68741
|
+
console.error(
|
|
68742
|
+
lang === "fr" ? `\u26A0 Registre \xE9crit sans les instantan\xE9s de ${blind.length} page(s) que cet audit dit avoir lues (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). L'\xE9vidence moissonn\xE9e ici est plus petite que celle qu'un run reconstruira, donc ces verdicts seront d\xE9clar\xE9s P\xC9RIM\xC9S au rejeu. Adjugez l\xE0 o\xF9 \`.ultra11y/pages/\` existe.` : `\u26A0 Ledger written without the snapshots of ${blind.length} page(s) this audit says it read (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). The evidence harvested here is smaller than the one a run will rebuild, so these verdicts will be dropped as STALE on replay. Adjudicate where \`.ultra11y/pages/\` exists.`
|
|
68743
|
+
);
|
|
68716
68744
|
const refused = new Set(r.rejectedCriteria);
|
|
68717
68745
|
const accepted = new Set(adj.items.map((it) => it.criteriaId).filter((id) => !refused.has(id)));
|
|
68718
68746
|
const fresh = entriesFrom(adj, accepted, r.audit.date);
|
|
@@ -68845,6 +68873,11 @@ async function cmdJudge(p) {
|
|
|
68845
68873
|
`);
|
|
68846
68874
|
const ledgerOut = ledgerTarget(p, adj.standard);
|
|
68847
68875
|
if (ledgerOut) {
|
|
68876
|
+
const blind = unreadableCaptures(r.audit);
|
|
68877
|
+
if (blind.length)
|
|
68878
|
+
console.error(
|
|
68879
|
+
lang === "fr" ? `\u26A0 Registre \xE9crit sans les instantan\xE9s de ${blind.length} page(s) que cet audit dit avoir lues (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). L'\xE9vidence moissonn\xE9e ici est plus petite que celle qu'un run reconstruira, donc ces verdicts seront d\xE9clar\xE9s P\xC9RIM\xC9S au rejeu. Adjugez l\xE0 o\xF9 \`.ultra11y/pages/\` existe.` : `\u26A0 Ledger written without the snapshots of ${blind.length} page(s) this audit says it read (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). The evidence harvested here is smaller than the one a run will rebuild, so these verdicts will be dropped as STALE on replay. Adjudicate where \`.ultra11y/pages/\` exists.`
|
|
68880
|
+
);
|
|
68848
68881
|
const refused = new Set(r.rejectedCriteria);
|
|
68849
68882
|
const accepted = new Set(adj.items.map((it) => it.criteriaId).filter((id) => !refused.has(id)));
|
|
68850
68883
|
const fresh = entriesFrom(adj, accepted, r.audit.date);
|
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.16.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -5,18 +5,23 @@ contrast (1.4.3), focus visible (2.4.7), reflow/zoom (1.4.4/1.4.10), text spacin
|
|
|
5
5
|
content on hover (1.4.13), target size (2.5.8). The dynamic tier decides them by running
|
|
6
6
|
**axe-core in a real headless browser** (Playwright). Two runtimes, same finding shape:
|
|
7
7
|
|
|
8
|
-
- **`--runtime local`** (recommended): resolves
|
|
9
|
-
**at runtime
|
|
10
|
-
|
|
8
|
+
- **`--runtime local`** (recommended): resolves Playwright + `@axe-core/playwright`
|
|
9
|
+
**at runtime**, from `--cwd` first and from ultra11y's own install second (no Docker, no
|
|
10
|
+
global install). Both are dependencies of this package, so a project that installed ultra11y
|
|
11
|
+
alone still gets the tier; a project that pins its own Playwright keeps it, because `--cwd`
|
|
12
|
+
is tried first. Adds the residual-criteria **probes** (below).
|
|
11
13
|
- **`--runtime docker`**: runs axe-core in a Docker image auto-built on first use (runner +
|
|
12
14
|
Dockerfile embedded in the bundle). No host deps beyond Docker. Axe + 320px reflow only.
|
|
13
|
-
- **`--runtime auto`** (default): local if Playwright resolves
|
|
14
|
-
an actionable error.
|
|
15
|
+
- **`--runtime auto`** (default): local if Playwright resolves **and** a browser binary is on
|
|
16
|
+
disk, else Docker, else an actionable error. Since the packages ship with ultra11y, what
|
|
17
|
+
decides `auto` in practice is the browser, not the modules.
|
|
15
18
|
|
|
16
19
|
## Prerequisites
|
|
17
20
|
|
|
18
|
-
- **local**: a
|
|
19
|
-
|
|
21
|
+
- **local**: a Chromium browser — `npx playwright install chromium`. The two npm packages
|
|
22
|
+
(`@playwright/test`, `@axe-core/playwright`) come with ultra11y and need no separate install;
|
|
23
|
+
installing them in the audited project is still supported and takes precedence, which is what
|
|
24
|
+
a repository with its own pinned Playwright wants.
|
|
20
25
|
- **docker**: Docker running.
|
|
21
26
|
|
|
22
27
|
The rest of the skill (static audit) needs neither.
|
|
@@ -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.16.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -2290,7 +2290,7 @@ var wcag_universe_default = {
|
|
|
2290
2290
|
wcagVersion: "2.2",
|
|
2291
2291
|
source: "https://www.w3.org/TR/WCAG22/",
|
|
2292
2292
|
criteriaSource: "https://github.com/w3c/wcag",
|
|
2293
|
-
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
2293
|
+
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-20 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
2294
2294
|
criteria: [
|
|
2295
2295
|
{
|
|
2296
2296
|
id: "1.1.1",
|
|
@@ -58678,6 +58678,9 @@ function readLedger(path) {
|
|
|
58678
58678
|
return void 0;
|
|
58679
58679
|
}
|
|
58680
58680
|
}
|
|
58681
|
+
function unreadableCaptures(audit2, cwd = ".") {
|
|
58682
|
+
return (audit2.scope.pagesAudited ?? []).filter((id) => !existsSync21(join36(cwd, PAGES_DIR, id, "dom.html")));
|
|
58683
|
+
}
|
|
58681
58684
|
function writeLedger(path, ledger) {
|
|
58682
58685
|
mkdirSync10(dirname10(path), { recursive: true });
|
|
58683
58686
|
const sorted = { ...ledger, entries: [...ledger.entries].sort((a, b) => a.criteriaId.localeCompare(b.criteriaId, "en", { numeric: true })) };
|
|
@@ -59891,17 +59894,38 @@ function localTestedScs(interact) {
|
|
|
59891
59894
|
}
|
|
59892
59895
|
var PW_SPEC = "@playwright/test";
|
|
59893
59896
|
var AXE_SPEC = "@axe-core/playwright";
|
|
59894
|
-
function
|
|
59895
|
-
|
|
59896
|
-
|
|
59897
|
+
function resolveAnchors(cwd) {
|
|
59898
|
+
return [resolve12(cwd, "package.json"), import.meta.url];
|
|
59899
|
+
}
|
|
59900
|
+
function requireRuntime(cwd, spec) {
|
|
59901
|
+
let last;
|
|
59902
|
+
for (const from of resolveAnchors(cwd)) {
|
|
59897
59903
|
try {
|
|
59898
|
-
|
|
59904
|
+
return createRequire(from)(spec);
|
|
59905
|
+
} catch (e) {
|
|
59906
|
+
last = e;
|
|
59907
|
+
}
|
|
59908
|
+
}
|
|
59909
|
+
throw last instanceof Error ? last : new Error(String(last));
|
|
59910
|
+
}
|
|
59911
|
+
function resolvesRuntime(cwd, spec) {
|
|
59912
|
+
return resolveAnchors(cwd).some((from) => {
|
|
59913
|
+
try {
|
|
59914
|
+
createRequire(from).resolve(spec);
|
|
59915
|
+
return true;
|
|
59899
59916
|
} catch {
|
|
59900
|
-
return
|
|
59917
|
+
return false;
|
|
59918
|
+
}
|
|
59919
|
+
});
|
|
59920
|
+
}
|
|
59921
|
+
function localTierStatus(cwd) {
|
|
59922
|
+
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
59923
|
+
if (!resolvesRuntime(cwd, spec)) {
|
|
59924
|
+
return { ok: false, reason: `${spec} resolves neither from ${cwd} nor from ultra11y itself` };
|
|
59901
59925
|
}
|
|
59902
59926
|
}
|
|
59903
59927
|
try {
|
|
59904
|
-
const pw =
|
|
59928
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59905
59929
|
const bin = pw.chromium?.executablePath?.();
|
|
59906
59930
|
if (typeof bin === "string" && bin.length > 0 && !existsSync24(bin)) {
|
|
59907
59931
|
return { ok: false, reason: `no browser binary at ${bin} \u2014 run \`npx playwright install chromium\`` };
|
|
@@ -59918,15 +59942,14 @@ function resolveLocalDeps(cwd) {
|
|
|
59918
59942
|
let chromium;
|
|
59919
59943
|
let AxeBuilder;
|
|
59920
59944
|
try {
|
|
59921
|
-
const
|
|
59922
|
-
const
|
|
59923
|
-
const axeMod = req(AXE_SPEC);
|
|
59945
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59946
|
+
const axeMod = requireRuntime(cwd, AXE_SPEC);
|
|
59924
59947
|
chromium = pw.chromium;
|
|
59925
59948
|
AxeBuilder = axeMod.default ?? axeMod;
|
|
59926
59949
|
} catch (e) {
|
|
59927
59950
|
const msg = e instanceof Error ? e.message : String(e);
|
|
59928
59951
|
throw new Error(
|
|
59929
|
-
`Playwright
|
|
59952
|
+
`Playwright resolves neither from "${cwd}" nor from ultra11y itself. Both are dependencies of this package, so the usual cause is the standalone engine bundle running with no node_modules beside it: pass --cwd <dir> at a project that has @playwright/test + @axe-core/playwright (e.g. --cwd packages/app), or use --runtime docker. (${msg})`
|
|
59930
59953
|
);
|
|
59931
59954
|
}
|
|
59932
59955
|
if (!chromium || typeof AxeBuilder !== "function") {
|
|
@@ -68713,6 +68736,11 @@ function applyAdjudicationFile(p, adj, lang) {
|
|
|
68713
68736
|
writeFileSync20(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
|
|
68714
68737
|
const ledgerOut = ledgerTarget(p, adj.standard);
|
|
68715
68738
|
if (ledgerOut) {
|
|
68739
|
+
const blind = unreadableCaptures(r.audit);
|
|
68740
|
+
if (blind.length)
|
|
68741
|
+
console.error(
|
|
68742
|
+
lang === "fr" ? `\u26A0 Registre \xE9crit sans les instantan\xE9s de ${blind.length} page(s) que cet audit dit avoir lues (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). L'\xE9vidence moissonn\xE9e ici est plus petite que celle qu'un run reconstruira, donc ces verdicts seront d\xE9clar\xE9s P\xC9RIM\xC9S au rejeu. Adjugez l\xE0 o\xF9 \`.ultra11y/pages/\` existe.` : `\u26A0 Ledger written without the snapshots of ${blind.length} page(s) this audit says it read (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). The evidence harvested here is smaller than the one a run will rebuild, so these verdicts will be dropped as STALE on replay. Adjudicate where \`.ultra11y/pages/\` exists.`
|
|
68743
|
+
);
|
|
68716
68744
|
const refused = new Set(r.rejectedCriteria);
|
|
68717
68745
|
const accepted = new Set(adj.items.map((it) => it.criteriaId).filter((id) => !refused.has(id)));
|
|
68718
68746
|
const fresh = entriesFrom(adj, accepted, r.audit.date);
|
|
@@ -68845,6 +68873,11 @@ async function cmdJudge(p) {
|
|
|
68845
68873
|
`);
|
|
68846
68874
|
const ledgerOut = ledgerTarget(p, adj.standard);
|
|
68847
68875
|
if (ledgerOut) {
|
|
68876
|
+
const blind = unreadableCaptures(r.audit);
|
|
68877
|
+
if (blind.length)
|
|
68878
|
+
console.error(
|
|
68879
|
+
lang === "fr" ? `\u26A0 Registre \xE9crit sans les instantan\xE9s de ${blind.length} page(s) que cet audit dit avoir lues (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). L'\xE9vidence moissonn\xE9e ici est plus petite que celle qu'un run reconstruira, donc ces verdicts seront d\xE9clar\xE9s P\xC9RIM\xC9S au rejeu. Adjugez l\xE0 o\xF9 \`.ultra11y/pages/\` existe.` : `\u26A0 Ledger written without the snapshots of ${blind.length} page(s) this audit says it read (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "\u2026" : ""}). The evidence harvested here is smaller than the one a run will rebuild, so these verdicts will be dropped as STALE on replay. Adjudicate where \`.ultra11y/pages/\` exists.`
|
|
68880
|
+
);
|
|
68848
68881
|
const refused = new Set(r.rejectedCriteria);
|
|
68849
68882
|
const accepted = new Set(adj.items.map((it) => it.criteriaId).filter((id) => !refused.has(id)));
|
|
68850
68883
|
const fresh = entriesFrom(adj, accepted, r.audit.date);
|
package/src/cli.ts
CHANGED
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
hydrateAdjudication,
|
|
51
51
|
type AdjudicationFile,
|
|
52
52
|
} from "./adjudicate.js";
|
|
53
|
-
import { entriesFrom, isLedger, ledgerPath, mergeLedger, readLedger, replayLedger, type VerdictLedger, writeLedger } from "./ledger.js";
|
|
53
|
+
import { entriesFrom, isLedger, ledgerPath, mergeLedger, readLedger, replayLedger, unreadableCaptures, type VerdictLedger, writeLedger } from "./ledger.js";
|
|
54
54
|
import { BATCH_SIZE, apiKeyFromEnv, applyRawVerdicts, judgeAll, modelFromEnv } from "./llm.js";
|
|
55
55
|
import { runScan, runScanMany, runCrawlScan, runSampleScan, mergeDynamic, mergeSnapshotAudit, cleanDynamic, dockerAvailable } from "./scan.js";
|
|
56
56
|
import { runScanLocal, runScanManyLocal, runCrawlScanLocal, runSampleScanLocal, localAvailable, localTierStatus } from "./scan-local.js";
|
|
@@ -2940,6 +2940,25 @@ function applyAdjudicationFile(p: ParsedArgs, adj: AdjudicationFile, lang: Lang)
|
|
|
2940
2940
|
// next replay, which is the whole thing the gate exists to prevent.
|
|
2941
2941
|
const ledgerOut = ledgerTarget(p, adj.standard);
|
|
2942
2942
|
if (ledgerOut) {
|
|
2943
|
+
// A LEDGER WRITTEN WITHOUT THE CAPTURES IS STALE ON ARRIVAL.
|
|
2944
|
+
//
|
|
2945
|
+
// The fingerprint is over the evidence the harvest READ FROM DISK, so recording a verdict
|
|
2946
|
+
// where the audit's page captures are absent fingerprints a strictly smaller set than the
|
|
2947
|
+
// one CI rebuilds — and the replay drops it as stale, every run, silently. Measured on a
|
|
2948
|
+
// real repository: RGAA 12.3 recorded over 13 evidence items where the run harvests 22.
|
|
2949
|
+
// The entry looked well-formed, the fold accepted it, it was committed and reviewed, and
|
|
2950
|
+
// it never once replayed.
|
|
2951
|
+
//
|
|
2952
|
+
// A warning, not a refusal: the verdict itself is sound and still lands on THIS audit, and
|
|
2953
|
+
// a caller who genuinely has no captures (a source-only repository) has nothing to fix.
|
|
2954
|
+
// What must not happen is writing it and saying nothing.
|
|
2955
|
+
const blind = unreadableCaptures(r.audit);
|
|
2956
|
+
if (blind.length)
|
|
2957
|
+
console.error(
|
|
2958
|
+
lang === "fr"
|
|
2959
|
+
? `⚠ Registre écrit sans les instantanés de ${blind.length} page(s) que cet audit dit avoir lues (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "…" : ""}). L'évidence moissonnée ici est plus petite que celle qu'un run reconstruira, donc ces verdicts seront déclarés PÉRIMÉS au rejeu. Adjugez là où \`.ultra11y/pages/\` existe.`
|
|
2960
|
+
: `⚠ Ledger written without the snapshots of ${blind.length} page(s) this audit says it read (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "…" : ""}). The evidence harvested here is smaller than the one a run will rebuild, so these verdicts will be dropped as STALE on replay. Adjudicate where \`.ultra11y/pages/\` exists.`,
|
|
2961
|
+
);
|
|
2943
2962
|
const refused = new Set(r.rejectedCriteria);
|
|
2944
2963
|
const accepted = new Set(adj.items.map((it) => it.criteriaId).filter((id) => !refused.has(id)));
|
|
2945
2964
|
const fresh = entriesFrom(adj, accepted, r.audit.date);
|
|
@@ -3129,6 +3148,25 @@ async function cmdJudge(p: ParsedArgs): Promise<number> {
|
|
|
3129
3148
|
// run can replay it for free.
|
|
3130
3149
|
const ledgerOut = ledgerTarget(p, adj.standard);
|
|
3131
3150
|
if (ledgerOut) {
|
|
3151
|
+
// A LEDGER WRITTEN WITHOUT THE CAPTURES IS STALE ON ARRIVAL.
|
|
3152
|
+
//
|
|
3153
|
+
// The fingerprint is over the evidence the harvest READ FROM DISK, so recording a verdict
|
|
3154
|
+
// where the audit's page captures are absent fingerprints a strictly smaller set than the
|
|
3155
|
+
// one CI rebuilds — and the replay drops it as stale, every run, silently. Measured on a
|
|
3156
|
+
// real repository: RGAA 12.3 recorded over 13 evidence items where the run harvests 22.
|
|
3157
|
+
// The entry looked well-formed, the fold accepted it, it was committed and reviewed, and
|
|
3158
|
+
// it never once replayed.
|
|
3159
|
+
//
|
|
3160
|
+
// A warning, not a refusal: the verdict itself is sound and still lands on THIS audit, and
|
|
3161
|
+
// a caller who genuinely has no captures (a source-only repository) has nothing to fix.
|
|
3162
|
+
// What must not happen is writing it and saying nothing.
|
|
3163
|
+
const blind = unreadableCaptures(r.audit);
|
|
3164
|
+
if (blind.length)
|
|
3165
|
+
console.error(
|
|
3166
|
+
lang === "fr"
|
|
3167
|
+
? `⚠ Registre écrit sans les instantanés de ${blind.length} page(s) que cet audit dit avoir lues (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "…" : ""}). L'évidence moissonnée ici est plus petite que celle qu'un run reconstruira, donc ces verdicts seront déclarés PÉRIMÉS au rejeu. Adjugez là où \`.ultra11y/pages/\` existe.`
|
|
3168
|
+
: `⚠ Ledger written without the snapshots of ${blind.length} page(s) this audit says it read (${blind.slice(0, 5).join(", ")}${blind.length > 5 ? "…" : ""}). The evidence harvested here is smaller than the one a run will rebuild, so these verdicts will be dropped as STALE on replay. Adjudicate where \`.ultra11y/pages/\` exists.`,
|
|
3169
|
+
);
|
|
3132
3170
|
const refused = new Set(r.rejectedCriteria);
|
|
3133
3171
|
const accepted = new Set(adj.items.map((it) => it.criteriaId).filter((id) => !refused.has(id)));
|
|
3134
3172
|
const fresh = entriesFrom(adj, accepted, r.audit.date);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"wcagVersion": "2.2",
|
|
3
3
|
"source": "https://www.w3.org/TR/WCAG22/",
|
|
4
4
|
"criteriaSource": "https://github.com/w3c/wcag",
|
|
5
|
-
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
5
|
+
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-20 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
6
6
|
"criteria": [
|
|
7
7
|
{
|
|
8
8
|
"id": "1.1.1",
|
package/src/ledger.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
31
31
|
import { dirname, join } from "node:path";
|
|
32
32
|
|
|
33
33
|
import { type AdjudicationFile, type AdjudicationItem, type AgentFinding, buildAdjudicationWorklist, type Evidence, readCitation } from "./adjudicate.js";
|
|
34
|
+
import { PAGES_DIR } from "./snapshot.js";
|
|
34
35
|
import { CORE, type StandardId } from "./standards/index.js";
|
|
35
36
|
import type { AuditResult } from "./types.js";
|
|
36
37
|
import { SCHEMA_VERSION } from "./types.js";
|
|
@@ -127,6 +128,25 @@ export function readLedger(path: string): VerdictLedger | undefined {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
/** Would a ledger written from THIS harvest be replayable? Names what is missing when not.
|
|
132
|
+
*
|
|
133
|
+
* A ledger entry is keyed on a fingerprint of the evidence the verdict was ruled against, and
|
|
134
|
+
* the harvest RE-READS the audited files from disk. So a verdict recorded where the audit's
|
|
135
|
+
* page captures are absent is fingerprinted over a strictly smaller evidence set than the one
|
|
136
|
+
* CI will rebuild — and it is stale on arrival, every time, silently.
|
|
137
|
+
*
|
|
138
|
+
* Measured while adjudicating a real repository from a checkout with no `.ultra11y/pages/`:
|
|
139
|
+
* RGAA 12.3 was recorded over 13 evidence items where the run harvests 22. The entry looked
|
|
140
|
+
* perfectly well-formed, the fold accepted it, the file was committed and reviewed — and the
|
|
141
|
+
* replay dropped it as stale on every subsequent run, with nothing anywhere saying why.
|
|
142
|
+
*
|
|
143
|
+
* Returns the page ids the audit says it read and whose capture is not on disk. Empty when
|
|
144
|
+
* there is nothing to warn about — including on a source-only audit, which legitimately has
|
|
145
|
+
* no captures to miss. */
|
|
146
|
+
export function unreadableCaptures(audit: AuditResult, cwd = "."): string[] {
|
|
147
|
+
return (audit.scope.pagesAudited ?? []).filter((id) => !existsSync(join(cwd, PAGES_DIR, id, "dom.html")));
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
export function writeLedger(path: string, ledger: VerdictLedger): void {
|
|
131
151
|
mkdirSync(dirname(path), { recursive: true });
|
|
132
152
|
// Entries id-sorted so the committed file has a stable diff — a ledger that reshuffles on
|
package/src/scan-local.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// `scan --runtime local` — the dynamic tier WITHOUT Docker. Resolves
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// `scan --runtime local` — the dynamic tier WITHOUT Docker. Resolves Playwright +
|
|
2
|
+
// @axe-core/playwright AT RUNTIME (createRequire, a node: builtin) from `--cwd` first and
|
|
3
|
+
// from this package second, so the zero-dep static engine bundle never gains a static
|
|
4
|
+
// browser import while a project that installed ultra11y alone still gets the tier.
|
|
4
5
|
// It runs the SAME axe-core pass as the Docker RUNNER, then the residual-criteria
|
|
5
6
|
// probes axe cannot decide (focus visibility, 200% zoom, text spacing, content on
|
|
6
7
|
// hover, target size). The Docker RUNNER / docker/* files are untouched (docker-sync).
|
|
@@ -59,6 +60,46 @@ interface LocalDeps {
|
|
|
59
60
|
const PW_SPEC = "@playwright/test";
|
|
60
61
|
const AXE_SPEC = "@axe-core/playwright";
|
|
61
62
|
|
|
63
|
+
/** Where a runtime dependency may live, in the order that must be tried.
|
|
64
|
+
*
|
|
65
|
+
* THE TARGET PROJECT FIRST, then this package — the same order `src/integrations/playwright.ts`
|
|
66
|
+
* uses, and for the same two reasons. A repository that pins its own Playwright must keep it:
|
|
67
|
+
* two copies in one process hand out `Page` objects the other one's fixtures do not recognise.
|
|
68
|
+
* And a repository that has none must still get the tier, now that `@playwright/test` and
|
|
69
|
+
* `@axe-core/playwright` are dependencies of ultra11y instead of something every caller had to
|
|
70
|
+
* duplicate into its own manifest. Anchoring on `cwd` ALONE was the older contract, and it made
|
|
71
|
+
* `--runtime local` unavailable to exactly the projects that had not read the prerequisites. */
|
|
72
|
+
function resolveAnchors(cwd: string): string[] {
|
|
73
|
+
return [resolve(cwd, "package.json"), import.meta.url];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Load `spec` from the target project, else from this package. Rethrows the LAST failure, so
|
|
77
|
+
* the message names the spec the caller asked for rather than the first anchor that missed. */
|
|
78
|
+
function requireRuntime(cwd: string, spec: string): Any {
|
|
79
|
+
let last: unknown;
|
|
80
|
+
for (const from of resolveAnchors(cwd)) {
|
|
81
|
+
try {
|
|
82
|
+
return createRequire(from)(spec);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
last = e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw last instanceof Error ? last : new Error(String(last));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Can `spec` be resolved from either anchor? Resolution only — never loads the module, so
|
|
91
|
+
* probing the tier stays as cheap as it was when it only looked at `cwd`. */
|
|
92
|
+
function resolvesRuntime(cwd: string, spec: string): boolean {
|
|
93
|
+
return resolveAnchors(cwd).some((from) => {
|
|
94
|
+
try {
|
|
95
|
+
createRequire(from).resolve(spec);
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
62
103
|
/** Why the LOCAL tier is (un)usable from `cwd` — the one line a reader of a
|
|
63
104
|
* `runtime: auto` log gets before the Docker fallback takes over. A silent degrade is
|
|
64
105
|
* indistinguishable from a working fallback, so the reason must NAME what to install. */
|
|
@@ -68,16 +109,13 @@ export type LocalTierStatus = { ok: true } | { ok: false; reason: string };
|
|
|
68
109
|
* boolean. Drives the `runtime: auto` decision AND the message printed when it degrades
|
|
69
110
|
* to Docker. */
|
|
70
111
|
export function localTierStatus(cwd: string): LocalTierStatus {
|
|
71
|
-
const req = createRequire(resolve(cwd, "package.json"));
|
|
72
112
|
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} catch {
|
|
76
|
-
return { ok: false, reason: `${spec} does not resolve from ${cwd}` };
|
|
113
|
+
if (!resolvesRuntime(cwd, spec)) {
|
|
114
|
+
return { ok: false, reason: `${spec} resolves neither from ${cwd} nor from ultra11y itself` };
|
|
77
115
|
}
|
|
78
116
|
}
|
|
79
117
|
try {
|
|
80
|
-
const pw =
|
|
118
|
+
const pw = requireRuntime(cwd, PW_SPEC) as { chromium?: { executablePath?: () => string } };
|
|
81
119
|
const bin = pw.chromium?.executablePath?.();
|
|
82
120
|
// No path at all ⇒ a Playwright that cannot tell us; treat as available and let the
|
|
83
121
|
// launch report its own failure, rather than refusing a tier that might work.
|
|
@@ -112,15 +150,14 @@ export function resolveLocalDeps(cwd: string): LocalDeps {
|
|
|
112
150
|
let chromium: Any;
|
|
113
151
|
let AxeBuilder: Any;
|
|
114
152
|
try {
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const axeMod = req(AXE_SPEC);
|
|
153
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
154
|
+
const axeMod = requireRuntime(cwd, AXE_SPEC);
|
|
118
155
|
chromium = pw.chromium;
|
|
119
156
|
AxeBuilder = axeMod.default ?? axeMod;
|
|
120
157
|
} catch (e) {
|
|
121
158
|
const msg = e instanceof Error ? e.message : String(e);
|
|
122
159
|
throw new Error(
|
|
123
|
-
`Playwright
|
|
160
|
+
`Playwright resolves neither from "${cwd}" nor from ultra11y itself. Both are dependencies of this package, so the usual cause is the standalone engine bundle running with no node_modules beside it: pass --cwd <dir> at a project that has @playwright/test + @axe-core/playwright (e.g. --cwd packages/app), or use --runtime docker. (${msg})`,
|
|
124
161
|
);
|
|
125
162
|
}
|
|
126
163
|
if (!chromium || typeof AxeBuilder !== "function") {
|
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.16.0";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|