ultra11y 2.32.1 → 3.0.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 +12 -1
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +68 -1
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +68 -1
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/references/ci.md +63 -0
- package/skills/ultra11y/references/judgment.md +8 -0
- package/skills/ultra11y/references/orchestrators.md +7 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +68 -1
- package/src/cli.ts +84 -0
- 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.0.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.0.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.0.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.0.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -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`):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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",
|
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.0.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -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.0.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.0.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -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.0.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -53,6 +53,69 @@ run and an RGAA run coexist rather than overwriting each other. The Markdown rep
|
|
|
53
53
|
through `check` before being uploaded, so CI cannot publish a report citing an invented
|
|
54
54
|
criterion.
|
|
55
55
|
|
|
56
|
+
## Adjudicating the judgment criteria in CI (`adjudicate`)
|
|
57
|
+
|
|
58
|
+
Of the 55 WCAG 2.2 AA criteria the engine decides a handful; **38 are judgment calls**, and
|
|
59
|
+
under RGAA **81 of 106** can only ever derive `manual`. Inside a coding agent the agent rules
|
|
60
|
+
on them. In CI nobody does, so they stay « à évaluer » and the published conformance rate is
|
|
61
|
+
partial by construction. `adjudicate` closes that — opt-in, in two modes.
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
env:
|
|
65
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # the job's env, never an input
|
|
66
|
+
steps:
|
|
67
|
+
- uses: maxgfr/ultra11y@v2
|
|
68
|
+
with:
|
|
69
|
+
standard: rgaa
|
|
70
|
+
adjudicate: api # or `agent`
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
| input | default | |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `adjudicate` | `none` | `api` · `agent` · `none` |
|
|
76
|
+
| `adjudicate-model` | *(empty)* | model id for `api`; else `$ULTRA11Y_LLM_MODEL`, else the engine default |
|
|
77
|
+
| `gate-adjudicated` | `false` | let a model-ruled NC fail the job |
|
|
78
|
+
|
|
79
|
+
**`api`** sends the worklist to the Messages API in batches of 8 and folds the verdicts back.
|
|
80
|
+
**`agent`** emits the worklist plus `orchestrate --eco`'s runbook and hands them to a
|
|
81
|
+
`claude-code-action` run, then folds the same way. Both end in the **same fail-closed gate** an
|
|
82
|
+
agent's verdicts pass through, so neither can assert a conformance the engine refuses.
|
|
83
|
+
|
|
84
|
+
The difference between them is **evidence, not trust**. `judge` rules from the harvested
|
|
85
|
+
evidence alone — capped at 30 items per criterion, snippets truncated — while the agent can
|
|
86
|
+
open the cited files and read around them, which is what *link purpose **in context*** actually
|
|
87
|
+
asks for. Expect the API mode to answer `manual` more often; that is it being honest, not
|
|
88
|
+
broken. The metric that separates them is the count of criteria left to assess.
|
|
89
|
+
|
|
90
|
+
**Both modes read `ANTHROPIC_API_KEY` from the job environment, never from an input** — a
|
|
91
|
+
composite action's steps inherit it, and a secret that never travels through an input is one
|
|
92
|
+
fewer secret to leak into a log. When the key is absent the tier **skips itself**: the job stays
|
|
93
|
+
green, the report is still written, and the criteria stay « à évaluer ». That is not an edge
|
|
94
|
+
case — it is precisely what a **fork's pull request** looks like, where secrets are never
|
|
95
|
+
exposed. An adjudication that fails mid-flight (a rate-limited batch refuses the whole fold) is
|
|
96
|
+
absorbed the same way, because losing the verdicts must never cost you the audit.
|
|
97
|
+
|
|
98
|
+
**Cost is per run and does not amortise.** Roughly $0.20 for a WCAG run and $0.50 for RGAA at
|
|
99
|
+
Sonnet pricing — the worklist is re-sent whole each time and no batch is large enough to earn
|
|
100
|
+
prompt caching. On a busy repository that is real money per push; this belongs on the default
|
|
101
|
+
branch or a schedule far more often than on every PR.
|
|
102
|
+
|
|
103
|
+
**`gate-adjudicated` trades reproducibility for reach.** By default the gate re-audits the
|
|
104
|
+
**source**, so the red/green is a pure function of the commit whatever a model said about it.
|
|
105
|
+
Turn it on and the gate evaluates the adjudicated audit instead (`audit --in`), letting a
|
|
106
|
+
model-ruled non-conformity fail the job — and accepting that two runs on the same commit may
|
|
107
|
+
now disagree. Default off, deliberately.
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
# The same re-gate, outside the action:
|
|
111
|
+
node scripts/ultra11y.mjs audit --in audits/audit-latest.json --fail-on blocking
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`--in` re-gates an audit that already exists rather than computing one, which is the only way
|
|
115
|
+
to gate on verdicts a second detection pass would not see. It refuses every flag that would
|
|
116
|
+
change *what* is audited (`--since`, `--baseline`, paths…): a gate that silently dropped your
|
|
117
|
+
scoping would be a gate nobody could trust.
|
|
118
|
+
|
|
56
119
|
# The `--format` renderings
|
|
57
120
|
|
|
58
121
|
A gate tells a developer *that* the build failed; it does not put the non-conformity **on the
|
|
@@ -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.0.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -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/src/cli.ts
CHANGED
|
@@ -69,6 +69,7 @@ Usage:
|
|
|
69
69
|
ultra11y audit [--changed | --since <ref> | --staged] [--max-files <n>] [--dedup exact|normalized|off] [--baseline <file>] [--fail-on blocking|major|minor]
|
|
70
70
|
ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures + .ultra11y/pages snapshots: audit real HTML)
|
|
71
71
|
ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
|
|
72
|
+
ultra11y audit --in <audit.json> [--fail-on blocking|major|minor] [--format sarif|github] (re-gate an audit already computed — e.g. one carrying adjudicated verdicts — without a second detection pass)
|
|
72
73
|
ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
|
|
73
74
|
ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--lang auto|en|fr]
|
|
74
75
|
ultra11y tickets --in <audit.json> [--provider auto|github|gitlab|jira] [--grain criterion|page|page-criterion|single|file] [--transport auto|cli|rest]
|
|
@@ -751,7 +752,90 @@ function emitCiFormat(result: AuditResult, format: CiFormat, standard: StandardI
|
|
|
751
752
|
}
|
|
752
753
|
}
|
|
753
754
|
|
|
755
|
+
// `audit --in <audit.json>` — RE-GATE an audit that was already computed, instead of
|
|
756
|
+
// computing one. It exists for one caller: a pipeline that folded ADJUDICATED verdicts into
|
|
757
|
+
// audit-latest.json and now wants the same severity gate applied to that result. Re-running
|
|
758
|
+
// detection there would be wrong twice over — it would not see the verdicts, and it would
|
|
759
|
+
// spend a second full pass to reach a conclusion the file already holds.
|
|
760
|
+
//
|
|
761
|
+
// Deliberately narrow: this path GATES and RENDERS, nothing else. Every flag that would
|
|
762
|
+
// change what is audited is refused rather than ignored, because a gate that silently drops
|
|
763
|
+
// your scoping is a gate nobody can trust. Re-running detection is what `audit <globs>` is
|
|
764
|
+
// for.
|
|
765
|
+
async function cmdAuditFromFile(p: ParsedArgs, inPath: string): Promise<number> {
|
|
766
|
+
const scoping = [
|
|
767
|
+
"since",
|
|
768
|
+
"changed",
|
|
769
|
+
"staged",
|
|
770
|
+
"baseline",
|
|
771
|
+
"graph",
|
|
772
|
+
"cross-file",
|
|
773
|
+
"jsx",
|
|
774
|
+
"include",
|
|
775
|
+
"exclude",
|
|
776
|
+
"ext",
|
|
777
|
+
"max-files",
|
|
778
|
+
"dedup",
|
|
779
|
+
"captures",
|
|
780
|
+
"no-captures",
|
|
781
|
+
"require-captures",
|
|
782
|
+
"no-default-excludes",
|
|
783
|
+
"out",
|
|
784
|
+
];
|
|
785
|
+
const offenders = scoping.filter((f) => p.flags[f] !== undefined);
|
|
786
|
+
if (p.positionals.length) offenders.unshift("<paths>");
|
|
787
|
+
if (offenders.length) {
|
|
788
|
+
console.error(
|
|
789
|
+
`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.`,
|
|
790
|
+
);
|
|
791
|
+
return 2;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
let result: AuditResult;
|
|
795
|
+
try {
|
|
796
|
+
const parsed = JSON.parse(inPath === "-" ? await readStdin() : readText(inPath)) as unknown;
|
|
797
|
+
if (!isCurrentAudit(parsed)) {
|
|
798
|
+
console.error("ultra11y audit: --in is not a current ultra11y AuditResult (WCAG-keyed, schema v2). Re-run `audit`.");
|
|
799
|
+
return 2;
|
|
800
|
+
}
|
|
801
|
+
result = parsed;
|
|
802
|
+
} catch {
|
|
803
|
+
console.error(`ultra11y audit: --in file not found or not valid JSON: ${inPath}.`);
|
|
804
|
+
return 2;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const lang = resolveLang(p.flags, { audit: result });
|
|
808
|
+
|
|
809
|
+
const failOnRaw = p.flags["fail-on"];
|
|
810
|
+
const failOnParsed = parseFailOn(failOnRaw);
|
|
811
|
+
if (failOnRaw !== undefined && failOnParsed === null) {
|
|
812
|
+
console.error(`ultra11y audit: --fail-on must be blocking|major|minor (got "${String(failOnRaw)}").`);
|
|
813
|
+
return 2;
|
|
814
|
+
}
|
|
815
|
+
const ciFormat = parseCiFormat(p.flags.format);
|
|
816
|
+
if (ciFormat === null) {
|
|
817
|
+
console.error(`ultra11y audit: --format must be sarif|github (got "${String(p.flags.format)}").`);
|
|
818
|
+
return 2;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const failOnSet = failOnRaw !== undefined;
|
|
822
|
+
const failOn = failOnSet ? (failOnParsed ?? "bloquant") : undefined;
|
|
823
|
+
const failing = failOn ? findingsAtOrAbove(result.findings, failOn) : [];
|
|
824
|
+
|
|
825
|
+
if (ciFormat) emitCiFormat(result, ciFormat, CORE, lang, failOn);
|
|
826
|
+
else if (p.flags.json) console.log(JSON.stringify(result, null, 2));
|
|
827
|
+
else {
|
|
828
|
+
console.log(auditSummary(result, lang));
|
|
829
|
+
if (failOnSet && failing.length)
|
|
830
|
+
console.error(lang === "fr" ? `✗ ${failing.length} non-conformité(s) ≥ ${failOn}.` : `✗ ${failing.length} non-conformity(ies) ≥ ${failOn}.`);
|
|
831
|
+
}
|
|
832
|
+
return failing.length ? 1 : 0;
|
|
833
|
+
}
|
|
834
|
+
|
|
754
835
|
async function cmdAudit(p: ParsedArgs): Promise<number> {
|
|
836
|
+
const inFlag = p.flags.in;
|
|
837
|
+
if (typeof inFlag === "string" && inFlag) return cmdAuditFromFile(p, inFlag);
|
|
838
|
+
|
|
755
839
|
const inputs = p.positionals.length ? p.positionals : ["."];
|
|
756
840
|
if (inputs.length === 0) {
|
|
757
841
|
console.error("ultra11y audit: provide files/globs, or '-' to read stdin.");
|
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 = "
|
|
3
|
+
export const VERSION = "3.0.0";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|