ultra11y 5.35.0 → 5.35.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +194 -49
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +194 -49
- package/skills/ultra11y/SKILL.md +9 -1
- package/skills/ultra11y/references/ci.md +13 -4
- package/skills/ultra11y/references/methodology.md +11 -0
- package/skills/ultra11y/references/runbook.md +6 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +194 -49
- package/src/adjudicate-subjects.ts +96 -2
- package/src/adjudicate.ts +6 -0
- package/src/annotate.ts +53 -40
- package/src/audit.ts +16 -1
- package/src/cli.ts +2 -6
- package/src/llm.ts +49 -8
- package/src/standards/derive.ts +29 -2
- package/src/types.ts +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "ultra11y",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
|
|
13
|
-
"version": "5.35.
|
|
13
|
+
"version": "5.35.2",
|
|
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.35.
|
|
4
|
+
"version": "5.35.2",
|
|
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.35.
|
|
4
|
+
"version": "5.35.2",
|
|
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.35.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.35.2";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.35.
|
|
3
|
+
"version": "5.35.2",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join as join53, 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.35.
|
|
9
|
+
var VERSION = "5.35.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -44713,10 +44713,18 @@ var PRESENTATIONAL_MARKUP_ATTRS = /* @__PURE__ */ new Set([
|
|
|
44713
44713
|
]);
|
|
44714
44714
|
var DOWNLOAD_HREF = /\.(pdf|docx?|xlsx?)(?:[?#]|$)/i;
|
|
44715
44715
|
var STATUS_CLASS = /(error|status|message|alert|notif|toast|feedback|live)/i;
|
|
44716
|
+
var TEMPORAL_MEDIA_VALUE = /(?:^|[/.?=&_-])(?:audio|video|player|podcast|mp[34]|webm|ogg|ogv|wav|m4a|mov|avi|mpeg)(?:$|[/.?=&_-])/i;
|
|
44716
44717
|
var ROUTER_IMPORT = /['"](?:react-router(?:-dom)?|next\/(?:router|navigation)|vue-router|@remix-run\/[\w-]+|@tanstack\/[\w-]*router|@sveltejs\/kit|\$app\/(?:navigation|stores))['"]/;
|
|
44717
44718
|
var FOREIGN_LEXICON = /\b(?:the|and|with|your|about|please|download|settings|welcome|dashboard|overview|read more|learn more|sign in|sign up|log in|get started|coming soon|powered by)\b/i;
|
|
44718
44719
|
var CONTROL_TAGS = ["input", "select", "textarea"];
|
|
44719
44720
|
var INTERACTIVE_TAGS = ["a", "button", "input", "select", "textarea", "summary", "details", "label"];
|
|
44721
|
+
function embeddedMediaKind(el) {
|
|
44722
|
+
const type = (attr(el, "type") ?? "").toLowerCase();
|
|
44723
|
+
const resource = `${attr(el, "src") ?? ""} ${attr(el, "data") ?? ""}`.toLowerCase();
|
|
44724
|
+
if (/^(?:audio|video)\//.test(type) || TEMPORAL_MEDIA_VALUE.test(`${type} ${resource}`)) return "temporal";
|
|
44725
|
+
if (/^image\//.test(type) || /\.(?:svg|png|jpe?g|webp|avif)(?:[?#]|$)/i.test(resource)) return "non-temporal";
|
|
44726
|
+
return "unknown";
|
|
44727
|
+
}
|
|
44720
44728
|
function labelFor(doc, el) {
|
|
44721
44729
|
const id = attr(el, "id");
|
|
44722
44730
|
const lbl = id ? elementsByTag(doc, "label").find((l) => attr(l, "for") === id) : void 0;
|
|
@@ -45099,6 +45107,66 @@ var SUBJECTS = {
|
|
|
45099
45107
|
(e) => h(d, e, `<${e.tag} autoplay=${attr(e, "autoplay") !== void 0} controls=${attr(e, "controls") !== void 0}>`, `media|${e.tag}`)
|
|
45100
45108
|
)
|
|
45101
45109
|
]),
|
|
45110
|
+
// RGAA 4.11 — temporal players and the code that controls their consultation. This must
|
|
45111
|
+
// not inherit WCAG 2.1.1/2.1.2's page-wide union of pointer handlers and focusables: an
|
|
45112
|
+
// unrelated clickable card says nothing about whether an <audio> can be paused.
|
|
45113
|
+
temporalMediaControls: (docs) => docs.flatMap((d) => [
|
|
45114
|
+
...d.elements.filter((e) => e.tag === "audio" || e.tag === "video" || ["object", "embed"].includes(e.tag) && embeddedMediaKind(e) !== "non-temporal").map(
|
|
45115
|
+
(e) => h(
|
|
45116
|
+
d,
|
|
45117
|
+
e,
|
|
45118
|
+
`<${e.tag}> temporal media \u2014 controls=${attr(e, "controls") !== void 0}, autoplay=${attr(e, "autoplay") !== void 0}; can play/pause/stop and sound be operated by keyboard and pointer?`,
|
|
45119
|
+
`temporal-media|${e.tag}|${attr(e, "src") ?? attr(e, "data") ?? ""}|controls=${attr(e, "controls") !== void 0}`
|
|
45120
|
+
)
|
|
45121
|
+
),
|
|
45122
|
+
...linesOf(d, /\.(?:play|pause)\s*\(|\b(?:currentTime|muted|volume|playbackRate)\s*=|\bHTMLMediaElement\b/).map(
|
|
45123
|
+
(l) => hAt(d, l.line, "media-control", `custom temporal-media control: ${l.text}`, `temporal-control|${l.text}`)
|
|
45124
|
+
)
|
|
45125
|
+
]),
|
|
45126
|
+
// RGAA 4.12 — non-temporal plug-in/SVG/canvas media. Static instances are included too:
|
|
45127
|
+
// the adjudicator must first establish whether they expose consultation controls before it
|
|
45128
|
+
// can legitimately answer NA. Temporal players and generic event handlers stay out.
|
|
45129
|
+
nonTemporalMediaControls: (docs) => docs.flatMap((d) => [
|
|
45130
|
+
...d.elements.filter((e) => e.tag === "svg" || e.tag === "canvas" || ["object", "embed"].includes(e.tag) && embeddedMediaKind(e) !== "temporal").map(
|
|
45131
|
+
(e) => h(
|
|
45132
|
+
d,
|
|
45133
|
+
e,
|
|
45134
|
+
`<${e.tag}> non-temporal media \u2014 inspect any pan/zoom/navigation/control function for keyboard and pointer operation`,
|
|
45135
|
+
`non-temporal-media|${e.tag}|${attr(e, "src") ?? attr(e, "data") ?? attr(e, "id") ?? ""}`
|
|
45136
|
+
)
|
|
45137
|
+
),
|
|
45138
|
+
...linesOf(d, /\b(?:svgPanZoom|panzoom|zoomIn|zoomOut|getContext)\b/i).map(
|
|
45139
|
+
(l) => hAt(d, l.line, "media-control", `non-temporal media/control code: ${l.text}`, `non-temporal-control|${l.text}`)
|
|
45140
|
+
)
|
|
45141
|
+
]),
|
|
45142
|
+
// WCAG 2.3.1 / RGAA 13.7 — only things capable of a luminance flash. The old `motion`
|
|
45143
|
+
// subject mixed autoplay audio and <marquee> into this criterion while omitting ordinary
|
|
45144
|
+
// images, canvas, scripts and stylesheet animation rules. That gave the adjudicator lots of
|
|
45145
|
+
// evidence and none of the evidence the three-flash test actually asks for.
|
|
45146
|
+
flashes: (docs) => docs.flatMap((d) => [
|
|
45147
|
+
...d.elements.filter((e) => ["video", "img", "svg", "canvas", "embed", "object", "blink"].includes(e.tag)).map(
|
|
45148
|
+
(e) => h(
|
|
45149
|
+
d,
|
|
45150
|
+
e,
|
|
45151
|
+
`<${e.tag}> visual/animated content \u2014 does it flash, and if so is it at most 3 flashes/s or below the area threshold?`,
|
|
45152
|
+
`flash-media|${e.tag}|${attr(e, "src") ?? attr(e, "data") ?? attr(e, "id") ?? ""}`
|
|
45153
|
+
)
|
|
45154
|
+
),
|
|
45155
|
+
...elementsByTag(d, "script").map(
|
|
45156
|
+
(e) => h(d, e, `<script> \u2014 inspect for luminance/colour states toggled rapidly`, `flash-script|${attr(e, "src") ?? t(e, 80)}`)
|
|
45157
|
+
),
|
|
45158
|
+
...d.elements.filter((e) => /animation|transition/i.test(attr(e, "style") ?? "")).map((e) => h(d, e, `inline visual animation: ${(attr(e, "style") ?? "").slice(0, 100)}`, `flash-inline|${attr(e, "style") ?? ""}`)),
|
|
45159
|
+
...(d.signals?.css?.rules ?? []).filter((r) => r.decls.animation !== void 0 || r.decls.animationName !== void 0).map(
|
|
45160
|
+
(r) => hAt(
|
|
45161
|
+
d,
|
|
45162
|
+
1,
|
|
45163
|
+
`css:${r.selector}`,
|
|
45164
|
+
`stylesheet animation: ${r.selector.slice(0, 60)} { ${declsText(r.decls)} } \u2014 inspect keyframes for rapid opposing luminance changes`,
|
|
45165
|
+
`flash-css|${r.selector}|${declsText(r.decls)}`
|
|
45166
|
+
)
|
|
45167
|
+
),
|
|
45168
|
+
...linesOf(d, /\b(?:flash|flashing|strobe|setInterval|requestAnimationFrame|classList\.(?:add|remove|replace|toggle))\b|animation(?:-name)?\s*:/i).filter((l) => !/^\s*(?:<!--|\/\/|\/\*)/.test(l.text)).map((l) => hAt(d, l.line, "flash-code", `possible scripted/CSS flash mechanism: ${l.text}`, `flash-code|${l.text}`))
|
|
45169
|
+
]),
|
|
45102
45170
|
// A change of context initiated by a script. RGAA 7.4 / WCAG 3.2.1-3.2.2.
|
|
45103
45171
|
//
|
|
45104
45172
|
// THE SUBJECT IS THE SCRIPT, and getting that wrong cost a release. This used to be a list of
|
|
@@ -45358,7 +45426,7 @@ var SC_SUBJECTS = {
|
|
|
45358
45426
|
"2.1.4": ["shortcuts"],
|
|
45359
45427
|
"2.2.1": ["timers"],
|
|
45360
45428
|
"2.2.2": ["motion", "timers"],
|
|
45361
|
-
"2.3.1": ["
|
|
45429
|
+
"2.3.1": ["flashes"],
|
|
45362
45430
|
"2.4.1": ["landmarks"],
|
|
45363
45431
|
"2.4.3": ["focusOrder"],
|
|
45364
45432
|
"2.4.4": ["links"],
|
|
@@ -45451,6 +45519,10 @@ var PACK_SUBJECTS = {
|
|
|
45451
45519
|
// WCAG 1.4.2, which is `static` and therefore has no subject of its own — but the pack
|
|
45452
45520
|
// flags 4.10 `judgment`, so judgmentGuard reopens it and it would arrive with nothing.
|
|
45453
45521
|
"4.10": ["motion"],
|
|
45522
|
+
// 4.11/4.12 map to the same keyboard SCs but ask about disjoint media populations.
|
|
45523
|
+
// Inheriting the union handed both criteria every pointer handler and focusable in scope.
|
|
45524
|
+
"4.11": ["temporalMediaControls"],
|
|
45525
|
+
"4.12": ["nonTemporalMediaControls"],
|
|
45454
45526
|
// Theme 3 — colour. The question is about what colour alone conveys, not the outline.
|
|
45455
45527
|
"3.1": ["colourPairs", "links"],
|
|
45456
45528
|
"3.2": ["colourPairs"],
|
|
@@ -45487,7 +45559,7 @@ var PACK_SUBJECTS = {
|
|
|
45487
45559
|
"13.2": ["newWindow"],
|
|
45488
45560
|
"13.3": ["downloadDocs"],
|
|
45489
45561
|
"13.4": ["downloadDocs"],
|
|
45490
|
-
"13.7": ["
|
|
45562
|
+
"13.7": ["flashes"],
|
|
45491
45563
|
"13.8": ["motion", "timers"],
|
|
45492
45564
|
"13.9": ["readingOrder"],
|
|
45493
45565
|
"13.10": ["pointerHandlers"],
|
|
@@ -49414,7 +49486,7 @@ var SUBJECT_MATTER = {
|
|
|
49414
49486
|
"2.1.4": hasCharShortcut,
|
|
49415
49487
|
"2.2.1": hasTimeLimit,
|
|
49416
49488
|
"2.2.2": hasMovingContent,
|
|
49417
|
-
"2.3.1":
|
|
49489
|
+
"2.3.1": hasFlashCandidate,
|
|
49418
49490
|
"2.5.1": hasGesture,
|
|
49419
49491
|
"2.5.2": hasDownEventAction,
|
|
49420
49492
|
"2.5.7": hasDrag
|
|
@@ -49459,6 +49531,14 @@ function hasMovingContent(d) {
|
|
|
49459
49531
|
if ((d.signals?.css?.rules ?? []).some((r) => r.decls.animation !== void 0 || r.decls.animationName !== void 0)) return true;
|
|
49460
49532
|
return /\b(?:carousel|autoplay|requestAnimationFrame|animation-iteration-count|\.gif["']|\bswiper\b)/i.test(d.source);
|
|
49461
49533
|
}
|
|
49534
|
+
function hasFlashCandidate(d) {
|
|
49535
|
+
if (has(d, "video", "img", "svg", "canvas", "embed", "object", "blink", "script")) return true;
|
|
49536
|
+
if (d.elements.some((e) => /animation|transition/i.test(e.attribs.style ?? ""))) return true;
|
|
49537
|
+
if ((d.signals?.css?.rules ?? []).some((r) => r.decls.animation !== void 0 || r.decls.animationName !== void 0)) return true;
|
|
49538
|
+
return /\b(?:flash|flashing|strobe|blink|setInterval|requestAnimationFrame|classList\.(?:add|remove|replace|toggle))\b|animation(?:-name)?\s*:/i.test(
|
|
49539
|
+
d.source
|
|
49540
|
+
);
|
|
49541
|
+
}
|
|
49462
49542
|
function hasFormControl(d) {
|
|
49463
49543
|
if (has(d, "input", "select", "textarea", "form", "fieldset", "output", "datalist")) return true;
|
|
49464
49544
|
return d.elements.some((e) => {
|
|
@@ -54611,8 +54691,24 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54611
54691
|
}
|
|
54612
54692
|
function findingsForStandard(audit2, standard) {
|
|
54613
54693
|
if (standard === CORE_KEY) return audit2.findings;
|
|
54614
|
-
const
|
|
54615
|
-
|
|
54694
|
+
const eligible = new Set(
|
|
54695
|
+
[
|
|
54696
|
+
...audit2.findings,
|
|
54697
|
+
...(audit2.packFindings ?? []).filter((finding) => finding.ruleId.startsWith(`pack:${standard}:`)),
|
|
54698
|
+
...audit2.packAdjudication?.standard === standard ? audit2.packAdjudication.criteria.flatMap((criterion) => criterion.findings) : []
|
|
54699
|
+
].map(findingId)
|
|
54700
|
+
);
|
|
54701
|
+
const seen = /* @__PURE__ */ new Set();
|
|
54702
|
+
const out2 = [];
|
|
54703
|
+
for (const criterion of derivePackResults(audit2, standard)) {
|
|
54704
|
+
for (const finding of criterion.findings) {
|
|
54705
|
+
const id = findingId(finding);
|
|
54706
|
+
if (!eligible.has(id) || seen.has(id)) continue;
|
|
54707
|
+
seen.add(id);
|
|
54708
|
+
out2.push(finding);
|
|
54709
|
+
}
|
|
54710
|
+
}
|
|
54711
|
+
return out2;
|
|
54616
54712
|
}
|
|
54617
54713
|
|
|
54618
54714
|
// src/standards/vocabulary.ts
|
|
@@ -59499,6 +59595,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59499
59595
|
};
|
|
59500
59596
|
const uncovered = (criteriaId) => notFolded.add(criteriaId);
|
|
59501
59597
|
canonicalizeAdjudication(adj);
|
|
59598
|
+
const itemCounts = /* @__PURE__ */ new Map();
|
|
59599
|
+
for (const item of adj.items) itemCounts.set(item.criteriaId, (itemCounts.get(item.criteriaId) ?? 0) + 1);
|
|
59600
|
+
for (const [criteriaId, count] of itemCounts) {
|
|
59601
|
+
if (count > 1)
|
|
59602
|
+
blame(criteriaId, `criterion ${criteriaId}: duplicate criterion id appears ${count} times in the adjudication \u2014 no implicit winner is accepted`);
|
|
59603
|
+
}
|
|
59502
59604
|
const byId2 = new Map(adj.items.map((it) => [it.criteriaId, it]));
|
|
59503
59605
|
const packMode = !isCore(adj.standard);
|
|
59504
59606
|
const open = /* @__PURE__ */ new Set();
|
|
@@ -60415,6 +60517,20 @@ var BATCH_SIZE = 8;
|
|
|
60415
60517
|
var CONCURRENCY = 4;
|
|
60416
60518
|
var MAX_ATTEMPTS = 4;
|
|
60417
60519
|
var MAX_TOKENS = 16e3;
|
|
60520
|
+
function batchWorklist(items, render2, size = BATCH_SIZE) {
|
|
60521
|
+
if (!Number.isInteger(size) || size < 1) throw new Error(`batch size must be a positive integer (got ${size})`);
|
|
60522
|
+
const ids = items.map((item) => item.criteriaId);
|
|
60523
|
+
if (new Set(ids).size !== ids.length) {
|
|
60524
|
+
const duplicate = [...new Set(ids.filter((id, index) => ids.indexOf(id) !== index))];
|
|
60525
|
+
throw new Error(`worklist contains duplicate criterion id(s): ${duplicate.join(", ")}`);
|
|
60526
|
+
}
|
|
60527
|
+
const batches = [];
|
|
60528
|
+
for (let i2 = 0; i2 < items.length; i2 += size) {
|
|
60529
|
+
const slice = items.slice(i2, i2 + size);
|
|
60530
|
+
batches.push({ items: slice, prompt: render2(slice) });
|
|
60531
|
+
}
|
|
60532
|
+
return batches;
|
|
60533
|
+
}
|
|
60418
60534
|
function apiKeyFromEnv() {
|
|
60419
60535
|
const k = process.env.ANTHROPIC_API_KEY?.trim();
|
|
60420
60536
|
return k || void 0;
|
|
@@ -60536,7 +60652,7 @@ function verdictsOf(res) {
|
|
|
60536
60652
|
if (!Array.isArray(input?.verdicts)) throw new Error("ultra11y judge: the model's tool call carried no verdicts array.");
|
|
60537
60653
|
return input.verdicts;
|
|
60538
60654
|
}
|
|
60539
|
-
async function judgeBatch(
|
|
60655
|
+
async function judgeBatch(_items, prompt, opts) {
|
|
60540
60656
|
if (!opts.apiKey) throw new Error("ultra11y judge: the Messages backend needs an API key (ANTHROPIC_API_KEY or --api-key).");
|
|
60541
60657
|
const res = await callOnce(
|
|
60542
60658
|
{
|
|
@@ -60549,8 +60665,22 @@ async function judgeBatch(items, prompt, opts) {
|
|
|
60549
60665
|
},
|
|
60550
60666
|
opts
|
|
60551
60667
|
);
|
|
60552
|
-
|
|
60553
|
-
|
|
60668
|
+
return verdictsOf(res);
|
|
60669
|
+
}
|
|
60670
|
+
function validateBatchVerdicts(items, landed) {
|
|
60671
|
+
const expected = new Set(items.map((item) => item.criteriaId));
|
|
60672
|
+
const counts = /* @__PURE__ */ new Map();
|
|
60673
|
+
for (const verdict of landed) counts.set(verdict.criteriaId, (counts.get(verdict.criteriaId) ?? 0) + 1);
|
|
60674
|
+
const duplicate = new Set([...counts].filter(([, count]) => count > 1).map(([id]) => id));
|
|
60675
|
+
const unknown = [...new Set(landed.map((v) => v.criteriaId).filter((id) => !expected.has(id)))];
|
|
60676
|
+
const accepted = landed.filter((verdict) => expected.has(verdict.criteriaId) && !duplicate.has(verdict.criteriaId));
|
|
60677
|
+
const acceptedIds = new Set(accepted.map((verdict) => verdict.criteriaId));
|
|
60678
|
+
const missing = [...expected].filter((id) => !acceptedIds.has(id) && !duplicate.has(id));
|
|
60679
|
+
const failures = [];
|
|
60680
|
+
if (duplicate.size) failures.push(`duplicate criterion id(s) in batch response: ${[...duplicate].join(", ")}`);
|
|
60681
|
+
if (unknown.length) failures.push(`unknown criterion id(s) in batch response: ${unknown.join(", ")}`);
|
|
60682
|
+
if (missing.length) failures.push(`missing criterion id(s) in batch response: ${missing.join(", ")}`);
|
|
60683
|
+
return { accepted, failures };
|
|
60554
60684
|
}
|
|
60555
60685
|
async function judgeAll(batches, opts) {
|
|
60556
60686
|
const verdicts = [];
|
|
@@ -60564,8 +60694,10 @@ async function judgeAll(batches, opts) {
|
|
|
60564
60694
|
for (let b = queue.shift(); b !== void 0; b = queue.shift()) {
|
|
60565
60695
|
try {
|
|
60566
60696
|
const landed = await backend(b.items, b.prompt, opts);
|
|
60567
|
-
|
|
60568
|
-
|
|
60697
|
+
const checked = validateBatchVerdicts(b.items, landed);
|
|
60698
|
+
failures.push(...checked.failures);
|
|
60699
|
+
verdicts.push(...checked.accepted);
|
|
60700
|
+
if (checked.accepted.length) opts.onVerdicts?.(checked.accepted);
|
|
60569
60701
|
} catch (e) {
|
|
60570
60702
|
failures.push(e instanceof Error ? e.message : String(e));
|
|
60571
60703
|
}
|
|
@@ -60576,7 +60708,9 @@ async function judgeAll(batches, opts) {
|
|
|
60576
60708
|
return { verdicts, failures };
|
|
60577
60709
|
}
|
|
60578
60710
|
function applyRawVerdicts(items, verdicts) {
|
|
60579
|
-
const
|
|
60711
|
+
const counts = /* @__PURE__ */ new Map();
|
|
60712
|
+
for (const verdict of verdicts) counts.set(verdict.criteriaId, (counts.get(verdict.criteriaId) ?? 0) + 1);
|
|
60713
|
+
const byId2 = new Map(verdicts.filter((v) => counts.get(v.criteriaId) === 1).map((v) => [v.criteriaId, v]));
|
|
60580
60714
|
let filled = 0;
|
|
60581
60715
|
for (const item of items) {
|
|
60582
60716
|
const v = byId2.get(item.criteriaId);
|
|
@@ -64249,7 +64383,9 @@ var S = {
|
|
|
64249
64383
|
fr: {
|
|
64250
64384
|
title: "Audit d'accessibilit\xE9 ultra11y",
|
|
64251
64385
|
files: "fichiers",
|
|
64252
|
-
|
|
64386
|
+
coverage: (decided, total) => `${decided}/${total} crit\xE8res tranch\xE9s dans ce run`,
|
|
64387
|
+
remaining: (n) => `${n} \xE0 compl\xE9ter par scan ou adjudication`,
|
|
64388
|
+
provenance: { engine: "moteur", scan: "scan", agent: "agent" },
|
|
64253
64389
|
none: "\u2705 Aucune non-conformit\xE9 d\xE9tect\xE9e par le moteur statique.",
|
|
64254
64390
|
findings: "Non-conformit\xE9s",
|
|
64255
64391
|
severity: "S\xE9v\xE9rit\xE9",
|
|
@@ -64317,7 +64453,9 @@ var S = {
|
|
|
64317
64453
|
en: {
|
|
64318
64454
|
title: "ultra11y accessibility audit",
|
|
64319
64455
|
files: "files",
|
|
64320
|
-
|
|
64456
|
+
coverage: (decided, total) => `${decided}/${total} criteria decided in this run`,
|
|
64457
|
+
remaining: (n) => `${n} still to complete by scan or adjudication`,
|
|
64458
|
+
provenance: { engine: "engine", scan: "scan", agent: "agent" },
|
|
64321
64459
|
none: "\u2705 No non-conformity detected by the static engine.",
|
|
64322
64460
|
findings: "Non-conformities",
|
|
64323
64461
|
severity: "Severity",
|
|
@@ -64439,11 +64577,24 @@ function groupByCriterion(groups) {
|
|
|
64439
64577
|
}
|
|
64440
64578
|
return [...byCriterion.values()].map(({ pageSet, ...c2 }) => ({ ...c2, pages: pageSet.size })).sort((a, b) => SEV_ORDER4.indexOf(a.severity) - SEV_ORDER4.indexOf(b.severity) || b.occurrences - a.occurrences || a.criterion.localeCompare(b.criterion));
|
|
64441
64579
|
}
|
|
64442
|
-
function
|
|
64580
|
+
function runCoverage(result, standard, lang) {
|
|
64443
64581
|
const groups = isCore(standard) ? reportGroups(result, lang) : packReportGroups(result, loadPack(standard), lang);
|
|
64444
64582
|
const { decided, total } = reportCoverage(groups);
|
|
64583
|
+
const decidedRows = groups.flatMap((g) => g.rows).filter((r) => r.status === "C" || r.status === "NC");
|
|
64584
|
+
const by = {
|
|
64585
|
+
engine: decidedRows.filter((r) => !r.decidedBy || r.decidedBy === "engine").length,
|
|
64586
|
+
scan: decidedRows.filter((r) => r.decidedBy === "scan").length,
|
|
64587
|
+
agent: decidedRows.filter((r) => r.decidedBy === "agent").length
|
|
64588
|
+
};
|
|
64589
|
+
const s = S[lang];
|
|
64590
|
+
const provenance = ["engine", "scan", "agent"].filter((key2) => by[key2] > 0).map((key2) => `${by[key2]} ${s.provenance[key2]}`);
|
|
64591
|
+
const remaining = Math.max(0, total - decided);
|
|
64445
64592
|
const agentRuled = groups.some((g) => g.rows.some((r) => r.decidedBy === "agent" && r.status === "C"));
|
|
64446
|
-
return {
|
|
64593
|
+
return {
|
|
64594
|
+
text: s.coverage(decided, total),
|
|
64595
|
+
detail: [...provenance, ...remaining ? [s.remaining(remaining)] : []].join(" \xB7 "),
|
|
64596
|
+
agentRuled
|
|
64597
|
+
};
|
|
64447
64598
|
}
|
|
64448
64599
|
var cell = (v) => v.replace(/\|/g, "\\|");
|
|
64449
64600
|
function criterionTableHead(s) {
|
|
@@ -64482,11 +64633,11 @@ function stepSummary(result, opts = {}) {
|
|
|
64482
64633
|
const lang = opts.lang ?? "en";
|
|
64483
64634
|
const s = S[lang];
|
|
64484
64635
|
const stdLabel = isCore(standard) ? "WCAG 2.2 AA" : loadPack(standard).name;
|
|
64485
|
-
const
|
|
64636
|
+
const coverage = runCoverage(result, standard, lang);
|
|
64486
64637
|
const out2 = [];
|
|
64487
64638
|
out2.push(`## ${s.title} \u2014 ${stdLabel}`, "");
|
|
64488
|
-
out2.push(`\`${result.date}\` \xB7 ${result.scope.files} ${s.files} \xB7 **${
|
|
64489
|
-
if (
|
|
64639
|
+
out2.push(`\`${result.date}\` \xB7 ${result.scope.files} ${s.files} \xB7 **${coverage.text}**${coverage.detail ? ` \xB7 ${coverage.detail}` : ""}`, "");
|
|
64640
|
+
if (coverage.agentRuled) out2.push(`> ${agentMarkNote(lang)}`, "");
|
|
64490
64641
|
const baseDir = opts.baseDir ?? process.cwd();
|
|
64491
64642
|
const all = findingsForStandard(result, standard);
|
|
64492
64643
|
if (!all.length) {
|
|
@@ -64537,15 +64688,15 @@ function prComment(result, opts = {}) {
|
|
|
64537
64688
|
const all = findingsForStandard(result, standard);
|
|
64538
64689
|
const normative = all.filter((f) => !f.advisory);
|
|
64539
64690
|
const blocking = normative.filter((f) => f.severity === "bloquant").length;
|
|
64540
|
-
const
|
|
64691
|
+
const coverage = runCoverage(result, standard, lang);
|
|
64541
64692
|
const grouped = groupFindings(all, standard, lang, baseDir);
|
|
64542
64693
|
const criteria = groupByCriterion(grouped);
|
|
64543
|
-
const orphans =
|
|
64694
|
+
const orphans = all.filter((f) => !f.advisory && !f.page).length;
|
|
64544
64695
|
const head = [];
|
|
64545
64696
|
head.push(`### ${s.title} \u2014 ${stdLabel}`, "");
|
|
64546
64697
|
head.push(blocking ? s.verdictFail(blocking) : normative.length ? s.verdictWarn : s.verdictPass, "");
|
|
64547
|
-
head.push(`\`${result.date}\` \xB7 ${result.scope.files} ${s.files} \xB7 **${
|
|
64548
|
-
if (
|
|
64698
|
+
head.push(`\`${result.date}\` \xB7 ${result.scope.files} ${s.files} \xB7 **${coverage.text}**${coverage.detail ? ` \xB7 ${coverage.detail}` : ""}`, "");
|
|
64699
|
+
if (coverage.agentRuled) head.push(`> ${agentMarkNote(lang)}`, "");
|
|
64549
64700
|
if (orphans) head.push(`> ${s.unattributed(orphans)}`, "");
|
|
64550
64701
|
const tail = [];
|
|
64551
64702
|
if (opts.artifactName) tail.push(s.artifact(opts.artifactName), "");
|
|
@@ -64581,7 +64732,7 @@ function perPageTable(result, standard = CORE2, lang = "en") {
|
|
|
64581
64732
|
"",
|
|
64582
64733
|
...scoreboardTable(result, derived, standard, s, lang),
|
|
64583
64734
|
"",
|
|
64584
|
-
...basisCaveats(result, derived, s, lang),
|
|
64735
|
+
...basisCaveats(result, derived, standard, s, lang),
|
|
64585
64736
|
...fullGridBlock(result, derived, standard, s, lang),
|
|
64586
64737
|
"",
|
|
64587
64738
|
...derived.flatMap((pg) => [...namedCriteriaBlock(result, pg, standard, s, lang), ""])
|
|
@@ -64613,7 +64764,8 @@ function scoreboardTable(result, derived, standard, s, lang) {
|
|
|
64613
64764
|
"| --- | --- | ---: | ---: | ---: | ---: | ---: |"
|
|
64614
64765
|
];
|
|
64615
64766
|
for (const pg of derived) {
|
|
64616
|
-
const
|
|
64767
|
+
const pageFindings = findingsForStandard(pageView(result, pg), standard).filter((f) => !f.advisory);
|
|
64768
|
+
const n = (sev) => pageFindings.filter((f) => f.severity === sev).length;
|
|
64617
64769
|
const t3 = pageTally(pageCriterionRows(result, pg, standard, lang));
|
|
64618
64770
|
out2.push(
|
|
64619
64771
|
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${t3.c} | ${t3.nc} | ${n("bloquant")} | ${n("majeur")} | ${n("mineur")} |`
|
|
@@ -64637,12 +64789,10 @@ function undecidedBlock(result, derived, standard, s, lang) {
|
|
|
64637
64789
|
out2.push("");
|
|
64638
64790
|
return out2;
|
|
64639
64791
|
}
|
|
64640
|
-
function
|
|
64641
|
-
return pg.findings.filter((f) => !f.advisory && f.severity === sev).length;
|
|
64642
|
-
}
|
|
64643
|
-
function basisCaveats(result, derived, s, lang) {
|
|
64792
|
+
function basisCaveats(result, derived, standard, s, lang) {
|
|
64644
64793
|
const out2 = [`> ${s.scoreboardNote}`, ""];
|
|
64645
|
-
const
|
|
64794
|
+
const projected = new Set(findingsForStandard(result, standard));
|
|
64795
|
+
const orphans = unattributedFindings(result).filter((f) => projected.has(f) && !f.advisory).length;
|
|
64646
64796
|
if (orphans) out2.push(`> ${s.unattributed(orphans)}`, "");
|
|
64647
64797
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> ${s.sourceBasis}`, "");
|
|
64648
64798
|
const notAudited = pageBasisWarning("not-audited", lang);
|
|
@@ -64654,9 +64804,11 @@ function pageBlock(result, page, standard, lang, baseDir) {
|
|
|
64654
64804
|
const s = S[lang];
|
|
64655
64805
|
const rows = pageCriterionRows(result, page, standard, lang);
|
|
64656
64806
|
const nc = rows.filter((r) => r.status === "NC");
|
|
64657
|
-
const
|
|
64807
|
+
const pageFindings = findingsForStandard(pageView(result, page), standard).filter((f) => !f.advisory);
|
|
64808
|
+
const occurrences = pageFindings.length;
|
|
64658
64809
|
if (!nc.length && !occurrences) return void 0;
|
|
64659
|
-
const
|
|
64810
|
+
const count = (severity) => pageFindings.filter((f) => f.severity === severity).length;
|
|
64811
|
+
const counts = `\u{1F534} ${count("bloquant")} \xB7 \u{1F7E0} ${count("majeur")} \xB7 \u{1F7E1} ${count("mineur")}`;
|
|
64660
64812
|
const withTests = nc.some((r) => r.tests.length);
|
|
64661
64813
|
const out2 = [
|
|
64662
64814
|
"<details>",
|
|
@@ -64675,12 +64827,7 @@ function pageBlock(result, page, standard, lang, baseDir) {
|
|
|
64675
64827
|
for (const r of nc) {
|
|
64676
64828
|
out2.push(withTests ? `| ${cell(r.label)} | ${r.tests.map((t3) => `\`${t3}\``).join(" ")} |` : `| ${cell(r.label)} |`);
|
|
64677
64829
|
}
|
|
64678
|
-
const defects = groupFindings(
|
|
64679
|
-
page.findings.filter((f) => !f.advisory),
|
|
64680
|
-
standard,
|
|
64681
|
-
lang,
|
|
64682
|
-
baseDir
|
|
64683
|
-
);
|
|
64830
|
+
const defects = groupFindings(pageFindings, standard, lang, baseDir);
|
|
64684
64831
|
const blocking = groupByCriterion(defects.filter((g) => g.severity === "bloquant"));
|
|
64685
64832
|
const rest = groupByCriterion(defects.filter((g) => g.severity !== "bloquant"));
|
|
64686
64833
|
const shownBlocking = Math.min(blocking.length, PAGE_CRITERIA_SHOWN);
|
|
@@ -64717,7 +64864,8 @@ function fullGridBlock(result, derived, standard, s, lang) {
|
|
|
64717
64864
|
return out2;
|
|
64718
64865
|
}
|
|
64719
64866
|
function orphansBlock(result, standard, s, lang, baseDir) {
|
|
64720
|
-
const
|
|
64867
|
+
const projected = new Set(findingsForStandard(result, standard));
|
|
64868
|
+
const orphans = unattributedFindings(result).filter((f) => projected.has(f) && !f.advisory);
|
|
64721
64869
|
if (!orphans.length) return [];
|
|
64722
64870
|
const criteria = groupByCriterion(groupFindings(orphans, standard, lang, baseDir));
|
|
64723
64871
|
const counts = SEV_ORDER4.map((sev) => `${ICON5[sev]} ${orphans.filter((f) => f.severity === sev).length}`).join(" \xB7 ");
|
|
@@ -64774,17 +64922,18 @@ function pagesComment(result, opts = {}) {
|
|
|
64774
64922
|
const derived = derivePages(result, scope);
|
|
64775
64923
|
const normative = findingsForStandard(result, standard).filter((f) => !f.advisory);
|
|
64776
64924
|
const blocking = normative.filter((f) => f.severity === "bloquant").length;
|
|
64777
|
-
const
|
|
64925
|
+
const coverage = runCoverage(result, standard, lang);
|
|
64778
64926
|
head.push(blocking ? s.verdictFail(blocking) : normative.length ? s.verdictWarn : s.verdictPass, "");
|
|
64779
|
-
head.push(`\`${result.date}\` \xB7 ${s.pagesCount(derived.length)} \xB7 **${
|
|
64780
|
-
if (
|
|
64927
|
+
head.push(`\`${result.date}\` \xB7 ${s.pagesCount(derived.length)} \xB7 **${coverage.text}**${coverage.detail ? ` \xB7 ${coverage.detail}` : ""}`, "");
|
|
64928
|
+
if (coverage.agentRuled) head.push(`> ${agentMarkNote(lang)}`, "");
|
|
64929
|
+
const severity = (page, level) => findingsForStandard(pageView(result, page), standard).filter((f) => !f.advisory && f.severity === level).length;
|
|
64781
64930
|
const blocks = [...derived].sort(
|
|
64782
|
-
(a, b) =>
|
|
64931
|
+
(a, b) => severity(b, "bloquant") - severity(a, "bloquant") || severity(b, "majeur") - severity(a, "majeur") || severity(b, "mineur") - severity(a, "mineur")
|
|
64783
64932
|
).map((p) => pageBlock(result, p, standard, lang, baseDir)).filter((b) => b !== void 0);
|
|
64784
64933
|
const assemble = (nBlocks2, nRows2, withGrid = true) => {
|
|
64785
64934
|
const body3 = [...scoreboardTable(result, derived.slice(0, nRows2), standard, s, lang), ""];
|
|
64786
64935
|
if (nRows2 < derived.length) body3.push(s.scoreboardClamped(derived.length - nRows2), "");
|
|
64787
|
-
body3.push(...basisCaveats(result, derived, s, lang));
|
|
64936
|
+
body3.push(...basisCaveats(result, derived, standard, s, lang));
|
|
64788
64937
|
body3.push(...undecidedBlock(result, derived, standard, s, lang));
|
|
64789
64938
|
if (redirected.length) body3.push(...renderRedirected(redirected, lang), "");
|
|
64790
64939
|
body3.push(...orphansBlock(result, standard, s, lang, baseDir), "");
|
|
@@ -71741,11 +71890,7 @@ async function cmdJudge(p) {
|
|
|
71741
71890
|
if (truncated) items = items.slice(0, max);
|
|
71742
71891
|
const size = grain === "criterion" ? 1 : BATCH_SIZE;
|
|
71743
71892
|
const render2 = (slice) => formatAdjudication(slice, lang, standard, { preamble: false, contract: false });
|
|
71744
|
-
const batches =
|
|
71745
|
-
for (let i2 = 0; i2 < items.length; i2 += size) {
|
|
71746
|
-
const slice = items.slice(i2, i2 + size);
|
|
71747
|
-
batches.push({ items: slice, prompt: render2(slice) });
|
|
71748
|
-
}
|
|
71893
|
+
const batches = batchWorklist(items, render2, size);
|
|
71749
71894
|
const explicitModel = typeof p.flags.model === "string" && p.flags.model ? p.flags.model : void 0;
|
|
71750
71895
|
const model = explicitModel ?? (runner === "claude" ? DEFAULT_CLI_MODEL : runner === "api" ? modelFromEnv() : void 0);
|
|
71751
71896
|
const modelLabel = model ?? "Codex account default";
|
|
@@ -3,7 +3,7 @@ name: review-a11y
|
|
|
3
3
|
description: "Use to REVIEW changed frontend code for accessibility: staged files, a working diff, branch, or PR; prompts such as 'review a11y', 'is this accessible?', or 'anything to fix before merge?'; and hook or CI findings on a change. Run the bundled install-free engine with cross-file JSX/TSX analysis, then adjudicate candidates and return a severity-ranked WCAG 2.2 AA review with file:line fixes, residual rendering risks, and a verdict. Use `ultra11y` instead for whole-repo audits, dated conformance reports, country standards, PRD backlogs, or authoring accessible markup."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: 5.35.
|
|
6
|
+
version: 5.35.2
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# review-a11y — accessibility review of changed code
|