ultra11y 5.27.0 → 5.28.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/dist/playwright.mjs +25 -7
- package/package.json +1 -1
- package/scripts/build-pack-rgaa.mjs +8 -1
- package/scripts/ultra11y.mjs +183 -38
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +183 -38
- package/skills/ultra11y/SKILL.md +3 -3
- package/skills/ultra11y/references/ci.md +1 -1
- package/skills/ultra11y/references/devtools.md +1 -1
- package/skills/ultra11y/references/judgment.md +1 -1
- package/skills/ultra11y/references/mcp.md +2 -2
- package/skills/ultra11y/references/pages.md +8 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +183 -38
- package/src/adjudicate-subjects.ts +35 -1
- package/src/annotate.ts +21 -1
- package/src/audit.ts +15 -0
- package/src/cli.ts +11 -4
- package/src/crawl.ts +34 -1
- package/src/data/standards/rgaa.json +1 -1
- package/src/html-report.ts +12 -2
- package/src/pages.ts +83 -4
- package/src/probes.ts +84 -8
- package/src/report.ts +17 -2
- package/src/scan-local.ts +56 -14
- package/src/scan.ts +18 -2
- package/src/standards/derive.ts +19 -0
- package/src/types.ts +11 -3
|
@@ -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.28.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.28.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.28.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.28.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -371,7 +371,7 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
|
|
|
371
371
|
fills an input or resizes the viewport — otherwise the snapshot would record our own
|
|
372
372
|
instrumentation instead of the site.
|
|
373
373
|
- **Page by page** — RGAA is a per-page norm; the engine's verdict is scope-wide. `pages`
|
|
374
|
-
bridges the two: one row per criterion, one column per page, embedded in `report` and
|
|
374
|
+
bridges the two: one row per criterion, one column per page URL, embedded in `report` and
|
|
375
375
|
rebuilt from a committed `audit.json` alone. The per-page status is not recomputed — a
|
|
376
376
|
per-page *view* of the audit runs through the very same projection the report uses, so grid
|
|
377
377
|
and report agree by construction. Two rules hold the line: a finding is attributed to a page
|
package/dist/playwright.mjs
CHANGED
|
@@ -287,14 +287,17 @@ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, dead
|
|
|
287
287
|
if (!count) return [];
|
|
288
288
|
const hits = [];
|
|
289
289
|
const seen = /* @__PURE__ */ new Set();
|
|
290
|
-
const limit =
|
|
290
|
+
const limit = tabPressBudget(count, limits);
|
|
291
|
+
let prevKey = null;
|
|
291
292
|
for (let i = 0; i < limit; i++) {
|
|
292
293
|
if (deadline?.out()) break;
|
|
293
294
|
await page.keyboard.press("Tab");
|
|
294
295
|
const r = await page.evaluate(FOCUS_CHECK_PROBE);
|
|
295
296
|
if (!r) continue;
|
|
297
|
+
if (r.key === prevKey) continue;
|
|
296
298
|
if (seen.has(r.key)) break;
|
|
297
299
|
seen.add(r.key);
|
|
300
|
+
prevKey = r.key;
|
|
298
301
|
if (!r.changed) {
|
|
299
302
|
hits.push({
|
|
300
303
|
selector: r.selector,
|
|
@@ -306,11 +309,23 @@ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, dead
|
|
|
306
309
|
}
|
|
307
310
|
return hits;
|
|
308
311
|
}
|
|
312
|
+
var NATIVE_SEGMENT_STOPS = {
|
|
313
|
+
date: 5,
|
|
314
|
+
time: 5,
|
|
315
|
+
"datetime-local": 8,
|
|
316
|
+
month: 4,
|
|
317
|
+
week: 4
|
|
318
|
+
};
|
|
319
|
+
function tabPressBudget(count, limits) {
|
|
320
|
+
return Math.min(count * 2 + 20, limits.maxFocusables * 2 + 20);
|
|
321
|
+
}
|
|
309
322
|
var FOCUS_WHERE_PROBE = `(() => { ${PRELUDE}
|
|
310
323
|
const e = document.activeElement;
|
|
311
324
|
if (!e || e === document.body || e === document.documentElement) return null;
|
|
312
325
|
const key = e.getAttribute && e.getAttribute('data-u11y-f');
|
|
313
|
-
|
|
326
|
+
const stops = ${JSON.stringify(NATIVE_SEGMENT_STOPS)};
|
|
327
|
+
const type = e.tagName === 'INPUT' ? (e.getAttribute('type') || 'text').toLowerCase() : '';
|
|
328
|
+
return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e), segments: stops[type] || 1 };
|
|
314
329
|
})()`;
|
|
315
330
|
async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
316
331
|
const count = await page.evaluate(focusSetupExpr("", limits.maxFocusables));
|
|
@@ -318,7 +333,7 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
318
333
|
const hits = [];
|
|
319
334
|
const seen = /* @__PURE__ */ new Set();
|
|
320
335
|
const confirmPresses = 2;
|
|
321
|
-
const limit =
|
|
336
|
+
const limit = tabPressBudget(count, limits);
|
|
322
337
|
let prev = null;
|
|
323
338
|
for (let i = 0; i < limit; i++) {
|
|
324
339
|
if (deadline?.out()) break;
|
|
@@ -326,8 +341,9 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
326
341
|
const now = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
327
342
|
if (!now) break;
|
|
328
343
|
if (prev?.tagged && now.tagged && now.key === prev.key) {
|
|
344
|
+
const budget = Math.max(confirmPresses, (now.segments ?? 1) - 1);
|
|
329
345
|
let stuck = true;
|
|
330
|
-
for (let k = 0; k <
|
|
346
|
+
for (let k = 0; k < budget && stuck; k++) {
|
|
331
347
|
if (deadline?.out()) break;
|
|
332
348
|
await page.keyboard.press("Tab");
|
|
333
349
|
const again = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
@@ -337,13 +353,15 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
337
353
|
hits.push({
|
|
338
354
|
selector: now.selector,
|
|
339
355
|
html: now.html,
|
|
340
|
-
detail: `Le focus reste sur cet \xE9l\xE9ment apr\xE8s ${1 +
|
|
356
|
+
detail: `Le focus reste sur cet \xE9l\xE9ment apr\xE8s ${1 + budget} appuis sur Tab, alors que la page compte ${count} \xE9l\xE9ments focalisables \u2014 pi\xE8ge au clavier (2.1.2).`
|
|
341
357
|
});
|
|
342
358
|
break;
|
|
343
359
|
}
|
|
344
360
|
}
|
|
345
|
-
if (
|
|
346
|
-
|
|
361
|
+
if (now.key !== prev?.key) {
|
|
362
|
+
if (seen.has(now.key)) break;
|
|
363
|
+
seen.add(now.key);
|
|
364
|
+
}
|
|
347
365
|
prev = now;
|
|
348
366
|
}
|
|
349
367
|
return hits;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.28.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",
|
|
@@ -251,9 +251,16 @@ const RULE_TO_CRITERIA = {
|
|
|
251
251
|
"error-not-associated": ["11.10"], "aria-invalid-no-description": ["11.10"],
|
|
252
252
|
"axe:label": ["11.1"], "axe:form-field-multiple-labels": ["11.1"], "axe:select-name": ["11.1"], "axe:label-title-only": ["11.1"],
|
|
253
253
|
"axe:autocomplete-valid": ["11.13"], "axe:fieldset": ["11.6"], "axe:input-button-name": ["11.9"], "axe:button-name": ["11.9"],
|
|
254
|
-
// Theme 12 — navigation (12.7 skip link, 12.8 tab order)
|
|
254
|
+
// Theme 12 — navigation (12.7 skip link, 12.8 tab order, 12.9 keyboard trap)
|
|
255
255
|
"skip-link-target-missing": ["12.7"], "axe:skip-link": ["12.7"], "axe:bypass": ["12.7"],
|
|
256
256
|
"positive-tabindex": ["12.8"], "axe:tabindex": ["12.8"],
|
|
257
|
+
// 12.9 « la navigation ne doit pas contenir de piège au clavier ». The probe that answers it
|
|
258
|
+
// has existed since `probeKeyboardTrap` landed, and it was mapped to WCAG 2.1.2 alone — so a
|
|
259
|
+
// trap the browser had just walked into raised a blocking WCAG non-conformity and left RGAA
|
|
260
|
+
// 12.9 « à évaluer », to be bought from a paid adjudicator on every run. The evidence was
|
|
261
|
+
// measured and then never delivered to the criterion it answers. No axe rule joins it: axe
|
|
262
|
+
// has none for keyboard traps, which is why the probe was written in the first place.
|
|
263
|
+
"dyn-keyboard-trap": ["12.9"],
|
|
257
264
|
// Theme 13 — consultation (13.1 time limits, 13.8 moving/blinking)
|
|
258
265
|
"meta-refresh-redirect": ["13.1"], "blink-marquee": ["13.8"], "autoplay-media": ["4.10", "13.8"],
|
|
259
266
|
"axe:no-autoplay-audio": ["4.10"], "axe:blink": ["13.8"], "axe:marquee": ["13.8"],
|
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.28.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -43052,7 +43052,7 @@ var rgaa_default = {
|
|
|
43052
43052
|
techniques: ["G21", "H91", "F10"],
|
|
43053
43053
|
wcag: ["2.1.1", "2.1.2"],
|
|
43054
43054
|
appliesTo: {
|
|
43055
|
-
ruleIds: []
|
|
43055
|
+
ruleIds: ["dyn-keyboard-trap"]
|
|
43056
43056
|
}
|
|
43057
43057
|
},
|
|
43058
43058
|
{
|
|
@@ -44949,6 +44949,32 @@ var SUBJECTS = {
|
|
|
44949
44949
|
)
|
|
44950
44950
|
]),
|
|
44951
44951
|
// The document's own language declaration.
|
|
44952
|
+
// THE LANGUAGE CODE THAT IS ACTUALLY THERE — RGAA 8.4's own subject, and NOT `docLang`.
|
|
44953
|
+
//
|
|
44954
|
+
// 8.3 and 8.4 read the same element and ask opposite questions. 8.3 (« la page a-t-elle une
|
|
44955
|
+
// langue par défaut ? ») is about the `<html>` element, and its subject must be present even
|
|
44956
|
+
// when the attribute is missing — that absence IS its non-conformity. 8.4 (« POUR CHAQUE PAGE
|
|
44957
|
+
// AYANT UNE LANGUE PAR DÉFAUT, le code est-il pertinent ? ») is about the code, and on a page
|
|
44958
|
+
// that declares none the criterion has, in the referential's own words, nothing to be about.
|
|
44959
|
+
//
|
|
44960
|
+
// Sharing `docLang` made 8.4 unclosable exactly there: the subject was seen (the `<html>`
|
|
44961
|
+
// element is always seen), so absence could never be concluded, and 8.4 is `judgment: true`
|
|
44962
|
+
// so silence never earned it a `C` either. Measured on tests/fixtures/realworld: one page
|
|
44963
|
+
// declares no language — that is its seeded 8.3 defect — and 8.4 stayed « à évaluer » on it
|
|
44964
|
+
// for ever, on every run, with nothing any adjudicator could read.
|
|
44965
|
+
//
|
|
44966
|
+
// Same correction as 13.2 and 7.4 further down: the refusal was right about the rule and
|
|
44967
|
+
// wrong about the SUBJECT.
|
|
44968
|
+
declaredLang: (docs) => docs.flatMap(
|
|
44969
|
+
(d) => elementsByTag(d, "html").filter((e) => (attr(e, "lang") ?? attr(e, "xml:lang") ?? "").trim() !== "").map(
|
|
44970
|
+
(e) => h(
|
|
44971
|
+
d,
|
|
44972
|
+
e,
|
|
44973
|
+
`<html lang="${attr(e, "lang") ?? attr(e, "xml:lang")}"> \u2014 is this code the language the content is actually written in?`,
|
|
44974
|
+
`declaredlang|${attr(e, "lang") ?? attr(e, "xml:lang")}`
|
|
44975
|
+
)
|
|
44976
|
+
)
|
|
44977
|
+
),
|
|
44952
44978
|
docLang: (docs) => docs.flatMap(
|
|
44953
44979
|
(d) => elementsByTag(d, "html").map(
|
|
44954
44980
|
(e) => h(d, e, `<html lang="${attr(e, "lang") ?? ""}"> \u2014 is it present, valid, and the language of the content?`, `doclang|${attr(e, "lang") ?? ""}`)
|
|
@@ -45370,7 +45396,8 @@ var PACK_SUBJECTS = {
|
|
|
45370
45396
|
// is the ONLY place its subject can be named.
|
|
45371
45397
|
"8.1": ["doctype"],
|
|
45372
45398
|
"8.3": ["docLang"],
|
|
45373
|
-
|
|
45399
|
+
// 8.4 asks about the CODE, 8.3 about the element — see `declaredLang`.
|
|
45400
|
+
"8.4": ["declaredLang"],
|
|
45374
45401
|
"8.5": ["docTitle"],
|
|
45375
45402
|
"8.6": ["docTitle"],
|
|
45376
45403
|
"8.7": ["langParts"],
|
|
@@ -45466,7 +45493,10 @@ var EXISTENCE_SUBJECTS = /* @__PURE__ */ new Set([
|
|
|
45466
45493
|
"frames",
|
|
45467
45494
|
"downloadDocs",
|
|
45468
45495
|
"newWindow",
|
|
45469
|
-
"contextChange"
|
|
45496
|
+
"contextChange",
|
|
45497
|
+
// A page that declares no default language has no language code for 8.4 to be about. The
|
|
45498
|
+
// `<html>` element is always there; the attribute is what this subject looks for.
|
|
45499
|
+
"declaredLang"
|
|
45470
45500
|
]);
|
|
45471
45501
|
var subjectsForSc = (sc) => SC_SUBJECTS[sc] ?? [];
|
|
45472
45502
|
var ownPackSubjects = (standard, id) => PACK_SUBJECTS[standard]?.[id];
|
|
@@ -49513,7 +49543,8 @@ function newAccum() {
|
|
|
49513
49543
|
renderedRan: /* @__PURE__ */ new Map(),
|
|
49514
49544
|
probedScs: /* @__PURE__ */ new Map(),
|
|
49515
49545
|
axeRan: /* @__PURE__ */ new Set(),
|
|
49516
|
-
subjectsSeen: /* @__PURE__ */ new Set()
|
|
49546
|
+
subjectsSeen: /* @__PURE__ */ new Set(),
|
|
49547
|
+
pageSubjects: /* @__PURE__ */ new Map()
|
|
49517
49548
|
};
|
|
49518
49549
|
}
|
|
49519
49550
|
function primarySubtag(lang) {
|
|
@@ -49554,6 +49585,9 @@ function foldDoc(acc, doc, graph) {
|
|
|
49554
49585
|
const pageId = snapshotPageId(doc.file);
|
|
49555
49586
|
if (pageId) {
|
|
49556
49587
|
acc.pageIds.add(pageId);
|
|
49588
|
+
const own = acc.pageSubjects.get(pageId) ?? /* @__PURE__ */ new Set();
|
|
49589
|
+
for (const id of subjectsPresentIn(doc, own)) own.add(id);
|
|
49590
|
+
acc.pageSubjects.set(pageId, own);
|
|
49557
49591
|
for (const ruleId of renderedRulesRan(doc.signals)) {
|
|
49558
49592
|
const seen = acc.renderedRan.get(ruleId) ?? /* @__PURE__ */ new Set();
|
|
49559
49593
|
seen.add(pageId);
|
|
@@ -49817,6 +49851,9 @@ function finalize(acc, inputs, extra = {}) {
|
|
|
49817
49851
|
// nothing, `undefined` says this audit predates the fold. A pack derivation must be
|
|
49818
49852
|
// able to tell those apart before concluding NA from silence.
|
|
49819
49853
|
...acc.fileCount > 0 ? { subjectsSeen: [...acc.subjectsSeen].sort() } : {},
|
|
49854
|
+
// Per page, and omitted when no page was read — same rule as above: absent means "this
|
|
49855
|
+
// audit predates the fold", never "nothing was found".
|
|
49856
|
+
...acc.pageSubjects.size ? { pageSubjects: Object.fromEntries([...acc.pageSubjects].map(([id, set]) => [id, [...set].sort()])) } : {},
|
|
49820
49857
|
// The pages this run genuinely read. Written UNCONDITIONALLY — `[]` is the whole point,
|
|
49821
49858
|
// because "this audit read no page" is exactly the claim a source-only run needs to make,
|
|
49822
49859
|
// and an omit-when-empty field would say nothing precisely then. See scope.pagesAudited.
|
|
@@ -54470,6 +54507,9 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54470
54507
|
if (normativeFindings.length) {
|
|
54471
54508
|
return { id: pc.id, theme: pc.theme, status: "NC", findings, scs: pc.wcag };
|
|
54472
54509
|
}
|
|
54510
|
+
if (subjectAbsent(pc)) {
|
|
54511
|
+
return { id: pc.id, theme: pc.theme, status: INAPPLICABLE_STATUS, findings, scs: pc.wcag, inapplicable: true };
|
|
54512
|
+
}
|
|
54473
54513
|
if (scResults.some((r) => r.status === "NC")) {
|
|
54474
54514
|
return { id: pc.id, theme: pc.theme, status: "manual", findings, scs: pc.wcag, scopedOut: true };
|
|
54475
54515
|
}
|
|
@@ -55537,7 +55577,8 @@ var L3 = {
|
|
|
55537
55577
|
source: "source",
|
|
55538
55578
|
notAudited: "non audit\xE9",
|
|
55539
55579
|
notAuditedNote: "Une page marqu\xE9e \xAB non audit\xE9 \xBB a bien un instantan\xE9, mais CET audit ne l'a pas lu (il ne portait que sur les sources). L'absence de constat n'y vaut donc PAS conformit\xE9 \u2014 relancez l'audit en incluant `.ultra11y/pages`.",
|
|
55540
|
-
agentMark: "`C*` : conformit\xE9 tranch\xE9e par l'agent IA \xE0 partir des \xE9vidences cit\xE9es (gat\xE9), et non prouv\xE9e par le moteur d\xE9terministe."
|
|
55580
|
+
agentMark: "`C*` : conformit\xE9 tranch\xE9e par l'agent IA \xE0 partir des \xE9vidences cit\xE9es (gat\xE9), et non prouv\xE9e par le moteur d\xE9terministe.",
|
|
55581
|
+
originNote: (o) => `Les colonnes sont les URL des pages, relatives \xE0 \`${o}\`.`
|
|
55541
55582
|
},
|
|
55542
55583
|
en: {
|
|
55543
55584
|
title: "Per-page grid",
|
|
@@ -55552,7 +55593,8 @@ var L3 = {
|
|
|
55552
55593
|
source: "source",
|
|
55553
55594
|
notAudited: "not audited",
|
|
55554
55595
|
notAuditedNote: 'A page marked "not audited" does have a snapshot, but THIS audit never read it (it covered sources only). Absence of a finding there does NOT mean conforming \u2014 re-run the audit with `.ultra11y/pages` in scope.',
|
|
55555
|
-
agentMark: "`C*`: conformity ruled by the AI agent from the evidence it cited (gated), not proven by the deterministic engine."
|
|
55596
|
+
agentMark: "`C*`: conformity ruled by the AI agent from the evidence it cited (gated), not proven by the deterministic engine.",
|
|
55597
|
+
originNote: (o) => `Columns are the pages' URLs, relative to \`${o}\`.`
|
|
55556
55598
|
}
|
|
55557
55599
|
};
|
|
55558
55600
|
function formatRate(rate, decided, total) {
|
|
@@ -55572,9 +55614,37 @@ function basisLabel(basis, lang) {
|
|
|
55572
55614
|
function unattributedNote(n, lang) {
|
|
55573
55615
|
return L3[lang].unattributed(n);
|
|
55574
55616
|
}
|
|
55617
|
+
function pageColumnLabel(page, origin) {
|
|
55618
|
+
let label = page.url;
|
|
55619
|
+
if (origin && page.url.startsWith(origin)) {
|
|
55620
|
+
const rest = page.url.slice(origin.length);
|
|
55621
|
+
label = rest === "" ? "/" : rest;
|
|
55622
|
+
}
|
|
55623
|
+
return `${label}${page.auth ? " \u{1F512}" : ""}`;
|
|
55624
|
+
}
|
|
55625
|
+
function commonOrigin(pages) {
|
|
55626
|
+
if (!pages.length) return void 0;
|
|
55627
|
+
let origin;
|
|
55628
|
+
for (const p of pages) {
|
|
55629
|
+
let o;
|
|
55630
|
+
try {
|
|
55631
|
+
o = new URL(p.url).origin;
|
|
55632
|
+
} catch {
|
|
55633
|
+
return void 0;
|
|
55634
|
+
}
|
|
55635
|
+
if (origin === void 0) origin = o;
|
|
55636
|
+
else if (origin !== o) return void 0;
|
|
55637
|
+
}
|
|
55638
|
+
return origin;
|
|
55639
|
+
}
|
|
55640
|
+
function pageOriginNote(origin, lang) {
|
|
55641
|
+
return origin ? L3[lang].originNote(origin) : void 0;
|
|
55642
|
+
}
|
|
55575
55643
|
function pageView(result, page) {
|
|
55644
|
+
const own = result.scope.pageSubjects?.[page.id];
|
|
55576
55645
|
return {
|
|
55577
55646
|
...result,
|
|
55647
|
+
...own ? { scope: { ...result.scope, subjectsSeen: own } } : {},
|
|
55578
55648
|
criteria: page.criteria,
|
|
55579
55649
|
findings: page.findings,
|
|
55580
55650
|
...result.packFindings ? { packFindings: result.packFindings.filter((f) => f.page === page.id) } : {}
|
|
@@ -55606,7 +55676,7 @@ function renderRedirected(redirected, lang = "en") {
|
|
|
55606
55676
|
"| --- | --- | --- | --- |"
|
|
55607
55677
|
];
|
|
55608
55678
|
for (const r of redirected) {
|
|
55609
|
-
const why = r.reason === "http-status" ? fr ? `HTTP ${r.status ?? "\u2265 400"} \u2014 page d'erreur rendue \xE0 la m\xEAme adresse` : `HTTP ${r.status ?? "\u2265 400"} \u2014 error page served at the same address` : fr ? "redirection" : "redirect";
|
|
55679
|
+
const why = r.reason === "http-status" ? fr ? `HTTP ${r.status ?? "\u2265 400"} \u2014 page d'erreur rendue \xE0 la m\xEAme adresse` : `HTTP ${r.status ?? "\u2265 400"} \u2014 error page served at the same address` : r.reason === "error" ? fr ? `le navigateur a \xE9chou\xE9 sur cette page${r.detail ? ` \u2014 ${r.detail}` : ""}` : `the browser failed on this page${r.detail ? ` \u2014 ${r.detail}` : ""}` : fr ? "redirection" : "redirect";
|
|
55610
55680
|
out2.push(`| ${r.name} (\`${r.id}\`) | \`${r.requested}\` | \`${r.landed}\` | ${why} |`);
|
|
55611
55681
|
}
|
|
55612
55682
|
return out2;
|
|
@@ -55633,7 +55703,10 @@ function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
|
|
|
55633
55703
|
}
|
|
55634
55704
|
out2.push(`> ${s.note}`, "");
|
|
55635
55705
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> \u26A0\uFE0F ${s.basisNote}`, "");
|
|
55636
|
-
const
|
|
55706
|
+
const origin = commonOrigin(derived);
|
|
55707
|
+
const originNote = pageOriginNote(origin, lang);
|
|
55708
|
+
if (originNote) out2.push(`> ${originNote}`, "");
|
|
55709
|
+
const head = [s.criterion, ...derived.map((p) => pageColumnLabel(p, origin))];
|
|
55637
55710
|
out2.push(`| ${head.join(" | ")} |`, `| ${head.map(() => "---").join(" | ")} |`);
|
|
55638
55711
|
const { rows, status } = pageGridModel(result, derived, standard, lang);
|
|
55639
55712
|
out2.push(`| **${s.rate}** | ${derived.map((p) => `**${formatRate(...gridRate(rows, status, p.id))}**`).join(" | ")} |`);
|
|
@@ -56202,7 +56275,7 @@ function render(r, lang, opts) {
|
|
|
56202
56275
|
for (const pg of derived) {
|
|
56203
56276
|
const nc = pg.findings.filter((f) => !f.advisory);
|
|
56204
56277
|
const adv = pg.findings.filter((f) => f.advisory);
|
|
56205
|
-
out2.push(
|
|
56278
|
+
out2.push("<details>", `<summary>${pg.name} \u2014 <code>${pg.url}</code> \u2014 ${pg.auth ? s.authYes : s.authNo}</summary>`, "");
|
|
56206
56279
|
out2.push(`- ${nc.length} ${s.ncCount}${adv.length ? ` \xB7 ${adv.length} ${s.advCount}` : ""}`);
|
|
56207
56280
|
const notes = pageScope.find((x) => x.id === pg.id)?.notes;
|
|
56208
56281
|
if (notes) out2.push(`- _${notes}_`);
|
|
@@ -56225,7 +56298,7 @@ function render(r, lang, opts) {
|
|
|
56225
56298
|
out2.push(` - [${label}] \`${f.selectorHint}\` \u2014 ${mdText(resolveMessage(f, lang))}`);
|
|
56226
56299
|
}
|
|
56227
56300
|
if (nc.length > PER_PAGE_MAX) out2.push(` - _${s.perPageMore(nc.length - PER_PAGE_MAX, nc.length)}_`);
|
|
56228
|
-
out2.push("");
|
|
56301
|
+
out2.push("", "</details>", "");
|
|
56229
56302
|
}
|
|
56230
56303
|
}
|
|
56231
56304
|
out2.push(`## ${s.cTitle}`, "");
|
|
@@ -60153,6 +60226,15 @@ function parseSitemapUrls(xml) {
|
|
|
60153
60226
|
while ((m = re.exec(xml)) !== null) out2.push(m[1]);
|
|
60154
60227
|
return out2;
|
|
60155
60228
|
}
|
|
60229
|
+
var DOWNLOAD_EXT = /\.(?:pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|tar|epub|dmg|exe|pkg|mp3|mp4|m4a|m4v|avi|mov|wmv|webm|ogg|wav|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf)$/i;
|
|
60230
|
+
function isDownloadUrl(url) {
|
|
60231
|
+
let path = url;
|
|
60232
|
+
try {
|
|
60233
|
+
path = new URL(url).pathname;
|
|
60234
|
+
} catch {
|
|
60235
|
+
}
|
|
60236
|
+
return DOWNLOAD_EXT.test(path);
|
|
60237
|
+
}
|
|
60156
60238
|
function extractLinks(html, baseUrl) {
|
|
60157
60239
|
const origin = new URL(baseUrl).origin;
|
|
60158
60240
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -60170,6 +60252,7 @@ function extractLinks(html, baseUrl) {
|
|
|
60170
60252
|
}
|
|
60171
60253
|
if (abs.protocol !== "http:" && abs.protocol !== "https:") continue;
|
|
60172
60254
|
if (abs.origin !== origin) continue;
|
|
60255
|
+
if (isDownloadUrl(abs.pathname)) continue;
|
|
60173
60256
|
abs.hash = "";
|
|
60174
60257
|
const url = abs.href;
|
|
60175
60258
|
if (seen.has(url)) continue;
|
|
@@ -60519,10 +60602,11 @@ function writeRunnerSnapshot(root, out2, target, page) {
|
|
|
60519
60602
|
if (!collected?.dom) return void 0;
|
|
60520
60603
|
const url = page?.url ?? hostPageOf(out2.url ?? collected.url, target);
|
|
60521
60604
|
const id = page?.id ?? pageIdFor(url);
|
|
60605
|
+
const name2 = page?.name?.trim() || collected.title?.trim() || nameFromUrl(url);
|
|
60522
60606
|
const meta2 = {
|
|
60523
60607
|
v: SNAPSHOT_VERSION,
|
|
60524
60608
|
id,
|
|
60525
|
-
name:
|
|
60609
|
+
name: name2,
|
|
60526
60610
|
url,
|
|
60527
60611
|
runner: "scan",
|
|
60528
60612
|
...collected.viewport ? { viewport: collected.viewport } : {},
|
|
@@ -61083,14 +61167,17 @@ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, dead
|
|
|
61083
61167
|
if (!count) return [];
|
|
61084
61168
|
const hits = [];
|
|
61085
61169
|
const seen = /* @__PURE__ */ new Set();
|
|
61086
|
-
const limit =
|
|
61170
|
+
const limit = tabPressBudget(count, limits);
|
|
61171
|
+
let prevKey = null;
|
|
61087
61172
|
for (let i2 = 0; i2 < limit; i2++) {
|
|
61088
61173
|
if (deadline?.out()) break;
|
|
61089
61174
|
await page.keyboard.press("Tab");
|
|
61090
61175
|
const r = await page.evaluate(FOCUS_CHECK_PROBE);
|
|
61091
61176
|
if (!r) continue;
|
|
61177
|
+
if (r.key === prevKey) continue;
|
|
61092
61178
|
if (seen.has(r.key)) break;
|
|
61093
61179
|
seen.add(r.key);
|
|
61180
|
+
prevKey = r.key;
|
|
61094
61181
|
if (!r.changed) {
|
|
61095
61182
|
hits.push({
|
|
61096
61183
|
selector: r.selector,
|
|
@@ -61102,11 +61189,23 @@ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, dead
|
|
|
61102
61189
|
}
|
|
61103
61190
|
return hits;
|
|
61104
61191
|
}
|
|
61192
|
+
var NATIVE_SEGMENT_STOPS = {
|
|
61193
|
+
date: 5,
|
|
61194
|
+
time: 5,
|
|
61195
|
+
"datetime-local": 8,
|
|
61196
|
+
month: 4,
|
|
61197
|
+
week: 4
|
|
61198
|
+
};
|
|
61199
|
+
function tabPressBudget(count, limits) {
|
|
61200
|
+
return Math.min(count * 2 + 20, limits.maxFocusables * 2 + 20);
|
|
61201
|
+
}
|
|
61105
61202
|
var FOCUS_WHERE_PROBE = `(() => { ${PRELUDE}
|
|
61106
61203
|
const e = document.activeElement;
|
|
61107
61204
|
if (!e || e === document.body || e === document.documentElement) return null;
|
|
61108
61205
|
const key = e.getAttribute && e.getAttribute('data-u11y-f');
|
|
61109
|
-
|
|
61206
|
+
const stops = ${JSON.stringify(NATIVE_SEGMENT_STOPS)};
|
|
61207
|
+
const type = e.tagName === 'INPUT' ? (e.getAttribute('type') || 'text').toLowerCase() : '';
|
|
61208
|
+
return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e), segments: stops[type] || 1 };
|
|
61110
61209
|
})()`;
|
|
61111
61210
|
async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
61112
61211
|
const count = await page.evaluate(focusSetupExpr("", limits.maxFocusables));
|
|
@@ -61114,7 +61213,7 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
61114
61213
|
const hits = [];
|
|
61115
61214
|
const seen = /* @__PURE__ */ new Set();
|
|
61116
61215
|
const confirmPresses = 2;
|
|
61117
|
-
const limit =
|
|
61216
|
+
const limit = tabPressBudget(count, limits);
|
|
61118
61217
|
let prev = null;
|
|
61119
61218
|
for (let i2 = 0; i2 < limit; i2++) {
|
|
61120
61219
|
if (deadline?.out()) break;
|
|
@@ -61122,8 +61221,9 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
61122
61221
|
const now = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
61123
61222
|
if (!now) break;
|
|
61124
61223
|
if (prev?.tagged && now.tagged && now.key === prev.key) {
|
|
61224
|
+
const budget = Math.max(confirmPresses, (now.segments ?? 1) - 1);
|
|
61125
61225
|
let stuck = true;
|
|
61126
|
-
for (let k = 0; k <
|
|
61226
|
+
for (let k = 0; k < budget && stuck; k++) {
|
|
61127
61227
|
if (deadline?.out()) break;
|
|
61128
61228
|
await page.keyboard.press("Tab");
|
|
61129
61229
|
const again = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
@@ -61133,13 +61233,15 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
61133
61233
|
hits.push({
|
|
61134
61234
|
selector: now.selector,
|
|
61135
61235
|
html: now.html,
|
|
61136
|
-
detail: `Le focus reste sur cet \xE9l\xE9ment apr\xE8s ${1 +
|
|
61236
|
+
detail: `Le focus reste sur cet \xE9l\xE9ment apr\xE8s ${1 + budget} appuis sur Tab, alors que la page compte ${count} \xE9l\xE9ments focalisables \u2014 pi\xE8ge au clavier (2.1.2).`
|
|
61137
61237
|
});
|
|
61138
61238
|
break;
|
|
61139
61239
|
}
|
|
61140
61240
|
}
|
|
61141
|
-
if (
|
|
61142
|
-
|
|
61241
|
+
if (now.key !== prev?.key) {
|
|
61242
|
+
if (seen.has(now.key)) break;
|
|
61243
|
+
seen.add(now.key);
|
|
61244
|
+
}
|
|
61143
61245
|
prev = now;
|
|
61144
61246
|
}
|
|
61145
61247
|
return hits;
|
|
@@ -61666,13 +61768,26 @@ async function runScanManyLocal(urls, opts) {
|
|
|
61666
61768
|
const redirected = [];
|
|
61667
61769
|
try {
|
|
61668
61770
|
for (const url of urls) {
|
|
61669
|
-
|
|
61670
|
-
|
|
61671
|
-
|
|
61672
|
-
|
|
61673
|
-
|
|
61674
|
-
|
|
61675
|
-
|
|
61771
|
+
let out2;
|
|
61772
|
+
try {
|
|
61773
|
+
out2 = await runOnPage(browser, AxeBuilder, url, false, {
|
|
61774
|
+
storageState: opts.storageState,
|
|
61775
|
+
interact,
|
|
61776
|
+
allowClicks: clicksAllowed(opts.storageState, opts.interactClicks),
|
|
61777
|
+
lang,
|
|
61778
|
+
snapshot: Boolean(opts.snapshotRoot)
|
|
61779
|
+
});
|
|
61780
|
+
} catch (e) {
|
|
61781
|
+
redirected.push({
|
|
61782
|
+
id: slugifyPageId(url),
|
|
61783
|
+
name: url,
|
|
61784
|
+
requested: url,
|
|
61785
|
+
landed: url,
|
|
61786
|
+
reason: "error",
|
|
61787
|
+
detail: String(e?.message ?? e).slice(0, 200)
|
|
61788
|
+
});
|
|
61789
|
+
continue;
|
|
61790
|
+
}
|
|
61676
61791
|
if (out2.httpStatus !== void 0 && out2.httpStatus >= 400) {
|
|
61677
61792
|
redirected.push({
|
|
61678
61793
|
id: slugifyPageId(url),
|
|
@@ -61714,13 +61829,26 @@ async function runSampleScanLocal(pages, opts) {
|
|
|
61714
61829
|
for (const page of pages) {
|
|
61715
61830
|
const storageState = page.storageState ?? opts.storageState;
|
|
61716
61831
|
const isFile = !/^https?:\/\//i.test(page.url);
|
|
61717
|
-
|
|
61718
|
-
|
|
61719
|
-
|
|
61720
|
-
|
|
61721
|
-
|
|
61722
|
-
|
|
61723
|
-
|
|
61832
|
+
let out2;
|
|
61833
|
+
try {
|
|
61834
|
+
out2 = await runOnPage(browser, AxeBuilder, page.url, isFile, {
|
|
61835
|
+
storageState,
|
|
61836
|
+
interact,
|
|
61837
|
+
allowClicks: clicksAllowed(storageState, opts.interactClicks),
|
|
61838
|
+
lang,
|
|
61839
|
+
snapshot: Boolean(opts.snapshotRoot)
|
|
61840
|
+
});
|
|
61841
|
+
} catch (e) {
|
|
61842
|
+
redirected.push({
|
|
61843
|
+
id: page.id,
|
|
61844
|
+
name: page.name,
|
|
61845
|
+
requested: page.url,
|
|
61846
|
+
landed: page.url,
|
|
61847
|
+
reason: "error",
|
|
61848
|
+
detail: String(e?.message ?? e).slice(0, 200)
|
|
61849
|
+
});
|
|
61850
|
+
continue;
|
|
61851
|
+
}
|
|
61724
61852
|
const requestedUrl = isFile ? "file://" + resolve12(page.url) : page.url;
|
|
61725
61853
|
const landedUrl = out2.landedUrl ?? out2.url;
|
|
61726
61854
|
if (!landedOnRequestedPage(requestedUrl, landedUrl)) {
|
|
@@ -63443,6 +63571,8 @@ function perPageTable(result, standard = CORE2, lang = "en") {
|
|
|
63443
63571
|
...scoreboardTable(result, derived, standard, s, lang),
|
|
63444
63572
|
"",
|
|
63445
63573
|
...basisCaveats(result, derived, s, lang),
|
|
63574
|
+
...fullGridBlock(result, derived, standard, s, lang),
|
|
63575
|
+
"",
|
|
63446
63576
|
...derived.flatMap((pg) => [...namedCriteriaBlock(result, pg, standard, s, lang), ""])
|
|
63447
63577
|
].join("\n");
|
|
63448
63578
|
}
|
|
@@ -63568,7 +63698,9 @@ function fullGridBlock(result, derived, standard, s, lang) {
|
|
|
63568
63698
|
const { rows, status } = pageGridModel(result, derived, standard, lang);
|
|
63569
63699
|
if (!rows.length || !derived.length) return [];
|
|
63570
63700
|
const cell = (v) => v.replace(/\|/g, "\\|");
|
|
63571
|
-
const
|
|
63701
|
+
const origin = commonOrigin(derived);
|
|
63702
|
+
const originNote = pageOriginNote(origin, lang);
|
|
63703
|
+
const head = [s.criterion, ...derived.map((p) => cell(pageColumnLabel(p, origin)))];
|
|
63572
63704
|
const out2 = [
|
|
63573
63705
|
"<details>",
|
|
63574
63706
|
`<summary><b>${s.fullGrid}</b> \u2014 ${rows.length} \xD7 ${derived.length}</summary>`,
|
|
@@ -63576,6 +63708,7 @@ function fullGridBlock(result, derived, standard, s, lang) {
|
|
|
63576
63708
|
"",
|
|
63577
63709
|
`> ${s.gridLegend}`,
|
|
63578
63710
|
"",
|
|
63711
|
+
...originNote ? [`> ${originNote}`, ""] : [],
|
|
63579
63712
|
`| ${head.join(" | ")} |`,
|
|
63580
63713
|
`| ${head.map(() => "---").join(" | ")} |`
|
|
63581
63714
|
];
|
|
@@ -64394,10 +64527,12 @@ function crossGridBlocks(result, derived, standard, lang) {
|
|
|
64394
64527
|
if (!derived.length) return [];
|
|
64395
64528
|
const { rows, status } = pageGridModel(result, derived, standard, lang);
|
|
64396
64529
|
if (!rows.length) return [];
|
|
64530
|
+
const origin = commonOrigin(derived);
|
|
64531
|
+
const originNote = pageOriginNote(origin, lang);
|
|
64397
64532
|
const table = {
|
|
64398
64533
|
kind: "table",
|
|
64399
64534
|
caption: t3.crossGridCaption,
|
|
64400
|
-
columns: [{ text: t3.criterion }, ...derived.map((p) => ({ text:
|
|
64535
|
+
columns: [{ text: t3.criterion }, ...derived.map((p) => ({ text: pageColumnLabel(p, origin) }))],
|
|
64401
64536
|
rows: []
|
|
64402
64537
|
};
|
|
64403
64538
|
let group = "";
|
|
@@ -64408,7 +64543,12 @@ function crossGridBlocks(result, derived, standard, lang) {
|
|
|
64408
64543
|
}
|
|
64409
64544
|
table.rows.push([{ text: row.label }, ...derived.map((p) => ({ status: status.get(row.id)?.get(p.id) ?? "manual", text: "" }))]);
|
|
64410
64545
|
}
|
|
64411
|
-
return [
|
|
64546
|
+
return [
|
|
64547
|
+
{ kind: "heading", level: 2, text: t3.grid, id: "grid" },
|
|
64548
|
+
{ kind: "note", tone: "info", runs: ticks(t3.legend) },
|
|
64549
|
+
...originNote ? [{ kind: "note", tone: "info", runs: ticks(originNote) }] : [],
|
|
64550
|
+
table
|
|
64551
|
+
];
|
|
64412
64552
|
}
|
|
64413
64553
|
function pageGridBlocks(result, page, standard, lang) {
|
|
64414
64554
|
const t3 = T4[lang];
|
|
@@ -68164,7 +68304,7 @@ Commands:
|
|
|
68164
68304
|
SKIP_A11Y=1. Called by hooks/pre-tool-use.mjs, not by hand.
|
|
68165
68305
|
pages The per-page criterion grid \u2014 RGAA is a per-page norm, the engine's
|
|
68166
68306
|
verdict is scope-wide, and this bridges the two. One row per criterion
|
|
68167
|
-
(the pack's own under --standard), one column per page. Rebuilt from a
|
|
68307
|
+
(the pack's own under --standard), one column per page URL. Rebuilt from a
|
|
68168
68308
|
committed audit.json alone: no snapshots on disk, no browser.
|
|
68169
68309
|
Two honesty rules: a finding is attributed to a page only when something
|
|
68170
68310
|
SAYS so (the snapshot it was raised on, the scanned URL, the sample page
|
|
@@ -70602,7 +70742,12 @@ async function cmdScan(p) {
|
|
|
70602
70742
|
}
|
|
70603
70743
|
if (dynamic.redirected?.length) {
|
|
70604
70744
|
for (const r of dynamic.redirected) {
|
|
70605
|
-
const why = r.reason === "http-status" ? lang === "fr" ? `a r\xE9pondu HTTP ${r.status} \xE0 la m\xEAme adresse` : `answered HTTP ${r.status} at the same address` :
|
|
70745
|
+
const why = r.reason === "http-status" ? lang === "fr" ? `a r\xE9pondu HTTP ${r.status} \xE0 la m\xEAme adresse` : `answered HTTP ${r.status} at the same address` : r.reason === "error" ? (
|
|
70746
|
+
// Named rather than folded into the redirect wording: « a redirigé vers » its own
|
|
70747
|
+
// address is a sentence that explains nothing, and the browser's message is the
|
|
70748
|
+
// one fact that says which page to go and look at.
|
|
70749
|
+
lang === "fr" ? `a fait \xE9chouer le navigateur${r.detail ? ` \u2014 ${r.detail}` : ""}` : `made the browser fail${r.detail ? ` \u2014 ${r.detail}` : ""}`
|
|
70750
|
+
) : lang === "fr" ? `a redirig\xE9 vers ${r.landed}` : `redirected to ${r.landed}`;
|
|
70606
70751
|
console.error(
|
|
70607
70752
|
lang === "fr" ? `\u26A0\uFE0F ultra11y scan : \xAB ${r.name} \xBB (${r.id}) non enregistr\xE9e \u2014 ${r.requested} ${why}. L'enregistrer aurait d\xE9crit cet \xE9cran sous le nom demand\xE9.` : `\u26A0\uFE0F ultra11y scan: "${r.name}" (${r.id}) not recorded \u2014 ${r.requested} ${why}. Recording it would have described that screen under the requested name.`
|
|
70608
70753
|
);
|
|
@@ -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.28.0
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|