ultra11y 5.3.3 → 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.3
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:
@@ -71,6 +71,31 @@ test).
71
71
  the **produced** HTML (see `render` / audit the build), otherwise the verdict is a false
72
72
  negative.
73
73
 
74
+ ## When the adjudicator has no shell (CI)
75
+
76
+ `verify --manual` writes the worklist **twice**, plus one brief per criterion:
77
+
78
+ | file | for |
79
+ |---|---|
80
+ | `ADJUDICATE.todo.json` · `ADJUDICATE.md` | a session **with a shell**: evidence inline, edit in place, fold yourself |
81
+ | `ADJUDICATE.verdicts.json` | the **only file to write** when you have no shell — verdicts, no evidence |
82
+ | `adjudicate/<criteriaId>.md` | one small brief per criterion: its evidence, its protocol, its citable tests |
83
+
84
+ Why the split exists, measured rather than assumed: under RGAA the worklist is 96 criteria
85
+ carrying 1590 harvested anchors — **536 KB** of JSON and **466 KB** of Markdown. An agent given
86
+ `Read/Grep/Glob/Edit/Write` cannot work with that: reading either document swamps its context,
87
+ and filling 96 verdicts inside half a megabyte is 96 exact-match edits. A real CI run spent 75
88
+ of 424 turns, hit 17 permission denials trying to run the commands the runbook prescribes, and
89
+ returned the file untouched — so the fail-closed fold discarded all 96 verdicts and every
90
+ criterion stayed « to assess », in a job that reported success. The verdicts file is **37 KB**
91
+ for the same worklist, and each brief is a few KB.
92
+
93
+ `verify --apply` accepts either file. Given the verdicts-only one it **re-derives the evidence
94
+ from the audit** (the worklist is a pure function of the audit) and then runs the identical
95
+ fold: same coverage checks in both directions, same citation matching against *that criterion's*
96
+ own anchors, same refusals. The smaller surface buys nothing from the gate — a `C` that cites
97
+ an anchor it was never shown is refused exactly as before.
98
+
74
99
  ## Adversarial verification of the non-conformities — `verify --report`
75
100
 
76
101
  Unchanged: `node scripts/ultra11y.mjs verify --report audits/wcag-YYYY-MM-DD.md` writes `VERIFY.md`
@@ -92,7 +117,7 @@ Then `node scripts/ultra11y.mjs verify --apply VERIFY.todo.json` is green again
92
117
  ## Under a country standard, the worklist speaks that standard
93
118
 
94
119
  `verify --manual --standard rgaa` keys the worklist by **RGAA criteria**, not WCAG success
95
- 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
96
121
  ever derive `manual`. Each item carries, inline:
97
122
 
98
123
  - the criterion's **numbered tests, in full** (`11.2.1` … `11.2.6`) — what actually has to be
@@ -138,15 +163,27 @@ gate refuses:
138
163
 
139
164
  | It returns | The gate does |
140
165
  |---|---|
141
- | `C`/`NA` with no justification | rejects the whole adjudication |
142
- | `NC` citing nothing, or a `file:line` that does not resolve against real source | rejects |
143
- | `NC` citing a `normativeRef` belonging to another criterion | rejects |
144
- | `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 |
145
170
  | a verdict for a criterion nobody asked about | dropped before it reaches the gate |
146
- | 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 |
147
172
  | all `manual` with reasons | **accepts** — that is a correct answer, not a failure |
148
173
 
149
- 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`.
150
187
 
151
188
  **Strictly opt-in.** With no key the command explains itself and exits 2; nothing else in the
152
189
  engine changes.
@@ -159,8 +196,9 @@ while an agent can open the cited files, which is what *link purpose in context*
159
196
  Both skip themselves when `ANTHROPIC_API_KEY` is absent from the job environment, and both
160
197
  absorb their own failure rather than taking the audit down with them. See `references/ci.md`.
161
198
 
162
- `--max` bounds the spend and says out loud which criteria it did not submit. It is **refused
163
- together with `--apply`**, before any call is made: the gate rejects an incomplete
164
- adjudication by design, so the pair could only ever bill you for a guaranteed failure. Drop
165
- `--max` to adjudicate everything, or drop `--apply` to produce the worklist now and fold it
166
- 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