ultra11y 5.15.1 → 5.17.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 +67 -27
- 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 +67 -27
- package/skills/ultra11y/SKILL.md +3 -3
- package/skills/ultra11y/references/dynamic.md +17 -7
- package/skills/ultra11y/scripts/ultra11y.mjs +67 -27
- package/src/cli.ts +18 -1
- package/src/data/wcag-universe.json +1 -1
- package/src/html-report.ts +13 -6
- package/src/init.ts +1 -1
- package/src/pages.ts +28 -2
- package/src/scan-local.ts +50 -13
- package/src/scan.ts +25 -7
- package/src/types.ts +1 -1
- package/dist/payload-DzQb84Kw.d.ts +0 -87
|
@@ -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.17.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.17.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.17.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.17.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`) plus `@playwright/test` and `@axe-core/playwright` — which **come with the npm package**, since they are its own dependencies; the audited project's own copies take precedence when it has them. Running the engine as a **standalone skill bundle** is the exception: an installed skill has no `node_modules` beside it, so there the two packages must be in the audited project, reachable from `--cwd`. The engine bundle itself stays dependency-free either way. 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.17.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.17.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",
|
|
@@ -54623,6 +54623,17 @@ function renderRedirected(redirected, lang = "en") {
|
|
|
54623
54623
|
}
|
|
54624
54624
|
return out2;
|
|
54625
54625
|
}
|
|
54626
|
+
function gridRate(rows, status, pageId) {
|
|
54627
|
+
let c2 = 0;
|
|
54628
|
+
let nc = 0;
|
|
54629
|
+
for (const row of rows) {
|
|
54630
|
+
const st = status.get(row.id)?.get(pageId);
|
|
54631
|
+
if (st === "C") c2++;
|
|
54632
|
+
else if (st === "NC") nc++;
|
|
54633
|
+
}
|
|
54634
|
+
const decided = c2 + nc;
|
|
54635
|
+
return [decided === 0 ? null : Math.round(c2 / decided * 100), decided, rows.length];
|
|
54636
|
+
}
|
|
54626
54637
|
function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
|
|
54627
54638
|
const s = L3[lang];
|
|
54628
54639
|
const out2 = [];
|
|
@@ -54636,9 +54647,9 @@ function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
|
|
|
54636
54647
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> \u26A0\uFE0F ${s.basisNote}`, "");
|
|
54637
54648
|
const head = [isCore(standard) ? s.criterion : s.criterion, ...derived.map((p) => `${p.name}${p.auth ? " \u{1F512}" : ""}`)];
|
|
54638
54649
|
out2.push(`| ${head.join(" | ")} |`, `| ${head.map(() => "---").join(" | ")} |`);
|
|
54639
|
-
out2.push(`| **${s.rate}** | ${derived.map((p) => `**${formatRate(p.conformancePct, p.decided, p.total)}**`).join(" | ")} |`);
|
|
54640
|
-
out2.push(`| _${s.snapshot}?_ | ${derived.map((p) => `_${basisLabel(p.basis, lang)}_`).join(" | ")} |`);
|
|
54641
54650
|
const { rows, status } = pageGridModel(result, derived, standard, lang);
|
|
54651
|
+
out2.push(`| **${s.rate}** | ${derived.map((p) => `**${formatRate(...gridRate(rows, status, p.id))}**`).join(" | ")} |`);
|
|
54652
|
+
out2.push(`| _${s.snapshot}?_ | ${derived.map((p) => `_${basisLabel(p.basis, lang)}_`).join(" | ")} |`);
|
|
54642
54653
|
let group = "";
|
|
54643
54654
|
for (const row of rows) {
|
|
54644
54655
|
if (row.group !== group) {
|
|
@@ -59669,11 +59680,13 @@ function mergeSnapshotAudit(base, snap) {
|
|
|
59669
59680
|
if (snap.packFindings?.length) merged.packFindings = [...merged.packFindings ?? [], ...snap.packFindings];
|
|
59670
59681
|
for (const c2 of merged.criteria) {
|
|
59671
59682
|
if (!c2.inapplicable) continue;
|
|
59672
|
-
|
|
59673
|
-
|
|
59674
|
-
|
|
59675
|
-
|
|
59676
|
-
|
|
59683
|
+
const measured = snapById.get(c2.id);
|
|
59684
|
+
if (!measured || measured.status !== "C" || measured.inapplicable) continue;
|
|
59685
|
+
c2.status = "C";
|
|
59686
|
+
delete c2.inapplicable;
|
|
59687
|
+
if (measured.justification) c2.justification = measured.justification;
|
|
59688
|
+
else delete c2.justification;
|
|
59689
|
+
if (measured.decidedBy) c2.decidedBy = measured.decidedBy;
|
|
59677
59690
|
}
|
|
59678
59691
|
const nowNc = new Set(snap.findings.filter((f) => !f.advisory).map((f) => f.criteriaId));
|
|
59679
59692
|
merged.residualRisks = merged.residualRisks.filter((r) => !nowNc.has(r.criteriaId));
|
|
@@ -59894,17 +59907,38 @@ function localTestedScs(interact) {
|
|
|
59894
59907
|
}
|
|
59895
59908
|
var PW_SPEC = "@playwright/test";
|
|
59896
59909
|
var AXE_SPEC = "@axe-core/playwright";
|
|
59897
|
-
function
|
|
59898
|
-
|
|
59899
|
-
|
|
59910
|
+
function resolveAnchors(cwd) {
|
|
59911
|
+
return [resolve12(cwd, "package.json"), import.meta.url];
|
|
59912
|
+
}
|
|
59913
|
+
function requireRuntime(cwd, spec) {
|
|
59914
|
+
let last;
|
|
59915
|
+
for (const from of resolveAnchors(cwd)) {
|
|
59900
59916
|
try {
|
|
59901
|
-
|
|
59917
|
+
return createRequire(from)(spec);
|
|
59918
|
+
} catch (e) {
|
|
59919
|
+
last = e;
|
|
59920
|
+
}
|
|
59921
|
+
}
|
|
59922
|
+
throw last instanceof Error ? last : new Error(String(last));
|
|
59923
|
+
}
|
|
59924
|
+
function resolvesRuntime(cwd, spec) {
|
|
59925
|
+
return resolveAnchors(cwd).some((from) => {
|
|
59926
|
+
try {
|
|
59927
|
+
createRequire(from).resolve(spec);
|
|
59928
|
+
return true;
|
|
59902
59929
|
} catch {
|
|
59903
|
-
return
|
|
59930
|
+
return false;
|
|
59931
|
+
}
|
|
59932
|
+
});
|
|
59933
|
+
}
|
|
59934
|
+
function localTierStatus(cwd) {
|
|
59935
|
+
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
59936
|
+
if (!resolvesRuntime(cwd, spec)) {
|
|
59937
|
+
return { ok: false, reason: `${spec} resolves neither from ${cwd} nor from ultra11y itself` };
|
|
59904
59938
|
}
|
|
59905
59939
|
}
|
|
59906
59940
|
try {
|
|
59907
|
-
const pw =
|
|
59941
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59908
59942
|
const bin = pw.chromium?.executablePath?.();
|
|
59909
59943
|
if (typeof bin === "string" && bin.length > 0 && !existsSync24(bin)) {
|
|
59910
59944
|
return { ok: false, reason: `no browser binary at ${bin} \u2014 run \`npx playwright install chromium\`` };
|
|
@@ -59921,15 +59955,14 @@ function resolveLocalDeps(cwd) {
|
|
|
59921
59955
|
let chromium;
|
|
59922
59956
|
let AxeBuilder;
|
|
59923
59957
|
try {
|
|
59924
|
-
const
|
|
59925
|
-
const
|
|
59926
|
-
const axeMod = req(AXE_SPEC);
|
|
59958
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59959
|
+
const axeMod = requireRuntime(cwd, AXE_SPEC);
|
|
59927
59960
|
chromium = pw.chromium;
|
|
59928
59961
|
AxeBuilder = axeMod.default ?? axeMod;
|
|
59929
59962
|
} catch (e) {
|
|
59930
59963
|
const msg = e instanceof Error ? e.message : String(e);
|
|
59931
59964
|
throw new Error(
|
|
59932
|
-
`Playwright
|
|
59965
|
+
`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})`
|
|
59933
59966
|
);
|
|
59934
59967
|
}
|
|
59935
59968
|
if (!chromium || typeof AxeBuilder !== "function") {
|
|
@@ -60996,7 +61029,7 @@ jobs:
|
|
|
60996
61029
|
ultra11y:
|
|
60997
61030
|
runs-on: ubuntu-latest
|
|
60998
61031
|
steps:
|
|
60999
|
-
- uses: actions/checkout@
|
|
61032
|
+
- uses: actions/checkout@v7
|
|
61000
61033
|
with:
|
|
61001
61034
|
fetch-depth: 0 # the diff gate needs the base ref
|
|
61002
61035
|
- uses: maxgfr/ultra11y@v${VERSION}
|
|
@@ -62955,12 +62988,16 @@ function scoreboardBlocks(result, standard, lang, sheetHref) {
|
|
|
62955
62988
|
const scope = pagesOf(result);
|
|
62956
62989
|
if (!scope.length) return [];
|
|
62957
62990
|
const derived = derivePages(result, scope);
|
|
62958
|
-
const rows = derived.map((p) =>
|
|
62959
|
-
|
|
62960
|
-
|
|
62961
|
-
|
|
62962
|
-
|
|
62963
|
-
|
|
62991
|
+
const rows = derived.map((p) => {
|
|
62992
|
+
const criteria = pageCriterionRows(result, p, standard, lang);
|
|
62993
|
+
const cov = pageCoverage(criteria);
|
|
62994
|
+
return [
|
|
62995
|
+
{ text: `${p.name}${p.auth ? " \u{1F512}" : ""}`, ...sheetHref ? { href: sheetHref(p.id) } : {} },
|
|
62996
|
+
{ text: p.url, mono: true },
|
|
62997
|
+
{ text: basisLabel(p.basis, lang) },
|
|
62998
|
+
{ text: formatRate(pageRatePct(criteria), cov.decided, cov.total), align: "end" }
|
|
62999
|
+
];
|
|
63000
|
+
});
|
|
62964
63001
|
const out2 = [
|
|
62965
63002
|
{ kind: "heading", level: 2, text: t3.perPage, id: "pages" },
|
|
62966
63003
|
{
|
|
@@ -67042,6 +67079,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
67042
67079
|
]);
|
|
67043
67080
|
var KNOWN_FLAGS = /* @__PURE__ */ new Set([...VALUE_FLAGS2, ...BOOLEAN_FLAGS]);
|
|
67044
67081
|
var LIST_FLAGS = /* @__PURE__ */ new Set(["include", "exclude", "ext"]);
|
|
67082
|
+
function looksLikeFlag(token) {
|
|
67083
|
+
return token?.startsWith("--") ?? false;
|
|
67084
|
+
}
|
|
67045
67085
|
function parseArgs(argv) {
|
|
67046
67086
|
const [command, ...rest] = argv;
|
|
67047
67087
|
const valueFlags = valueFlagsFor(command ?? "");
|
|
@@ -67056,7 +67096,7 @@ function parseArgs(argv) {
|
|
|
67056
67096
|
const inlineVal = eq === -1 ? void 0 : a.slice(eq + 1);
|
|
67057
67097
|
let val;
|
|
67058
67098
|
if (inlineVal !== void 0) val = inlineVal;
|
|
67059
|
-
else if (valueFlags.has(key)) val = rest[++i2] ?? "";
|
|
67099
|
+
else if (valueFlags.has(key) && !looksLikeFlag(rest[i2 + 1])) val = rest[++i2] ?? "";
|
|
67060
67100
|
else val = true;
|
|
67061
67101
|
const prev = flags2[key];
|
|
67062
67102
|
if (LIST_FLAGS.has(key) && typeof prev === "string" && typeof val === "string") {
|
|
@@ -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.17.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.17.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",
|
|
@@ -54623,6 +54623,17 @@ function renderRedirected(redirected, lang = "en") {
|
|
|
54623
54623
|
}
|
|
54624
54624
|
return out2;
|
|
54625
54625
|
}
|
|
54626
|
+
function gridRate(rows, status, pageId) {
|
|
54627
|
+
let c2 = 0;
|
|
54628
|
+
let nc = 0;
|
|
54629
|
+
for (const row of rows) {
|
|
54630
|
+
const st = status.get(row.id)?.get(pageId);
|
|
54631
|
+
if (st === "C") c2++;
|
|
54632
|
+
else if (st === "NC") nc++;
|
|
54633
|
+
}
|
|
54634
|
+
const decided = c2 + nc;
|
|
54635
|
+
return [decided === 0 ? null : Math.round(c2 / decided * 100), decided, rows.length];
|
|
54636
|
+
}
|
|
54626
54637
|
function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
|
|
54627
54638
|
const s = L3[lang];
|
|
54628
54639
|
const out2 = [];
|
|
@@ -54636,9 +54647,9 @@ function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
|
|
|
54636
54647
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> \u26A0\uFE0F ${s.basisNote}`, "");
|
|
54637
54648
|
const head = [isCore(standard) ? s.criterion : s.criterion, ...derived.map((p) => `${p.name}${p.auth ? " \u{1F512}" : ""}`)];
|
|
54638
54649
|
out2.push(`| ${head.join(" | ")} |`, `| ${head.map(() => "---").join(" | ")} |`);
|
|
54639
|
-
out2.push(`| **${s.rate}** | ${derived.map((p) => `**${formatRate(p.conformancePct, p.decided, p.total)}**`).join(" | ")} |`);
|
|
54640
|
-
out2.push(`| _${s.snapshot}?_ | ${derived.map((p) => `_${basisLabel(p.basis, lang)}_`).join(" | ")} |`);
|
|
54641
54650
|
const { rows, status } = pageGridModel(result, derived, standard, lang);
|
|
54651
|
+
out2.push(`| **${s.rate}** | ${derived.map((p) => `**${formatRate(...gridRate(rows, status, p.id))}**`).join(" | ")} |`);
|
|
54652
|
+
out2.push(`| _${s.snapshot}?_ | ${derived.map((p) => `_${basisLabel(p.basis, lang)}_`).join(" | ")} |`);
|
|
54642
54653
|
let group = "";
|
|
54643
54654
|
for (const row of rows) {
|
|
54644
54655
|
if (row.group !== group) {
|
|
@@ -59669,11 +59680,13 @@ function mergeSnapshotAudit(base, snap) {
|
|
|
59669
59680
|
if (snap.packFindings?.length) merged.packFindings = [...merged.packFindings ?? [], ...snap.packFindings];
|
|
59670
59681
|
for (const c2 of merged.criteria) {
|
|
59671
59682
|
if (!c2.inapplicable) continue;
|
|
59672
|
-
|
|
59673
|
-
|
|
59674
|
-
|
|
59675
|
-
|
|
59676
|
-
|
|
59683
|
+
const measured = snapById.get(c2.id);
|
|
59684
|
+
if (!measured || measured.status !== "C" || measured.inapplicable) continue;
|
|
59685
|
+
c2.status = "C";
|
|
59686
|
+
delete c2.inapplicable;
|
|
59687
|
+
if (measured.justification) c2.justification = measured.justification;
|
|
59688
|
+
else delete c2.justification;
|
|
59689
|
+
if (measured.decidedBy) c2.decidedBy = measured.decidedBy;
|
|
59677
59690
|
}
|
|
59678
59691
|
const nowNc = new Set(snap.findings.filter((f) => !f.advisory).map((f) => f.criteriaId));
|
|
59679
59692
|
merged.residualRisks = merged.residualRisks.filter((r) => !nowNc.has(r.criteriaId));
|
|
@@ -59894,17 +59907,38 @@ function localTestedScs(interact) {
|
|
|
59894
59907
|
}
|
|
59895
59908
|
var PW_SPEC = "@playwright/test";
|
|
59896
59909
|
var AXE_SPEC = "@axe-core/playwright";
|
|
59897
|
-
function
|
|
59898
|
-
|
|
59899
|
-
|
|
59910
|
+
function resolveAnchors(cwd) {
|
|
59911
|
+
return [resolve12(cwd, "package.json"), import.meta.url];
|
|
59912
|
+
}
|
|
59913
|
+
function requireRuntime(cwd, spec) {
|
|
59914
|
+
let last;
|
|
59915
|
+
for (const from of resolveAnchors(cwd)) {
|
|
59900
59916
|
try {
|
|
59901
|
-
|
|
59917
|
+
return createRequire(from)(spec);
|
|
59918
|
+
} catch (e) {
|
|
59919
|
+
last = e;
|
|
59920
|
+
}
|
|
59921
|
+
}
|
|
59922
|
+
throw last instanceof Error ? last : new Error(String(last));
|
|
59923
|
+
}
|
|
59924
|
+
function resolvesRuntime(cwd, spec) {
|
|
59925
|
+
return resolveAnchors(cwd).some((from) => {
|
|
59926
|
+
try {
|
|
59927
|
+
createRequire(from).resolve(spec);
|
|
59928
|
+
return true;
|
|
59902
59929
|
} catch {
|
|
59903
|
-
return
|
|
59930
|
+
return false;
|
|
59931
|
+
}
|
|
59932
|
+
});
|
|
59933
|
+
}
|
|
59934
|
+
function localTierStatus(cwd) {
|
|
59935
|
+
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
59936
|
+
if (!resolvesRuntime(cwd, spec)) {
|
|
59937
|
+
return { ok: false, reason: `${spec} resolves neither from ${cwd} nor from ultra11y itself` };
|
|
59904
59938
|
}
|
|
59905
59939
|
}
|
|
59906
59940
|
try {
|
|
59907
|
-
const pw =
|
|
59941
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59908
59942
|
const bin = pw.chromium?.executablePath?.();
|
|
59909
59943
|
if (typeof bin === "string" && bin.length > 0 && !existsSync24(bin)) {
|
|
59910
59944
|
return { ok: false, reason: `no browser binary at ${bin} \u2014 run \`npx playwright install chromium\`` };
|
|
@@ -59921,15 +59955,14 @@ function resolveLocalDeps(cwd) {
|
|
|
59921
59955
|
let chromium;
|
|
59922
59956
|
let AxeBuilder;
|
|
59923
59957
|
try {
|
|
59924
|
-
const
|
|
59925
|
-
const
|
|
59926
|
-
const axeMod = req(AXE_SPEC);
|
|
59958
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59959
|
+
const axeMod = requireRuntime(cwd, AXE_SPEC);
|
|
59927
59960
|
chromium = pw.chromium;
|
|
59928
59961
|
AxeBuilder = axeMod.default ?? axeMod;
|
|
59929
59962
|
} catch (e) {
|
|
59930
59963
|
const msg = e instanceof Error ? e.message : String(e);
|
|
59931
59964
|
throw new Error(
|
|
59932
|
-
`Playwright
|
|
59965
|
+
`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})`
|
|
59933
59966
|
);
|
|
59934
59967
|
}
|
|
59935
59968
|
if (!chromium || typeof AxeBuilder !== "function") {
|
|
@@ -60996,7 +61029,7 @@ jobs:
|
|
|
60996
61029
|
ultra11y:
|
|
60997
61030
|
runs-on: ubuntu-latest
|
|
60998
61031
|
steps:
|
|
60999
|
-
- uses: actions/checkout@
|
|
61032
|
+
- uses: actions/checkout@v7
|
|
61000
61033
|
with:
|
|
61001
61034
|
fetch-depth: 0 # the diff gate needs the base ref
|
|
61002
61035
|
- uses: maxgfr/ultra11y@v${VERSION}
|
|
@@ -62955,12 +62988,16 @@ function scoreboardBlocks(result, standard, lang, sheetHref) {
|
|
|
62955
62988
|
const scope = pagesOf(result);
|
|
62956
62989
|
if (!scope.length) return [];
|
|
62957
62990
|
const derived = derivePages(result, scope);
|
|
62958
|
-
const rows = derived.map((p) =>
|
|
62959
|
-
|
|
62960
|
-
|
|
62961
|
-
|
|
62962
|
-
|
|
62963
|
-
|
|
62991
|
+
const rows = derived.map((p) => {
|
|
62992
|
+
const criteria = pageCriterionRows(result, p, standard, lang);
|
|
62993
|
+
const cov = pageCoverage(criteria);
|
|
62994
|
+
return [
|
|
62995
|
+
{ text: `${p.name}${p.auth ? " \u{1F512}" : ""}`, ...sheetHref ? { href: sheetHref(p.id) } : {} },
|
|
62996
|
+
{ text: p.url, mono: true },
|
|
62997
|
+
{ text: basisLabel(p.basis, lang) },
|
|
62998
|
+
{ text: formatRate(pageRatePct(criteria), cov.decided, cov.total), align: "end" }
|
|
62999
|
+
];
|
|
63000
|
+
});
|
|
62964
63001
|
const out2 = [
|
|
62965
63002
|
{ kind: "heading", level: 2, text: t3.perPage, id: "pages" },
|
|
62966
63003
|
{
|
|
@@ -67042,6 +67079,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
67042
67079
|
]);
|
|
67043
67080
|
var KNOWN_FLAGS = /* @__PURE__ */ new Set([...VALUE_FLAGS2, ...BOOLEAN_FLAGS]);
|
|
67044
67081
|
var LIST_FLAGS = /* @__PURE__ */ new Set(["include", "exclude", "ext"]);
|
|
67082
|
+
function looksLikeFlag(token) {
|
|
67083
|
+
return token?.startsWith("--") ?? false;
|
|
67084
|
+
}
|
|
67045
67085
|
function parseArgs(argv) {
|
|
67046
67086
|
const [command, ...rest] = argv;
|
|
67047
67087
|
const valueFlags = valueFlagsFor(command ?? "");
|
|
@@ -67056,7 +67096,7 @@ function parseArgs(argv) {
|
|
|
67056
67096
|
const inlineVal = eq === -1 ? void 0 : a.slice(eq + 1);
|
|
67057
67097
|
let val;
|
|
67058
67098
|
if (inlineVal !== void 0) val = inlineVal;
|
|
67059
|
-
else if (valueFlags.has(key)) val = rest[++i2] ?? "";
|
|
67099
|
+
else if (valueFlags.has(key) && !looksLikeFlag(rest[i2 + 1])) val = rest[++i2] ?? "";
|
|
67060
67100
|
else val = true;
|
|
67061
67101
|
const prev = flags2[key];
|
|
67062
67102
|
if (LIST_FLAGS.has(key) && typeof prev === "string" && typeof val === "string") {
|
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.17.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -314,7 +314,7 @@ node scripts/ultra11y.mjs pack check ./packs/section508.json #
|
|
|
314
314
|
```
|
|
315
315
|
Add `--standard rgaa` to `report`/`prd`/`criteria`/`check`/`verify` for a country standard,
|
|
316
316
|
`--pack ./pack.json` to load one at runtime, and `--json` anywhere for machine output.
|
|
317
|
-
`--lang` follows the conversation (pass it explicitly — Core rule
|
|
317
|
+
`--lang` follows the conversation (pass it explicitly — Core rule 7).
|
|
318
318
|
|
|
319
319
|
## The loop: audit → render → judge → fix → re-audit
|
|
320
320
|
|
|
@@ -347,7 +347,7 @@ drive the judgment and content stages:
|
|
|
347
347
|
per NC criterion) and `prd` (the same blocks as an actionable backlog; `tickets`
|
|
348
348
|
filing one GitHub issue per criterion with that identical block) are two views of the
|
|
349
349
|
ONE building block, in the language of this conversation (pass `--lang` explicitly —
|
|
350
|
-
Core rule
|
|
350
|
+
Core rule 7).
|
|
351
351
|
|
|
352
352
|
6. **Hand the change over to `review-a11y`, in a subagent.** Once the deliverable is out,
|
|
353
353
|
the code you just fixed still has to be reviewed as a *change*, and that is the other
|
|
@@ -5,18 +5,28 @@ 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). A project that pins its own Playwright keeps it, because `--cwd` is tried
|
|
11
|
+
first. Adds the residual-criteria **probes** (below).
|
|
11
12
|
- **`--runtime docker`**: runs axe-core in a Docker image auto-built on first use (runner +
|
|
12
13
|
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.
|
|
14
|
+
- **`--runtime auto`** (default): local if Playwright resolves **and** a browser binary is on
|
|
15
|
+
disk, else Docker, else an actionable error.
|
|
15
16
|
|
|
16
17
|
## Prerequisites
|
|
17
18
|
|
|
18
|
-
- **local**: a
|
|
19
|
-
|
|
19
|
+
- **local**: a Chromium browser (`npx playwright install chromium`), plus the two npm packages
|
|
20
|
+
— and **where they have to live depends on how you run the engine**:
|
|
21
|
+
|
|
22
|
+
| Channel | `@playwright/test` + `@axe-core/playwright` |
|
|
23
|
+
|---|---|
|
|
24
|
+
| ultra11y as an npm dependency (`ultra11y/playwright`, `pnpm exec ultra11y`) | **come with it** — they are its own `dependencies`. Nothing to install. |
|
|
25
|
+
| the standalone skill bundle (`node scripts/ultra11y.mjs` from an installed skill) | **must be in the audited project**, reachable from `--cwd`. An installed skill is a directory of files with no `node_modules` beside it, so the second anchor finds nothing there. |
|
|
26
|
+
|
|
27
|
+
Either way, a project that installs them itself wins: `--cwd` is tried first, which is what a
|
|
28
|
+
repository with its own pinned Playwright wants. When neither anchor resolves, `auto` degrades
|
|
29
|
+
to Docker and says which package it could not find.
|
|
20
30
|
- **docker**: Docker running.
|
|
21
31
|
|
|
22
32
|
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.17.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",
|
|
@@ -54623,6 +54623,17 @@ function renderRedirected(redirected, lang = "en") {
|
|
|
54623
54623
|
}
|
|
54624
54624
|
return out2;
|
|
54625
54625
|
}
|
|
54626
|
+
function gridRate(rows, status, pageId) {
|
|
54627
|
+
let c2 = 0;
|
|
54628
|
+
let nc = 0;
|
|
54629
|
+
for (const row of rows) {
|
|
54630
|
+
const st = status.get(row.id)?.get(pageId);
|
|
54631
|
+
if (st === "C") c2++;
|
|
54632
|
+
else if (st === "NC") nc++;
|
|
54633
|
+
}
|
|
54634
|
+
const decided = c2 + nc;
|
|
54635
|
+
return [decided === 0 ? null : Math.round(c2 / decided * 100), decided, rows.length];
|
|
54636
|
+
}
|
|
54626
54637
|
function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
|
|
54627
54638
|
const s = L3[lang];
|
|
54628
54639
|
const out2 = [];
|
|
@@ -54636,9 +54647,9 @@ function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
|
|
|
54636
54647
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> \u26A0\uFE0F ${s.basisNote}`, "");
|
|
54637
54648
|
const head = [isCore(standard) ? s.criterion : s.criterion, ...derived.map((p) => `${p.name}${p.auth ? " \u{1F512}" : ""}`)];
|
|
54638
54649
|
out2.push(`| ${head.join(" | ")} |`, `| ${head.map(() => "---").join(" | ")} |`);
|
|
54639
|
-
out2.push(`| **${s.rate}** | ${derived.map((p) => `**${formatRate(p.conformancePct, p.decided, p.total)}**`).join(" | ")} |`);
|
|
54640
|
-
out2.push(`| _${s.snapshot}?_ | ${derived.map((p) => `_${basisLabel(p.basis, lang)}_`).join(" | ")} |`);
|
|
54641
54650
|
const { rows, status } = pageGridModel(result, derived, standard, lang);
|
|
54651
|
+
out2.push(`| **${s.rate}** | ${derived.map((p) => `**${formatRate(...gridRate(rows, status, p.id))}**`).join(" | ")} |`);
|
|
54652
|
+
out2.push(`| _${s.snapshot}?_ | ${derived.map((p) => `_${basisLabel(p.basis, lang)}_`).join(" | ")} |`);
|
|
54642
54653
|
let group = "";
|
|
54643
54654
|
for (const row of rows) {
|
|
54644
54655
|
if (row.group !== group) {
|
|
@@ -59669,11 +59680,13 @@ function mergeSnapshotAudit(base, snap) {
|
|
|
59669
59680
|
if (snap.packFindings?.length) merged.packFindings = [...merged.packFindings ?? [], ...snap.packFindings];
|
|
59670
59681
|
for (const c2 of merged.criteria) {
|
|
59671
59682
|
if (!c2.inapplicable) continue;
|
|
59672
|
-
|
|
59673
|
-
|
|
59674
|
-
|
|
59675
|
-
|
|
59676
|
-
|
|
59683
|
+
const measured = snapById.get(c2.id);
|
|
59684
|
+
if (!measured || measured.status !== "C" || measured.inapplicable) continue;
|
|
59685
|
+
c2.status = "C";
|
|
59686
|
+
delete c2.inapplicable;
|
|
59687
|
+
if (measured.justification) c2.justification = measured.justification;
|
|
59688
|
+
else delete c2.justification;
|
|
59689
|
+
if (measured.decidedBy) c2.decidedBy = measured.decidedBy;
|
|
59677
59690
|
}
|
|
59678
59691
|
const nowNc = new Set(snap.findings.filter((f) => !f.advisory).map((f) => f.criteriaId));
|
|
59679
59692
|
merged.residualRisks = merged.residualRisks.filter((r) => !nowNc.has(r.criteriaId));
|
|
@@ -59894,17 +59907,38 @@ function localTestedScs(interact) {
|
|
|
59894
59907
|
}
|
|
59895
59908
|
var PW_SPEC = "@playwright/test";
|
|
59896
59909
|
var AXE_SPEC = "@axe-core/playwright";
|
|
59897
|
-
function
|
|
59898
|
-
|
|
59899
|
-
|
|
59910
|
+
function resolveAnchors(cwd) {
|
|
59911
|
+
return [resolve12(cwd, "package.json"), import.meta.url];
|
|
59912
|
+
}
|
|
59913
|
+
function requireRuntime(cwd, spec) {
|
|
59914
|
+
let last;
|
|
59915
|
+
for (const from of resolveAnchors(cwd)) {
|
|
59900
59916
|
try {
|
|
59901
|
-
|
|
59917
|
+
return createRequire(from)(spec);
|
|
59918
|
+
} catch (e) {
|
|
59919
|
+
last = e;
|
|
59920
|
+
}
|
|
59921
|
+
}
|
|
59922
|
+
throw last instanceof Error ? last : new Error(String(last));
|
|
59923
|
+
}
|
|
59924
|
+
function resolvesRuntime(cwd, spec) {
|
|
59925
|
+
return resolveAnchors(cwd).some((from) => {
|
|
59926
|
+
try {
|
|
59927
|
+
createRequire(from).resolve(spec);
|
|
59928
|
+
return true;
|
|
59902
59929
|
} catch {
|
|
59903
|
-
return
|
|
59930
|
+
return false;
|
|
59931
|
+
}
|
|
59932
|
+
});
|
|
59933
|
+
}
|
|
59934
|
+
function localTierStatus(cwd) {
|
|
59935
|
+
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
59936
|
+
if (!resolvesRuntime(cwd, spec)) {
|
|
59937
|
+
return { ok: false, reason: `${spec} resolves neither from ${cwd} nor from ultra11y itself` };
|
|
59904
59938
|
}
|
|
59905
59939
|
}
|
|
59906
59940
|
try {
|
|
59907
|
-
const pw =
|
|
59941
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59908
59942
|
const bin = pw.chromium?.executablePath?.();
|
|
59909
59943
|
if (typeof bin === "string" && bin.length > 0 && !existsSync24(bin)) {
|
|
59910
59944
|
return { ok: false, reason: `no browser binary at ${bin} \u2014 run \`npx playwright install chromium\`` };
|
|
@@ -59921,15 +59955,14 @@ function resolveLocalDeps(cwd) {
|
|
|
59921
59955
|
let chromium;
|
|
59922
59956
|
let AxeBuilder;
|
|
59923
59957
|
try {
|
|
59924
|
-
const
|
|
59925
|
-
const
|
|
59926
|
-
const axeMod = req(AXE_SPEC);
|
|
59958
|
+
const pw = requireRuntime(cwd, PW_SPEC);
|
|
59959
|
+
const axeMod = requireRuntime(cwd, AXE_SPEC);
|
|
59927
59960
|
chromium = pw.chromium;
|
|
59928
59961
|
AxeBuilder = axeMod.default ?? axeMod;
|
|
59929
59962
|
} catch (e) {
|
|
59930
59963
|
const msg = e instanceof Error ? e.message : String(e);
|
|
59931
59964
|
throw new Error(
|
|
59932
|
-
`Playwright
|
|
59965
|
+
`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})`
|
|
59933
59966
|
);
|
|
59934
59967
|
}
|
|
59935
59968
|
if (!chromium || typeof AxeBuilder !== "function") {
|
|
@@ -60996,7 +61029,7 @@ jobs:
|
|
|
60996
61029
|
ultra11y:
|
|
60997
61030
|
runs-on: ubuntu-latest
|
|
60998
61031
|
steps:
|
|
60999
|
-
- uses: actions/checkout@
|
|
61032
|
+
- uses: actions/checkout@v7
|
|
61000
61033
|
with:
|
|
61001
61034
|
fetch-depth: 0 # the diff gate needs the base ref
|
|
61002
61035
|
- uses: maxgfr/ultra11y@v${VERSION}
|
|
@@ -62955,12 +62988,16 @@ function scoreboardBlocks(result, standard, lang, sheetHref) {
|
|
|
62955
62988
|
const scope = pagesOf(result);
|
|
62956
62989
|
if (!scope.length) return [];
|
|
62957
62990
|
const derived = derivePages(result, scope);
|
|
62958
|
-
const rows = derived.map((p) =>
|
|
62959
|
-
|
|
62960
|
-
|
|
62961
|
-
|
|
62962
|
-
|
|
62963
|
-
|
|
62991
|
+
const rows = derived.map((p) => {
|
|
62992
|
+
const criteria = pageCriterionRows(result, p, standard, lang);
|
|
62993
|
+
const cov = pageCoverage(criteria);
|
|
62994
|
+
return [
|
|
62995
|
+
{ text: `${p.name}${p.auth ? " \u{1F512}" : ""}`, ...sheetHref ? { href: sheetHref(p.id) } : {} },
|
|
62996
|
+
{ text: p.url, mono: true },
|
|
62997
|
+
{ text: basisLabel(p.basis, lang) },
|
|
62998
|
+
{ text: formatRate(pageRatePct(criteria), cov.decided, cov.total), align: "end" }
|
|
62999
|
+
];
|
|
63000
|
+
});
|
|
62964
63001
|
const out2 = [
|
|
62965
63002
|
{ kind: "heading", level: 2, text: t3.perPage, id: "pages" },
|
|
62966
63003
|
{
|
|
@@ -67042,6 +67079,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
67042
67079
|
]);
|
|
67043
67080
|
var KNOWN_FLAGS = /* @__PURE__ */ new Set([...VALUE_FLAGS2, ...BOOLEAN_FLAGS]);
|
|
67044
67081
|
var LIST_FLAGS = /* @__PURE__ */ new Set(["include", "exclude", "ext"]);
|
|
67082
|
+
function looksLikeFlag(token) {
|
|
67083
|
+
return token?.startsWith("--") ?? false;
|
|
67084
|
+
}
|
|
67045
67085
|
function parseArgs(argv) {
|
|
67046
67086
|
const [command, ...rest] = argv;
|
|
67047
67087
|
const valueFlags = valueFlagsFor(command ?? "");
|
|
@@ -67056,7 +67096,7 @@ function parseArgs(argv) {
|
|
|
67056
67096
|
const inlineVal = eq === -1 ? void 0 : a.slice(eq + 1);
|
|
67057
67097
|
let val;
|
|
67058
67098
|
if (inlineVal !== void 0) val = inlineVal;
|
|
67059
|
-
else if (valueFlags.has(key)) val = rest[++i2] ?? "";
|
|
67099
|
+
else if (valueFlags.has(key) && !looksLikeFlag(rest[i2 + 1])) val = rest[++i2] ?? "";
|
|
67060
67100
|
else val = true;
|
|
67061
67101
|
const prev = flags2[key];
|
|
67062
67102
|
if (LIST_FLAGS.has(key) && typeof prev === "string" && typeof val === "string") {
|
package/src/cli.ts
CHANGED
|
@@ -691,6 +691,23 @@ export interface ParsedArgs {
|
|
|
691
691
|
unknown: string[];
|
|
692
692
|
}
|
|
693
693
|
|
|
694
|
+
/** Is this token the NEXT FLAG rather than the previous flag's value?
|
|
695
|
+
*
|
|
696
|
+
* Several value flags take an OPTIONAL value — `--ledger` bare means the standard's default
|
|
697
|
+
* path, `--require-decided` bare means the run's grid — and the bare form is almost always
|
|
698
|
+
* followed by another flag. Consumed blindly, the next flag became the value:
|
|
699
|
+
* `verify --apply … --ledger --lang fr` wrote a 33 KB verdict ledger to a file literally
|
|
700
|
+
* named `--lang`, never wrote `.ultra11y/verdicts/rgaa.json`, and — `--lang` having been
|
|
701
|
+
* eaten — printed the run in the wrong language. Nothing failed and nothing warned; the next
|
|
702
|
+
* run simply paid for the whole adjudication again, which is the cost the ledger exists to
|
|
703
|
+
* remove.
|
|
704
|
+
*
|
|
705
|
+
* Only `--` counts. A single dash is the stdin positional, and a value may legitimately be
|
|
706
|
+
* negative or start with one. */
|
|
707
|
+
function looksLikeFlag(token: string | undefined): boolean {
|
|
708
|
+
return token?.startsWith("--") ?? false;
|
|
709
|
+
}
|
|
710
|
+
|
|
694
711
|
export function parseArgs(argv: string[]): ParsedArgs {
|
|
695
712
|
const [command, ...rest] = argv;
|
|
696
713
|
const valueFlags = valueFlagsFor(command ?? "");
|
|
@@ -708,7 +725,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
708
725
|
const inlineVal = eq === -1 ? undefined : a.slice(eq + 1);
|
|
709
726
|
let val: string | boolean;
|
|
710
727
|
if (inlineVal !== undefined) val = inlineVal;
|
|
711
|
-
else if (valueFlags.has(key)) val = rest[++i] ?? "";
|
|
728
|
+
else if (valueFlags.has(key) && !looksLikeFlag(rest[i + 1])) val = rest[++i] ?? "";
|
|
712
729
|
else val = true;
|
|
713
730
|
const prev = flags[key];
|
|
714
731
|
if (LIST_FLAGS.has(key) && typeof prev === "string" && typeof val === "string") {
|
|
@@ -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/html-report.ts
CHANGED
|
@@ -301,12 +301,19 @@ export function scoreboardBlocks(result: AuditResult, standard: StandardId, lang
|
|
|
301
301
|
const scope = pagesOf(result);
|
|
302
302
|
if (!scope.length) return [];
|
|
303
303
|
const derived = derivePages(result, scope);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
304
|
+
// Through the standard-aware helpers, exactly like `renderPageRates` in src/report.ts:
|
|
305
|
+
// `PageResult.conformancePct` is the WCAG core projection, so under a pack this cell used to
|
|
306
|
+
// quote a rate out of 55 beside a document counting 106.
|
|
307
|
+
const rows = derived.map((p) => {
|
|
308
|
+
const criteria = pageCriterionRows(result, p, standard, lang);
|
|
309
|
+
const cov = pageCoverage(criteria);
|
|
310
|
+
return [
|
|
311
|
+
{ text: `${p.name}${p.auth ? " 🔒" : ""}`, ...(sheetHref ? { href: sheetHref(p.id) } : {}) },
|
|
312
|
+
{ text: p.url, mono: true },
|
|
313
|
+
{ text: basisLabel(p.basis, lang) },
|
|
314
|
+
{ text: formatRate(pageRatePct(criteria), cov.decided, cov.total), align: "end" as const },
|
|
315
|
+
];
|
|
316
|
+
});
|
|
310
317
|
const out: Block[] = [
|
|
311
318
|
{ kind: "heading", level: 2, text: t.perPage, id: "pages" },
|
|
312
319
|
{
|
package/src/init.ts
CHANGED
package/src/pages.ts
CHANGED
|
@@ -489,6 +489,25 @@ export function renderRedirected(redirected: ScanRedirect[], lang: Lang = "en"):
|
|
|
489
489
|
return out;
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
+
/** One page's standing over the rows of the grid it is printed on — same C ÷ (C + NC) basis
|
|
493
|
+
* as everywhere else, and the same NULL for "nothing decided". Taking it from the grid model
|
|
494
|
+
* rather than from `PageResult` is what makes the header a summary of the body under a pack
|
|
495
|
+
* as well as under the core: `PageResult` only ever carries the WCAG projection.
|
|
496
|
+
*
|
|
497
|
+
* Returned as the argument tuple `formatRate` takes, so no caller can pair a rate with
|
|
498
|
+
* somebody else's denominator. */
|
|
499
|
+
export function gridRate(rows: { id: string }[], status: Map<string, Map<string, Status>>, pageId: string): [number | null, number, number] {
|
|
500
|
+
let c = 0;
|
|
501
|
+
let nc = 0;
|
|
502
|
+
for (const row of rows) {
|
|
503
|
+
const st = status.get(row.id)?.get(pageId);
|
|
504
|
+
if (st === "C") c++;
|
|
505
|
+
else if (st === "NC") nc++;
|
|
506
|
+
}
|
|
507
|
+
const decided = c + nc;
|
|
508
|
+
return [decided === 0 ? null : Math.round((c / decided) * 100), decided, rows.length];
|
|
509
|
+
}
|
|
510
|
+
|
|
492
511
|
/** The Markdown grid: one row per criterion, one column per page. */
|
|
493
512
|
export function renderPageGrid(result: AuditResult, pages: PageScope[], standard: StandardId = CORE, lang: Lang = "en"): string {
|
|
494
513
|
const s = L[lang];
|
|
@@ -504,10 +523,17 @@ export function renderPageGrid(result: AuditResult, pages: PageScope[], standard
|
|
|
504
523
|
|
|
505
524
|
const head = [isCore(standard) ? s.criterion : s.criterion, ...derived.map((p) => `${p.name}${p.auth ? " 🔒" : ""}`)];
|
|
506
525
|
out.push(`| ${head.join(" | ")} |`, `| ${head.map(() => "---").join(" | ")} |`);
|
|
507
|
-
|
|
526
|
+
// THE MODEL FIRST, because the rate is a summary OF THIS GRID and has to be computed from
|
|
527
|
+
// the rows it is printed above. It was read off `PageResult` instead — which is always the
|
|
528
|
+
// WCAG core projection — so under a pack the header and the body answered different
|
|
529
|
+
// questions: measured on a two-page RGAA run, the grid listed all 106 criteria and then
|
|
530
|
+
// announced « 89 % (9/55) », while the same page's own dossier said 47/106. Same command,
|
|
531
|
+
// same page, two denominators; and this grid is what `comment-kind: pages` pastes onto a
|
|
532
|
+
// pull request, so the wrong one was the one people read.
|
|
533
|
+
const { rows, status } = pageGridModel(result, derived, standard, lang);
|
|
534
|
+
out.push(`| **${s.rate}** | ${derived.map((p) => `**${formatRate(...gridRate(rows, status, p.id))}**`).join(" | ")} |`);
|
|
508
535
|
out.push(`| _${s.snapshot}?_ | ${derived.map((p) => `_${basisLabel(p.basis, lang)}_`).join(" | ")} |`);
|
|
509
536
|
|
|
510
|
-
const { rows, status } = pageGridModel(result, derived, standard, lang);
|
|
511
537
|
let group = "";
|
|
512
538
|
for (const row of rows) {
|
|
513
539
|
if (row.group !== group) {
|
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/scan.ts
CHANGED
|
@@ -741,15 +741,33 @@ export function mergeSnapshotAudit(base: AuditResult, snap: AuditResult): AuditR
|
|
|
741
741
|
if (snap.packFindings?.length) merged.packFindings = [...(merged.packFindings ?? []), ...snap.packFindings];
|
|
742
742
|
|
|
743
743
|
// A criterion the static pass closed for want of a subject, which the snapshot then MEASURED.
|
|
744
|
-
// Both read `C`, so the discriminator is the
|
|
745
|
-
//
|
|
744
|
+
// Both read `C`, so the discriminator is the FLAG, not the status — and testing the status
|
|
745
|
+
// is exactly what this loop used to do. The snapshot pages have no time-based media either,
|
|
746
|
+
// so they closed 1.2.1 the same way the source did, `C` + `inapplicable`; reading only the
|
|
747
|
+
// status, the merge took that for a measurement and stripped the flag from every criterion
|
|
748
|
+
// both halves had closed for want of a subject.
|
|
749
|
+
//
|
|
750
|
+
// Two things then broke, both silently. The `C` lost the justification that made it
|
|
751
|
+
// falsifiable, becoming the bare uncited conformity this engine refuses everywhere else. And
|
|
752
|
+
// `pageStatus` reads `inapplicable` to hold "a conformity reached for want of a subject
|
|
753
|
+
// holds on every page" — without the flag those criteria fell through to the honesty rule
|
|
754
|
+
// that only `static` criteria earn a verdict by silence, and came back « à évaluer » on
|
|
755
|
+
// EVERY page. Measured on a two-page RGAA crawl: the run reported a complete 106/106 grid
|
|
756
|
+
// while each page carried 10 criteria nobody could ever adjudicate, because there was
|
|
757
|
+
// nothing there to adjudicate.
|
|
746
758
|
for (const c of merged.criteria) {
|
|
747
759
|
if (!c.inapplicable) continue;
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
760
|
+
const measured = snapById.get(c.id);
|
|
761
|
+
// Only a snapshot that reached `C` WITHOUT the flag has falsified "nothing of that kind
|
|
762
|
+
// here" — that one looked at the rendered page and found the subject.
|
|
763
|
+
if (!measured || measured.status !== "C" || measured.inapplicable) continue;
|
|
764
|
+
c.status = "C";
|
|
765
|
+
delete c.inapplicable;
|
|
766
|
+
// Keep the claim citable: adopt what the measuring half said, rather than leaving a `C`
|
|
767
|
+
// with nothing behind it.
|
|
768
|
+
if (measured.justification) c.justification = measured.justification;
|
|
769
|
+
else delete c.justification;
|
|
770
|
+
if (measured.decidedBy) c.decidedBy = measured.decidedBy;
|
|
753
771
|
}
|
|
754
772
|
|
|
755
773
|
const nowNc = new Set(snap.findings.filter((f) => !f.advisory).map((f) => f.criteriaId));
|
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.17.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,
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
interface FindingLike {
|
|
2
|
-
ruleId: string;
|
|
3
|
-
criteriaId: string;
|
|
4
|
-
file: string;
|
|
5
|
-
line: number;
|
|
6
|
-
severity: string;
|
|
7
|
-
message: string;
|
|
8
|
-
advisory?: boolean;
|
|
9
|
-
origin?: {
|
|
10
|
-
sourceFile?: string;
|
|
11
|
-
sourceLine?: number;
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
interface AuditLike {
|
|
15
|
-
findings?: FindingLike[];
|
|
16
|
-
}
|
|
17
|
-
interface SnapshotPayload {
|
|
18
|
-
meta: Record<string, unknown>;
|
|
19
|
-
dom: string;
|
|
20
|
-
styles?: unknown;
|
|
21
|
-
boxes?: unknown;
|
|
22
|
-
css?: unknown;
|
|
23
|
-
screenshot?: string;
|
|
24
|
-
/** What the live probes measured, when the caller ran them. */
|
|
25
|
-
probes?: unknown;
|
|
26
|
-
}
|
|
27
|
-
/** Same slug rule as the engine (src/snapshot.ts slugifyPageId): accent-folded URL path. */
|
|
28
|
-
declare function slugify(url: string): string;
|
|
29
|
-
interface CheckOptions {
|
|
30
|
-
/** The page id (directory name). Defaults to the slugified URL path. */
|
|
31
|
-
as?: string;
|
|
32
|
-
/** Human page name for the report. Defaults to the document title. */
|
|
33
|
-
name?: string;
|
|
34
|
-
/** Severity at which the check fails, or `false` to record without ever failing. */
|
|
35
|
-
failOn?: "blocking" | "major" | "minor" | false;
|
|
36
|
-
/** Mark the page as sitting behind authentication. */
|
|
37
|
-
auth?: boolean;
|
|
38
|
-
/** Source files that rendered it — findings are then reported against YOUR code. */
|
|
39
|
-
sources?: string[];
|
|
40
|
-
/** Reproduction notes carried into the auditor ticket. */
|
|
41
|
-
notes?: string;
|
|
42
|
-
/** Capture a viewport screenshot for the pixel tier (Playwright: on by default). */
|
|
43
|
-
screenshot?: boolean;
|
|
44
|
-
/** The path you NAVIGATED to. Given it, the fixture refuses to record a page the browser
|
|
45
|
-
* did not stay on — a guarded route that redirected, a session that expired — instead of
|
|
46
|
-
* filing that other screen under this page's `as`/`name`. Nothing about such a snapshot
|
|
47
|
-
* looks wrong, which is what makes it the worst thing an accessibility report can carry.
|
|
48
|
-
* Compared by path (a query or fragment the app appends to its own route is the same
|
|
49
|
-
* page); a full URL works too. `scan --sample` applies the same rule. */
|
|
50
|
-
expectPath?: string;
|
|
51
|
-
/** Run the LIVE PROBES on this page after collecting it: 200% zoom, 320px reflow, the
|
|
52
|
-
* text-spacing override, focus visibility and content-on-hover.
|
|
53
|
-
*
|
|
54
|
-
* These are the measurements a recorded snapshot can never settle, because they are
|
|
55
|
-
* properties of a page being acted on rather than of a page as it stands. `scan` runs them
|
|
56
|
-
* in its own browser, which is exactly why they were undecidable on any screen behind a
|
|
57
|
-
* login or a state machine — the scanner arrives cold and gets a redirect. Your suite has
|
|
58
|
-
* the page in the right state; this is how it says so.
|
|
59
|
-
*
|
|
60
|
-
* Off by default: the probes press Tab, hover, resize the viewport and inject a stylesheet,
|
|
61
|
-
* and a suite should opt into that. Everything is restored before it returns, and the
|
|
62
|
-
* snapshot has already been collected, so what they measure cannot alter what was recorded.
|
|
63
|
-
* Pass an array to run only some of them (by criterion: "1.4.4", "1.4.10", "1.4.12",
|
|
64
|
-
* "1.4.13", "2.4.7"), or an object to also set the bounds — how wide the reflow probe
|
|
65
|
-
* narrows to, how many focusables are worth tabbing through, how many hits are worth
|
|
66
|
-
* recording. 320px is normative and stays the default; the other two are judgements about
|
|
67
|
-
* YOUR pages, and a screen with 400 focusables should be able to say so rather than being
|
|
68
|
-
* quietly cut off at 120. */
|
|
69
|
-
probes?: boolean | string[] | ProbeTuning;
|
|
70
|
-
}
|
|
71
|
-
/** The bounds a repository may put on the live probes. Stated once, here, because the same
|
|
72
|
-
* list is read from `.ultra11yrc.json`, accepted at the call site and handed to the engine —
|
|
73
|
-
* three copies of a field list is three chances for one of them to fall behind.
|
|
74
|
-
*
|
|
75
|
-
* `budgetMs` and `actionTimeoutMs` are not tuning in the same sense as the others: they are
|
|
76
|
-
* what stops a probe from spending SOMEBODY ELSE'S test timeout. See ProbeLimits. */
|
|
77
|
-
interface ProbeTuning {
|
|
78
|
-
only?: string[];
|
|
79
|
-
reflowWidth?: number;
|
|
80
|
-
maxFocusables?: number;
|
|
81
|
-
maxHits?: number;
|
|
82
|
-
maxTriggers?: number;
|
|
83
|
-
actionTimeoutMs?: number;
|
|
84
|
-
budgetMs?: number;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export { type AuditLike as A, type CheckOptions as C, type FindingLike as F, type SnapshotPayload as S, slugify as s };
|