ultra11y 4.1.0 → 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.
@@ -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": "4.1.0",
13
+ "version": "4.2.0",
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": "4.1.0",
4
+ "version": "4.2.0",
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": "4.1.0",
4
+ "version": "4.2.0",
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 = "4.1.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "4.2.0";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
package/README.md CHANGED
@@ -411,10 +411,11 @@ node scripts/ultra11y.mjs check --report audits/wcag-YYYY-MM-DD.md # integrity
411
411
 
412
412
  ### A report someone will actually open
413
413
 
414
- `--html` turns the audit into a page: `audits/index.html` as the entry point, plus a
415
- detachable single file that prints to PDF. Self-contained — no script, no external asset,
416
- nothing pointing outside the directory — and it passes this engine's own accessibility audit,
417
- which is the least a tool like this owes.
414
+ `--html` turns the audit into a page: `audits/index.html` as the entry point, plus — from
415
+ `report` — a detachable single file that prints to PDF. Self-contained — no script, no
416
+ external asset, nothing pointing outside the directory — and it passes this engine's own
417
+ accessibility audit, which is the least a tool like this owes. `pages --html` writes the
418
+ navigable sheets only: **one composite per artifact**, or every inlined crop travels twice.
418
419
 
419
420
  ```sh
420
421
  node scripts/ultra11y.mjs report --in audit.json --html --evidence --out audits
@@ -424,9 +425,14 @@ node scripts/ultra11y.mjs pages --in audit.json --format report --split page \
424
425
 
425
426
  `--evidence` illustrates each non-conformity with an **annotated crop of the offending
426
427
  element**, cut from the page snapshot: `selectorHint` is lossy, and a rendered-tier finding on
427
- a client-rendered page is anchored at `dom.html:2` whatever it is really about. It needs
428
- snapshots, and it says per page and per criterion what it could not draw and why — an
429
- occurrence with no picture must never read as an occurrence with no defect.
428
+ a client-rendered page is anchored at `dom.html:2` whatever it is really about. It illustrates
429
+ the Markdown conformance report as much as the HTML — one set of files in `assets/`, referenced
430
+ by every document that shows the defect — so `--html` is not required to get pictures.
431
+
432
+ It needs snapshots, and it says per page and per criterion what it could not draw and why — an
433
+ occurrence with no picture must never read as an occurrence with no defect. It distinguishes
434
+ an occurrence *folded* into another's picture from a distinct defect a limit *cut off*, because
435
+ only the second is a gap.
430
436
 
431
437
  In the GitHub Action both are **on by default**, so the uploaded artifact has a front door
432
438
  without changing a workflow. See `references/ci.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
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 join47, relative as relative5, sep as sep7, dirname as dirname1
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "4.1.0";
9
+ var VERSION = "4.2.0";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -49748,12 +49748,13 @@ function render(r, lang, opts) {
49748
49748
  const group = ncUnits.filter((u) => u.severity === sev);
49749
49749
  if (!group.length) continue;
49750
49750
  out2.push(`### ${ICON3[sev]} ${s.sev[sev]} (${group.length})`, "");
49751
- for (const u of group) out2.push(...renderAuditorUnit(u, opts.standard, lang, { heading: "####" }));
49751
+ for (const u of group) out2.push(...renderAuditorUnit(u, opts.standard, lang, { heading: "####", ...opts.cropFor ? { cropFor: opts.cropFor } : {} }));
49752
49752
  }
49753
49753
  }
49754
49754
  if (advisoryUnits.length) {
49755
49755
  out2.push(`## \u{1F4A1} ${s.recTitle}`, "", `> ${s.recNote}`, "");
49756
- for (const u of advisoryUnits) out2.push(...renderAuditorUnit(u, opts.standard, lang, { heading: "###" }));
49756
+ for (const u of advisoryUnits)
49757
+ out2.push(...renderAuditorUnit(u, opts.standard, lang, { heading: "###", ...opts.cropFor ? { cropFor: opts.cropFor } : {} }));
49757
49758
  }
49758
49759
  const pageScope = pagesOf(r);
49759
49760
  if (pageScope.length) attributePages(r, pageScope);
@@ -49837,9 +49838,9 @@ function reportGroups(r, lang = "en") {
49837
49838
  }
49838
49839
  return r.guidelines.map((g) => ({ key: g.key, title: guidelineTitle(g.key, lang) ?? g.title, rows: byGuideline.get(g.key) ?? [] }));
49839
49840
  }
49840
- function renderReport(r, lang = "en", outDir) {
49841
+ function renderReport(r, lang = "en", outDir, cropFor) {
49841
49842
  const s = L4[lang];
49842
- return render(r, lang, { std: s.wcagStd, groupHead: s.byGuideline, groups: reportGroups(r, lang), standard: CORE2, outDir });
49843
+ return render(r, lang, { std: s.wcagStd, groupHead: s.byGuideline, groups: reportGroups(r, lang), standard: CORE2, outDir, ...cropFor ? { cropFor } : {} });
49843
49844
  }
49844
49845
  function packReportGroups(r, pack, lang = "en") {
49845
49846
  const derived = derivePackResults(r, pack.key);
@@ -49862,7 +49863,7 @@ function packReportGroups(r, pack, lang = "en") {
49862
49863
  }
49863
49864
  return pack.themes.map((t2) => ({ key: `${t2.number}.`, title: themeName(pack, t2.number, lang) ?? "", rows: byTheme.get(t2.number) ?? [] }));
49864
49865
  }
49865
- function renderPackReport(r, pack, lang = "en", outDir) {
49866
+ function renderPackReport(r, pack, lang = "en", outDir, cropFor) {
49866
49867
  const derived = derivePackResults(r, pack.key);
49867
49868
  const std = `${pack.name} ${pack.baseVersion}`;
49868
49869
  return render(r, lang, {
@@ -49876,12 +49877,13 @@ function renderPackReport(r, pack, lang = "en", outDir) {
49876
49877
  // Forwarded, unlike before: without it the per-page screenshots resolved against the
49877
49878
  // CWD instead of the report's own directory, so a pack report written to `audits/`
49878
49879
  // carried links that only worked when read from the repo root.
49879
- outDir
49880
+ outDir,
49881
+ ...cropFor ? { cropFor } : {}
49880
49882
  });
49881
49883
  }
49882
49884
  function writeReport(r, opts) {
49883
49885
  const core = isCore(opts.standard);
49884
- const md = core ? renderReport(r, opts.lang, opts.out) : renderPackReport(r, loadPack(opts.standard), opts.lang, opts.out);
49886
+ const md = core ? renderReport(r, opts.lang, opts.out, opts.cropFor) : renderPackReport(r, loadPack(opts.standard), opts.lang, opts.out, opts.cropFor);
49885
49887
  mkdirSync7(opts.out, { recursive: true });
49886
49888
  const path = join29(opts.out, `${core ? "wcag" : opts.standard}-${r.date}.md`);
49887
49889
  writeFileSync8(path, md);
@@ -50372,17 +50374,19 @@ function resolve4(opts) {
50372
50374
  const mode = opts.transport ?? "auto";
50373
50375
  const api = (env.GITHUB_API_URL || "https://api.github.com").replace(/\/+$/, "");
50374
50376
  const token = env.GH_TOKEN || env.GITHUB_TOKEN;
50375
- const cliOk = (opts.cliAvailable ?? ghAvailable)();
50377
+ const probe = opts.cliAvailable ?? ghAvailable;
50378
+ let cached;
50379
+ const cliOk = () => cached ??= probe();
50376
50380
  if (mode === "cli") {
50377
- return cliOk ? { transport: "cli", api } : { transport: "cli", api, reason: "`gh` is not installed or not authenticated (run `gh auth login`)" };
50381
+ return cliOk() ? { transport: "cli", api } : { transport: "cli", api, reason: "`gh` is not installed or not authenticated (run `gh auth login`)" };
50378
50382
  }
50379
- const repo = env.ULTRA11Y_GITHUB_REPO || env.GITHUB_REPOSITORY || (mode === "rest" || !cliOk ? gitRemoteSlug("github.com") : void 0);
50383
+ const repo = env.ULTRA11Y_GITHUB_REPO || env.GITHUB_REPOSITORY || (mode === "rest" || !cliOk() ? gitRemoteSlug("github.com") : void 0);
50380
50384
  if (mode === "rest") {
50381
50385
  if (!token) return { transport: "rest", api, reason: "no GitHub token \u2014 set GH_TOKEN or GITHUB_TOKEN" };
50382
50386
  if (!repo) return { transport: "rest", api, token, reason: "no repository \u2014 set GITHUB_REPOSITORY (owner/name) or ULTRA11Y_GITHUB_REPO" };
50383
50387
  return { transport: "rest", api, token, repo };
50384
50388
  }
50385
- if (cliOk) return { transport: "cli", api };
50389
+ if (cliOk()) return { transport: "cli", api };
50386
50390
  if (token && repo) return { transport: "rest", api, token, repo };
50387
50391
  const missing = [!token ? "a token (GH_TOKEN/GITHUB_TOKEN)" : "", !repo ? "a repository (GITHUB_REPOSITORY)" : ""].filter(Boolean);
50388
50392
  return {
@@ -51516,7 +51520,8 @@ function renderPageReport(result, page, opts = {}) {
51516
51520
  const shot = opts.screenshots?.get(page.id);
51517
51521
  if (shot) out2.push(`![${s.screenshotAlt(page.name)}](${shot})`, "");
51518
51522
  else out2.push(`_${s.noScreenshot}_`, "");
51519
- if (opts.evidenceNotice?.length) out2.push(...opts.evidenceNotice, "");
51523
+ const refused = opts.evidenceNotice?.(page.id) ?? [];
51524
+ if (refused.length) out2.push(...refused, "");
51520
51525
  const withTests = rows.some((r) => r.tests.length);
51521
51526
  out2.push(`${h}# ${s.gridTitle}`, "", `> ${s.gridNote}`, "");
51522
51527
  out2.push(withTests ? `| ${s.criterion} | ${s.tests} | ${s.status} |` : `| ${s.criterion} | ${s.status} |`);
@@ -56586,7 +56591,7 @@ var S2 = {
56586
56591
  alt: (sel, page) => `Capture recadr\xE9e de l'\xE9l\xE9ment ${sel} sur la page ${page}, entour\xE9 d'un cadre`,
56587
56592
  notImaged: (n) => `${n} occurrence(s) ne sont pas illustr\xE9es :`,
56588
56593
  reasons: {
56589
- "no-snapshot": "constat lev\xE9 sur le code source, pas sur une page captur\xE9e \u2014 il n'y a pas de pixels \xE0 montrer",
56594
+ "no-snapshot": "la capture de page que ce constat d\xE9signe n'est pas sur ce disque \u2014 sa r\xE9f\xE9rence fichier:ligne reste valable",
56590
56595
  "no-screenshot": "le producteur n'a pas fourni de capture pour cette page, donc le niveau pixel est inactif ici",
56591
56596
  "unreadable-image": "la capture n'est pas d\xE9codable (le moteur lit le truecolour 8 bits uniquement) \u2014 mieux vaut aucune image qu'une fausse",
56592
56597
  "no-boxes": "les positions d'\xE9l\xE9ments ne se v\xE9rifient pas contre le DOM s\xE9rialis\xE9, elles ont donc \xE9t\xE9 refus\xE9es en bloc plut\xF4t que d'encadrer le mauvais \xE9l\xE9ment",
@@ -56597,14 +56602,15 @@ var S2 = {
56597
56602
  "zero-area": "l'\xE9l\xE9ment n'occupe aucune surface peinte",
56598
56603
  "below-the-fold": "l'\xE9l\xE9ment est hors de la capture (la capture couvre la fen\xEAtre, pas la page enti\xE8re) \u2014 sa r\xE9f\xE9rence fichier:ligne reste valable",
56599
56604
  "unknown-scale": "le rapport pixels/CSS est ind\xE9termin\xE9 pour cette page ; un rapport devin\xE9 encadrerait le mauvais endroit",
56600
- capped: "occurrences regroup\xE9es : une illustration par \xE9l\xE9ment distinct, dans la limite fix\xE9e"
56605
+ deduplicated: "m\xEAme d\xE9faut sur le m\xEAme \xE9l\xE9ment : il est montr\xE9 par l'illustration d'une autre occurrence, rien n'a \xE9t\xE9 perdu",
56606
+ capped: "d\xE9faut distinct laiss\xE9 sans image : le plafond de vignettes de ce lot est atteint \u2014 relevez `--evidence-max` pour les obtenir"
56601
56607
  }
56602
56608
  },
56603
56609
  en: {
56604
56610
  alt: (sel, page) => `Cropped screenshot of the ${sel} element on the ${page} page, outlined`,
56605
56611
  notImaged: (n) => `${n} occurrence(s) are not illustrated:`,
56606
56612
  reasons: {
56607
- "no-snapshot": "raised on source code, not on a captured page \u2014 there are no pixels to show",
56613
+ "no-snapshot": "the page snapshot this finding names is not on this disk \u2014 its file:line reference still holds",
56608
56614
  "no-screenshot": "the producer supplied no screenshot for this page, so the pixel tier is inactive here",
56609
56615
  "unreadable-image": "the screenshot does not decode (the engine reads 8-bit truecolour only) \u2014 no image beats a wrong one",
56610
56616
  "no-boxes": "the element positions do not verify against the serialized DOM, so they were refused wholesale rather than outlining the wrong element",
@@ -56615,22 +56621,26 @@ var S2 = {
56615
56621
  "zero-area": "the element paints no box",
56616
56622
  "below-the-fold": "the element sits outside the capture (the screenshot covers the viewport, not the whole page) \u2014 its file:line reference still holds",
56617
56623
  "unknown-scale": "the image-pixel to CSS-pixel ratio is indeterminate for this page; a guessed ratio would outline the wrong place",
56618
- capped: "occurrences grouped: one illustration per distinct element, up to the cap"
56624
+ deduplicated: "the same defect on the same element: it is shown by the illustration of another occurrence, nothing was lost",
56625
+ capped: "a distinct defect left undrawn: this run's crop limit was reached \u2014 raise `--evidence-max` to get them"
56619
56626
  }
56620
56627
  }
56621
56628
  };
56622
- function evidenceNotice(m, pageId, lang) {
56629
+ function evidenceRefusals(m, pageId, lang) {
56623
56630
  const t2 = pageId === null ? m.totals : m.perPage.get(pageId);
56624
- if (!t2) return [];
56631
+ if (!t2) return void 0;
56625
56632
  const missing = t2.located - t2.imaged;
56626
- if (missing <= 0) return [];
56633
+ if (missing <= 0) return void 0;
56634
+ const entries = Object.entries(t2.skipped).filter(([, n]) => n);
56627
56635
  const s = S2[lang];
56628
- const out2 = [s.notImaged(missing)];
56629
- for (const [reason, n] of Object.entries(t2.skipped).sort((a, b) => (b[1] ?? 0) - (a[1] ?? 0))) {
56630
- if (!n) continue;
56631
- out2.push(`- ${n} \u2014 ${s.reasons[reason]}`);
56632
- }
56633
- return out2;
56636
+ return {
56637
+ headline: s.notImaged(missing),
56638
+ reasons: entries.sort((a, b) => b[1] - a[1]).map(([reason, n]) => `${n} \u2014 ${s.reasons[reason]}`)
56639
+ };
56640
+ }
56641
+ function evidenceNotice(m, pageId, lang) {
56642
+ const r = evidenceRefusals(m, pageId, lang);
56643
+ return r ? [r.headline, ...r.reasons.map((x) => `- ${x}`)] : [];
56634
56644
  }
56635
56645
  function bump(t2, reason) {
56636
56646
  t2.skipped[reason] = (t2.skipped[reason] ?? 0) + 1;
@@ -56678,10 +56688,12 @@ function writeEvidence(result, opts) {
56678
56688
  const id = findingId(f);
56679
56689
  const key = `${f.ruleId}\0${f.selectorHint}`;
56680
56690
  const ruleCount = perRule.get(f.ruleId) ?? 0;
56681
- if (seen.has(key) || ruleCount >= caps.perRule || onPage >= caps.perPage || manifest.crops.size >= caps.total) {
56682
- manifest.skipped.set(id, "capped");
56683
- bump(tally, "capped");
56684
- bump(manifest.totals, "capped");
56691
+ const overLimit = ruleCount >= caps.perRule || onPage >= caps.perPage || manifest.crops.size >= caps.total;
56692
+ const refusal = seen.has(key) ? "deduplicated" : overLimit ? "capped" : null;
56693
+ if (refusal) {
56694
+ manifest.skipped.set(id, refusal);
56695
+ bump(tally, refusal);
56696
+ bump(manifest.totals, refusal);
56685
56697
  continue;
56686
56698
  }
56687
56699
  const crop = cropFinding(ctx, f, opts.geometry);
@@ -56888,6 +56900,13 @@ function renderHtmlDocument(doc) {
56888
56900
  }
56889
56901
 
56890
56902
  // src/html-report.ts
56903
+ function refusalBlocks(r) {
56904
+ if (!r) return [];
56905
+ return [
56906
+ { kind: "note", tone: "warn", runs: [{ text: r.headline }] },
56907
+ { kind: "list", items: r.reasons.map((text) => [{ text }]) }
56908
+ ];
56909
+ }
56891
56910
  var T4 = {
56892
56911
  fr: {
56893
56912
  reportTitle: "Rapport de conformit\xE9",
@@ -56937,8 +56956,7 @@ var T4 = {
56937
56956
  openComposite: "Rapport complet, en un seul fichier (imprimable en PDF)",
56938
56957
  openPages: "Rapport page par page",
56939
56958
  noScreenshot: "Aucune capture d'\xE9cran pour cette page.",
56940
- screenshotAlt: (n) => `Capture d'\xE9cran de la page ${n}`,
56941
- notImaged: (n) => `${n} occurrence(s) n'ont pas pu \xEAtre illustr\xE9es.`
56959
+ screenshotAlt: (n) => `Capture d'\xE9cran de la page ${n}`
56942
56960
  },
56943
56961
  en: {
56944
56962
  reportTitle: "Conformance report",
@@ -56988,8 +57006,7 @@ var T4 = {
56988
57006
  openComposite: "Full report, in a single file (printable to PDF)",
56989
57007
  openPages: "Page-by-page report",
56990
57008
  noScreenshot: "No screenshot for this page.",
56991
- screenshotAlt: (n) => `Screenshot of the ${n} page`,
56992
- notImaged: (n) => `${n} occurrence(s) could not be illustrated.`
57009
+ screenshotAlt: (n) => `Screenshot of the ${n} page`
56993
57010
  }
56994
57011
  };
56995
57012
  function ticks(text) {
@@ -57084,10 +57101,11 @@ function criterionBlocks(unit, standard, lang, level, crops) {
57084
57101
  function resolveOccurrence(f, lang) {
57085
57102
  return f.msg ? auditorUnitModel({ criteriaId: f.criteriaId, title: "", label: "", refs: [], severity: f.severity, findings: [f] }, CORE2, lang).messages[0] ?? f.message : f.message;
57086
57103
  }
57087
- function findingsBlocks(result, standard, lang, level, crops) {
57104
+ function findingsBlocks(result, standard, lang, level, crops, refusals) {
57088
57105
  const t2 = T4[lang];
57089
57106
  const { nc, advisory } = partitionUnits(prdUnits(result, standard, lang));
57090
57107
  const out2 = [{ kind: "heading", level: 2, text: t2.ncTitle, id: "nc" }];
57108
+ out2.push(...refusalBlocks(refusals?.(null)));
57091
57109
  if (!nc.length) out2.push({ kind: "para", runs: [{ text: t2.noNc }] });
57092
57110
  for (const u of nc) out2.push(...criterionBlocks(u, standard, lang, level === 2 ? 3 : 4, crops));
57093
57111
  if (advisory.length) {
@@ -57199,7 +57217,7 @@ function compositeDoc(result, opts = {}) {
57199
57217
  const h = headline(result, standard, lang);
57200
57218
  const blocks = [...synthesisBlocks(result, standard, lang)];
57201
57219
  if (h.agentRuled) blocks.push({ kind: "note", tone: "warn", runs: ticks(agentMarkNote(lang)) });
57202
- blocks.push(...findingsBlocks(result, standard, lang, 2, opts.crops));
57220
+ blocks.push(...findingsBlocks(result, standard, lang, 2, opts.crops, opts.refusals));
57203
57221
  blocks.push(...scoreboardBlocks(result, standard, lang));
57204
57222
  const scope = pagesOf(result);
57205
57223
  if (scope.length) blocks.push(...crossGridBlocks(result, derivePages(result, scope), standard, lang));
@@ -57236,6 +57254,7 @@ function pageDoc(result, page, opts = {}) {
57236
57254
  blocks.push(
57237
57255
  opts.screenshot ? { kind: "figure", src: opts.screenshot, alt: t2.screenshotAlt(page.name) } : { kind: "para", runs: [{ text: t2.noScreenshot, em: true }] }
57238
57256
  );
57257
+ blocks.push(...refusalBlocks(opts.refusals?.(page.id)));
57239
57258
  blocks.push(...pageGridBlocks(result, page, standard, lang));
57240
57259
  const view = { ...result, criteria: page.criteria, findings: page.findings };
57241
57260
  const { nc, advisory } = partitionUnits(prdUnits(view, standard, lang));
@@ -57300,6 +57319,10 @@ function cropLookup(m, lang, hrefOf) {
57300
57319
  return href ? { href, alt: c2.alt[lang] } : void 0;
57301
57320
  };
57302
57321
  }
57322
+ function refusalLookup(m, lang) {
57323
+ if (!m) return void 0;
57324
+ return (pageId) => evidenceRefusals(m, pageId, lang);
57325
+ }
57303
57326
  function pickRung(cropBytes, shotBytes, budget) {
57304
57327
  const sum = (xs) => xs.reduce((a, b) => a + b, 0);
57305
57328
  if (sum(cropBytes) + sum(shotBytes) <= budget) return { steps: [], cropsPerCriterion: Number.POSITIVE_INFINITY, shots: true };
@@ -57361,9 +57384,16 @@ function writeHtml(result, opts) {
57361
57384
  { href: `./${compositeName}`, text: t2.docTitle },
57362
57385
  ...opts.pages ? [{ href: "./pages/index.html", text: t2.pagesTitle }] : []
57363
57386
  ];
57387
+ const refusals = refusalLookup(opts.evidence, lang);
57364
57388
  let compositePath;
57365
57389
  if (!flat) {
57366
- const composite = compositeDoc(result, { standard, lang, crops: budgetedCrops, nav: nav.map((n) => ({ ...n, current: n.href === `./${compositeName}` })) });
57390
+ const composite = compositeDoc(result, {
57391
+ standard,
57392
+ lang,
57393
+ crops: budgetedCrops,
57394
+ ...refusals ? { refusals } : {},
57395
+ nav: nav.map((n) => ({ ...n, current: n.href === `./${compositeName}` }))
57396
+ });
57367
57397
  if (notices.length) composite.blocks.unshift({ kind: "note", tone: "warn", runs: noticeRuns(notices) });
57368
57398
  compositePath = join41(opts.outDir, compositeName);
57369
57399
  writeFileSync16(compositePath, renderHtmlDocument(composite));
@@ -57401,6 +57431,7 @@ function writeHtml(result, opts) {
57401
57431
  standard,
57402
57432
  lang,
57403
57433
  crops: fileCrops,
57434
+ ...refusals ? { refusals } : {},
57404
57435
  nav: sheetNav,
57405
57436
  // Where `cmdPages`' screenshot copier already put the capture: `assets/<id>.png`,
57406
57437
  // beside the entry point, seen from wherever this sheet sits.
@@ -59835,7 +59866,8 @@ Usage:
59835
59866
  ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
59836
59867
  ultra11y audit --in <audit.json> [--fail-on blocking|major|minor] [--format sarif|github] (re-gate an audit already computed \u2014 e.g. one carrying adjudicated verdicts \u2014 without a second detection pass)
59837
59868
  ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
59838
- ultra11y report --in <audit.json> --html [--evidence] [--inline-budget <bytes>] [--out <dir>] (index.html + a printable single file; --evidence adds annotated crops)
59869
+ ultra11y report --in <audit.json> [--evidence [--evidence-max <n>]] [--out <dir>] (the Markdown report, illustrated with annotated crops)
59870
+ ultra11y report --in <audit.json> --html [--evidence] [--inline-budget <bytes>] [--out <dir>] (index.html + a printable single file)
59839
59871
  ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--lang auto|en|fr]
59840
59872
  ultra11y tickets --in <audit.json> [--provider auto|github|gitlab|jira] [--grain criterion|page|page-criterion|single|file] [--transport auto|cli|rest]
59841
59873
  ultra11y tickets [--out <dir>] [--max-tickets <n>] [--dry-run] [--json] [--standard <pack>] [--format audit|remediation] [--lang auto|en|fr]
@@ -59860,7 +59892,7 @@ Usage:
59860
59892
  ultra11y snapshot list [--root <dir>] [--json]
59861
59893
  ultra11y pages --in <audit.json> [--standard <pack>] [--json] [--lang auto|en|fr] (the per-page criterion grid)
59862
59894
  ultra11y pages --in <audit.json> --format report [--split page] [--out <dir>] (the per-page report, with screenshots)
59863
- ultra11y pages --in <audit.json> --format report --out <dir> [--evidence] [--html] (annotated crops of each non-conformity, and the HTML site)
59895
+ ultra11y pages --in <audit.json> --format report --out <dir> [--evidence [--evidence-max <n>]] [--html] (annotated crops of each non-conformity, and the HTML site)
59864
59896
  ultra11y pages discover --sitemap <url> | --crawl <url> | --from-snapshots [--depth <n>] [--max <n>] [--write] [--json] (build the page sample)
59865
59897
  ultra11y pages --in <audit.json> --standard <pack> --diff <external.json> (hold the grid against an audit someone else performed)
59866
59898
  ultra11y import --from file <report.json> | --from ara <id> [--source <adapter>] [--out <dir>] [--json]
@@ -60259,6 +60291,8 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
60259
60291
  "format",
60260
60292
  // Bytes of image data the single-file HTML report may carry inline (src/html-emit.ts).
60261
60293
  "inline-budget",
60294
+ // Hard cap on the crops one run writes (src/evidence.ts `DEFAULT_CAPS.total`).
60295
+ "evidence-max",
60262
60296
  "guidance",
60263
60297
  "runtime",
60264
60298
  "cwd",
@@ -60903,7 +60937,7 @@ async function cmdPages(p) {
60903
60937
  return 0;
60904
60938
  }
60905
60939
  mkdirSync17(outDir, { recursive: true });
60906
- const manifest = wantEvidence ? writeEvidence(result, { outDir }) : void 0;
60940
+ const manifest = wantEvidence ? writeEvidence(result, { outDir, ...evidenceCapsOf(p) }) : void 0;
60907
60941
  const cropFor = manifest ? (f) => {
60908
60942
  const c2 = manifest.crops.get(findingId(f));
60909
60943
  return c2 ? { href: c2.href, alt: c2.alt[lang] } : void 0;
@@ -60916,7 +60950,8 @@ async function cmdPages(p) {
60916
60950
  for (const line of total) console.error(line);
60917
60951
  }
60918
60952
  const evidenceOpts = {
60919
- ...cropFor ? { cropFor } : {}
60953
+ ...cropFor ? { cropFor } : {},
60954
+ ...manifest ? { evidenceNotice: (id) => evidenceNotice(manifest, id, lang) } : {}
60920
60955
  };
60921
60956
  if (!split) {
60922
60957
  const file = join47(outDir, `pages-${result.date}.md`);
@@ -60939,12 +60974,8 @@ async function cmdPages(p) {
60939
60974
  const sheet = (id) => `page-${id}.md`;
60940
60975
  const hrefs = new Map(derived.map((pg) => [pg.id, `./${sheet(pg.id)}`]));
60941
60976
  for (const pg of derived) {
60942
- const notice = manifest ? evidenceNotice(manifest, pg.id, lang) : [];
60943
- writeFileSync19(
60944
- join47(outDir, sheet(pg.id)),
60945
- `${renderPageDocument(result, pg, { standard, lang, screenshots: shots, ...evidenceOpts, ...notice.length ? { evidenceNotice: notice } : {} })}
60946
- `
60947
- );
60977
+ writeFileSync19(join47(outDir, sheet(pg.id)), `${renderPageDocument(result, pg, { standard, lang, screenshots: shots, ...evidenceOpts })}
60978
+ `);
60948
60979
  }
60949
60980
  const index = join47(outDir, "index.md");
60950
60981
  writeFileSync19(index, `${renderPagesIndex(result, derived, { standard, lang, hrefs })}
@@ -60962,11 +60993,20 @@ async function cmdPages(p) {
60962
60993
  console.log(index);
60963
60994
  return html?.imagesDropped ? 1 : 0;
60964
60995
  }
60965
- function budgetOf(p) {
60966
- const raw = p.flags["inline-budget"];
60996
+ function positiveIntFlag(p, name2) {
60997
+ const raw = p.flags[name2];
60967
60998
  if (typeof raw !== "string" || !raw) return void 0;
60968
60999
  const n = Number.parseInt(raw, 10);
60969
- return Number.isFinite(n) && n > 0 ? n : void 0;
61000
+ if (Number.isFinite(n) && n > 0) return n;
61001
+ console.error(`ultra11y: --${name2} expects a positive integer (got "${raw}"). Ignored \u2014 the default applies.`);
61002
+ return void 0;
61003
+ }
61004
+ function budgetOf(p) {
61005
+ return positiveIntFlag(p, "inline-budget");
61006
+ }
61007
+ function evidenceCapsOf(p) {
61008
+ const total = positiveIntFlag(p, "evidence-max");
61009
+ return total === void 0 ? {} : { caps: { total } };
60970
61010
  }
60971
61011
  function emitHtml(result, opts) {
60972
61012
  const res = writeHtml(result, opts);
@@ -61205,17 +61245,18 @@ async function cmdReport(p) {
61205
61245
  emitCiFormat(result, ciFormat, standard, lang);
61206
61246
  return 0;
61207
61247
  }
61208
- const path = writeReport(result, { out: out2, lang, standard });
61248
+ const manifest = p.flags.evidence === true ? writeEvidence(result, { outDir: out2, ...evidenceCapsOf(p) }) : void 0;
61249
+ if (manifest) {
61250
+ for (const line of evidenceNotice(manifest, null, lang)) console.error(line);
61251
+ }
61252
+ const cropFor = manifest ? (f) => {
61253
+ const c2 = manifest.crops.get(findingId(f));
61254
+ return c2 ? { href: c2.href, alt: c2.alt[lang] } : void 0;
61255
+ } : void 0;
61256
+ const path = writeReport(result, { out: out2, lang, standard, ...cropFor ? { cropFor } : {} });
61209
61257
  let html;
61210
61258
  if (p.flags.html === true) {
61211
- const manifest = p.flags.evidence === true ? writeEvidence(result, { outDir: out2 }) : void 0;
61212
- if (manifest) for (const line of evidenceNotice(manifest, null, lang)) console.error(line);
61213
61259
  html = emitHtml(result, { outDir: out2, standard, lang, ...manifest ? { evidence: manifest } : {}, inlineBudget: budgetOf(p) });
61214
- } else if (p.flags.evidence === true) {
61215
- console.error(
61216
- lang === "fr" ? "ultra11y report : --evidence n'illustre que le rapport HTML. Ajoutez --html, ou utilisez `pages --format report --evidence` pour des fiches Markdown illustr\xE9es." : "ultra11y report: --evidence only illustrates the HTML report. Add --html, or use `pages --format report --evidence` for illustrated Markdown sheets."
61217
- );
61218
- return 2;
61219
61260
  }
61220
61261
  const untested = isCore(standard) ? [] : untestedNeedsRendering(result);
61221
61262
  const partial = untested.length > 0;
@@ -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: 4.1.0
8
+ version: 4.2.0
9
9
  ---
10
10
 
11
11
  # review-a11y — accessibility review of changed code