ultra11y 2.31.2 → 2.31.3
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/build-pack-rgaa.mjs +31 -0
- package/scripts/ultra11y.mjs +183 -56
- package/scripts/vendor/wcag-2.2-fr.json +1 -1
- package/scripts/vendor/wcag-2.2-sc-universe.json +1 -1
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +183 -56
- package/skills/ultra11y/SKILL.md +10 -5
- package/skills/ultra11y/references/pages.md +7 -3
- package/skills/ultra11y/scripts/ultra11y.mjs +183 -56
- package/src/adjudicate.ts +77 -12
- package/src/auditor.ts +2 -2
- package/src/cli.ts +26 -10
- package/src/data/standards/rgaa.json +10 -5
- package/src/data/wcag-universe.json +1 -1
- package/src/llm.ts +23 -2
- package/src/pages-report.ts +51 -12
- package/src/prd.ts +3 -2
- package/src/report.ts +70 -19
- package/src/standards/derive.ts +33 -2
- package/src/standards/pack.ts +13 -0
- package/src/standards/types.ts +13 -0
- package/src/types.ts +1 -1
package/src/report.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// WCAG-keyed result. Both keep the honest structure: per-guideline/theme synthesis,
|
|
5
5
|
// non-conformities, conforming + not-applicable lists, and the manual worklist
|
|
6
6
|
// (never silently C).
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { join, relative } from "node:path";
|
|
9
9
|
import type { AuditResult, Finding, Lang, Severity, Status } from "./types.js";
|
|
10
10
|
import { guidelineTitle, scTitle } from "./wcag.js";
|
|
@@ -18,10 +18,10 @@ import {
|
|
|
18
18
|
CORE,
|
|
19
19
|
isCore,
|
|
20
20
|
loadPack,
|
|
21
|
-
getCriterion as getPackCriterion,
|
|
22
21
|
derivePackResults,
|
|
23
22
|
packCriteriaForFinding,
|
|
24
23
|
packConformancePct,
|
|
24
|
+
packTestIds,
|
|
25
25
|
title as packTitle,
|
|
26
26
|
themeName,
|
|
27
27
|
type StandardPack,
|
|
@@ -56,6 +56,9 @@ const L = {
|
|
|
56
56
|
sev: { bloquant: "Bloquant", majeur: "Majeur", mineur: "Mineur" } as Record<Severity, string>,
|
|
57
57
|
none: "Aucune non-conformité détectée par le moteur statique.",
|
|
58
58
|
cTitle: "3. Critères conformes (C)",
|
|
59
|
+
cAgentTitle: "Conformes par adjudication de l'agent (jugement, non prouvé par le moteur)",
|
|
60
|
+
cAgentNote:
|
|
61
|
+
"Ces critères ont été tranchés par l'agent IA à partir des évidences citées, non décidés par le moteur déterministe. Ils sont gatés (chaque verdict cite une évidence résolvable) mais restent un jugement : ils ne sont pas comptés dans le taux de réussite automatique ci-dessus.",
|
|
59
62
|
naTitle: "4. Critères non applicables (NA)",
|
|
60
63
|
manualTitle: "5. Critères à adjuger (jugement / rendu) — non décidés par le moteur statique",
|
|
61
64
|
manualWarn:
|
|
@@ -65,6 +68,7 @@ const L = {
|
|
|
65
68
|
"Générez la worklist : `verify --manual --in <audit.json> --standard <pack> --out <dir>`. Chaque item y porte l'énoncé complet de ses tests, sa note technique, ses cas particuliers, sa guidance et les termes que le référentiel définit.",
|
|
66
69
|
outOfScope: "Hors périmètre moteur — mappé sur des SC hors WCAG 2.2 AA ; vérification manuelle.",
|
|
67
70
|
scopedOut: "Les non-conformités WCAG relevées concernent des éléments hors du périmètre de ce critère — à évaluer séparément.",
|
|
71
|
+
judgment: "L’énoncé du critère demande davantage que les CS WCAG auxquels il est rattaché — le moteur n’y a pas répondu, à trancher.",
|
|
68
72
|
nothing: "Aucun.",
|
|
69
73
|
dedup: "Dédup",
|
|
70
74
|
canonical: "fichier(s) canonique(s) audité(s)",
|
|
@@ -117,6 +121,9 @@ const L = {
|
|
|
117
121
|
sev: { bloquant: "Blocking", majeur: "Major", mineur: "Minor" } as Record<Severity, string>,
|
|
118
122
|
none: "No non-conformity detected by the static engine.",
|
|
119
123
|
cTitle: "3. Conforming criteria (C)",
|
|
124
|
+
cAgentTitle: "Conforming by agent adjudication (judgement, not proven by the engine)",
|
|
125
|
+
cAgentNote:
|
|
126
|
+
"These criteria were ruled on by the AI agent from the evidence it cited, not decided by the deterministic engine. They are gated (every verdict cites resolvable evidence) but remain a judgement: they are not counted in the automatic pass rate above.",
|
|
120
127
|
naTitle: "4. Not-applicable criteria (NA)",
|
|
121
128
|
manualTitle: "5. Criteria to adjudicate (judgment / rendering) — not decided by the static engine",
|
|
122
129
|
manualWarn:
|
|
@@ -126,6 +133,7 @@ const L = {
|
|
|
126
133
|
"Generate the worklist: `verify --manual --in <audit.json> --standard <pack> --out <dir>`. Each item carries the full wording of its tests, its technical note, its particular cases, its guidance and the terms the standard defines.",
|
|
127
134
|
outOfScope: "Out of engine scope — mapped to SCs outside WCAG 2.2 AA; manual verification.",
|
|
128
135
|
scopedOut: "The WCAG failures found concern elements outside this criterion's scope — assess separately.",
|
|
136
|
+
judgment: "The criterion asks more than the WCAG SCs it maps to — the engine did not answer it; rule on it.",
|
|
129
137
|
nothing: "None.",
|
|
130
138
|
dedup: "Dedup",
|
|
131
139
|
canonical: "canonical file(s) audited",
|
|
@@ -194,6 +202,11 @@ interface Row {
|
|
|
194
202
|
status: Status;
|
|
195
203
|
findings: Finding[];
|
|
196
204
|
justification?: string;
|
|
205
|
+
// Who decided this criterion. Absent/"engine" = the deterministic engine; "agent" = an
|
|
206
|
+
// adjudication (gated, but a judgement call); "scan" = the rendered tier. Rendered so a
|
|
207
|
+
// conformity the engine PROVED is never presented as the same thing as one an agent
|
|
208
|
+
// RULED — see the split in section 3 and the header rate.
|
|
209
|
+
decidedBy?: "engine" | "agent" | "scan";
|
|
197
210
|
}
|
|
198
211
|
|
|
199
212
|
interface Group {
|
|
@@ -329,16 +342,27 @@ function render(
|
|
|
329
342
|
out.push(`- ${nc.length} ${s.ncCount}${adv.length ? ` · ${adv.length} ${s.advCount}` : ""}`);
|
|
330
343
|
const notes = pageScope.find((x) => x.id === pg.id)?.notes;
|
|
331
344
|
if (notes) out.push(`- _${notes}_`);
|
|
332
|
-
// The screenshot the snapshot already holds.
|
|
333
|
-
// directory
|
|
345
|
+
// The screenshot the snapshot already holds. Copied next to the report when there is
|
|
346
|
+
// an output directory, so the directory travels intact — CI uploads `audits/` alone,
|
|
347
|
+
// and a `../.ultra11y/…` reference is a broken image in the artifact the reviewer
|
|
348
|
+
// opens. Without an --out (stdout), stay relative: there is nothing to be
|
|
349
|
+
// self-contained about.
|
|
334
350
|
const shot = join(PAGES_DIR, pg.id, "screen.png");
|
|
335
|
-
if (existsSync(shot))
|
|
336
|
-
|
|
337
|
-
""
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
351
|
+
if (existsSync(shot)) {
|
|
352
|
+
let href = relative(opts.outDir ?? ".", shot)
|
|
353
|
+
.split("\\")
|
|
354
|
+
.join("/");
|
|
355
|
+
if (opts.outDir) {
|
|
356
|
+
try {
|
|
357
|
+
mkdirSync(join(opts.outDir, "assets"), { recursive: true });
|
|
358
|
+
copyFileSync(shot, join(opts.outDir, "assets", `${pg.id}.png`));
|
|
359
|
+
href = `./assets/${pg.id}.png`;
|
|
360
|
+
} catch {
|
|
361
|
+
// Unwritable output dir: keep the relative reference rather than lose the image.
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
out.push("", ``);
|
|
365
|
+
}
|
|
342
366
|
for (const f of nc.slice(0, 30)) {
|
|
343
367
|
const crits = pack ? packCriteriaForFinding(pack, f) : [];
|
|
344
368
|
const label = crits.length ? crits.join(", ") : f.criteriaId; // graceful fallback to the WCAG SC
|
|
@@ -348,10 +372,23 @@ function render(
|
|
|
348
372
|
}
|
|
349
373
|
}
|
|
350
374
|
|
|
351
|
-
// 3. conforming
|
|
375
|
+
// 3. conforming — split by PROVENANCE. A criterion the deterministic engine decided and
|
|
376
|
+
// one an agent ruled on are both "C", but they are not the same claim, and merging them
|
|
377
|
+
// into one list (and one headline rate) is how an adjudication pass could publish dozens
|
|
378
|
+
// of conformities nobody had verified. The gate makes an agent C evidence-bound; this
|
|
379
|
+
// keeps it legible as an agent's judgement all the way to the reader.
|
|
352
380
|
out.push(`## ${s.cTitle}`, "");
|
|
353
381
|
const conform = rows.filter((x) => x.status === "C");
|
|
354
|
-
|
|
382
|
+
const byEngine = conform.filter((x) => x.decidedBy !== "agent");
|
|
383
|
+
const byAgent = conform.filter((x) => x.decidedBy === "agent");
|
|
384
|
+
if (!conform.length) out.push(s.nothing, "");
|
|
385
|
+
else {
|
|
386
|
+
if (byEngine.length) out.push(...byEngine.map((x) => `- ${x.label}`), "");
|
|
387
|
+
if (byAgent.length) {
|
|
388
|
+
out.push(`### ${s.cAgentTitle}`, "", `> ${s.cAgentNote}`, "");
|
|
389
|
+
out.push(...byAgent.map((x) => `- ${x.label}${x.justification ? ` — _${x.justification}_` : ""}`), "");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
355
392
|
|
|
356
393
|
// 4. not applicable
|
|
357
394
|
out.push(`## ${s.naTitle}`, "");
|
|
@@ -368,8 +405,8 @@ function render(
|
|
|
368
405
|
else {
|
|
369
406
|
const pack5 = isCore(opts.standard) ? undefined : loadPack(opts.standard);
|
|
370
407
|
for (const x of manual) {
|
|
371
|
-
const tests = pack5 ?
|
|
372
|
-
const testRef = tests.length ? ` — ${s.testsToRule}: ${tests.map((
|
|
408
|
+
const tests = pack5 ? packTestIds(pack5, x.id) : [];
|
|
409
|
+
const testRef = tests.length ? ` — ${s.testsToRule}: ${tests.map((t) => `\`${t}\``).join(" · ")}` : "";
|
|
373
410
|
out.push(`- ${x.label}${x.justification ? ` — _${x.justification}_` : ""}${testRef}`);
|
|
374
411
|
}
|
|
375
412
|
out.push("", `> ${s.manualHowTo}`, "");
|
|
@@ -384,7 +421,14 @@ export function renderReport(r: AuditResult, lang: Lang = "en", outDir?: string)
|
|
|
384
421
|
const byGuideline = new Map<string, Row[]>();
|
|
385
422
|
for (const c of r.criteria) {
|
|
386
423
|
const title = scTitle(c.id, lang);
|
|
387
|
-
const row: Row = {
|
|
424
|
+
const row: Row = {
|
|
425
|
+
id: c.id,
|
|
426
|
+
label: title ? `${c.id} — ${title}` : c.id,
|
|
427
|
+
status: c.status,
|
|
428
|
+
findings: c.findings,
|
|
429
|
+
justification: c.justification,
|
|
430
|
+
decidedBy: c.decidedBy,
|
|
431
|
+
};
|
|
388
432
|
(byGuideline.get(c.guideline) ?? byGuideline.set(c.guideline, []).get(c.guideline)!).push(row);
|
|
389
433
|
}
|
|
390
434
|
// `g.title` on the AuditResult's GuidelineTally is the baked-in English title (kept for
|
|
@@ -409,15 +453,18 @@ export function renderPackReport(r: AuditResult, pack: StandardPack, lang: Lang
|
|
|
409
453
|
label: `${pack.name} ${pr.id} — ${packTitle(pack, pc, lang)}`,
|
|
410
454
|
status: pr.status,
|
|
411
455
|
findings: pr.findings,
|
|
456
|
+
...(pr.decidedBy ? { decidedBy: pr.decidedBy } : {}),
|
|
412
457
|
// outOfScope / scopedOut criteria are "manual" (not NA) with their own dedicated
|
|
413
458
|
// justification — never mixed with the ordinary NA reason (see the manual section above).
|
|
414
459
|
...(pr.outOfScope
|
|
415
460
|
? { justification: s.outOfScope }
|
|
416
461
|
: pr.scopedOut
|
|
417
462
|
? { justification: s.scopedOut }
|
|
418
|
-
: pr.
|
|
419
|
-
? { justification:
|
|
420
|
-
:
|
|
463
|
+
: pr.judgment
|
|
464
|
+
? { justification: s.judgment }
|
|
465
|
+
: pr.status === "NA"
|
|
466
|
+
? { justification: naReason }
|
|
467
|
+
: {}),
|
|
421
468
|
};
|
|
422
469
|
(byTheme.get(pr.theme) ?? byTheme.set(pr.theme, []).get(pr.theme)!).push(row);
|
|
423
470
|
}
|
|
@@ -434,6 +481,10 @@ export function renderPackReport(r: AuditResult, pack: StandardPack, lang: Lang
|
|
|
434
481
|
standard: pack.key,
|
|
435
482
|
partialAudit: untestedNeedsRendering(r),
|
|
436
483
|
headerRatePct: packConformancePct(derived),
|
|
484
|
+
// Forwarded, unlike before: without it the per-page screenshots resolved against the
|
|
485
|
+
// CWD instead of the report's own directory, so a pack report written to `audits/`
|
|
486
|
+
// carried links that only worked when read from the repo root.
|
|
487
|
+
outDir,
|
|
437
488
|
});
|
|
438
489
|
}
|
|
439
490
|
|
package/src/standards/derive.ts
CHANGED
|
@@ -23,6 +23,16 @@ export interface PackCriterionResult {
|
|
|
23
23
|
// sibling's failure. It derives as `manual` (assess separately) with a dedicated
|
|
24
24
|
// scoped-out justification (see src/report.ts renderPackReport).
|
|
25
25
|
scopedOut?: boolean;
|
|
26
|
+
// Set when the pack marks this criterion `judgment` and the projection WOULD have
|
|
27
|
+
// inherited a `C` from a mapped SC that never answered the criterion's own question
|
|
28
|
+
// (RGAA 8.6 "is the title pertinent?" off WCAG 2.4.2 "a title exists"). It derives
|
|
29
|
+
// `manual` instead — see `judgmentGuard` and src/report.ts for the justification.
|
|
30
|
+
judgment?: boolean;
|
|
31
|
+
// Recorded on the adjudicated branch below (an agent verdict at the pack's own
|
|
32
|
+
// granularity wins over the derivation). Declared here because renderings distinguish a
|
|
33
|
+
// conformity the engine PROVED from one an agent RULED — they are not the same claim.
|
|
34
|
+
justification?: string;
|
|
35
|
+
decidedBy?: "engine" | "agent" | "scan";
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
// NC dominates (a real failure anywhere fails the criterion); then a decided C; then
|
|
@@ -68,7 +78,11 @@ export function packCriteriaForFinding(pack: StandardPack, finding: Finding): st
|
|
|
68
78
|
* same as core. Exists so a pack report's header rate can't drift from its own table once
|
|
69
79
|
* pack overrides (advisory/severity flips) make the two diverge from core `conformancePct`. */
|
|
70
80
|
export function packConformancePct(derived: PackCriterionResult[]): number {
|
|
71
|
-
|
|
81
|
+
// Agent-adjudicated conformities are deliberately excluded, mirroring the core
|
|
82
|
+
// (`recomputeTallies` in src/adjudicate.ts): this number is the AUTOMATIC pass rate, and
|
|
83
|
+
// a judgement — however well gated — is not an automatic verification. An agent NC still
|
|
84
|
+
// counts; lowering the rate off evidenced findings is the safe direction.
|
|
85
|
+
const c = derived.filter((d) => d.status === "C" && d.decidedBy !== "agent").length;
|
|
72
86
|
const nc = derived.filter((d) => d.status === "NC").length;
|
|
73
87
|
return c + nc === 0 ? 100 : Math.round((c / (c + nc)) * 100);
|
|
74
88
|
}
|
|
@@ -123,6 +137,23 @@ function applySecondaryMappings(
|
|
|
123
137
|
return { ...base, findings };
|
|
124
138
|
}
|
|
125
139
|
|
|
140
|
+
/** A criterion the pack marks `judgment` never INHERITS a `C` from its mapped WCAG SCs.
|
|
141
|
+
*
|
|
142
|
+
* The projection folds the SCs DINUM's own crosswalk cites, and `aggregate` returns `C`
|
|
143
|
+
* as soon as one of them is `C`. That is the right answer when the RGAA question and the
|
|
144
|
+
* SC ask the same thing, and a fabricated conformity when the RGAA wording asks more:
|
|
145
|
+
* RGAA 8.6 wants a *pertinent* page title, WCAG 2.4.2 only a present one; RGAA 13.3 wants
|
|
146
|
+
* an accessible version of a downloaded document, and no mapped SC ever opened it. Those
|
|
147
|
+
* derived `C`s were the one error this tool must not make, so they become `manual` and go
|
|
148
|
+
* to the agent instead.
|
|
149
|
+
*
|
|
150
|
+
* Only `C` is intercepted: an `NC` was evidenced by a rule that really fired on this
|
|
151
|
+
* criterion, and an `NA` means nothing in scope is concerned — both stay. */
|
|
152
|
+
function judgmentGuard(r: PackCriterionResult, pc: PackCriterion): PackCriterionResult {
|
|
153
|
+
if (!pc.judgment || r.status !== "C") return r;
|
|
154
|
+
return { ...r, status: "manual" as Status, judgment: true };
|
|
155
|
+
}
|
|
156
|
+
|
|
126
157
|
export function derivePackResults(audit: AuditResult, packKey: string): PackCriterionResult[] {
|
|
127
158
|
const pack = loadPack(packKey);
|
|
128
159
|
const byScId = new Map(audit.criteria.map((c) => [c.id, c]));
|
|
@@ -207,7 +238,7 @@ export function derivePackResults(audit: AuditResult, packKey: string): PackCrit
|
|
|
207
238
|
decidedBy: "agent" as const,
|
|
208
239
|
};
|
|
209
240
|
}
|
|
210
|
-
const base = deriveBase(pc);
|
|
241
|
+
const base = judgmentGuard(deriveBase(pc), pc);
|
|
211
242
|
return enabledSecondary.length ? applySecondaryMappings(base, pc, enabledSecondary, secondarySources, pack.defaultLocale) : base;
|
|
212
243
|
});
|
|
213
244
|
}
|
package/src/standards/pack.ts
CHANGED
|
@@ -41,6 +41,19 @@ export function titlePlain(pack: StandardPack, c: PackCriterion, lang: Lang): st
|
|
|
41
41
|
return localize(pack, c.titlePlain, lang);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** The criterion's own numbered test ids, in pack order: RGAA 11.1 → ["11.1.1", "11.1.2",
|
|
45
|
+
* "11.1.3"]. These are what an auditor actually rules on, and what a `normativeRef` must
|
|
46
|
+
* cite, so several surfaces render them (the auditor block, the report's to-rule-on list,
|
|
47
|
+
* the PRD, the adjudication worklist, the per-page grid). Centralised here so the id shape
|
|
48
|
+
* is defined once — it used to be re-derived inline at each call site.
|
|
49
|
+
*
|
|
50
|
+
* Callers render these inside backticks, never followed by an em dash: `check`'s criterion
|
|
51
|
+
* scanner matches `(\d+\.\d+)\s*—` and would capture "2.1" out of "6.2.1 — …". */
|
|
52
|
+
export function packTestIds(pack: StandardPack, id: string): string[] {
|
|
53
|
+
const tests = getCriterion(pack, id)?.tests;
|
|
54
|
+
return tests ? Object.keys(tests).map((k) => `${id}.${k}`) : [];
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
export function resolveGlossary(packKey: string, anchor: string): GlossaryEntry | undefined {
|
|
45
58
|
return packGlossary(packKey)?.[anchor];
|
|
46
59
|
}
|
package/src/standards/types.ts
CHANGED
|
@@ -37,6 +37,19 @@ export interface PackCriterion {
|
|
|
37
37
|
// failure). Optional/additive: a pack WITHOUT `appliesTo` keeps the legacy fan-out
|
|
38
38
|
// (every mapped SC's findings attach) so third-party packs are unaffected.
|
|
39
39
|
appliesTo?: { ruleIds: string[] };
|
|
40
|
+
// The criterion's own wording asks MORE than the WCAG SCs it maps to, so a `C` on those
|
|
41
|
+
// SCs is not an answer to it. RGAA 8.6 asks whether the page title is *pertinent*; WCAG
|
|
42
|
+
// 2.4.2 only that a title exists. RGAA 13.3 asks whether a downloadable document has an
|
|
43
|
+
// accessible version; no mapped SC ever opened the document. Without this flag the
|
|
44
|
+
// projection returns `C` as soon as one mapped SC is `C` (see `aggregate` in derive.ts)
|
|
45
|
+
// and publishes a conformity nobody assessed.
|
|
46
|
+
//
|
|
47
|
+
// A flagged criterion can still derive `NC` (a rule actually fired on it) and `NA`
|
|
48
|
+
// (nothing applicable in scope); it simply never inherits a `C` — it derives `manual`
|
|
49
|
+
// and goes to the agent, who rules on it against the criterion's own numbered tests.
|
|
50
|
+
// `appliesTo` already carries the mirror-image promise for NC ("never NC from a
|
|
51
|
+
// sibling's failure"); this is the same guarantee on the conforming side.
|
|
52
|
+
judgment?: boolean;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
// The localized DISPLAY vocabulary a standard uses when its audit is rendered for an
|
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 = "2.31.
|
|
3
|
+
export const VERSION = "2.31.3";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|