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.
@@ -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;
@@ -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.1.0
7
+ version: 4.2.0
8
8
  ---
9
9
 
10
10
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -271,7 +271,7 @@ opens. `html: 'true'` and `evidence: 'true'` are on by default, so it now has a
271
271
  audits/
272
272
  ├── index.html ← the entry point: rate, synthesis, page scoreboard, links
273
273
  ├── ultra11y-<std>-<date>.html ← the whole audit in ONE file, printable to PDF
274
- ├── <std>-<date>.md ← the Markdown report (unchanged)
274
+ ├── <std>-<date>.md ← the Markdown report, illustrated by the same crops
275
275
  ├── audit-latest.json ← the machine-readable audit (unchanged)
276
276
  ├── assets/
277
277
  │ ├── <page-id>.png ← the page screenshot
@@ -298,26 +298,46 @@ to a `::warning::` and cannot catch its own regression.
298
298
 
299
299
  ### The crops
300
300
 
301
- An occurrence in a sheet gains a sub-bullet showing the element, ringed:
301
+ An occurrence gains a sub-bullet showing the element, ringed:
302
302
 
303
303
  ```md
304
304
  - [ ] `.ultra11y/pages/accueil/dom.html:412` (`div.card`) — <img> without an alt attribute…
305
305
  - ![Cropped capture of the img element on the accueil page, outlined](./assets/accueil/c90959b13aa2.png)
306
306
  ```
307
307
 
308
- They are derived AT RENDER TIME from the page snapshot, never stamped on the finding: a
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
309
317
  rectangle in pixels is a property of the image, and a stamped box goes silently wrong the
310
318
  moment the audit is re-rendered against a different capture. So `evidence` needs snapshots —
311
- `snapshot: 'true'` (the default), the E2E plugins, or `ultra11y dev`.
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.
312
322
 
313
323
  The mark is **not monochrome**: a white halo, a red ring and corner brackets whose *shape*
314
324
  says "here". A tool that reports 1.4.1 failures cannot ship a deliverable that commits one.
315
325
 
316
326
  When a crop cannot be drawn, the report says so per page and per criterion, with the reason —
317
- `no-screenshot`, `below-the-fold`, `unknown-scale`, `capped`, and eight more. **An occurrence
318
- without a picture must never read as an occurrence without a defect.** Caps are 6 per rule, 12
319
- per page and `evidence-max` overall (200 by default), because one design-system defect
320
- multiplied by 38 routes is one defect, not 472 pictures.
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.
321
341
 
322
342
  ### Printing it
323
343
 
@@ -326,9 +346,11 @@ The single file is the deliverable an auditor hands to a client: open
326
346
  its evidence on one sheet rather than splitting them across a page break.
327
347
 
328
348
  Its images travel inside it as `data:` URIs, and base64 costs a third more than the bytes it
329
- encodes — so there is a budget (`--inline-budget`, 12 MB) and a ladder. Over budget, page
330
- screenshots go first, then all but one crop per criterion, then the images entirely. Every
331
- rung is written **into the document** and onto stderr. Images degrade; the non-conformities
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
332
354
  never do.
333
355
 
334
356
  ### Publishing it as a page (optional, and it costs something)
@@ -318,21 +318,32 @@ The mark is not monochrome: a white halo, a red ring, corner brackets whose shap
318
318
  and a dashed inner line that survives a monochrome print. This tool reports 1.4.1 failures; its
319
319
  own deliverable cannot commit one.
320
320
 
321
- **Nothing is cut in silence.** Twelve refusal reasons, each with its own sentence, counted per
322
- page and per criterion in the sheet itself:
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
323
 
324
324
  | Reason | What happened |
325
325
  |---|---|
326
- | `no-snapshot` · `no-screenshot` | the page has no capture to crop from |
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 |
327
328
  | `unreadable-image` · `no-boxes` · `truncated` | the capture or the box digest cannot be trusted |
328
329
  | `no-offsets` · `unjoinable` | the finding has no byte range, or it resolves to no element |
329
330
  | `page-scope` | the finding is about the document, not an element you can frame |
330
331
  | `zero-area` · `below-the-fold` | the element is invisible, or outside a viewport-only capture |
331
332
  | `unknown-scale` | the device pixel ratio could not be derived — a crop would be off-target |
332
- | `capped` | 6 per rule, 12 per page, 200 per run |
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.
333
343
 
334
344
  An occurrence with no picture must never read as an occurrence with no defect, so the sheet
335
- says how many were not illustrated and why.
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.
336
347
 
337
348
  Without `--evidence` the Markdown is **byte-identical** to what it was. The crop bullet carries
338
349
  no `[ ]`, so `verify` builds the same worklist with or without it — indentation alone would not
@@ -340,10 +351,18 @@ be enough, since `AUDITOR_OCCURRENCE` is anchored `^\s*-\s\[ \]` and tolerates l
340
351
 
341
352
  ### The same, as a page (`--html`)
342
353
 
343
- `--html` writes `index.html` and one `page-<id>.html` beside the Markdown they mirror, plus a
344
- detachable single file that prints to PDF. Self-contained: no script, no external asset,
345
- nothing pointing outside the output directory. See `references/ci.md` for the artifact layout
346
- and the inline-size budget.
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.
347
366
 
348
367
  ## Holding an external audit against the grid
349
368