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
|
@@ -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";
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: ultra11y
|
|
|
3
3
|
description: "Use to AUDIT a repository, site, or page against WCAG 2.2 AA or a country standard such as RGAA; produce a dated conformance report, criterion grid, PRD backlog, or tickets; AUTHOR accessible markup; fix accessibility; or wire a repo gate and rendered-DOM capture pipeline. The bundled install-free engine runs 81 static checks tied to success criteria, while the agent adjudicates judgment criteria and routes rendering criteria to browser scans; check/verify reject invented non-conformities. Use `review-a11y` instead for a review scoped only to a diff, branch, or PR."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: 5.35.
|
|
6
|
+
version: 5.35.2
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -20,6 +20,14 @@ criteria (computed contrast, visible focus, zoom/reflow, content-on-hover) fall
|
|
|
20
20
|
tier (axe-core in a real browser); a human is at most optional oversight. Gates stop any
|
|
21
21
|
hallucinated non-conformity from surviving, and nothing is ever silently "conforming".
|
|
22
22
|
|
|
23
|
+
**Do not confuse rules with criteria.** The bundle ships 81 static checks, WCAG AA has 55
|
|
24
|
+
success criteria, and RGAA has 106 criteria. Several rules may evidence one criterion, one
|
|
25
|
+
rule may contribute to several pack criteria, and most criteria remain open until a scan or
|
|
26
|
+
an adjudication. The only honest run-level coverage is the grid's live provenance tally
|
|
27
|
+
(`moteur + scan + agent`, plus any named undecidable residue), never a fixed promise such as
|
|
28
|
+
“48 agent criteria”. GitHub summaries therefore show `decided/total`, not a percentage over
|
|
29
|
+
the small decided subset.
|
|
30
|
+
|
|
23
31
|
**WCAG 2.2 Level AA is the worldwide core.** Country standards — France's **RGAA**, the
|
|
24
32
|
US **Section 508**, the EU **EN 301 549** — are pluggable *standards packs* that map their
|
|
25
33
|
criteria onto WCAG. Add `--standard rgaa` to re-key **every command and every output** —
|
|
@@ -339,18 +339,27 @@ plan renders inline on the diff, and appends a Markdown job summary to
|
|
|
339
339
|
been scanned and merged). With no `$GITHUB_STEP_SUMMARY` set, the summary goes to stderr so a
|
|
340
340
|
local run still shows it.
|
|
341
341
|
|
|
342
|
+
The GitHub summary deliberately prints **coverage, never a percentage**:
|
|
343
|
+
`12/106 criteria decided in this run · 12 engine · 94 still to complete by scan or
|
|
344
|
+
adjudication`. A percentage over the decided subset looks like a conformance score for the
|
|
345
|
+
whole grid. Coverage and provenance are derived from the selected standard's criterion rows,
|
|
346
|
+
and the occurrence count is derived from the same projection; a WCAG finding that makes no
|
|
347
|
+
RGAA criterion non-conformant therefore appears in neither the heading nor an empty table.
|
|
348
|
+
|
|
342
349
|
URL-keyed findings are **skipped** for annotations — there is no repo line to point at — and
|
|
343
350
|
counted in the summary instead, so they are never silently dropped.
|
|
344
351
|
|
|
345
352
|
## Which command to run it from
|
|
346
353
|
|
|
347
|
-
|
|
348
|
-
|
|
354
|
+
The engine remains WCAG-keyed internally, while `audit --standard <pack>` writes a pack-keyed
|
|
355
|
+
document with that core under `core`. CI renderers always project from the active standard's
|
|
356
|
+
grid. So:
|
|
349
357
|
|
|
350
358
|
| You want | Run |
|
|
351
359
|
|---|---|
|
|
352
360
|
| WCAG-keyed CI output, one shot | `audit … --format sarif` |
|
|
353
|
-
| **RGAA**-keyed CI output | `audit … --
|
|
361
|
+
| **RGAA**-keyed CI output, one shot | `audit … --standard rgaa --format sarif` |
|
|
362
|
+
| Re-render an existing RGAA audit | `report --in audits/audit-latest.json --standard rgaa --format sarif` |
|
|
354
363
|
| Only what the PR introduced | `audit --since origin/main --baseline audits/baseline.json --format sarif` |
|
|
355
364
|
|
|
356
365
|
In `--baseline` gate mode the CI rendering covers **exactly the new findings** — the subject
|
|
@@ -376,7 +385,7 @@ with the action's `comment-kind` (`ULTRA11Y_PR_COMMENT_KIND`):
|
|
|
376
385
|
| kind | what it posts |
|
|
377
386
|
|---|---|
|
|
378
387
|
| `digest` (default) | The verdict, the coverage, and the distinct defects — one row per (criterion, rule, selector), so 472 occurrences of one design-system defect are one row. Then a link. |
|
|
379
|
-
| `pages` | The page-by-page grid: one row per page with its basis, its
|
|
388
|
+
| `pages` | The page-by-page grid: one row per page with its basis, its `C`/`NC` counts and severities — no misleading decided-subset percentage; then the FULL criterion × page grid (`C` / `NC` / `—` / `?`, collapsed), the criteria nobody has ruled on **named**, and a collapsed block per failing page listing its non-conforming criteria and where to fix them. Needs pages in scope; with none it says so rather than posting an empty scoreboard. |
|
|
380
389
|
| `full` | Both, in **one** comment under its own marker: everything `pages` posts, plus the run's distinct defects with their locations — the digest's actionable half. For a workflow that wants a single comment at the end with everything in it rather than two a reviewer has to reconcile. Same scope requirement as `pages`. |
|
|
381
390
|
|
|
382
391
|
**The marker carries both the standard and the kind**, so a WCAG run and an RGAA run keep
|
|
@@ -19,6 +19,17 @@ criteria are coarser than the engine's rules, that denominator is deliberately s
|
|
|
19
19
|
it is **not** a full conformance rate. Full WCAG 2.2 AA conformance requires completing
|
|
20
20
|
the "to assess" criteria by hand.
|
|
21
21
|
|
|
22
|
+
That rate remains available in the complete artifact, beside its qualification. GitHub job
|
|
23
|
+
summaries and PR comments do **not** print it: they print `decided/total`, the provenance
|
|
24
|
+
breakdown (`engine`, `scan`, `agent`) and the remaining count. This prevents a static result
|
|
25
|
+
such as 11 conformities and 1 non-conformity from being read as “92% RGAA” when only 12 of the
|
|
26
|
+
106 RGAA criteria have actually been decided.
|
|
27
|
+
|
|
28
|
+
The 81 bundled static checks are **rules, not criteria**. Their relationship is many-to-many:
|
|
29
|
+
several rules can evidence one criterion, and a pack can scope one rule to several of its own
|
|
30
|
+
criteria. Rule count, criterion coverage and agent worklist size must never be substituted for
|
|
31
|
+
one another.
|
|
32
|
+
|
|
22
33
|
## Non-conformity priorities
|
|
23
34
|
|
|
24
35
|
- 🔴 **Blocking** — prevents access to content/function (e.g. missing alt, unlabeled
|
|
@@ -88,6 +88,12 @@ with Haiku, against a reference ledger adjudicated by a stronger model through t
|
|
|
88
88
|
`11.2.1` really is a test of `11.2`. It never asks whether the reading was right, so an
|
|
89
89
|
adjudicator that files « this field has no label » (which is 11.1's) under 11.2 passes cleanly.
|
|
90
90
|
|
|
91
|
+
The `48` above is the intersection of two measured experiment outputs, **not** a fixed number
|
|
92
|
+
of RGAA criteria assigned to the agent. On every real run, read the final provenance tally.
|
|
93
|
+
For example, `26 engine + 32 scan + 46 agent + 2 named undecidable = 106` is complete; calling
|
|
94
|
+
that “48 agent criteria” would hide both the actual 46 model verdicts and the two explicit
|
|
95
|
+
exceptions.
|
|
96
|
+
|
|
91
97
|
Three things now stand between that and a deliverable, and only the third is a model:
|
|
92
98
|
|
|
93
99
|
1. **The brief names the neighbour.** A criterion states which of its theme-mates owns the
|