ultra11y 2.31.2 → 2.32.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.
@@ -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 remediation backlog or GitHub issues; 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: 2.31.2
7
+ version: 2.32.0
8
8
  ---
9
9
 
10
10
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -38,21 +38,26 @@ adjudicates. So the engine's clean run is a starting point, never a verdict —
38
38
  > **Core rules:**
39
39
  > 1. **Never invent a non-conformity**: every `NC` cites a real, resolvable element (`check` verifies it).
40
40
  > 2. **Native HTML first, ARIA last**; never duplicate implicit semantics.
41
- > 3. **Residual is explicit, never silently conforming**: the AI agent *adjudicates* every
41
+ > 3. **Never invent a conformity either**: a `C` is cited exactly like an `NC` — it names in
42
+ > `citations[]` the harvested evidence it cleared, each anchor resolvable and drawn from
43
+ > the evidence that criterion was actually shown. A criterion presented with *no* evidence
44
+ > cannot be `C` at all; it stays `manual`. And an adjudicated `C` is never merged with an
45
+ > engine-decided one: reports list it separately and keep it out of the automatic pass rate.
46
+ > 4. **Residual is explicit, never silently conforming**: the AI agent *adjudicates* every
42
47
  > *judgment* criterion itself (`verify --manual`, gated), and the *rendering* criteria go to
43
48
  > `scan`; any criterion still unproven stays "to assess manually" — no status without a
44
49
  > recorded, justified verdict.
45
- > 4. **The FINAL rendered semantic HTML must be correct.** The engine sees only source; a
50
+ > 5. **The FINAL rendered semantic HTML must be correct.** The engine sees only source; a
46
51
  > component library (DSFR/MUI…) or `.vue`/`.svelte`/`.astro` SFC hides the real markup, so
47
52
  > a green source audit is not proof. Verify the produced semantic HTML — install the
48
53
  > zero-touch **capture** harvester (`render --setup`) so every component your tests render
49
54
  > is serialized to `.ultra11y/captures` and audited, with `audit --require-captures` gating
50
55
  > the blind spots. See `references/automation.md` / `rendered.md`.
51
- > 5. **Language**: ALWAYS pass `--lang` matching the language of your conversation with
56
+ > 6. **Language**: ALWAYS pass `--lang` matching the language of your conversation with
52
57
  > the user; ask the user when ambiguous. Without the flag the CLI auto-detects (repo
53
58
  > `<html lang>` → the active standard's default locale → English) — a scripted/CI
54
59
  > fallback, not a substitute for passing `--lang` yourself.
55
- > 6. **Technical tokens stay in English, even in French prose.** In any French deliverable
60
+ > 7. **Technical tokens stay in English, even in French prose.** In any French deliverable
56
61
  > you write (report commentary, PRD, GitHub issues, judgment verdicts), attribute/
57
62
  > element/role names and their values are code, not prose — never translate them:
58
63
  > `aria-live` stays `aria-live` (never « région live »), same for `tabindex`, `alt`,
@@ -85,6 +90,7 @@ Domain knowledge first, then the tooling. Read the one that matches the question
85
90
  | `references/false-positives.md` | Where a finding may still be wrong, and how to refute it |
86
91
  | `references/rendered.md` · `references/automation.md` | Auditing produced HTML, captures, hooks and CI |
87
92
  | `references/harnesses.md` | Where this runs: Claude Code, Codex CLI, OpenCode, and the AGENTS.md fallback |
93
+ | `references/orchestrators.md` | Driving the engine from a workflow engine / non-GitHub CI: pinning, the six-step pipeline, exit codes, `--issues-json` for any tracker |
88
94
  | `references/ci.md` | `--format sarif\|github`: inline PR annotations, code scanning, job summary |
89
95
  | `references/pages.md` | Page snapshots + the per-page criterion grid (`pages`), and its honesty rules |
90
96
  | `references/e2e.md` | `render --e2e`: auditing a page during your Playwright/Cypress run |
@@ -0,0 +1,176 @@
1
+ # Drive the engine from an orchestrator
2
+
3
+ The skill and the GitHub Action assume a person or a coding harness is in the loop. An
4
+ **orchestrator** — an agent workflow engine, a CI job that is not GitHub, a cron runner —
5
+ is different: nothing reads a banner, nothing retries on a hunch, and the only things that
6
+ travel between steps are exit codes and JSON. This page is the contract for that caller.
7
+
8
+ The shape that matters: **the engine finds, the agent adjudicates, the gates refuse.** Keep
9
+ the detection deterministic and there is no model between "a defect exists" and "a defect is
10
+ counted" — which is the whole reason to wire the engine in rather than ask a model to audit.
11
+
12
+ ## Resolving the engine
13
+
14
+ Pin the version. `npx` resolves `latest` otherwise, so what runs can change with no commit
15
+ anywhere.
16
+
17
+ ```sh
18
+ npm_config_update_notifier=false npx -y ultra11y@2.31.2 --version # → 2.31.2, exit 0
19
+ ```
20
+
21
+ - **`npm_config_update_notifier=false` is not optional.** Without it npm prints its
22
+ "New major version available" notice into the output a step is about to parse.
23
+ - **`ULTRA11Y_BIN`** points at an engine you already have (a vendored `ultra11y.mjs`, a
24
+ container layer). The engine is one self-contained file with no runtime dependencies, so
25
+ vendoring is a supported path — that is exactly what the skills and the Action do.
26
+ - **Node ≥ 22.18.** No other system dependency, no API key. (`judge` takes a key; nothing
27
+ else does. `scan` needs a browser tier — see `references/dynamic.md`.)
28
+ - **Verify what you resolved, and fail if it is not what you asked for.** A step that lets a
29
+ missing engine fall through reports zero findings, which reads exactly like a clean audit.
30
+ This is the one failure mode worth being loud about.
31
+
32
+ ## The pipeline
33
+
34
+ Six steps. Four are deterministic; only step 3 involves a model.
35
+
36
+ ```sh
37
+ ENGINE="npx -y ultra11y@2.31.2" # or "node $ULTRA11Y_BIN"
38
+ RUN=/tmp/a11y-run # out-of-tree: see "Where output lands"
39
+
40
+ # 1. FIND — the static engine. No model. This is the finding set.
41
+ $ENGINE audit "<globs>" --graph --out "$RUN" --json --lang en > /dev/null
42
+ # …or, on a pull request, exactly what the branch introduced:
43
+ $ENGINE audit --since "$BASE_REF" --graph --out "$RUN" --json --lang en > /dev/null
44
+
45
+ # 2. WORKLIST — a draft report, then the criteria a static pass cannot decide.
46
+ $ENGINE report --in "$RUN/audit-latest.json" --out "$RUN" --lang en > /dev/null
47
+ $ENGINE verify --report "$RUN"/wcag-*.md --in "$RUN/audit-latest.json" --manual --out "$RUN" --json
48
+
49
+ # 3. ADJUDICATE — hand $RUN/ADJUDICATE.todo.json to the agent; it fills every `verdict`.
50
+ # The file carries its own `contract` (see below) — the agent needs nothing else.
51
+
52
+ # 4. FOLD — fail-closed. Exit 1 with per-criterion reasons if a verdict is unsupported.
53
+ $ENGINE verify --apply "$RUN/adjudication.json" --in "$RUN/audit-latest.json" --out "$RUN" --json
54
+
55
+ # 5. REPORT — deterministic rendering of the adjudicated audit.
56
+ $ENGINE report --in "$RUN/audit-latest.json" --out audits --standard rgaa --lang fr
57
+
58
+ # 6. GATE — integrity. Exit ≠ 0 on an invented criterion or unjustified NA.
59
+ $ENGINE check --report audits/rgaa-*.md --in "$RUN/audit-latest.json" --json
60
+ ```
61
+
62
+ Step 4 is the one to understand: it is a **deterministic gate on the model's output**. A
63
+ verdict with no justification, an NC with no resolvable `normativeRef`, a criterion missing
64
+ from the adjudication — each is refused by name, and the run stops. That is what keeps the
65
+ final report defensible.
66
+
67
+ ## The adjudication contract
68
+
69
+ `ADJUDICATE.todo.json` declares what may be written into it, so the filler never has to read
70
+ ultra11y's source:
71
+
72
+ ```jsonc
73
+ {
74
+ "contract": {
75
+ "verdicts": ["C", "NC", "NA", "manual"],
76
+ "manualReasons": ["needs-rendered-dom", "undecidable"],
77
+ "requires": {
78
+ "C": "a non-empty justification AND citations[] naming the harvested evidence it cleared …",
79
+ "NA": "a non-empty justification",
80
+ "NC": "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
81
+ "manual": "a reason ∈ {needs-rendered-dom, undecidable}"
82
+ }
83
+ },
84
+ "items": [ /* one per criterion the engine could not decide; fill `verdict` */ ]
85
+ }
86
+ ```
87
+
88
+ Note what `C` costs: a clearing verdict is gated exactly like an accusing one. It must
89
+ name in `citations[]` the evidence it cleared, each anchor resolvable and drawn from that
90
+ criterion's own harvested evidence — and a criterion presented with **no** evidence cannot
91
+ be `C` at all. This is what stops "C everywhere with a plausible sentence" from passing.
92
+
93
+ The header is **advisory**. `verify --apply` validates against its own constants, so editing
94
+ the contract in the file widens nothing.
95
+
96
+ Verdicts are matched **case-insensitively** (`na` is `NA`, `MANUAL` is `manual`) — a spelling
97
+ accident is not a disagreement about accessibility. A verdict outside the vocabulary is still
98
+ refused, and the message names the four that are not.
99
+
100
+ `manual` is the honest verdict for anything that genuinely needs a rendered page. It keeps
101
+ the criterion in `residualRisks`, where the report declares it as a residual risk. **Never
102
+ map an undecided criterion to `C`** — that is the one way to turn this pipeline back into
103
+ the thing it replaces.
104
+
105
+ ## Filing findings on a tracker that is not GitHub
106
+
107
+ `--gh-issues` shells out to the `gh` CLI. For any other board, take the same items as JSON:
108
+
109
+ ```sh
110
+ $ENGINE prd --in "$RUN/audit-latest.json" --out "$RUN" --issues-json --lang en
111
+ # → $RUN/issues-<date>.json
112
+ ```
113
+
114
+ ```jsonc
115
+ {
116
+ "tool": "ultra11y", "kind": "issues", "schemaVersion": 1,
117
+ "standard": "wcag", "date": "2026-08-12", "count": 4,
118
+ "issues": [{
119
+ "criteriaId": "1.1.1",
120
+ "title": "[a11y] WCAG 1.1.1 — Non-text Content", // the de-dupe grain: stable across runs
121
+ "body": "…", // the auditor block, rendered
122
+ "labels": ["accessibility", "wcag", "bloquant"],
123
+ "severity": "bloquant",
124
+ "advisory": false, // true ⇒ a good practice, NOT a non-conformity
125
+ "occurrences": [{ "file": "src/Page.tsx", "line": 4, "selector": "img", "message": "…" }]
126
+ }]
127
+ }
128
+ ```
129
+
130
+ One issue per criterion, not per occurrence — a missing `alt` on forty images is one item
131
+ with forty `occurrences`, not forty tickets. De-dupe on `title` and re-running files nothing
132
+ twice. `advisory: true` marks a non-normative recommendation: it must not be triaged as a
133
+ non-conformity, and it never enters the conformance rate.
134
+
135
+ ## Where output lands
136
+
137
+ `--out <dir>` fully contains what a command writes: pointed outside the checkout, an audit
138
+ leaves the target repository byte-identical. That matters when the orchestrator audits a
139
+ repo it does not own — write the run's intermediates out-of-tree and let only the final
140
+ dated report land in the project, if at all.
141
+
142
+ `.ultra11y/pages/` is written only by the page-snapshot commands (`scan`, `snapshot`,
143
+ `dev`); `--root` relocates it. `audit`, `report`, `prd`, `verify` and `check` never touch it.
144
+
145
+ ## Exit codes
146
+
147
+ | | 0 | 1 | 2 |
148
+ |---|---|---|---|
149
+ | `audit` | reported (no `--fail-on`), or clean | findings ≥ `--fail-on` | usage / bad input |
150
+ | `verify --apply` | folded | the adjudication was refused (see `issues[]`) | usage / bad input |
151
+ | `check` | report is sound | integrity failure | report not found / bad input |
152
+ | `prd` | written | `--gh-*` had failures | usage / bad input |
153
+
154
+ Read exit codes from the process, **not** from a pipeline — `cmd --json | head` reports
155
+ `head`'s status, which is how a failing gate reads as a pass.
156
+
157
+ ## What the JSON tells you
158
+
159
+ - `audit --json` → the `AuditResult` (also at `<out>/audit-latest.json`): `findings[]` with
160
+ stable `findingId`, `criteriaId`, `severity`, `file`, `line`; `residualRisks[]`;
161
+ `conformancePct` — the **automatic static pass rate**, not a conformance rate.
162
+ - `verify --apply --json` → `{ ok, auditPath, applied, stillManual, conformancePct, findings, grounding }`
163
+ on success; `{ ok: false, issues[], audit }` when refused.
164
+ - `check --json` → `{ ok, issues[] }`.
165
+
166
+ ## Honesty rules an orchestrator has to carry
167
+
168
+ The engine is explicit about what it did not decide; a wrapper that flattens that away
169
+ produces exactly the false clean bill of health this design exists to prevent.
170
+
171
+ 1. **A missing engine is a failure, never an empty finding set.**
172
+ 2. **Report the residual criteria.** The rendering criteria (contrast, focus, zoom, reflow)
173
+ are undecided until `scan` runs. Say so; do not let a step summary imply full coverage.
174
+ 3. **`conformancePct` is the static pass rate.** Label it as such.
175
+ 4. **Never let a model write the counts.** Take them from the JSON — the numbers exist
176
+ precisely so nothing has to be re-derived by a model that may drop a field.
@@ -205,9 +205,13 @@ Two things it deliberately does NOT do:
205
205
  page sheet, the compliance report, the PRD and the GitHub issue are the same block — and
206
206
  the occurrence lines stay parseable by `verify`.
207
207
 
208
- The screenshot is *referenced*, never copied: `.ultra11y/pages/<id>/screen.png` is already on
209
- disk, so the path is resolved relative to wherever the file lands. A page with no screenshot
210
- says so, rather than showing nothing.
208
+ The screenshot is **copied next to the report** when you pass `--out`: each page's
209
+ `.ultra11y/pages/<id>/screen.png` lands in `<out>/assets/<id>.png` and is linked relatively
210
+ from there. That is what makes the output directory self-contained — CI uploads `audits/`
211
+ alone as the artefact, and a `../../.ultra11y/…` reference would be a broken image in every
212
+ sheet the reviewer opens. Without `--out` (streaming to stdout) the path stays relative,
213
+ since there is no directory to keep intact. A page with no screenshot says so, rather than
214
+ showing nothing.
211
215
 
212
216
  Sheets are named `page-<id>.md`: a page whose id is `index` — the ordinary id for an
213
217
  `index.html` target — would otherwise be written over the index itself.