ultra11y 5.35.0 → 5.35.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.35.0",
13
+ "version": "5.35.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.35.0",
4
+ "version": "5.35.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.35.0",
4
+ "version": "5.35.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.35.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.35.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.35.0",
3
+ "version": "5.35.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 join53, 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.35.0";
9
+ var VERSION = "5.35.2";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -44713,10 +44713,18 @@ var PRESENTATIONAL_MARKUP_ATTRS = /* @__PURE__ */ new Set([
44713
44713
  ]);
44714
44714
  var DOWNLOAD_HREF = /\.(pdf|docx?|xlsx?)(?:[?#]|$)/i;
44715
44715
  var STATUS_CLASS = /(error|status|message|alert|notif|toast|feedback|live)/i;
44716
+ var TEMPORAL_MEDIA_VALUE = /(?:^|[/.?=&_-])(?:audio|video|player|podcast|mp[34]|webm|ogg|ogv|wav|m4a|mov|avi|mpeg)(?:$|[/.?=&_-])/i;
44716
44717
  var ROUTER_IMPORT = /['"](?:react-router(?:-dom)?|next\/(?:router|navigation)|vue-router|@remix-run\/[\w-]+|@tanstack\/[\w-]*router|@sveltejs\/kit|\$app\/(?:navigation|stores))['"]/;
44717
44718
  var FOREIGN_LEXICON = /\b(?:the|and|with|your|about|please|download|settings|welcome|dashboard|overview|read more|learn more|sign in|sign up|log in|get started|coming soon|powered by)\b/i;
44718
44719
  var CONTROL_TAGS = ["input", "select", "textarea"];
44719
44720
  var INTERACTIVE_TAGS = ["a", "button", "input", "select", "textarea", "summary", "details", "label"];
44721
+ function embeddedMediaKind(el) {
44722
+ const type = (attr(el, "type") ?? "").toLowerCase();
44723
+ const resource = `${attr(el, "src") ?? ""} ${attr(el, "data") ?? ""}`.toLowerCase();
44724
+ if (/^(?:audio|video)\//.test(type) || TEMPORAL_MEDIA_VALUE.test(`${type} ${resource}`)) return "temporal";
44725
+ if (/^image\//.test(type) || /\.(?:svg|png|jpe?g|webp|avif)(?:[?#]|$)/i.test(resource)) return "non-temporal";
44726
+ return "unknown";
44727
+ }
44720
44728
  function labelFor(doc, el) {
44721
44729
  const id = attr(el, "id");
44722
44730
  const lbl = id ? elementsByTag(doc, "label").find((l) => attr(l, "for") === id) : void 0;
@@ -45099,6 +45107,66 @@ var SUBJECTS = {
45099
45107
  (e) => h(d, e, `<${e.tag} autoplay=${attr(e, "autoplay") !== void 0} controls=${attr(e, "controls") !== void 0}>`, `media|${e.tag}`)
45100
45108
  )
45101
45109
  ]),
45110
+ // RGAA 4.11 — temporal players and the code that controls their consultation. This must
45111
+ // not inherit WCAG 2.1.1/2.1.2's page-wide union of pointer handlers and focusables: an
45112
+ // unrelated clickable card says nothing about whether an <audio> can be paused.
45113
+ temporalMediaControls: (docs) => docs.flatMap((d) => [
45114
+ ...d.elements.filter((e) => e.tag === "audio" || e.tag === "video" || ["object", "embed"].includes(e.tag) && embeddedMediaKind(e) !== "non-temporal").map(
45115
+ (e) => h(
45116
+ d,
45117
+ e,
45118
+ `<${e.tag}> temporal media \u2014 controls=${attr(e, "controls") !== void 0}, autoplay=${attr(e, "autoplay") !== void 0}; can play/pause/stop and sound be operated by keyboard and pointer?`,
45119
+ `temporal-media|${e.tag}|${attr(e, "src") ?? attr(e, "data") ?? ""}|controls=${attr(e, "controls") !== void 0}`
45120
+ )
45121
+ ),
45122
+ ...linesOf(d, /\.(?:play|pause)\s*\(|\b(?:currentTime|muted|volume|playbackRate)\s*=|\bHTMLMediaElement\b/).map(
45123
+ (l) => hAt(d, l.line, "media-control", `custom temporal-media control: ${l.text}`, `temporal-control|${l.text}`)
45124
+ )
45125
+ ]),
45126
+ // RGAA 4.12 — non-temporal plug-in/SVG/canvas media. Static instances are included too:
45127
+ // the adjudicator must first establish whether they expose consultation controls before it
45128
+ // can legitimately answer NA. Temporal players and generic event handlers stay out.
45129
+ nonTemporalMediaControls: (docs) => docs.flatMap((d) => [
45130
+ ...d.elements.filter((e) => e.tag === "svg" || e.tag === "canvas" || ["object", "embed"].includes(e.tag) && embeddedMediaKind(e) !== "temporal").map(
45131
+ (e) => h(
45132
+ d,
45133
+ e,
45134
+ `<${e.tag}> non-temporal media \u2014 inspect any pan/zoom/navigation/control function for keyboard and pointer operation`,
45135
+ `non-temporal-media|${e.tag}|${attr(e, "src") ?? attr(e, "data") ?? attr(e, "id") ?? ""}`
45136
+ )
45137
+ ),
45138
+ ...linesOf(d, /\b(?:svgPanZoom|panzoom|zoomIn|zoomOut|getContext)\b/i).map(
45139
+ (l) => hAt(d, l.line, "media-control", `non-temporal media/control code: ${l.text}`, `non-temporal-control|${l.text}`)
45140
+ )
45141
+ ]),
45142
+ // WCAG 2.3.1 / RGAA 13.7 — only things capable of a luminance flash. The old `motion`
45143
+ // subject mixed autoplay audio and <marquee> into this criterion while omitting ordinary
45144
+ // images, canvas, scripts and stylesheet animation rules. That gave the adjudicator lots of
45145
+ // evidence and none of the evidence the three-flash test actually asks for.
45146
+ flashes: (docs) => docs.flatMap((d) => [
45147
+ ...d.elements.filter((e) => ["video", "img", "svg", "canvas", "embed", "object", "blink"].includes(e.tag)).map(
45148
+ (e) => h(
45149
+ d,
45150
+ e,
45151
+ `<${e.tag}> visual/animated content \u2014 does it flash, and if so is it at most 3 flashes/s or below the area threshold?`,
45152
+ `flash-media|${e.tag}|${attr(e, "src") ?? attr(e, "data") ?? attr(e, "id") ?? ""}`
45153
+ )
45154
+ ),
45155
+ ...elementsByTag(d, "script").map(
45156
+ (e) => h(d, e, `<script> \u2014 inspect for luminance/colour states toggled rapidly`, `flash-script|${attr(e, "src") ?? t(e, 80)}`)
45157
+ ),
45158
+ ...d.elements.filter((e) => /animation|transition/i.test(attr(e, "style") ?? "")).map((e) => h(d, e, `inline visual animation: ${(attr(e, "style") ?? "").slice(0, 100)}`, `flash-inline|${attr(e, "style") ?? ""}`)),
45159
+ ...(d.signals?.css?.rules ?? []).filter((r) => r.decls.animation !== void 0 || r.decls.animationName !== void 0).map(
45160
+ (r) => hAt(
45161
+ d,
45162
+ 1,
45163
+ `css:${r.selector}`,
45164
+ `stylesheet animation: ${r.selector.slice(0, 60)} { ${declsText(r.decls)} } \u2014 inspect keyframes for rapid opposing luminance changes`,
45165
+ `flash-css|${r.selector}|${declsText(r.decls)}`
45166
+ )
45167
+ ),
45168
+ ...linesOf(d, /\b(?:flash|flashing|strobe|setInterval|requestAnimationFrame|classList\.(?:add|remove|replace|toggle))\b|animation(?:-name)?\s*:/i).filter((l) => !/^\s*(?:<!--|\/\/|\/\*)/.test(l.text)).map((l) => hAt(d, l.line, "flash-code", `possible scripted/CSS flash mechanism: ${l.text}`, `flash-code|${l.text}`))
45169
+ ]),
45102
45170
  // A change of context initiated by a script. RGAA 7.4 / WCAG 3.2.1-3.2.2.
45103
45171
  //
45104
45172
  // THE SUBJECT IS THE SCRIPT, and getting that wrong cost a release. This used to be a list of
@@ -45358,7 +45426,7 @@ var SC_SUBJECTS = {
45358
45426
  "2.1.4": ["shortcuts"],
45359
45427
  "2.2.1": ["timers"],
45360
45428
  "2.2.2": ["motion", "timers"],
45361
- "2.3.1": ["motion"],
45429
+ "2.3.1": ["flashes"],
45362
45430
  "2.4.1": ["landmarks"],
45363
45431
  "2.4.3": ["focusOrder"],
45364
45432
  "2.4.4": ["links"],
@@ -45451,6 +45519,10 @@ var PACK_SUBJECTS = {
45451
45519
  // WCAG 1.4.2, which is `static` and therefore has no subject of its own — but the pack
45452
45520
  // flags 4.10 `judgment`, so judgmentGuard reopens it and it would arrive with nothing.
45453
45521
  "4.10": ["motion"],
45522
+ // 4.11/4.12 map to the same keyboard SCs but ask about disjoint media populations.
45523
+ // Inheriting the union handed both criteria every pointer handler and focusable in scope.
45524
+ "4.11": ["temporalMediaControls"],
45525
+ "4.12": ["nonTemporalMediaControls"],
45454
45526
  // Theme 3 — colour. The question is about what colour alone conveys, not the outline.
45455
45527
  "3.1": ["colourPairs", "links"],
45456
45528
  "3.2": ["colourPairs"],
@@ -45487,7 +45559,7 @@ var PACK_SUBJECTS = {
45487
45559
  "13.2": ["newWindow"],
45488
45560
  "13.3": ["downloadDocs"],
45489
45561
  "13.4": ["downloadDocs"],
45490
- "13.7": ["motion"],
45562
+ "13.7": ["flashes"],
45491
45563
  "13.8": ["motion", "timers"],
45492
45564
  "13.9": ["readingOrder"],
45493
45565
  "13.10": ["pointerHandlers"],
@@ -49414,7 +49486,7 @@ var SUBJECT_MATTER = {
49414
49486
  "2.1.4": hasCharShortcut,
49415
49487
  "2.2.1": hasTimeLimit,
49416
49488
  "2.2.2": hasMovingContent,
49417
- "2.3.1": hasMovingContent,
49489
+ "2.3.1": hasFlashCandidate,
49418
49490
  "2.5.1": hasGesture,
49419
49491
  "2.5.2": hasDownEventAction,
49420
49492
  "2.5.7": hasDrag
@@ -49459,6 +49531,14 @@ function hasMovingContent(d) {
49459
49531
  if ((d.signals?.css?.rules ?? []).some((r) => r.decls.animation !== void 0 || r.decls.animationName !== void 0)) return true;
49460
49532
  return /\b(?:carousel|autoplay|requestAnimationFrame|animation-iteration-count|\.gif["']|\bswiper\b)/i.test(d.source);
49461
49533
  }
49534
+ function hasFlashCandidate(d) {
49535
+ if (has(d, "video", "img", "svg", "canvas", "embed", "object", "blink", "script")) return true;
49536
+ if (d.elements.some((e) => /animation|transition/i.test(e.attribs.style ?? ""))) return true;
49537
+ if ((d.signals?.css?.rules ?? []).some((r) => r.decls.animation !== void 0 || r.decls.animationName !== void 0)) return true;
49538
+ return /\b(?:flash|flashing|strobe|blink|setInterval|requestAnimationFrame|classList\.(?:add|remove|replace|toggle))\b|animation(?:-name)?\s*:/i.test(
49539
+ d.source
49540
+ );
49541
+ }
49462
49542
  function hasFormControl(d) {
49463
49543
  if (has(d, "input", "select", "textarea", "form", "fieldset", "output", "datalist")) return true;
49464
49544
  return d.elements.some((e) => {
@@ -54611,8 +54691,24 @@ function derivePackResults(audit2, packKey, pageId) {
54611
54691
  }
54612
54692
  function findingsForStandard(audit2, standard) {
54613
54693
  if (standard === CORE_KEY) return audit2.findings;
54614
- const mine = (audit2.packFindings ?? []).filter((f) => f.ruleId.startsWith(`pack:${standard}:`));
54615
- return mine.length ? [...audit2.findings, ...mine] : audit2.findings;
54694
+ const eligible = new Set(
54695
+ [
54696
+ ...audit2.findings,
54697
+ ...(audit2.packFindings ?? []).filter((finding) => finding.ruleId.startsWith(`pack:${standard}:`)),
54698
+ ...audit2.packAdjudication?.standard === standard ? audit2.packAdjudication.criteria.flatMap((criterion) => criterion.findings) : []
54699
+ ].map(findingId)
54700
+ );
54701
+ const seen = /* @__PURE__ */ new Set();
54702
+ const out2 = [];
54703
+ for (const criterion of derivePackResults(audit2, standard)) {
54704
+ for (const finding of criterion.findings) {
54705
+ const id = findingId(finding);
54706
+ if (!eligible.has(id) || seen.has(id)) continue;
54707
+ seen.add(id);
54708
+ out2.push(finding);
54709
+ }
54710
+ }
54711
+ return out2;
54616
54712
  }
54617
54713
 
54618
54714
  // src/standards/vocabulary.ts
@@ -59499,6 +59595,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
59499
59595
  };
59500
59596
  const uncovered = (criteriaId) => notFolded.add(criteriaId);
59501
59597
  canonicalizeAdjudication(adj);
59598
+ const itemCounts = /* @__PURE__ */ new Map();
59599
+ for (const item of adj.items) itemCounts.set(item.criteriaId, (itemCounts.get(item.criteriaId) ?? 0) + 1);
59600
+ for (const [criteriaId, count] of itemCounts) {
59601
+ if (count > 1)
59602
+ blame(criteriaId, `criterion ${criteriaId}: duplicate criterion id appears ${count} times in the adjudication \u2014 no implicit winner is accepted`);
59603
+ }
59502
59604
  const byId2 = new Map(adj.items.map((it) => [it.criteriaId, it]));
59503
59605
  const packMode = !isCore(adj.standard);
59504
59606
  const open = /* @__PURE__ */ new Set();
@@ -60415,6 +60517,20 @@ var BATCH_SIZE = 8;
60415
60517
  var CONCURRENCY = 4;
60416
60518
  var MAX_ATTEMPTS = 4;
60417
60519
  var MAX_TOKENS = 16e3;
60520
+ function batchWorklist(items, render2, size = BATCH_SIZE) {
60521
+ if (!Number.isInteger(size) || size < 1) throw new Error(`batch size must be a positive integer (got ${size})`);
60522
+ const ids = items.map((item) => item.criteriaId);
60523
+ if (new Set(ids).size !== ids.length) {
60524
+ const duplicate = [...new Set(ids.filter((id, index) => ids.indexOf(id) !== index))];
60525
+ throw new Error(`worklist contains duplicate criterion id(s): ${duplicate.join(", ")}`);
60526
+ }
60527
+ const batches = [];
60528
+ for (let i2 = 0; i2 < items.length; i2 += size) {
60529
+ const slice = items.slice(i2, i2 + size);
60530
+ batches.push({ items: slice, prompt: render2(slice) });
60531
+ }
60532
+ return batches;
60533
+ }
60418
60534
  function apiKeyFromEnv() {
60419
60535
  const k = process.env.ANTHROPIC_API_KEY?.trim();
60420
60536
  return k || void 0;
@@ -60536,7 +60652,7 @@ function verdictsOf(res) {
60536
60652
  if (!Array.isArray(input?.verdicts)) throw new Error("ultra11y judge: the model's tool call carried no verdicts array.");
60537
60653
  return input.verdicts;
60538
60654
  }
60539
- async function judgeBatch(items, prompt, opts) {
60655
+ async function judgeBatch(_items, prompt, opts) {
60540
60656
  if (!opts.apiKey) throw new Error("ultra11y judge: the Messages backend needs an API key (ANTHROPIC_API_KEY or --api-key).");
60541
60657
  const res = await callOnce(
60542
60658
  {
@@ -60549,8 +60665,22 @@ async function judgeBatch(items, prompt, opts) {
60549
60665
  },
60550
60666
  opts
60551
60667
  );
60552
- const known = new Set(items.map((i2) => i2.criteriaId));
60553
- return verdictsOf(res).filter((v) => known.has(v.criteriaId));
60668
+ return verdictsOf(res);
60669
+ }
60670
+ function validateBatchVerdicts(items, landed) {
60671
+ const expected = new Set(items.map((item) => item.criteriaId));
60672
+ const counts = /* @__PURE__ */ new Map();
60673
+ for (const verdict of landed) counts.set(verdict.criteriaId, (counts.get(verdict.criteriaId) ?? 0) + 1);
60674
+ const duplicate = new Set([...counts].filter(([, count]) => count > 1).map(([id]) => id));
60675
+ const unknown = [...new Set(landed.map((v) => v.criteriaId).filter((id) => !expected.has(id)))];
60676
+ const accepted = landed.filter((verdict) => expected.has(verdict.criteriaId) && !duplicate.has(verdict.criteriaId));
60677
+ const acceptedIds = new Set(accepted.map((verdict) => verdict.criteriaId));
60678
+ const missing = [...expected].filter((id) => !acceptedIds.has(id) && !duplicate.has(id));
60679
+ const failures = [];
60680
+ if (duplicate.size) failures.push(`duplicate criterion id(s) in batch response: ${[...duplicate].join(", ")}`);
60681
+ if (unknown.length) failures.push(`unknown criterion id(s) in batch response: ${unknown.join(", ")}`);
60682
+ if (missing.length) failures.push(`missing criterion id(s) in batch response: ${missing.join(", ")}`);
60683
+ return { accepted, failures };
60554
60684
  }
60555
60685
  async function judgeAll(batches, opts) {
60556
60686
  const verdicts = [];
@@ -60564,8 +60694,10 @@ async function judgeAll(batches, opts) {
60564
60694
  for (let b = queue.shift(); b !== void 0; b = queue.shift()) {
60565
60695
  try {
60566
60696
  const landed = await backend(b.items, b.prompt, opts);
60567
- verdicts.push(...landed);
60568
- if (landed.length) opts.onVerdicts?.(landed);
60697
+ const checked = validateBatchVerdicts(b.items, landed);
60698
+ failures.push(...checked.failures);
60699
+ verdicts.push(...checked.accepted);
60700
+ if (checked.accepted.length) opts.onVerdicts?.(checked.accepted);
60569
60701
  } catch (e) {
60570
60702
  failures.push(e instanceof Error ? e.message : String(e));
60571
60703
  }
@@ -60576,7 +60708,9 @@ async function judgeAll(batches, opts) {
60576
60708
  return { verdicts, failures };
60577
60709
  }
60578
60710
  function applyRawVerdicts(items, verdicts) {
60579
- const byId2 = new Map(verdicts.map((v) => [v.criteriaId, v]));
60711
+ const counts = /* @__PURE__ */ new Map();
60712
+ for (const verdict of verdicts) counts.set(verdict.criteriaId, (counts.get(verdict.criteriaId) ?? 0) + 1);
60713
+ const byId2 = new Map(verdicts.filter((v) => counts.get(v.criteriaId) === 1).map((v) => [v.criteriaId, v]));
60580
60714
  let filled = 0;
60581
60715
  for (const item of items) {
60582
60716
  const v = byId2.get(item.criteriaId);
@@ -64249,7 +64383,9 @@ var S = {
64249
64383
  fr: {
64250
64384
  title: "Audit d'accessibilit\xE9 ultra11y",
64251
64385
  files: "fichiers",
64252
- rate: "r\xE9ussite automatique",
64386
+ coverage: (decided, total) => `${decided}/${total} crit\xE8res tranch\xE9s dans ce run`,
64387
+ remaining: (n) => `${n} \xE0 compl\xE9ter par scan ou adjudication`,
64388
+ provenance: { engine: "moteur", scan: "scan", agent: "agent" },
64253
64389
  none: "\u2705 Aucune non-conformit\xE9 d\xE9tect\xE9e par le moteur statique.",
64254
64390
  findings: "Non-conformit\xE9s",
64255
64391
  severity: "S\xE9v\xE9rit\xE9",
@@ -64317,7 +64453,9 @@ var S = {
64317
64453
  en: {
64318
64454
  title: "ultra11y accessibility audit",
64319
64455
  files: "files",
64320
- rate: "automatic pass rate",
64456
+ coverage: (decided, total) => `${decided}/${total} criteria decided in this run`,
64457
+ remaining: (n) => `${n} still to complete by scan or adjudication`,
64458
+ provenance: { engine: "engine", scan: "scan", agent: "agent" },
64321
64459
  none: "\u2705 No non-conformity detected by the static engine.",
64322
64460
  findings: "Non-conformities",
64323
64461
  severity: "Severity",
@@ -64439,11 +64577,24 @@ function groupByCriterion(groups) {
64439
64577
  }
64440
64578
  return [...byCriterion.values()].map(({ pageSet, ...c2 }) => ({ ...c2, pages: pageSet.size })).sort((a, b) => SEV_ORDER4.indexOf(a.severity) - SEV_ORDER4.indexOf(b.severity) || b.occurrences - a.occurrences || a.criterion.localeCompare(b.criterion));
64441
64579
  }
64442
- function runRate(result, standard, lang) {
64580
+ function runCoverage(result, standard, lang) {
64443
64581
  const groups = isCore(standard) ? reportGroups(result, lang) : packReportGroups(result, loadPack(standard), lang);
64444
64582
  const { decided, total } = reportCoverage(groups);
64583
+ const decidedRows = groups.flatMap((g) => g.rows).filter((r) => r.status === "C" || r.status === "NC");
64584
+ const by = {
64585
+ engine: decidedRows.filter((r) => !r.decidedBy || r.decidedBy === "engine").length,
64586
+ scan: decidedRows.filter((r) => r.decidedBy === "scan").length,
64587
+ agent: decidedRows.filter((r) => r.decidedBy === "agent").length
64588
+ };
64589
+ const s = S[lang];
64590
+ const provenance = ["engine", "scan", "agent"].filter((key2) => by[key2] > 0).map((key2) => `${by[key2]} ${s.provenance[key2]}`);
64591
+ const remaining = Math.max(0, total - decided);
64445
64592
  const agentRuled = groups.some((g) => g.rows.some((r) => r.decidedBy === "agent" && r.status === "C"));
64446
- return { text: `${formatRate(decided === 0 ? null : result.conformancePct, decided, total)}${agentRuled ? "*" : ""}`, agentRuled };
64593
+ return {
64594
+ text: s.coverage(decided, total),
64595
+ detail: [...provenance, ...remaining ? [s.remaining(remaining)] : []].join(" \xB7 "),
64596
+ agentRuled
64597
+ };
64447
64598
  }
64448
64599
  var cell = (v) => v.replace(/\|/g, "\\|");
64449
64600
  function criterionTableHead(s) {
@@ -64482,11 +64633,11 @@ function stepSummary(result, opts = {}) {
64482
64633
  const lang = opts.lang ?? "en";
64483
64634
  const s = S[lang];
64484
64635
  const stdLabel = isCore(standard) ? "WCAG 2.2 AA" : loadPack(standard).name;
64485
- const rate = runRate(result, standard, lang);
64636
+ const coverage = runCoverage(result, standard, lang);
64486
64637
  const out2 = [];
64487
64638
  out2.push(`## ${s.title} \u2014 ${stdLabel}`, "");
64488
- out2.push(`\`${result.date}\` \xB7 ${result.scope.files} ${s.files} \xB7 **${rate.text}** ${s.rate}`, "");
64489
- if (rate.agentRuled) out2.push(`> ${agentMarkNote(lang)}`, "");
64639
+ out2.push(`\`${result.date}\` \xB7 ${result.scope.files} ${s.files} \xB7 **${coverage.text}**${coverage.detail ? ` \xB7 ${coverage.detail}` : ""}`, "");
64640
+ if (coverage.agentRuled) out2.push(`> ${agentMarkNote(lang)}`, "");
64490
64641
  const baseDir = opts.baseDir ?? process.cwd();
64491
64642
  const all = findingsForStandard(result, standard);
64492
64643
  if (!all.length) {
@@ -64537,15 +64688,15 @@ function prComment(result, opts = {}) {
64537
64688
  const all = findingsForStandard(result, standard);
64538
64689
  const normative = all.filter((f) => !f.advisory);
64539
64690
  const blocking = normative.filter((f) => f.severity === "bloquant").length;
64540
- const rate = runRate(result, standard, lang);
64691
+ const coverage = runCoverage(result, standard, lang);
64541
64692
  const grouped = groupFindings(all, standard, lang, baseDir);
64542
64693
  const criteria = groupByCriterion(grouped);
64543
- const orphans = unattributedFindings(result).filter((f) => !f.advisory).length;
64694
+ const orphans = all.filter((f) => !f.advisory && !f.page).length;
64544
64695
  const head = [];
64545
64696
  head.push(`### ${s.title} \u2014 ${stdLabel}`, "");
64546
64697
  head.push(blocking ? s.verdictFail(blocking) : normative.length ? s.verdictWarn : s.verdictPass, "");
64547
- head.push(`\`${result.date}\` \xB7 ${result.scope.files} ${s.files} \xB7 **${rate.text}** ${s.rate}`, "");
64548
- if (rate.agentRuled) head.push(`> ${agentMarkNote(lang)}`, "");
64698
+ head.push(`\`${result.date}\` \xB7 ${result.scope.files} ${s.files} \xB7 **${coverage.text}**${coverage.detail ? ` \xB7 ${coverage.detail}` : ""}`, "");
64699
+ if (coverage.agentRuled) head.push(`> ${agentMarkNote(lang)}`, "");
64549
64700
  if (orphans) head.push(`> ${s.unattributed(orphans)}`, "");
64550
64701
  const tail = [];
64551
64702
  if (opts.artifactName) tail.push(s.artifact(opts.artifactName), "");
@@ -64581,7 +64732,7 @@ function perPageTable(result, standard = CORE2, lang = "en") {
64581
64732
  "",
64582
64733
  ...scoreboardTable(result, derived, standard, s, lang),
64583
64734
  "",
64584
- ...basisCaveats(result, derived, s, lang),
64735
+ ...basisCaveats(result, derived, standard, s, lang),
64585
64736
  ...fullGridBlock(result, derived, standard, s, lang),
64586
64737
  "",
64587
64738
  ...derived.flatMap((pg) => [...namedCriteriaBlock(result, pg, standard, s, lang), ""])
@@ -64613,7 +64764,8 @@ function scoreboardTable(result, derived, standard, s, lang) {
64613
64764
  "| --- | --- | ---: | ---: | ---: | ---: | ---: |"
64614
64765
  ];
64615
64766
  for (const pg of derived) {
64616
- const n = (sev) => severityCount(pg, sev);
64767
+ const pageFindings = findingsForStandard(pageView(result, pg), standard).filter((f) => !f.advisory);
64768
+ const n = (sev) => pageFindings.filter((f) => f.severity === sev).length;
64617
64769
  const t3 = pageTally(pageCriterionRows(result, pg, standard, lang));
64618
64770
  out2.push(
64619
64771
  `| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${t3.c} | ${t3.nc} | ${n("bloquant")} | ${n("majeur")} | ${n("mineur")} |`
@@ -64637,12 +64789,10 @@ function undecidedBlock(result, derived, standard, s, lang) {
64637
64789
  out2.push("");
64638
64790
  return out2;
64639
64791
  }
64640
- function severityCount(pg, sev) {
64641
- return pg.findings.filter((f) => !f.advisory && f.severity === sev).length;
64642
- }
64643
- function basisCaveats(result, derived, s, lang) {
64792
+ function basisCaveats(result, derived, standard, s, lang) {
64644
64793
  const out2 = [`> ${s.scoreboardNote}`, ""];
64645
- const orphans = unattributedFindings(result).filter((f) => !f.advisory).length;
64794
+ const projected = new Set(findingsForStandard(result, standard));
64795
+ const orphans = unattributedFindings(result).filter((f) => projected.has(f) && !f.advisory).length;
64646
64796
  if (orphans) out2.push(`> ${s.unattributed(orphans)}`, "");
64647
64797
  if (derived.some((p) => p.basis === "attributed")) out2.push(`> ${s.sourceBasis}`, "");
64648
64798
  const notAudited = pageBasisWarning("not-audited", lang);
@@ -64654,9 +64804,11 @@ function pageBlock(result, page, standard, lang, baseDir) {
64654
64804
  const s = S[lang];
64655
64805
  const rows = pageCriterionRows(result, page, standard, lang);
64656
64806
  const nc = rows.filter((r) => r.status === "NC");
64657
- const occurrences = page.findings.filter((f) => !f.advisory).length;
64807
+ const pageFindings = findingsForStandard(pageView(result, page), standard).filter((f) => !f.advisory);
64808
+ const occurrences = pageFindings.length;
64658
64809
  if (!nc.length && !occurrences) return void 0;
64659
- const counts = `\u{1F534} ${severityCount(page, "bloquant")} \xB7 \u{1F7E0} ${severityCount(page, "majeur")} \xB7 \u{1F7E1} ${severityCount(page, "mineur")}`;
64810
+ const count = (severity) => pageFindings.filter((f) => f.severity === severity).length;
64811
+ const counts = `\u{1F534} ${count("bloquant")} \xB7 \u{1F7E0} ${count("majeur")} \xB7 \u{1F7E1} ${count("mineur")}`;
64660
64812
  const withTests = nc.some((r) => r.tests.length);
64661
64813
  const out2 = [
64662
64814
  "<details>",
@@ -64675,12 +64827,7 @@ function pageBlock(result, page, standard, lang, baseDir) {
64675
64827
  for (const r of nc) {
64676
64828
  out2.push(withTests ? `| ${cell(r.label)} | ${r.tests.map((t3) => `\`${t3}\``).join(" ")} |` : `| ${cell(r.label)} |`);
64677
64829
  }
64678
- const defects = groupFindings(
64679
- page.findings.filter((f) => !f.advisory),
64680
- standard,
64681
- lang,
64682
- baseDir
64683
- );
64830
+ const defects = groupFindings(pageFindings, standard, lang, baseDir);
64684
64831
  const blocking = groupByCriterion(defects.filter((g) => g.severity === "bloquant"));
64685
64832
  const rest = groupByCriterion(defects.filter((g) => g.severity !== "bloquant"));
64686
64833
  const shownBlocking = Math.min(blocking.length, PAGE_CRITERIA_SHOWN);
@@ -64717,7 +64864,8 @@ function fullGridBlock(result, derived, standard, s, lang) {
64717
64864
  return out2;
64718
64865
  }
64719
64866
  function orphansBlock(result, standard, s, lang, baseDir) {
64720
- const orphans = unattributedFindings(result).filter((f) => !f.advisory);
64867
+ const projected = new Set(findingsForStandard(result, standard));
64868
+ const orphans = unattributedFindings(result).filter((f) => projected.has(f) && !f.advisory);
64721
64869
  if (!orphans.length) return [];
64722
64870
  const criteria = groupByCriterion(groupFindings(orphans, standard, lang, baseDir));
64723
64871
  const counts = SEV_ORDER4.map((sev) => `${ICON5[sev]} ${orphans.filter((f) => f.severity === sev).length}`).join(" \xB7 ");
@@ -64774,17 +64922,18 @@ function pagesComment(result, opts = {}) {
64774
64922
  const derived = derivePages(result, scope);
64775
64923
  const normative = findingsForStandard(result, standard).filter((f) => !f.advisory);
64776
64924
  const blocking = normative.filter((f) => f.severity === "bloquant").length;
64777
- const rate = runRate(result, standard, lang);
64925
+ const coverage = runCoverage(result, standard, lang);
64778
64926
  head.push(blocking ? s.verdictFail(blocking) : normative.length ? s.verdictWarn : s.verdictPass, "");
64779
- head.push(`\`${result.date}\` \xB7 ${s.pagesCount(derived.length)} \xB7 **${rate.text}** ${s.rate}`, "");
64780
- if (rate.agentRuled) head.push(`> ${agentMarkNote(lang)}`, "");
64927
+ head.push(`\`${result.date}\` \xB7 ${s.pagesCount(derived.length)} \xB7 **${coverage.text}**${coverage.detail ? ` \xB7 ${coverage.detail}` : ""}`, "");
64928
+ if (coverage.agentRuled) head.push(`> ${agentMarkNote(lang)}`, "");
64929
+ const severity = (page, level) => findingsForStandard(pageView(result, page), standard).filter((f) => !f.advisory && f.severity === level).length;
64781
64930
  const blocks = [...derived].sort(
64782
- (a, b) => severityCount(b, "bloquant") - severityCount(a, "bloquant") || severityCount(b, "majeur") - severityCount(a, "majeur") || severityCount(b, "mineur") - severityCount(a, "mineur")
64931
+ (a, b) => severity(b, "bloquant") - severity(a, "bloquant") || severity(b, "majeur") - severity(a, "majeur") || severity(b, "mineur") - severity(a, "mineur")
64783
64932
  ).map((p) => pageBlock(result, p, standard, lang, baseDir)).filter((b) => b !== void 0);
64784
64933
  const assemble = (nBlocks2, nRows2, withGrid = true) => {
64785
64934
  const body3 = [...scoreboardTable(result, derived.slice(0, nRows2), standard, s, lang), ""];
64786
64935
  if (nRows2 < derived.length) body3.push(s.scoreboardClamped(derived.length - nRows2), "");
64787
- body3.push(...basisCaveats(result, derived, s, lang));
64936
+ body3.push(...basisCaveats(result, derived, standard, s, lang));
64788
64937
  body3.push(...undecidedBlock(result, derived, standard, s, lang));
64789
64938
  if (redirected.length) body3.push(...renderRedirected(redirected, lang), "");
64790
64939
  body3.push(...orphansBlock(result, standard, s, lang, baseDir), "");
@@ -71741,11 +71890,7 @@ async function cmdJudge(p) {
71741
71890
  if (truncated) items = items.slice(0, max);
71742
71891
  const size = grain === "criterion" ? 1 : BATCH_SIZE;
71743
71892
  const render2 = (slice) => formatAdjudication(slice, lang, standard, { preamble: false, contract: false });
71744
- const batches = [];
71745
- for (let i2 = 0; i2 < items.length; i2 += size) {
71746
- const slice = items.slice(i2, i2 + size);
71747
- batches.push({ items: slice, prompt: render2(slice) });
71748
- }
71893
+ const batches = batchWorklist(items, render2, size);
71749
71894
  const explicitModel = typeof p.flags.model === "string" && p.flags.model ? p.flags.model : void 0;
71750
71895
  const model = explicitModel ?? (runner === "claude" ? DEFAULT_CLI_MODEL : runner === "api" ? modelFromEnv() : void 0);
71751
71896
  const modelLabel = model ?? "Codex account default";
@@ -3,7 +3,7 @@ name: review-a11y
3
3
  description: "Use to REVIEW changed frontend code for accessibility: staged files, a working diff, branch, or PR; prompts such as 'review a11y', 'is this accessible?', or 'anything to fix before merge?'; and hook or CI findings on a change. Run the bundled install-free engine with cross-file JSX/TSX analysis, then adjudicate candidates and return a severity-ranked WCAG 2.2 AA review with file:line fixes, residual rendering risks, and a verdict. Use `ultra11y` instead for whole-repo audits, dated conformance reports, country standards, PRD backlogs, or authoring accessible markup."
4
4
  license: MIT
5
5
  metadata:
6
- version: 5.35.0
6
+ version: 5.35.2
7
7
  ---
8
8
 
9
9
  # review-a11y — accessibility review of changed code