ultra11y 5.2.1 → 5.3.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/README.md +7 -0
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +126 -19
- package/skills/review-a11y/SKILL.md +9 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +126 -19
- package/skills/ultra11y/SKILL.md +33 -1
- package/skills/ultra11y/references/ci.md +28 -6
- package/skills/ultra11y/references/orchestration.md +16 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +126 -19
- package/src/annotate.ts +192 -10
- package/src/cli.ts +9 -4
- package/src/pages-report.ts +10 -1
- package/src/pr-comment.ts +38 -9
- 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.2
|
|
13
|
+
"version": "5.3.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.2
|
|
4
|
+
"version": "5.3.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.2
|
|
4
|
+
"version": "5.3.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.2
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.3.2";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -391,6 +391,13 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
|
|
|
391
391
|
rules on the judgment criteria that would otherwise stay « à évaluer » in CI, reading its key
|
|
392
392
|
from the job environment and skipping itself without one. `ultra11y init --ci` writes a
|
|
393
393
|
workflow using it. See `references/ci.md`.
|
|
394
|
+
- **Two PR comments, not one** — `comment-kind: digest` (the default) posts the distinct
|
|
395
|
+
defects a reviewer can act on; `comment-kind: pages` posts the **page-by-page grid**: one row
|
|
396
|
+
per page with its basis, its rate *and its denominator*, then the non-conforming criteria of
|
|
397
|
+
each failing page. Each kind is keyed by its own sticky marker, so a workflow that gates code
|
|
398
|
+
in one job and sweeps pages in another keeps **both** comments — sharing a marker is how a
|
|
399
|
+
684-occurrence sweep silently overwrote a four-finding gate, and the actionable half is the
|
|
400
|
+
one that vanished.
|
|
394
401
|
- **CI surfaces** — a red job says *that* the build broke, not *where*. `--format sarif` emits
|
|
395
402
|
SARIF 2.1.0 for GitHub code scanning, so each finding lands as an **inline annotation on the
|
|
396
403
|
causing line** of the PR; `--format github` does the same via `::error::` workflow commands
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.2
|
|
3
|
+
"version": "5.3.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 join50, relative as relative5, resolve as resolve15, 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.2
|
|
9
|
+
var VERSION = "5.3.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -53611,13 +53611,16 @@ function ghAvailable() {
|
|
|
53611
53611
|
}
|
|
53612
53612
|
|
|
53613
53613
|
// src/pr-comment.ts
|
|
53614
|
-
function COMMENT_MARKER(standard) {
|
|
53615
|
-
return `<!-- ultra11y:report standard="${standard}" -->`;
|
|
53614
|
+
function COMMENT_MARKER(standard, kind = "digest") {
|
|
53615
|
+
return `<!-- ultra11y:report standard="${standard}"${kind === "digest" ? "" : ` kind="${kind}"`} -->`;
|
|
53616
53616
|
}
|
|
53617
|
-
function stickyBody(markdown, standard) {
|
|
53618
|
-
return `${COMMENT_MARKER(standard)}
|
|
53617
|
+
function stickyBody(markdown, standard, kind = "digest") {
|
|
53618
|
+
return `${COMMENT_MARKER(standard, kind)}
|
|
53619
53619
|
${markdown}`;
|
|
53620
53620
|
}
|
|
53621
|
+
function commentKindFrom(value) {
|
|
53622
|
+
return value === "pages" ? "pages" : "digest";
|
|
53623
|
+
}
|
|
53621
53624
|
function prNumberFromEnv(env = process.env) {
|
|
53622
53625
|
const explicit = Number.parseInt(env.ULTRA11Y_PR ?? "", 10);
|
|
53623
53626
|
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
@@ -53628,11 +53631,11 @@ function prNumberFromEnv(env = process.env) {
|
|
|
53628
53631
|
function pickExistingComment(comments, marker) {
|
|
53629
53632
|
return comments.find((c2) => typeof c2.body === "string" && c2.body.includes(marker));
|
|
53630
53633
|
}
|
|
53631
|
-
function pushPrComment(markdown, standard = "wcag") {
|
|
53634
|
+
function pushPrComment(markdown, standard = "wcag", kind = "digest") {
|
|
53632
53635
|
const pr = prNumberFromEnv();
|
|
53633
53636
|
if (pr === void 0) return { ok: true, action: "skipped", reason: "not a pull-request run" };
|
|
53634
|
-
const marker = COMMENT_MARKER(standard);
|
|
53635
|
-
const body3 = stickyBody(markdown, standard);
|
|
53637
|
+
const marker = COMMENT_MARKER(standard, kind);
|
|
53638
|
+
const body3 = stickyBody(markdown, standard, kind);
|
|
53636
53639
|
try {
|
|
53637
53640
|
const repo = ghExec(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]).trim();
|
|
53638
53641
|
let existing;
|
|
@@ -55184,6 +55187,9 @@ function pageTally(rows) {
|
|
|
55184
55187
|
manual: rows.filter((r) => r.status === "manual").length
|
|
55185
55188
|
};
|
|
55186
55189
|
}
|
|
55190
|
+
function pageTallyNote(t2, lang) {
|
|
55191
|
+
return L7[lang].tally(t2.c, t2.nc, t2.na, t2.manual);
|
|
55192
|
+
}
|
|
55187
55193
|
function pageCoverage(rows) {
|
|
55188
55194
|
const t2 = pageTally(rows);
|
|
55189
55195
|
return { decided: t2.c + t2.nc, total: rows.length };
|
|
@@ -55209,7 +55215,7 @@ function renderPageReport(result, page, opts = {}) {
|
|
|
55209
55215
|
const cov = pageCoverage(rows);
|
|
55210
55216
|
const rate = pageRatePct(rows);
|
|
55211
55217
|
out2.push(`- **${s.rate}** : **${rate === null ? "\u2014" : `${rate} %`}** _(${s.rateNote})_`);
|
|
55212
|
-
out2.push(`- ${
|
|
55218
|
+
out2.push(`- ${pageTallyNote(t2, lang)}`);
|
|
55213
55219
|
out2.push(`- ${s.coverage(cov.decided, cov.total)}`, "");
|
|
55214
55220
|
if (page.basis !== "snapshot") out2.push(`> \u26A0\uFE0F ${s.sourceWarn}`, "");
|
|
55215
55221
|
const shot = opts.screenshots?.get(page.id);
|
|
@@ -60052,7 +60058,15 @@ var S = {
|
|
|
60052
60058
|
clamped: (n) => `_${n} groupe(s) retir\xE9(s) de ce commentaire pour tenir dans la limite de GitHub \u2014 le r\xE9sum\xE9 de job les porte tous._`,
|
|
60053
60059
|
unanchored: (n) => `${n} constat(s) rattach\xE9(s) \xE0 une URL, sans ligne de code \xE0 annoter \u2014 voir le rapport.`,
|
|
60054
60060
|
unattributed: (n) => `${n} constat(s) ne sont rattach\xE9s \xE0 aucune page (code partag\xE9, fichier hors routes) \u2014 compt\xE9s dans l'audit global, jamais r\xE9partis d'office.`,
|
|
60055
|
-
sourceBasis: "Une page marqu\xE9e \xAB source \xBB n'a pas d'instantan\xE9 : l'absence de constat n'y vaut PAS conformit\xE9, et son taux ne porte que sur ce que le moteur a pu d\xE9cider ailleurs."
|
|
60061
|
+
sourceBasis: "Une page marqu\xE9e \xAB source \xBB n'a pas d'instantan\xE9 : l'absence de constat n'y vaut PAS conformit\xE9, et son taux ne porte que sur ce que le moteur a pu d\xE9cider ailleurs.",
|
|
60062
|
+
pageByPage: "page par page",
|
|
60063
|
+
pagesCount: (n) => `${n} page(s)`,
|
|
60064
|
+
testsCol: "Tests",
|
|
60065
|
+
noPages: "Aucune page dans le p\xE9rim\xE8tre de ce run : le balayage n'a produit aucun instantan\xE9. Ce n'est pas un bilan vide, c'est un bilan absent \u2014 les crit\xE8res au rendu restent \xE0 \xE9valuer.",
|
|
60066
|
+
pagesDetailNote: "Un bloc par page portant au moins une non-conformit\xE9, et seulement ses crit\xE8res **non conformes** \u2014 la grille compl\xE8te (tous les crit\xE8res de chaque page, avec leurs tests et leurs captures) vit dans l'artefact.",
|
|
60067
|
+
pagesClamped: (n) => `_Le d\xE9tail de ${n} page(s) a \xE9t\xE9 retir\xE9 de ce commentaire pour tenir dans la limite de GitHub \u2014 l'artefact les porte toutes._`,
|
|
60068
|
+
scoreboardClamped: (n) => `_${n} page(s) retir\xE9e(s) du tableau pour tenir dans la limite de GitHub \u2014 l'artefact les porte toutes._`,
|
|
60069
|
+
noCriterionForFindings: (n) => `${n} constat(s) sur cette page ne rendent aucun crit\xE8re du r\xE9f\xE9rentiel non conforme : leur r\xE8gle sort du p\xE9rim\xE8tre d'application de chacun. Ils comptent dans les colonnes ci-dessus, et sont d\xE9taill\xE9s dans l'artefact.`
|
|
60056
60070
|
},
|
|
60057
60071
|
en: {
|
|
60058
60072
|
title: "ultra11y accessibility audit",
|
|
@@ -60085,7 +60099,15 @@ var S = {
|
|
|
60085
60099
|
clamped: (n) => `_${n} group(s) dropped from this comment to fit GitHub's limit \u2014 the job summary carries them all._`,
|
|
60086
60100
|
unanchored: (n) => `${n} finding(s) keyed to a URL, with no code line to annotate \u2014 see the report.`,
|
|
60087
60101
|
unattributed: (n) => `${n} finding(s) are attributed to no page (shared code, file outside any route) \u2014 counted in the overall audit, never spread across pages.`,
|
|
60088
|
-
sourceBasis: 'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming, and its rate covers only what the engine could decide elsewhere.'
|
|
60102
|
+
sourceBasis: 'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming, and its rate covers only what the engine could decide elsewhere.',
|
|
60103
|
+
pageByPage: "page by page",
|
|
60104
|
+
pagesCount: (n) => `${n} page(s)`,
|
|
60105
|
+
testsCol: "Tests",
|
|
60106
|
+
noPages: "No page in this run's scope: the sweep produced no snapshot. This is not an empty scoreboard, it is a missing one \u2014 the rendering criteria stay to assess.",
|
|
60107
|
+
pagesDetailNote: "One block per page carrying at least one non-conformity, and only its **non-conforming** criteria \u2014 the full grid (every criterion of every page, with its tests and its screenshot) lives in the artifact.",
|
|
60108
|
+
pagesClamped: (n) => `_The detail of ${n} page(s) was dropped from this comment to fit GitHub's limit \u2014 the artifact carries them all._`,
|
|
60109
|
+
scoreboardClamped: (n) => `_${n} page(s) dropped from the table to fit GitHub's limit \u2014 the artifact carries them all._`,
|
|
60110
|
+
noCriterionForFindings: (n) => `${n} finding(s) on this page make no criterion of the standard non-conforming: their rule falls outside every criterion's applicability. They are counted in the columns above, and detailed in the artifact.`
|
|
60089
60111
|
}
|
|
60090
60112
|
};
|
|
60091
60113
|
var MAX_ROWS = 50;
|
|
@@ -60201,23 +60223,106 @@ function perPageTable(result, standard = CORE2, lang = "en") {
|
|
|
60201
60223
|
if (!scope.length) return "";
|
|
60202
60224
|
attributePages(result, scope);
|
|
60203
60225
|
const derived = derivePages(result, scope);
|
|
60204
|
-
|
|
60205
|
-
|
|
60226
|
+
return [`### ${s.perPage}`, "", ...scoreboardTable(result, derived, standard, s, lang), "", ...basisCaveats(result, derived, s, lang)].join("\n");
|
|
60227
|
+
}
|
|
60228
|
+
function scoreboardTable(result, derived, standard, s, lang) {
|
|
60229
|
+
const out2 = [`| ${s.page} | ${s.basis} | ${s.pageRate} | \u{1F534} | \u{1F7E0} | \u{1F7E1} |`, "| --- | --- | ---: | ---: | ---: | ---: |"];
|
|
60206
60230
|
for (const pg of derived) {
|
|
60207
|
-
const
|
|
60208
|
-
const
|
|
60231
|
+
const n = (sev) => severityCount(pg, sev);
|
|
60232
|
+
const rows = pageCriterionRows(result, pg, standard, lang);
|
|
60233
|
+
const cov = pageCoverage(rows);
|
|
60209
60234
|
out2.push(
|
|
60210
|
-
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${formatRate(
|
|
60235
|
+
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${formatRate(pageRatePct(rows), cov.decided, cov.total)} | ${n("bloquant")} | ${n("majeur")} | ${n("mineur")} |`
|
|
60211
60236
|
);
|
|
60212
60237
|
}
|
|
60213
|
-
out2
|
|
60238
|
+
return out2;
|
|
60239
|
+
}
|
|
60240
|
+
function severityCount(pg, sev) {
|
|
60241
|
+
return pg.findings.filter((f) => !f.advisory && f.severity === sev).length;
|
|
60242
|
+
}
|
|
60243
|
+
function basisCaveats(result, derived, s, lang) {
|
|
60244
|
+
const out2 = [];
|
|
60214
60245
|
const orphans = unattributedFindings(result).filter((f) => !f.advisory).length;
|
|
60215
60246
|
if (orphans) out2.push(`> ${s.unattributed(orphans)}`, "");
|
|
60216
60247
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> ${s.sourceBasis}`, "");
|
|
60217
60248
|
const notAudited = pageBasisWarning("not-audited", lang);
|
|
60218
60249
|
if (notAudited && derived.some((p) => p.basis === "not-audited")) out2.push(`> ${notAudited}`, "");
|
|
60250
|
+
return out2;
|
|
60251
|
+
}
|
|
60252
|
+
function pageBlock(result, page, standard, lang) {
|
|
60253
|
+
const s = S[lang];
|
|
60254
|
+
const rows = pageCriterionRows(result, page, standard, lang);
|
|
60255
|
+
const nc = rows.filter((r) => r.status === "NC");
|
|
60256
|
+
const occurrences = page.findings.filter((f) => !f.advisory).length;
|
|
60257
|
+
if (!nc.length && !occurrences) return void 0;
|
|
60258
|
+
const cell = (v) => v.replace(/\|/g, "\\|");
|
|
60259
|
+
const counts = `\u{1F534} ${severityCount(page, "bloquant")} \xB7 \u{1F7E0} ${severityCount(page, "majeur")} \xB7 \u{1F7E1} ${severityCount(page, "mineur")}`;
|
|
60260
|
+
const withTests = nc.some((r) => r.tests.length);
|
|
60261
|
+
const out2 = [
|
|
60262
|
+
"<details>",
|
|
60263
|
+
`<summary><b>${cell(page.name)}</b>${page.auth ? " \u{1F512}" : ""} \u2014 ${counts}</summary>`,
|
|
60264
|
+
// GFM only renders Markdown inside <details> after a blank line; without it the table
|
|
60265
|
+
// ships to the reader as literal pipes.
|
|
60266
|
+
"",
|
|
60267
|
+
`${pageTallyNote(pageTally(rows), lang)}`,
|
|
60268
|
+
""
|
|
60269
|
+
];
|
|
60270
|
+
if (!nc.length) {
|
|
60271
|
+
out2.push(s.noCriterionForFindings(occurrences), "", "</details>");
|
|
60272
|
+
return out2.join("\n");
|
|
60273
|
+
}
|
|
60274
|
+
out2.push(withTests ? `| ${s.criterion} | ${s.testsCol} |` : `| ${s.criterion} |`, withTests ? "| --- | --- |" : "| --- |");
|
|
60275
|
+
for (const r of nc) {
|
|
60276
|
+
out2.push(withTests ? `| ${cell(r.label)} | ${r.tests.map((t2) => `\`${t2}\``).join(" ")} |` : `| ${cell(r.label)} |`);
|
|
60277
|
+
}
|
|
60278
|
+
out2.push("", "</details>");
|
|
60219
60279
|
return out2.join("\n");
|
|
60220
60280
|
}
|
|
60281
|
+
function pagesComment(result, opts = {}) {
|
|
60282
|
+
const standard = opts.standard ?? CORE2;
|
|
60283
|
+
const lang = opts.lang ?? "en";
|
|
60284
|
+
const s = S[lang];
|
|
60285
|
+
const stdLabel = isCore(standard) ? "WCAG 2.2 AA" : loadPack(standard).name;
|
|
60286
|
+
const redirected = result.scope.redirected ?? [];
|
|
60287
|
+
const scope = pagesOf(result);
|
|
60288
|
+
const head = [`### ${s.title} \u2014 ${stdLabel} \xB7 ${s.pageByPage}`, ""];
|
|
60289
|
+
const tail = [];
|
|
60290
|
+
if (opts.artifactName) tail.push(s.artifact(opts.artifactName), "");
|
|
60291
|
+
if (opts.runUrl) tail.push(s.runLink(opts.runUrl), "");
|
|
60292
|
+
if (!scope.length) {
|
|
60293
|
+
head.push(s.noPages, "");
|
|
60294
|
+
if (redirected.length) head.push(...renderRedirected(redirected, lang), "");
|
|
60295
|
+
return [...head, ...tail].join("\n").trimEnd();
|
|
60296
|
+
}
|
|
60297
|
+
attributePages(result, scope);
|
|
60298
|
+
const derived = derivePages(result, scope);
|
|
60299
|
+
const normative = findingsForStandard(result, standard).filter((f) => !f.advisory);
|
|
60300
|
+
const blocking = normative.filter((f) => f.severity === "bloquant").length;
|
|
60301
|
+
const rate = runRate(result, standard, lang);
|
|
60302
|
+
head.push(blocking ? s.verdictFail(blocking) : normative.length ? s.verdictWarn : s.verdictPass, "");
|
|
60303
|
+
head.push(`\`${result.date}\` \xB7 ${s.pagesCount(derived.length)} \xB7 **${rate.text}** ${s.rate}`, "");
|
|
60304
|
+
if (rate.agentRuled) head.push(`> ${agentMarkNote(lang)}`, "");
|
|
60305
|
+
const blocks = [...derived].sort(
|
|
60306
|
+
(a, b) => severityCount(b, "bloquant") - severityCount(a, "bloquant") || severityCount(b, "majeur") - severityCount(a, "majeur") || severityCount(b, "mineur") - severityCount(a, "mineur")
|
|
60307
|
+
).map((p) => pageBlock(result, p, standard, lang)).filter((b) => b !== void 0);
|
|
60308
|
+
const assemble = (nBlocks2, nRows2) => {
|
|
60309
|
+
const body3 = [...scoreboardTable(result, derived.slice(0, nRows2), standard, s, lang), ""];
|
|
60310
|
+
if (nRows2 < derived.length) body3.push(s.scoreboardClamped(derived.length - nRows2), "");
|
|
60311
|
+
body3.push(...basisCaveats(result, derived, s, lang));
|
|
60312
|
+
if (redirected.length) body3.push(...renderRedirected(redirected, lang), "");
|
|
60313
|
+
if (blocks.length) {
|
|
60314
|
+
body3.push(`> ${s.pagesDetailNote}`, "");
|
|
60315
|
+
body3.push(...blocks.slice(0, nBlocks2).flatMap((b) => [b, ""]));
|
|
60316
|
+
if (nBlocks2 < blocks.length) body3.push(s.pagesClamped(blocks.length - nBlocks2), "");
|
|
60317
|
+
}
|
|
60318
|
+
return [...head, ...body3, ...tail].join("\n").trimEnd();
|
|
60319
|
+
};
|
|
60320
|
+
let nBlocks = blocks.length;
|
|
60321
|
+
let nRows = derived.length;
|
|
60322
|
+
while (nBlocks > 0 && assemble(nBlocks, nRows).length > COMMENT_LIMIT) nBlocks--;
|
|
60323
|
+
while (nRows > 0 && assemble(nBlocks, nRows).length > COMMENT_LIMIT) nRows--;
|
|
60324
|
+
return assemble(nBlocks, nRows);
|
|
60325
|
+
}
|
|
60221
60326
|
|
|
60222
60327
|
// src/evidence.ts
|
|
60223
60328
|
import { createHash as createHash6 } from "crypto";
|
|
@@ -65032,7 +65137,9 @@ function emitCiFormat(result, format, standard, lang, failOn) {
|
|
|
65032
65137
|
console.error(md);
|
|
65033
65138
|
}
|
|
65034
65139
|
if (process.env.ULTRA11Y_PR_COMMENT === "1") {
|
|
65035
|
-
const
|
|
65140
|
+
const kind = commentKindFrom(process.env.ULTRA11Y_PR_COMMENT_KIND);
|
|
65141
|
+
const render2 = kind === "pages" ? pagesComment : prComment;
|
|
65142
|
+
const body3 = render2(result, {
|
|
65036
65143
|
standard,
|
|
65037
65144
|
lang,
|
|
65038
65145
|
// The run is known before the artifact exists, so the link is always safe. The artifact
|
|
@@ -65041,7 +65148,7 @@ function emitCiFormat(result, format, standard, lang, failOn) {
|
|
|
65041
65148
|
...process.env.ULTRA11Y_RUN_URL ? { runUrl: process.env.ULTRA11Y_RUN_URL } : {},
|
|
65042
65149
|
...process.env.ULTRA11Y_ARTIFACT_NAME ? { artifactName: process.env.ULTRA11Y_ARTIFACT_NAME } : {}
|
|
65043
65150
|
});
|
|
65044
|
-
const c2 = pushPrComment(
|
|
65151
|
+
const c2 = pushPrComment(body3, standard, kind);
|
|
65045
65152
|
console.error(
|
|
65046
65153
|
c2.ok ? lang === "fr" ? `ultra11y : commentaire de PR ${c2.action === "updated" ? "mis \xE0 jour" : c2.action === "created" ? "cr\xE9\xE9" : "ignor\xE9"}${c2.reason ? ` (${c2.reason})` : ""}.` : `ultra11y: PR comment ${c2.action}${c2.reason ? ` (${c2.reason})` : ""}.` : lang === "fr" ? `ultra11y : commentaire de PR impossible${c2.reason ? ` \u2014 ${c2.reason}` : ""}.` : `ultra11y: PR comment failed${c2.reason ? ` \u2014 ${c2.reason}` : ""}.`
|
|
65047
65154
|
);
|
|
@@ -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.2
|
|
8
|
+
version: 5.3.2
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -110,6 +110,14 @@ rendered DOM; residual risks listed above.
|
|
|
110
110
|
and criterion-grounded.
|
|
111
111
|
- Pre-existing issues spotted in passing go in ONE final line, out of the verdict.
|
|
112
112
|
|
|
113
|
+
**Running as a subagent.** This skill is often dispatched rather than typed: by the plugin
|
|
114
|
+
hook on a pending `git commit` / `git push` / `gh pr create`, or by the `ultra11y` skill once
|
|
115
|
+
its audit has produced fixes. When it is, the report above **is** the return value — it
|
|
116
|
+
becomes a tool result for the caller, not a message to a human. So return it whole and
|
|
117
|
+
nothing else: no preamble, no "I ran the engine and found…", no offer to fix. A caller that
|
|
118
|
+
receives a summary has to guess at the findings, which is the one thing this skill exists to
|
|
119
|
+
stop. Everything in §4 stays opt-in and belongs to the caller, not to you.
|
|
120
|
+
|
|
113
121
|
## 4. After the review (opt-in)
|
|
114
122
|
|
|
115
123
|
- **Safe mechanical fixes**: `fix --staged --write --safe` (auto-applies the safe codemods
|
|
@@ -6,7 +6,7 @@ import { join as join50, relative as relative5, resolve as resolve15, 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.2
|
|
9
|
+
var VERSION = "5.3.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -53611,13 +53611,16 @@ function ghAvailable() {
|
|
|
53611
53611
|
}
|
|
53612
53612
|
|
|
53613
53613
|
// src/pr-comment.ts
|
|
53614
|
-
function COMMENT_MARKER(standard) {
|
|
53615
|
-
return `<!-- ultra11y:report standard="${standard}" -->`;
|
|
53614
|
+
function COMMENT_MARKER(standard, kind = "digest") {
|
|
53615
|
+
return `<!-- ultra11y:report standard="${standard}"${kind === "digest" ? "" : ` kind="${kind}"`} -->`;
|
|
53616
53616
|
}
|
|
53617
|
-
function stickyBody(markdown, standard) {
|
|
53618
|
-
return `${COMMENT_MARKER(standard)}
|
|
53617
|
+
function stickyBody(markdown, standard, kind = "digest") {
|
|
53618
|
+
return `${COMMENT_MARKER(standard, kind)}
|
|
53619
53619
|
${markdown}`;
|
|
53620
53620
|
}
|
|
53621
|
+
function commentKindFrom(value) {
|
|
53622
|
+
return value === "pages" ? "pages" : "digest";
|
|
53623
|
+
}
|
|
53621
53624
|
function prNumberFromEnv(env = process.env) {
|
|
53622
53625
|
const explicit = Number.parseInt(env.ULTRA11Y_PR ?? "", 10);
|
|
53623
53626
|
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
@@ -53628,11 +53631,11 @@ function prNumberFromEnv(env = process.env) {
|
|
|
53628
53631
|
function pickExistingComment(comments, marker) {
|
|
53629
53632
|
return comments.find((c2) => typeof c2.body === "string" && c2.body.includes(marker));
|
|
53630
53633
|
}
|
|
53631
|
-
function pushPrComment(markdown, standard = "wcag") {
|
|
53634
|
+
function pushPrComment(markdown, standard = "wcag", kind = "digest") {
|
|
53632
53635
|
const pr = prNumberFromEnv();
|
|
53633
53636
|
if (pr === void 0) return { ok: true, action: "skipped", reason: "not a pull-request run" };
|
|
53634
|
-
const marker = COMMENT_MARKER(standard);
|
|
53635
|
-
const body3 = stickyBody(markdown, standard);
|
|
53637
|
+
const marker = COMMENT_MARKER(standard, kind);
|
|
53638
|
+
const body3 = stickyBody(markdown, standard, kind);
|
|
53636
53639
|
try {
|
|
53637
53640
|
const repo = ghExec(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]).trim();
|
|
53638
53641
|
let existing;
|
|
@@ -55184,6 +55187,9 @@ function pageTally(rows) {
|
|
|
55184
55187
|
manual: rows.filter((r) => r.status === "manual").length
|
|
55185
55188
|
};
|
|
55186
55189
|
}
|
|
55190
|
+
function pageTallyNote(t2, lang) {
|
|
55191
|
+
return L7[lang].tally(t2.c, t2.nc, t2.na, t2.manual);
|
|
55192
|
+
}
|
|
55187
55193
|
function pageCoverage(rows) {
|
|
55188
55194
|
const t2 = pageTally(rows);
|
|
55189
55195
|
return { decided: t2.c + t2.nc, total: rows.length };
|
|
@@ -55209,7 +55215,7 @@ function renderPageReport(result, page, opts = {}) {
|
|
|
55209
55215
|
const cov = pageCoverage(rows);
|
|
55210
55216
|
const rate = pageRatePct(rows);
|
|
55211
55217
|
out2.push(`- **${s.rate}** : **${rate === null ? "\u2014" : `${rate} %`}** _(${s.rateNote})_`);
|
|
55212
|
-
out2.push(`- ${
|
|
55218
|
+
out2.push(`- ${pageTallyNote(t2, lang)}`);
|
|
55213
55219
|
out2.push(`- ${s.coverage(cov.decided, cov.total)}`, "");
|
|
55214
55220
|
if (page.basis !== "snapshot") out2.push(`> \u26A0\uFE0F ${s.sourceWarn}`, "");
|
|
55215
55221
|
const shot = opts.screenshots?.get(page.id);
|
|
@@ -60052,7 +60058,15 @@ var S = {
|
|
|
60052
60058
|
clamped: (n) => `_${n} groupe(s) retir\xE9(s) de ce commentaire pour tenir dans la limite de GitHub \u2014 le r\xE9sum\xE9 de job les porte tous._`,
|
|
60053
60059
|
unanchored: (n) => `${n} constat(s) rattach\xE9(s) \xE0 une URL, sans ligne de code \xE0 annoter \u2014 voir le rapport.`,
|
|
60054
60060
|
unattributed: (n) => `${n} constat(s) ne sont rattach\xE9s \xE0 aucune page (code partag\xE9, fichier hors routes) \u2014 compt\xE9s dans l'audit global, jamais r\xE9partis d'office.`,
|
|
60055
|
-
sourceBasis: "Une page marqu\xE9e \xAB source \xBB n'a pas d'instantan\xE9 : l'absence de constat n'y vaut PAS conformit\xE9, et son taux ne porte que sur ce que le moteur a pu d\xE9cider ailleurs."
|
|
60061
|
+
sourceBasis: "Une page marqu\xE9e \xAB source \xBB n'a pas d'instantan\xE9 : l'absence de constat n'y vaut PAS conformit\xE9, et son taux ne porte que sur ce que le moteur a pu d\xE9cider ailleurs.",
|
|
60062
|
+
pageByPage: "page par page",
|
|
60063
|
+
pagesCount: (n) => `${n} page(s)`,
|
|
60064
|
+
testsCol: "Tests",
|
|
60065
|
+
noPages: "Aucune page dans le p\xE9rim\xE8tre de ce run : le balayage n'a produit aucun instantan\xE9. Ce n'est pas un bilan vide, c'est un bilan absent \u2014 les crit\xE8res au rendu restent \xE0 \xE9valuer.",
|
|
60066
|
+
pagesDetailNote: "Un bloc par page portant au moins une non-conformit\xE9, et seulement ses crit\xE8res **non conformes** \u2014 la grille compl\xE8te (tous les crit\xE8res de chaque page, avec leurs tests et leurs captures) vit dans l'artefact.",
|
|
60067
|
+
pagesClamped: (n) => `_Le d\xE9tail de ${n} page(s) a \xE9t\xE9 retir\xE9 de ce commentaire pour tenir dans la limite de GitHub \u2014 l'artefact les porte toutes._`,
|
|
60068
|
+
scoreboardClamped: (n) => `_${n} page(s) retir\xE9e(s) du tableau pour tenir dans la limite de GitHub \u2014 l'artefact les porte toutes._`,
|
|
60069
|
+
noCriterionForFindings: (n) => `${n} constat(s) sur cette page ne rendent aucun crit\xE8re du r\xE9f\xE9rentiel non conforme : leur r\xE8gle sort du p\xE9rim\xE8tre d'application de chacun. Ils comptent dans les colonnes ci-dessus, et sont d\xE9taill\xE9s dans l'artefact.`
|
|
60056
60070
|
},
|
|
60057
60071
|
en: {
|
|
60058
60072
|
title: "ultra11y accessibility audit",
|
|
@@ -60085,7 +60099,15 @@ var S = {
|
|
|
60085
60099
|
clamped: (n) => `_${n} group(s) dropped from this comment to fit GitHub's limit \u2014 the job summary carries them all._`,
|
|
60086
60100
|
unanchored: (n) => `${n} finding(s) keyed to a URL, with no code line to annotate \u2014 see the report.`,
|
|
60087
60101
|
unattributed: (n) => `${n} finding(s) are attributed to no page (shared code, file outside any route) \u2014 counted in the overall audit, never spread across pages.`,
|
|
60088
|
-
sourceBasis: 'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming, and its rate covers only what the engine could decide elsewhere.'
|
|
60102
|
+
sourceBasis: 'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming, and its rate covers only what the engine could decide elsewhere.',
|
|
60103
|
+
pageByPage: "page by page",
|
|
60104
|
+
pagesCount: (n) => `${n} page(s)`,
|
|
60105
|
+
testsCol: "Tests",
|
|
60106
|
+
noPages: "No page in this run's scope: the sweep produced no snapshot. This is not an empty scoreboard, it is a missing one \u2014 the rendering criteria stay to assess.",
|
|
60107
|
+
pagesDetailNote: "One block per page carrying at least one non-conformity, and only its **non-conforming** criteria \u2014 the full grid (every criterion of every page, with its tests and its screenshot) lives in the artifact.",
|
|
60108
|
+
pagesClamped: (n) => `_The detail of ${n} page(s) was dropped from this comment to fit GitHub's limit \u2014 the artifact carries them all._`,
|
|
60109
|
+
scoreboardClamped: (n) => `_${n} page(s) dropped from the table to fit GitHub's limit \u2014 the artifact carries them all._`,
|
|
60110
|
+
noCriterionForFindings: (n) => `${n} finding(s) on this page make no criterion of the standard non-conforming: their rule falls outside every criterion's applicability. They are counted in the columns above, and detailed in the artifact.`
|
|
60089
60111
|
}
|
|
60090
60112
|
};
|
|
60091
60113
|
var MAX_ROWS = 50;
|
|
@@ -60201,23 +60223,106 @@ function perPageTable(result, standard = CORE2, lang = "en") {
|
|
|
60201
60223
|
if (!scope.length) return "";
|
|
60202
60224
|
attributePages(result, scope);
|
|
60203
60225
|
const derived = derivePages(result, scope);
|
|
60204
|
-
|
|
60205
|
-
|
|
60226
|
+
return [`### ${s.perPage}`, "", ...scoreboardTable(result, derived, standard, s, lang), "", ...basisCaveats(result, derived, s, lang)].join("\n");
|
|
60227
|
+
}
|
|
60228
|
+
function scoreboardTable(result, derived, standard, s, lang) {
|
|
60229
|
+
const out2 = [`| ${s.page} | ${s.basis} | ${s.pageRate} | \u{1F534} | \u{1F7E0} | \u{1F7E1} |`, "| --- | --- | ---: | ---: | ---: | ---: |"];
|
|
60206
60230
|
for (const pg of derived) {
|
|
60207
|
-
const
|
|
60208
|
-
const
|
|
60231
|
+
const n = (sev) => severityCount(pg, sev);
|
|
60232
|
+
const rows = pageCriterionRows(result, pg, standard, lang);
|
|
60233
|
+
const cov = pageCoverage(rows);
|
|
60209
60234
|
out2.push(
|
|
60210
|
-
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${formatRate(
|
|
60235
|
+
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${formatRate(pageRatePct(rows), cov.decided, cov.total)} | ${n("bloquant")} | ${n("majeur")} | ${n("mineur")} |`
|
|
60211
60236
|
);
|
|
60212
60237
|
}
|
|
60213
|
-
out2
|
|
60238
|
+
return out2;
|
|
60239
|
+
}
|
|
60240
|
+
function severityCount(pg, sev) {
|
|
60241
|
+
return pg.findings.filter((f) => !f.advisory && f.severity === sev).length;
|
|
60242
|
+
}
|
|
60243
|
+
function basisCaveats(result, derived, s, lang) {
|
|
60244
|
+
const out2 = [];
|
|
60214
60245
|
const orphans = unattributedFindings(result).filter((f) => !f.advisory).length;
|
|
60215
60246
|
if (orphans) out2.push(`> ${s.unattributed(orphans)}`, "");
|
|
60216
60247
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> ${s.sourceBasis}`, "");
|
|
60217
60248
|
const notAudited = pageBasisWarning("not-audited", lang);
|
|
60218
60249
|
if (notAudited && derived.some((p) => p.basis === "not-audited")) out2.push(`> ${notAudited}`, "");
|
|
60250
|
+
return out2;
|
|
60251
|
+
}
|
|
60252
|
+
function pageBlock(result, page, standard, lang) {
|
|
60253
|
+
const s = S[lang];
|
|
60254
|
+
const rows = pageCriterionRows(result, page, standard, lang);
|
|
60255
|
+
const nc = rows.filter((r) => r.status === "NC");
|
|
60256
|
+
const occurrences = page.findings.filter((f) => !f.advisory).length;
|
|
60257
|
+
if (!nc.length && !occurrences) return void 0;
|
|
60258
|
+
const cell = (v) => v.replace(/\|/g, "\\|");
|
|
60259
|
+
const counts = `\u{1F534} ${severityCount(page, "bloquant")} \xB7 \u{1F7E0} ${severityCount(page, "majeur")} \xB7 \u{1F7E1} ${severityCount(page, "mineur")}`;
|
|
60260
|
+
const withTests = nc.some((r) => r.tests.length);
|
|
60261
|
+
const out2 = [
|
|
60262
|
+
"<details>",
|
|
60263
|
+
`<summary><b>${cell(page.name)}</b>${page.auth ? " \u{1F512}" : ""} \u2014 ${counts}</summary>`,
|
|
60264
|
+
// GFM only renders Markdown inside <details> after a blank line; without it the table
|
|
60265
|
+
// ships to the reader as literal pipes.
|
|
60266
|
+
"",
|
|
60267
|
+
`${pageTallyNote(pageTally(rows), lang)}`,
|
|
60268
|
+
""
|
|
60269
|
+
];
|
|
60270
|
+
if (!nc.length) {
|
|
60271
|
+
out2.push(s.noCriterionForFindings(occurrences), "", "</details>");
|
|
60272
|
+
return out2.join("\n");
|
|
60273
|
+
}
|
|
60274
|
+
out2.push(withTests ? `| ${s.criterion} | ${s.testsCol} |` : `| ${s.criterion} |`, withTests ? "| --- | --- |" : "| --- |");
|
|
60275
|
+
for (const r of nc) {
|
|
60276
|
+
out2.push(withTests ? `| ${cell(r.label)} | ${r.tests.map((t2) => `\`${t2}\``).join(" ")} |` : `| ${cell(r.label)} |`);
|
|
60277
|
+
}
|
|
60278
|
+
out2.push("", "</details>");
|
|
60219
60279
|
return out2.join("\n");
|
|
60220
60280
|
}
|
|
60281
|
+
function pagesComment(result, opts = {}) {
|
|
60282
|
+
const standard = opts.standard ?? CORE2;
|
|
60283
|
+
const lang = opts.lang ?? "en";
|
|
60284
|
+
const s = S[lang];
|
|
60285
|
+
const stdLabel = isCore(standard) ? "WCAG 2.2 AA" : loadPack(standard).name;
|
|
60286
|
+
const redirected = result.scope.redirected ?? [];
|
|
60287
|
+
const scope = pagesOf(result);
|
|
60288
|
+
const head = [`### ${s.title} \u2014 ${stdLabel} \xB7 ${s.pageByPage}`, ""];
|
|
60289
|
+
const tail = [];
|
|
60290
|
+
if (opts.artifactName) tail.push(s.artifact(opts.artifactName), "");
|
|
60291
|
+
if (opts.runUrl) tail.push(s.runLink(opts.runUrl), "");
|
|
60292
|
+
if (!scope.length) {
|
|
60293
|
+
head.push(s.noPages, "");
|
|
60294
|
+
if (redirected.length) head.push(...renderRedirected(redirected, lang), "");
|
|
60295
|
+
return [...head, ...tail].join("\n").trimEnd();
|
|
60296
|
+
}
|
|
60297
|
+
attributePages(result, scope);
|
|
60298
|
+
const derived = derivePages(result, scope);
|
|
60299
|
+
const normative = findingsForStandard(result, standard).filter((f) => !f.advisory);
|
|
60300
|
+
const blocking = normative.filter((f) => f.severity === "bloquant").length;
|
|
60301
|
+
const rate = runRate(result, standard, lang);
|
|
60302
|
+
head.push(blocking ? s.verdictFail(blocking) : normative.length ? s.verdictWarn : s.verdictPass, "");
|
|
60303
|
+
head.push(`\`${result.date}\` \xB7 ${s.pagesCount(derived.length)} \xB7 **${rate.text}** ${s.rate}`, "");
|
|
60304
|
+
if (rate.agentRuled) head.push(`> ${agentMarkNote(lang)}`, "");
|
|
60305
|
+
const blocks = [...derived].sort(
|
|
60306
|
+
(a, b) => severityCount(b, "bloquant") - severityCount(a, "bloquant") || severityCount(b, "majeur") - severityCount(a, "majeur") || severityCount(b, "mineur") - severityCount(a, "mineur")
|
|
60307
|
+
).map((p) => pageBlock(result, p, standard, lang)).filter((b) => b !== void 0);
|
|
60308
|
+
const assemble = (nBlocks2, nRows2) => {
|
|
60309
|
+
const body3 = [...scoreboardTable(result, derived.slice(0, nRows2), standard, s, lang), ""];
|
|
60310
|
+
if (nRows2 < derived.length) body3.push(s.scoreboardClamped(derived.length - nRows2), "");
|
|
60311
|
+
body3.push(...basisCaveats(result, derived, s, lang));
|
|
60312
|
+
if (redirected.length) body3.push(...renderRedirected(redirected, lang), "");
|
|
60313
|
+
if (blocks.length) {
|
|
60314
|
+
body3.push(`> ${s.pagesDetailNote}`, "");
|
|
60315
|
+
body3.push(...blocks.slice(0, nBlocks2).flatMap((b) => [b, ""]));
|
|
60316
|
+
if (nBlocks2 < blocks.length) body3.push(s.pagesClamped(blocks.length - nBlocks2), "");
|
|
60317
|
+
}
|
|
60318
|
+
return [...head, ...body3, ...tail].join("\n").trimEnd();
|
|
60319
|
+
};
|
|
60320
|
+
let nBlocks = blocks.length;
|
|
60321
|
+
let nRows = derived.length;
|
|
60322
|
+
while (nBlocks > 0 && assemble(nBlocks, nRows).length > COMMENT_LIMIT) nBlocks--;
|
|
60323
|
+
while (nRows > 0 && assemble(nBlocks, nRows).length > COMMENT_LIMIT) nRows--;
|
|
60324
|
+
return assemble(nBlocks, nRows);
|
|
60325
|
+
}
|
|
60221
60326
|
|
|
60222
60327
|
// src/evidence.ts
|
|
60223
60328
|
import { createHash as createHash6 } from "crypto";
|
|
@@ -65032,7 +65137,9 @@ function emitCiFormat(result, format, standard, lang, failOn) {
|
|
|
65032
65137
|
console.error(md);
|
|
65033
65138
|
}
|
|
65034
65139
|
if (process.env.ULTRA11Y_PR_COMMENT === "1") {
|
|
65035
|
-
const
|
|
65140
|
+
const kind = commentKindFrom(process.env.ULTRA11Y_PR_COMMENT_KIND);
|
|
65141
|
+
const render2 = kind === "pages" ? pagesComment : prComment;
|
|
65142
|
+
const body3 = render2(result, {
|
|
65036
65143
|
standard,
|
|
65037
65144
|
lang,
|
|
65038
65145
|
// The run is known before the artifact exists, so the link is always safe. The artifact
|
|
@@ -65041,7 +65148,7 @@ function emitCiFormat(result, format, standard, lang, failOn) {
|
|
|
65041
65148
|
...process.env.ULTRA11Y_RUN_URL ? { runUrl: process.env.ULTRA11Y_RUN_URL } : {},
|
|
65042
65149
|
...process.env.ULTRA11Y_ARTIFACT_NAME ? { artifactName: process.env.ULTRA11Y_ARTIFACT_NAME } : {}
|
|
65043
65150
|
});
|
|
65044
|
-
const c2 = pushPrComment(
|
|
65151
|
+
const c2 = pushPrComment(body3, standard, kind);
|
|
65045
65152
|
console.error(
|
|
65046
65153
|
c2.ok ? lang === "fr" ? `ultra11y : commentaire de PR ${c2.action === "updated" ? "mis \xE0 jour" : c2.action === "created" ? "cr\xE9\xE9" : "ignor\xE9"}${c2.reason ? ` (${c2.reason})` : ""}.` : `ultra11y: PR comment ${c2.action}${c2.reason ? ` (${c2.reason})` : ""}.` : lang === "fr" ? `ultra11y : commentaire de PR impossible${c2.reason ? ` \u2014 ${c2.reason}` : ""}.` : `ultra11y: PR comment failed${c2.reason ? ` \u2014 ${c2.reason}` : ""}.`
|
|
65047
65154
|
);
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibili
|
|
|
4
4
|
when_to_use: "Invoke when the user asks for an accessibility AUDIT or a formal deliverable: audit a repo, site or page against WCAG 2.2 AA or a country standard (RGAA, Section 508, EN 301 549); produce a dated conformance report, a criterion grid, a PRD backlog, or tickets (GitHub/GitLab/Jira); author accessible markup from scratch; wire the repo gate (init --hook/--ci) or a rendered-DOM capture pipeline. For a review of the code UNDER CHANGE (diff, branch, PR), use the `review-a11y` skill instead."
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: 5.2
|
|
7
|
+
version: 5.3.2
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -332,10 +332,42 @@ drive the judgment and content stages:
|
|
|
332
332
|
ONE building block, in the language of this conversation (pass `--lang` explicitly —
|
|
333
333
|
Core rule 5).
|
|
334
334
|
|
|
335
|
+
6. **Hand the change over to `review-a11y`, in a subagent.** Once the deliverable is out,
|
|
336
|
+
the code you just fixed still has to be reviewed as a *change*, and that is the other
|
|
337
|
+
skill's job — see **Handing over to `review-a11y`** below.
|
|
338
|
+
|
|
335
339
|
**Stop** when `check` and `verify --apply` are green again and only explicitly-named
|
|
336
340
|
residual risks remain. (To automate the outer cadence, the harness `/loop` command can
|
|
337
341
|
re-run this cycle.)
|
|
338
342
|
|
|
343
|
+
## Handing over to `review-a11y`
|
|
344
|
+
|
|
345
|
+
This skill does the analysis end to end: audit, render, adjudicate, fix, re-audit, deliver.
|
|
346
|
+
When it has produced fixes, **dispatch `review-a11y` on the code under change as a subagent**,
|
|
347
|
+
and report what it returns.
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
Agent(subagent_type: "general-purpose", prompt: "Use the review-a11y skill on the working diff. Return its report verbatim.")
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Two skills, two scopes, and the split is not cosmetic:
|
|
354
|
+
|
|
355
|
+
| | scope | asks |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| `ultra11y` (this one) | the repository, a page sample, a standard | *does this product conform, and where is the proof?* |
|
|
358
|
+
| `review-a11y` | exactly the code under change (staged, diff, or branch vs merge-base) | *is this change safe to merge?* |
|
|
359
|
+
|
|
360
|
+
**Why a subagent rather than an inline pass.** A full audit fills a context with a 3 MB
|
|
361
|
+
engine's output, dozens of reference files and a page sample; a review has to read the diff
|
|
362
|
+
closely with none of that in the way. Handing it over keeps the review's judgment
|
|
363
|
+
independent of the audit that just ran — the same reason `verify` refutes findings instead
|
|
364
|
+
of trusting them. If the harness has no subagent tool, invoke `review-a11y` directly: the
|
|
365
|
+
routing table in `references/orchestration.md` names the fallbacks, and the review is
|
|
366
|
+
identical either way.
|
|
367
|
+
|
|
368
|
+
**Do not paraphrase its verdict.** It returns a severity-ranked review with `file:line` and a
|
|
369
|
+
one-line verdict; that is the deliverable, not a summary of it.
|
|
370
|
+
|
|
339
371
|
## Combining engine, judgment and residual risk
|
|
340
372
|
|
|
341
373
|
`audit` classes each criterion `C`/`NC`/`NA` for the static subset and `manual` for the rest,
|