ultra11y 5.3.4 → 5.4.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 backlog, or tickets (GitHub/GitLab/Jira); author accessible markup from scratch; wire the repo gate (init --hook/--ci) or a rendered-DOM capture pipeline. For a review of the code UNDER CHANGE (diff, branch, PR), use the `review-a11y` skill instead."
5
5
  license: MIT
6
6
  metadata:
7
- version: 5.3.4
7
+ version: 5.4.0
8
8
  ---
9
9
 
10
10
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -54,7 +54,7 @@ adjudicates. So the engine's clean run is a starting point, never a verdict —
54
54
  > decides what its terms mean. An auditor block that cites `8.3.1` after reading it is
55
55
  > grounded; one that cites it from memory is a guess wearing a reference. Before auditing
56
56
  > against a country standard, run the plan (`ultra11y_method`): it tells you how many of its
57
- > criteria no tool will decide for you — for RGAA, **58 of 106**. See `references/mcp.md`.
57
+ > criteria no tool will decide for you — for RGAA, **59 of 106**. See `references/mcp.md`.
58
58
  > 6. **The FINAL rendered semantic HTML must be correct.** The engine sees only source; a
59
59
  > component library (DSFR/MUI…) or `.vue`/`.svelte`/`.astro` SFC hides the real markup, so
60
60
  > a green source audit is not proof. Verify the produced semantic HTML — install the
@@ -62,12 +62,54 @@ run and an RGAA run coexist rather than overwriting each other. The Markdown rep
62
62
  through `check` before being uploaded, so CI cannot publish a report citing an invented
63
63
  criterion.
64
64
 
65
- ## Adjudicating the judgment criteria in CI (`adjudicate`)
65
+ ## The verdict ledger (`ledger`) — a complete grid in CI, with no model
66
66
 
67
67
  Of the 55 WCAG 2.2 AA criteria the engine decides a handful; **38 are judgment calls**, and
68
- under RGAA **81 of 106** can only ever derive `manual`. Inside a coding agent the agent rules
69
- on them. In CI nobody does, so they stay « à évaluer » and the published conformance rate is
70
- partial by construction. `adjudicate` closes that — opt-in, in two modes.
68
+ under RGAA **59 of 106** can only ever derive `manual`. Inside a coding agent the agent rules on
69
+ them. In CI nobody does — so before the ledger, a CI grid was partial by construction and the
70
+ only way to close it was to pay for an agent pass on every run.
71
+
72
+ A **ledger** is a file of verdicts committed to the audited repository
73
+ (`.ultra11y/verdicts/<standard>.json`), each carrying its justification, its citations, and a
74
+ FINGERPRINT of the evidence it was ruled against:
75
+
76
+ ```yaml
77
+ - uses: maxgfr/ultra11y@v5
78
+ with:
79
+ standard: rgaa
80
+ ledger: 'true' # or an explicit path
81
+ ```
82
+
83
+ Replaying it is **a re-verification, not a cache hit**. Every stored verdict is rebuilt into an
84
+ ordinary adjudication over the evidence re-derived from the audit in front of it, and folded
85
+ through the same `applyAdjudication` — same coverage checks in both directions, same citation
86
+ matching against the criterion's own anchors, same content-level re-grounding. A verdict nobody
87
+ can prove is refused in CI exactly as it would be in a session.
88
+
89
+ Three properties are worth knowing:
90
+
91
+ - **A stale verdict is dropped, never carried.** When the code under a criterion changes, the
92
+ evidence changes with it, the fingerprint stops matching, and the criterion returns to « à
93
+ évaluer » saying so — with the date it was recorded and both evidence counts. A verdict that
94
+ silently outlives the code it described is the one thing a conformance deliverable cannot
95
+ afford. The log names every stale and every uncovered criterion, so a refresh pass knows
96
+ exactly what to re-adjudicate.
97
+ - **A reformatting is not a change.** The fingerprint ignores line numbers and the replay
98
+ re-anchors stored citations onto today's lines, so adding a comment at the top of a file does
99
+ not invalidate every verdict in it.
100
+ - **A missing or refused ledger never fails the job.** The criteria stay « à évaluer », which is
101
+ where they would have been without one.
102
+
103
+ To FILL it, run an adjudication with `ledger` set: the accepted verdicts are recorded on the way
104
+ out. Only accepted ones — a refused verdict in the ledger would be laundered back in on the next
105
+ replay. A defensible split is a weekly (or `workflow_dispatch`) job that adjudicates and commits
106
+ the refreshed ledger, and a pull-request job that only replays it.
107
+
108
+ ## Adjudicating the judgment criteria in CI (`adjudicate`)
109
+
110
+ `adjudicate` is what FILLS the ledger — opt-in, in two modes. It runs **after** the replay, so a
111
+ paid pass only ever covers what the ledger did not: on a repository whose ledger is current, it
112
+ has nothing left to rule on and costs nothing.
71
113
 
72
114
  ```yaml
73
115
  env:
@@ -117,7 +117,7 @@ Then `node scripts/ultra11y.mjs verify --apply VERIFY.todo.json` is green again
117
117
  ## Under a country standard, the worklist speaks that standard
118
118
 
119
119
  `verify --manual --standard rgaa` keys the worklist by **RGAA criteria**, not WCAG success
120
- criteria — which is the granularity that matters, since 99 of RGAA's 106 criteria can only
120
+ criteria — which is the granularity that matters, since 59 of RGAA's 106 criteria can only
121
121
  ever derive `manual`. Each item carries, inline:
122
122
 
123
123
  - the criterion's **numbered tests, in full** (`11.2.1` … `11.2.6`) — what actually has to be
@@ -163,15 +163,27 @@ gate refuses:
163
163
 
164
164
  | It returns | The gate does |
165
165
  |---|---|
166
- | `C`/`NA` with no justification | rejects the whole adjudication |
167
- | `NC` citing nothing, or a `file:line` that does not resolve against real source | rejects |
168
- | `NC` citing a `normativeRef` belonging to another criterion | rejects |
169
- | `manual` with no reason | rejects |
166
+ | `C`/`NA` with no justification | refuses THAT criterion |
167
+ | `NC` citing nothing, or a `file:line` that does not resolve against real source | refuses it |
168
+ | `NC` citing a `normativeRef` belonging to another criterion | refuses it |
169
+ | `manual` with no reason | refuses it |
170
170
  | a verdict for a criterion nobody asked about | dropped before it reaches the gate |
171
- | fewer verdicts than criteria (a truncated `--max` run, a failed batch) | rejects on coverage |
171
+ | fewer verdicts than criteria (a bounded `--max` run, a failed batch) | the missing ones stay « à évaluer »; the rest lands |
172
172
  | all `manual` with reasons | **accepts** — that is a correct answer, not a failure |
173
173
 
174
- A rejected adjudication leaves `audit-latest.json` untouched.
174
+ **The fold is fail-closed per VERDICT, not per FILE.** No refused verdict is ever applied, and a
175
+ refusal costs its own criterion and nothing else: that criterion stays « à évaluer » carrying the
176
+ refusal as its reason, and every verdict that proved itself lands. This is not a loosening of the
177
+ gate — every check above is unchanged — it is a change of blast radius, and it was measured:
178
+ a CI run filled 95 of 96 verdicts correctly, one came back `null`, and the old file-level fold
179
+ discarded all 96, so a $16 adjudication published « à évaluer » across the whole grid in a job
180
+ that reported success.
181
+
182
+ `--strict` restores the all-or-nothing fold for a caller who genuinely wants it (a deliverable
183
+ signed off in one pass); there, a rejected adjudication leaves `audit-latest.json` untouched.
184
+
185
+ Whatever lands can be RECORDED with `--ledger <path>` and replayed on a later run with no model
186
+ in the loop — see `references/ci.md`.
175
187
 
176
188
  **Strictly opt-in.** With no key the command explains itself and exits 2; nothing else in the
177
189
  engine changes.
@@ -184,8 +196,9 @@ while an agent can open the cited files, which is what *link purpose in context*
184
196
  Both skip themselves when `ANTHROPIC_API_KEY` is absent from the job environment, and both
185
197
  absorb their own failure rather than taking the audit down with them. See `references/ci.md`.
186
198
 
187
- `--max` bounds the spend and says out loud which criteria it did not submit. It is **refused
188
- together with `--apply`**, before any call is made: the gate rejects an incomplete
189
- adjudication by design, so the pair could only ever bill you for a guaranteed failure. Drop
190
- `--max` to adjudicate everything, or drop `--apply` to produce the worklist now and fold it
191
- in later.
199
+ `--max` bounds the spend and says out loud which criteria it did not submit. It now composes
200
+ with `--apply`: the fold is per-verdict, so a bounded run lands what it covered and the rest stay
201
+ « à évaluer » — which is exactly what bounding spend is for. (It used to be refused outright, and
202
+ rightly so while the fold was all-or-nothing: the pair could only ever bill a full run to
203
+ guarantee a coverage failure. That refusal survives under `--apply --strict`, where it is still
204
+ true.)
@@ -292,10 +292,19 @@ criterion means one definite failure fired *somewhere* — not that the engine c
292
292
  criterion. On a page where the failure did not fire, the engine knows "no definite failure
293
293
  here", which is not "conforming": alt relevance, link purpose and reading order are still
294
294
  nobody's verdict. So only the `static` criteria (the ones with an applicability predicate and
295
- rules, `src/audit.ts` `APPLICABLE`) earn `C` by silence — under RGAA that is 4.10 and
296
- 8.3/8.4/8.5/8.6. Everything else stays « à évaluer » until `scan` or an adjudication decides
297
- it. Before this rule a page with no images scored 100% on « chaque image a-t-elle une
298
- alternative pertinente ? », a rate computed over criteria nobody had assessed.
295
+ rules, `src/audit.ts` `APPLICABLE`) earn `C` by silence — under RGAA that is **8.3 and 8.5, and
296
+ those two only**. 4.10, 8.4 and 8.6 look like they should qualify and do not: each is flagged
297
+ `judgment: true`, because the criterion asks more than its mapped success criterion does (8.6
298
+ asks whether the page title is *pertinent*; 2.4.2 only that a title exists), so `judgmentGuard`
299
+ turns their `C` into `manual` on purpose. Everything else stays « à évaluer » until `scan`, the
300
+ snapshot tier or an adjudication decides it. Before this rule a page with no images scored 100%
301
+ on « chaque image a-t-elle une alternative pertinente ? », a rate computed over criteria nobody
302
+ had assessed.
303
+
304
+ What a criterion CAN be decided as, without any of that, is a separate question: since the
305
+ engine learned to prove applicability (`src/audit.ts` `SUBJECT_MATTER`), a criterion whose
306
+ subject matter is absent from the whole scope closes as a justified `NA` — so a repository with
307
+ no media reports RGAA theme 4 as not applicable rather than as « à évaluer ».
299
308
 
300
309
  A non-normative recommendation never flips a page criterion to `NC`, exactly as in core.
301
310