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;
package/src/cli.ts CHANGED
@@ -88,7 +88,8 @@ Usage:
88
88
  ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
89
89
  ultra11y audit --in <audit.json> [--fail-on blocking|major|minor] [--format sarif|github] (re-gate an audit already computed — e.g. one carrying adjudicated verdicts — without a second detection pass)
90
90
  ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
91
- ultra11y report --in <audit.json> --html [--evidence] [--inline-budget <bytes>] [--out <dir>] (index.html + a printable single file; --evidence adds annotated crops)
91
+ ultra11y report --in <audit.json> [--evidence [--evidence-max <n>]] [--out <dir>] (the Markdown report, illustrated with annotated crops)
92
+ ultra11y report --in <audit.json> --html [--evidence] [--inline-budget <bytes>] [--out <dir>] (index.html + a printable single file)
92
93
  ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--lang auto|en|fr]
93
94
  ultra11y tickets --in <audit.json> [--provider auto|github|gitlab|jira] [--grain criterion|page|page-criterion|single|file] [--transport auto|cli|rest]
94
95
  ultra11y tickets [--out <dir>] [--max-tickets <n>] [--dry-run] [--json] [--standard <pack>] [--format audit|remediation] [--lang auto|en|fr]
@@ -113,7 +114,7 @@ Usage:
113
114
  ultra11y snapshot list [--root <dir>] [--json]
114
115
  ultra11y pages --in <audit.json> [--standard <pack>] [--json] [--lang auto|en|fr] (the per-page criterion grid)
115
116
  ultra11y pages --in <audit.json> --format report [--split page] [--out <dir>] (the per-page report, with screenshots)
116
- ultra11y pages --in <audit.json> --format report --out <dir> [--evidence] [--html] (annotated crops of each non-conformity, and the HTML site)
117
+ ultra11y pages --in <audit.json> --format report --out <dir> [--evidence [--evidence-max <n>]] [--html] (annotated crops of each non-conformity, and the HTML site)
117
118
  ultra11y pages discover --sitemap <url> | --crawl <url> | --from-snapshots [--depth <n>] [--max <n>] [--write] [--json] (build the page sample)
118
119
  ultra11y pages --in <audit.json> --standard <pack> --diff <external.json> (hold the grid against an audit someone else performed)
119
120
  ultra11y import --from file <report.json> | --from ara <id> [--source <adapter>] [--out <dir>] [--json]
@@ -516,6 +517,8 @@ const VALUE_FLAGS = new Set([
516
517
  "format",
517
518
  // Bytes of image data the single-file HTML report may carry inline (src/html-emit.ts).
518
519
  "inline-budget",
520
+ // Hard cap on the crops one run writes (src/evidence.ts `DEFAULT_CAPS.total`).
521
+ "evidence-max",
519
522
  "guidance",
520
523
  "runtime",
521
524
  "cwd",
@@ -1409,7 +1412,7 @@ async function cmdPages(p: ParsedArgs): Promise<number> {
1409
1412
  // was raised on — never stamped on the finding, because a rectangle in pixels is a property
1410
1413
  // of the image and goes silently wrong the moment the audit is re-rendered against another
1411
1414
  // capture. `writeEvidence` reports what it could NOT draw, and that refusal is printed.
1412
- const manifest = wantEvidence ? writeEvidence(result, { outDir }) : undefined;
1415
+ const manifest = wantEvidence ? writeEvidence(result, { outDir, ...evidenceCapsOf(p) }) : undefined;
1413
1416
  const cropFor = manifest
1414
1417
  ? (f: Finding) => {
1415
1418
  const c = manifest.crops.get(findingId(f));
@@ -1425,8 +1428,12 @@ async function cmdPages(p: ParsedArgs): Promise<number> {
1425
1428
  );
1426
1429
  for (const line of total) console.error(line);
1427
1430
  }
1431
+ // ONE object, both paths. The split path used to assemble its own notice and the combined
1432
+ // path silently went without — so the deliverable that fits in a single file was the one
1433
+ // that never said what it had not illustrated.
1428
1434
  const evidenceOpts = {
1429
1435
  ...(cropFor ? { cropFor } : {}),
1436
+ ...(manifest ? { evidenceNotice: (id: string) => evidenceNotice(manifest, id, lang) } : {}),
1430
1437
  };
1431
1438
 
1432
1439
  if (!split) {
@@ -1456,11 +1463,7 @@ async function cmdPages(p: ParsedArgs): Promise<number> {
1456
1463
  const sheet = (id: string): string => `page-${id}.md`;
1457
1464
  const hrefs = new Map(derived.map((pg) => [pg.id, `./${sheet(pg.id)}`]));
1458
1465
  for (const pg of derived) {
1459
- const notice = manifest ? evidenceNotice(manifest, pg.id, lang) : [];
1460
- writeFileSync(
1461
- join(outDir, sheet(pg.id)),
1462
- `${renderPageDocument(result, pg, { standard, lang, screenshots: shots, ...evidenceOpts, ...(notice.length ? { evidenceNotice: notice } : {}) })}\n`,
1463
- );
1466
+ writeFileSync(join(outDir, sheet(pg.id)), `${renderPageDocument(result, pg, { standard, lang, screenshots: shots, ...evidenceOpts })}\n`);
1464
1467
  }
1465
1468
  const index = join(outDir, "index.md");
1466
1469
  writeFileSync(index, `${renderPagesIndex(result, derived, { standard, lang, hrefs })}\n`);
@@ -1480,14 +1483,32 @@ async function cmdPages(p: ParsedArgs): Promise<number> {
1480
1483
  return html?.imagesDropped ? 1 : 0;
1481
1484
  }
1482
1485
 
1483
- /** `--inline-budget` in bytes, or undefined for the module default. Rejected loudly rather
1484
- * than coerced: silently falling back to 12 MB on a typo is how a run appears to honour a
1485
- * budget it never read. */
1486
- function budgetOf(p: ParsedArgs): number | undefined {
1487
- const raw = p.flags["inline-budget"];
1486
+ /** A positive integer flag, or undefined for the module default. A malformed value is
1487
+ * REPORTED, never coerced: silently falling back to the default on a typo is how a run
1488
+ * appears to honour a limit it never read — the same class of defect as an input the
1489
+ * action declares and no step passes. */
1490
+ function positiveIntFlag(p: ParsedArgs, name: string): number | undefined {
1491
+ const raw = p.flags[name];
1488
1492
  if (typeof raw !== "string" || !raw) return undefined;
1489
1493
  const n = Number.parseInt(raw, 10);
1490
- return Number.isFinite(n) && n > 0 ? n : undefined;
1494
+ if (Number.isFinite(n) && n > 0) return n;
1495
+ console.error(`ultra11y: --${name} expects a positive integer (got "${raw}"). Ignored — the default applies.`);
1496
+ return undefined;
1497
+ }
1498
+
1499
+ /** `--inline-budget` in bytes, or undefined for the module default. */
1500
+ function budgetOf(p: ParsedArgs): number | undefined {
1501
+ return positiveIntFlag(p, "inline-budget");
1502
+ }
1503
+
1504
+ /** `--evidence-max` as the evidence tier's cap override. Only the RUN-WIDE total is exposed:
1505
+ * the per-rule and per-page caps are what make one image stand for a repeated defect, and a
1506
+ * user raising them would get back the 472-pictures-of-one-defect artifact the caps exist to
1507
+ * prevent. What a user actually needs to control is how big the upload gets — that is the
1508
+ * total, and it is the only one that is a size fuse rather than a de-duplication rule. */
1509
+ function evidenceCapsOf(p: ParsedArgs): { caps: { total: number } } | Record<string, never> {
1510
+ const total = positiveIntFlag(p, "evidence-max");
1511
+ return total === undefined ? {} : { caps: { total } };
1491
1512
  }
1492
1513
 
1493
1514
  /** Write the HTML documents and report the budget ladder. Shared by `pages` and `report`, so
@@ -1797,21 +1818,28 @@ async function cmdReport(p: ParsedArgs): Promise<number> {
1797
1818
  return 0;
1798
1819
  }
1799
1820
 
1800
- const path = writeReport(result, { out, lang, standard });
1821
+ // THE EVIDENCE TIER, before anything is rendered — because BOTH deliverables read it. The
1822
+ // crops are written once into `<out>/assets/<page>/`; the Markdown report references them
1823
+ // as files and the composite inlines the same bytes as data: URIs, because only the
1824
+ // composite has to travel alone. Rendering the Markdown first is what left those files
1825
+ // referenced by no document at all while the composite carried a second copy of each.
1826
+ const manifest = p.flags.evidence === true ? writeEvidence(result, { outDir: out, ...evidenceCapsOf(p) }) : undefined;
1827
+ if (manifest) {
1828
+ for (const line of evidenceNotice(manifest, null, lang)) console.error(line);
1829
+ }
1830
+ const cropFor = manifest
1831
+ ? (f: Finding) => {
1832
+ const c = manifest.crops.get(findingId(f));
1833
+ return c ? { href: c.href, alt: c.alt[lang] } : undefined;
1834
+ }
1835
+ : undefined;
1836
+
1837
+ const path = writeReport(result, { out, lang, standard, ...(cropFor ? { cropFor } : {}) });
1801
1838
  // The HTML tier: the artifact's front door plus the detachable, printable composite. Written
1802
1839
  // beside the Markdown, in the same `--out`, so the directory stays one self-contained root.
1803
1840
  let html: ReturnType<typeof writeHtml> | undefined;
1804
1841
  if (p.flags.html === true) {
1805
- const manifest = p.flags.evidence === true ? writeEvidence(result, { outDir: out }) : undefined;
1806
- if (manifest) for (const line of evidenceNotice(manifest, null, lang)) console.error(line);
1807
1842
  html = emitHtml(result, { outDir: out, standard, lang, ...(manifest ? { evidence: manifest } : {}), inlineBudget: budgetOf(p) });
1808
- } else if (p.flags.evidence === true) {
1809
- console.error(
1810
- lang === "fr"
1811
- ? "ultra11y report : --evidence n'illustre que le rapport HTML. Ajoutez --html, ou utilisez `pages --format report --evidence` pour des fiches Markdown illustrées."
1812
- : "ultra11y report: --evidence only illustrates the HTML report. Add --html, or use `pages --format report --evidence` for illustrated Markdown sheets.",
1813
- );
1814
- return 2;
1815
1843
  }
1816
1844
  // Partial-audit advisory (owner decision): a pack (RGAA) report whose scan coverage
1817
1845
  // leaves needs-rendering criteria untested — warn prominently on the CLI, naming exactly