ultra11y 5.5.0 → 5.5.2

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.
@@ -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": "5.5.0",
13
+ "version": "5.5.2",
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": "5.5.0",
4
+ "version": "5.5.2",
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": "5.5.0",
4
+ "version": "5.5.2",
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 = "5.5.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.5.2";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.5.0",
3
+ "version": "5.5.2",
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",
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.5.0";
9
+ var VERSION = "5.5.2";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -5456,8 +5456,11 @@ function snippet(doc, el, max = 120) {
5456
5456
  const lineStart = doc.lineStarts[el.line - 1] ?? 0;
5457
5457
  let end = doc.source.indexOf("\n", lineStart);
5458
5458
  if (end === -1) end = doc.source.length;
5459
- const raw = doc.source.slice(lineStart, end).trim();
5460
- return raw.length > max ? raw.slice(0, max - 1) + "\u2026" : raw;
5459
+ const from = Math.min(Math.max(el.start, lineStart), end);
5460
+ const tagEnd = doc.source.indexOf(">", from);
5461
+ const to = Math.min(from + max, Math.max(Math.min(end, from + max), tagEnd === -1 ? end : tagEnd + 1));
5462
+ const raw = doc.source.slice(from, Math.min(to, doc.source.length)).replace(/\s+/g, " ").trim();
5463
+ return raw.length > max ? `${raw.slice(0, max - 1)}\u2026` : raw;
5461
5464
  }
5462
5465
 
5463
5466
  // src/parse/jsx.ts
@@ -55755,6 +55758,13 @@ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
55755
55758
  import { join as join35, resolve as resolve10 } from "path";
55756
55759
 
55757
55760
  // src/adjudicate-subjects.ts
55761
+ var VOLATILE = [
55762
+ [/\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g, "<date>"],
55763
+ [/\b\d{4}-\d{2}-\d{2}\b/g, "<date>"],
55764
+ [/\b\d{1,2}:\d{2}(?::\d{2})?\b/g, "<time>"],
55765
+ [/\b\d{6,}\b/g, "<num>"]
55766
+ ];
55767
+ var stable = (s) => VOLATILE.reduce((acc, [re, to]) => acc.replace(re, to), s);
55758
55768
  var selectorFor = (el) => {
55759
55769
  const id = el.attribs.id ? `#${el.attribs.id}` : "";
55760
55770
  const cls = el.attribs.class ? `.${el.attribs.class.trim().split(/\s+/)[0]}` : "";
@@ -55763,14 +55773,14 @@ var selectorFor = (el) => {
55763
55773
  function h(doc, el, note, cls) {
55764
55774
  return {
55765
55775
  ev: { file: doc.file, line: el.line, selector: selectorFor(el), snippet: snippet(doc, el, 160), note },
55766
- cls: cls ?? `${el.tag}|${note}`,
55776
+ cls: stable(cls ?? `${el.tag}|${note}`),
55767
55777
  at: el.start
55768
55778
  };
55769
55779
  }
55770
55780
  function hAt(doc, line, selector, note, cls) {
55771
55781
  return {
55772
55782
  ev: { file: doc.file, line, selector, snippet: (doc.source.split("\n")[line - 1] ?? "").trim().slice(0, 160), note },
55773
- cls,
55783
+ cls: stable(cls),
55774
55784
  at: line
55775
55785
  };
55776
55786
  }
@@ -57171,7 +57181,7 @@ function adjudicationText(sc, lang) {
57171
57181
  }
57172
57182
 
57173
57183
  // src/adjudicate.ts
57174
- var ADJUDICATE_MAX_EVIDENCE_CLASSES = 600;
57184
+ var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
57175
57185
  var ALSO_AT_MAX = 8;
57176
57186
  function adjudicationContract() {
57177
57187
  return {
@@ -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: 5.5.0
8
+ version: 5.5.2
9
9
  ---
10
10
 
11
11
  # review-a11y — accessibility review of changed code
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.5.0";
9
+ var VERSION = "5.5.2";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -5456,8 +5456,11 @@ function snippet(doc, el, max = 120) {
5456
5456
  const lineStart = doc.lineStarts[el.line - 1] ?? 0;
5457
5457
  let end = doc.source.indexOf("\n", lineStart);
5458
5458
  if (end === -1) end = doc.source.length;
5459
- const raw = doc.source.slice(lineStart, end).trim();
5460
- return raw.length > max ? raw.slice(0, max - 1) + "\u2026" : raw;
5459
+ const from = Math.min(Math.max(el.start, lineStart), end);
5460
+ const tagEnd = doc.source.indexOf(">", from);
5461
+ const to = Math.min(from + max, Math.max(Math.min(end, from + max), tagEnd === -1 ? end : tagEnd + 1));
5462
+ const raw = doc.source.slice(from, Math.min(to, doc.source.length)).replace(/\s+/g, " ").trim();
5463
+ return raw.length > max ? `${raw.slice(0, max - 1)}\u2026` : raw;
5461
5464
  }
5462
5465
 
5463
5466
  // src/parse/jsx.ts
@@ -55755,6 +55758,13 @@ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
55755
55758
  import { join as join35, resolve as resolve10 } from "path";
55756
55759
 
55757
55760
  // src/adjudicate-subjects.ts
55761
+ var VOLATILE = [
55762
+ [/\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g, "<date>"],
55763
+ [/\b\d{4}-\d{2}-\d{2}\b/g, "<date>"],
55764
+ [/\b\d{1,2}:\d{2}(?::\d{2})?\b/g, "<time>"],
55765
+ [/\b\d{6,}\b/g, "<num>"]
55766
+ ];
55767
+ var stable = (s) => VOLATILE.reduce((acc, [re, to]) => acc.replace(re, to), s);
55758
55768
  var selectorFor = (el) => {
55759
55769
  const id = el.attribs.id ? `#${el.attribs.id}` : "";
55760
55770
  const cls = el.attribs.class ? `.${el.attribs.class.trim().split(/\s+/)[0]}` : "";
@@ -55763,14 +55773,14 @@ var selectorFor = (el) => {
55763
55773
  function h(doc, el, note, cls) {
55764
55774
  return {
55765
55775
  ev: { file: doc.file, line: el.line, selector: selectorFor(el), snippet: snippet(doc, el, 160), note },
55766
- cls: cls ?? `${el.tag}|${note}`,
55776
+ cls: stable(cls ?? `${el.tag}|${note}`),
55767
55777
  at: el.start
55768
55778
  };
55769
55779
  }
55770
55780
  function hAt(doc, line, selector, note, cls) {
55771
55781
  return {
55772
55782
  ev: { file: doc.file, line, selector, snippet: (doc.source.split("\n")[line - 1] ?? "").trim().slice(0, 160), note },
55773
- cls,
55783
+ cls: stable(cls),
55774
55784
  at: line
55775
55785
  };
55776
55786
  }
@@ -57171,7 +57181,7 @@ function adjudicationText(sc, lang) {
57171
57181
  }
57172
57182
 
57173
57183
  // src/adjudicate.ts
57174
- var ADJUDICATE_MAX_EVIDENCE_CLASSES = 600;
57184
+ var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
57175
57185
  var ALSO_AT_MAX = 8;
57176
57186
  function adjudicationContract() {
57177
57187
  return {
@@ -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.5.0
7
+ version: 5.5.2
8
8
  ---
9
9
 
10
10
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.5.0";
9
+ var VERSION = "5.5.2";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -5456,8 +5456,11 @@ function snippet(doc, el, max = 120) {
5456
5456
  const lineStart = doc.lineStarts[el.line - 1] ?? 0;
5457
5457
  let end = doc.source.indexOf("\n", lineStart);
5458
5458
  if (end === -1) end = doc.source.length;
5459
- const raw = doc.source.slice(lineStart, end).trim();
5460
- return raw.length > max ? raw.slice(0, max - 1) + "\u2026" : raw;
5459
+ const from = Math.min(Math.max(el.start, lineStart), end);
5460
+ const tagEnd = doc.source.indexOf(">", from);
5461
+ const to = Math.min(from + max, Math.max(Math.min(end, from + max), tagEnd === -1 ? end : tagEnd + 1));
5462
+ const raw = doc.source.slice(from, Math.min(to, doc.source.length)).replace(/\s+/g, " ").trim();
5463
+ return raw.length > max ? `${raw.slice(0, max - 1)}\u2026` : raw;
5461
5464
  }
5462
5465
 
5463
5466
  // src/parse/jsx.ts
@@ -55755,6 +55758,13 @@ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
55755
55758
  import { join as join35, resolve as resolve10 } from "path";
55756
55759
 
55757
55760
  // src/adjudicate-subjects.ts
55761
+ var VOLATILE = [
55762
+ [/\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g, "<date>"],
55763
+ [/\b\d{4}-\d{2}-\d{2}\b/g, "<date>"],
55764
+ [/\b\d{1,2}:\d{2}(?::\d{2})?\b/g, "<time>"],
55765
+ [/\b\d{6,}\b/g, "<num>"]
55766
+ ];
55767
+ var stable = (s) => VOLATILE.reduce((acc, [re, to]) => acc.replace(re, to), s);
55758
55768
  var selectorFor = (el) => {
55759
55769
  const id = el.attribs.id ? `#${el.attribs.id}` : "";
55760
55770
  const cls = el.attribs.class ? `.${el.attribs.class.trim().split(/\s+/)[0]}` : "";
@@ -55763,14 +55773,14 @@ var selectorFor = (el) => {
55763
55773
  function h(doc, el, note, cls) {
55764
55774
  return {
55765
55775
  ev: { file: doc.file, line: el.line, selector: selectorFor(el), snippet: snippet(doc, el, 160), note },
55766
- cls: cls ?? `${el.tag}|${note}`,
55776
+ cls: stable(cls ?? `${el.tag}|${note}`),
55767
55777
  at: el.start
55768
55778
  };
55769
55779
  }
55770
55780
  function hAt(doc, line, selector, note, cls) {
55771
55781
  return {
55772
55782
  ev: { file: doc.file, line, selector, snippet: (doc.source.split("\n")[line - 1] ?? "").trim().slice(0, 160), note },
55773
- cls,
55783
+ cls: stable(cls),
55774
55784
  at: line
55775
55785
  };
55776
55786
  }
@@ -57171,7 +57181,7 @@ function adjudicationText(sc, lang) {
57171
57181
  }
57172
57182
 
57173
57183
  // src/adjudicate.ts
57174
- var ADJUDICATE_MAX_EVIDENCE_CLASSES = 600;
57184
+ var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
57175
57185
  var ALSO_AT_MAX = 8;
57176
57186
  function adjudicationContract() {
57177
57187
  return {
@@ -27,6 +27,29 @@ import { type Doc, type El, ancestors, attr, elementsByTag, snippet as elSnippet
27
27
  import { parseInlineStyle } from "./color.js";
28
28
  import type { Evidence } from "./adjudicate.js";
29
29
 
30
+ /** Mask the parts of a text that change between runs without the code changing.
31
+ *
32
+ * Only the class IDENTITY is masked — the note and the snippet keep the real text, so the
33
+ * adjudicator still reads what the page says. What this decides is whether two anchors are
34
+ * "the same thing": a row rendered at 13:04 and the same row rendered at 14:12 are one
35
+ * decision about accessibility, not two.
36
+ *
37
+ * It matters beyond tidiness. The verdict ledger fingerprints the evidence a criterion was
38
+ * ruled against, and a fingerprint that moves every run makes a stored verdict stale on
39
+ * arrival — the criterion returns to « to assess » and the ledger stops paying for itself.
40
+ * Measured on a real capture set: four criteria carried a run timestamp in their evidence.
41
+ *
42
+ * Deliberately narrow. Generated ids that are stable for a given tree (React `useId`) and
43
+ * content hashes that are stable for given source are NOT masked: when those move, the code
44
+ * moved, and a verdict about it SHOULD be re-examined. */
45
+ const VOLATILE: [RegExp, string][] = [
46
+ [/\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g, "<date>"],
47
+ [/\b\d{4}-\d{2}-\d{2}\b/g, "<date>"],
48
+ [/\b\d{1,2}:\d{2}(?::\d{2})?\b/g, "<time>"],
49
+ [/\b\d{6,}\b/g, "<num>"],
50
+ ];
51
+ const stable = (s: string): string => VOLATILE.reduce((acc, [re, to]) => acc.replace(re, to), s);
52
+
30
53
  /** One harvested anchor, plus the content class it belongs to. Two anchors sharing a class
31
54
  * are the same thing rendered twice — a header link on 38 pages, not 38 link problems. */
32
55
  export interface Harvested {
@@ -52,7 +75,7 @@ const selectorFor = (el: El): string => {
52
75
  function h(doc: Doc, el: El, note: string, cls?: string): Harvested {
53
76
  return {
54
77
  ev: { file: doc.file, line: el.line, selector: selectorFor(el), snippet: elSnippet(doc, el, 160), note },
55
- cls: cls ?? `${el.tag}|${note}`,
78
+ cls: stable(cls ?? `${el.tag}|${note}`),
56
79
  at: el.start,
57
80
  };
58
81
  }
@@ -62,7 +85,7 @@ function h(doc: Doc, el: El, note: string, cls?: string): Harvested {
62
85
  function hAt(doc: Doc, line: number, selector: string, note: string, cls: string): Harvested {
63
86
  return {
64
87
  ev: { file: doc.file, line, selector, snippet: (doc.source.split("\n")[line - 1] ?? "").trim().slice(0, 160), note },
65
- cls,
88
+ cls: stable(cls),
66
89
  at: line,
67
90
  };
68
91
  }
package/src/adjudicate.ts CHANGED
@@ -47,15 +47,18 @@ import { guidanceExampleBlock } from "./prd.js";
47
47
  * 887 links over 38 captured pages are 97 distinct (text, href) pairs. When even the class
48
48
  * count exceeds this, `evidenceComplete` goes false and the fold refuses a `C` outright —
49
49
  * an incomplete reading may still find a real failure, it may never clear one. */
50
- export const ADJUDICATE_MAX_EVIDENCE_CLASSES = 600;
50
+ export const ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
51
51
 
52
- // Why 600 and not a rounder, smaller number: it is set by what a real application actually
53
- // contains, measured, not by taste. On a 338-file product with 38 captured pages the largest
54
- // per-criterion populations are 456 classes (what survives when CSS is off), 404 (reading
55
- // order), 360 (structure) and 334 (the heading outline) — and those ARE the populations, not
56
- // samples of them. A cap below them would not bound anything worth bounding; it would simply
57
- // refuse every `C` on the criteria that matter most, and refusing to conclude is only honest
58
- // when something really was not looked at.
52
+ // The number is set by what a real application actually contains, measured, and then given
53
+ // room to grow. On a 338-file product with 38 captured pages the largest per-criterion
54
+ // populations run to 592 classes (what survives when CSS is off), 531 (reading order) and 487
55
+ // (structure) — and those ARE the populations, not samples of them.
56
+ //
57
+ // The headroom is the point. A cap sitting just above today's largest criterion is a gate
58
+ // that flips the day someone adds a heading: the criterion becomes unclearable for reasons of
59
+ // VOLUME rather than of uncertainty, which is the wrong reason to refuse a verdict and an
60
+ // especially confusing one, since nothing about the code got worse. Refusing to conclude is
61
+ // honest only when something really was not looked at.
59
62
 
60
63
  /** Sibling anchors listed per class. Bounded for readability; `occurrences` carries the true
61
64
  * count, and the citation gate accepts any anchor of the class, listed or not. */
package/src/parse/html.ts CHANGED
@@ -261,10 +261,32 @@ export function allIds(doc: Doc): { id: string; el: El }[] {
261
261
  }
262
262
 
263
263
  /** The source line(s) the element starts on, trimmed and truncated. */
264
+ /** The source text that SHOWS this element, bounded to `max`.
265
+ *
266
+ * Anchored at the element's own offset, not at the start of its line. On hand-written source
267
+ * the two are the same thing once indentation is trimmed — but a serialized DOM capture is a
268
+ * SINGLE LINE, so anchoring on the line handed back the document head for every element in
269
+ * the page, identical for all of them. Measured on a real capture: every one of 128 form
270
+ * controls carried `<html data-fr-scheme="system" lang="fr">…` as its snippet, which tells a
271
+ * reader nothing and gives the grounding check nothing to match on.
272
+ *
273
+ * The line still bounds the slice, so a multi-line element shows its opening tag rather than
274
+ * its whole subtree. */
264
275
  export function snippet(doc: Doc, el: El, max = 120): string {
265
276
  const lineStart = doc.lineStarts[el.line - 1] ?? 0;
266
277
  let end = doc.source.indexOf("\n", lineStart);
267
278
  if (end === -1) end = doc.source.length;
268
- const raw = doc.source.slice(lineStart, end).trim();
269
- return raw.length > max ? raw.slice(0, max - 1) + "…" : raw;
279
+ const from = Math.min(Math.max(el.start, lineStart), end);
280
+ // A JSX element usually opens on one line and carries its attributes on the next ones, so
281
+ // stopping at the line break showed "<a" and nothing else. Read to the end of the OPENING
282
+ // TAG instead — that is what identifies the element — and collapse the whitespace, which
283
+ // grounding normalises on both sides anyway (src/grounding.ts).
284
+ //
285
+ // The opening tag and not the element: a snippet longer than `max` gets an ellipsis, and an
286
+ // ellipsis is not in the source, so `includes` stops matching and grounding refuses a
287
+ // citation that was perfectly real. Keeping the slice short keeps it verbatim.
288
+ const tagEnd = doc.source.indexOf(">", from);
289
+ const to = Math.min(from + max, Math.max(Math.min(end, from + max), tagEnd === -1 ? end : tagEnd + 1));
290
+ const raw = doc.source.slice(from, Math.min(to, doc.source.length)).replace(/\s+/g, " ").trim();
291
+ return raw.length > max ? `${raw.slice(0, max - 1)}…` : raw;
270
292
  }
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 = "5.5.0";
3
+ export const VERSION = "5.5.2";
4
4
  export const SCHEMA_VERSION = 2;
5
5
 
6
6
  // Lang = the UI FRAME's language — the `L` localization tables each module (report,