ultra11y 2.32.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/README.md +15 -3
- package/package.json +1 -1
- package/scripts/build-extension-icons.mjs +11 -43
- package/scripts/ultra11y.mjs +69 -2
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +69 -2
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/references/ci.md +73 -8
- package/skills/ultra11y/references/judgment.md +8 -0
- package/skills/ultra11y/references/orchestrators.md +7 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +69 -2
- package/src/cli.ts +84 -0
- package/src/evidence.ts +499 -0
- package/src/pixel.ts +174 -2
- package/src/snapshot.ts +9 -4
- 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": "
|
|
13
|
+
"version": "3.1.0",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
16
16
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
// Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
|
|
28
28
|
// does not carry it, so a user's own ultra11y.js is never clobbered.
|
|
29
29
|
const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
|
|
30
|
-
const ULTRA11Y_PLUGIN_VERSION = "
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "3.1.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ claude mcp add ultra11y -- npx -y ultra11y mcp
|
|
|
125
125
|
**As a GitHub Action** — audits the PR diff and, optionally, the served pages:
|
|
126
126
|
|
|
127
127
|
```yaml
|
|
128
|
-
- uses: maxgfr/ultra11y@
|
|
128
|
+
- uses: maxgfr/ultra11y@v3
|
|
129
129
|
with: { since: auto, standard: rgaa, fail-on: blocking }
|
|
130
130
|
```
|
|
131
131
|
|
|
@@ -351,7 +351,9 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
|
|
|
351
351
|
optionally, the **pages** (it can start your app, wait for it, then scan real URLs or your
|
|
352
352
|
declared sample). The engine ships inside the action, so there is nothing to install and no
|
|
353
353
|
`setup-node`. The gate runs **last**, after SARIF, annotations, the summary, the sticky PR
|
|
354
|
-
comment and the report — a red job is never a dead end. `
|
|
354
|
+
comment and the report — a red job is never a dead end. `adjudicate: api|agent` optionally
|
|
355
|
+
rules on the judgment criteria that would otherwise stay « à évaluer » in CI, reading its key
|
|
356
|
+
from the job environment and skipping itself without one. `ultra11y init --ci` writes a
|
|
355
357
|
workflow using it. See `references/ci.md`.
|
|
356
358
|
- **CI surfaces** — a red job says *that* the build broke, not *where*. `--format sarif` emits
|
|
357
359
|
SARIF 2.1.0 for GitHub code scanning, so each finding lands as an **inline annotation on the
|
|
@@ -378,6 +380,15 @@ adjudication leaves the audit untouched. All-`manual` with reasons is accepted,
|
|
|
378
380
|
a correct answer. It is the **only** part of the tool that takes an API key; without one the
|
|
379
381
|
command explains itself and exits, and nothing else changes.
|
|
380
382
|
|
|
383
|
+
The GitHub Action wires this as `adjudicate: api`, and offers `adjudicate: agent` as the other
|
|
384
|
+
half — the same worklist handed to a `claude-code-action` run, which can **open the cited files**
|
|
385
|
+
instead of ruling from harvested evidence alone. Both end in the same fail-closed gate; they
|
|
386
|
+
differ in evidence, not in trust. Both read `ANTHROPIC_API_KEY` from the **job environment**,
|
|
387
|
+
never from an input, and **skip themselves when it is absent** — which is exactly what a fork's
|
|
388
|
+
pull request looks like, so the job stays green and the criteria stay « à évaluer ». See
|
|
389
|
+
`references/ci.md` for the cost per run and for `gate-adjudicated`, which lets a model-ruled
|
|
390
|
+
non-conformity fail the build at the price of a red/green that no longer reproduces.
|
|
391
|
+
|
|
381
392
|
### Optional dynamic tier (axe-core)
|
|
382
393
|
|
|
383
394
|
`scan` runs **axe-core in a headless browser** to decide the *needs-rendering* criteria the static engine leaves as residual risks — chiefly **computed colour contrast (1.4.3)** plus a 320px **reflow** check (1.4.10) and a render cross-check of the structural rules. Two runtimes (default `--runtime auto`): **`--runtime local`** uses a Playwright that resolves from your project (`--cwd`) — **no Docker** — and additionally probes focus visibility (2.4.7), 200% zoom (1.4.4), text spacing (1.4.12) and content-on-hover (1.4.13), plus **stateful** probes (fill inputs then re-measure overflow, and a live-region probe for status messages 4.1.3 — opt out with `--no-interact`, `--interact-clicks` for authed-scan button clicks), and takes `--storage-state` for authenticated pages; **`--runtime docker`** falls back to a self-contained image built on first use. `--merge` folds the findings into a static `AuditResult`, upgrading `manual` criteria to `C`/`NC`. A country-standard audit scans its declared page sample with `scan --sample` (lint it first with `sample check`):
|
|
@@ -418,7 +429,8 @@ Releases are cut automatically by **semantic-release** on push to `main` (GitHub
|
|
|
418
429
|
npm publish via OIDC trusted publishing, plus the moving `v2` major tag). The version bump is
|
|
419
430
|
decided by **Conventional Commits**: a commit whose subject carries no `feat:`/`fix:` prefix
|
|
420
431
|
is analysed as *no release*, so a change that must ship needs at least one conventional commit
|
|
421
|
-
in the range — otherwise
|
|
432
|
+
in the range — otherwise the major alias and npm keep serving the previous version while `main`
|
|
433
|
+
moves on.
|
|
422
434
|
|
|
423
435
|
## Data & licensing
|
|
424
436
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
|
@@ -7,15 +7,22 @@
|
|
|
7
7
|
// script produces — the same reproducibility contract the engine bundle is held to.
|
|
8
8
|
//
|
|
9
9
|
// The mark is an eye (the action's own branding icon), drawn as a filled lens with a pupil on
|
|
10
|
-
// the purple the action already declares.
|
|
11
|
-
//
|
|
10
|
+
// the purple the action already declares. The PNG encoder is the engine's own
|
|
11
|
+
// (`src/pixel.ts`), imported through Node's type-stripping — the repo has no image dependency
|
|
12
|
+
// and is not about to grow one for four small files.
|
|
13
|
+
//
|
|
14
|
+
// That import runs both ways: because these four icons are committed, `--check` re-encodes
|
|
15
|
+
// them and proves `encodePng` still emits the exact bytes it emitted when they were drawn.
|
|
16
|
+
// A decode/encode round-trip could only prove the pair is self-consistent; this proves the
|
|
17
|
+
// encoder is PNG-correct against files a browser accepts. Keep `src/pixel.ts` a leaf (one
|
|
18
|
+
// runtime import, `node:zlib`) or this import stops resolving.
|
|
12
19
|
//
|
|
13
20
|
// node scripts/build-extension-icons.mjs # write
|
|
14
21
|
// node scripts/build-extension-icons.mjs --check # fail if the committed files differ
|
|
15
|
-
import { deflateSync } from "node:zlib";
|
|
16
22
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
17
23
|
import { dirname, join } from "node:path";
|
|
18
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { encodePng } from "../src/pixel.ts";
|
|
19
26
|
|
|
20
27
|
const OUT = join(dirname(fileURLToPath(import.meta.url)), "..", "extension", "icons");
|
|
21
28
|
const SIZES = [16, 32, 48, 128];
|
|
@@ -24,45 +31,6 @@ const SIZES = [16, 32, 48, 128];
|
|
|
24
31
|
const BG = [107, 70, 193]; // #6b46c1
|
|
25
32
|
const FG = [255, 255, 255];
|
|
26
33
|
|
|
27
|
-
function crc32(buf) {
|
|
28
|
-
let c = ~0;
|
|
29
|
-
for (const b of buf) {
|
|
30
|
-
c ^= b;
|
|
31
|
-
for (let i = 0; i < 8; i++) c = (c >>> 1) ^ (0xedb88320 & -(c & 1));
|
|
32
|
-
}
|
|
33
|
-
return ~c >>> 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function chunk(type, data) {
|
|
37
|
-
const len = Buffer.alloc(4);
|
|
38
|
-
len.writeUInt32BE(data.length);
|
|
39
|
-
const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
|
|
40
|
-
const crc = Buffer.alloc(4);
|
|
41
|
-
crc.writeUInt32BE(crc32(body));
|
|
42
|
-
return Buffer.concat([len, body, crc]);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** An RGBA pixel buffer → a PNG. Colour type 6, 8-bit, one filter byte per row. */
|
|
46
|
-
function png(size, rgba) {
|
|
47
|
-
const raw = Buffer.alloc(size * (size * 4 + 1));
|
|
48
|
-
for (let y = 0; y < size; y++) {
|
|
49
|
-
raw[y * (size * 4 + 1)] = 0; // filter: none — the images are tiny, filtering buys nothing
|
|
50
|
-
rgba.copy(raw, y * (size * 4 + 1) + 1, y * size * 4, (y + 1) * size * 4);
|
|
51
|
-
}
|
|
52
|
-
const ihdr = Buffer.alloc(13);
|
|
53
|
-
ihdr.writeUInt32BE(size, 0);
|
|
54
|
-
ihdr.writeUInt32BE(size, 4);
|
|
55
|
-
ihdr[8] = 8; // bit depth
|
|
56
|
-
ihdr[9] = 6; // RGBA
|
|
57
|
-
return Buffer.concat([
|
|
58
|
-
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
59
|
-
chunk("IHDR", ihdr),
|
|
60
|
-
// level 9 so the bytes are a function of the pixels alone, not of zlib's default tuning.
|
|
61
|
-
chunk("IDAT", deflateSync(raw, { level: 9 })),
|
|
62
|
-
chunk("IEND", Buffer.alloc(0)),
|
|
63
|
-
]);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
34
|
/** The eye mark, supersampled 4× so the curves do not alias at 16px. */
|
|
67
35
|
function draw(size) {
|
|
68
36
|
const S = 4;
|
|
@@ -118,7 +86,7 @@ mkdirSync(OUT, { recursive: true });
|
|
|
118
86
|
let stale = [];
|
|
119
87
|
for (const size of SIZES) {
|
|
120
88
|
const file = join(OUT, `icon-${size}.png`);
|
|
121
|
-
const bytes =
|
|
89
|
+
const bytes = encodePng({ width: size, height: size, data: draw(size) });
|
|
122
90
|
if (check) {
|
|
123
91
|
let current = null;
|
|
124
92
|
try {
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join as join45, relative as relative5, sep as sep7, dirname as dirname1
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "
|
|
9
|
+
var VERSION = "3.1.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -39131,7 +39131,7 @@ var colorsRules = [contrastLiteral];
|
|
|
39131
39131
|
import { readFileSync as readFileSync12 } from "fs";
|
|
39132
39132
|
|
|
39133
39133
|
// src/pixel.ts
|
|
39134
|
-
import { inflateSync } from "zlib";
|
|
39134
|
+
import { deflateSync, inflateSync } from "zlib";
|
|
39135
39135
|
var SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
39136
39136
|
function paeth(a, b, c2) {
|
|
39137
39137
|
const p = a + b - c2;
|
|
@@ -58277,6 +58277,7 @@ Usage:
|
|
|
58277
58277
|
ultra11y audit [--changed | --since <ref> | --staged] [--max-files <n>] [--dedup exact|normalized|off] [--baseline <file>] [--fail-on blocking|major|minor]
|
|
58278
58278
|
ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures + .ultra11y/pages snapshots: audit real HTML)
|
|
58279
58279
|
ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
|
|
58280
|
+
ultra11y audit --in <audit.json> [--fail-on blocking|major|minor] [--format sarif|github] (re-gate an audit already computed \u2014 e.g. one carrying adjudicated verdicts \u2014 without a second detection pass)
|
|
58280
58281
|
ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
|
|
58281
58282
|
ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--lang auto|en|fr]
|
|
58282
58283
|
ultra11y tickets --in <audit.json> [--provider auto|github|gitlab|jira] [--grain criterion|page|page-criterion|single|file] [--transport auto|cli|rest]
|
|
@@ -58869,7 +58870,73 @@ function emitCiFormat(result, format, standard, lang, failOn) {
|
|
|
58869
58870
|
);
|
|
58870
58871
|
}
|
|
58871
58872
|
}
|
|
58873
|
+
async function cmdAuditFromFile(p, inPath) {
|
|
58874
|
+
const scoping = [
|
|
58875
|
+
"since",
|
|
58876
|
+
"changed",
|
|
58877
|
+
"staged",
|
|
58878
|
+
"baseline",
|
|
58879
|
+
"graph",
|
|
58880
|
+
"cross-file",
|
|
58881
|
+
"jsx",
|
|
58882
|
+
"include",
|
|
58883
|
+
"exclude",
|
|
58884
|
+
"ext",
|
|
58885
|
+
"max-files",
|
|
58886
|
+
"dedup",
|
|
58887
|
+
"captures",
|
|
58888
|
+
"no-captures",
|
|
58889
|
+
"require-captures",
|
|
58890
|
+
"no-default-excludes",
|
|
58891
|
+
"out"
|
|
58892
|
+
];
|
|
58893
|
+
const offenders = scoping.filter((f) => p.flags[f] !== void 0);
|
|
58894
|
+
if (p.positionals.length) offenders.unshift("<paths>");
|
|
58895
|
+
if (offenders.length) {
|
|
58896
|
+
console.error(
|
|
58897
|
+
`ultra11y audit: --in re-gates an audit that already exists, so these have nothing left to change: ${offenders.join(", ")}. Drop them, or run a fresh audit without --in.`
|
|
58898
|
+
);
|
|
58899
|
+
return 2;
|
|
58900
|
+
}
|
|
58901
|
+
let result;
|
|
58902
|
+
try {
|
|
58903
|
+
const parsed = JSON.parse(inPath === "-" ? await readStdin() : readText(inPath));
|
|
58904
|
+
if (!isCurrentAudit(parsed)) {
|
|
58905
|
+
console.error("ultra11y audit: --in is not a current ultra11y AuditResult (WCAG-keyed, schema v2). Re-run `audit`.");
|
|
58906
|
+
return 2;
|
|
58907
|
+
}
|
|
58908
|
+
result = parsed;
|
|
58909
|
+
} catch {
|
|
58910
|
+
console.error(`ultra11y audit: --in file not found or not valid JSON: ${inPath}.`);
|
|
58911
|
+
return 2;
|
|
58912
|
+
}
|
|
58913
|
+
const lang = resolveLang(p.flags, { audit: result });
|
|
58914
|
+
const failOnRaw = p.flags["fail-on"];
|
|
58915
|
+
const failOnParsed = parseFailOn(failOnRaw);
|
|
58916
|
+
if (failOnRaw !== void 0 && failOnParsed === null) {
|
|
58917
|
+
console.error(`ultra11y audit: --fail-on must be blocking|major|minor (got "${String(failOnRaw)}").`);
|
|
58918
|
+
return 2;
|
|
58919
|
+
}
|
|
58920
|
+
const ciFormat = parseCiFormat(p.flags.format);
|
|
58921
|
+
if (ciFormat === null) {
|
|
58922
|
+
console.error(`ultra11y audit: --format must be sarif|github (got "${String(p.flags.format)}").`);
|
|
58923
|
+
return 2;
|
|
58924
|
+
}
|
|
58925
|
+
const failOnSet = failOnRaw !== void 0;
|
|
58926
|
+
const failOn = failOnSet ? failOnParsed ?? "bloquant" : void 0;
|
|
58927
|
+
const failing = failOn ? findingsAtOrAbove(result.findings, failOn) : [];
|
|
58928
|
+
if (ciFormat) emitCiFormat(result, ciFormat, CORE2, lang, failOn);
|
|
58929
|
+
else if (p.flags.json) console.log(JSON.stringify(result, null, 2));
|
|
58930
|
+
else {
|
|
58931
|
+
console.log(auditSummary(result, lang));
|
|
58932
|
+
if (failOnSet && failing.length)
|
|
58933
|
+
console.error(lang === "fr" ? `\u2717 ${failing.length} non-conformit\xE9(s) \u2265 ${failOn}.` : `\u2717 ${failing.length} non-conformity(ies) \u2265 ${failOn}.`);
|
|
58934
|
+
}
|
|
58935
|
+
return failing.length ? 1 : 0;
|
|
58936
|
+
}
|
|
58872
58937
|
async function cmdAudit(p) {
|
|
58938
|
+
const inFlag = p.flags.in;
|
|
58939
|
+
if (typeof inFlag === "string" && inFlag) return cmdAuditFromFile(p, inFlag);
|
|
58873
58940
|
const inputs = p.positionals.length ? p.positionals : ["."];
|
|
58874
58941
|
if (inputs.length === 0) {
|
|
58875
58942
|
console.error("ultra11y audit: provide files/globs, or '-' to read stdin.");
|
|
@@ -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:
|
|
8
|
+
version: 3.1.0
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -6,7 +6,7 @@ import { join as join45, relative as relative5, sep as sep7, dirname as dirname1
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "
|
|
9
|
+
var VERSION = "3.1.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -39131,7 +39131,7 @@ var colorsRules = [contrastLiteral];
|
|
|
39131
39131
|
import { readFileSync as readFileSync12 } from "fs";
|
|
39132
39132
|
|
|
39133
39133
|
// src/pixel.ts
|
|
39134
|
-
import { inflateSync } from "zlib";
|
|
39134
|
+
import { deflateSync, inflateSync } from "zlib";
|
|
39135
39135
|
var SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
39136
39136
|
function paeth(a, b, c2) {
|
|
39137
39137
|
const p = a + b - c2;
|
|
@@ -58277,6 +58277,7 @@ Usage:
|
|
|
58277
58277
|
ultra11y audit [--changed | --since <ref> | --staged] [--max-files <n>] [--dedup exact|normalized|off] [--baseline <file>] [--fail-on blocking|major|minor]
|
|
58278
58278
|
ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures + .ultra11y/pages snapshots: audit real HTML)
|
|
58279
58279
|
ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
|
|
58280
|
+
ultra11y audit --in <audit.json> [--fail-on blocking|major|minor] [--format sarif|github] (re-gate an audit already computed \u2014 e.g. one carrying adjudicated verdicts \u2014 without a second detection pass)
|
|
58280
58281
|
ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
|
|
58281
58282
|
ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--lang auto|en|fr]
|
|
58282
58283
|
ultra11y tickets --in <audit.json> [--provider auto|github|gitlab|jira] [--grain criterion|page|page-criterion|single|file] [--transport auto|cli|rest]
|
|
@@ -58869,7 +58870,73 @@ function emitCiFormat(result, format, standard, lang, failOn) {
|
|
|
58869
58870
|
);
|
|
58870
58871
|
}
|
|
58871
58872
|
}
|
|
58873
|
+
async function cmdAuditFromFile(p, inPath) {
|
|
58874
|
+
const scoping = [
|
|
58875
|
+
"since",
|
|
58876
|
+
"changed",
|
|
58877
|
+
"staged",
|
|
58878
|
+
"baseline",
|
|
58879
|
+
"graph",
|
|
58880
|
+
"cross-file",
|
|
58881
|
+
"jsx",
|
|
58882
|
+
"include",
|
|
58883
|
+
"exclude",
|
|
58884
|
+
"ext",
|
|
58885
|
+
"max-files",
|
|
58886
|
+
"dedup",
|
|
58887
|
+
"captures",
|
|
58888
|
+
"no-captures",
|
|
58889
|
+
"require-captures",
|
|
58890
|
+
"no-default-excludes",
|
|
58891
|
+
"out"
|
|
58892
|
+
];
|
|
58893
|
+
const offenders = scoping.filter((f) => p.flags[f] !== void 0);
|
|
58894
|
+
if (p.positionals.length) offenders.unshift("<paths>");
|
|
58895
|
+
if (offenders.length) {
|
|
58896
|
+
console.error(
|
|
58897
|
+
`ultra11y audit: --in re-gates an audit that already exists, so these have nothing left to change: ${offenders.join(", ")}. Drop them, or run a fresh audit without --in.`
|
|
58898
|
+
);
|
|
58899
|
+
return 2;
|
|
58900
|
+
}
|
|
58901
|
+
let result;
|
|
58902
|
+
try {
|
|
58903
|
+
const parsed = JSON.parse(inPath === "-" ? await readStdin() : readText(inPath));
|
|
58904
|
+
if (!isCurrentAudit(parsed)) {
|
|
58905
|
+
console.error("ultra11y audit: --in is not a current ultra11y AuditResult (WCAG-keyed, schema v2). Re-run `audit`.");
|
|
58906
|
+
return 2;
|
|
58907
|
+
}
|
|
58908
|
+
result = parsed;
|
|
58909
|
+
} catch {
|
|
58910
|
+
console.error(`ultra11y audit: --in file not found or not valid JSON: ${inPath}.`);
|
|
58911
|
+
return 2;
|
|
58912
|
+
}
|
|
58913
|
+
const lang = resolveLang(p.flags, { audit: result });
|
|
58914
|
+
const failOnRaw = p.flags["fail-on"];
|
|
58915
|
+
const failOnParsed = parseFailOn(failOnRaw);
|
|
58916
|
+
if (failOnRaw !== void 0 && failOnParsed === null) {
|
|
58917
|
+
console.error(`ultra11y audit: --fail-on must be blocking|major|minor (got "${String(failOnRaw)}").`);
|
|
58918
|
+
return 2;
|
|
58919
|
+
}
|
|
58920
|
+
const ciFormat = parseCiFormat(p.flags.format);
|
|
58921
|
+
if (ciFormat === null) {
|
|
58922
|
+
console.error(`ultra11y audit: --format must be sarif|github (got "${String(p.flags.format)}").`);
|
|
58923
|
+
return 2;
|
|
58924
|
+
}
|
|
58925
|
+
const failOnSet = failOnRaw !== void 0;
|
|
58926
|
+
const failOn = failOnSet ? failOnParsed ?? "bloquant" : void 0;
|
|
58927
|
+
const failing = failOn ? findingsAtOrAbove(result.findings, failOn) : [];
|
|
58928
|
+
if (ciFormat) emitCiFormat(result, ciFormat, CORE2, lang, failOn);
|
|
58929
|
+
else if (p.flags.json) console.log(JSON.stringify(result, null, 2));
|
|
58930
|
+
else {
|
|
58931
|
+
console.log(auditSummary(result, lang));
|
|
58932
|
+
if (failOnSet && failing.length)
|
|
58933
|
+
console.error(lang === "fr" ? `\u2717 ${failing.length} non-conformit\xE9(s) \u2265 ${failOn}.` : `\u2717 ${failing.length} non-conformity(ies) \u2265 ${failOn}.`);
|
|
58934
|
+
}
|
|
58935
|
+
return failing.length ? 1 : 0;
|
|
58936
|
+
}
|
|
58872
58937
|
async function cmdAudit(p) {
|
|
58938
|
+
const inFlag = p.flags.in;
|
|
58939
|
+
if (typeof inFlag === "string" && inFlag) return cmdAuditFromFile(p, inFlag);
|
|
58873
58940
|
const inputs = p.positionals.length ? p.positionals : ["."];
|
|
58874
58941
|
if (inputs.length === 0) {
|
|
58875
58942
|
console.error("ultra11y audit: provide files/globs, or '-' to read stdin.");
|
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:
|
|
7
|
+
version: 3.1.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -15,7 +15,7 @@ permissions:
|
|
|
15
15
|
steps:
|
|
16
16
|
- uses: actions/checkout@v4
|
|
17
17
|
with: { fetch-depth: 0 } # the diff gate needs the base ref
|
|
18
|
-
- uses: maxgfr/ultra11y@
|
|
18
|
+
- uses: maxgfr/ultra11y@v3 # or pin the exact version, as `init --ci` does
|
|
19
19
|
with:
|
|
20
20
|
since: auto # the PR's base branch
|
|
21
21
|
standard: rgaa
|
|
@@ -27,7 +27,7 @@ steps:
|
|
|
27
27
|
Page by page, with a real browser, in the same step:
|
|
28
28
|
|
|
29
29
|
```yaml
|
|
30
|
-
- uses: maxgfr/ultra11y@
|
|
30
|
+
- uses: maxgfr/ultra11y@v3
|
|
31
31
|
with:
|
|
32
32
|
standard: rgaa
|
|
33
33
|
start: npm run start
|
|
@@ -37,9 +37,11 @@ Page by page, with a real browser, in the same step:
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
`ultra11y init --ci` writes a workflow using it, **pinned to the exact engine version that
|
|
40
|
-
generated the file** (`@v<that version>`) so a CI run stays reproducible. `@
|
|
41
|
-
the release workflow
|
|
42
|
-
fixes automatically.
|
|
40
|
+
generated the file** (`@v<that version>`) so a CI run stays reproducible. `@v<major>` — `@v3`
|
|
41
|
+
today — is a moving alias the release workflow repoints at each release, for teams who would
|
|
42
|
+
rather take fixes automatically. It moves **within** a major only: when a breaking change cuts
|
|
43
|
+
the next one, the old alias freezes where it is, so a pipeline pinned to it keeps working and
|
|
44
|
+
stops receiving features. Never `@main`: it would change under you with no version to blame.
|
|
43
45
|
|
|
44
46
|
**Order matters, and it is deliberate**: the audit runs first, then SARIF, annotations, the
|
|
45
47
|
summary, the comment and the report — and the **gate runs last**. A failing audit has
|
|
@@ -53,6 +55,69 @@ run and an RGAA run coexist rather than overwriting each other. The Markdown rep
|
|
|
53
55
|
through `check` before being uploaded, so CI cannot publish a report citing an invented
|
|
54
56
|
criterion.
|
|
55
57
|
|
|
58
|
+
## Adjudicating the judgment criteria in CI (`adjudicate`)
|
|
59
|
+
|
|
60
|
+
Of the 55 WCAG 2.2 AA criteria the engine decides a handful; **38 are judgment calls**, and
|
|
61
|
+
under RGAA **81 of 106** can only ever derive `manual`. Inside a coding agent the agent rules
|
|
62
|
+
on them. In CI nobody does, so they stay « à évaluer » and the published conformance rate is
|
|
63
|
+
partial by construction. `adjudicate` closes that — opt-in, in two modes.
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
env:
|
|
67
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # the job's env, never an input
|
|
68
|
+
steps:
|
|
69
|
+
- uses: maxgfr/ultra11y@v3
|
|
70
|
+
with:
|
|
71
|
+
standard: rgaa
|
|
72
|
+
adjudicate: api # or `agent`
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| input | default | |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `adjudicate` | `none` | `api` · `agent` · `none` |
|
|
78
|
+
| `adjudicate-model` | *(empty)* | model id for `api`; else `$ULTRA11Y_LLM_MODEL`, else the engine default |
|
|
79
|
+
| `gate-adjudicated` | `false` | let a model-ruled NC fail the job |
|
|
80
|
+
|
|
81
|
+
**`api`** sends the worklist to the Messages API in batches of 8 and folds the verdicts back.
|
|
82
|
+
**`agent`** emits the worklist plus `orchestrate --eco`'s runbook and hands them to a
|
|
83
|
+
`claude-code-action` run, then folds the same way. Both end in the **same fail-closed gate** an
|
|
84
|
+
agent's verdicts pass through, so neither can assert a conformance the engine refuses.
|
|
85
|
+
|
|
86
|
+
The difference between them is **evidence, not trust**. `judge` rules from the harvested
|
|
87
|
+
evidence alone — capped at 30 items per criterion, snippets truncated — while the agent can
|
|
88
|
+
open the cited files and read around them, which is what *link purpose **in context*** actually
|
|
89
|
+
asks for. Expect the API mode to answer `manual` more often; that is it being honest, not
|
|
90
|
+
broken. The metric that separates them is the count of criteria left to assess.
|
|
91
|
+
|
|
92
|
+
**Both modes read `ANTHROPIC_API_KEY` from the job environment, never from an input** — a
|
|
93
|
+
composite action's steps inherit it, and a secret that never travels through an input is one
|
|
94
|
+
fewer secret to leak into a log. When the key is absent the tier **skips itself**: the job stays
|
|
95
|
+
green, the report is still written, and the criteria stay « à évaluer ». That is not an edge
|
|
96
|
+
case — it is precisely what a **fork's pull request** looks like, where secrets are never
|
|
97
|
+
exposed. An adjudication that fails mid-flight (a rate-limited batch refuses the whole fold) is
|
|
98
|
+
absorbed the same way, because losing the verdicts must never cost you the audit.
|
|
99
|
+
|
|
100
|
+
**Cost is per run and does not amortise.** Roughly $0.20 for a WCAG run and $0.50 for RGAA at
|
|
101
|
+
Sonnet pricing — the worklist is re-sent whole each time and no batch is large enough to earn
|
|
102
|
+
prompt caching. On a busy repository that is real money per push; this belongs on the default
|
|
103
|
+
branch or a schedule far more often than on every PR.
|
|
104
|
+
|
|
105
|
+
**`gate-adjudicated` trades reproducibility for reach.** By default the gate re-audits the
|
|
106
|
+
**source**, so the red/green is a pure function of the commit whatever a model said about it.
|
|
107
|
+
Turn it on and the gate evaluates the adjudicated audit instead (`audit --in`), letting a
|
|
108
|
+
model-ruled non-conformity fail the job — and accepting that two runs on the same commit may
|
|
109
|
+
now disagree. Default off, deliberately.
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
# The same re-gate, outside the action:
|
|
113
|
+
node scripts/ultra11y.mjs audit --in audits/audit-latest.json --fail-on blocking
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`--in` re-gates an audit that already exists rather than computing one, which is the only way
|
|
117
|
+
to gate on verdicts a second detection pass would not see. It refuses every flag that would
|
|
118
|
+
change *what* is audited (`--since`, `--baseline`, paths…): a gate that silently dropped your
|
|
119
|
+
scoping would be a gate nobody could trust.
|
|
120
|
+
|
|
56
121
|
# The `--format` renderings
|
|
57
122
|
|
|
58
123
|
A gate tells a developer *that* the build failed; it does not put the non-conformity **on the
|
|
@@ -170,7 +235,7 @@ The action audits the **code** and, when you point it at a served app, the **pag
|
|
|
170
235
|
list can come from three places, and none of them has to be written by hand:
|
|
171
236
|
|
|
172
237
|
```yaml
|
|
173
|
-
- uses: maxgfr/ultra11y@
|
|
238
|
+
- uses: maxgfr/ultra11y@v3
|
|
174
239
|
with:
|
|
175
240
|
standard: rgaa
|
|
176
241
|
start: npm run start
|
|
@@ -221,9 +286,9 @@ one job — the code diff, then the served pages — died on a `409 Conflict`, w
|
|
|
221
286
|
written and never uploaded. Pass `artifact-name` on each invocation:
|
|
222
287
|
|
|
223
288
|
```yaml
|
|
224
|
-
- uses: maxgfr/ultra11y@
|
|
289
|
+
- uses: maxgfr/ultra11y@v3
|
|
225
290
|
with: { since: auto, artifact-name: a11y-code }
|
|
226
|
-
- uses: maxgfr/ultra11y@
|
|
291
|
+
- uses: maxgfr/ultra11y@v3
|
|
227
292
|
with: { crawl: http://localhost:3000, artifact-name: a11y-pages }
|
|
228
293
|
```
|
|
229
294
|
|
|
@@ -151,6 +151,14 @@ A rejected adjudication leaves `audit-latest.json` untouched.
|
|
|
151
151
|
**Strictly opt-in.** With no key the command explains itself and exits 2; nothing else in the
|
|
152
152
|
engine changes.
|
|
153
153
|
|
|
154
|
+
**In the GitHub Action.** `adjudicate: api` runs exactly the command above; `adjudicate: agent`
|
|
155
|
+
hands the same worklist — plus `orchestrate --eco`'s runbook and the adjudicator contract — to
|
|
156
|
+
a `claude-code-action` run, then folds it with `verify --apply`. The agent mode exists because
|
|
157
|
+
`judge` rules from the harvested evidence alone (30 items per criterion, snippets truncated)
|
|
158
|
+
while an agent can open the cited files, which is what *link purpose in context* really needs.
|
|
159
|
+
Both skip themselves when `ANTHROPIC_API_KEY` is absent from the job environment, and both
|
|
160
|
+
absorb their own failure rather than taking the audit down with them. See `references/ci.md`.
|
|
161
|
+
|
|
154
162
|
`--max` bounds the spend and says out loud which criteria it did not submit. It is **refused
|
|
155
163
|
together with `--apply`**, before any call is made: the gate rejects an incomplete
|
|
156
164
|
adjudication by design, so the pair could only ever bill you for a guaranteed failure. Drop
|
|
@@ -64,6 +64,13 @@ verdict with no justification, an NC with no resolvable `normativeRef`, a criter
|
|
|
64
64
|
from the adjudication — each is refused by name, and the run stops. That is what keeps the
|
|
65
65
|
final report defensible.
|
|
66
66
|
|
|
67
|
+
**The shipped GitHub Action now runs this pipeline for you.** `adjudicate: agent` performs
|
|
68
|
+
steps 2–4 — `verify --manual`, then `orchestrate --run audits --phase adjudicate --eco`, then a
|
|
69
|
+
`claude-code-action` run pointed at the emitted `RUNBOOK.md`, then `verify --apply`. Reach for
|
|
70
|
+
the raw pipeline above when your orchestrator is *not* GitHub Actions, or when you want to fan
|
|
71
|
+
step 3 out across subagents rather than walk the eco path. Either way steps 1, 2, 4, 5 and 6
|
|
72
|
+
stay deterministic, and only step 3 involves a model.
|
|
73
|
+
|
|
67
74
|
## The adjudication contract
|
|
68
75
|
|
|
69
76
|
`ADJUDICATE.todo.json` declares what may be written into it, so the filler never has to read
|
|
@@ -6,7 +6,7 @@ import { join as join45, relative as relative5, sep as sep7, dirname as dirname1
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "
|
|
9
|
+
var VERSION = "3.1.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -39131,7 +39131,7 @@ var colorsRules = [contrastLiteral];
|
|
|
39131
39131
|
import { readFileSync as readFileSync12 } from "fs";
|
|
39132
39132
|
|
|
39133
39133
|
// src/pixel.ts
|
|
39134
|
-
import { inflateSync } from "zlib";
|
|
39134
|
+
import { deflateSync, inflateSync } from "zlib";
|
|
39135
39135
|
var SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
39136
39136
|
function paeth(a, b, c2) {
|
|
39137
39137
|
const p = a + b - c2;
|
|
@@ -58277,6 +58277,7 @@ Usage:
|
|
|
58277
58277
|
ultra11y audit [--changed | --since <ref> | --staged] [--max-files <n>] [--dedup exact|normalized|off] [--baseline <file>] [--fail-on blocking|major|minor]
|
|
58278
58278
|
ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures + .ultra11y/pages snapshots: audit real HTML)
|
|
58279
58279
|
ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
|
|
58280
|
+
ultra11y audit --in <audit.json> [--fail-on blocking|major|minor] [--format sarif|github] (re-gate an audit already computed \u2014 e.g. one carrying adjudicated verdicts \u2014 without a second detection pass)
|
|
58280
58281
|
ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
|
|
58281
58282
|
ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--lang auto|en|fr]
|
|
58282
58283
|
ultra11y tickets --in <audit.json> [--provider auto|github|gitlab|jira] [--grain criterion|page|page-criterion|single|file] [--transport auto|cli|rest]
|
|
@@ -58869,7 +58870,73 @@ function emitCiFormat(result, format, standard, lang, failOn) {
|
|
|
58869
58870
|
);
|
|
58870
58871
|
}
|
|
58871
58872
|
}
|
|
58873
|
+
async function cmdAuditFromFile(p, inPath) {
|
|
58874
|
+
const scoping = [
|
|
58875
|
+
"since",
|
|
58876
|
+
"changed",
|
|
58877
|
+
"staged",
|
|
58878
|
+
"baseline",
|
|
58879
|
+
"graph",
|
|
58880
|
+
"cross-file",
|
|
58881
|
+
"jsx",
|
|
58882
|
+
"include",
|
|
58883
|
+
"exclude",
|
|
58884
|
+
"ext",
|
|
58885
|
+
"max-files",
|
|
58886
|
+
"dedup",
|
|
58887
|
+
"captures",
|
|
58888
|
+
"no-captures",
|
|
58889
|
+
"require-captures",
|
|
58890
|
+
"no-default-excludes",
|
|
58891
|
+
"out"
|
|
58892
|
+
];
|
|
58893
|
+
const offenders = scoping.filter((f) => p.flags[f] !== void 0);
|
|
58894
|
+
if (p.positionals.length) offenders.unshift("<paths>");
|
|
58895
|
+
if (offenders.length) {
|
|
58896
|
+
console.error(
|
|
58897
|
+
`ultra11y audit: --in re-gates an audit that already exists, so these have nothing left to change: ${offenders.join(", ")}. Drop them, or run a fresh audit without --in.`
|
|
58898
|
+
);
|
|
58899
|
+
return 2;
|
|
58900
|
+
}
|
|
58901
|
+
let result;
|
|
58902
|
+
try {
|
|
58903
|
+
const parsed = JSON.parse(inPath === "-" ? await readStdin() : readText(inPath));
|
|
58904
|
+
if (!isCurrentAudit(parsed)) {
|
|
58905
|
+
console.error("ultra11y audit: --in is not a current ultra11y AuditResult (WCAG-keyed, schema v2). Re-run `audit`.");
|
|
58906
|
+
return 2;
|
|
58907
|
+
}
|
|
58908
|
+
result = parsed;
|
|
58909
|
+
} catch {
|
|
58910
|
+
console.error(`ultra11y audit: --in file not found or not valid JSON: ${inPath}.`);
|
|
58911
|
+
return 2;
|
|
58912
|
+
}
|
|
58913
|
+
const lang = resolveLang(p.flags, { audit: result });
|
|
58914
|
+
const failOnRaw = p.flags["fail-on"];
|
|
58915
|
+
const failOnParsed = parseFailOn(failOnRaw);
|
|
58916
|
+
if (failOnRaw !== void 0 && failOnParsed === null) {
|
|
58917
|
+
console.error(`ultra11y audit: --fail-on must be blocking|major|minor (got "${String(failOnRaw)}").`);
|
|
58918
|
+
return 2;
|
|
58919
|
+
}
|
|
58920
|
+
const ciFormat = parseCiFormat(p.flags.format);
|
|
58921
|
+
if (ciFormat === null) {
|
|
58922
|
+
console.error(`ultra11y audit: --format must be sarif|github (got "${String(p.flags.format)}").`);
|
|
58923
|
+
return 2;
|
|
58924
|
+
}
|
|
58925
|
+
const failOnSet = failOnRaw !== void 0;
|
|
58926
|
+
const failOn = failOnSet ? failOnParsed ?? "bloquant" : void 0;
|
|
58927
|
+
const failing = failOn ? findingsAtOrAbove(result.findings, failOn) : [];
|
|
58928
|
+
if (ciFormat) emitCiFormat(result, ciFormat, CORE2, lang, failOn);
|
|
58929
|
+
else if (p.flags.json) console.log(JSON.stringify(result, null, 2));
|
|
58930
|
+
else {
|
|
58931
|
+
console.log(auditSummary(result, lang));
|
|
58932
|
+
if (failOnSet && failing.length)
|
|
58933
|
+
console.error(lang === "fr" ? `\u2717 ${failing.length} non-conformit\xE9(s) \u2265 ${failOn}.` : `\u2717 ${failing.length} non-conformity(ies) \u2265 ${failOn}.`);
|
|
58934
|
+
}
|
|
58935
|
+
return failing.length ? 1 : 0;
|
|
58936
|
+
}
|
|
58872
58937
|
async function cmdAudit(p) {
|
|
58938
|
+
const inFlag = p.flags.in;
|
|
58939
|
+
if (typeof inFlag === "string" && inFlag) return cmdAuditFromFile(p, inFlag);
|
|
58873
58940
|
const inputs = p.positionals.length ? p.positionals : ["."];
|
|
58874
58941
|
if (inputs.length === 0) {
|
|
58875
58942
|
console.error("ultra11y audit: provide files/globs, or '-' to read stdin.");
|