ultra11y 5.3.2 → 5.3.4
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 +47 -12
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +47 -12
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/references/judgment.md +25 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +47 -12
- package/src/adjudicate.ts +70 -4
- package/src/annotate.ts +32 -14
- package/src/cli.ts +13 -1
- 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.3.
|
|
13
|
+
"version": "5.3.4",
|
|
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.3.
|
|
4
|
+
"version": "5.3.4",
|
|
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.3.
|
|
4
|
+
"version": "5.3.4",
|
|
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.3.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.3.4";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.4",
|
|
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.3.
|
|
9
|
+
var VERSION = "5.3.4";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -56773,11 +56773,11 @@ function packGuidanceBlock(standard, criterionId, lang) {
|
|
|
56773
56773
|
function plainTest(s) {
|
|
56774
56774
|
return s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
|
|
56775
56775
|
}
|
|
56776
|
-
function formatAdjudication(items, lang = "en", standard = CORE2) {
|
|
56776
|
+
function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
|
|
56777
56777
|
const s = T2[lang];
|
|
56778
56778
|
const pack = isCore(standard) ? void 0 : loadPack(standard);
|
|
56779
|
-
const out2 = [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
|
|
56780
|
-
if (pack) out2.push(`> ${s.packIntro(pack.name)}`, "");
|
|
56779
|
+
const out2 = opts.preamble === false ? [] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
|
|
56780
|
+
if (pack && opts.preamble !== false) out2.push(`> ${s.packIntro(pack.name)}`, "");
|
|
56781
56781
|
for (const it of items) {
|
|
56782
56782
|
out2.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
|
|
56783
56783
|
out2.push("", `> ${s.evidence} (${it.evidence.length}${it.evidenceTruncated ? ` / ${it.evidenceTruncated.total}` : ""}):`, "");
|
|
@@ -56838,7 +56838,31 @@ function writeAdjudication(items, outDir, opts) {
|
|
|
56838
56838
|
};
|
|
56839
56839
|
writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
|
|
56840
56840
|
writeFileSync10(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
|
|
56841
|
-
|
|
56841
|
+
const verdictsPath = join35(outDir, "ADJUDICATE.verdicts.json");
|
|
56842
|
+
writeFileSync10(verdictsPath, JSON.stringify({ ...file, items: slimAdjudicationItems(items) }, null, 2) + "\n");
|
|
56843
|
+
const itemsDir = join35(outDir, "adjudicate");
|
|
56844
|
+
mkdirSync9(itemsDir, { recursive: true });
|
|
56845
|
+
for (const it of items) {
|
|
56846
|
+
writeFileSync10(join35(itemsDir, `${it.criteriaId}.md`), formatAdjudication([it], opts.lang ?? "en", opts.standard, { preamble: false }));
|
|
56847
|
+
}
|
|
56848
|
+
return { todoPath, mdPath, verdictsPath, itemsDir, count: items.length };
|
|
56849
|
+
}
|
|
56850
|
+
function slimAdjudicationItems(items) {
|
|
56851
|
+
return items.map((it) => {
|
|
56852
|
+
const { evidence: _evidence, evidenceTruncated: _truncated, ...rest } = it;
|
|
56853
|
+
return { ...rest, evidence: [] };
|
|
56854
|
+
});
|
|
56855
|
+
}
|
|
56856
|
+
function hydrateAdjudication(adj, audit2, opts = {}) {
|
|
56857
|
+
const needs = adj.items.filter((it) => !it.evidence || it.evidence.length === 0);
|
|
56858
|
+
if (!needs.length) return;
|
|
56859
|
+
const derived = new Map(buildAdjudicationWorklist(audit2, { ...opts, standard: adj.standard }).map((it) => [it.criteriaId, it]));
|
|
56860
|
+
for (const it of needs) {
|
|
56861
|
+
const full = derived.get(it.criteriaId);
|
|
56862
|
+
if (!full) continue;
|
|
56863
|
+
it.evidence = full.evidence;
|
|
56864
|
+
if (full.evidenceTruncated) it.evidenceTruncated = full.evidenceTruncated;
|
|
56865
|
+
}
|
|
56842
56866
|
}
|
|
56843
56867
|
|
|
56844
56868
|
// src/llm.ts
|
|
@@ -60058,10 +60082,14 @@ var S = {
|
|
|
60058
60082
|
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._`,
|
|
60059
60083
|
unanchored: (n) => `${n} constat(s) rattach\xE9(s) \xE0 une URL, sans ligne de code \xE0 annoter \u2014 voir le rapport.`,
|
|
60060
60084
|
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.`,
|
|
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
|
|
60085
|
+
sourceBasis: "Une page marqu\xE9e \xAB source \xBB n'a pas d'instantan\xE9 : l'absence de constat n'y vaut PAS conformit\xE9. Ses crit\xE8res non d\xE9cid\xE9s restent \xAB \xE0 \xE9valuer \xBB, ils ne basculent jamais en conformes par silence.",
|
|
60062
60086
|
pageByPage: "page par page",
|
|
60063
60087
|
pagesCount: (n) => `${n} page(s)`,
|
|
60064
60088
|
testsCol: "Tests",
|
|
60089
|
+
conformingCol: "C",
|
|
60090
|
+
nonConformingCol: "NC",
|
|
60091
|
+
toAssessCol: "\xC0 \xE9valuer",
|
|
60092
|
+
scoreboardNote: "`C` conforme \xB7 `NC` non conforme \xB7 \xAB \xC0 \xE9valuer \xBB : personne ne l'a encore tranch\xE9 \u2014 ni conforme ni non conforme. Pas de pourcentage ici : un taux calcul\xE9 sur les seuls crit\xE8res d\xE9cid\xE9s se lit comme une note de page, et vaut la m\xEAme chose sur une bonne page que sur une mauvaise. Le taux et sa couverture sont dans la fiche par page de l'artefact.",
|
|
60065
60093
|
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
60094
|
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
60095
|
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._`,
|
|
@@ -60099,10 +60127,14 @@ var S = {
|
|
|
60099
60127
|
clamped: (n) => `_${n} group(s) dropped from this comment to fit GitHub's limit \u2014 the job summary carries them all._`,
|
|
60100
60128
|
unanchored: (n) => `${n} finding(s) keyed to a URL, with no code line to annotate \u2014 see the report.`,
|
|
60101
60129
|
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.`,
|
|
60102
|
-
sourceBasis: 'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming
|
|
60130
|
+
sourceBasis: 'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming. Its undecided criteria stay \u201Cto assess\u201D; they never turn conforming by silence.',
|
|
60103
60131
|
pageByPage: "page by page",
|
|
60104
60132
|
pagesCount: (n) => `${n} page(s)`,
|
|
60105
60133
|
testsCol: "Tests",
|
|
60134
|
+
conformingCol: "C",
|
|
60135
|
+
nonConformingCol: "NC",
|
|
60136
|
+
toAssessCol: "To assess",
|
|
60137
|
+
scoreboardNote: "`C` conforming \xB7 `NC` non-conforming \xB7 \u201CTo assess\u201D: nobody has ruled on it yet \u2014 neither conforming nor not. No percentage here: a rate over the decided criteria alone reads as a page score, and reads the same on a good page as on a bad one. The rate and its coverage live in the artifact's per-page sheet.",
|
|
60106
60138
|
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
60139
|
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
60140
|
pagesClamped: (n) => `_The detail of ${n} page(s) was dropped from this comment to fit GitHub's limit \u2014 the artifact carries them all._`,
|
|
@@ -60226,13 +60258,15 @@ function perPageTable(result, standard = CORE2, lang = "en") {
|
|
|
60226
60258
|
return [`### ${s.perPage}`, "", ...scoreboardTable(result, derived, standard, s, lang), "", ...basisCaveats(result, derived, s, lang)].join("\n");
|
|
60227
60259
|
}
|
|
60228
60260
|
function scoreboardTable(result, derived, standard, s, lang) {
|
|
60229
|
-
const out2 = [
|
|
60261
|
+
const out2 = [
|
|
60262
|
+
`| ${s.page} | ${s.basis} | ${s.conformingCol} | ${s.nonConformingCol} | ${s.toAssessCol} | \u{1F534} | \u{1F7E0} | \u{1F7E1} |`,
|
|
60263
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |"
|
|
60264
|
+
];
|
|
60230
60265
|
for (const pg of derived) {
|
|
60231
60266
|
const n = (sev) => severityCount(pg, sev);
|
|
60232
|
-
const
|
|
60233
|
-
const cov = pageCoverage(rows);
|
|
60267
|
+
const t2 = pageTally(pageCriterionRows(result, pg, standard, lang));
|
|
60234
60268
|
out2.push(
|
|
60235
|
-
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${
|
|
60269
|
+
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${t2.c} | ${t2.nc} | ${t2.manual} | ${n("bloquant")} | ${n("majeur")} | ${n("mineur")} |`
|
|
60236
60270
|
);
|
|
60237
60271
|
}
|
|
60238
60272
|
return out2;
|
|
@@ -60241,7 +60275,7 @@ function severityCount(pg, sev) {
|
|
|
60241
60275
|
return pg.findings.filter((f) => !f.advisory && f.severity === sev).length;
|
|
60242
60276
|
}
|
|
60243
60277
|
function basisCaveats(result, derived, s, lang) {
|
|
60244
|
-
const out2 = [];
|
|
60278
|
+
const out2 = [`> ${s.scoreboardNote}`, ""];
|
|
60245
60279
|
const orphans = unattributedFindings(result).filter((f) => !f.advisory).length;
|
|
60246
60280
|
if (orphans) out2.push(`> ${s.unattributed(orphans)}`, "");
|
|
60247
60281
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> ${s.sourceBasis}`, "");
|
|
@@ -66549,6 +66583,7 @@ function applyAdjudicationFile(p, adj, lang) {
|
|
|
66549
66583
|
console.error(`ultra11y verify: --in file not found or not valid JSON: ${inFlag}.`);
|
|
66550
66584
|
return 2;
|
|
66551
66585
|
}
|
|
66586
|
+
hydrateAdjudication(adj, audit2, { cwd: typeof p.flags.cwd === "string" ? p.flags.cwd : void 0 });
|
|
66552
66587
|
const r = applyAdjudication(audit2, adj);
|
|
66553
66588
|
if (!r.ok) {
|
|
66554
66589
|
if (p.flags.json) console.log(JSON.stringify(r, null, 2));
|
|
@@ -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.3.
|
|
8
|
+
version: 5.3.4
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -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.3.
|
|
9
|
+
var VERSION = "5.3.4";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -56773,11 +56773,11 @@ function packGuidanceBlock(standard, criterionId, lang) {
|
|
|
56773
56773
|
function plainTest(s) {
|
|
56774
56774
|
return s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
|
|
56775
56775
|
}
|
|
56776
|
-
function formatAdjudication(items, lang = "en", standard = CORE2) {
|
|
56776
|
+
function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
|
|
56777
56777
|
const s = T2[lang];
|
|
56778
56778
|
const pack = isCore(standard) ? void 0 : loadPack(standard);
|
|
56779
|
-
const out2 = [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
|
|
56780
|
-
if (pack) out2.push(`> ${s.packIntro(pack.name)}`, "");
|
|
56779
|
+
const out2 = opts.preamble === false ? [] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
|
|
56780
|
+
if (pack && opts.preamble !== false) out2.push(`> ${s.packIntro(pack.name)}`, "");
|
|
56781
56781
|
for (const it of items) {
|
|
56782
56782
|
out2.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
|
|
56783
56783
|
out2.push("", `> ${s.evidence} (${it.evidence.length}${it.evidenceTruncated ? ` / ${it.evidenceTruncated.total}` : ""}):`, "");
|
|
@@ -56838,7 +56838,31 @@ function writeAdjudication(items, outDir, opts) {
|
|
|
56838
56838
|
};
|
|
56839
56839
|
writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
|
|
56840
56840
|
writeFileSync10(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
|
|
56841
|
-
|
|
56841
|
+
const verdictsPath = join35(outDir, "ADJUDICATE.verdicts.json");
|
|
56842
|
+
writeFileSync10(verdictsPath, JSON.stringify({ ...file, items: slimAdjudicationItems(items) }, null, 2) + "\n");
|
|
56843
|
+
const itemsDir = join35(outDir, "adjudicate");
|
|
56844
|
+
mkdirSync9(itemsDir, { recursive: true });
|
|
56845
|
+
for (const it of items) {
|
|
56846
|
+
writeFileSync10(join35(itemsDir, `${it.criteriaId}.md`), formatAdjudication([it], opts.lang ?? "en", opts.standard, { preamble: false }));
|
|
56847
|
+
}
|
|
56848
|
+
return { todoPath, mdPath, verdictsPath, itemsDir, count: items.length };
|
|
56849
|
+
}
|
|
56850
|
+
function slimAdjudicationItems(items) {
|
|
56851
|
+
return items.map((it) => {
|
|
56852
|
+
const { evidence: _evidence, evidenceTruncated: _truncated, ...rest } = it;
|
|
56853
|
+
return { ...rest, evidence: [] };
|
|
56854
|
+
});
|
|
56855
|
+
}
|
|
56856
|
+
function hydrateAdjudication(adj, audit2, opts = {}) {
|
|
56857
|
+
const needs = adj.items.filter((it) => !it.evidence || it.evidence.length === 0);
|
|
56858
|
+
if (!needs.length) return;
|
|
56859
|
+
const derived = new Map(buildAdjudicationWorklist(audit2, { ...opts, standard: adj.standard }).map((it) => [it.criteriaId, it]));
|
|
56860
|
+
for (const it of needs) {
|
|
56861
|
+
const full = derived.get(it.criteriaId);
|
|
56862
|
+
if (!full) continue;
|
|
56863
|
+
it.evidence = full.evidence;
|
|
56864
|
+
if (full.evidenceTruncated) it.evidenceTruncated = full.evidenceTruncated;
|
|
56865
|
+
}
|
|
56842
56866
|
}
|
|
56843
56867
|
|
|
56844
56868
|
// src/llm.ts
|
|
@@ -60058,10 +60082,14 @@ var S = {
|
|
|
60058
60082
|
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._`,
|
|
60059
60083
|
unanchored: (n) => `${n} constat(s) rattach\xE9(s) \xE0 une URL, sans ligne de code \xE0 annoter \u2014 voir le rapport.`,
|
|
60060
60084
|
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.`,
|
|
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
|
|
60085
|
+
sourceBasis: "Une page marqu\xE9e \xAB source \xBB n'a pas d'instantan\xE9 : l'absence de constat n'y vaut PAS conformit\xE9. Ses crit\xE8res non d\xE9cid\xE9s restent \xAB \xE0 \xE9valuer \xBB, ils ne basculent jamais en conformes par silence.",
|
|
60062
60086
|
pageByPage: "page par page",
|
|
60063
60087
|
pagesCount: (n) => `${n} page(s)`,
|
|
60064
60088
|
testsCol: "Tests",
|
|
60089
|
+
conformingCol: "C",
|
|
60090
|
+
nonConformingCol: "NC",
|
|
60091
|
+
toAssessCol: "\xC0 \xE9valuer",
|
|
60092
|
+
scoreboardNote: "`C` conforme \xB7 `NC` non conforme \xB7 \xAB \xC0 \xE9valuer \xBB : personne ne l'a encore tranch\xE9 \u2014 ni conforme ni non conforme. Pas de pourcentage ici : un taux calcul\xE9 sur les seuls crit\xE8res d\xE9cid\xE9s se lit comme une note de page, et vaut la m\xEAme chose sur une bonne page que sur une mauvaise. Le taux et sa couverture sont dans la fiche par page de l'artefact.",
|
|
60065
60093
|
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
60094
|
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
60095
|
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._`,
|
|
@@ -60099,10 +60127,14 @@ var S = {
|
|
|
60099
60127
|
clamped: (n) => `_${n} group(s) dropped from this comment to fit GitHub's limit \u2014 the job summary carries them all._`,
|
|
60100
60128
|
unanchored: (n) => `${n} finding(s) keyed to a URL, with no code line to annotate \u2014 see the report.`,
|
|
60101
60129
|
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.`,
|
|
60102
|
-
sourceBasis: 'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming
|
|
60130
|
+
sourceBasis: 'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming. Its undecided criteria stay \u201Cto assess\u201D; they never turn conforming by silence.',
|
|
60103
60131
|
pageByPage: "page by page",
|
|
60104
60132
|
pagesCount: (n) => `${n} page(s)`,
|
|
60105
60133
|
testsCol: "Tests",
|
|
60134
|
+
conformingCol: "C",
|
|
60135
|
+
nonConformingCol: "NC",
|
|
60136
|
+
toAssessCol: "To assess",
|
|
60137
|
+
scoreboardNote: "`C` conforming \xB7 `NC` non-conforming \xB7 \u201CTo assess\u201D: nobody has ruled on it yet \u2014 neither conforming nor not. No percentage here: a rate over the decided criteria alone reads as a page score, and reads the same on a good page as on a bad one. The rate and its coverage live in the artifact's per-page sheet.",
|
|
60106
60138
|
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
60139
|
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
60140
|
pagesClamped: (n) => `_The detail of ${n} page(s) was dropped from this comment to fit GitHub's limit \u2014 the artifact carries them all._`,
|
|
@@ -60226,13 +60258,15 @@ function perPageTable(result, standard = CORE2, lang = "en") {
|
|
|
60226
60258
|
return [`### ${s.perPage}`, "", ...scoreboardTable(result, derived, standard, s, lang), "", ...basisCaveats(result, derived, s, lang)].join("\n");
|
|
60227
60259
|
}
|
|
60228
60260
|
function scoreboardTable(result, derived, standard, s, lang) {
|
|
60229
|
-
const out2 = [
|
|
60261
|
+
const out2 = [
|
|
60262
|
+
`| ${s.page} | ${s.basis} | ${s.conformingCol} | ${s.nonConformingCol} | ${s.toAssessCol} | \u{1F534} | \u{1F7E0} | \u{1F7E1} |`,
|
|
60263
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |"
|
|
60264
|
+
];
|
|
60230
60265
|
for (const pg of derived) {
|
|
60231
60266
|
const n = (sev) => severityCount(pg, sev);
|
|
60232
|
-
const
|
|
60233
|
-
const cov = pageCoverage(rows);
|
|
60267
|
+
const t2 = pageTally(pageCriterionRows(result, pg, standard, lang));
|
|
60234
60268
|
out2.push(
|
|
60235
|
-
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${
|
|
60269
|
+
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${t2.c} | ${t2.nc} | ${t2.manual} | ${n("bloquant")} | ${n("majeur")} | ${n("mineur")} |`
|
|
60236
60270
|
);
|
|
60237
60271
|
}
|
|
60238
60272
|
return out2;
|
|
@@ -60241,7 +60275,7 @@ function severityCount(pg, sev) {
|
|
|
60241
60275
|
return pg.findings.filter((f) => !f.advisory && f.severity === sev).length;
|
|
60242
60276
|
}
|
|
60243
60277
|
function basisCaveats(result, derived, s, lang) {
|
|
60244
|
-
const out2 = [];
|
|
60278
|
+
const out2 = [`> ${s.scoreboardNote}`, ""];
|
|
60245
60279
|
const orphans = unattributedFindings(result).filter((f) => !f.advisory).length;
|
|
60246
60280
|
if (orphans) out2.push(`> ${s.unattributed(orphans)}`, "");
|
|
60247
60281
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> ${s.sourceBasis}`, "");
|
|
@@ -66549,6 +66583,7 @@ function applyAdjudicationFile(p, adj, lang) {
|
|
|
66549
66583
|
console.error(`ultra11y verify: --in file not found or not valid JSON: ${inFlag}.`);
|
|
66550
66584
|
return 2;
|
|
66551
66585
|
}
|
|
66586
|
+
hydrateAdjudication(adj, audit2, { cwd: typeof p.flags.cwd === "string" ? p.flags.cwd : void 0 });
|
|
66552
66587
|
const r = applyAdjudication(audit2, adj);
|
|
66553
66588
|
if (!r.ok) {
|
|
66554
66589
|
if (p.flags.json) console.log(JSON.stringify(r, null, 2));
|
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.3.
|
|
7
|
+
version: 5.3.4
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -71,6 +71,31 @@ test).
|
|
|
71
71
|
the **produced** HTML (see `render` / audit the build), otherwise the verdict is a false
|
|
72
72
|
negative.
|
|
73
73
|
|
|
74
|
+
## When the adjudicator has no shell (CI)
|
|
75
|
+
|
|
76
|
+
`verify --manual` writes the worklist **twice**, plus one brief per criterion:
|
|
77
|
+
|
|
78
|
+
| file | for |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `ADJUDICATE.todo.json` · `ADJUDICATE.md` | a session **with a shell**: evidence inline, edit in place, fold yourself |
|
|
81
|
+
| `ADJUDICATE.verdicts.json` | the **only file to write** when you have no shell — verdicts, no evidence |
|
|
82
|
+
| `adjudicate/<criteriaId>.md` | one small brief per criterion: its evidence, its protocol, its citable tests |
|
|
83
|
+
|
|
84
|
+
Why the split exists, measured rather than assumed: under RGAA the worklist is 96 criteria
|
|
85
|
+
carrying 1590 harvested anchors — **536 KB** of JSON and **466 KB** of Markdown. An agent given
|
|
86
|
+
`Read/Grep/Glob/Edit/Write` cannot work with that: reading either document swamps its context,
|
|
87
|
+
and filling 96 verdicts inside half a megabyte is 96 exact-match edits. A real CI run spent 75
|
|
88
|
+
of 424 turns, hit 17 permission denials trying to run the commands the runbook prescribes, and
|
|
89
|
+
returned the file untouched — so the fail-closed fold discarded all 96 verdicts and every
|
|
90
|
+
criterion stayed « to assess », in a job that reported success. The verdicts file is **37 KB**
|
|
91
|
+
for the same worklist, and each brief is a few KB.
|
|
92
|
+
|
|
93
|
+
`verify --apply` accepts either file. Given the verdicts-only one it **re-derives the evidence
|
|
94
|
+
from the audit** (the worklist is a pure function of the audit) and then runs the identical
|
|
95
|
+
fold: same coverage checks in both directions, same citation matching against *that criterion's*
|
|
96
|
+
own anchors, same refusals. The smaller surface buys nothing from the gate — a `C` that cites
|
|
97
|
+
an anchor it was never shown is refused exactly as before.
|
|
98
|
+
|
|
74
99
|
## Adversarial verification of the non-conformities — `verify --report`
|
|
75
100
|
|
|
76
101
|
Unchanged: `node scripts/ultra11y.mjs verify --report audits/wcag-YYYY-MM-DD.md` writes `VERIFY.md`
|
|
@@ -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.3.
|
|
9
|
+
var VERSION = "5.3.4";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -56773,11 +56773,11 @@ function packGuidanceBlock(standard, criterionId, lang) {
|
|
|
56773
56773
|
function plainTest(s) {
|
|
56774
56774
|
return s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
|
|
56775
56775
|
}
|
|
56776
|
-
function formatAdjudication(items, lang = "en", standard = CORE2) {
|
|
56776
|
+
function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
|
|
56777
56777
|
const s = T2[lang];
|
|
56778
56778
|
const pack = isCore(standard) ? void 0 : loadPack(standard);
|
|
56779
|
-
const out2 = [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
|
|
56780
|
-
if (pack) out2.push(`> ${s.packIntro(pack.name)}`, "");
|
|
56779
|
+
const out2 = opts.preamble === false ? [] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
|
|
56780
|
+
if (pack && opts.preamble !== false) out2.push(`> ${s.packIntro(pack.name)}`, "");
|
|
56781
56781
|
for (const it of items) {
|
|
56782
56782
|
out2.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
|
|
56783
56783
|
out2.push("", `> ${s.evidence} (${it.evidence.length}${it.evidenceTruncated ? ` / ${it.evidenceTruncated.total}` : ""}):`, "");
|
|
@@ -56838,7 +56838,31 @@ function writeAdjudication(items, outDir, opts) {
|
|
|
56838
56838
|
};
|
|
56839
56839
|
writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
|
|
56840
56840
|
writeFileSync10(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
|
|
56841
|
-
|
|
56841
|
+
const verdictsPath = join35(outDir, "ADJUDICATE.verdicts.json");
|
|
56842
|
+
writeFileSync10(verdictsPath, JSON.stringify({ ...file, items: slimAdjudicationItems(items) }, null, 2) + "\n");
|
|
56843
|
+
const itemsDir = join35(outDir, "adjudicate");
|
|
56844
|
+
mkdirSync9(itemsDir, { recursive: true });
|
|
56845
|
+
for (const it of items) {
|
|
56846
|
+
writeFileSync10(join35(itemsDir, `${it.criteriaId}.md`), formatAdjudication([it], opts.lang ?? "en", opts.standard, { preamble: false }));
|
|
56847
|
+
}
|
|
56848
|
+
return { todoPath, mdPath, verdictsPath, itemsDir, count: items.length };
|
|
56849
|
+
}
|
|
56850
|
+
function slimAdjudicationItems(items) {
|
|
56851
|
+
return items.map((it) => {
|
|
56852
|
+
const { evidence: _evidence, evidenceTruncated: _truncated, ...rest } = it;
|
|
56853
|
+
return { ...rest, evidence: [] };
|
|
56854
|
+
});
|
|
56855
|
+
}
|
|
56856
|
+
function hydrateAdjudication(adj, audit2, opts = {}) {
|
|
56857
|
+
const needs = adj.items.filter((it) => !it.evidence || it.evidence.length === 0);
|
|
56858
|
+
if (!needs.length) return;
|
|
56859
|
+
const derived = new Map(buildAdjudicationWorklist(audit2, { ...opts, standard: adj.standard }).map((it) => [it.criteriaId, it]));
|
|
56860
|
+
for (const it of needs) {
|
|
56861
|
+
const full = derived.get(it.criteriaId);
|
|
56862
|
+
if (!full) continue;
|
|
56863
|
+
it.evidence = full.evidence;
|
|
56864
|
+
if (full.evidenceTruncated) it.evidenceTruncated = full.evidenceTruncated;
|
|
56865
|
+
}
|
|
56842
56866
|
}
|
|
56843
56867
|
|
|
56844
56868
|
// src/llm.ts
|
|
@@ -60058,10 +60082,14 @@ var S = {
|
|
|
60058
60082
|
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._`,
|
|
60059
60083
|
unanchored: (n) => `${n} constat(s) rattach\xE9(s) \xE0 une URL, sans ligne de code \xE0 annoter \u2014 voir le rapport.`,
|
|
60060
60084
|
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.`,
|
|
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
|
|
60085
|
+
sourceBasis: "Une page marqu\xE9e \xAB source \xBB n'a pas d'instantan\xE9 : l'absence de constat n'y vaut PAS conformit\xE9. Ses crit\xE8res non d\xE9cid\xE9s restent \xAB \xE0 \xE9valuer \xBB, ils ne basculent jamais en conformes par silence.",
|
|
60062
60086
|
pageByPage: "page par page",
|
|
60063
60087
|
pagesCount: (n) => `${n} page(s)`,
|
|
60064
60088
|
testsCol: "Tests",
|
|
60089
|
+
conformingCol: "C",
|
|
60090
|
+
nonConformingCol: "NC",
|
|
60091
|
+
toAssessCol: "\xC0 \xE9valuer",
|
|
60092
|
+
scoreboardNote: "`C` conforme \xB7 `NC` non conforme \xB7 \xAB \xC0 \xE9valuer \xBB : personne ne l'a encore tranch\xE9 \u2014 ni conforme ni non conforme. Pas de pourcentage ici : un taux calcul\xE9 sur les seuls crit\xE8res d\xE9cid\xE9s se lit comme une note de page, et vaut la m\xEAme chose sur une bonne page que sur une mauvaise. Le taux et sa couverture sont dans la fiche par page de l'artefact.",
|
|
60065
60093
|
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
60094
|
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
60095
|
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._`,
|
|
@@ -60099,10 +60127,14 @@ var S = {
|
|
|
60099
60127
|
clamped: (n) => `_${n} group(s) dropped from this comment to fit GitHub's limit \u2014 the job summary carries them all._`,
|
|
60100
60128
|
unanchored: (n) => `${n} finding(s) keyed to a URL, with no code line to annotate \u2014 see the report.`,
|
|
60101
60129
|
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.`,
|
|
60102
|
-
sourceBasis: 'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming
|
|
60130
|
+
sourceBasis: 'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming. Its undecided criteria stay \u201Cto assess\u201D; they never turn conforming by silence.',
|
|
60103
60131
|
pageByPage: "page by page",
|
|
60104
60132
|
pagesCount: (n) => `${n} page(s)`,
|
|
60105
60133
|
testsCol: "Tests",
|
|
60134
|
+
conformingCol: "C",
|
|
60135
|
+
nonConformingCol: "NC",
|
|
60136
|
+
toAssessCol: "To assess",
|
|
60137
|
+
scoreboardNote: "`C` conforming \xB7 `NC` non-conforming \xB7 \u201CTo assess\u201D: nobody has ruled on it yet \u2014 neither conforming nor not. No percentage here: a rate over the decided criteria alone reads as a page score, and reads the same on a good page as on a bad one. The rate and its coverage live in the artifact's per-page sheet.",
|
|
60106
60138
|
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
60139
|
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
60140
|
pagesClamped: (n) => `_The detail of ${n} page(s) was dropped from this comment to fit GitHub's limit \u2014 the artifact carries them all._`,
|
|
@@ -60226,13 +60258,15 @@ function perPageTable(result, standard = CORE2, lang = "en") {
|
|
|
60226
60258
|
return [`### ${s.perPage}`, "", ...scoreboardTable(result, derived, standard, s, lang), "", ...basisCaveats(result, derived, s, lang)].join("\n");
|
|
60227
60259
|
}
|
|
60228
60260
|
function scoreboardTable(result, derived, standard, s, lang) {
|
|
60229
|
-
const out2 = [
|
|
60261
|
+
const out2 = [
|
|
60262
|
+
`| ${s.page} | ${s.basis} | ${s.conformingCol} | ${s.nonConformingCol} | ${s.toAssessCol} | \u{1F534} | \u{1F7E0} | \u{1F7E1} |`,
|
|
60263
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |"
|
|
60264
|
+
];
|
|
60230
60265
|
for (const pg of derived) {
|
|
60231
60266
|
const n = (sev) => severityCount(pg, sev);
|
|
60232
|
-
const
|
|
60233
|
-
const cov = pageCoverage(rows);
|
|
60267
|
+
const t2 = pageTally(pageCriterionRows(result, pg, standard, lang));
|
|
60234
60268
|
out2.push(
|
|
60235
|
-
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${
|
|
60269
|
+
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${t2.c} | ${t2.nc} | ${t2.manual} | ${n("bloquant")} | ${n("majeur")} | ${n("mineur")} |`
|
|
60236
60270
|
);
|
|
60237
60271
|
}
|
|
60238
60272
|
return out2;
|
|
@@ -60241,7 +60275,7 @@ function severityCount(pg, sev) {
|
|
|
60241
60275
|
return pg.findings.filter((f) => !f.advisory && f.severity === sev).length;
|
|
60242
60276
|
}
|
|
60243
60277
|
function basisCaveats(result, derived, s, lang) {
|
|
60244
|
-
const out2 = [];
|
|
60278
|
+
const out2 = [`> ${s.scoreboardNote}`, ""];
|
|
60245
60279
|
const orphans = unattributedFindings(result).filter((f) => !f.advisory).length;
|
|
60246
60280
|
if (orphans) out2.push(`> ${s.unattributed(orphans)}`, "");
|
|
60247
60281
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> ${s.sourceBasis}`, "");
|
|
@@ -66549,6 +66583,7 @@ function applyAdjudicationFile(p, adj, lang) {
|
|
|
66549
66583
|
console.error(`ultra11y verify: --in file not found or not valid JSON: ${inFlag}.`);
|
|
66550
66584
|
return 2;
|
|
66551
66585
|
}
|
|
66586
|
+
hydrateAdjudication(adj, audit2, { cwd: typeof p.flags.cwd === "string" ? p.flags.cwd : void 0 });
|
|
66552
66587
|
const r = applyAdjudication(audit2, adj);
|
|
66553
66588
|
if (!r.ok) {
|
|
66554
66589
|
if (p.flags.json) console.log(JSON.stringify(r, null, 2));
|
package/src/adjudicate.ts
CHANGED
|
@@ -909,11 +909,18 @@ function plainTest(s: string): string {
|
|
|
909
909
|
return s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
|
|
910
910
|
}
|
|
911
911
|
|
|
912
|
-
|
|
912
|
+
/** `preamble: false` renders the criterion sections ALONE.
|
|
913
|
+
*
|
|
914
|
+
* The preamble names the file to write into and the command to fold with, which is right for
|
|
915
|
+
* the one combined document and wrong for a per-criterion brief: repeated 96 times it is 96
|
|
916
|
+
* copies of instructions that contradict a CI adjudicator's toolset (it has no shell) and
|
|
917
|
+
* point at the wrong file. The briefs carry evidence and protocol; the instructions live once,
|
|
918
|
+
* in the worklist's `contract` field and the dispatch contract. */
|
|
919
|
+
export function formatAdjudication(items: AdjudicationItem[], lang: Lang = "en", standard: StandardId = CORE, opts: { preamble?: boolean } = {}): string {
|
|
913
920
|
const s = T[lang];
|
|
914
921
|
const pack = isCore(standard) ? undefined : loadPack(standard);
|
|
915
|
-
const out: string[] = [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
|
|
916
|
-
if (pack) out.push(`> ${s.packIntro(pack.name)}`, "");
|
|
922
|
+
const out: string[] = opts.preamble === false ? [] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
|
|
923
|
+
if (pack && opts.preamble !== false) out.push(`> ${s.packIntro(pack.name)}`, "");
|
|
917
924
|
for (const it of items) {
|
|
918
925
|
out.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` — ${it.title}` : ""} _(${it.automatability})_`);
|
|
919
926
|
out.push("", `> ${s.evidence} (${it.evidence.length}${it.evidenceTruncated ? ` / ${it.evidenceTruncated.total}` : ""}):`, "");
|
|
@@ -970,6 +977,10 @@ export function formatAdjudication(items: AdjudicationItem[], lang: Lang = "en",
|
|
|
970
977
|
export interface WriteAdjudicationResult {
|
|
971
978
|
todoPath: string;
|
|
972
979
|
mdPath: string;
|
|
980
|
+
/** The verdicts-only file, for an adjudicator that cannot edit half a megabyte of JSON. */
|
|
981
|
+
verdictsPath: string;
|
|
982
|
+
/** One small Markdown brief per criterion, for one that cannot read it either. */
|
|
983
|
+
itemsDir: string;
|
|
973
984
|
count: number;
|
|
974
985
|
}
|
|
975
986
|
|
|
@@ -992,5 +1003,60 @@ export function writeAdjudication(
|
|
|
992
1003
|
};
|
|
993
1004
|
writeFileSync(todoPath, JSON.stringify(file, null, 2) + "\n");
|
|
994
1005
|
writeFileSync(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
|
|
995
|
-
|
|
1006
|
+
|
|
1007
|
+
// THE SPLIT SURFACE, for an adjudicator that cannot shell out.
|
|
1008
|
+
//
|
|
1009
|
+
// The two documents above are sized for a session with a shell: 96 RGAA criteria carrying
|
|
1010
|
+
// 30 evidence anchors each come to ~540 KB of JSON and ~470 KB of Markdown, and the runbook
|
|
1011
|
+
// tells the reader to edit the JSON in place and then run the engine to fold it. That is
|
|
1012
|
+
// fine locally. In CI the adjudicator is handed Read/Grep/Glob/Edit/Write and nothing else,
|
|
1013
|
+
// and against those tools the same instructions are impossible: reading either document
|
|
1014
|
+
// swamps the context, editing 96 verdicts inside half a megabyte of JSON is 96 exact-match
|
|
1015
|
+
// edits, and every prescribed `node …` command is denied. Measured on a real run: 17
|
|
1016
|
+
// permission denials, 75 of 424 turns used, and a todo file that came back untouched — so
|
|
1017
|
+
// the fail-closed fold correctly discarded all 96 and every criterion stayed "to assess".
|
|
1018
|
+
//
|
|
1019
|
+
// So the same worklist is also written as one small file to WRITE and one small file per
|
|
1020
|
+
// criterion to READ. Additive on purpose: the two documents above keep their exact shape and
|
|
1021
|
+
// every existing caller, gate and test is untouched.
|
|
1022
|
+
const verdictsPath = join(outDir, "ADJUDICATE.verdicts.json");
|
|
1023
|
+
writeFileSync(verdictsPath, JSON.stringify({ ...file, items: slimAdjudicationItems(items) }, null, 2) + "\n");
|
|
1024
|
+
const itemsDir = join(outDir, "adjudicate");
|
|
1025
|
+
mkdirSync(itemsDir, { recursive: true });
|
|
1026
|
+
for (const it of items) {
|
|
1027
|
+
writeFileSync(join(itemsDir, `${it.criteriaId}.md`), formatAdjudication([it], opts.lang ?? "en", opts.standard, { preamble: false }));
|
|
1028
|
+
}
|
|
1029
|
+
return { todoPath, mdPath, verdictsPath, itemsDir, count: items.length };
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/** The worklist with the evidence stripped: what an adjudicator has to WRITE, without the
|
|
1033
|
+
* bulk it only has to READ. Every field the fold requires — verdict, justification, reason,
|
|
1034
|
+
* findings, citations, recommendations — is kept, so a filled slim file carries a complete
|
|
1035
|
+
* decision. The evidence comes back at fold time via `hydrateAdjudication`. */
|
|
1036
|
+
export function slimAdjudicationItems(items: AdjudicationItem[]): AdjudicationItem[] {
|
|
1037
|
+
return items.map((it) => {
|
|
1038
|
+
const { evidence: _evidence, evidenceTruncated: _truncated, ...rest } = it;
|
|
1039
|
+
return { ...rest, evidence: [] } as AdjudicationItem;
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/** Put the evidence back on a slim adjudication, re-derived from the audit it was built from.
|
|
1044
|
+
*
|
|
1045
|
+
* Safe because the worklist is a pure function of the audit: same audit, same harvesters, same
|
|
1046
|
+
* anchors. This is what lets `verify --apply` accept a verdicts-only file WITHOUT relaxing a
|
|
1047
|
+
* single check — the citation gate still matches each cited anchor against this criterion's
|
|
1048
|
+
* own harvested evidence, exactly as it does for an inline worklist.
|
|
1049
|
+
*
|
|
1050
|
+
* An item the re-derivation does not know is left alone: it is a surplus verdict, and the
|
|
1051
|
+
* coverage check in `applyAdjudication` is what must reject it. */
|
|
1052
|
+
export function hydrateAdjudication(adj: AdjudicationFile, audit: AuditResult, opts: { cwd?: string } = {}): void {
|
|
1053
|
+
const needs = adj.items.filter((it) => !it.evidence || it.evidence.length === 0);
|
|
1054
|
+
if (!needs.length) return;
|
|
1055
|
+
const derived = new Map(buildAdjudicationWorklist(audit, { ...opts, standard: adj.standard }).map((it) => [it.criteriaId, it]));
|
|
1056
|
+
for (const it of needs) {
|
|
1057
|
+
const full = derived.get(it.criteriaId);
|
|
1058
|
+
if (!full) continue;
|
|
1059
|
+
it.evidence = full.evidence;
|
|
1060
|
+
if (full.evidenceTruncated) it.evidenceTruncated = full.evidenceTruncated;
|
|
1061
|
+
}
|
|
996
1062
|
}
|
package/src/annotate.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
renderRedirected,
|
|
37
37
|
unattributedFindings,
|
|
38
38
|
} from "./pages.js";
|
|
39
|
-
import {
|
|
39
|
+
import { pageCriterionRows, pageTally, pageTallyNote } from "./pages-report.js";
|
|
40
40
|
|
|
41
41
|
export interface AnnotateOptions {
|
|
42
42
|
standard?: StandardId;
|
|
@@ -125,10 +125,15 @@ const S = {
|
|
|
125
125
|
unattributed: (n: number) =>
|
|
126
126
|
`${n} constat(s) ne sont rattachés à aucune page (code partagé, fichier hors routes) — comptés dans l'audit global, jamais répartis d'office.`,
|
|
127
127
|
sourceBasis:
|
|
128
|
-
"Une page marquée « source » n'a pas d'instantané : l'absence de constat n'y vaut PAS conformit
|
|
128
|
+
"Une page marquée « source » n'a pas d'instantané : l'absence de constat n'y vaut PAS conformité. Ses critères non décidés restent « à évaluer », ils ne basculent jamais en conformes par silence.",
|
|
129
129
|
pageByPage: "page par page",
|
|
130
130
|
pagesCount: (n: number) => `${n} page(s)`,
|
|
131
131
|
testsCol: "Tests",
|
|
132
|
+
conformingCol: "C",
|
|
133
|
+
nonConformingCol: "NC",
|
|
134
|
+
toAssessCol: "À évaluer",
|
|
135
|
+
scoreboardNote:
|
|
136
|
+
"`C` conforme · `NC` non conforme · « À évaluer » : personne ne l'a encore tranché — ni conforme ni non conforme. Pas de pourcentage ici : un taux calculé sur les seuls critères décidés se lit comme une note de page, et vaut la même chose sur une bonne page que sur une mauvaise. Le taux et sa couverture sont dans la fiche par page de l'artefact.",
|
|
132
137
|
noPages:
|
|
133
138
|
"Aucune page dans le périmètre de ce run : le balayage n'a produit aucun instantané. Ce n'est pas un bilan vide, c'est un bilan absent — les critères au rendu restent à évaluer.",
|
|
134
139
|
pagesDetailNote:
|
|
@@ -173,10 +178,15 @@ const S = {
|
|
|
173
178
|
unattributed: (n: number) =>
|
|
174
179
|
`${n} finding(s) are attributed to no page (shared code, file outside any route) — counted in the overall audit, never spread across pages.`,
|
|
175
180
|
sourceBasis:
|
|
176
|
-
'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming
|
|
181
|
+
'A page marked "source" has no snapshot: the absence of a finding there does NOT mean conforming. Its undecided criteria stay “to assess”; they never turn conforming by silence.',
|
|
177
182
|
pageByPage: "page by page",
|
|
178
183
|
pagesCount: (n: number) => `${n} page(s)`,
|
|
179
184
|
testsCol: "Tests",
|
|
185
|
+
conformingCol: "C",
|
|
186
|
+
nonConformingCol: "NC",
|
|
187
|
+
toAssessCol: "To assess",
|
|
188
|
+
scoreboardNote:
|
|
189
|
+
"`C` conforming · `NC` non-conforming · “To assess”: nobody has ruled on it yet — neither conforming nor not. No percentage here: a rate over the decided criteria alone reads as a page score, and reads the same on a good page as on a bad one. The rate and its coverage live in the artifact's per-page sheet.",
|
|
180
190
|
noPages:
|
|
181
191
|
"No page in this run's scope: the sweep produced no snapshot. This is not an empty scoreboard, it is a missing one — the rendering criteria stay to assess.",
|
|
182
192
|
pagesDetailNote:
|
|
@@ -391,20 +401,27 @@ export function perPageTable(result: AuditResult, standard: StandardId = CORE, l
|
|
|
391
401
|
* comment draw ONE table from one projection — a second copy of this loop is a second chance
|
|
392
402
|
* to disagree about what a page's rate is.
|
|
393
403
|
*
|
|
394
|
-
*
|
|
395
|
-
* `PageResult.conformancePct
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
404
|
+
* COUNTS, not a percentage. Every status comes from `pageCriterionRows`, in the ACTIVE
|
|
405
|
+
* standard's vocabulary — `PageResult.conformancePct` is always WCAG-keyed and would report a
|
|
406
|
+
* pack page against the core's 55.
|
|
407
|
+
*
|
|
408
|
+
* A percentage was worse than useless here. When the judgment criteria are undecided — which
|
|
409
|
+
* is the NORMAL state without an adjudication pass, and the state a rejected one falls back to
|
|
410
|
+
* — a page has 2 conforming and 2 non-conforming out of 106, and the cell read « 50 % (4/106) »
|
|
411
|
+
* on every row. Half of four criteria is not half a page, but the eye reads a page score, and
|
|
412
|
+
* it read the same score for a good page and a bad one. The three counts cannot be misread:
|
|
413
|
+
* what conforms, what does not, and how much nobody has ruled on yet. The percentage stays in
|
|
414
|
+
* the artifact's per-page sheet, next to the coverage sentence that qualifies it. */
|
|
400
415
|
function scoreboardTable(result: AuditResult, derived: PageResult[], standard: StandardId, s: (typeof S)[Lang], lang: Lang): string[] {
|
|
401
|
-
const out: string[] = [
|
|
416
|
+
const out: string[] = [
|
|
417
|
+
`| ${s.page} | ${s.basis} | ${s.conformingCol} | ${s.nonConformingCol} | ${s.toAssessCol} | 🔴 | 🟠 | 🟡 |`,
|
|
418
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |",
|
|
419
|
+
];
|
|
402
420
|
for (const pg of derived) {
|
|
403
421
|
const n = (sev: Severity): number => severityCount(pg, sev);
|
|
404
|
-
const
|
|
405
|
-
const cov = pageCoverage(rows);
|
|
422
|
+
const t = pageTally(pageCriterionRows(result, pg, standard, lang));
|
|
406
423
|
out.push(
|
|
407
|
-
`| ${pg.name}${pg.auth ? " 🔒" : ""} — \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${
|
|
424
|
+
`| ${pg.name}${pg.auth ? " 🔒" : ""} — \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${t.c} | ${t.nc} | ${t.manual} | ${n("bloquant")} | ${n("majeur")} | ${n("mineur")} |`,
|
|
408
425
|
);
|
|
409
426
|
}
|
|
410
427
|
return out;
|
|
@@ -418,7 +435,8 @@ function severityCount(pg: PageResult, sev: Severity): number {
|
|
|
418
435
|
* than a snapshot actually present. One caveat per basis, from the shared sentences — a
|
|
419
436
|
* « non audité » page must not be explained by the note that asserts it has no snapshot. */
|
|
420
437
|
function basisCaveats(result: AuditResult, derived: PageResult[], s: (typeof S)[Lang], lang: Lang): string[] {
|
|
421
|
-
|
|
438
|
+
// What the three count columns mean, and why there is no percentage beside them.
|
|
439
|
+
const out: string[] = [`> ${s.scoreboardNote}`, ""];
|
|
422
440
|
const orphans = unattributedFindings(result).filter((f) => !f.advisory).length;
|
|
423
441
|
if (orphans) out.push(`> ${s.unattributed(orphans)}`, "");
|
|
424
442
|
if (derived.some((p) => p.basis === "attributed")) out.push(`> ${s.sourceBasis}`, "");
|
package/src/cli.ts
CHANGED
|
@@ -42,7 +42,14 @@ import { runCriteria, renderCriteriaReference } from "./criteria.js";
|
|
|
42
42
|
import { checkReport, checkSemantic } from "./check.js";
|
|
43
43
|
import { buildWorklist, writeWorklist, applyVerdicts, VERIFY_MAX, type VerifyItem } from "./verify.js";
|
|
44
44
|
import { groundItems } from "./grounding.js";
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
buildAdjudicationWorklist,
|
|
47
|
+
formatAdjudication,
|
|
48
|
+
writeAdjudication,
|
|
49
|
+
applyAdjudication,
|
|
50
|
+
hydrateAdjudication,
|
|
51
|
+
type AdjudicationFile,
|
|
52
|
+
} from "./adjudicate.js";
|
|
46
53
|
import { BATCH_SIZE, apiKeyFromEnv, applyRawVerdicts, judgeAll, modelFromEnv } from "./llm.js";
|
|
47
54
|
import { runScan, runScanMany, runCrawlScan, runSampleScan, mergeDynamic, mergeSnapshotAudit, cleanDynamic, dockerAvailable } from "./scan.js";
|
|
48
55
|
import { runScanLocal, runScanManyLocal, runCrawlScanLocal, runSampleScanLocal, localAvailable } from "./scan-local.js";
|
|
@@ -2637,6 +2644,11 @@ function applyAdjudicationFile(p: ParsedArgs, adj: AdjudicationFile, lang: Lang)
|
|
|
2637
2644
|
console.error(`ultra11y verify: --in file not found or not valid JSON: ${inFlag}.`);
|
|
2638
2645
|
return 2;
|
|
2639
2646
|
}
|
|
2647
|
+
// A verdicts-only file (`ADJUDICATE.verdicts.json`) carries decisions without the evidence
|
|
2648
|
+
// they were made against. Put it back from the audit before folding: the worklist is a pure
|
|
2649
|
+
// function of the audit, so this reconstructs the very anchors the citation gate checks
|
|
2650
|
+
// against — the fold below is the same fold, with the same refusals.
|
|
2651
|
+
hydrateAdjudication(adj, audit, { cwd: typeof p.flags.cwd === "string" ? (p.flags.cwd as string) : undefined });
|
|
2640
2652
|
const r = applyAdjudication(audit, adj);
|
|
2641
2653
|
if (!r.ok) {
|
|
2642
2654
|
if (p.flags.json) console.log(JSON.stringify(r, null, 2));
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Single source of truth for shared types + the embedded version. sync-version.mjs
|
|
2
2
|
// rewrites VERSION at release time (kept in lockstep with package.json + SKILL.md).
|
|
3
|
-
export const VERSION = "5.3.
|
|
3
|
+
export const VERSION = "5.3.4";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|