ultra11y 4.0.1 → 4.2.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: 4.0.1
7
+ version: 4.2.0
8
8
  ---
9
9
 
10
10
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -262,6 +262,115 @@ Two surfaces come out of it:
262
262
 
263
263
  A run with no page in scope is not a failure: the report step says so and the job carries on.
264
264
 
265
+ ## What the artifact looks like when you open it
266
+
267
+ `audits/` used to hold JSON, SARIF and Markdown: everything a machine needs and nothing that
268
+ opens. `html: 'true'` and `evidence: 'true'` are on by default, so it now has a front door.
269
+
270
+ ```
271
+ audits/
272
+ ├── index.html ← the entry point: rate, synthesis, page scoreboard, links
273
+ ├── ultra11y-<std>-<date>.html ← the whole audit in ONE file, printable to PDF
274
+ ├── <std>-<date>.md ← the Markdown report, illustrated by the same crops
275
+ ├── audit-latest.json ← the machine-readable audit (unchanged)
276
+ ├── assets/
277
+ │ ├── <page-id>.png ← the page screenshot
278
+ │ └── <page-id>/<hash>.png ← one annotated crop per distinct defect on that page
279
+ └── pages/
280
+ ├── index.md + page-<id>.md ← the per-page dossiers (unchanged)
281
+ └── index.html + page-<id>.html ← the same, navigable
282
+ ```
283
+
284
+ Two conventions share `assets/` and that is deliberate: `<page-id>.png` is the whole page,
285
+ `<page-id>/` holds the crops OF that page. Renaming either would break the Markdown sheets
286
+ already published by earlier versions.
287
+
288
+ **One artifact, not two.** The HTML is written into the same `audits/` the Markdown already
289
+ travels in, and the upload now fires when EITHER producer ran. A separate artifact would mean
290
+ a second name to keep unique, a second 409 to hit, and a reviewer choosing between two
291
+ downloads.
292
+
293
+ **Everything is self-contained.** No script (an artifact viewer will not run one), no external
294
+ stylesheet or font, and no `src`/`href` that leaves `audits/` — the artifact is read after
295
+ being unzipped somewhere else, so a `../..` climb is a broken image for every reader. `ci.yml`
296
+ asserts this on a real `download-artifact` round trip, because the HTML step itself degrades
297
+ to a `::warning::` and cannot catch its own regression.
298
+
299
+ ### The crops
300
+
301
+ An occurrence gains a sub-bullet showing the element, ringed:
302
+
303
+ ```md
304
+ - [ ] `.ultra11y/pages/accueil/dom.html:412` (`div.card`) — <img> without an alt attribute…
305
+ - ![Cropped capture of the img element on the accueil page, outlined](./assets/accueil/c90959b13aa2.png)
306
+ ```
307
+
308
+ **Every document that shows a non-conformity shows its crop**: the Markdown conformance
309
+ report, the per-page sheets, and the single-file composite. They all reference one set of
310
+ files in `assets/`; only the composite pays the base64 tax, because only it has to travel
311
+ alone. `report --evidence` therefore works with or without `--html` — the images are not an
312
+ HTML-only tier. `prd`, `gh` and `jira` deliberately stay text: their output is read on
313
+ github.com, where a path relative to `audits/` resolves to nothing, and a broken image in an
314
+ issue is worse than no image.
315
+
316
+ Crops are derived AT RENDER TIME from the page snapshot, never stamped on the finding: a
317
+ rectangle in pixels is a property of the image, and a stamped box goes silently wrong the
318
+ moment the audit is re-rendered against a different capture. So `evidence` needs snapshots —
319
+ `snapshot: 'true'` (the default), the E2E plugins, or `ultra11y dev`. Audited from source
320
+ alone, the tier is simply inactive and says nothing: a source finding never had pixels, so it
321
+ is not an occurrence that failed to be illustrated.
322
+
323
+ The mark is **not monochrome**: a white halo, a red ring and corner brackets whose *shape*
324
+ says "here". A tool that reports 1.4.1 failures cannot ship a deliverable that commits one.
325
+
326
+ When a crop cannot be drawn, the report says so per page and per criterion, with the reason —
327
+ `no-screenshot`, `below-the-fold`, `unknown-scale`, and nine more. **An occurrence without a
328
+ picture must never read as an occurrence without a defect.**
329
+
330
+ Two of those reasons look alike and are not:
331
+
332
+ | Reason | What it means | Costs the reader |
333
+ |---|---|---|
334
+ | `deduplicated` | the same defect on the same element, already shown by another occurrence's picture | nothing |
335
+ | `capped` | a **distinct** defect left undrawn because a limit ran out | a picture — raise `evidence-max` |
336
+
337
+ `evidence-max` (200 by default) is the run-wide fuse and the only one that is a setting: it
338
+ guards the upload's size. The per-rule (6) and per-page (12) limits are fixed, because they
339
+ are what makes one picture stand for a repeated defect — one design-system failure multiplied
340
+ by 38 routes is one defect, not 472 pictures.
341
+
342
+ ### Printing it
343
+
344
+ The single file is the deliverable an auditor hands to a client: open
345
+ `ultra11y-<std>-<date>.html`, print, *Save as PDF*. The print sheet keeps each criterion and
346
+ its evidence on one sheet rather than splitting them across a page break.
347
+
348
+ Its images travel inside it as `data:` URIs, and base64 costs a third more than the bytes it
349
+ encodes — so there is a budget (the `inline-budget` input, `--inline-budget` on the CLI, 12 MB
350
+ by default) and a ladder. Over budget, page screenshots go first, then all but one crop per
351
+ criterion, then the images entirely. Every rung is written **into the document** and onto
352
+ stderr, and the other documents still reference every crop as a file. Images degrade; the
353
+ non-conformities
354
+ never do.
355
+
356
+ ### Publishing it as a page (optional, and it costs something)
357
+
358
+ An artifact needs a download and a login. If the report should be a URL, GitHub Pages will
359
+ serve `audits/` as-is — it is already self-contained:
360
+
361
+ ```yaml
362
+ - uses: actions/upload-pages-artifact@v3
363
+ with: { path: audits }
364
+ - uses: actions/deploy-pages@v4
365
+ ```
366
+
367
+ Know the trade before you take it: **a Pages site is public**, and this one names your
368
+ non-conformities, your file paths and your routes, with screenshots. On a private repository
369
+ that is a disclosure, not a convenience. It also needs `pages: write` and `id-token: write`,
370
+ and it overwrites the previous deployment — there is no per-run history the way artifacts have
371
+ one. Left alone, the artifact is the safer default, which is why this is a recipe here and not
372
+ an input on the action.
373
+
265
374
  ## The action is executed in ITS OWN CI, not just parsed
266
375
 
267
376
  `tests/action.test.ts` reads `action.yml` and gates its shape. That cannot prove the thing
@@ -288,6 +288,82 @@ both a CSSOM measurement and a screenshot measurement, and presenting those as o
288
288
  be a lie. It is off everywhere else: the backlog, `prd --split criterion` and every tracker issue
289
289
  body are byte-identical to what they were.
290
290
 
291
+ ### Showing the defect, not only citing it (`--evidence`)
292
+
293
+ `selectorHint` is lossy by construction — first class only, `href` truncated — and every
294
+ rendered-tier finding on a client-rendered page is anchored at `dom.html:2`, the `<html>`
295
+ element. What a non-technical reviewer receives is `` `.ultra11y/pages/accueil/dom.html:412`
296
+ (`div.card`) ``, which locates nothing.
297
+
298
+ ```sh
299
+ node scripts/ultra11y.mjs pages --in audits/audit-latest.json --format report \
300
+ --split page --evidence --out audits/pages
301
+ ```
302
+
303
+ Each occurrence gains an inert sub-bullet with an annotated crop of the offending element,
304
+ written to `assets/<page-id>/<hash>.png`:
305
+
306
+ ```md
307
+ - [ ] `.ultra11y/pages/accueil/dom.html:412` (`div.card`) — <img> without an alt attribute…
308
+ - ![Cropped capture of the img element on the accueil page, outlined](./assets/accueil/c90959b13aa2.png)
309
+ ```
310
+
311
+ **Derived at render time, never stamped on the finding.** A rectangle in pixels is a property
312
+ of the image: a box written onto a `Finding` becomes silently wrong the moment the audit is
313
+ re-rendered against another capture. The join goes `sourceStart` → document-order ordinal →
314
+ `boxes.json`, through the same `alignedBoxes` the rendered tier uses — including its refusal
315
+ to align a digest whose tags disagree. `Finding` gains no field.
316
+
317
+ The mark is not monochrome: a white halo, a red ring, corner brackets whose shape says "here",
318
+ and a dashed inner line that survives a monochrome print. This tool reports 1.4.1 failures; its
319
+ own deliverable cannot commit one.
320
+
321
+ **Nothing is cut in silence.** Thirteen refusal reasons, each with its own sentence, counted
322
+ per page and per criterion in the sheet itself:
323
+
324
+ | Reason | What happened |
325
+ |---|---|
326
+ | `no-snapshot` | the capture this finding's own path names is not on this disk |
327
+ | `no-screenshot` | the producer captured no image for that page |
328
+ | `unreadable-image` · `no-boxes` · `truncated` | the capture or the box digest cannot be trusted |
329
+ | `no-offsets` · `unjoinable` | the finding has no byte range, or it resolves to no element |
330
+ | `page-scope` | the finding is about the document, not an element you can frame |
331
+ | `zero-area` · `below-the-fold` | the element is invisible, or outside a viewport-only capture |
332
+ | `unknown-scale` | the device pixel ratio could not be derived — a crop would be off-target |
333
+ | `deduplicated` | the same defect on the same element — another occurrence's picture shows it |
334
+ | `capped` | a **distinct** defect went undrawn: 6 per rule, 12 per page, `--evidence-max` per run |
335
+
336
+ The last two are counted apart on purpose. `deduplicated` costs the reader nothing — the
337
+ defect is on screen, under another occurrence. `capped` is a real gap, and it is the only one
338
+ a setting can close.
339
+
340
+ A finding raised on SOURCE is in neither column: it never had pixels, so it is not an
341
+ occurrence that failed to be illustrated, and a repository audited from source alone gets no
342
+ notice at all rather than one line per finding.
343
+
344
+ An occurrence with no picture must never read as an occurrence with no defect, so the sheet
345
+ says how many were not illustrated and why. So does the combined document, the HTML sheets and
346
+ the composite — every surface that can show a crop can say what it did not show.
347
+
348
+ Without `--evidence` the Markdown is **byte-identical** to what it was. The crop bullet carries
349
+ no `[ ]`, so `verify` builds the same worklist with or without it — indentation alone would not
350
+ be enough, since `AUDITOR_OCCURRENCE` is anchored `^\s*-\s\[ \]` and tolerates leading space.
351
+
352
+ ### The same, as a page (`--html`)
353
+
354
+ `--html` writes `index.html` and one `page-<id>.html` beside the Markdown they mirror.
355
+ Self-contained: no script, no external asset, nothing pointing outside the output directory.
356
+
357
+ **The printable single file comes from `report --html`, not from here.** One composite per
358
+ artifact: emitting one from both commands would put a second copy of every inlined crop into
359
+ the same upload, so `pages --html` deliberately writes none.
360
+
361
+ ```sh
362
+ node scripts/ultra11y.mjs report --in audits/audit-latest.json --html --evidence --out audits
363
+ ```
364
+
365
+ See `references/ci.md` for the artifact layout and the inline-size budget.
366
+
291
367
  ## Holding an external audit against the grid
292
368
 
293
369
  A human auditor's verdict is evidence, not a measurement this engine can redo. `import` reads one