ultra11y 2.22.0 → 2.24.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.
Files changed (43) hide show
  1. package/README.md +93 -9
  2. package/package.json +6 -2
  3. package/scripts/build-pack-rgaa.mjs +95 -1
  4. package/scripts/build-standards.mjs +5 -1
  5. package/scripts/ultra11y.mjs +2920 -320
  6. package/skills/review-a11y/SKILL.md +1 -1
  7. package/skills/review-a11y/scripts/ultra11y.mjs +2920 -320
  8. package/skills/ultra11y/SKILL.md +38 -2
  9. package/skills/ultra11y/references/ci.md +165 -0
  10. package/skills/ultra11y/references/criteria.md +5 -5
  11. package/skills/ultra11y/references/devtools.md +67 -0
  12. package/skills/ultra11y/references/e2e.md +112 -0
  13. package/skills/ultra11y/references/judgment.md +25 -0
  14. package/skills/ultra11y/references/pages.md +205 -0
  15. package/skills/ultra11y/scripts/ultra11y.mjs +2920 -320
  16. package/src/adjudicate.ts +270 -45
  17. package/src/annotate.ts +158 -0
  18. package/src/audit.ts +8 -1
  19. package/src/capture.ts +4 -0
  20. package/src/cli.ts +485 -14
  21. package/src/criteria.ts +58 -0
  22. package/src/data/standards/rgaa.json +155 -30
  23. package/src/data/wcag.json +5 -5
  24. package/src/dev.ts +427 -0
  25. package/src/e2e.ts +303 -0
  26. package/src/gh.ts +72 -0
  27. package/src/init.ts +39 -7
  28. package/src/messages.ts +87 -0
  29. package/src/orchestrate-templates.ts +12 -4
  30. package/src/pages.ts +283 -0
  31. package/src/parse/html.ts +12 -0
  32. package/src/pixel.ts +217 -0
  33. package/src/prd.ts +37 -0
  34. package/src/report.ts +28 -2
  35. package/src/rules/registry.ts +2 -0
  36. package/src/rules/rendered.ts +379 -0
  37. package/src/rules/rule.ts +3 -0
  38. package/src/sarif.ts +196 -0
  39. package/src/scan-local.ts +4 -0
  40. package/src/snapshot.ts +442 -0
  41. package/src/standards/derive.ts +32 -1
  42. package/src/types.ts +161 -1
  43. package/src/util.ts +18 -0
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { realpathSync as realpathSync4, writeFileSync as writeFileSync13, mkdirSync as mkdirSync10, existsSync as existsSync21, readFileSync as readFileSync17, appendFileSync } from "fs";
5
- import { join as join36, relative as relative3, sep as sep6, dirname as dirname10 } from "path";
4
+ import { realpathSync as realpathSync4, writeFileSync as writeFileSync14, mkdirSync as mkdirSync11, existsSync as existsSync22, readFileSync as readFileSync19, appendFileSync } from "fs";
5
+ import { join as join38, relative as relative3, sep as sep6, dirname as dirname11 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "2.22.0";
9
+ var VERSION = "2.24.0";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -385,7 +385,7 @@ var wcag_default = {
385
385
  level: "AA",
386
386
  addedIn: "2.1",
387
387
  automatability: "needs-rendering",
388
- ruleIds: [],
388
+ ruleIds: ["rendered-orientation-lock"],
389
389
  understanding: "https://www.w3.org/WAI/WCAG22/Understanding/orientation.html"
390
390
  },
391
391
  {
@@ -410,7 +410,7 @@ var wcag_default = {
410
410
  level: "A",
411
411
  addedIn: "2.0",
412
412
  automatability: "needs-rendering",
413
- ruleIds: [],
413
+ ruleIds: ["rendered-link-colour-only"],
414
414
  understanding: "https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html",
415
415
  techniques: ["C15", "F14", "F26", "F73", "F78", "G111", "G117", "G138", "G14", "G140", "G149", "G165", "G182", "G183", "G195", "G205", "G96", "SCR31"]
416
416
  },
@@ -436,7 +436,7 @@ var wcag_default = {
436
436
  level: "AA",
437
437
  addedIn: "2.0",
438
438
  automatability: "needs-rendering",
439
- ruleIds: ["contrast-literal"],
439
+ ruleIds: ["contrast-literal", "rendered-contrast", "rendered-contrast-pixel"],
440
440
  understanding: "https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html",
441
441
  techniques: ["C29", "F24", "G136", "G145", "G148", "G174", "G18"]
442
442
  },
@@ -488,7 +488,7 @@ var wcag_default = {
488
488
  level: "AA",
489
489
  addedIn: "2.1",
490
490
  automatability: "needs-rendering",
491
- ruleIds: [],
491
+ ruleIds: ["rendered-nontext-contrast"],
492
492
  understanding: "https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html",
493
493
  techniques: ["F78", "G145", "G174", "G18", "G183", "G195", "G207"]
494
494
  },
@@ -745,7 +745,7 @@ var wcag_default = {
745
745
  level: "AA",
746
746
  addedIn: "2.0",
747
747
  automatability: "needs-rendering",
748
- ruleIds: [],
748
+ ruleIds: ["rendered-focus-not-visible"],
749
749
  understanding: "https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html",
750
750
  techniques: ["C15", "F42", "F54", "F55", "F73", "F78", "G149", "G165", "G183", "G195", "G202", "G90", "SCR2", "SCR20", "SCR29", "SCR31", "SCR35"]
751
751
  },
@@ -1770,6 +1770,9 @@ function scsByGuideline() {
1770
1770
  function techniques(id) {
1771
1771
  return byId.get(id)?.techniques ?? [];
1772
1772
  }
1773
+ function understanding(id) {
1774
+ return byId.get(id)?.understanding;
1775
+ }
1773
1776
  function meta() {
1774
1777
  return {
1775
1778
  wcagVersion: data.wcagVersion,
@@ -1779,6 +1782,15 @@ function meta() {
1779
1782
  criteriaSource: data.criteriaSource
1780
1783
  };
1781
1784
  }
1785
+ function compareSC(a, b) {
1786
+ const pa = a.split(".").map(Number);
1787
+ const pb = b.split(".").map(Number);
1788
+ for (let i2 = 0; i2 < Math.max(pa.length, pb.length); i2++) {
1789
+ const d = (pa[i2] ?? 0) - (pb[i2] ?? 0);
1790
+ if (d) return d;
1791
+ }
1792
+ return 0;
1793
+ }
1782
1794
  function techniquesFor(id) {
1783
1795
  return getSC(id)?.techniques ?? [];
1784
1796
  }
@@ -10708,12 +10720,12 @@ var Tokenizer2 = class extends CommentsParser {
10708
10720
  continue;
10709
10721
  }
10710
10722
  ++this.state.pos;
10711
- const esc = this.readCodePoint(true);
10712
- if (esc !== null) {
10713
- if (!identifierCheck(esc)) {
10723
+ const esc3 = this.readCodePoint(true);
10724
+ if (esc3 !== null) {
10725
+ if (!identifierCheck(esc3)) {
10714
10726
  this.raise(Errors.EscapedCharNotAnIdentifier, escStart);
10715
10727
  }
10716
- word += String.fromCodePoint(esc);
10728
+ word += String.fromCodePoint(esc3);
10717
10729
  }
10718
10730
  chunkStart = this.state.pos;
10719
10731
  } else {
@@ -18794,6 +18806,15 @@ function readText(path) {
18794
18806
  function ext(path) {
18795
18807
  return extname(path).toLowerCase();
18796
18808
  }
18809
+ function isUrlPath(file) {
18810
+ return /^https?:\/\//i.test(file);
18811
+ }
18812
+ function repoRelative(file, baseDir) {
18813
+ const posix3 = file.split("\\").join("/").replace(/^\.\//, "");
18814
+ const base = baseDir.split("\\").join("/").replace(/\/+$/, "");
18815
+ if (base && posix3.startsWith(`${base}/`)) return posix3.slice(base.length + 1);
18816
+ return posix3;
18817
+ }
18797
18818
  async function readStdin() {
18798
18819
  const chunks = [];
18799
18820
  for await (const chunk of process.stdin) chunks.push(chunk);
@@ -31494,13 +31515,13 @@ function computeDelta(graph, symbols, diff, depth = DEFAULT_DELTA_DEPTH) {
31494
31515
  const shown = exportedNames.slice(0, 3).join(", ") + (exportedNames.length > 3 ? ", \u2026" : "");
31495
31516
  reasons.push(exportedNames.length === 1 ? `exported symbol ${shown} changed` : `exported symbols ${shown} changed`);
31496
31517
  }
31497
- const pct = percentile(metricValues, metricOf(m));
31498
- if (pct >= 0.9) {
31518
+ const pct2 = percentile(metricValues, metricOf(m));
31519
+ if (pct2 >= 0.9) {
31499
31520
  score += RISK_WEIGHTS.hubHigh;
31500
- reasons.push(`${metricName} p${Math.round(pct * 100)} hub`);
31501
- } else if (pct >= 0.75) {
31521
+ reasons.push(`${metricName} p${Math.round(pct2 * 100)} hub`);
31522
+ } else if (pct2 >= 0.75) {
31502
31523
  score += RISK_WEIGHTS.hubMed;
31503
- reasons.push(`${metricName} p${Math.round(pct * 100)} hub`);
31524
+ reasons.push(`${metricName} p${Math.round(pct2 * 100)} hub`);
31504
31525
  }
31505
31526
  const depthByRel = /* @__PURE__ */ new Map();
31506
31527
  const impModules = /* @__PURE__ */ new Set();
@@ -32456,6 +32477,8 @@ function formatCaptureComment(prov) {
32456
32477
  if (prov.component) attrs.push(`component="${escapeCommentValue(prov.component)}"`);
32457
32478
  if (prov.test) attrs.push(`test="${escapeCommentValue(prov.test)}"`);
32458
32479
  if (prov.name) attrs.push(`name="${escapeCommentValue(prov.name)}"`);
32480
+ if (prov.page) attrs.push(`page="${escapeCommentValue(prov.page)}"`);
32481
+ if (prov.url) attrs.push(`url="${escapeCommentValue(prov.url)}"`);
32459
32482
  return `<!-- ultra11y:capture ${attrs.join(" ")} -->`;
32460
32483
  }
32461
32484
  var SCAN_LIMIT = 4096;
@@ -32478,7 +32501,9 @@ function parseCaptureProvenance(source) {
32478
32501
  ...kv.has("source") ? { sourceFile: kv.get("source") } : {},
32479
32502
  ...kv.has("component") ? { component: kv.get("component") } : {},
32480
32503
  ...kv.has("test") ? { test: kv.get("test") } : {},
32481
- ...kv.has("name") ? { name: kv.get("name") } : {}
32504
+ ...kv.has("name") ? { name: kv.get("name") } : {},
32505
+ ...kv.has("page") ? { page: kv.get("page") } : {},
32506
+ ...kv.has("url") ? { url: kv.get("url") } : {}
32482
32507
  };
32483
32508
  }
32484
32509
  function pathMatch(a, b) {
@@ -32579,6 +32604,271 @@ function parseSourceWithAst(source, file, opts = {}) {
32579
32604
  return { doc, ast: null };
32580
32605
  }
32581
32606
 
32607
+ // src/snapshot.ts
32608
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync10, readdirSync as readdirSync4, writeFileSync as writeFileSync5 } from "fs";
32609
+ import { dirname as dirname5, join as join21 } from "path";
32610
+ var SNAPSHOT_VERSION = 1;
32611
+ var PAGES_DIR = ".ultra11y/pages";
32612
+ var ID_RE = /^[a-z0-9][a-z0-9-]*$/i;
32613
+ function validateSnapshotMeta(raw) {
32614
+ const issues = [];
32615
+ const err2 = (path, message) => {
32616
+ issues.push({ path, message });
32617
+ };
32618
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
32619
+ err2("meta", "meta must be an object { v, id, name, url, \u2026 }");
32620
+ return { ok: false, issues };
32621
+ }
32622
+ const m = raw;
32623
+ const v = typeof m.v === "number" ? m.v : Number.NaN;
32624
+ if (!Number.isFinite(v) || v < 1) err2("meta.v", "v must be a positive integer snapshot-format version");
32625
+ else if (v > SNAPSHOT_VERSION) err2("meta.v", `snapshot format v${v} is newer than this engine understands (v${SNAPSHOT_VERSION}) \u2014 upgrade ultra11y`);
32626
+ if (typeof m.id !== "string" || !m.id.trim()) err2("meta.id", "id must be a non-empty string");
32627
+ else if (!ID_RE.test(m.id)) err2("meta.id", `id "${m.id}" must match ${ID_RE} (it becomes a directory name)`);
32628
+ if (typeof m.name !== "string" || !m.name.trim()) err2("meta.name", "name must be a non-empty string");
32629
+ if (typeof m.url !== "string" || !m.url.trim()) err2("meta.url", "url must be a non-empty string");
32630
+ if (m.auth !== void 0 && typeof m.auth !== "boolean") err2("meta.auth", "auth must be a boolean");
32631
+ if (m.route !== void 0 && typeof m.route !== "string") err2("meta.route", "route must be a string");
32632
+ if (m.notes !== void 0 && typeof m.notes !== "string") err2("meta.notes", "notes must be a string");
32633
+ if (m.sources !== void 0 && (!Array.isArray(m.sources) || m.sources.some((s) => typeof s !== "string")))
32634
+ err2("meta.sources", "sources must be an array of strings");
32635
+ if (issues.length) return { ok: false, issues };
32636
+ return {
32637
+ ok: true,
32638
+ issues,
32639
+ meta: {
32640
+ v,
32641
+ id: m.id,
32642
+ name: m.name,
32643
+ url: m.url,
32644
+ ...typeof m.route === "string" ? { route: m.route } : {},
32645
+ ...typeof m.auth === "boolean" ? { auth: m.auth } : {},
32646
+ ...m.viewport && typeof m.viewport === "object" ? { viewport: m.viewport } : {},
32647
+ ...typeof m.capturedAt === "string" ? { capturedAt: m.capturedAt } : {},
32648
+ ...typeof m.runner === "string" ? { runner: m.runner } : {},
32649
+ ...Array.isArray(m.sources) ? { sources: m.sources } : {},
32650
+ ...typeof m.notes === "string" ? { notes: m.notes } : {}
32651
+ }
32652
+ };
32653
+ }
32654
+ function snapshotDir(root, id) {
32655
+ return join21(root, PAGES_DIR, id);
32656
+ }
32657
+ function writeSnapshot(root, snap) {
32658
+ const dir = snapshotDir(root, snap.meta.id);
32659
+ mkdirSync4(dir, { recursive: true });
32660
+ writeFileSync5(join21(dir, "meta.json"), `${JSON.stringify(snap.meta, null, 2)}
32661
+ `);
32662
+ const comment = formatCaptureComment({
32663
+ v: 1,
32664
+ page: snap.meta.id,
32665
+ url: snap.meta.url,
32666
+ ...snap.meta.sources?.[0] ? { sourceFile: snap.meta.sources[0] } : {},
32667
+ name: snap.meta.name
32668
+ });
32669
+ writeFileSync5(join21(dir, "dom.html"), `${comment}
32670
+ ${snap.dom}
32671
+ `);
32672
+ if (snap.styles) writeFileSync5(join21(dir, "styles.json"), `${JSON.stringify(snap.styles)}
32673
+ `);
32674
+ if (snap.boxes) writeFileSync5(join21(dir, "boxes.json"), `${JSON.stringify(snap.boxes)}
32675
+ `);
32676
+ if (snap.axtree) writeFileSync5(join21(dir, "axtree.json"), `${JSON.stringify(snap.axtree)}
32677
+ `);
32678
+ if (snap.css) writeFileSync5(join21(dir, "css.json"), `${JSON.stringify(snap.css)}
32679
+ `);
32680
+ return dir;
32681
+ }
32682
+ function readJson2(file) {
32683
+ try {
32684
+ return JSON.parse(readFileSync10(file, "utf8"));
32685
+ } catch {
32686
+ return void 0;
32687
+ }
32688
+ }
32689
+ function readSnapshot(dir) {
32690
+ const rawMeta = readJson2(join21(dir, "meta.json"));
32691
+ if (rawMeta === void 0) return null;
32692
+ const v = validateSnapshotMeta(rawMeta);
32693
+ if (!v.ok || !v.meta) return null;
32694
+ let dom;
32695
+ try {
32696
+ dom = readFileSync10(join21(dir, "dom.html"), "utf8");
32697
+ } catch {
32698
+ return null;
32699
+ }
32700
+ const styles = readJson2(join21(dir, "styles.json"));
32701
+ const boxes = readJson2(join21(dir, "boxes.json"));
32702
+ const axtree = readJson2(join21(dir, "axtree.json"));
32703
+ const css = readJson2(join21(dir, "css.json"));
32704
+ const shot = join21(dir, "screen.png");
32705
+ return {
32706
+ meta: v.meta,
32707
+ dom,
32708
+ ...styles ? { styles } : {},
32709
+ ...boxes ? { boxes } : {},
32710
+ ...axtree ? { axtree } : {},
32711
+ ...css ? { css } : {},
32712
+ ...existsSync9(shot) ? { screenshot: "screen.png" } : {}
32713
+ };
32714
+ }
32715
+ function readSnapshots(root) {
32716
+ const base = join21(root, PAGES_DIR);
32717
+ let dirs;
32718
+ try {
32719
+ dirs = readdirSync4(base, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
32720
+ } catch {
32721
+ return [];
32722
+ }
32723
+ const out2 = [];
32724
+ for (const d of dirs.sort()) {
32725
+ const s = readSnapshot(join21(base, d));
32726
+ if (s) out2.push(s);
32727
+ }
32728
+ return out2;
32729
+ }
32730
+ var COLLECTED_CSS = [
32731
+ "color",
32732
+ "backgroundColor",
32733
+ "backgroundImage",
32734
+ "fontSize",
32735
+ "fontWeight",
32736
+ "fontStyle",
32737
+ "textDecorationLine",
32738
+ "textTransform",
32739
+ "lineHeight",
32740
+ "letterSpacing",
32741
+ "wordSpacing",
32742
+ "whiteSpace",
32743
+ "display",
32744
+ "visibility",
32745
+ "opacity",
32746
+ "position",
32747
+ "overflowX",
32748
+ "overflowY",
32749
+ "outlineStyle",
32750
+ "outlineWidth",
32751
+ "outlineColor",
32752
+ // All four borders + the box shadow: WCAG 1.4.11 asks whether a control's BOUNDARY is
32753
+ // perceivable, and a boundary can be drawn by any side, by the fill, by an outline or by a
32754
+ // shadow. Collecting only one side would manufacture non-conformities on the other three.
32755
+ "borderTopStyle",
32756
+ "borderRightStyle",
32757
+ "borderBottomStyle",
32758
+ "borderLeftStyle",
32759
+ "borderTopWidth",
32760
+ "borderRightWidth",
32761
+ "borderBottomWidth",
32762
+ "borderLeftWidth",
32763
+ "borderTopColor",
32764
+ "borderRightColor",
32765
+ "borderBottomColor",
32766
+ "borderLeftColor",
32767
+ "boxShadow",
32768
+ "cursor"
32769
+ ];
32770
+ var COLLECT_MAX_ELEMENTS = 5e3;
32771
+ var COLLECT_SNAPSHOT = `(() => {
32772
+ const PROPS = ${JSON.stringify(COLLECTED_CSS)};
32773
+ const MAX = ${COLLECT_MAX_ELEMENTS};
32774
+ const els = document.querySelectorAll('*');
32775
+ const styles = [];
32776
+ const boxes = [];
32777
+ const n = Math.min(els.length, MAX);
32778
+ for (let i = 0; i < n; i++) {
32779
+ const el = els[i];
32780
+ const tag = el.tagName.toLowerCase();
32781
+ const cs = getComputedStyle(el);
32782
+ const css = {};
32783
+ for (const p of PROPS) {
32784
+ const v = cs[p];
32785
+ if (v !== undefined && v !== null && v !== '') css[p] = String(v);
32786
+ }
32787
+ styles.push({ i: i, tag: tag, css: css });
32788
+ const r = el.getBoundingClientRect();
32789
+ boxes.push({ i: i, tag: tag, x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) });
32790
+ }
32791
+ // The page's own stylesheets. Some criteria are properties of the STYLESHEET rather than of
32792
+ // any element's computed style \u2014 focus styling removed with no replacement (2.4.7), a media
32793
+ // query that locks the orientation (1.3.4) \u2014 and this is the only place they exist.
32794
+ // A cross-origin sheet throws on .cssRules: COUNT those instead of ignoring them, so a rule
32795
+ // reading this digest can tell "nothing there" apart from "I could not look".
32796
+ const cssRules = [];
32797
+ let unreadable = 0;
32798
+ const MAXR = 4000;
32799
+ const camel = (p) => p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
32800
+ const walk = (list, media) => {
32801
+ for (const r of list) {
32802
+ if (cssRules.length >= MAXR) return;
32803
+ if (typeof r.selectorText === 'string' && r.style) {
32804
+ const decls = {};
32805
+ for (let i = 0; i < r.style.length; i++) {
32806
+ const prop = r.style[i];
32807
+ decls[camel(prop)] = r.style.getPropertyValue(prop);
32808
+ }
32809
+ cssRules.push(media ? { selector: r.selectorText, media: media, decls: decls } : { selector: r.selectorText, decls: decls });
32810
+ } else if (r.cssRules) {
32811
+ const cond = r.conditionText || (r.media && r.media.mediaText) || media;
32812
+ walk(r.cssRules, cond);
32813
+ }
32814
+ }
32815
+ };
32816
+ for (const sheet of document.styleSheets) {
32817
+ try {
32818
+ walk(sheet.cssRules, undefined);
32819
+ } catch (e) {
32820
+ unreadable++;
32821
+ }
32822
+ }
32823
+
32824
+ const truncated = els.length > MAX;
32825
+ return {
32826
+ dom: document.documentElement.outerHTML,
32827
+ css: { v: 1, rules: cssRules, unreadable: unreadable, truncated: cssRules.length >= MAXR },
32828
+ title: document.title,
32829
+ lang: document.documentElement.getAttribute('lang') || '',
32830
+ url: location.href,
32831
+ viewport: { width: window.innerWidth, height: window.innerHeight },
32832
+ styles: { v: 1, entries: styles, truncated: truncated },
32833
+ boxes: { v: 1, entries: boxes, truncated: truncated }
32834
+ };
32835
+ })()`;
32836
+ function isSnapshotDom(file) {
32837
+ const posix3 = file.split("\\").join("/");
32838
+ return posix3.endsWith("/dom.html") && posix3.includes(`${PAGES_DIR}/`);
32839
+ }
32840
+ function align(doc, entries) {
32841
+ const out2 = /* @__PURE__ */ new Map();
32842
+ for (const e of entries) {
32843
+ const el = doc.elements[e.i];
32844
+ if (!el || el.tag !== e.tag) return null;
32845
+ out2.set(e.i, e);
32846
+ }
32847
+ return out2;
32848
+ }
32849
+ function attachSignals(doc) {
32850
+ if (!isSnapshotDom(doc.file)) return;
32851
+ const dir = dirname5(doc.file);
32852
+ const styles = readJson2(join21(dir, "styles.json"));
32853
+ const boxes = readJson2(join21(dir, "boxes.json"));
32854
+ const axtree = readJson2(join21(dir, "axtree.json"));
32855
+ const css = readJson2(join21(dir, "css.json"));
32856
+ const shot = join21(dir, "screen.png");
32857
+ const alignedStyleMap = styles ? align(doc, styles.entries) : null;
32858
+ const alignedBoxMap = boxes ? align(doc, boxes.entries) : null;
32859
+ const truncated = Boolean(styles?.truncated || boxes?.truncated);
32860
+ const signals = {
32861
+ ...alignedStyleMap ? { styles: alignedStyleMap } : {},
32862
+ ...alignedBoxMap ? { boxes: alignedBoxMap } : {},
32863
+ ...axtree ? { axtree } : {},
32864
+ // Not element-indexed, so it needs no alignment — it is a property of the stylesheet.
32865
+ ...css ? { css } : {},
32866
+ ...existsSync9(shot) ? { screenshot: shot } : {},
32867
+ ...truncated ? { truncated } : {}
32868
+ };
32869
+ if (Object.keys(signals).length) doc.signals = signals;
32870
+ }
32871
+
32582
32872
  // src/messages.ts
32583
32873
  var MSG_CATALOG = {
32584
32874
  // ---- Theme 1 — Images (src/rules/images.ts) ---------------------------------
@@ -32824,6 +33114,69 @@ var MSG_CATALOG = {
32824
33114
  en: (p) => `Darken the text or lighten the background to reach at least ${p.min}:1 (contrast computed from literal inline colours).`
32825
33115
  }
32826
33116
  },
33117
+ // ---- Rendered tier (src/rules/rendered.ts) — decided from a page snapshot's signals -----
33118
+ // These say WHERE the measurement came from, because that is what makes them auditable: a
33119
+ // computed style, or a region of the screenshot. An auditor can go and re-check either.
33120
+ "rendered-contrast": {
33121
+ message: {
33122
+ fr: (p) => `Contraste insuffisant au rendu : ratio ${p.ratio}:1 entre le texte et son fond effectif (minimum ${p.min}:1 pour du texte ${p.textSize === "large" ? "large" : "normal"}).`,
33123
+ en: (p) => `Insufficient contrast at render: ${p.ratio}:1 between the text and its effective background (minimum ${p.min}:1 for ${p.textSize === "large" ? "large" : "normal"} text).`
33124
+ },
33125
+ remediation: {
33126
+ fr: (p) => `Assombrissez le texte ou \xE9claircissez le fond pour atteindre au moins ${p.min}:1 (mesur\xE9 sur les styles calcul\xE9s de la page rendue).`,
33127
+ en: (p) => `Darken the text or lighten the background to reach at least ${p.min}:1 (measured from the rendered page's computed styles).`
33128
+ }
33129
+ },
33130
+ "rendered-contrast-pixel": {
33131
+ message: {
33132
+ fr: (p) => `Contraste insuffisant mesur\xE9 sur la capture d'\xE9cran : ratio ${p.ratio}:1 (minimum ${p.min}:1 pour du texte ${p.textSize === "large" ? "large" : "normal"}). Le fond vient d'une image ou d'un d\xE9grad\xE9, invisible aux styles calcul\xE9s.`,
33133
+ en: (p) => `Insufficient contrast measured on the screenshot: ${p.ratio}:1 (minimum ${p.min}:1 for ${p.textSize === "large" ? "large" : "normal"} text). The background comes from an image or gradient, which computed styles cannot express.`
33134
+ },
33135
+ remediation: {
33136
+ fr: (p) => `Garantissez au moins ${p.min}:1 sur toute la zone : ajoutez un fond opaque sous le texte, un voile, ou une ombre port\xE9e. Le fond a \xE9t\xE9 mesur\xE9 sur les pixels de la zone occup\xE9e par le texte.`,
33137
+ en: (p) => `Guarantee at least ${p.min}:1 across the whole area: add an opaque backing behind the text, a scrim, or a text shadow. The background was measured on the pixels of the text's own box.`
33138
+ }
33139
+ },
33140
+ "rendered-link-colour-only": {
33141
+ message: {
33142
+ fr: (p) => `Lien dans un bloc de texte distingu\xE9 par la COULEUR SEULE (\xE9cart de ${p.ratio}:1 avec le texte environnant, sans soulignement, bordure ni fond) \u2014 invisible pour qui ne per\xE7oit pas cette diff\xE9rence de couleur.`,
33143
+ en: (p) => `Link inside a text block distinguished by COLOUR ALONE (${p.ratio}:1 apart from the surrounding text, with no underline, border or background) \u2014 invisible to anyone who does not perceive that colour difference.`
33144
+ },
33145
+ remediation: {
33146
+ fr: () => `Ajoutez un indice non chromatique : text-decoration: underline (le plus simple), une bordure basse, ou une graisse nettement sup\xE9rieure. \xC0 d\xE9faut, un contraste d'au moins 3:1 avec le texte environnant ET un indice au survol/focus.`,
33147
+ en: () => `Add a non-colour cue: text-decoration: underline (simplest), a bottom border, or a distinctly heavier weight. Failing that, at least 3:1 contrast with the surrounding text AND a cue on hover/focus.`
33148
+ }
33149
+ },
33150
+ "rendered-nontext-contrast": {
33151
+ message: {
33152
+ fr: (p) => `Limite du composant <${p.control}> non perceptible : ni son fond (${p.ratio}:1 avec l'environnement) ni sa bordure n'atteignent 3:1 \u2014 rien n'indique visuellement o\xF9 commence le champ.`,
33153
+ en: (p) => `The <${p.control}> boundary is not perceivable: neither its fill (${p.ratio}:1 against its surroundings) nor its border reaches 3:1 \u2014 nothing shows visually where the control begins.`
33154
+ },
33155
+ remediation: {
33156
+ fr: () => `Donnez au composant une bordure visible contrastant \xE0 3:1 avec le fond environnant, ou un fond distinct du fond environnant \xE0 3:1. Un box-shadow ou un outline visible conviennent aussi.`,
33157
+ en: () => `Give the control a visible border at 3:1 against the surrounding background, or a fill distinct from it at 3:1. A visible box-shadow or outline also qualifies.`
33158
+ }
33159
+ },
33160
+ "rendered-focus-not-visible": {
33161
+ message: {
33162
+ fr: (p) => `La feuille de styles supprime l'indicateur de focus (\`${p.selector}\`) et aucune r\xE8gle \`:focus\` ne le remplace \u2014 un utilisateur au clavier ne peut plus savoir o\xF9 il se trouve.`,
33163
+ en: (p) => `The stylesheet removes the focus indicator (\`${p.selector}\`) and no \`:focus\` rule puts one back \u2014 a keyboard user can no longer tell where they are.`
33164
+ },
33165
+ remediation: {
33166
+ fr: () => `Ne supprimez jamais outline sans remplacement. Si le style par d\xE9faut ne convient pas, remplacez-le : \`:focus-visible { outline: 2px solid <couleur contrast\xE9e>; outline-offset: 2px }\`.`,
33167
+ en: () => `Never remove outline without a replacement. If the default does not fit, replace it: \`:focus-visible { outline: 2px solid <contrasting colour>; outline-offset: 2px }\`.`
33168
+ }
33169
+ },
33170
+ "rendered-orientation-lock": {
33171
+ message: {
33172
+ fr: (p) => `Le contenu est verrouill\xE9 dans une seule orientation : \`@media ${p.media}\` applique \`transform: ${p.transform}\` au document \u2014 l'utilisateur ne peut pas consulter la page dans l'orientation de son choix.`,
33173
+ en: (p) => `Content is locked to a single orientation: \`@media ${p.media}\` applies \`transform: ${p.transform}\` to the document \u2014 the user cannot view the page in the orientation they need.`
33174
+ },
33175
+ remediation: {
33176
+ fr: () => `Supprimez la rotation et rendez la mise en page responsive dans les deux orientations. Un verrouillage n'est admis que si l'orientation est essentielle (piano, ch\xE8que bancaire).`,
33177
+ en: () => `Remove the rotation and make the layout work in both orientations. Locking is only acceptable where the orientation is essential (a piano keyboard, a bank cheque).`
33178
+ }
33179
+ },
32827
33180
  // ---- Theme 5 — Data tables (src/rules/tables.ts) ----------------------------
32828
33181
  "data-table-no-headers.no-th": {
32829
33182
  message: {
@@ -33566,6 +33919,9 @@ function toFinding(doc, ruleId, def, rf, ruleAdvisory) {
33566
33919
  // Capture findings are rendered ground truth (NOT preliminary): re-attribute them
33567
33920
  // to the source component recorded in the capture's provenance.
33568
33921
  ...doc.capture ? { origin: { capture: doc.file, sourceFile: doc.capture.sourceFile, component: doc.capture.component } } : {},
33922
+ // A PAGE snapshot's capture carries which page it is; stamp it so the per-page grid can
33923
+ // key on the finding directly (src/pages.ts). Component captures carry no page.
33924
+ ...doc.capture?.page ? { page: doc.capture.page } : {},
33569
33925
  // Non-normative recommendation: the per-finding override (rf.advisory) wins over the
33570
33926
  // rule-level default (ruleAdvisory); only stamp the flag when it is actually true, so
33571
33927
  // a normative finding stays byte-identical (no `advisory: false` noise in the JSON).
@@ -36282,6 +36638,422 @@ var contrastLiteral = {
36282
36638
  };
36283
36639
  var colorsRules = [contrastLiteral];
36284
36640
 
36641
+ // src/rules/rendered.ts
36642
+ import { readFileSync as readFileSync11 } from "fs";
36643
+
36644
+ // src/pixel.ts
36645
+ import { inflateSync } from "zlib";
36646
+ var SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
36647
+ function paeth(a, b, c2) {
36648
+ const p = a + b - c2;
36649
+ const pa = Math.abs(p - a);
36650
+ const pb = Math.abs(p - b);
36651
+ const pc = Math.abs(p - c2);
36652
+ if (pa <= pb && pa <= pc) return a;
36653
+ return pb <= pc ? b : c2;
36654
+ }
36655
+ function decodePng(buf) {
36656
+ if (buf.length < 8 + 25) return null;
36657
+ for (let i2 = 0; i2 < SIGNATURE.length; i2++) if (buf[i2] !== SIGNATURE[i2]) return null;
36658
+ let width = 0;
36659
+ let height = 0;
36660
+ let bitDepth = 0;
36661
+ let colorType = 0;
36662
+ let interlace = 0;
36663
+ const idat = [];
36664
+ let off = 8;
36665
+ while (off + 8 <= buf.length) {
36666
+ const len = buf.readUInt32BE(off);
36667
+ const type = buf.toString("ascii", off + 4, off + 8);
36668
+ const dataStart = off + 8;
36669
+ const dataEnd = dataStart + len;
36670
+ if (dataEnd + 4 > buf.length) return null;
36671
+ if (type === "IHDR") {
36672
+ if (len < 13) return null;
36673
+ width = buf.readUInt32BE(dataStart);
36674
+ height = buf.readUInt32BE(dataStart + 4);
36675
+ bitDepth = buf[dataStart + 8] ?? 0;
36676
+ colorType = buf[dataStart + 9] ?? 0;
36677
+ interlace = buf[dataStart + 12] ?? 0;
36678
+ } else if (type === "IDAT") {
36679
+ idat.push(buf.subarray(dataStart, dataEnd));
36680
+ } else if (type === "IEND") {
36681
+ break;
36682
+ }
36683
+ off = dataEnd + 4;
36684
+ }
36685
+ if (!width || !height || !idat.length) return null;
36686
+ if (bitDepth !== 8) return null;
36687
+ if (colorType !== 2 && colorType !== 6) return null;
36688
+ if (interlace !== 0) return null;
36689
+ const channels = colorType === 6 ? 4 : 3;
36690
+ let raw;
36691
+ try {
36692
+ raw = inflateSync(Buffer.concat(idat));
36693
+ } catch {
36694
+ return null;
36695
+ }
36696
+ const stride = width * channels;
36697
+ if (raw.length < height * (stride + 1)) return null;
36698
+ const out2 = Buffer.alloc(height * stride);
36699
+ for (let y = 0; y < height; y++) {
36700
+ const filter = raw[y * (stride + 1)] ?? 0;
36701
+ const lineIn = y * (stride + 1) + 1;
36702
+ const lineOut = y * stride;
36703
+ const prevOut = lineOut - stride;
36704
+ for (let i2 = 0; i2 < stride; i2++) {
36705
+ const x = raw[lineIn + i2] ?? 0;
36706
+ const a = i2 >= channels ? out2[lineOut + i2 - channels] ?? 0 : 0;
36707
+ const b = y > 0 ? out2[prevOut + i2] ?? 0 : 0;
36708
+ const c2 = y > 0 && i2 >= channels ? out2[prevOut + i2 - channels] ?? 0 : 0;
36709
+ let v;
36710
+ switch (filter) {
36711
+ case 0:
36712
+ v = x;
36713
+ break;
36714
+ case 1:
36715
+ v = x + a;
36716
+ break;
36717
+ case 2:
36718
+ v = x + b;
36719
+ break;
36720
+ case 3:
36721
+ v = x + (a + b >> 1);
36722
+ break;
36723
+ case 4:
36724
+ v = x + paeth(a, b, c2);
36725
+ break;
36726
+ default:
36727
+ return null;
36728
+ }
36729
+ out2[lineOut + i2] = v & 255;
36730
+ }
36731
+ }
36732
+ return {
36733
+ width,
36734
+ height,
36735
+ at(x, y) {
36736
+ if (x < 0 || y < 0 || x >= width || y >= height) return void 0;
36737
+ const p = y * stride + x * channels;
36738
+ return {
36739
+ r: out2[p] ?? 0,
36740
+ g: out2[p + 1] ?? 0,
36741
+ b: out2[p + 2] ?? 0,
36742
+ a: channels === 4 ? (out2[p + 3] ?? 255) / 255 : 1
36743
+ };
36744
+ }
36745
+ };
36746
+ }
36747
+ function clamp(img, r) {
36748
+ const x = Math.max(0, Math.round(r.x));
36749
+ const y = Math.max(0, Math.round(r.y));
36750
+ const x2 = Math.min(img.width, Math.round(r.x + r.w));
36751
+ const y2 = Math.min(img.height, Math.round(r.y + r.h));
36752
+ if (x2 <= x || y2 <= y) return null;
36753
+ return { x, y, w: x2 - x, h: y2 - y };
36754
+ }
36755
+ var MAX_SAMPLES = 4096;
36756
+ var DOMINANCE = 0.6;
36757
+ var BUCKET = 16;
36758
+ function dominantBackground(img, rect) {
36759
+ const r = clamp(img, rect);
36760
+ if (!r) return null;
36761
+ const step = Math.max(1, Math.floor(Math.sqrt(r.w * r.h / MAX_SAMPLES)));
36762
+ const counts = /* @__PURE__ */ new Map();
36763
+ let total = 0;
36764
+ for (let y = r.y; y < r.y + r.h; y += step) {
36765
+ for (let x = r.x; x < r.x + r.w; x += step) {
36766
+ const p = img.at(x, y);
36767
+ if (!p) continue;
36768
+ const key = Math.round(p.r / BUCKET) << 16 | Math.round(p.g / BUCKET) << 8 | Math.round(p.b / BUCKET);
36769
+ const cur = counts.get(key) ?? { n: 0, r: 0, g: 0, b: 0 };
36770
+ cur.n++;
36771
+ cur.r += p.r;
36772
+ cur.g += p.g;
36773
+ cur.b += p.b;
36774
+ counts.set(key, cur);
36775
+ total++;
36776
+ }
36777
+ }
36778
+ if (!total) return null;
36779
+ let best = null;
36780
+ for (const c2 of counts.values()) if (!best || c2.n > best.n) best = c2;
36781
+ if (!best || best.n / total < DOMINANCE) return null;
36782
+ return { r: Math.round(best.r / best.n), g: Math.round(best.g / best.n), b: Math.round(best.b / best.n), a: 1 };
36783
+ }
36784
+
36785
+ // src/rules/rendered.ts
36786
+ var SKIP_TAGS2 = /* @__PURE__ */ new Set(["script", "style", "head", "title", "meta", "noscript", "link", "html", "br", "wbr"]);
36787
+ function styleAt(doc, i2) {
36788
+ return doc.signals?.styles?.get(i2);
36789
+ }
36790
+ function hasDirectText2(el) {
36791
+ return el.children.some((c2) => c2.type === "text" && c2.data.trim() !== "");
36792
+ }
36793
+ function invisible(css) {
36794
+ if (css.display === "none" || css.visibility === "hidden") return true;
36795
+ const op = Number.parseFloat(css.opacity ?? "1");
36796
+ return Number.isFinite(op) && op === 0;
36797
+ }
36798
+ function backdropOf(doc, index, el) {
36799
+ for (let p = el; p; p = p.parent) {
36800
+ const i2 = index.get(p);
36801
+ if (i2 === void 0) continue;
36802
+ const css = styleAt(doc, i2)?.css;
36803
+ if (!css) continue;
36804
+ const img = css.backgroundImage;
36805
+ if (img && img !== "none") return void 0;
36806
+ const c2 = parseColor(css.backgroundColor ?? "");
36807
+ if (c2 && c2.a >= 1) return { color: c2, fromImage: false };
36808
+ }
36809
+ return void 0;
36810
+ }
36811
+ function isLargeText(css) {
36812
+ const px = Number.parseFloat(css.fontSize ?? "");
36813
+ if (!Number.isFinite(px)) return false;
36814
+ const weight = Number.parseInt(css.fontWeight ?? "400", 10);
36815
+ const bold = Number.isFinite(weight) ? weight >= 700 : css.fontWeight === "bold";
36816
+ return px >= 24 || px >= 18.66 && bold;
36817
+ }
36818
+ function elementIndex(doc) {
36819
+ const m = /* @__PURE__ */ new Map();
36820
+ doc.elements.forEach((el, i2) => m.set(el, i2));
36821
+ return m;
36822
+ }
36823
+ var renderedContrast = {
36824
+ id: "rendered-contrast",
36825
+ criteria: ["1.4.3"],
36826
+ severity: "majeur",
36827
+ run(doc) {
36828
+ if (!doc.signals?.styles) return [];
36829
+ const index = elementIndex(doc);
36830
+ const out2 = [];
36831
+ for (const [el, i2] of index) {
36832
+ if (SKIP_TAGS2.has(el.tag) || !hasDirectText2(el)) continue;
36833
+ const css = styleAt(doc, i2)?.css;
36834
+ if (!css || invisible(css)) continue;
36835
+ const fg = parseColor(css.color ?? "");
36836
+ if (!fg || fg.a < 1) continue;
36837
+ const bd = backdropOf(doc, index, el);
36838
+ if (!bd) continue;
36839
+ const ratio = contrastRatio(fg, bd.color);
36840
+ const large = isLargeText(css);
36841
+ const min = large ? 3 : 4.5;
36842
+ if (ratio >= min) continue;
36843
+ out2.push({
36844
+ criteriaId: "1.4.3",
36845
+ el,
36846
+ msgId: "rendered-contrast",
36847
+ params: { ratio: ratio.toFixed(2), min, textSize: large ? "large" : "normal" }
36848
+ });
36849
+ }
36850
+ return out2;
36851
+ }
36852
+ };
36853
+ var renderedContrastPixel = {
36854
+ id: "rendered-contrast-pixel",
36855
+ criteria: ["1.4.3"],
36856
+ severity: "majeur",
36857
+ run(doc) {
36858
+ const shot = doc.signals?.screenshot;
36859
+ const boxes = doc.signals?.boxes;
36860
+ if (!shot || !boxes || !doc.signals?.styles) return [];
36861
+ let img = null;
36862
+ try {
36863
+ img = decodePng(readFileSync11(shot));
36864
+ } catch {
36865
+ return [];
36866
+ }
36867
+ if (!img) return [];
36868
+ const index = elementIndex(doc);
36869
+ const out2 = [];
36870
+ for (const [el, i2] of index) {
36871
+ if (SKIP_TAGS2.has(el.tag) || !hasDirectText2(el)) continue;
36872
+ const css = styleAt(doc, i2)?.css;
36873
+ if (!css || invisible(css)) continue;
36874
+ if (backdropOf(doc, index, el)) continue;
36875
+ const fg = parseColor(css.color ?? "");
36876
+ if (!fg || fg.a < 1) continue;
36877
+ const box = boxes.get(i2);
36878
+ if (!box || box.w < 4 || box.h < 4) continue;
36879
+ const bg = dominantBackground(img, box);
36880
+ if (!bg) continue;
36881
+ const ratio = contrastRatio(fg, bg);
36882
+ const large = isLargeText(css);
36883
+ const min = large ? 3 : 4.5;
36884
+ if (ratio >= min) continue;
36885
+ out2.push({
36886
+ criteriaId: "1.4.3",
36887
+ el,
36888
+ msgId: "rendered-contrast-pixel",
36889
+ params: { ratio: ratio.toFixed(2), min, textSize: large ? "large" : "normal" }
36890
+ });
36891
+ }
36892
+ return out2;
36893
+ }
36894
+ };
36895
+ function hasNonColourAffordance(css, parentCss) {
36896
+ const deco = css.textDecorationLine ?? "";
36897
+ if (deco && deco !== "none") return true;
36898
+ if ((css.borderBottomStyle ?? "none") !== "none" && Number.parseFloat(css.borderBottomWidth ?? "0") > 0) return true;
36899
+ const bg = parseColor(css.backgroundColor ?? "");
36900
+ if (bg && bg.a > 0) return true;
36901
+ const w = Number.parseInt(css.fontWeight ?? "400", 10);
36902
+ const pw = Number.parseInt(parentCss?.fontWeight ?? "400", 10);
36903
+ if (Number.isFinite(w) && Number.isFinite(pw) && w >= pw + 200) return true;
36904
+ return false;
36905
+ }
36906
+ var TEXT_BLOCK = /* @__PURE__ */ new Set(["p", "li", "dd", "dt", "td", "th", "blockquote", "figcaption", "caption"]);
36907
+ var renderedLinkColourOnly = {
36908
+ id: "rendered-link-colour-only",
36909
+ criteria: ["1.4.1"],
36910
+ severity: "majeur",
36911
+ run(doc) {
36912
+ if (!doc.signals?.styles) return [];
36913
+ const index = elementIndex(doc);
36914
+ const out2 = [];
36915
+ for (const [el, i2] of index) {
36916
+ if (el.tag !== "a" || !hasDirectText2(el)) continue;
36917
+ const parent = el.parent;
36918
+ if (!parent || !TEXT_BLOCK.has(parent.tag)) continue;
36919
+ const css = styleAt(doc, i2)?.css;
36920
+ const pi = index.get(parent);
36921
+ const parentCss = pi === void 0 ? void 0 : styleAt(doc, pi)?.css;
36922
+ if (!css || !parentCss || invisible(css)) continue;
36923
+ if (hasNonColourAffordance(css, parentCss)) continue;
36924
+ const linkColor = parseColor(css.color ?? "");
36925
+ const textColor = parseColor(parentCss.color ?? "");
36926
+ if (!linkColor || !textColor) continue;
36927
+ const distinct = contrastRatio(linkColor, textColor);
36928
+ if (distinct <= 1.02) continue;
36929
+ out2.push({
36930
+ criteriaId: "1.4.1",
36931
+ el,
36932
+ msgId: "rendered-link-colour-only",
36933
+ params: { ratio: distinct.toFixed(2) }
36934
+ });
36935
+ }
36936
+ return out2;
36937
+ }
36938
+ };
36939
+ var BOUNDED_CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea", "button"]);
36940
+ var SIDES = ["Top", "Right", "Bottom", "Left"];
36941
+ function borderColours(css) {
36942
+ const out2 = [];
36943
+ for (const s of SIDES) {
36944
+ const style = css[`border${s}Style`];
36945
+ const width = Number.parseFloat(css[`border${s}Width`] ?? "0");
36946
+ if (!style || style === "none" || style === "hidden" || !(width > 0)) continue;
36947
+ const c2 = parseColor(css[`border${s}Color`] ?? "");
36948
+ if (c2 && c2.a >= 1) out2.push(c2);
36949
+ }
36950
+ return out2;
36951
+ }
36952
+ var renderedNonTextContrast = {
36953
+ id: "rendered-nontext-contrast",
36954
+ criteria: ["1.4.11"],
36955
+ severity: "majeur",
36956
+ run(doc) {
36957
+ if (!doc.signals?.styles) return [];
36958
+ const index = elementIndex(doc);
36959
+ const out2 = [];
36960
+ for (const [el, i2] of index) {
36961
+ if (!BOUNDED_CONTROLS.has(el.tag)) continue;
36962
+ const type = (el.attribs.type ?? "").toLowerCase();
36963
+ if (el.tag === "input" && (type === "hidden" || type === "image")) continue;
36964
+ const css = styleAt(doc, i2)?.css;
36965
+ if (!css || invisible(css)) continue;
36966
+ if (css.boxShadow && css.boxShadow !== "none") continue;
36967
+ if ((css.outlineStyle ?? "none") !== "none" && Number.parseFloat(css.outlineWidth ?? "0") > 0) continue;
36968
+ const surrounding = el.parent ? backdropOf(doc, index, el.parent) : void 0;
36969
+ if (!surrounding) continue;
36970
+ const own = parseColor(css.backgroundColor ?? "");
36971
+ const fill = own && own.a >= 1 ? own : surrounding.color;
36972
+ if (contrastRatio(fill, surrounding.color) >= 3) continue;
36973
+ const borders = borderColours(css);
36974
+ if (borders.some((b) => contrastRatio(b, surrounding.color) >= 3 || contrastRatio(b, fill) >= 3)) continue;
36975
+ out2.push({
36976
+ criteriaId: "1.4.11",
36977
+ el,
36978
+ msgId: "rendered-nontext-contrast",
36979
+ params: { ratio: contrastRatio(fill, surrounding.color).toFixed(2), control: el.tag }
36980
+ });
36981
+ }
36982
+ return out2;
36983
+ }
36984
+ };
36985
+ var FOCUS_SEL = /:focus(-visible|-within)?\b/;
36986
+ function killsOutline(d) {
36987
+ const o = (d.outline ?? "").trim();
36988
+ if (o === "none" || /^0(px)?( |$)/.test(o) || /\bnone\b/.test(o)) return true;
36989
+ if ((d.outlineStyle ?? "") === "none") return true;
36990
+ return d.outlineWidth !== void 0 && Number.parseFloat(d.outlineWidth) === 0;
36991
+ }
36992
+ function restoresIndicator(d) {
36993
+ if (d.boxShadow && d.boxShadow !== "none") return true;
36994
+ if (d.outlineStyle && d.outlineStyle !== "none" && Number.parseFloat(d.outlineWidth ?? "1") > 0) return true;
36995
+ if (d.outline && !killsOutline(d)) return true;
36996
+ if (d.textDecorationLine && d.textDecorationLine !== "none") return true;
36997
+ for (const k of Object.keys(d)) {
36998
+ if (/^border(Top|Right|Bottom|Left)?(Color|Width|Style)?$/.test(k) && d[k] && d[k] !== "none") return true;
36999
+ if (/^background(Color|Image)?$/.test(k) && d[k] && d[k] !== "none" && d[k] !== "rgba(0, 0, 0, 0)") return true;
37000
+ }
37001
+ return d.filter !== void 0 || d.transform !== void 0 || d.color !== void 0;
37002
+ }
37003
+ var renderedFocusNotVisible = {
37004
+ id: "rendered-focus-not-visible",
37005
+ criteria: ["2.4.7"],
37006
+ severity: "majeur",
37007
+ scope: "page",
37008
+ run(doc) {
37009
+ const css = doc.signals?.css;
37010
+ if (!css || css.unreadable > 0 || css.truncated) return [];
37011
+ const focusRules = css.rules.filter((r) => FOCUS_SEL.test(r.selector));
37012
+ if (!focusRules.length) return [];
37013
+ if (!focusRules.some((r) => killsOutline(r.decls))) return [];
37014
+ if (focusRules.some((r) => restoresIndicator(r.decls))) return [];
37015
+ const anchor = doc.elements.find((e) => ["a", "button", "input", "select", "textarea"].includes(e.tag));
37016
+ if (!anchor) return [];
37017
+ const killer = focusRules.find((r) => killsOutline(r.decls));
37018
+ return [{ criteriaId: "2.4.7", el: doc.elements[0] ?? anchor, msgId: "rendered-focus-not-visible", params: { selector: killer?.selector ?? ":focus" } }];
37019
+ }
37020
+ };
37021
+ var DOC_SEL = /^\s*(\*|:root|html|body)\s*$/i;
37022
+ function quarterTurn(transform) {
37023
+ const m = /rotate[ZY]?\(\s*(-?[\d.]+)deg\s*\)/i.exec(transform);
37024
+ if (!m) return false;
37025
+ const deg = Math.abs(Number.parseFloat(m[1])) % 180;
37026
+ return Math.abs(deg - 90) < 1;
37027
+ }
37028
+ var renderedOrientationLock = {
37029
+ id: "rendered-orientation-lock",
37030
+ criteria: ["1.3.4"],
37031
+ severity: "majeur",
37032
+ scope: "page",
37033
+ run(doc) {
37034
+ const css = doc.signals?.css;
37035
+ if (!css || css.unreadable > 0) return [];
37036
+ for (const r of css.rules) {
37037
+ if (!r.media || !/orientation\s*:/i.test(r.media)) continue;
37038
+ if (!r.selector.split(",").some((s) => DOC_SEL.test(s))) continue;
37039
+ const t2 = r.decls.transform ?? "";
37040
+ if (!quarterTurn(t2)) continue;
37041
+ const el = doc.elements[0];
37042
+ if (!el) continue;
37043
+ return [{ criteriaId: "1.3.4", el, msgId: "rendered-orientation-lock", params: { media: r.media, transform: t2 } }];
37044
+ }
37045
+ return [];
37046
+ }
37047
+ };
37048
+ var renderedRules = [
37049
+ renderedContrast,
37050
+ renderedContrastPixel,
37051
+ renderedLinkColourOnly,
37052
+ renderedNonTextContrast,
37053
+ renderedFocusNotVisible,
37054
+ renderedOrientationLock
37055
+ ];
37056
+
36285
37057
  // src/rules/timing.ts
36286
37058
  var metaRefreshRedirect = {
36287
37059
  id: "meta-refresh-redirect",
@@ -36329,6 +37101,7 @@ var timingRules = [metaRefreshRedirect, blinkMarquee];
36329
37101
  // src/rules/registry.ts
36330
37102
  var ALL_RULES = [
36331
37103
  ...colorsRules,
37104
+ ...renderedRules,
36332
37105
  ...imagesRules,
36333
37106
  ...framesRules,
36334
37107
  ...scriptsAriaRules,
@@ -36390,7 +37163,7 @@ function crossToFinding(doc, ruleId, def, cf) {
36390
37163
  import { resolve as absPath } from "path";
36391
37164
 
36392
37165
  // src/graph/resolve.ts
36393
- import { dirname as dirname5, join as join21 } from "path";
37166
+ import { dirname as dirname6, join as join22 } from "path";
36394
37167
  var EXT_ORDER = [".tsx", ".jsx", ".ts", ".js"];
36395
37168
  function candidates(base) {
36396
37169
  const out2 = [base];
@@ -36400,7 +37173,7 @@ function candidates(base) {
36400
37173
  for (const e of EXT_ORDER) out2.push(stripped + e);
36401
37174
  }
36402
37175
  for (const e of EXT_ORDER) out2.push(base + e);
36403
- for (const e of EXT_ORDER) out2.push(join21(base, `index${e}`));
37176
+ for (const e of EXT_ORDER) out2.push(join22(base, `index${e}`));
36404
37177
  return out2;
36405
37178
  }
36406
37179
  function matchKnown(base, known) {
@@ -36412,7 +37185,7 @@ function matchKnown(base, known) {
36412
37185
  }
36413
37186
  function resolveSpecifier(fromFile, spec, known, aliases) {
36414
37187
  if (spec.startsWith(".")) {
36415
- return matchKnown(join21(dirname5(toPosix(fromFile)), spec), known);
37188
+ return matchKnown(join22(dirname6(toPosix(fromFile)), spec), known);
36416
37189
  }
36417
37190
  if (aliases?.length) {
36418
37191
  for (const rule of aliases) {
@@ -36420,7 +37193,7 @@ function resolveSpecifier(fromFile, spec, known, aliases) {
36420
37193
  if (!spec.startsWith(rule.prefix)) continue;
36421
37194
  const rest = spec.slice(rule.prefix.length);
36422
37195
  for (const base of rule.bases) {
36423
- const hit = matchKnown(join21(base, rest), known);
37196
+ const hit = matchKnown(join22(base, rest), known);
36424
37197
  if (hit) return hit;
36425
37198
  }
36426
37199
  } else if (spec === rule.prefix) {
@@ -36435,11 +37208,11 @@ function resolveSpecifier(fromFile, spec, known, aliases) {
36435
37208
  }
36436
37209
 
36437
37210
  // src/graph/tsconfig.ts
36438
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
36439
- import { dirname as dirname6, join as join22, resolve as resolve3, relative } from "path";
37211
+ import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
37212
+ import { dirname as dirname7, join as join23, resolve as resolve3, relative } from "path";
36440
37213
  function readJsonish(path) {
36441
37214
  try {
36442
- const raw = readFileSync10(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/[^\n]*/g, "$1").replace(/,(\s*[}\]])/g, "$1");
37215
+ const raw = readFileSync12(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/[^\n]*/g, "$1").replace(/,(\s*[}\]])/g, "$1");
36443
37216
  return JSON.parse(raw);
36444
37217
  } catch {
36445
37218
  return null;
@@ -36448,9 +37221,9 @@ function readJsonish(path) {
36448
37221
  function findTsconfig(startDir) {
36449
37222
  let dir = resolve3(startDir);
36450
37223
  for (let i2 = 0; i2 < 30; i2++) {
36451
- const p = join22(dir, "tsconfig.json");
36452
- if (existsSync9(p)) return p;
36453
- const parent = dirname6(dir);
37224
+ const p = join23(dir, "tsconfig.json");
37225
+ if (existsSync10(p)) return p;
37226
+ const parent = dirname7(dir);
36454
37227
  if (parent === dir) break;
36455
37228
  dir = parent;
36456
37229
  }
@@ -36466,15 +37239,15 @@ function tsconfigChain(startDir) {
36466
37239
  const p = queue.shift();
36467
37240
  if (seen.has(p)) continue;
36468
37241
  seen.add(p);
36469
- if (!existsSync9(p)) continue;
37242
+ if (!existsSync10(p)) continue;
36470
37243
  out2.push(p);
36471
37244
  const cfg = readJsonish(p);
36472
37245
  const exts = Array.isArray(cfg?.extends) ? cfg.extends : typeof cfg?.extends === "string" ? [cfg.extends] : [];
36473
- const dir = dirname6(p);
37246
+ const dir = dirname7(p);
36474
37247
  for (const e of exts) {
36475
37248
  if (typeof e !== "string") continue;
36476
37249
  const cands = e.endsWith(".json") ? [resolve3(dir, e)] : [resolve3(dir, `${e}.json`), resolve3(dir, e, "tsconfig.json")];
36477
- const hit = cands.find((c2) => existsSync9(c2));
37250
+ const hit = cands.find((c2) => existsSync10(c2));
36478
37251
  if (hit) queue.push(hit);
36479
37252
  }
36480
37253
  }
@@ -36483,7 +37256,7 @@ function tsconfigChain(startDir) {
36483
37256
  function readTsAliases(startDir, cwd = process.cwd()) {
36484
37257
  const tsconfigPath = findTsconfig(startDir);
36485
37258
  if (!tsconfigPath) return [];
36486
- const root = dirname6(tsconfigPath);
37259
+ const root = dirname7(tsconfigPath);
36487
37260
  const cfg = readJsonish(tsconfigPath);
36488
37261
  if (!cfg) return [];
36489
37262
  let co = cfg.compilerOptions ?? {};
@@ -36507,7 +37280,7 @@ function readTsAliases(startDir, cwd = process.cwd()) {
36507
37280
  const bases = targets.map((t2) => {
36508
37281
  const star = t2.indexOf("*");
36509
37282
  const tp = wildcard && star >= 0 ? t2.slice(0, star) : t2;
36510
- return toPosix(relative(cwd, join22(baseAbs, tp))) || ".";
37283
+ return toPosix(relative(cwd, join23(baseAbs, tp))) || ".";
36511
37284
  });
36512
37285
  out2.push({ prefix: prefix2, wildcard, bases });
36513
37286
  }
@@ -36967,6 +37740,56 @@ var rgaa_default = {
36967
37740
  en: "State the file format and size in the link text, e.g. \u201CAnnual report (PDF, 2 MB)\u201D.",
36968
37741
  fr: "Indiquez le format et le poids du fichier dans l\u2019intitul\xE9 du lien, par exemple \xAB Rapport annuel (PDF, 2 Mo) \xBB."
36969
37742
  }
37743
+ },
37744
+ {
37745
+ id: "optgroup-without-label",
37746
+ criterion: "11.8",
37747
+ wcag: ["1.3.1"],
37748
+ severity: "majeur",
37749
+ match: {
37750
+ tag: "optgroup",
37751
+ attrs: [
37752
+ {
37753
+ name: "label",
37754
+ op: "absent"
37755
+ }
37756
+ ]
37757
+ },
37758
+ message: {
37759
+ en: "<optgroup> without a label attribute \u2014 the group of options is unnamed (RGAA test 11.8.2).",
37760
+ fr: "<optgroup> sans attribut label \u2014 le regroupement d\u2019options n\u2019est pas nomm\xE9 (test RGAA 11.8.2)."
37761
+ },
37762
+ remediation: {
37763
+ en: 'Add label="\u2026" naming what the options in this group have in common, e.g. <optgroup label="Europe">.',
37764
+ fr: 'Ajoutez label="\u2026" nommant ce que les options du groupe ont en commun, par ex. <optgroup label="Europe">.'
37765
+ }
37766
+ },
37767
+ {
37768
+ id: "dir-value-invalid",
37769
+ criterion: "8.10",
37770
+ wcag: ["1.3.2"],
37771
+ severity: "majeur",
37772
+ match: {
37773
+ attrs: [
37774
+ {
37775
+ name: "dir",
37776
+ op: "present"
37777
+ },
37778
+ {
37779
+ name: "dir",
37780
+ op: "matches",
37781
+ value: "^(?!(rtl|ltr|auto)$).+$"
37782
+ }
37783
+ ]
37784
+ },
37785
+ message: {
37786
+ en: "dir attribute with a value that is not rtl, ltr or auto \u2014 the reading direction change is not declared conformantly (RGAA test 8.10.2).",
37787
+ fr: "Attribut dir dont la valeur n\u2019est ni rtl, ni ltr, ni auto \u2014 le changement de sens de lecture n\u2019est pas d\xE9clar\xE9 conform\xE9ment (test RGAA 8.10.2)."
37788
+ },
37789
+ remediation: {
37790
+ en: 'Use dir="rtl" or dir="ltr" (or dir="auto"), matching the reading direction of the text it carries.',
37791
+ fr: 'Utilisez dir="rtl" ou dir="ltr" (ou dir="auto"), en accord avec le sens de lecture du texte port\xE9.'
37792
+ }
36970
37793
  }
36971
37794
  ],
36972
37795
  themes: [
@@ -37697,10 +38520,16 @@ var rgaa_default = {
37697
38520
  ]
37698
38521
  },
37699
38522
  techniques: ["G18", "G136", "G148", "G174", "G145", "C29"],
37700
- particularCases: ["Dans ces situations, les crit\xE8res sont non applicables pour ces \xE9l\xE9ments\xA0:", "[object Object]"],
38523
+ particularCases: [
38524
+ "Dans ces situations, les crit\xE8res sont non applicables pour ces \xE9l\xE9ments\xA0:",
38525
+ "- Le texte fait partie d\u2019un logo ou d\u2019un nom de marque d\u2019un organisme ou d\u2019une soci\xE9t\xE9\xA0;",
38526
+ "- Le texte ou l\u2019image de texte est purement d\xE9coratif\xA0;",
38527
+ "- Le texte fait partie d\u2019une image v\xE9hiculant une information mais le texte lui-m\xEAme n\u2019apporte aucune information essentielle\xA0;",
38528
+ "- Le texte ou l\u2019image de texte fait partie d\u2019un \xE9l\xE9ment d\u2019interface sur lequel aucune action n\u2019est possible (par exemple un bouton avec l\u2019attribut `disabled`)."
38529
+ ],
37701
38530
  wcag: ["1.4.3"],
37702
38531
  appliesTo: {
37703
- ruleIds: ["axe:color-contrast", "axe:color-contrast-enhanced", "contrast-literal"]
38532
+ ruleIds: ["axe:color-contrast", "axe:color-contrast-enhanced", "contrast-literal", "rendered-contrast", "rendered-contrast-pixel"]
37704
38533
  }
37705
38534
  },
37706
38535
  {
@@ -37733,10 +38562,19 @@ var rgaa_default = {
37733
38562
  ]
37734
38563
  },
37735
38564
  techniques: ["G18", "G195", "G207", "G174", "G145", "G183", "F78"],
37736
- particularCases: ["Les cas suivants sont non applicables pour ce crit\xE8re\xA0:", "[object Object]"],
38565
+ particularCases: [
38566
+ "Les cas suivants sont non applicables pour ce crit\xE8re\xA0:",
38567
+ "- Composant d\u2019interface inactif (par exemple, un bouton avec un attribut `disabled`) sur lequel aucune action n\u2019est possible\xA0;",
38568
+ "- Composant d\u2019interface pour lequel l\u2019apparence est g\xE9r\xE9e par les styles natifs du navigateur sans aucune modification par l\u2019auteur (par exemple, le style au focus natif dans Chrome ou Firefox)\xA0;",
38569
+ "- Composant d\u2019interface pour lequel la couleur n\u2019est pas n\xE9cessaire pour identifier le composant ou son \xE9tat (par exemple, un groupe de liens faisant office de navigation dont la position dans la page, la taille et la couleur du texte permettent de comprendre qu\u2019il s\u2019agit de liens m\xEAme si la couleur du soulignement des liens avec le fond blanc n\u2019a pas un ratio de 3:1 et que le texte lui a un ratio de 4.5:1)\xA0;",
38570
+ "- [\xC9l\xE9ment graphique](#element-graphique) ou parties d\u2019\xE9l\xE9ment graphique non porteur d\u2019information ou ayant une alternative (description longue, informations identiques visibles dans la page)\xA0;",
38571
+ "- [\xC9l\xE9ment graphique](#element-graphique) ou parties d\u2019\xE9l\xE9ment graphique faisant partie d\u2019un logo ou du nom de marque d\u2019un organisme ou d\u2019une soci\xE9t\xE9\xA0;",
38572
+ "- [\xC9l\xE9ment graphique](#element-graphique) ou parties d\u2019\xE9l\xE9ment graphique dont la pr\xE9sentation est essentielle \xE0 l\u2019information v\xE9hicul\xE9e (par exemple, drapeaux, logotypes, photos de personnes ou de sc\xE8nes, captures d\u2019\xE9cran, diagrammes m\xE9dicaux, carte de chaleurs)\xA0;",
38573
+ "- [\xC9l\xE9ment graphique](#element-graphique) ou parties d\u2019\xE9l\xE9ment graphique dynamiques dont le contraste au survol / focus est suffisant."
38574
+ ],
37737
38575
  wcag: ["1.4.11"],
37738
38576
  appliesTo: {
37739
- ruleIds: []
38577
+ ruleIds: ["rendered-nontext-contrast"]
37740
38578
  }
37741
38579
  },
37742
38580
  {
@@ -37774,7 +38612,13 @@ var rgaa_default = {
37774
38612
  techniques: ["G58", "G69", "G78", "G158", "G159", "G173", "G8", "G166", "H96", "SM6", "SM7"],
37775
38613
  particularCases: [
37776
38614
  "Il existe une gestion de cas particulier lorsque\xA0:",
37777
- "[object Object]",
38615
+ "- Le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) est utilis\xE9 \xE0 des fins d\xE9coratives (c\u2019est-\xE0-dire qu\u2019il n\u2019apporte aucune information)\xA0;",
38616
+ "- Le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) est lui-m\xEAme une alternative \xE0 un contenu de la page (une vid\xE9o en langue des signes ou la vocalisation d\u2019un texte, par exemple)\xA0;",
38617
+ "- Le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) est utilis\xE9 pour acc\xE9der \xE0 une version agrandie\xA0;",
38618
+ "- Le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) est utilis\xE9 comme un [CAPTCHA](#captcha)\xA0;",
38619
+ "- Le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) fait partie d\u2019un test qui deviendrait inutile si la [transcription textuelle](#transcription-textuelle-media-temporel), les [sous-titres synchronis\xE9s](#sous-titres-synchronises-objet-multimedia) ou l\u2019[audiodescription](#audiodescription-synchronisee-media-temporel) \xE9taient communiqu\xE9s\xA0;",
38620
+ "- Pour les services de l\u2019\xC9tat, les collectivit\xE9s territoriales et leurs \xE9tablissements\xA0: si le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) a \xE9t\xE9 publi\xE9 entre le 23 septembre 2019 et le 23 septembre 2020 sur un site internet, intranet ou extranet cr\xE9\xE9 depuis le 23 septembre 2018, il est exempt\xE9 de l\u2019obligation d\u2019accessibilit\xE9\xA0;",
38621
+ "- Pour les personnes de droit priv\xE9 mentionn\xE9es aux 2\xB0 \xE0 4\xB0 du I de l\u2019article 47 de la loi du 11 f\xE9vrier 2005\xA0: si le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) a \xE9t\xE9 publi\xE9 avant le 23 septembre 2020, il est exempt\xE9 de l\u2019obligation d\u2019accessibilit\xE9.",
37778
38622
  "Dans ces situations, le crit\xE8re est non applicable.",
37779
38623
  "Ce cas particulier s\u2019applique \xE9galement aux crit\xE8res 4.2, 4.3, 4.5."
37780
38624
  ],
@@ -37959,7 +38803,13 @@ var rgaa_default = {
37959
38803
  ]
37960
38804
  },
37961
38805
  techniques: ["H35", "H46"],
37962
- particularCases: ["Il existe une gestion de cas particulier lorsque\xA0:", "[object Object]", "Dans ces situations, le crit\xE8re est non applicable."],
38806
+ particularCases: [
38807
+ "Il existe une gestion de cas particulier lorsque\xA0:",
38808
+ "- Le [m\xE9dia non temporel](#media-non-temporel) est utilis\xE9 \xE0 des fins d\xE9coratives (c\u2019est-\xE0-dire qu\u2019il n\u2019apporte aucune information)\xA0;",
38809
+ "- Le [m\xE9dia non temporel](#media-non-temporel) est diffus\xE9 dans un [environnement ma\xEEtris\xE9](#environnement-maitrise)\xA0;",
38810
+ "- Le [m\xE9dia non temporel](#media-non-temporel) est ins\xE9r\xE9 via JavaScript en v\xE9rifiant la pr\xE9sence et la version du plug-in, en remplacement d\u2019un [contenu alternatif](#contenu-alternatif) d\xE9j\xE0 pr\xE9sent.",
38811
+ "Dans ces situations, le crit\xE8re est non applicable."
38812
+ ],
37963
38813
  wcag: ["1.1.1"],
37964
38814
  appliesTo: {
37965
38815
  ruleIds: []
@@ -38340,14 +39190,18 @@ var rgaa_default = {
38340
39190
  techniques: ["H30", "H78", "H79", "H80", "H81", "G53", "G91", "F63", "F89", "ARIA7", "ARIA8"],
38341
39191
  technicalNote: [
38342
39192
  "Lorsque l\u2019intitul\xE9 visible est compl\xE9t\xE9 par une autre expression dans le nom accessible\xA0:",
38343
- "[object Object]",
39193
+ "- WCAG insiste sur le placement de l\u2019intitul\xE9 visible au d\xE9but du nom accessible sans toutefois r\xE9server l\u2019exclusivit\xE9 de cet emplacement\xA0;",
39194
+ "- WCAG consid\xE8re comme un cas d\u2019\xE9chec une correspondance non exacte de la cha\xEEne de caract\xE8res de l\u2019intitul\xE9 visible au sein du nom accessible.",
38344
39195
  "Par exemple, si l\u2019on consid\xE8re l\u2019intitul\xE9 visible \xAB\xA0Commander maintenant\xA0\xBB compl\xE9t\xE9 dans le nom accessible par l\u2019expression \xAB\xA0produit X\xA0\xBB, on peut avoir les diff\xE9rents cas suivants\xA0:",
38345
- "[object Object]"
39196
+ "- \xAB\xA0Commander maintenant produit X\xA0\xBB est valide (bonne pratique)\xA0;",
39197
+ "- \xAB\xA0Produit X : commander maintenant\xA0\xBB est valide\xA0;",
39198
+ "- \xAB\xA0Commander produit X maintenant\xA0\xBB est non valide."
38346
39199
  ],
38347
39200
  particularCases: [
38348
39201
  "Il existe une gestion de cas particuliers pour les tests 6.1.1, 6.1.2, 6.1.3 et 6.1.4 lorsque le lien est [ambigu pour tout le monde](#ambigu-pour-tout-le-monde). Dans cette situation, o\xF9 il n\u2019est pas possible de rendre le lien explicite dans son contexte, le crit\xE8re est non applicable.",
38349
39202
  "Il existe une gestion de cas particuliers pour le test 6.1.5 lorsque\xA0:",
38350
- "[object Object]",
39203
+ "- La ponctuation et les lettres majuscules sont pr\xE9sentes dans le texte de l\u2019[intitul\xE9 visible](#intitule-visible)\xA0: elles peuvent \xEAtre ignor\xE9es dans le nom accessible sans porter \xE0 cons\xE9quence\xA0;",
39204
+ "- Le texte de l\u2019[intitul\xE9 visible](#intitule-visible) sert de symbole\xA0: le texte ne doit pas \xEAtre interpr\xE9t\xE9 litt\xE9ralement au niveau du nom accessible. Le nom doit exprimer la fonction v\xE9hicul\xE9e par le symbole (par exemple, \u201CB\u201D au niveau d\u2019un \xE9diteur de texte aura pour nom accessible \u201CMettre en gras\u201D, le signe \u201C>\u201D en fonction du contexte signifiera \u201CSuivant\u201D ou \u201CLancer la vid\xE9o\u201D). Le cas des symboles math\xE9matiques fait cependant exception (voir la note ci-dessous).",
38351
39205
  "Note\xA0: si l\u2019\xE9tiquette visible repr\xE9sente une expression math\xE9matique, les symboles math\xE9matiques peuvent \xEAtre repris litt\xE9ralement pour servir d\u2019\xE9tiquette au nom accessible (ex.\xA0: \u201CA>B\u201D). Il est laiss\xE9 \xE0 l\u2019utilisateur le soin d\u2019op\xE9rer la correspondance entre l\u2019expression et ce qu\u2019il doit \xE9peler compte tenu de la connaissance qu\u2019il a du fonctionnement de son logiciel de saisie vocale (\u201CA plus grand que B\u201D ou \u201CA sup\xE9rieur \xE0 B\u201D)."
38352
39206
  ],
38353
39207
  wcag: ["1.1.1", "2.4.4", "2.5.3"],
@@ -38411,7 +39265,8 @@ var rgaa_default = {
38411
39265
  ],
38412
39266
  particularCases: [
38413
39267
  "Il existe une gestion de cas particuliers pour le test 7.1.3 lorsque\xA0:",
38414
- "[object Object]",
39268
+ "- La ponctuation et les lettres majuscules sont pr\xE9sentes dans le texte de l\u2019intitul\xE9 visible : elles peuvent \xEAtre ignor\xE9es dans le nom accessible sans porter \xE0 cons\xE9quence\xA0;",
39269
+ "- Le texte de l\u2019intitul\xE9 visible sert de symbole : le texte ne doit pas \xEAtre interpr\xE9t\xE9 litt\xE9ralement au niveau du nom accessible. Le nom doit exprimer la fonction v\xE9hicul\xE9e par le symbole (par exemple, \u201CB\u201D au niveau d\u2019un \xE9diteur de texte aura pour nom accessible \u201CMettre en gras\u201D, le signe \u201C>\u201D en fonction du contexte signifiera \u201CSuivant\u201D ou \u201CLancer la vid\xE9o\u201D). Le cas des symboles math\xE9matiques fait cependant exception (voir la note ci-dessous).",
38415
39270
  "Note\xA0: si l\u2019\xE9tiquette visible repr\xE9sente une expression math\xE9matique, les symboles math\xE9matiques peuvent \xEAtre repris litt\xE9ralement pour servir d\u2019\xE9tiquette au nom accessible (ex.\xA0: \u201CA>B\u201D). Il est laiss\xE9 \xE0 l\u2019utilisateur le soin d\u2019op\xE9rer la correspondance entre l\u2019expression et ce qu\u2019il doit \xE9peler compte tenu de la connaissance qu\u2019il a du fonctionnement de son logiciel de saisie vocale (\u201CA plus grand que B\u201D ou \u201CA sup\xE9rieur \xE0 B\u201D)."
38416
39271
  ],
38417
39272
  wcag: ["2.5.3", "4.1.2"],
@@ -38541,7 +39396,9 @@ var rgaa_default = {
38541
39396
  techniques: ["ARIA19", "ARIA22", "ARIA23"],
38542
39397
  technicalNote: [
38543
39398
  "Les r\xF4les WAI-ARIA `log`, `status` et `alert` ont implicitement une valeur d\u2019attribut WAI-ARIA `aria-live` et `aria-atomic`. On pourra donc consid\xE9rer (conform\xE9ment \xE0 la sp\xE9cification WAI-ARIA 1.1) que\xA0:",
38544
- "[object Object]",
39399
+ '- Un attribut WAI-ARIA `aria-live="polite"` associ\xE9 \xE0 un message de statut peut valoir pour un r\xF4le WAI-ARIA `log`\xA0;',
39400
+ '- Un attribut WAI-ARIA `aria-live="polite"` et un attribut WAI-ARIA `aria-atomic="true"` associ\xE9s \xE0 un message de statut peuvent valoir pour un r\xF4le WAI-ARIA `status`\xA0;',
39401
+ '- Un attribut WAI-ARIA `aria-live="assertive"` et un attribut WAI-ARIA `aria-atomic="true"` associ\xE9s \xE0 un message de statut peuvent valoir pour un r\xF4le WAI-ARIA `alert`.',
38545
39402
  "C\u2019est sous r\xE9serve que la nature du message de statut satisfasse bien \xE0 la correspondance implicitement \xE9tablie. Dans le cas d\u2019un message de statut indiquant la progression d\u2019un processus et mat\xE9rialis\xE9 graphiquement par une barre de progression, un r\xF4le WAI-ARIA `progressbar` explicite est n\xE9cessaire."
38546
39403
  ],
38547
39404
  wcag: ["4.1.3"],
@@ -38695,7 +39552,11 @@ var rgaa_default = {
38695
39552
  techniques: ["H58"],
38696
39553
  particularCases: [
38697
39554
  "Il y a une gestion de cas particuliers sur le changement de langue pour les cas suivants\xA0:",
38698
- "[object Object]",
39555
+ "- Nom propre, le crit\xE8re est non applicable\xA0;",
39556
+ "- Nom commun de langue \xE9trang\xE8re pr\xE9sent dans le dictionnaire officiel de la langue (voir note 1 ci-dessous) par d\xE9faut de la page web, le crit\xE8re est non applicable\xA0;",
39557
+ "- Le terme de langue \xE9trang\xE8re soumis, via un [champ de formulaire](#champ-de-saisie-de-formulaire) et rappel\xE9 dans la page (par exemple comme indication du terme recherch\xE9 dans le cas d\u2019un moteur de recherche), le crit\xE8re est non applicable\xA0;",
39558
+ "- Passage de texte dont la langue ne peut pas \xEAtre d\xE9termin\xE9e : le crit\xE8re est non applicable\xA0;",
39559
+ "- Terme ou passage de texte issus d\u2019une langue morte ou imaginaire pour laquelle il n\u2019existe pas d\u2019interpr\xE9tation vocale : le crit\xE8re est non applicable.",
38699
39560
  "Note 1\xA0: le dictionnaire officiel est celui recommand\xE9 par l\u2019acad\xE9mie en charge de la langue en question. Pour la France, par exemple, le lien vers le dictionnaire officiel se trouve sur le site de l\u2019Acad\xE9mie fran\xE7aise \xE0 l\u2019adresse suivante\xA0: http://www.academie-francaise.fr/le-dictionnaire/la-9e-edition. Pour toute demande aupr\xE8s du service du dictionnaire de l\u2019Acad\xE9mie fran\xE7aise, utiliser le formulaire de contact du service du dictionnaire.",
38700
39561
  "Note 2\xA0: pour les noms communs de langue \xE9trang\xE8re, absents dans le dictionnaire officiel de la langue par d\xE9faut de la page web, et qui sont pass\xE9s dans le langage commun (exemple\xA0: newsletter)\xA0: le crit\xE8re est applicable, uniquement lorsque l\u2019absence d\u2019indication de langue peut provoquer une incompr\xE9hension pour la restitution."
38701
39562
  ],
@@ -38768,7 +39629,7 @@ var rgaa_default = {
38768
39629
  techniques: ["H56"],
38769
39630
  wcag: ["1.3.2"],
38770
39631
  appliesTo: {
38771
- ruleIds: []
39632
+ ruleIds: ["pack:rgaa:dir-value-invalid"]
38772
39633
  }
38773
39634
  },
38774
39635
  {
@@ -38976,7 +39837,9 @@ var rgaa_default = {
38976
39837
  techniques: ["G146", "G179", "F69", "F80", "SCR34", "C12", "C13", "C14", "C17", "C28"],
38977
39838
  particularCases: [
38978
39839
  "Font exception \xE0 ce crit\xE8re, les contenus pour lesquels l\u2019utilisateur n\u2019a pas de possibilit\xE9 de personnalisation\xA0:",
38979
- "[object Object]"
39840
+ "- Les sous-titres incrust\xE9s dans une vid\xE9o\xA0;",
39841
+ "- Les textes en image\xA0;",
39842
+ "- Le texte au sein d\u2019une balise `<canvas>`."
38980
39843
  ],
38981
39844
  wcag: ["1.4.4"],
38982
39845
  appliesTo: {
@@ -39006,7 +39869,7 @@ var rgaa_default = {
39006
39869
  techniques: ["F24"],
39007
39870
  wcag: ["1.4.3"],
39008
39871
  appliesTo: {
39009
- ruleIds: ["axe:color-contrast", "axe:color-contrast-enhanced", "contrast-literal"]
39872
+ ruleIds: ["axe:color-contrast", "axe:color-contrast-enhanced", "contrast-literal", "rendered-contrast", "rendered-contrast-pixel"]
39010
39873
  }
39011
39874
  },
39012
39875
  {
@@ -39029,7 +39892,7 @@ var rgaa_default = {
39029
39892
  techniques: ["G183", "F73"],
39030
39893
  wcag: ["1.4.1"],
39031
39894
  appliesTo: {
39032
- ruleIds: []
39895
+ ruleIds: ["rendered-link-colour-only"]
39033
39896
  }
39034
39897
  },
39035
39898
  {
@@ -39051,7 +39914,7 @@ var rgaa_default = {
39051
39914
  techniques: ["G149", "G165", "G183", "G195", "F73", "F78", "SCR31", "C15"],
39052
39915
  wcag: ["1.4.1", "2.4.7"],
39053
39916
  appliesTo: {
39054
- ruleIds: ["dyn-focus-visible"]
39917
+ ruleIds: ["dyn-focus-visible", "rendered-focus-not-visible"]
39055
39918
  }
39056
39919
  },
39057
39920
  {
@@ -39162,7 +40025,11 @@ var rgaa_default = {
39162
40025
  particularCases: [
39163
40026
  "L'objectif de ce crit\xE8re est de garantir un d\xE9filement dans une unique direction pour une lecture facilit\xE9e selon le sens de l'\xE9criture.",
39164
40027
  "Font exception \xE0 ce crit\xE8re, les contenus dont l'agencement requiert deux dimensions pour \xEAtre compris ou utilis\xE9s comme\xA0:",
39165
- "[object Object]",
40028
+ "- Les images, les graphiques ou les vid\xE9os\xA0;",
40029
+ "- Les jeux (jeux de plateforme, par exemple)\xA0;",
40030
+ "- Les pr\xE9sentations (type diaporama, par exemple)\xA0;",
40031
+ "- Les tableaux de donn\xE9es\xA0;",
40032
+ "- Les interfaces o\xF9 il est n\xE9cessaire d'avoir un ascenseur horizontal lors de la manipulation de l'interface.",
39166
40033
  "Note\xA0: la majorit\xE9 des navigateurs sur les syst\xE8mes d'exploitation sur mobile (Android, iOS) ne g\xE8re pas correctement la redistribution en cas de zoom. Dans ce contexte, le crit\xE8re sera consid\xE9r\xE9 comme non applicable sur ces environnements."
39167
40034
  ],
39168
40035
  wcag: ["1.4.10"],
@@ -39191,7 +40058,9 @@ var rgaa_default = {
39191
40058
  techniques: ["C8", "C21", "C35", "C36"],
39192
40059
  particularCases: [
39193
40060
  "Font exception \xE0 ce crit\xE8re, les contenus pour lesquels l\u2019utilisateur n\u2019a pas de possibilit\xE9 de personnalisation\xA0:",
39194
- "[object Object]"
40061
+ "- Les sous-titres directement int\xE9gr\xE9s \xE0 une vid\xE9o\xA0;",
40062
+ "- Les images texte\xA0;",
40063
+ "- Le texte au sein d\u2019une balise `<canvas>`."
39195
40064
  ],
39196
40065
  wcag: ["1.4.12"],
39197
40066
  appliesTo: {
@@ -39344,7 +40213,8 @@ var rgaa_default = {
39344
40213
  techniques: ["G82", "G131", "H44", "H65", "ARIA6", "ARIA9", "ARIA14", "ARIA16"],
39345
40214
  particularCases: [
39346
40215
  "Il existe une gestion de cas particuliers pour le test 11.2.5 lorsque\xA0:",
39347
- "[object Object]",
40216
+ "- La ponctuation et les lettres majuscules sont pr\xE9sentes dans le texte de l\u2019[intitul\xE9 visible](#intitule-visible) : elles peuvent \xEAtre ignor\xE9es dans le nom accessible sans porter \xE0 cons\xE9quence\xA0;",
40217
+ "- Le texte de l\u2019[intitul\xE9 visible](#intitule-visible) sert de symbole\xA0: le texte ne doit pas \xEAtre interpr\xE9t\xE9 litt\xE9ralement au niveau du nom accessible. Le nom doit exprimer la fonction v\xE9hicul\xE9e par le symbole (par exemple, \u201CB\u201D au niveau d\u2019un \xE9diteur de texte aura pour nom accessible \u201CMettre en gras\u201D, le signe \u201C>\u201D en fonction du contexte signifiera \u201CSuivant\u201D ou \u201CLancer la vid\xE9o\u201D). Le cas des symboles math\xE9matiques fait cependant exception (voir la note ci-dessous).",
39348
40218
  "Note\xA0: si l\u2019\xE9tiquette visible repr\xE9sente une expression math\xE9matique, les symboles math\xE9matiques peuvent \xEAtre repris litt\xE9ralement pour servir d\u2019\xE9tiquette au nom accessible (ex.\xA0: \u201CA>B\u201D). Il est laiss\xE9 \xE0 l\u2019utilisateur le soin d\u2019op\xE9rer la correspondance entre l\u2019expression et ce qu\u2019il doit \xE9peler compte tenu de la connaissance qu\u2019il a du fonctionnement de son logiciel de saisie vocale (\u201CA plus grand que B\u201D ou \u201CA sup\xE9rieur \xE0 B\u201D).",
39349
40219
  "Ce cas particulier s\u2019applique \xE9galement au test 11.9.2."
39350
40220
  ],
@@ -39401,7 +40271,13 @@ var rgaa_default = {
39401
40271
  ]
39402
40272
  },
39403
40273
  techniques: ["G162"],
39404
- particularCases: ["Les tests 11.4.2 et 11.4.3 seront consid\xE9r\xE9s comme non applicables\xA0:", "[object Object]"],
40274
+ particularCases: [
40275
+ "Les tests 11.4.2 et 11.4.3 seront consid\xE9r\xE9s comme non applicables\xA0:",
40276
+ "- Dans le cas o\xF9 l\u2019[\xE9tiquette](#etiquette-de-champ-de-formulaire) m\xE9lange une portion de texte qui se lit de droite \xE0 gauche avec une portion de texte qui se lit de gauche \xE0 droite\xA0;",
40277
+ "- Dans le cas o\xF9 un formulaire contient des labels de plusieurs langues qui se liraient de droite \xE0 gauche et inversement. Par exemple, un formulaire de commande en arabe qui propose une liste de cases \xE0 cocher de produit en langue fran\xE7aise ou mixant des produits en langue arabe ou en langue fran\xE7aise\xA0;",
40278
+ '- Dans le cas o\xF9 les champs de type `radio` ou `checkbox` et les balises ayant un attribut WAI-ARIA `role="checkbox"`, `role="radio"` ou `role="switch"` ne sont pas visuellement pr\xE9sent\xE9s sous forme de bouton radio ou de case \xE0 cocher\xA0;',
40279
+ "- Dans le cas o\xF9 les champs seraient utilis\xE9s dans un contexte o\xF9 il pourrait \xEAtre l\xE9gitime, du point de vue de l\u2019exp\xE9rience utilisateur, de placer les \xE9tiquettes de mani\xE8re diff\xE9rente \xE0 celle requise dans les tests 11.4.2 et 11.4.3."
40280
+ ],
39405
40281
  wcag: ["3.3.2"],
39406
40282
  appliesTo: {
39407
40283
  ruleIds: []
@@ -39488,7 +40364,7 @@ var rgaa_default = {
39488
40364
  ],
39489
40365
  wcag: ["1.3.1"],
39490
40366
  appliesTo: {
39491
- ruleIds: []
40367
+ ruleIds: ["pack:rgaa:optgroup-without-label"]
39492
40368
  }
39493
40369
  },
39494
40370
  {
@@ -39595,11 +40471,14 @@ var rgaa_default = {
39595
40471
  ],
39596
40472
  technicalNote: [
39597
40473
  "Dans un long formulaire dont la majorit\xE9 des champs sont obligatoires, on pourrait constater que ce sont les quelques champs rest\xE9s facultatifs qui sont explicitement signal\xE9s comme tels. Dans ce cas, il faudrait s\u2019assurer que\xA0:",
39598
- "[object Object]"
40474
+ "- Un message pr\xE9cise visuellement en haut de formulaire que \u201Ctous les champs sont obligatoires sauf ceux indiqu\xE9s comme \xE9tant facultatifs\u201D\xA0;",
40475
+ "- Une mention \u201Cfacultatif\u201D est pr\xE9sente visuellement dans le libell\xE9 des champs facultatifs ou dans la l\xE9gende d\u2019un groupe de champs facultatifs\xA0;",
40476
+ '- Un attribut `required` ou `aria-required="true"` reste associ\xE9 \xE0 chaque champ qui n\u2019est pas concern\xE9 par ce caract\xE8re facultatif.'
39599
40477
  ],
39600
40478
  particularCases: [
39601
40479
  "Le test 11.10.1 et le test 11.10.2 seront consid\xE9r\xE9s comme non applicables lorsque le formulaire comporte un seul [champ de formulaire](#champ-de-saisie-de-formulaire) ou qu\u2019il indique les champs optionnels de mani\xE8re\xA0:",
39602
- "[object Object]",
40480
+ "- Visible\xA0;",
40481
+ "- Dans la balise `<label>` ou dans la [l\xE9gende](#legende) associ\xE9e au champ.",
39603
40482
  "Dans le cas o\xF9 l\u2019ensemble des champs d\u2019un formulaire sont obligatoires, les tests 11.10.1 et 11.10.2 restent applicables."
39604
40483
  ],
39605
40484
  wcag: ["3.3.1", "3.3.2"],
@@ -39729,7 +40608,13 @@ var rgaa_default = {
39729
40608
  ]
39730
40609
  },
39731
40610
  techniques: ["G61", "F66"],
39732
- particularCases: ["Il existe une gestion de cas particuliers lorsque\xA0:", "[object Object]", "Dans ces situations, le crit\xE8re est non applicable."],
40611
+ particularCases: [
40612
+ "Il existe une gestion de cas particuliers lorsque\xA0:",
40613
+ "- La page est la page d\u2019accueil\xA0;",
40614
+ "- Le site web est constitu\xE9 d\u2019une seule page\xA0;",
40615
+ "- Le changement fait suite \xE0 une modification initi\xE9e par l\u2019utilisateur.",
40616
+ "Dans ces situations, le crit\xE8re est non applicable."
40617
+ ],
39733
40618
  wcag: ["3.2.3"],
39734
40619
  appliesTo: {
39735
40620
  ruleIds: []
@@ -40032,7 +40917,11 @@ var rgaa_default = {
40032
40917
  ]
40033
40918
  },
40034
40919
  techniques: ["F15", "G10", "G135"],
40035
- particularCases: ["Il existe une gestion de cas particuliers\xA0:", "[object Object]", "Dans cette situation, le crit\xE8re est non applicable."],
40920
+ particularCases: [
40921
+ "Il existe une gestion de cas particuliers\xA0:",
40922
+ "- Pour les personnes de droit priv\xE9 mentionn\xE9es aux 2\xB0 \xE0 4\xB0 du I de l\u2019article 47 de la loi du 11 f\xE9vrier 2005\xA0: si les fichiers bureautiques (ex\xA0: PDF, documents Microsoft ou LibreOffice, etc.) ont \xE9t\xE9 publi\xE9s avant le 23 septembre 2018 (sauf si ce sont des documents n\xE9cessaires pour accomplir une d\xE9marche administrative relevant des t\xE2ches effectu\xE9es par l\u2019organisme concern\xE9), ils sont exempt\xE9s de l\u2019obligation d\u2019accessibilit\xE9.",
40923
+ "Dans cette situation, le crit\xE8re est non applicable."
40924
+ ],
40036
40925
  wcag: ["1.1.1", "1.3.1", "1.3.2", "2.4.1", "2.4.3", "3.1.1", "4.1.2"],
40037
40926
  appliesTo: {
40038
40927
  ruleIds: []
@@ -40191,7 +41080,7 @@ var rgaa_default = {
40191
41080
  ],
40192
41081
  wcag: ["1.3.4"],
40193
41082
  appliesTo: {
40194
- ruleIds: []
41083
+ ruleIds: ["rendered-orientation-lock"]
40195
41084
  }
40196
41085
  },
40197
41086
  {
@@ -40212,7 +41101,11 @@ var rgaa_default = {
40212
41101
  ]
40213
41102
  },
40214
41103
  techniques: ["G215", "G216"],
40215
- particularCases: ["Il existe une gestion de cas particuliers dans deux types de situation\xA0:", "[object Object]"],
41104
+ particularCases: [
41105
+ "Il existe une gestion de cas particuliers dans deux types de situation\xA0:",
41106
+ "- Le crit\xE8re ne s\u2019applique qu\u2019\xE0 des fonctionnalit\xE9s mises en place par l\u2019auteur du site. Il ne concerne donc pas les gestes requis par l\u2019agent utilisateur ou le syst\xE8me d\u2019exploitation\xA0;",
41107
+ "- Le crit\xE8re ne s\u2019applique pas aux fonctionnalit\xE9s dont la r\xE9alisation d\u2019un geste complexe est essentielle (ex\xE9cuter le trac\xE9 d\u2019une signature, par exemple)."
41108
+ ],
40216
41109
  wcag: ["2.5.1"],
40217
41110
  appliesTo: {
40218
41111
  ruleIds: []
@@ -40238,7 +41131,8 @@ var rgaa_default = {
40238
41131
  techniques: [],
40239
41132
  technicalNote: [
40240
41133
  "Deux exemples de m\xE9canisme mis en place pour annuler ou abandonner une action d\xE9clench\xE9e au moyen d\u2019un dispositif de pointage sur un point unique de l\u2019\xE9cran\xA0:",
40241
- "[object Object]"
41134
+ "- Une fen\xEAtre modale permettant d\u2019annuler l\u2019action apr\xE8s son ach\xE8vement\xA0;",
41135
+ "- Pour une fonction de glisser/d\xE9poser, le fait d\u2019abandonner l\u2019action si l\u2019utilisateur rel\xE2che l\u2019\xE9l\xE9ment en dehors de la zone cible."
40242
41136
  ],
40243
41137
  particularCases: [
40244
41138
  "Il existe une gestion de cas particulier lorsque la fonctionnalit\xE9 n\xE9cessite que le comportement attendu soit r\xE9alis\xE9 lors d\u2019un \xE9v\xE9nement descendant, par exemple, un \xE9mulateur de clavier dont les touches doivent s\u2019activer \xE0 la pression comme sur un clavier physique. Dans ces situations, le crit\xE8re est non applicable."
@@ -40269,7 +41163,11 @@ var rgaa_default = {
40269
41163
  ]
40270
41164
  },
40271
41165
  techniques: [],
40272
- particularCases: ["Il existe une gestion de cas particulier lorsque\xA0:", "[object Object]"],
41166
+ particularCases: [
41167
+ "Il existe une gestion de cas particulier lorsque\xA0:",
41168
+ "- Le mouvement est essentiel \xE0 l\u2019accomplissement de la fonctionnalit\xE9 (ex. podom\xE8tre)\xA0;",
41169
+ "- La d\xE9tection du mouvement est utilis\xE9e pour contr\xF4ler une fonctionnalit\xE9 au travers d\u2019une interface compatible avec l\u2019accessibilit\xE9."
41170
+ ],
40273
41171
  wcag: ["2.5.4"],
40274
41172
  appliesTo: {
40275
41173
  ruleIds: []
@@ -41210,6 +42108,9 @@ function loadPack(key) {
41210
42108
  function getPack(key) {
41211
42109
  return registry.get(key)?.pack;
41212
42110
  }
42111
+ function packGlossary(key) {
42112
+ return registry.get(key)?.glossary;
42113
+ }
41213
42114
  function listStandards() {
41214
42115
  return [CORE_KEY, ...registry.keys()];
41215
42116
  }
@@ -41306,7 +42207,7 @@ function runPackRules(doc, pack) {
41306
42207
  }
41307
42208
 
41308
42209
  // src/graph/build.ts
41309
- import { dirname as dirname7 } from "path";
42210
+ import { dirname as dirname8 } from "path";
41310
42211
 
41311
42212
  // src/graph/imports.ts
41312
42213
  var NAME_PROPS2 = /* @__PURE__ */ new Set(["aria-label", "aria-labelledby", "title", "label", "alt"]);
@@ -41604,14 +42505,14 @@ function buildGraphAndDocs(files, opts = {}) {
41604
42505
  }
41605
42506
  nodes.push(extractGraphNode(ast, doc, file, { sfc }));
41606
42507
  }
41607
- const startDir = files[0] ? dirname7(files[0]) : process.cwd();
42508
+ const startDir = files[0] ? dirname8(files[0]) : process.cwd();
41608
42509
  return { graph: buildGraph2(nodes, readTsAliases(startDir), startDir), docs };
41609
42510
  }
41610
42511
 
41611
42512
  // src/discover.ts
41612
- import { existsSync as existsSync10 } from "fs";
42513
+ import { existsSync as existsSync11 } from "fs";
41613
42514
  import { execFileSync } from "child_process";
41614
- import { join as join23, relative as relative2 } from "path";
42515
+ import { join as join24, relative as relative2 } from "path";
41615
42516
  function git(args2, maxBuffer) {
41616
42517
  try {
41617
42518
  return execFileSync("git", args2, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], ...maxBuffer ? { maxBuffer } : {} });
@@ -41636,7 +42537,7 @@ function gitChangedFiles(ref) {
41636
42537
  add2(git(["diff", "--name-only", "--diff-filter=d", "--cached", base]));
41637
42538
  add2(git(["ls-files", "--others", "--exclude-standard"]));
41638
42539
  const cwd = process.cwd();
41639
- return [...out2].map((p) => relative2(cwd, join23(repoRoot2, p)));
42540
+ return [...out2].map((p) => relative2(cwd, join24(repoRoot2, p)));
41640
42541
  }
41641
42542
  function gitStagedFiles() {
41642
42543
  const top = git(["rev-parse", "--show-toplevel"]);
@@ -41645,7 +42546,7 @@ function gitStagedFiles() {
41645
42546
  if (out2 === null) return null;
41646
42547
  const repoRoot2 = top.trim();
41647
42548
  const cwd = process.cwd();
41648
- return out2.split("\n").map((s) => s.trim()).filter(Boolean).map((p) => relative2(cwd, join23(repoRoot2, p)));
42549
+ return out2.split("\n").map((s) => s.trim()).filter(Boolean).map((p) => relative2(cwd, join24(repoRoot2, p)));
41649
42550
  }
41650
42551
  function stagedContent(file) {
41651
42552
  return git(["show", `:./${toPosix(file)}`], 32 * 1024 * 1024);
@@ -41730,7 +42631,7 @@ function discover(inputs, opts = {}) {
41730
42631
  } else {
41731
42632
  const filter = makeFilter(opts);
41732
42633
  const inScope = inScopeMatcher(inputs);
41733
- files = changed.filter((f) => existsSync10(f) && filter(f) && (!inScope || inScope(f)));
42634
+ files = changed.filter((f) => existsSync11(f) && filter(f) && (!inScope || inScope(f)));
41734
42635
  }
41735
42636
  } else {
41736
42637
  files = expandInputs(inputs, opts);
@@ -41956,7 +42857,9 @@ function runAudit(opts) {
41956
42857
  }
41957
42858
  seen.add(h);
41958
42859
  }
41959
- foldDoc(acc, reused ?? parseSource(content, file, { forceJsx: opts.forceJsx }), graph);
42860
+ const doc = reused ?? parseSource(content, file, { forceJsx: opts.forceJsx });
42861
+ attachSignals(doc);
42862
+ foldDoc(acc, doc, graph);
41960
42863
  }
41961
42864
  const canonicalFiles = acc.fileCount;
41962
42865
  if (opts.inputs.includes("-") && opts.stdin !== void 0 && !(opts.maxFiles && opts.maxFiles > 0 && acc.fileCount >= opts.maxFiles)) {
@@ -41974,12 +42877,12 @@ function runAudit(opts) {
41974
42877
  }
41975
42878
 
41976
42879
  // src/report.ts
41977
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
41978
- import { join as join25 } from "path";
42880
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
42881
+ import { join as join26 } from "path";
41979
42882
 
41980
42883
  // src/prd.ts
41981
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
41982
- import { join as join24 } from "path";
42884
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
42885
+ import { join as join25 } from "path";
41983
42886
 
41984
42887
  // src/standards/pack.ts
41985
42888
  function localize(pack, s, lang) {
@@ -42005,6 +42908,9 @@ function title(pack, c2, lang) {
42005
42908
  function titlePlain(pack, c2, lang) {
42006
42909
  return localize(pack, c2.titlePlain, lang);
42007
42910
  }
42911
+ function resolveGlossary(packKey, anchor) {
42912
+ return packGlossary(packKey)?.[anchor];
42913
+ }
42008
42914
  function neutralizeCaptureGroups(pattern) {
42009
42915
  let out2 = "";
42010
42916
  for (let i2 = 0; i2 < pattern.length; i2++) {
@@ -42104,11 +43010,29 @@ function derivePackResults(audit2, packKey) {
42104
43010
  };
42105
43011
  const enabledSecondary = (pack.secondaryMappings ?? []).filter((m) => m.enabled === true);
42106
43012
  const secondarySources = enabledSecondary.length ? [...audit2.criteria.flatMap((c2) => c2.findings), ...myPackFindings] : [];
43013
+ const adjudicated = audit2.packAdjudication?.standard === packKey ? new Map(audit2.packAdjudication.criteria.map((c2) => [c2.id, c2])) : void 0;
42107
43014
  return pack.criteria.map((pc) => {
43015
+ const decided = adjudicated?.get(pc.id);
43016
+ if (decided) {
43017
+ return {
43018
+ id: pc.id,
43019
+ theme: pc.theme,
43020
+ status: decided.status,
43021
+ findings: decided.findings,
43022
+ scs: pc.wcag,
43023
+ ...decided.justification ? { justification: decided.justification } : {},
43024
+ decidedBy: "agent"
43025
+ };
43026
+ }
42108
43027
  const base = deriveBase(pc);
42109
43028
  return enabledSecondary.length ? applySecondaryMappings(base, pc, enabledSecondary, secondarySources, pack.defaultLocale) : base;
42110
43029
  });
42111
43030
  }
43031
+ function findingsForStandard(audit2, standard) {
43032
+ if (standard === CORE_KEY) return audit2.findings;
43033
+ const mine = (audit2.packFindings ?? []).filter((f) => f.ruleId.startsWith(`pack:${standard}:`));
43034
+ return mine.length ? [...audit2.findings, ...mine] : audit2.findings;
43035
+ }
42112
43036
 
42113
43037
  // src/standards/vocabulary.ts
42114
43038
  var DEFAULT = {
@@ -44948,6 +45872,9 @@ var L2 = {
44948
45872
  acWhen: "un utilisateur de technologie d'assistance y acc\xE8de",
44949
45873
  givenElements: (sel) => `les \xE9l\xE9ments ${sel} concern\xE9s`,
44950
45874
  techniques: "Techniques WCAG",
45875
+ toRuleOn: "Crit\xE8res \xE0 trancher",
45876
+ toRuleOnNote: "Le moteur ne peut pas les d\xE9cider : ils rel\xE8vent du jugement ou du rendu. Ce ne sont PAS des non-conformit\xE9s \u2014 ils sont ind\xE9cid\xE9s. G\xE9n\xE9rez la worklist (`verify --manual --standard <pack>`), qui porte l'\xE9nonc\xE9 complet de chaque test.",
45877
+ tests: "tests",
44951
45878
  docNote: "Document d'exigences (PRD) g\xE9n\xE9r\xE9 depuis l'audit statique : une \xE9pop\xE9e par th\xE8me, une user story par crit\xE8re, des crit\xE8res d'acceptation ancr\xE9s sur les intitul\xE9s WCAG. Adjugez les crit\xE8res \xAB \xE0 \xE9valuer \xBB avec `verify --manual` (agent IA, gat\xE9), le rendu via `scan`."
44952
45879
  },
44953
45880
  en: {
@@ -44978,6 +45905,9 @@ var L2 = {
44978
45905
  acWhen: "a user of assistive technology reaches them",
44979
45906
  givenElements: (sel) => `the affected ${sel} elements`,
44980
45907
  techniques: "WCAG techniques",
45908
+ toRuleOn: "Criteria to rule on",
45909
+ toRuleOnNote: "The engine cannot decide these: they are judgment or rendering calls. They are NOT non-conformities \u2014 they are undecided. Generate the worklist (`verify --manual --standard <pack>`), which carries the full wording of every test.",
45910
+ tests: "tests",
44981
45911
  docNote: "Product-requirements document generated from the static audit: one epic per theme, one user story per criterion, acceptance criteria anchored to the WCAG success-criterion text. Adjudicate the \u201Cto assess\u201D criteria with `verify --manual` (AI agent, gated), rendering via `scan`."
44982
45912
  }
44983
45913
  };
@@ -45100,6 +46030,7 @@ function renderBacklog(r, lang = "en", standard = "wcag") {
45100
46030
  const out2 = header(r, lang, s.title(standardLabel(standard)), void 0, ratePct);
45101
46031
  if (!units.length) {
45102
46032
  out2.push(s.none, "");
46033
+ out2.push(...toRuleOnSection(r, standard, lang));
45103
46034
  return out2.join("\n");
45104
46035
  }
45105
46036
  for (const sev of SEV_ORDER2) {
@@ -45153,6 +46084,22 @@ function acceptanceCriteria(unit, standard, lang, opts = {}) {
45153
46084
  return `${prefix2}**${s.given}** ${s.givenElements(hints)} \xB7 **${s.when}** ${s.acWhen} \xB7 **${s.then}** \xAB ${req} \xBB (WCAG ${sc}).`;
45154
46085
  });
45155
46086
  }
46087
+ function toRuleOnSection(r, standard, lang) {
46088
+ if (isCore(standard)) return [];
46089
+ const s = L2[lang];
46090
+ const pack = loadPack(standard);
46091
+ const manual = derivePackResults(r, standard).filter((pc) => pc.status === "manual");
46092
+ if (!manual.length) return [];
46093
+ const out2 = [`## ${s.toRuleOn} (${manual.length})`, "", `> ${s.toRuleOnNote}`, ""];
46094
+ for (const pc of manual) {
46095
+ const crit = pack.criteria.find((c2) => c2.id === pc.id);
46096
+ const title2 = crit ? titlePlain(pack, crit, lang) : pc.id;
46097
+ const tests = Object.keys(crit?.tests ?? {});
46098
+ out2.push(`- [ ] **${pack.name} ${pc.id}** \u2014 ${title2}${tests.length ? ` \xB7 ${s.tests}: ${tests.map((k) => `\`${pc.id}.${k}\``).join(" ")}` : ""}`);
46099
+ }
46100
+ out2.push("");
46101
+ return out2;
46102
+ }
45156
46103
  function renderPrdDoc(r, lang = "en", standard = "wcag") {
45157
46104
  const s = L2[lang];
45158
46105
  const units = prdUnits(r, standard, lang);
@@ -45162,6 +46109,7 @@ function renderPrdDoc(r, lang = "en", standard = "wcag") {
45162
46109
  out2.push(s.none, "");
45163
46110
  return out2.join("\n");
45164
46111
  }
46112
+ out2.push(...toRuleOnSection(r, standard, lang));
45165
46113
  for (const epic of epicsOf(units, standard, lang)) {
45166
46114
  out2.push(`## ${s.epic} \u2014 ${epic.title}`, "");
45167
46115
  for (const u of epic.units) {
@@ -45183,10 +46131,10 @@ function renderPrdDoc(r, lang = "en", standard = "wcag") {
45183
46131
  return out2.join("\n");
45184
46132
  }
45185
46133
  function writePrd(r, opts) {
45186
- mkdirSync4(opts.out, { recursive: true });
46134
+ mkdirSync5(opts.out, { recursive: true });
45187
46135
  if (opts.format === "doc") {
45188
- const p2 = join24(opts.out, `prd-doc-${r.date}.md`);
45189
- writeFileSync5(p2, renderPrdDoc(r, opts.lang, opts.standard));
46136
+ const p2 = join25(opts.out, `prd-doc-${r.date}.md`);
46137
+ writeFileSync6(p2, renderPrdDoc(r, opts.lang, opts.standard));
45190
46138
  return [p2];
45191
46139
  }
45192
46140
  const remediation = opts.format === "remediation";
@@ -45195,21 +46143,207 @@ function writePrd(r, opts) {
45195
46143
  const files = remediation ? renderPerCriterion(r, opts.lang, opts.standard) : renderAuditorPerCriterion(r, opts.lang, opts.standard, { technical });
45196
46144
  const paths = [];
45197
46145
  for (const f of files) {
45198
- const p2 = join24(opts.out, f.name);
45199
- writeFileSync5(p2, f.content);
46146
+ const p2 = join25(opts.out, f.name);
46147
+ writeFileSync6(p2, f.content);
45200
46148
  paths.push(p2);
45201
46149
  }
45202
46150
  return paths;
45203
46151
  }
45204
- const p = join24(opts.out, `prd-${r.date}.md`);
45205
- writeFileSync5(p, remediation ? renderBacklog(r, opts.lang, opts.standard) : renderAuditorBacklog(r, opts.lang, opts.standard, { technical }));
46152
+ const p = join25(opts.out, `prd-${r.date}.md`);
46153
+ writeFileSync6(p, remediation ? renderBacklog(r, opts.lang, opts.standard) : renderAuditorBacklog(r, opts.lang, opts.standard, { technical }));
45206
46154
  return [p];
45207
46155
  }
45208
46156
 
46157
+ // src/pages.ts
46158
+ function pageScopesFrom(snapshots) {
46159
+ return snapshots.map((s) => ({
46160
+ id: s.meta.id,
46161
+ name: s.meta.name,
46162
+ url: s.meta.url,
46163
+ ...s.meta.auth !== void 0 ? { auth: s.meta.auth } : {},
46164
+ ...s.meta.route ? { route: s.meta.route } : {},
46165
+ ...s.meta.sources ? { sources: s.meta.sources } : {},
46166
+ ...s.meta.notes ? { notes: s.meta.notes } : {},
46167
+ basis: "snapshot"
46168
+ }));
46169
+ }
46170
+ function pageScopesFromSample(sample) {
46171
+ return (sample?.pages ?? []).map((p) => ({
46172
+ id: p.id,
46173
+ name: p.name,
46174
+ url: p.url,
46175
+ ...p.auth !== void 0 ? { auth: p.auth } : {},
46176
+ ...p.notes ? { notes: p.notes } : {},
46177
+ basis: "attributed"
46178
+ }));
46179
+ }
46180
+ function pagesOf(result) {
46181
+ const fromScope = result.scope.pages ?? [];
46182
+ const ids = new Set(fromScope.map((p) => p.id));
46183
+ const urls = new Set(fromScope.map((p) => p.url));
46184
+ const extra = pageScopesFromSample(result.scope.sample).filter((p) => !ids.has(p.id) && !urls.has(p.url));
46185
+ return [...fromScope, ...extra];
46186
+ }
46187
+ function pathMatch2(a, b) {
46188
+ const x = a.split("\\").join("/");
46189
+ const y = b.split("\\").join("/");
46190
+ return x === y || x.endsWith(`/${y}`) || y.endsWith(`/${x}`);
46191
+ }
46192
+ function attributePages(result, pages) {
46193
+ if (!pages.length) return;
46194
+ const byName = new Map(pages.map((p) => [p.name.toLowerCase(), p.id]));
46195
+ const byUrl = new Map(pages.map((p) => [p.url, p.id]));
46196
+ for (const f of [...result.findings, ...result.packFindings ?? []]) {
46197
+ if (f.page) continue;
46198
+ if (isUrlPath(f.file)) {
46199
+ const hit = byUrl.get(f.file);
46200
+ if (hit) f.page = hit;
46201
+ continue;
46202
+ }
46203
+ const sampleName = f.sample?.page?.toLowerCase();
46204
+ if (sampleName && byName.has(sampleName)) {
46205
+ f.page = byName.get(sampleName);
46206
+ continue;
46207
+ }
46208
+ const src = f.origin?.sourceFile ?? f.file;
46209
+ for (const p of pages) {
46210
+ if (p.sources?.some((s) => pathMatch2(src, s))) {
46211
+ f.page = p.id;
46212
+ break;
46213
+ }
46214
+ }
46215
+ }
46216
+ }
46217
+ function unattributedFindings(result) {
46218
+ return result.findings.filter((f) => !f.page);
46219
+ }
46220
+ function pageStatus(c2, pageFindings, basis) {
46221
+ if (pageFindings.some((f) => !f.advisory)) return "NC";
46222
+ if (c2.status === "manual") return "manual";
46223
+ if (c2.status === "NA") return "NA";
46224
+ return basis === "snapshot" ? "C" : "manual";
46225
+ }
46226
+ function pct(criteria) {
46227
+ const c2 = criteria.filter((x) => x.status === "C").length;
46228
+ const nc = criteria.filter((x) => x.status === "NC").length;
46229
+ return c2 + nc === 0 ? 100 : Math.round(c2 / (c2 + nc) * 100);
46230
+ }
46231
+ function derivePages(result, pages) {
46232
+ if (!pages.length) return [];
46233
+ const out2 = [];
46234
+ for (const p of pages) {
46235
+ const own = result.findings.filter((f) => f.page === p.id);
46236
+ const criteria = result.criteria.map((c2) => {
46237
+ const pf = own.filter((f) => f.criteriaId === c2.id);
46238
+ return {
46239
+ id: c2.id,
46240
+ guideline: c2.guideline,
46241
+ status: pageStatus(c2, pf, p.basis),
46242
+ findings: pf,
46243
+ ...c2.justification ? { justification: c2.justification } : {},
46244
+ ...c2.decidedBy ? { decidedBy: c2.decidedBy } : {}
46245
+ };
46246
+ });
46247
+ out2.push({
46248
+ id: p.id,
46249
+ name: p.name,
46250
+ url: p.url,
46251
+ ...p.auth !== void 0 ? { auth: p.auth } : {},
46252
+ basis: p.basis,
46253
+ criteria,
46254
+ findings: own,
46255
+ conformancePct: pct(criteria)
46256
+ });
46257
+ }
46258
+ return out2;
46259
+ }
46260
+ var MARK = { C: "C", NC: "NC", NA: "\u2014", manual: "?" };
46261
+ var L3 = {
46262
+ fr: {
46263
+ title: "Grille par page",
46264
+ note: "Statut de chaque crit\xE8re, page par page. `C` conforme \xB7 `NC` non conforme \xB7 `\u2014` non applicable \xB7 `?` \xE0 \xE9valuer.",
46265
+ criterion: "Crit\xE8re",
46266
+ theme: "Th\xE9matique",
46267
+ none: "Aucune page dans le p\xE9rim\xE8tre : aucun instantan\xE9 (.ultra11y/pages) ni \xE9chantillon scann\xE9.",
46268
+ basisNote: "Une page marqu\xE9e \xAB source \xBB n'a pas d'instantan\xE9 : ses constats proviennent du code, donc l'absence de constat n'y vaut PAS conformit\xE9 \u2014 les crit\xE8res restent \xAB \xE0 \xE9valuer \xBB.",
46269
+ unattributed: (n) => `${n} constat(s) non rattach\xE9(s) \xE0 une page (code partag\xE9, fichier hors routes) \u2014 compt\xE9s dans l'audit global, jamais r\xE9partis d'office.`,
46270
+ rate: "Taux",
46271
+ snapshot: "instantan\xE9",
46272
+ source: "source"
46273
+ },
46274
+ en: {
46275
+ title: "Per-page grid",
46276
+ note: "Each criterion's status, page by page. `C` conforming \xB7 `NC` non-conforming \xB7 `\u2014` not applicable \xB7 `?` to assess.",
46277
+ criterion: "Criterion",
46278
+ theme: "Theme",
46279
+ none: "No page in scope: no snapshot (.ultra11y/pages) and no scanned sample.",
46280
+ basisNote: 'A page marked "source" has no snapshot: its findings come from the code, so the absence of a finding there does NOT mean conforming \u2014 those criteria stay "to assess".',
46281
+ unattributed: (n) => `${n} unattributed finding(s) (shared code, file outside any route) \u2014 counted in the overall audit, never spread across pages.`,
46282
+ rate: "Rate",
46283
+ snapshot: "snapshot",
46284
+ source: "source"
46285
+ }
46286
+ };
46287
+ function pageView(result, page) {
46288
+ return {
46289
+ ...result,
46290
+ criteria: page.criteria,
46291
+ findings: page.findings,
46292
+ ...result.packFindings ? { packFindings: result.packFindings.filter((f) => f.page === page.id) } : {}
46293
+ };
46294
+ }
46295
+ function gridOf(result, derived, standard, lang) {
46296
+ const status = /* @__PURE__ */ new Map();
46297
+ const put = (rowId, pageId, s) => {
46298
+ const m = status.get(rowId) ?? /* @__PURE__ */ new Map();
46299
+ m.set(pageId, s);
46300
+ status.set(rowId, m);
46301
+ };
46302
+ if (isCore(standard)) {
46303
+ const rows2 = [...result.criteria].sort((a, b) => compareSC(a.id, b.id)).map((c2) => ({ id: c2.id, label: `${c2.id} ${scTitle(c2.id, lang) ?? ""}`.trim(), group: c2.guideline }));
46304
+ for (const p of derived) for (const c2 of p.criteria) put(c2.id, p.id, c2.status);
46305
+ return { rows: rows2, status };
46306
+ }
46307
+ const pack = loadPack(standard);
46308
+ const rows = pack.criteria.map((pc) => ({ id: pc.id, label: pc.id, group: `${pc.theme}. ${themeName(pack, pc.theme, lang) ?? ""}`.trim() }));
46309
+ for (const p of derived) for (const pc of derivePackResults(pageView(result, p), standard)) put(pc.id, p.id, pc.status);
46310
+ return { rows, status };
46311
+ }
46312
+ function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
46313
+ const s = L3[lang];
46314
+ const out2 = [];
46315
+ out2.push(`## \u{1F4CA} ${s.title}`, "");
46316
+ const derived = derivePages(result, pages);
46317
+ if (!derived.length) {
46318
+ out2.push(s.none, "");
46319
+ return out2.join("\n");
46320
+ }
46321
+ out2.push(`> ${s.note}`, "");
46322
+ if (derived.some((p) => p.basis === "attributed")) out2.push(`> \u26A0\uFE0F ${s.basisNote}`, "");
46323
+ const head = [isCore(standard) ? s.criterion : s.criterion, ...derived.map((p) => `${p.name}${p.auth ? " \u{1F512}" : ""}`)];
46324
+ out2.push(`| ${head.join(" | ")} |`, `| ${head.map(() => "---").join(" | ")} |`);
46325
+ out2.push(`| **${s.rate}** | ${derived.map((p) => `**${p.conformancePct}%**`).join(" | ")} |`);
46326
+ out2.push(`| _${s.snapshot}?_ | ${derived.map((p) => p.basis === "snapshot" ? `_${s.snapshot}_` : `_${s.source}_`).join(" | ")} |`);
46327
+ const { rows, status } = gridOf(result, derived, standard, lang);
46328
+ let group = "";
46329
+ for (const row of rows) {
46330
+ if (row.group !== group) {
46331
+ group = row.group;
46332
+ out2.push(`| **${group}** | ${derived.map(() => "").join(" | ")} |`);
46333
+ }
46334
+ const cells = derived.map((p) => MARK[status.get(row.id)?.get(p.id) ?? "manual"]);
46335
+ out2.push(`| ${row.label} | ${cells.join(" | ")} |`);
46336
+ }
46337
+ out2.push("");
46338
+ const orphans = unattributedFindings(result);
46339
+ if (orphans.length) out2.push(`> ${s.unattributed(orphans.length)}`, "");
46340
+ return out2.join("\n");
46341
+ }
46342
+
45209
46343
  // src/report.ts
45210
46344
  var ICON3 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
45211
46345
  var SEV_ORDER3 = ["bloquant", "majeur", "mineur"];
45212
- var L3 = {
46346
+ var L4 = {
45213
46347
  fr: {
45214
46348
  title: (std) => `Rapport d'audit d'accessibilit\xE9 \u2014 ${std}`,
45215
46349
  wcagStd: "WCAG 2.2 niveau AA",
@@ -45236,6 +46370,8 @@ var L3 = {
45236
46370
  naTitle: "4. Crit\xE8res non applicables (NA)",
45237
46371
  manualTitle: "5. Crit\xE8res \xE0 adjuger (jugement / rendu) \u2014 non d\xE9cid\xE9s par le moteur statique",
45238
46372
  manualWarn: "Adjugez-les avec `verify --manual` (l'agent d\xE9cide depuis la source, de fa\xE7on gat\xE9e) ; les crit\xE8res de rendu passent par `scan`. Aucun ne doit \xEAtre marqu\xE9 \xAB conforme \xBB sans justification enregistr\xE9e et gat\xE9e.",
46373
+ testsToRule: "tests \xE0 trancher",
46374
+ manualHowTo: "G\xE9n\xE9rez la worklist : `verify --manual --in <audit.json> --standard <pack> --out <dir>`. Chaque item y porte l'\xE9nonc\xE9 complet de ses tests, sa note technique, ses cas particuliers, sa guidance et les termes que le r\xE9f\xE9rentiel d\xE9finit.",
45239
46375
  outOfScope: "Hors p\xE9rim\xE8tre moteur \u2014 mapp\xE9 sur des SC hors WCAG 2.2 AA ; v\xE9rification manuelle.",
45240
46376
  scopedOut: "Les non-conformit\xE9s WCAG relev\xE9es concernent des \xE9l\xE9ments hors du p\xE9rim\xE8tre de ce crit\xE8re \u2014 \xE0 \xE9valuer s\xE9par\xE9ment.",
45241
46377
  nothing: "Aucun.",
@@ -45285,6 +46421,8 @@ var L3 = {
45285
46421
  naTitle: "4. Not-applicable criteria (NA)",
45286
46422
  manualTitle: "5. Criteria to adjudicate (judgment / rendering) \u2014 not decided by the static engine",
45287
46423
  manualWarn: "Adjudicate these with `verify --manual` (the agent decides from source, gated); rendering criteria go to `scan`. None may be marked \u201Cconforming\u201D without a recorded, gated justification.",
46424
+ testsToRule: "tests to rule on",
46425
+ manualHowTo: "Generate the worklist: `verify --manual --in <audit.json> --standard <pack> --out <dir>`. Each item carries the full wording of its tests, its technical note, its particular cases, its guidance and the terms the standard defines.",
45288
46426
  outOfScope: "Out of engine scope \u2014 mapped to SCs outside WCAG 2.2 AA; manual verification.",
45289
46427
  scopedOut: "The WCAG failures found concern elements outside this criterion's scope \u2014 assess separately.",
45290
46428
  nothing: "None.",
@@ -45325,10 +46463,10 @@ function untestedNeedsRendering(r) {
45325
46463
  function partialAuditBanner(lang, untested = NEEDS_RENDERING.map((c2) => c2.sc)) {
45326
46464
  const set = new Set(untested);
45327
46465
  const labels = NEEDS_RENDERING.filter((c2) => set.has(c2.sc)).map((c2) => c2.label[lang]);
45328
- return L3[lang].partialAudit(labels.join(", "));
46466
+ return L4[lang].partialAudit(labels.join(", "));
45329
46467
  }
45330
46468
  function render(r, lang, opts) {
45331
- const s = L3[lang];
46469
+ const s = L4[lang];
45332
46470
  const out2 = [];
45333
46471
  out2.push(`# ${s.title(opts.std)}`, "");
45334
46472
  out2.push(`- **${s.date}** : ${r.date}`);
@@ -45378,6 +46516,8 @@ function render(r, lang, opts) {
45378
46516
  out2.push(`## \u{1F4A1} ${s.recTitle}`, "", `> ${s.recNote}`, "");
45379
46517
  for (const u of advisoryUnits) out2.push(...renderAuditorUnit(u, opts.standard, lang, { heading: "###" }));
45380
46518
  }
46519
+ const pageScope = pagesOf(r);
46520
+ if (pageScope.length) out2.push(renderPageGrid(r, pageScope, opts.standard, lang));
45381
46521
  if (r.scope.sample?.pages.length) {
45382
46522
  out2.push(`## \u{1F4C4} ${s.perPageTitle}`, "", `> ${s.perPageNote}`, "");
45383
46523
  if (r.scope.sample.transverse?.length) out2.push(`> ${s.transverseNote(r.scope.sample.transverse.join(", "))}`, "");
@@ -45405,11 +46545,20 @@ function render(r, lang, opts) {
45405
46545
  out2.push(na.length ? na.map((x) => `- ${x.label}${x.justification ? ` \u2014 _${x.justification}_` : ""}`).join("\n") : s.nothing, "");
45406
46546
  out2.push(`## ${s.manualTitle}`, "", `> ${s.manualWarn}`, "");
45407
46547
  const manual = rows.filter((x) => x.status === "manual");
45408
- out2.push(manual.length ? manual.map((x) => `- ${x.label}${x.justification ? ` \u2014 _${x.justification}_` : ""}`).join("\n") : s.nothing, "");
46548
+ if (!manual.length) out2.push(s.nothing, "");
46549
+ else {
46550
+ const pack5 = isCore(opts.standard) ? void 0 : loadPack(opts.standard);
46551
+ for (const x of manual) {
46552
+ const tests = pack5 ? Object.keys(getCriterion(pack5, x.id)?.tests ?? {}) : [];
46553
+ const testRef = tests.length ? ` \u2014 ${s.testsToRule}: ${tests.map((k) => `\`${x.id}.${k}\``).join(" \xB7 ")}` : "";
46554
+ out2.push(`- ${x.label}${x.justification ? ` \u2014 _${x.justification}_` : ""}${testRef}`);
46555
+ }
46556
+ out2.push("", `> ${s.manualHowTo}`, "");
46557
+ }
45409
46558
  return out2.join("\n");
45410
46559
  }
45411
46560
  function renderReport(r, lang = "en") {
45412
- const s = L3[lang];
46561
+ const s = L4[lang];
45413
46562
  const byGuideline = /* @__PURE__ */ new Map();
45414
46563
  for (const c2 of r.criteria) {
45415
46564
  const title2 = scTitle(c2.id, lang);
@@ -45422,7 +46571,7 @@ function renderReport(r, lang = "en") {
45422
46571
  function renderPackReport(r, pack, lang = "en") {
45423
46572
  const derived = derivePackResults(r, pack.key);
45424
46573
  const std = `${pack.name} ${pack.baseVersion}`;
45425
- const s = L3[lang];
46574
+ const s = L4[lang];
45426
46575
  const naReason = lang === "fr" ? "Aucun crit\xE8re de succ\xE8s WCAG mapp\xE9 n'est applicable dans le p\xE9rim\xE8tre." : "No mapped WCAG success criterion is applicable in scope.";
45427
46576
  const byTheme = /* @__PURE__ */ new Map();
45428
46577
  for (const pr of derived) {
@@ -45441,7 +46590,7 @@ function renderPackReport(r, pack, lang = "en") {
45441
46590
  const groups = pack.themes.map((t2) => ({ key: `${t2.number}.`, title: themeName(pack, t2.number, lang) ?? "", rows: byTheme.get(t2.number) ?? [] }));
45442
46591
  return render(r, lang, {
45443
46592
  std,
45444
- groupHead: L3[lang].byTheme,
46593
+ groupHead: L4[lang].byTheme,
45445
46594
  groups,
45446
46595
  derivedOf: std,
45447
46596
  standard: pack.key,
@@ -45452,9 +46601,9 @@ function renderPackReport(r, pack, lang = "en") {
45452
46601
  function writeReport(r, opts) {
45453
46602
  const core = isCore(opts.standard);
45454
46603
  const md = core ? renderReport(r, opts.lang) : renderPackReport(r, loadPack(opts.standard), opts.lang);
45455
- mkdirSync5(opts.out, { recursive: true });
45456
- const path = join25(opts.out, `${core ? "wcag" : opts.standard}-${r.date}.md`);
45457
- writeFileSync6(path, md);
46604
+ mkdirSync6(opts.out, { recursive: true });
46605
+ const path = join26(opts.out, `${core ? "wcag" : opts.standard}-${r.date}.md`);
46606
+ writeFileSync7(path, md);
45458
46607
  return path;
45459
46608
  }
45460
46609
 
@@ -45596,6 +46745,46 @@ function pushSingleIssue(units, lang, standard = "wcag", format = "audit") {
45596
46745
  }
45597
46746
  return result;
45598
46747
  }
46748
+ function COMMENT_MARKER(standard) {
46749
+ return `<!-- ultra11y:report standard="${standard}" -->`;
46750
+ }
46751
+ function stickyBody(markdown, standard) {
46752
+ return `${COMMENT_MARKER(standard)}
46753
+ ${markdown}`;
46754
+ }
46755
+ function prNumberFromEnv(env = process.env) {
46756
+ const explicit = Number.parseInt(env.ULTRA11Y_PR ?? "", 10);
46757
+ if (Number.isFinite(explicit) && explicit > 0) return explicit;
46758
+ const m = /^refs\/pull\/(\d+)\//.exec(env.GITHUB_REF ?? "");
46759
+ const n = m ? Number.parseInt(m[1], 10) : Number.NaN;
46760
+ return Number.isFinite(n) && n > 0 ? n : void 0;
46761
+ }
46762
+ function pickExistingComment(comments, marker) {
46763
+ return comments.find((c2) => typeof c2.body === "string" && c2.body.includes(marker));
46764
+ }
46765
+ function pushPrComment(markdown, standard = "wcag") {
46766
+ const pr = prNumberFromEnv();
46767
+ if (pr === void 0) return { ok: true, action: "skipped", reason: "not a pull-request run" };
46768
+ const marker = COMMENT_MARKER(standard);
46769
+ const body2 = stickyBody(markdown, standard);
46770
+ try {
46771
+ const repo = gh(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]).trim();
46772
+ let existing;
46773
+ try {
46774
+ const raw = gh(["api", `repos/${repo}/issues/${pr}/comments`, "--paginate"]);
46775
+ existing = pickExistingComment(JSON.parse(raw), marker);
46776
+ } catch {
46777
+ }
46778
+ if (existing) {
46779
+ gh(["api", "--method", "PATCH", `repos/${repo}/issues/comments/${existing.id}`, "-f", `body=${body2}`]);
46780
+ return { ok: true, action: "updated" };
46781
+ }
46782
+ gh(["api", "--method", "POST", `repos/${repo}/issues/${pr}/comments`, "-f", `body=${body2}`]);
46783
+ return { ok: true, action: "created" };
46784
+ } catch (e) {
46785
+ return { ok: false, action: "skipped", reason: ghErrorReason(e) };
46786
+ }
46787
+ }
45599
46788
 
45600
46789
  // src/render.ts
45601
46790
  var KNOWN_LIBS = {
@@ -45626,7 +46815,7 @@ function detectFrameworks(deps, has2) {
45626
46815
  for (const [pkg, label] of Object.entries(KNOWN_LIBS)) if (dep(pkg)) componentLibraries.push(label);
45627
46816
  return { frameworks, componentLibraries };
45628
46817
  }
45629
- var L4 = {
46818
+ var L5 = {
45630
46819
  fr: {
45631
46820
  title: "Obtenir du HTML rendu \xE0 auditer (render)",
45632
46821
  why: "Auditer les sources JSX d'une biblioth\xE8que de composants donne des faux n\xE9gatifs : il faut auditer le HTML r\xE9ellement produit.",
@@ -45657,7 +46846,7 @@ var L4 = {
45657
46846
  }
45658
46847
  };
45659
46848
  function renderPlan(d, lang = "fr") {
45660
- const s = L4[lang];
46849
+ const s = L5[lang];
45661
46850
  const out2 = [`# ${s.title}`, "", `> ${s.why}`, ""];
45662
46851
  if (d.componentLibraries.length) out2.push(`> \u{1F9E9} ${s.libNote(d.componentLibraries.join(", "))}`, "");
45663
46852
  out2.push(`## ${s.detected}`, "");
@@ -45918,8 +47107,40 @@ function runWcag(opts) {
45918
47107
  console.log(opts.json ? JSON.stringify(allSC(), null, 2) : wcagList(opts.lang));
45919
47108
  return 0;
45920
47109
  }
47110
+ var foldTerm = (s) => s.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
47111
+ function runGlossary(opts) {
47112
+ const pack = loadPack(opts.standard);
47113
+ const glossary = packGlossary(opts.standard) ?? {};
47114
+ const anchors = Object.keys(glossary);
47115
+ if (!anchors.length) {
47116
+ console.error(`ultra11y criteria: ${pack.name} ships no glossary.`);
47117
+ return 2;
47118
+ }
47119
+ const term = typeof opts.glossary === "string" ? opts.glossary.trim() : "";
47120
+ if (!term) {
47121
+ if (opts.json) console.log(JSON.stringify(glossary, null, 2));
47122
+ else for (const a of anchors.sort()) console.log(`${a} ${glossary[a]?.title ?? ""}`);
47123
+ return 0;
47124
+ }
47125
+ const want = foldTerm(term);
47126
+ const hit = anchors.find((a) => a === term) ?? anchors.find((a) => foldTerm(a) === want) ?? anchors.find((a) => foldTerm(glossary[a]?.title ?? "") === want) ?? anchors.find((a) => foldTerm(a).startsWith(want));
47127
+ if (!hit) {
47128
+ const near = anchors.filter((a) => foldTerm(a).includes(want) || foldTerm(glossary[a]?.title ?? "").includes(want)).slice(0, 8);
47129
+ console.error(`ultra11y criteria: no ${pack.name} glossary term matching "${term}".${near.length ? ` Did you mean: ${near.join(", ")}?` : ""}`);
47130
+ return 2;
47131
+ }
47132
+ const entry = glossary[hit];
47133
+ if (opts.json) console.log(JSON.stringify({ anchor: hit, ...entry }, null, 2));
47134
+ else {
47135
+ console.log(`${pack.name} \u2014 ${entry.title} (#${hit})`);
47136
+ console.log("");
47137
+ console.log(entry.body);
47138
+ }
47139
+ return 0;
47140
+ }
45921
47141
  function runPack(opts) {
45922
47142
  const pack = loadPack(opts.standard);
47143
+ if (opts.glossary !== void 0 && opts.glossary !== false) return runGlossary(opts);
45923
47144
  if (opts.id) {
45924
47145
  const c2 = getCriterion(pack, opts.id);
45925
47146
  if (!c2) {
@@ -45943,6 +47164,10 @@ function runPack(opts) {
45943
47164
  return 0;
45944
47165
  }
45945
47166
  function runCriteria(opts) {
47167
+ if (opts.glossary !== void 0 && opts.glossary !== false && isCore(opts.standard)) {
47168
+ console.error("ultra11y criteria: --glossary needs a country standard (e.g. --standard rgaa); WCAG ships no glossary here.");
47169
+ return 2;
47170
+ }
45946
47171
  return isCore(opts.standard) ? runWcag(opts) : runPack(opts);
45947
47172
  }
45948
47173
  function renderCriteriaReference() {
@@ -45977,12 +47202,12 @@ function renderCriteriaReference() {
45977
47202
  }
45978
47203
 
45979
47204
  // src/check.ts
45980
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
45981
- import { dirname as dirname8, join as join27 } from "path";
47205
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
47206
+ import { dirname as dirname9, join as join28 } from "path";
45982
47207
 
45983
47208
  // src/verify.ts
45984
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
45985
- import { join as join26 } from "path";
47209
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
47210
+ import { join as join27 } from "path";
45986
47211
  var VERIFY_MAX = 40;
45987
47212
  var plain = (s) => s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
45988
47213
  function auditorCriterionLine(standard) {
@@ -46156,16 +47381,16 @@ function applyVerdicts(items, expected) {
46156
47381
  return { ok: failures.length === 0, total, refuted, unsupported, unadjudicated, invalid, missing, failures };
46157
47382
  }
46158
47383
  function writeWorklist(items, outDir, semantic, standard = "wcag", lang = "en") {
46159
- mkdirSync6(outDir, { recursive: true });
46160
- const todoPath = join26(outDir, "VERIFY.todo.json");
46161
- const mdPath = join26(outDir, "VERIFY.md");
46162
- writeFileSync7(todoPath, JSON.stringify(items, null, 2) + "\n");
46163
- writeFileSync7(mdPath, formatWorklist(items, semantic, standard, lang));
47384
+ mkdirSync7(outDir, { recursive: true });
47385
+ const todoPath = join27(outDir, "VERIFY.todo.json");
47386
+ const mdPath = join27(outDir, "VERIFY.md");
47387
+ writeFileSync8(todoPath, JSON.stringify(items, null, 2) + "\n");
47388
+ writeFileSync8(mdPath, formatWorklist(items, semantic, standard, lang));
46164
47389
  return { todoPath, mdPath, count: items.length };
46165
47390
  }
46166
47391
 
46167
47392
  // src/grounding.ts
46168
- import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
47393
+ import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
46169
47394
  import { resolve as resolve4 } from "path";
46170
47395
  var WINDOW = 10;
46171
47396
  var norm2 = (s) => s.replace(/\s+/g, " ").trim();
@@ -46184,10 +47409,10 @@ function selectorProbes(selector) {
46184
47409
  function groundFinding(g, opts = {}) {
46185
47410
  if (isUnresolvable(g.file)) return { ok: true, moved: false };
46186
47411
  const path = resolve4(opts.cwd ?? process.cwd(), g.file);
46187
- if (!existsSync11(path)) return { ok: false, moved: false, issue: `cited file not found: ${g.file}` };
47412
+ if (!existsSync12(path)) return { ok: false, moved: false, issue: `cited file not found: ${g.file}` };
46188
47413
  let text;
46189
47414
  try {
46190
- text = readFileSync11(path, "utf8");
47415
+ text = readFileSync13(path, "utf8");
46191
47416
  } catch {
46192
47417
  return { ok: false, moved: false, issue: `cited file unreadable: ${g.file}` };
46193
47418
  }
@@ -46300,14 +47525,14 @@ function checkReport(md, standard = "wcag", lang = "en", opts = {}) {
46300
47525
  if (!rateM) {
46301
47526
  issues.push(s.rateMissing);
46302
47527
  } else {
46303
- const pct = parseFloat(rateM[1].replace(",", "."));
46304
- if (pct < 0 || pct > 100) issues.push(s.rateRange(rateM[1]));
47528
+ const pct2 = parseFloat(rateM[1].replace(",", "."));
47529
+ if (pct2 < 0 || pct2 > 100) issues.push(s.rateRange(rateM[1]));
46305
47530
  else if (core) {
46306
47531
  const totals = synthesisTotals(md);
46307
47532
  if (totals) {
46308
47533
  const { c: c2, nc } = totals;
46309
47534
  const expected = c2 + nc === 0 ? 100 : Math.round(c2 / (c2 + nc) * 100);
46310
- if (Math.abs(pct - expected) > 1) issues.push(s.rateInconsistent(rateM[1], expected, c2, nc));
47535
+ if (Math.abs(pct2 - expected) > 1) issues.push(s.rateInconsistent(rateM[1], expected, c2, nc));
46311
47536
  }
46312
47537
  }
46313
47538
  }
@@ -46334,11 +47559,11 @@ function checkSemantic(md, opts) {
46334
47559
  const standard = opts.standard ?? "wcag";
46335
47560
  const s = M[lang];
46336
47561
  const empty = { total: 0, grounded: 0, moved: 0, failed: 0 };
46337
- const artifact = opts.verdictsPath ?? join27(dirname8(opts.reportPath), "VERIFY.todo.json");
46338
- if (!existsSync12(artifact)) return { ok: false, issues: [s.semanticMissing(artifact)], ...empty };
47562
+ const artifact = opts.verdictsPath ?? join28(dirname9(opts.reportPath), "VERIFY.todo.json");
47563
+ if (!existsSync13(artifact)) return { ok: false, issues: [s.semanticMissing(artifact)], ...empty };
46339
47564
  let items;
46340
47565
  try {
46341
- const parsed = JSON.parse(readFileSync12(artifact, "utf8"));
47566
+ const parsed = JSON.parse(readFileSync14(artifact, "utf8"));
46342
47567
  if (!Array.isArray(parsed)) throw new Error("not an array");
46343
47568
  items = parsed;
46344
47569
  } catch {
@@ -46370,8 +47595,8 @@ function sectionBody(md, n) {
46370
47595
  }
46371
47596
 
46372
47597
  // src/adjudicate.ts
46373
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
46374
- import { join as join28 } from "path";
47598
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
47599
+ import { join as join29 } from "path";
46375
47600
 
46376
47601
  // src/data/adjudication.json
46377
47602
  var adjudication_default = {
@@ -47294,49 +48519,91 @@ function docsForAudit(audit2, cwd) {
47294
48519
  const docs = [];
47295
48520
  for (const f of files) {
47296
48521
  try {
47297
- docs.push(parseSource(readText(cwd ? join28(cwd, f) : f), f));
48522
+ docs.push(parseSource(readText(cwd ? join29(cwd, f) : f), f));
47298
48523
  } catch {
47299
48524
  }
47300
48525
  }
47301
48526
  return docs;
47302
48527
  }
48528
+ function blankItem(criteriaId, automatability, title2, harvested) {
48529
+ const evidence = harvested.slice(0, ADJUDICATE_MAX_EVIDENCE);
48530
+ return {
48531
+ criteriaId,
48532
+ automatability,
48533
+ ...title2 ? { title: title2 } : {},
48534
+ evidence,
48535
+ ...harvested.length > ADJUDICATE_MAX_EVIDENCE ? { evidenceTruncated: { shown: evidence.length, total: harvested.length } } : {},
48536
+ verdict: null,
48537
+ justification: "",
48538
+ reason: null,
48539
+ findings: [],
48540
+ recommendations: [],
48541
+ decidedBy: "agent"
48542
+ };
48543
+ }
48544
+ function packEvidence(scs, docs) {
48545
+ const out2 = [];
48546
+ const seen = /* @__PURE__ */ new Set();
48547
+ for (const sc of scs) {
48548
+ for (const e of HARVESTERS[sc]?.(docs) ?? []) {
48549
+ const key = `${e.file}:${e.line}:${e.selector}`;
48550
+ if (seen.has(key)) continue;
48551
+ seen.add(key);
48552
+ out2.push(e);
48553
+ }
48554
+ }
48555
+ return out2;
48556
+ }
47303
48557
  function buildAdjudicationWorklist(audit2, opts = {}) {
47304
48558
  const docs = docsForAudit(audit2, opts.cwd);
47305
- return audit2.residualRisks.map((r) => {
47306
- const harvested = HARVESTERS[r.criteriaId]?.(docs) ?? [];
47307
- const evidence = harvested.slice(0, ADJUDICATE_MAX_EVIDENCE);
47308
- return {
47309
- criteriaId: r.criteriaId,
47310
- automatability: r.automatability,
47311
- title: scTitle(r.criteriaId) ?? void 0,
47312
- evidence,
47313
- ...harvested.length > ADJUDICATE_MAX_EVIDENCE ? { evidenceTruncated: { shown: evidence.length, total: harvested.length } } : {},
47314
- verdict: null,
47315
- justification: "",
47316
- reason: null,
47317
- findings: [],
47318
- recommendations: [],
47319
- decidedBy: "agent"
47320
- };
47321
- });
48559
+ const standard = opts.standard;
48560
+ if (standard !== void 0 && !isCore(standard)) {
48561
+ const pack = loadPack(standard);
48562
+ return derivePackResults(audit2, standard).filter((pc) => pc.status === "manual").map((pc) => {
48563
+ const crit = getCriterion(pack, pc.id);
48564
+ const scs = crit?.wcag ?? pc.scs;
48565
+ const autos = scs.map((sc) => getSC(sc)?.automatability).filter((a) => !!a);
48566
+ const automatability = autos.includes("needs-rendering") ? "needs-rendering" : "judgment";
48567
+ return blankItem(pc.id, automatability, crit ? titlePlain(pack, crit, "fr") : void 0, packEvidence(scs, docs));
48568
+ });
48569
+ }
48570
+ return audit2.residualRisks.map(
48571
+ (r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, HARVESTERS[r.criteriaId]?.(docs) ?? [])
48572
+ );
47322
48573
  }
47323
48574
  var NC_SEVERITY_DEFAULT = "majeur";
47324
48575
  var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
47325
- function normativeRefResolves(ref, standard) {
48576
+ function normativeRefResolves(ref, standard, itemCriterionId) {
47326
48577
  const r = (ref ?? "").trim();
47327
48578
  if (!r) return false;
47328
48579
  if (isCore(standard)) return hasSC(r);
47329
48580
  const pack = loadPack(standard);
47330
- if (hasId(pack, r)) return true;
47331
- const dot = r.lastIndexOf(".");
47332
- if (dot <= 0) return false;
47333
- const crit = getCriterion(pack, r.slice(0, dot));
47334
- return !!crit?.tests && Object.hasOwn(crit.tests, r.slice(dot + 1));
48581
+ let critId = hasId(pack, r) ? r : void 0;
48582
+ let testKey;
48583
+ if (critId === void 0) {
48584
+ const dot = r.lastIndexOf(".");
48585
+ if (dot <= 0) return false;
48586
+ const head = r.slice(0, dot);
48587
+ if (!hasId(pack, head)) return false;
48588
+ critId = head;
48589
+ testKey = r.slice(dot + 1);
48590
+ }
48591
+ const crit = getCriterion(pack, critId);
48592
+ if (!crit) return false;
48593
+ if (testKey !== void 0 && !(crit.tests && Object.hasOwn(crit.tests, testKey))) return false;
48594
+ return itemCriterionId === void 0 || critId === itemCriterionId;
47335
48595
  }
47336
48596
  function applyAdjudication(audit2, adj, opts = {}) {
47337
48597
  const issues = [];
47338
48598
  const byId2 = new Map(adj.items.map((it) => [it.criteriaId, it]));
47339
- for (const r of audit2.residualRisks) if (!byId2.has(r.criteriaId)) issues.push(`criterion ${r.criteriaId}: missing from the adjudication (coverage gap)`);
48599
+ const packMode = !isCore(adj.standard);
48600
+ if (packMode) {
48601
+ for (const pc of derivePackResults(audit2, adj.standard)) {
48602
+ if (pc.status === "manual" && !byId2.has(pc.id)) issues.push(`criterion ${pc.id}: missing from the adjudication (coverage gap)`);
48603
+ }
48604
+ } else {
48605
+ for (const r of audit2.residualRisks) if (!byId2.has(r.criteriaId)) issues.push(`criterion ${r.criteriaId}: missing from the adjudication (coverage gap)`);
48606
+ }
47340
48607
  const groundInputs = [];
47341
48608
  for (const it of adj.items) {
47342
48609
  const v = it.verdict;
@@ -47349,8 +48616,10 @@ function applyAdjudication(audit2, adj, opts = {}) {
47349
48616
  for (const f of it.findings ?? []) {
47350
48617
  if (!f.normativeRef || !f.normativeRef.trim()) {
47351
48618
  issues.push(`criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
47352
- } else if (!normativeRefResolves(f.normativeRef, adj.standard)) {
47353
- issues.push(`criterion ${it.criteriaId}: normativeRef "${f.normativeRef}" does not resolve to a test of ${adj.standard} (fabricated?)`);
48619
+ } else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
48620
+ issues.push(
48621
+ isCore(adj.standard) ? `criterion ${it.criteriaId}: normativeRef "${f.normativeRef}" does not resolve to a test of ${adj.standard} (fabricated?)` : `criterion ${it.criteriaId}: normativeRef "${f.normativeRef}" is not a test of ${adj.standard} ${it.criteriaId} \u2014 cite one of its own tests (e.g. "${it.criteriaId}.1"); a WCAG id looks alike but denotes an unrelated test`
48622
+ );
47354
48623
  }
47355
48624
  groundInputs.push({ file: f.file, line: f.line, selector: f.selector, snippet: f.snippet });
47356
48625
  }
@@ -47372,6 +48641,38 @@ function applyAdjudication(audit2, adj, opts = {}) {
47372
48641
  const newFindings = [];
47373
48642
  let applied = 0;
47374
48643
  let stillManual = 0;
48644
+ if (packMode) {
48645
+ const decided = [];
48646
+ for (const it of adj.items) {
48647
+ if (it.verdict === "manual") {
48648
+ stillManual++;
48649
+ decided.push({
48650
+ id: it.criteriaId,
48651
+ status: "manual",
48652
+ reason: it.reason === "needs-rendered-dom" ? "needs-rendered-dom" : "undecidable",
48653
+ justification: it.reason === "needs-rendered-dom" ? residualScanReason() : residualUndecidableReason(),
48654
+ findings: [],
48655
+ decidedBy: "agent"
48656
+ });
48657
+ continue;
48658
+ }
48659
+ applied++;
48660
+ const fs2 = it.verdict === "NC" ? it.findings.map((f) => agentFinding(it.criteriaId, f)) : [];
48661
+ const recs = (it.recommendations ?? []).map((rec) => agentFinding(it.criteriaId, rec, true));
48662
+ newFindings.push(...fs2, ...recs);
48663
+ decided.push({
48664
+ id: it.criteriaId,
48665
+ status: it.verdict,
48666
+ ...it.verdict === "C" || it.verdict === "NA" ? { justification: it.justification.trim() } : {},
48667
+ findings: [...fs2, ...recs],
48668
+ decidedBy: "agent"
48669
+ });
48670
+ }
48671
+ next.packAdjudication = { standard: adj.standard, criteria: decided };
48672
+ next.findings = [...next.findings, ...newFindings];
48673
+ next.adjudicated = { date: adj.auditDate, applied, stillManual };
48674
+ return { ok: true, audit: next, issues, applied, stillManual, grounding };
48675
+ }
47375
48676
  for (const it of adj.items) {
47376
48677
  const c2 = critById.get(it.criteriaId);
47377
48678
  if (!c2) continue;
@@ -47454,7 +48755,12 @@ var T2 = {
47454
48755
  questions: "\xC0 v\xE9rifier manuellement",
47455
48756
  decide: "R\xE8gle de d\xE9cision",
47456
48757
  na: "Non applicable si",
47457
- refs: "R\xE9f\xE9rences normatives mobilisables (techniques/\xE9checs W3C de ce crit\xE8re)"
48758
+ refs: "R\xE9f\xE9rences normatives mobilisables (techniques/\xE9checs W3C de ce crit\xE8re)",
48759
+ packIntro: (name2) => `R\xE9f\xE9rentiel actif : **${name2}**. Les items ci-dessous sont des crit\xE8res ${name2}, pas des crit\xE8res de succ\xE8s WCAG. Un \`normativeRef\` DOIT citer un test du crit\xE8re de l'item (par ex. \`11.2.1\`) \u2014 un id WCAG y ressemble mais d\xE9signe un tout autre test et sera rejet\xE9.`,
48760
+ packTests: (name2, id) => `Tests ${name2} ${id} \xE0 trancher`,
48761
+ technicalNote: "Note technique",
48762
+ particularCases: "Cas particuliers",
48763
+ glossary: "Termes d\xE9finis par le r\xE9f\xE9rentiel"
47458
48764
  },
47459
48765
  en: {
47460
48766
  title: "# Criteria adjudication (ultra11y)",
@@ -47472,16 +48778,60 @@ var T2 = {
47472
48778
  questions: "To verify manually",
47473
48779
  decide: "Decision rule",
47474
48780
  na: "Not applicable when",
47475
- refs: "Normative references you may cite (this criterion's W3C techniques/failures)"
48781
+ refs: "Normative references you may cite (this criterion's W3C techniques/failures)",
48782
+ packIntro: (name2) => `Active standard: **${name2}**. The items below are ${name2} criteria, not WCAG success criteria. A \`normativeRef\` MUST cite a test OF THE ITEM'S CRITERION (e.g. \`11.2.1\`) \u2014 a WCAG id looks alike but denotes an unrelated test and will be rejected.`,
48783
+ packTests: (name2, id) => `${name2} ${id} tests to rule on`,
48784
+ technicalNote: "Technical note",
48785
+ particularCases: "Particular cases",
48786
+ glossary: "Terms the standard defines"
47476
48787
  }
47477
48788
  };
47478
48789
  var ADJUDICATION = adjudication_default;
47479
48790
  var MAX_REFS = 12;
47480
- function formatAdjudication(items, lang = "en") {
48791
+ var GLOSSARY_REF = /\[[^\]]+\]\(#([^)]+)\)/g;
48792
+ var MAX_GLOSSARY_TERMS = 8;
48793
+ var MAX_GLOSSARY_CHARS = 600;
48794
+ function glossaryAnchorsOf(crit) {
48795
+ if (!crit) return [];
48796
+ const texts = [...Object.values(crit.tests ?? {}).flat(), ...crit.technicalNote ?? [], ...crit.particularCases ?? []];
48797
+ const seen = /* @__PURE__ */ new Set();
48798
+ for (const t2 of texts) {
48799
+ GLOSSARY_REF.lastIndex = 0;
48800
+ for (let m = GLOSSARY_REF.exec(t2); m; m = GLOSSARY_REF.exec(t2)) if (m[1]) seen.add(m[1]);
48801
+ }
48802
+ return [...seen];
48803
+ }
48804
+ function glossaryBlock(pack, crit, lang) {
48805
+ const anchors = glossaryAnchorsOf(crit).slice(0, MAX_GLOSSARY_TERMS);
48806
+ if (!anchors.length) return [];
47481
48807
  const s = T2[lang];
48808
+ const out2 = [`> **${s.glossary}**`, ""];
48809
+ let any = false;
48810
+ for (const a of anchors) {
48811
+ const entry = resolveGlossary(pack.key, a);
48812
+ if (!entry) continue;
48813
+ any = true;
48814
+ const body2 = entry.body.replace(/\s+/g, " ").trim();
48815
+ out2.push(`- **${entry.title}** \u2014 ${body2.length > MAX_GLOSSARY_CHARS ? `${body2.slice(0, MAX_GLOSSARY_CHARS)}\u2026` : body2}`);
48816
+ }
48817
+ out2.push("");
48818
+ return any ? out2 : [];
48819
+ }
48820
+ function packGuidanceBlock(standard, criterionId, lang) {
48821
+ const entries = guidanceForCriterion(standard, criterionId);
48822
+ if (!entries.length) return [];
48823
+ return guidanceExampleBlock(entries, lang);
48824
+ }
48825
+ function plainTest(s) {
48826
+ return s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
48827
+ }
48828
+ function formatAdjudication(items, lang = "en", standard = CORE2) {
48829
+ const s = T2[lang];
48830
+ const pack = isCore(standard) ? void 0 : loadPack(standard);
47482
48831
  const out2 = [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
48832
+ if (pack) out2.push(`> ${s.packIntro(pack.name)}`, "");
47483
48833
  for (const it of items) {
47484
- out2.push(`## ${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
48834
+ out2.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
47485
48835
  out2.push("", `> ${s.evidence} (${it.evidence.length}${it.evidenceTruncated ? ` / ${it.evidenceTruncated.total}` : ""}):`, "");
47486
48836
  if (!it.evidence.length) out2.push(s.none, "");
47487
48837
  else {
@@ -47498,17 +48848,37 @@ function formatAdjudication(items, lang = "en") {
47498
48848
  out2.push("");
47499
48849
  }
47500
48850
  }
47501
- const refs = techniquesFor(it.criteriaId);
47502
- if (refs.length) {
47503
- out2.push(`> ${s.refs}: ${refs.slice(0, MAX_REFS).join(", ")}${refs.length > MAX_REFS ? ` \u2026 (\`criteria ${it.criteriaId}\`)` : ""}`, "");
48851
+ if (pack) {
48852
+ const crit = getCriterion(pack, it.criteriaId);
48853
+ const tests = crit?.tests ?? {};
48854
+ const keys = Object.keys(tests);
48855
+ if (keys.length) {
48856
+ out2.push(`> **${s.packTests(pack.name, it.criteriaId)}**`, "");
48857
+ for (const k of keys) {
48858
+ const lines = tests[k] ?? [];
48859
+ out2.push(`- \`${it.criteriaId}.${k}\` ${plainTest(lines[0] ?? "")}`);
48860
+ for (const line of lines.slice(1)) out2.push(` - ${plainTest(line)}`);
48861
+ }
48862
+ out2.push("");
48863
+ }
48864
+ if (crit?.technicalNote?.length) out2.push(`> **${s.technicalNote}** \u2014 ${crit.technicalNote.map(plainTest).join(" ")}`, "");
48865
+ if (crit?.particularCases?.length) out2.push(`> **${s.particularCases}** \u2014 ${crit.particularCases.map(plainTest).join(" ")}`, "");
48866
+ out2.push(...glossaryBlock(pack, crit, lang));
48867
+ out2.push(...packGuidanceBlock(standard, it.criteriaId, lang));
48868
+ if (keys.length) out2.push(`> ${s.refs}: ${keys.map((k) => `\`${it.criteriaId}.${k}\``).join(", ")}`, "");
48869
+ } else {
48870
+ const refs = techniquesFor(it.criteriaId);
48871
+ if (refs.length) {
48872
+ out2.push(`> ${s.refs}: ${refs.slice(0, MAX_REFS).join(", ")}${refs.length > MAX_REFS ? ` \u2026 (\`criteria ${it.criteriaId}\`)` : ""}`, "");
48873
+ }
47504
48874
  }
47505
48875
  }
47506
48876
  return out2.join("\n");
47507
48877
  }
47508
48878
  function writeAdjudication(items, outDir, opts) {
47509
- mkdirSync7(outDir, { recursive: true });
47510
- const todoPath = join28(outDir, "ADJUDICATE.todo.json");
47511
- const mdPath = join28(outDir, "ADJUDICATE.md");
48879
+ mkdirSync8(outDir, { recursive: true });
48880
+ const todoPath = join29(outDir, "ADJUDICATE.todo.json");
48881
+ const mdPath = join29(outDir, "ADJUDICATE.md");
47512
48882
  const file = {
47513
48883
  tool: "ultra11y",
47514
48884
  kind: "adjudication",
@@ -47517,16 +48887,16 @@ function writeAdjudication(items, outDir, opts) {
47517
48887
  auditDate: opts.auditDate,
47518
48888
  items
47519
48889
  };
47520
- writeFileSync8(todoPath, JSON.stringify(file, null, 2) + "\n");
47521
- writeFileSync8(mdPath, formatAdjudication(items, opts.lang ?? "en"));
48890
+ writeFileSync9(todoPath, JSON.stringify(file, null, 2) + "\n");
48891
+ writeFileSync9(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
47522
48892
  return { todoPath, mdPath, count: items.length };
47523
48893
  }
47524
48894
 
47525
48895
  // src/scan.ts
47526
48896
  import { execFileSync as execFileSync3 } from "child_process";
47527
- import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync9, existsSync as existsSync14, statSync as statSync7, readdirSync as readdirSync4, rmSync as rmSync3, readFileSync as readFileSync13 } from "fs";
48897
+ import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync10, existsSync as existsSync15, statSync as statSync7, readdirSync as readdirSync5, rmSync as rmSync3, readFileSync as readFileSync15 } from "fs";
47528
48898
  import { tmpdir } from "os";
47529
- import { join as join29, resolve as resolve5 } from "path";
48899
+ import { join as join30, resolve as resolve5 } from "path";
47530
48900
  import { fileURLToPath as fileURLToPath3 } from "url";
47531
48901
 
47532
48902
  // src/axe-map.ts
@@ -47768,7 +49138,7 @@ async function crawlUrls(start2, opts) {
47768
49138
  }
47769
49139
 
47770
49140
  // src/sample.ts
47771
- import { existsSync as existsSync13 } from "fs";
49141
+ import { existsSync as existsSync14 } from "fs";
47772
49142
  function looksLikeTarget(u) {
47773
49143
  return /^https?:\/\//i.test(u) || /^(\.\.?[/\\]|[/\\])/.test(u) || /\.x?html?$/i.test(u);
47774
49144
  }
@@ -47804,7 +49174,7 @@ function validateSample(raw) {
47804
49174
  if (p.auth !== void 0 && typeof p.auth !== "boolean") err2(`sample.pages[${i2}].auth`, "auth must be a boolean");
47805
49175
  if (p.storageState !== void 0 && (typeof p.storageState !== "string" || p.storageState.trim() === "")) {
47806
49176
  err2(`sample.pages[${i2}].storageState`, "storageState must be a non-empty file path string");
47807
- } else if (typeof p.storageState === "string" && p.storageState.trim() !== "" && !existsSync13(p.storageState)) {
49177
+ } else if (typeof p.storageState === "string" && p.storageState.trim() !== "" && !existsSync14(p.storageState)) {
47808
49178
  warn(
47809
49179
  `sample.pages[${i2}].storageState`,
47810
49180
  `storageState path not found: "${p.storageState}" (resolved from the current directory) \u2014 the scan will not be able to load this session`
@@ -47929,11 +49299,11 @@ function imageExists(tag) {
47929
49299
  }
47930
49300
  var CTX_PREFIX = "ultra11y-dyn-";
47931
49301
  function buildImage(tag = IMAGE_TAG) {
47932
- const ctx = mkdtempSync2(join29(tmpdir(), CTX_PREFIX));
49302
+ const ctx = mkdtempSync2(join30(tmpdir(), CTX_PREFIX));
47933
49303
  try {
47934
- writeFileSync9(join29(ctx, "runner.mjs"), RUNNER);
47935
- writeFileSync9(join29(ctx, "package.json"), PKG);
47936
- writeFileSync9(join29(ctx, "Dockerfile"), DOCKERFILE);
49304
+ writeFileSync10(join30(ctx, "runner.mjs"), RUNNER);
49305
+ writeFileSync10(join30(ctx, "package.json"), PKG);
49306
+ writeFileSync10(join30(ctx, "Dockerfile"), DOCKERFILE);
47937
49307
  execFileSync3("docker", ["build", "-t", tag, ctx], { stdio: "inherit", timeout: 9e5 });
47938
49308
  } finally {
47939
49309
  rmSync3(ctx, { recursive: true, force: true });
@@ -47942,9 +49312,9 @@ function buildImage(tag = IMAGE_TAG) {
47942
49312
  function cleanTempContexts() {
47943
49313
  let removed = 0;
47944
49314
  const dir = tmpdir();
47945
- for (const name2 of readdirSync4(dir)) {
49315
+ for (const name2 of readdirSync5(dir)) {
47946
49316
  if (!name2.startsWith(CTX_PREFIX)) continue;
47947
- rmSync3(join29(dir, name2), { recursive: true, force: true });
49317
+ rmSync3(join30(dir, name2), { recursive: true, force: true });
47948
49318
  removed++;
47949
49319
  }
47950
49320
  return removed;
@@ -48054,8 +49424,8 @@ function toDynamicResult(out2, target, lang = "en", engine = "axe-core@playwrigh
48054
49424
  }
48055
49425
  var DOCKER_TESTED_SCS = ["1.4.10"];
48056
49426
  function runScan(opts) {
48057
- const isUrl = /^https?:\/\//i.test(opts.target);
48058
- if (!isUrl && !existsSync14(opts.target)) {
49427
+ const isUrl3 = /^https?:\/\//i.test(opts.target);
49428
+ if (!isUrl3 && !existsSync15(opts.target)) {
48059
49429
  throw new Error(`File not found: ${opts.target}. Pass an http(s):// URL or an existing HTML file.`);
48060
49430
  }
48061
49431
  if (!dockerAvailable()) {
@@ -48063,7 +49433,7 @@ function runScan(opts) {
48063
49433
  }
48064
49434
  const tag = opts.tag ?? IMAGE_TAG;
48065
49435
  if (!imageExists(tag)) buildImage(tag);
48066
- const isFile = !isUrl && existsSync14(opts.target) && statSync7(opts.target).isFile();
49436
+ const isFile = !isUrl3 && existsSync15(opts.target) && statSync7(opts.target).isFile();
48067
49437
  const out2 = runRunner(opts.target, isFile, tag);
48068
49438
  return { ...toDynamicResult(out2, opts.target), testedScs: [...DOCKER_TESTED_SCS] };
48069
49439
  }
@@ -48141,10 +49511,10 @@ async function runCrawlScan(opts) {
48141
49511
  var sevRank = { bloquant: 3, majeur: 2, mineur: 1 };
48142
49512
  function resolveHostAnchor(file, snippet2) {
48143
49513
  const s = snippet2?.trim();
48144
- if (!s || !existsSync14(file)) return null;
49514
+ if (!s || !existsSync15(file)) return null;
48145
49515
  let source;
48146
49516
  try {
48147
- source = readFileSync13(file, "utf8");
49517
+ source = readFileSync15(file, "utf8");
48148
49518
  } catch {
48149
49519
  return null;
48150
49520
  }
@@ -48240,7 +49610,7 @@ function mergeDynamic(audit2, dynamic, lang = "en") {
48240
49610
  }
48241
49611
 
48242
49612
  // src/scan-local.ts
48243
- import { existsSync as existsSync15, statSync as statSync8 } from "fs";
49613
+ import { existsSync as existsSync16, statSync as statSync8 } from "fs";
48244
49614
  import { createRequire } from "module";
48245
49615
  import { resolve as resolve6 } from "path";
48246
49616
  var LOCAL_ENGINE = "axe-core@playwright (local)";
@@ -48795,11 +50165,11 @@ async function runOnPage(browser, AxeBuilder, target, isFile, opts) {
48795
50165
  }
48796
50166
  }
48797
50167
  async function runScanLocal(opts) {
48798
- const isUrl = /^https?:\/\//i.test(opts.target);
48799
- if (!isUrl && !existsSync15(opts.target)) {
50168
+ const isUrl3 = /^https?:\/\//i.test(opts.target);
50169
+ if (!isUrl3 && !existsSync16(opts.target)) {
48800
50170
  throw new Error(`File not found: ${opts.target}. Pass an http(s):// URL or an existing HTML file.`);
48801
50171
  }
48802
- const isFile = !isUrl && statSync8(opts.target).isFile();
50172
+ const isFile = !isUrl3 && statSync8(opts.target).isFile();
48803
50173
  const lang = opts.lang ?? "en";
48804
50174
  const interact = opts.interact !== false;
48805
50175
  const { chromium, AxeBuilder } = resolveLocalDeps(opts.cwd);
@@ -48877,7 +50247,7 @@ async function runCrawlScanLocal(opts) {
48877
50247
  }
48878
50248
 
48879
50249
  // src/fix.ts
48880
- import { writeFileSync as writeFileSync10 } from "fs";
50250
+ import { writeFileSync as writeFileSync11 } from "fs";
48881
50251
 
48882
50252
  // src/fix/edits.ts
48883
50253
  function openTag(source, start2) {
@@ -49091,7 +50461,7 @@ function fixOne(file, source, opts, canWrite = true) {
49091
50461
  regression = after.length > findings.length || after.some((f) => !beforeKinds.has(f.ruleId));
49092
50462
  if (opts.write && !regression && file !== "<stdin>") {
49093
50463
  if (canWrite) {
49094
- writeFileSync10(file, output);
50464
+ writeFileSync11(file, output);
49095
50465
  written = true;
49096
50466
  } else {
49097
50467
  skippedPartialStage = true;
@@ -49214,9 +50584,9 @@ function baselineSummary(diff, lang = "fr") {
49214
50584
  }
49215
50585
 
49216
50586
  // src/init.ts
49217
- import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync8, chmodSync } from "fs";
50587
+ import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync9, chmodSync } from "fs";
49218
50588
  import { execFileSync as execFileSync4 } from "child_process";
49219
- import { join as join30 } from "path";
50589
+ import { join as join31 } from "path";
49220
50590
  var EN_SEV = { bloquant: "blocking", majeur: "major", mineur: "minor" };
49221
50591
  function repoRoot() {
49222
50592
  try {
@@ -49269,35 +50639,61 @@ function ciWorkflow(enginePath, failOn) {
49269
50639
  return `name: a11y
49270
50640
  # Generated by \`ultra11y init --ci\`. Fails only on NEW non-conformities >= ${EN_SEV[failOn]}
49271
50641
  # introduced by the PR (vs audits/baseline.json) \u2014 not the existing backlog.
50642
+ #
50643
+ # Findings are reported IN the pull request: SARIF \u2192 code scanning puts each one on the line
50644
+ # of code that caused it, and \`::error::\` annotations cover repositories without Advanced
50645
+ # Security. Set \`comment: 'true'\` to also get a sticky summary comment (needs
50646
+ # \`pull-requests: write\`), and \`standard: 'rgaa'\` to report against a country standard.
50647
+ #
50648
+ # The action is PINNED to the engine version that generated this file, so a CI run stays
50649
+ # reproducible. \`maxgfr/ultra11y@v${VERSION.split(".")[0]}\` also resolves (a moving major
50650
+ # tag) if you would rather track the major line and take fixes automatically.
49272
50651
  on:
49273
50652
  pull_request:
50653
+
50654
+ permissions:
50655
+ contents: read
50656
+ security-events: write # SARIF upload; drop it and the annotations still report
50657
+ # pull-requests: write # uncomment together with \`comment: 'true'\` below
50658
+
49274
50659
  jobs:
49275
50660
  ultra11y:
49276
50661
  runs-on: ubuntu-latest
49277
50662
  steps:
49278
50663
  - uses: actions/checkout@v4
49279
50664
  with:
49280
- fetch-depth: 0
49281
- - uses: actions/setup-node@v4
50665
+ fetch-depth: 0 # the diff gate needs the base ref
50666
+ - uses: maxgfr/ultra11y@v${VERSION}
49282
50667
  with:
49283
- node-version: 20
49284
- - name: ultra11y accessibility regression gate
49285
- run: node "${enginePath}" audit --since "origin/\${{ github.base_ref }}" --baseline audits/baseline.json --fail-on ${EN_SEV[failOn]}
50668
+ since: auto # the PR's base branch
50669
+ baseline: audits/baseline.json
50670
+ fail-on: ${EN_SEV[failOn]}
50671
+ # standard: rgaa
50672
+ # comment: 'true'
50673
+ #
50674
+ # Page-by-page, with a real browser (computed contrast, focus, zoom, reflow):
50675
+ # start: npm run start
50676
+ # wait-on: http://localhost:3000
50677
+ # urls: http://localhost:3000 http://localhost:3000/contact
50678
+ # \u2026or \`sample: 'true'\` to scan the sample declared in .ultra11yrc.json.
50679
+ #
50680
+ # Vendored alternative \u2014 no marketplace action, just the bundle in this repo:
50681
+ # - run: node "${enginePath}" audit --since "origin/\${{ github.base_ref }}" --baseline audits/baseline.json --fail-on ${EN_SEV[failOn]}
49286
50682
  `;
49287
50683
  }
49288
50684
  function writeHook(root, enginePath, failOn, mode = "staged") {
49289
- const dir = join30(root, ".git", "hooks");
49290
- mkdirSync8(dir, { recursive: true });
49291
- const path = join30(dir, "pre-commit");
49292
- writeFileSync11(path, mode === "baseline" ? hookScript(enginePath, failOn) : stagedHookScript(enginePath, failOn));
50685
+ const dir = join31(root, ".git", "hooks");
50686
+ mkdirSync9(dir, { recursive: true });
50687
+ const path = join31(dir, "pre-commit");
50688
+ writeFileSync12(path, mode === "baseline" ? hookScript(enginePath, failOn) : stagedHookScript(enginePath, failOn));
49293
50689
  chmodSync(path, 493);
49294
50690
  return path;
49295
50691
  }
49296
50692
  function writeCi(root, enginePath, failOn) {
49297
- const dir = join30(root, ".github", "workflows");
49298
- mkdirSync8(dir, { recursive: true });
49299
- const path = join30(dir, "a11y.yml");
49300
- writeFileSync11(path, ciWorkflow(enginePath, failOn));
50693
+ const dir = join31(root, ".github", "workflows");
50694
+ mkdirSync9(dir, { recursive: true });
50695
+ const path = join31(dir, "a11y.yml");
50696
+ writeFileSync12(path, ciWorkflow(enginePath, failOn));
49301
50697
  return path;
49302
50698
  }
49303
50699
 
@@ -49380,12 +50776,826 @@ function captureCoverageSummary(cov, lang) {
49380
50776
  return lines.join("\n");
49381
50777
  }
49382
50778
 
50779
+ // src/sarif.ts
50780
+ var INFO_URI = "https://github.com/maxgfr/ultra11y";
50781
+ var SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
50782
+ var FINGERPRINT_KEY = "ultra11yFindingId/v1";
50783
+ var LEVEL = { bloquant: "error", majeur: "warning", mineur: "note" };
50784
+ function levelFor(f) {
50785
+ return f.advisory ? "note" : LEVEL[f.severity];
50786
+ }
50787
+ var toUri = repoRelative;
50788
+ var isUrl = isUrlPath;
50789
+ function physicalLocation(f, baseDir) {
50790
+ if (isUrl(f.file)) return [];
50791
+ return [
50792
+ {
50793
+ physicalLocation: {
50794
+ artifactLocation: { uri: toUri(f.file, baseDir) },
50795
+ region: {
50796
+ // SARIF regions are 1-based. A merged dynamic finding can carry line 0 when the
50797
+ // host-anchor resolver declined to guess (src/scan.ts resolveHostAnchor) — clamp
50798
+ // rather than emit an invalid region.
50799
+ startLine: Math.max(1, f.line),
50800
+ startColumn: Math.max(1, f.col),
50801
+ ...f.snippet ? { snippet: { text: f.snippet } } : {}
50802
+ }
50803
+ }
50804
+ }
50805
+ ];
50806
+ }
50807
+ function relatedLocations(f, baseDir) {
50808
+ const r = f.related;
50809
+ if (!r || isUrl(r.file)) return void 0;
50810
+ return [
50811
+ { physicalLocation: { artifactLocation: { uri: toUri(r.file, baseDir) }, region: { startLine: Math.max(1, r.line), startColumn: Math.max(1, r.col) } } }
50812
+ ];
50813
+ }
50814
+ function criterionLabel(f, standard) {
50815
+ if (isCore(standard)) return `WCAG ${f.criteriaId}`;
50816
+ const pack = loadPack(standard);
50817
+ const ids = packCriteriaForFinding(pack, f);
50818
+ return ids.length ? `${pack.name} ${ids.join(", ")}` : `WCAG ${f.criteriaId}`;
50819
+ }
50820
+ function ruleFor(f, standard, lang) {
50821
+ const sc = f.criteriaId;
50822
+ const title2 = scTitle(sc, lang);
50823
+ const tags = ["accessibility", `wcag:${sc}`];
50824
+ if (!isCore(standard)) {
50825
+ const pack = loadPack(standard);
50826
+ for (const id of packCriteriaForFinding(pack, f)) tags.push(`${pack.key}:${id}`);
50827
+ }
50828
+ if (f.advisory) tags.push("recommendation");
50829
+ const level = levelFor(f);
50830
+ return {
50831
+ id: f.ruleId,
50832
+ shortDescription: { text: title2 ? `${f.ruleId} \u2014 WCAG ${sc} ${title2}` : `${f.ruleId} \u2014 WCAG ${sc}` },
50833
+ fullDescription: { text: resolveRemediation(f, lang) },
50834
+ ...understanding(sc) ? { helpUri: understanding(sc) } : {},
50835
+ defaultConfiguration: { level },
50836
+ properties: { tags, "problem.severity": level }
50837
+ };
50838
+ }
50839
+ function toSarif(result, opts = {}) {
50840
+ const standard = opts.standard ?? CORE2;
50841
+ const lang = opts.lang ?? "en";
50842
+ const baseDir = opts.baseDir ?? process.cwd();
50843
+ const rules = [];
50844
+ const indexOf = /* @__PURE__ */ new Map();
50845
+ const results = [];
50846
+ for (const f of findingsForStandard(result, standard)) {
50847
+ let idx = indexOf.get(f.ruleId);
50848
+ if (idx === void 0) {
50849
+ idx = rules.length;
50850
+ indexOf.set(f.ruleId, idx);
50851
+ rules.push(ruleFor(f, standard, lang));
50852
+ }
50853
+ const related = relatedLocations(f, baseDir);
50854
+ const page = f.sample?.page;
50855
+ const properties = { criterion: criterionLabel(f, standard) };
50856
+ if (isUrl(f.file)) properties.url = f.file;
50857
+ if (page) properties.page = page;
50858
+ if (f.advisory) properties.advisory = true;
50859
+ if (f.preliminary) properties.preliminary = true;
50860
+ results.push({
50861
+ ruleId: f.ruleId,
50862
+ ruleIndex: idx,
50863
+ level: levelFor(f),
50864
+ message: { text: `[${criterionLabel(f, standard)}] ${resolveMessage(f, lang)} \u2014 ${resolveRemediation(f, lang)}` },
50865
+ locations: physicalLocation(f, baseDir),
50866
+ ...related ? { relatedLocations: related } : {},
50867
+ partialFingerprints: { [FINGERPRINT_KEY]: findingId(f) },
50868
+ properties
50869
+ });
50870
+ }
50871
+ return {
50872
+ $schema: SCHEMA,
50873
+ version: "2.1.0",
50874
+ runs: [
50875
+ {
50876
+ tool: { driver: { name: "ultra11y", version: VERSION, informationUri: INFO_URI, rules } },
50877
+ // Distinguishes concurrent uploads in one PR (source audit vs page scan) so
50878
+ // GitHub keeps them as separate analyses instead of overwriting one another.
50879
+ automationDetails: { id: `ultra11y/${standard}/` },
50880
+ results
50881
+ }
50882
+ ]
50883
+ };
50884
+ }
50885
+
50886
+ // src/annotate.ts
50887
+ var LEVEL2 = { bloquant: "error", majeur: "warning", mineur: "notice" };
50888
+ var ICON6 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
50889
+ var SEV_ORDER5 = ["bloquant", "majeur", "mineur"];
50890
+ function esc(s) {
50891
+ return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
50892
+ }
50893
+ function escProp(s) {
50894
+ return esc(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
50895
+ }
50896
+ var isUrl2 = isUrlPath;
50897
+ function criterionLabel2(f, standard) {
50898
+ if (isCore(standard)) return `WCAG ${f.criteriaId}`;
50899
+ const pack = loadPack(standard);
50900
+ const ids = packCriteriaForFinding(pack, f);
50901
+ return ids.length ? `${pack.name} ${ids.join(", ")}` : `WCAG ${f.criteriaId}`;
50902
+ }
50903
+ function annotations(result, opts = {}) {
50904
+ const standard = opts.standard ?? CORE2;
50905
+ const lang = opts.lang ?? "en";
50906
+ const baseDir = opts.baseDir ?? process.cwd();
50907
+ const all = findingsForStandard(result, standard);
50908
+ const scoped = opts.failOn ? findingsAtOrAbove(all, opts.failOn) : all;
50909
+ const out2 = [];
50910
+ for (const f of scoped) {
50911
+ if (isUrl2(f.file)) continue;
50912
+ const level = f.advisory ? "notice" : LEVEL2[f.severity];
50913
+ const file = repoRelative(f.file, baseDir);
50914
+ const title2 = `${criterionLabel2(f, standard)} \xB7 ${f.ruleId}`;
50915
+ const body2 = `${resolveMessage(f, lang)}
50916
+ ${resolveRemediation(f, lang)}`;
50917
+ out2.push(`::${level} file=${escProp(file)},line=${Math.max(1, f.line)},col=${Math.max(1, f.col)},title=${escProp(title2)}::${esc(body2)}`);
50918
+ }
50919
+ return out2;
50920
+ }
50921
+ var S = {
50922
+ fr: {
50923
+ title: "Audit d'accessibilit\xE9 ultra11y",
50924
+ files: "fichiers",
50925
+ rate: "r\xE9ussite automatique",
50926
+ none: "\u2705 Aucune non-conformit\xE9 d\xE9tect\xE9e par le moteur statique.",
50927
+ findings: "Non-conformit\xE9s",
50928
+ severity: "S\xE9v\xE9rit\xE9",
50929
+ criterion: "Crit\xE8re",
50930
+ where: "Emplacement",
50931
+ what: "Constat",
50932
+ more: (n) => `\u2026 et ${n} autre(s).`,
50933
+ perPage: "Constats par page",
50934
+ page: "Page",
50935
+ count: "Constats",
50936
+ unanchored: (n) => `${n} constat(s) rattach\xE9(s) \xE0 une URL, sans ligne de code \xE0 annoter \u2014 voir le rapport.`
50937
+ },
50938
+ en: {
50939
+ title: "ultra11y accessibility audit",
50940
+ files: "files",
50941
+ rate: "automatic pass rate",
50942
+ none: "\u2705 No non-conformity detected by the static engine.",
50943
+ findings: "Non-conformities",
50944
+ severity: "Severity",
50945
+ criterion: "Criterion",
50946
+ where: "Location",
50947
+ what: "Finding",
50948
+ more: (n) => `\u2026 and ${n} more.`,
50949
+ perPage: "Findings per page",
50950
+ page: "Page",
50951
+ count: "Findings",
50952
+ unanchored: (n) => `${n} finding(s) keyed to a URL, with no code line to annotate \u2014 see the report.`
50953
+ }
50954
+ };
50955
+ var MAX_ROWS = 50;
50956
+ function stepSummary(result, opts = {}) {
50957
+ const standard = opts.standard ?? CORE2;
50958
+ const lang = opts.lang ?? "en";
50959
+ const s = S[lang];
50960
+ const stdLabel = isCore(standard) ? "WCAG 2.2 AA" : loadPack(standard).name;
50961
+ const out2 = [];
50962
+ out2.push(`## ${s.title} \u2014 ${stdLabel}`, "");
50963
+ out2.push(`\`${result.date}\` \xB7 ${result.scope.files} ${s.files} \xB7 **${result.conformancePct}%** ${s.rate}`, "");
50964
+ const baseDir = opts.baseDir ?? process.cwd();
50965
+ const all = findingsForStandard(result, standard);
50966
+ if (!all.length) {
50967
+ out2.push(s.none, "");
50968
+ return out2.join("\n");
50969
+ }
50970
+ const sorted = [...all].sort((a, b) => SEV_ORDER5.indexOf(a.severity) - SEV_ORDER5.indexOf(b.severity));
50971
+ out2.push(`### ${s.findings} (${sorted.length})`, "");
50972
+ out2.push(`| ${s.severity} | ${s.criterion} | ${s.where} | ${s.what} |`, "| --- | --- | --- | --- |");
50973
+ for (const f of sorted.slice(0, MAX_ROWS)) {
50974
+ const where = isUrl2(f.file) ? f.file : `${repoRelative(f.file, baseDir)}:${Math.max(1, f.line)}`;
50975
+ const msg = resolveMessage(f, lang).replace(/\|/g, "\\|");
50976
+ out2.push(`| ${ICON6[f.severity]} ${f.severity} | ${criterionLabel2(f, standard)} | \`${where}\` | ${msg} |`);
50977
+ }
50978
+ if (sorted.length > MAX_ROWS) out2.push("", s.more(sorted.length - MAX_ROWS));
50979
+ out2.push("");
50980
+ const unanchored = all.filter((f) => isUrl2(f.file)).length;
50981
+ if (unanchored) out2.push(`> ${s.unanchored(unanchored)}`, "");
50982
+ const pages = result.scope.sample?.pages ?? [];
50983
+ if (pages.length) {
50984
+ out2.push(`### ${s.perPage}`, "");
50985
+ out2.push(`| ${s.page} | ${s.count} |`, "| --- | --- |");
50986
+ for (const pg of pages) {
50987
+ const n = all.filter((f) => f.file === pg.url || f.sample?.page !== void 0 && f.sample.page === pg.name).length;
50988
+ out2.push(`| ${pg.name} \u2014 \`${pg.url}\` | ${n} |`);
50989
+ }
50990
+ out2.push("");
50991
+ }
50992
+ return out2.join("\n");
50993
+ }
50994
+
50995
+ // src/dev.ts
50996
+ import { createServer } from "http";
50997
+ import { join as join32 } from "path";
50998
+ var DEV_DEFAULT_PORT = 4111;
50999
+ function criterionLabel3(f, standard) {
51000
+ if (isCore(standard)) return `WCAG ${f.criteriaId}`;
51001
+ const pack = loadPack(standard);
51002
+ const ids = packCriteriaForFinding(pack, f);
51003
+ return ids.length ? `${pack.name} ${ids.join(", ")}` : `WCAG ${f.criteriaId}`;
51004
+ }
51005
+ function overlayJs() {
51006
+ return `(() => {
51007
+ const ENDPOINT = window.__ULTRA11Y_ENDPOINT__ || "http://127.0.0.1:${DEV_DEFAULT_PORT}";
51008
+ const HOST_ID = "__ultra11y_overlay";
51009
+ if (document.getElementById(HOST_ID)) return;
51010
+
51011
+ const host = document.createElement("div");
51012
+ host.id = HOST_ID;
51013
+ const shadow = host.attachShadow({ mode: "open" });
51014
+ const style = document.createElement("style");
51015
+ style.textContent = \`
51016
+ :host { all: initial; }
51017
+ .bar { position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;
51018
+ font: 13px/1.4 ui-sans-serif, system-ui, sans-serif; }
51019
+ button.fab { border: 0; border-radius: 999px; padding: 10px 14px; cursor: pointer;
51020
+ background: #1c1c1e; color: #fff; box-shadow: 0 2px 12px rgba(0,0,0,.35); }
51021
+ button.fab[data-state="ok"] { background: #0a7d33; }
51022
+ button.fab[data-state="bad"] { background: #b3261e; }
51023
+ .panel { position: fixed; right: 16px; bottom: 64px; width: min(460px, calc(100vw - 32px));
51024
+ max-height: min(60vh, 560px); overflow: auto; background: #fff; color: #111;
51025
+ border-radius: 10px; box-shadow: 0 8px 32px rgba(0,0,0,.28); padding: 12px 14px; }
51026
+ @media (prefers-color-scheme: dark) { .panel { background: #1c1c1e; color: #f2f2f7; } }
51027
+ h2 { font-size: 13px; margin: 0 0 8px; }
51028
+ ul { list-style: none; margin: 0; padding: 0; }
51029
+ li { padding: 7px 0; border-top: 1px solid rgba(127,127,127,.25); }
51030
+ .sev { font-weight: 700; margin-right: 6px; }
51031
+ .b { color: #b3261e; } .m { color: #b06000; } .n { color: #666; }
51032
+ a { color: inherit; }
51033
+ code { font: 11px/1.4 ui-monospace, monospace; opacity: .8; }
51034
+ .empty { opacity: .75; }
51035
+ \`;
51036
+ const root = document.createElement("div");
51037
+ root.className = "bar";
51038
+ const fab = document.createElement("button");
51039
+ fab.className = "fab";
51040
+ fab.type = "button";
51041
+ fab.textContent = "a11y \u2026";
51042
+ const panel = document.createElement("div");
51043
+ panel.className = "panel";
51044
+ panel.hidden = true;
51045
+ root.append(fab, panel);
51046
+ shadow.append(style, root);
51047
+ document.body.appendChild(host);
51048
+
51049
+ fab.addEventListener("click", () => { panel.hidden = !panel.hidden; });
51050
+
51051
+ const SEV = { bloquant: ["b", "BLOQUANT"], majeur: ["m", "MAJEUR"], mineur: ["n", "mineur"] };
51052
+
51053
+ function render(findings, error) {
51054
+ if (error) {
51055
+ fab.textContent = "a11y ?";
51056
+ fab.dataset.state = "";
51057
+ panel.innerHTML = "<h2>ultra11y</h2><p class='empty'>" + String(error) + "</p>";
51058
+ return;
51059
+ }
51060
+ const nc = findings.filter((f) => !f.advisory);
51061
+ fab.textContent = nc.length ? "a11y " + nc.length : "a11y \u2713";
51062
+ fab.dataset.state = nc.length ? "bad" : "ok";
51063
+ if (!findings.length) {
51064
+ panel.innerHTML = "<h2>ultra11y</h2><p class='empty'>No non-conformity detected on this page by the static engine. The judgment criteria remain yours.</p>";
51065
+ return;
51066
+ }
51067
+ const h = document.createElement("h2");
51068
+ h.textContent = "ultra11y \u2014 " + nc.length + " non-conformity(ies) on this page";
51069
+ const ul = document.createElement("ul");
51070
+ for (const f of findings.slice(0, 60)) {
51071
+ const li = document.createElement("li");
51072
+ const s = SEV[f.severity] || ["n", f.severity];
51073
+ const sev = document.createElement("span");
51074
+ sev.className = "sev " + s[0];
51075
+ sev.textContent = f.advisory ? "reco" : s[1];
51076
+ li.append(sev, document.createTextNode(f.message));
51077
+ const where = (f.origin && f.origin.sourceFile) || f.file;
51078
+ const line = (f.origin && f.origin.sourceLine) || f.line || 1;
51079
+ if (where && !/^https?:/.test(where)) {
51080
+ const a = document.createElement("a");
51081
+ // Next exposes this endpoint in dev; it opens the file in the configured editor.
51082
+ a.href = "/__nextjs_launch-editor?file=" + encodeURIComponent(where + ":" + line + ":1");
51083
+ a.textContent = where + ":" + line;
51084
+ a.addEventListener("click", (e) => { e.preventDefault(); fetch(a.href).catch(() => {}); });
51085
+ const code = document.createElement("code");
51086
+ code.append(document.createElement("br"), a);
51087
+ li.append(code);
51088
+ }
51089
+ ul.append(li);
51090
+ }
51091
+ panel.replaceChildren(h, ul);
51092
+ }
51093
+
51094
+ let busy = false;
51095
+ async function check() {
51096
+ if (busy) return;
51097
+ busy = true;
51098
+ fab.textContent = "a11y \u2026";
51099
+ // Detach the overlay so it is neither serialized nor counted: its presence would shift
51100
+ // every document-order index by one and break the styles/DOM join.
51101
+ const parent = host.parentNode;
51102
+ if (parent) parent.removeChild(host);
51103
+ let collected;
51104
+ try {
51105
+ collected = (0, eval)(${JSON.stringify(COLLECT_SNAPSHOT)});
51106
+ } finally {
51107
+ if (parent) parent.appendChild(host);
51108
+ busy = false;
51109
+ }
51110
+ try {
51111
+ const res = await fetch(ENDPOINT + "/snapshot", {
51112
+ method: "POST",
51113
+ headers: { "content-type": "application/json" },
51114
+ body: JSON.stringify({
51115
+ meta: { v: 1, id: slug(location.pathname), name: document.title || location.pathname, url: location.href, runner: "dev" },
51116
+ dom: collected.dom, styles: collected.styles, boxes: collected.boxes, css: collected.css,
51117
+ }),
51118
+ });
51119
+ const json = await res.json();
51120
+ render(json.findings || [], json.error);
51121
+ } catch (e) {
51122
+ render([], "ultra11y dev server unreachable at " + ENDPOINT + " \u2014 run \`ultra11y dev\`.");
51123
+ }
51124
+ }
51125
+
51126
+ function slug(path) {
51127
+ const s = path.normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
51128
+ return s || (path === "/" ? "accueil" : "page");
51129
+ }
51130
+
51131
+ // Re-check on client-side navigation: patch the history methods a router uses, plus back/forward.
51132
+ let timer;
51133
+ const schedule = () => { clearTimeout(timer); timer = setTimeout(check, 400); };
51134
+ for (const m of ["pushState", "replaceState"]) {
51135
+ const orig = history[m];
51136
+ history[m] = function () { const r = orig.apply(this, arguments); schedule(); return r; };
51137
+ }
51138
+ addEventListener("popstate", schedule);
51139
+ schedule();
51140
+ })();`;
51141
+ }
51142
+ function nextOverlayComponent(port) {
51143
+ return `"use client";
51144
+ // ultra11y dev overlay for Next.js. Generated by \`ultra11y dev --next\`.
51145
+ //
51146
+ // // app/layout.tsx
51147
+ // import { Ultra11yOverlay } from "../.ultra11y/next/overlay";
51148
+ // \u2026
51149
+ // <body>{children}<Ultra11yOverlay /></body>
51150
+ //
51151
+ // Renders NOTHING in production: the whole component short-circuits on NODE_ENV, so shipping
51152
+ // it is inert. Start the side-car with \`ultra11y dev\` for it to have anything to talk to.
51153
+ import { useEffect } from "react";
51154
+
51155
+ export function Ultra11yOverlay({ endpoint = "http://127.0.0.1:${port}" }) {
51156
+ useEffect(() => {
51157
+ if (process.env.NODE_ENV !== "development") return;
51158
+ if (document.getElementById("__ultra11y_overlay_script")) return;
51159
+ window.__ULTRA11Y_ENDPOINT__ = endpoint;
51160
+ const s = document.createElement("script");
51161
+ s.id = "__ultra11y_overlay_script";
51162
+ s.src = endpoint + "/overlay.js";
51163
+ document.body.appendChild(s);
51164
+ }, [endpoint]);
51165
+ return null;
51166
+ }
51167
+
51168
+ export default Ultra11yOverlay;
51169
+ `;
51170
+ }
51171
+ var MARK2 = { C: "C", NC: "NC", NA: "\u2014", manual: "?" };
51172
+ var CLASS = { C: "c", NC: "nc", NA: "na", manual: "m" };
51173
+ function esc2(s) {
51174
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
51175
+ }
51176
+ function dashboardHtml(result, pages, standard, lang) {
51177
+ const fr = lang === "fr";
51178
+ const stdLabel = isCore(standard) ? "WCAG 2.2 AA" : loadPack(standard).name;
51179
+ const head = `<!doctype html><html lang="${fr ? "fr" : "en"}"><head><meta charset="utf-8">
51180
+ <meta name="viewport" content="width=device-width, initial-scale=1">
51181
+ <title>ultra11y \u2014 ${esc2(stdLabel)}</title>
51182
+ <style>
51183
+ :root { color-scheme: light dark; --bg:#fff; --fg:#111; --line:#e3e3e6; --mut:#666; }
51184
+ @media (prefers-color-scheme: dark) { :root { --bg:#141416; --fg:#f2f2f7; --line:#2c2c2e; --mut:#a1a1a6; } }
51185
+ body { margin:0; padding:24px; background:var(--bg); color:var(--fg); font:14px/1.5 ui-sans-serif,system-ui,sans-serif; }
51186
+ h1 { font-size:18px; margin:0 0 4px; } p.sub { color:var(--mut); margin:0 0 20px; }
51187
+ table { border-collapse:collapse; width:100%; } th,td { padding:5px 8px; border-bottom:1px solid var(--line); text-align:left; }
51188
+ th { position:sticky; top:0; background:var(--bg); }
51189
+ td.c,td.nc,td.na,td.m { text-align:center; font-weight:600; width:80px; }
51190
+ td.c{color:#0a7d33} td.nc{color:#b3261e} td.na{color:var(--mut)} td.m{color:#b06000}
51191
+ tr.theme td { font-weight:700; background:color-mix(in srgb, var(--fg) 6%, transparent); }
51192
+ .rate { font-size:22px; font-weight:700; }
51193
+ .empty { color:var(--mut); max-width:60ch; }
51194
+ code { font:12px ui-monospace,monospace; color:var(--mut); }
51195
+ </style></head><body>`;
51196
+ if (!result || !pages.length) {
51197
+ return `${head}<h1>ultra11y</h1>
51198
+ <p class="empty">${fr ? "Aucune page captur\xE9e pour l'instant. Ouvrez votre application avec l'overlay actif : chaque page visit\xE9e appara\xEEtra ici." : "No page captured yet. Open your app with the overlay active: every page you visit shows up here."}</p></body></html>`;
51199
+ }
51200
+ const rows = [];
51201
+ const status = /* @__PURE__ */ new Map();
51202
+ if (isCore(standard)) {
51203
+ for (const c2 of result.criteria) rows.push({ id: c2.id, label: c2.id, group: c2.guideline });
51204
+ for (const p of pages) for (const c2 of p.criteria) (status.get(c2.id) ?? status.set(c2.id, /* @__PURE__ */ new Map()).get(c2.id)).set(p.id, c2.status);
51205
+ } else {
51206
+ const pack = loadPack(standard);
51207
+ for (const pc of pack.criteria) rows.push({ id: pc.id, label: pc.id, group: `${pc.theme}. ${themeName(pack, pc.theme, lang) ?? ""}` });
51208
+ for (const p of pages) {
51209
+ const view = { ...result, criteria: p.criteria, findings: p.findings };
51210
+ for (const pc of derivePackResults(view, standard)) (status.get(pc.id) ?? status.set(pc.id, /* @__PURE__ */ new Map()).get(pc.id)).set(p.id, pc.status);
51211
+ }
51212
+ }
51213
+ const out2 = [head];
51214
+ out2.push(`<h1>ultra11y \u2014 ${esc2(stdLabel)}</h1>`);
51215
+ out2.push(`<p class="sub">${pages.length} ${fr ? "page(s) captur\xE9e(s)" : "page(s) captured"} \xB7 <code>${esc2(result.date)}</code></p>`);
51216
+ out2.push("<table><thead><tr><th></th>");
51217
+ for (const p of pages) out2.push(`<th>${esc2(p.name)}${p.auth ? " \u{1F512}" : ""}<br><code>${esc2(p.url)}</code></th>`);
51218
+ out2.push("</tr></thead><tbody>");
51219
+ out2.push(`<tr><td>${fr ? "Taux" : "Rate"}</td>${pages.map((p) => `<td class="rate">${p.conformancePct}%</td>`).join("")}</tr>`);
51220
+ let group = "";
51221
+ for (const row of rows) {
51222
+ if (row.group !== group) {
51223
+ group = row.group;
51224
+ out2.push(`<tr class="theme"><td colspan="${pages.length + 1}">${esc2(group)}</td></tr>`);
51225
+ }
51226
+ const cells = pages.map((p) => {
51227
+ const s = status.get(row.id)?.get(p.id) ?? "manual";
51228
+ return `<td class="${CLASS[s]}">${MARK2[s]}</td>`;
51229
+ });
51230
+ out2.push(`<tr><td>${esc2(row.label)}</td>${cells.join("")}</tr>`);
51231
+ }
51232
+ out2.push("</tbody></table></body></html>");
51233
+ return out2.join("\n");
51234
+ }
51235
+ function cors(res) {
51236
+ res.setHeader("access-control-allow-origin", "*");
51237
+ res.setHeader("access-control-allow-headers", "content-type");
51238
+ res.setHeader("access-control-allow-methods", "POST, GET, OPTIONS");
51239
+ }
51240
+ async function readBody(req, limit = 64 * 1024 * 1024) {
51241
+ const chunks = [];
51242
+ let size = 0;
51243
+ for await (const c2 of req) {
51244
+ const buf = c2;
51245
+ size += buf.length;
51246
+ if (size > limit) throw new Error("payload too large");
51247
+ chunks.push(buf);
51248
+ }
51249
+ return Buffer.concat(chunks).toString("utf8");
51250
+ }
51251
+ function auditCollected(root, payload) {
51252
+ const v = validateSnapshotMeta(payload.meta);
51253
+ if (!v.ok || !v.meta) return { ok: false, error: v.issues.map((i2) => `${i2.path}: ${i2.message}`).join("; ") };
51254
+ if (typeof payload.dom !== "string" || !payload.dom.trim()) return { ok: false, error: "dom is required" };
51255
+ const dir = writeSnapshot(root, {
51256
+ meta: v.meta,
51257
+ dom: payload.dom,
51258
+ ...payload.styles ? { styles: payload.styles } : {},
51259
+ ...payload.boxes ? { boxes: payload.boxes } : {},
51260
+ ...payload.axtree ? { axtree: payload.axtree } : {},
51261
+ ...payload.css ? { css: payload.css } : {}
51262
+ });
51263
+ return { ok: true, result: runAudit({ inputs: [join32(dir, "dom.html")] }) };
51264
+ }
51265
+ function projectPages(root) {
51266
+ const snaps = readSnapshots(root);
51267
+ if (!snaps.length) return { result: null, pages: [] };
51268
+ const scope = pageScopesFrom(snaps);
51269
+ const result = runAudit({ inputs: [join32(root, ".ultra11y/pages")] });
51270
+ result.scope.pages = scope;
51271
+ attributePages(result, scope);
51272
+ return { result, pages: derivePages(result, scope) };
51273
+ }
51274
+ function startDevServer(opts) {
51275
+ const log = opts.onLog ?? (() => {
51276
+ });
51277
+ const server = createServer((req, res) => {
51278
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
51279
+ cors(res);
51280
+ if (req.method === "OPTIONS") {
51281
+ res.writeHead(204).end();
51282
+ return;
51283
+ }
51284
+ if (url.pathname === "/overlay.js") {
51285
+ res.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "cache-control": "no-store" }).end(overlayJs());
51286
+ return;
51287
+ }
51288
+ if (url.pathname === "/snapshot" && req.method === "POST") {
51289
+ void (async () => {
51290
+ try {
51291
+ const body2 = await readBody(req);
51292
+ const payload = JSON.parse(body2);
51293
+ const r = auditCollected(opts.root, payload);
51294
+ if (!r.ok) {
51295
+ res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: r.error }));
51296
+ return;
51297
+ }
51298
+ const nc = r.result.findings.filter((f) => !f.advisory);
51299
+ const name2 = payload.meta?.name ?? "page";
51300
+ log(opts.lang === "fr" ? `ultra11y dev : ${name2} \u2014 ${nc.length} non-conformit\xE9(s)` : `ultra11y dev: ${name2} \u2014 ${nc.length} non-conformity(ies)`);
51301
+ for (const f of nc.slice(0, 20)) {
51302
+ const where = f.origin?.sourceFile ?? f.file;
51303
+ log(
51304
+ ` ${f.severity} ${criterionLabel3(f, opts.standard)} \xB7 ${where}:${Math.max(1, f.origin?.sourceLine ?? f.line)} \u2014 ${resolveMessage(f, opts.lang)}`
51305
+ );
51306
+ }
51307
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ findings: r.result.findings }));
51308
+ } catch (e) {
51309
+ res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
51310
+ }
51311
+ })();
51312
+ return;
51313
+ }
51314
+ if (url.pathname === "/" || url.pathname === "/index.html") {
51315
+ const { result, pages } = projectPages(opts.root);
51316
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(dashboardHtml(result, pages, opts.standard, opts.lang));
51317
+ return;
51318
+ }
51319
+ res.writeHead(404, { "content-type": "text/plain" }).end("not found");
51320
+ });
51321
+ return new Promise((resolve10, reject) => {
51322
+ server.once("error", reject);
51323
+ server.listen(opts.port, "127.0.0.1", () => {
51324
+ resolve10({
51325
+ port: server.address().port,
51326
+ close: () => new Promise((r) => server.close(() => r()))
51327
+ });
51328
+ });
51329
+ });
51330
+ }
51331
+
51332
+ // src/e2e.ts
51333
+ function detectE2eRunner(deps, has2) {
51334
+ const dep = (n) => Object.hasOwn(deps, n);
51335
+ const out2 = [];
51336
+ if (dep("@playwright/test") || dep("playwright") || ["ts", "js", "mjs", "cjs"].some((e) => has2(`playwright.config.${e}`))) out2.push("playwright");
51337
+ if (dep("cypress") || ["ts", "js", "mjs", "cjs"].some((e) => has2(`cypress.config.${e}`))) out2.push("cypress");
51338
+ return out2;
51339
+ }
51340
+ function runnerCore(enginePath) {
51341
+ return `import { spawnSync } from "node:child_process";
51342
+ import { createRequire } from "node:module";
51343
+
51344
+ // Engine resolution, in order: the ULTRA11Y env override (same convention as the git hook),
51345
+ // then the path baked in when \`render --e2e\` generated this file.
51346
+ const ENGINE = process.env.ULTRA11Y || ${JSON.stringify(enginePath)};
51347
+
51348
+ /** Persist a collected page as a snapshot and audit it. Returns the AuditResult. */
51349
+ export function auditSnapshot(payload) {
51350
+ const res = spawnSync(process.execPath, [ENGINE, "snapshot", "write", "--json"], {
51351
+ input: JSON.stringify(payload),
51352
+ encoding: "utf8",
51353
+ maxBuffer: 64 * 1024 * 1024,
51354
+ });
51355
+ if (res.error) throw new Error("ultra11y: could not run the engine at " + ENGINE + " \u2014 " + res.error.message);
51356
+ if (!res.stdout) throw new Error("ultra11y: the engine produced no output (exit " + res.status + ")\\n" + (res.stderr || ""));
51357
+ try {
51358
+ return JSON.parse(res.stdout);
51359
+ } catch {
51360
+ throw new Error("ultra11y: could not parse the engine output\\n" + res.stdout.slice(0, 500));
51361
+ }
51362
+ }
51363
+
51364
+ const RANK = { bloquant: 0, majeur: 1, mineur: 2 };
51365
+ const THRESHOLD = { blocking: 0, bloquant: 0, major: 1, majeur: 1, minor: 2, mineur: 2 };
51366
+
51367
+ /** Findings at or above the threshold, ignoring non-normative recommendations. */
51368
+ export function failingFindings(result, failOn) {
51369
+ const max = THRESHOLD[failOn];
51370
+ if (max === undefined) throw new Error('ultra11y: failOn must be blocking|major|minor (got "' + failOn + '")');
51371
+ return (result.findings || []).filter((f) => !f.advisory && RANK[f.severity] <= max);
51372
+ }
51373
+
51374
+ export function formatFailure(pageName, failing) {
51375
+ const lines = ["ultra11y: " + failing.length + " accessibility non-conformity(ies) on \\"" + pageName + "\\":"];
51376
+ for (const f of failing.slice(0, 20)) {
51377
+ lines.push(" [" + f.severity + "] " + f.ruleId + " (WCAG " + f.criteriaId + ") \u2014 " + (f.origin && f.origin.sourceFile ? f.origin.sourceFile : f.file) + " \u2014 " + f.message);
51378
+ }
51379
+ if (failing.length > 20) lines.push(" \u2026 and " + (failing.length - 20) + " more.");
51380
+ lines.push("Full detail: .ultra11y/pages/ \u2014 re-audit offline with \`ultra11y audit\`.");
51381
+ return lines.join("\\n");
51382
+ }
51383
+ `;
51384
+ }
51385
+ function playwrightFixture(enginePath) {
51386
+ return `// ultra11y \u2014 Playwright integration. Generated by \`ultra11y render --e2e\`.
51387
+ //
51388
+ // import { test, checkA11y } from "../.ultra11y/e2e/playwright.mjs";
51389
+ //
51390
+ // test("home page is accessible", async ({ page, ultra11y }) => {
51391
+ // await page.goto("/");
51392
+ // await ultra11y({ as: "accueil" }); // via the fixture
51393
+ // });
51394
+ //
51395
+ // // \u2026or without the fixture:
51396
+ // await checkA11y(page, { as: "accueil", failOn: "blocking" });
51397
+ //
51398
+ // Each checked page is persisted to .ultra11y/pages/<id>/ (DOM + computed styles + boxes),
51399
+ // so the SAME page can be re-audited later, offline, with no browser \u2014 that is what lets CI
51400
+ // and the RGAA report speak page by page. Commit the snapshots to gate on them.
51401
+ //
51402
+ // Options: { as?: string (page id), name?: string, failOn?: "blocking"|"major"|"minor"|false,
51403
+ // auth?: boolean, sources?: string[], notes?: string }
51404
+ // \`failOn: false\` records the snapshot without ever failing the test.
51405
+ ${runnerCore(enginePath)}
51406
+ const COLLECT = ${JSON.stringify(COLLECT_SNAPSHOT)};
51407
+
51408
+ export async function checkA11y(page, opts = {}) {
51409
+ const collected = await page.evaluate(COLLECT);
51410
+ // A VIEWPORT screenshot, deliberately \u2014 the boxes come from getBoundingClientRect, which is
51411
+ // viewport-relative, so a full-page capture would put the two coordinate systems out of
51412
+ // step. It feeds the pixel tier: contrast where the CSSOM cannot answer (text over an
51413
+ // image or a gradient). \`screenshot: false\` skips it.
51414
+ let shot;
51415
+ if (opts.screenshot !== false) {
51416
+ try {
51417
+ shot = (await page.screenshot({ fullPage: false })).toString("base64");
51418
+ } catch {
51419
+ // A screenshot failure must never fail the accessibility check itself.
51420
+ }
51421
+ }
51422
+ const url = collected.url || page.url();
51423
+ const id = opts.as || slugify(url);
51424
+ const payload = {
51425
+ meta: {
51426
+ v: 1,
51427
+ id: id,
51428
+ name: opts.name || collected.title || id,
51429
+ url: url,
51430
+ runner: "playwright",
51431
+ viewport: collected.viewport,
51432
+ capturedAt: new Date().toISOString(),
51433
+ ...(opts.auth !== undefined ? { auth: opts.auth } : {}),
51434
+ ...(opts.sources ? { sources: opts.sources } : {}),
51435
+ ...(opts.notes ? { notes: opts.notes } : {}),
51436
+ },
51437
+ dom: collected.dom,
51438
+ styles: collected.styles,
51439
+ boxes: collected.boxes,
51440
+ css: collected.css,
51441
+ ...(shot ? { screenshot: shot } : {}),
51442
+ };
51443
+ const result = auditSnapshot(payload);
51444
+ const failOn = opts.failOn === undefined ? "blocking" : opts.failOn;
51445
+ if (failOn !== false) {
51446
+ const failing = failingFindings(result, failOn);
51447
+ if (failing.length) throw new Error(formatFailure(payload.meta.name, failing));
51448
+ }
51449
+ return result;
51450
+ }
51451
+
51452
+ // Same slug rule as the engine (src/snapshot.ts slugifyPageId): accent-folded URL path.
51453
+ function slugify(url) {
51454
+ let path = url;
51455
+ try {
51456
+ path = new URL(url).pathname;
51457
+ } catch {}
51458
+ const slug = path.normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
51459
+ return slug || (path === "/" || path === "" ? "accueil" : "page");
51460
+ }
51461
+
51462
+ // Optional fixture form. Import \`test\` from here instead of @playwright/test.
51463
+ // Playwright is resolved through createRequire (this file is ESM, so \`require\` does not
51464
+ // exist) and lazily, so the module still loads in a project that has not installed it.
51465
+ export const test = (() => {
51466
+ try {
51467
+ const base = createRequire(import.meta.url)("@playwright/test").test;
51468
+ return base.extend({
51469
+ ultra11y: async ({ page }, use) => {
51470
+ await use((opts) => checkA11y(page, opts));
51471
+ },
51472
+ });
51473
+ } catch {
51474
+ return undefined;
51475
+ }
51476
+ })();
51477
+ `;
51478
+ }
51479
+ function cypressPlugin(enginePath) {
51480
+ return `// ultra11y \u2014 Cypress plugin (Node side). Generated by \`ultra11y render --e2e\`.
51481
+ //
51482
+ // // cypress.config.js
51483
+ // import ultra11y from "./.ultra11y/e2e/cypress-plugin.mjs";
51484
+ // export default defineConfig({ e2e: { setupNodeEvents(on, config) { ultra11y(on); return config; } } });
51485
+ //
51486
+ // Cypress test code runs in the BROWSER and cannot write to disk, so the collected page
51487
+ // round-trips through this task. The browser half is cypress-commands.mjs.
51488
+ ${runnerCore(enginePath)}
51489
+ export default function register(on) {
51490
+ on("task", {
51491
+ ultra11ySnapshot(payload) {
51492
+ const result = auditSnapshot(payload);
51493
+ const failOn = payload.failOn === undefined ? "blocking" : payload.failOn;
51494
+ const failing = failOn === false ? [] : failingFindings(result, failOn);
51495
+ // Return rather than throw: the browser half raises the assertion, so the failure is
51496
+ // attributed to the test rather than to the plugin.
51497
+ return { findings: result.findings || [], failing: failing, message: failing.length ? formatFailure(payload.meta.name, failing) : "" };
51498
+ },
51499
+ });
51500
+ }
51501
+ `;
51502
+ }
51503
+ function cypressCommands() {
51504
+ return `// ultra11y \u2014 Cypress command (browser side). Generated by \`ultra11y render --e2e\`.
51505
+ //
51506
+ // // cypress/support/e2e.js (the supportFile)
51507
+ // import "../../.ultra11y/e2e/cypress-commands.mjs";
51508
+ //
51509
+ // cy.visit("/");
51510
+ // cy.ultra11y({ as: "accueil" });
51511
+ //
51512
+ // Options: { as?, name?, failOn?: "blocking"|"major"|"minor"|false, auth?, sources?, notes? }
51513
+ const COLLECT = ${JSON.stringify(COLLECT_SNAPSHOT)};
51514
+
51515
+ Cypress.Commands.add("ultra11y", (opts = {}) => {
51516
+ cy.window({ log: false }).then((win) => {
51517
+ // eslint-disable-next-line no-eval
51518
+ const collected = win.eval(COLLECT);
51519
+ const url = collected.url || win.location.href;
51520
+ const id = opts.as || slugify(url);
51521
+ const payload = {
51522
+ meta: {
51523
+ v: 1,
51524
+ id: id,
51525
+ name: opts.name || collected.title || id,
51526
+ url: url,
51527
+ runner: "cypress",
51528
+ viewport: collected.viewport,
51529
+ capturedAt: new Date().toISOString(),
51530
+ ...(opts.auth !== undefined ? { auth: opts.auth } : {}),
51531
+ ...(opts.sources ? { sources: opts.sources } : {}),
51532
+ ...(opts.notes ? { notes: opts.notes } : {}),
51533
+ },
51534
+ dom: collected.dom,
51535
+ styles: collected.styles,
51536
+ boxes: collected.boxes,
51537
+ css: collected.css,
51538
+ failOn: opts.failOn,
51539
+ };
51540
+ return cy.task("ultra11ySnapshot", payload, { log: false }).then((res) => {
51541
+ if (res && res.failing && res.failing.length) throw new Error(res.message);
51542
+ return res;
51543
+ });
51544
+ });
51545
+ });
51546
+
51547
+ function slugify(url) {
51548
+ let path = url;
51549
+ try {
51550
+ path = new URL(url).pathname;
51551
+ } catch (e) {}
51552
+ const slug = path.normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
51553
+ return slug || (path === "/" || path === "" ? "accueil" : "page");
51554
+ }
51555
+ `;
51556
+ }
51557
+ function e2eSetupPlan(runners, paths, lang = "en") {
51558
+ const fr = lang === "fr";
51559
+ const out2 = [];
51560
+ if (!runners.length) {
51561
+ out2.push(
51562
+ fr ? "Aucun runner E2E d\xE9tect\xE9 (ni Playwright, ni Cypress). Installez-en un, ou forcez l'\xE9criture avec --runner playwright|cypress." : "No E2E runner detected (neither Playwright nor Cypress). Install one, or force the files with --runner playwright|cypress."
51563
+ );
51564
+ return out2.join("\n");
51565
+ }
51566
+ if (paths.playwright) {
51567
+ out2.push(fr ? `Fixture Playwright \xE9crite : ${paths.playwright}` : `Playwright fixture written: ${paths.playwright}`);
51568
+ out2.push("");
51569
+ out2.push(fr ? "Dans un test :" : "In a test:");
51570
+ out2.push(` import { test, checkA11y } from "${paths.playwright}";`);
51571
+ out2.push(` await page.goto("/");`);
51572
+ out2.push(` await checkA11y(page, { as: "accueil" });`);
51573
+ out2.push("");
51574
+ }
51575
+ if (paths.cypressPlugin) {
51576
+ out2.push(fr ? `Plugin Cypress \xE9crit : ${paths.cypressPlugin}` : `Cypress plugin written: ${paths.cypressPlugin}`);
51577
+ if (paths.cypressCommands) out2.push(fr ? `Commande Cypress \xE9crite : ${paths.cypressCommands}` : `Cypress command written: ${paths.cypressCommands}`);
51578
+ out2.push("");
51579
+ out2.push(fr ? "Dans cypress.config.js (setupNodeEvents) :" : "In cypress.config.js (setupNodeEvents):");
51580
+ out2.push(` import ultra11y from "./${paths.cypressPlugin}";`);
51581
+ out2.push(` setupNodeEvents(on, config) { ultra11y(on); return config; }`);
51582
+ out2.push(fr ? "Puis dans le supportFile :" : "Then in the supportFile:");
51583
+ out2.push(` import "${paths.cypressCommands ? `../../${paths.cypressCommands}` : ""}";`);
51584
+ out2.push(fr ? "Dans un test : cy.visit('/'); cy.ultra11y({ as: 'accueil' });" : "In a test: cy.visit('/'); cy.ultra11y({ as: 'accueil' });");
51585
+ out2.push("");
51586
+ }
51587
+ out2.push(
51588
+ fr ? "Chaque page v\xE9rifi\xE9e est enregistr\xE9e dans .ultra11y/pages/<id>/ \u2014 committez-la pour auditer la vraie page hors ligne (CI, rapport page par page)." : "Every checked page is recorded in .ultra11y/pages/<id>/ \u2014 commit it to audit the real page offline (CI, per-page report)."
51589
+ );
51590
+ return out2.join("\n");
51591
+ }
51592
+
49383
51593
  // src/config.ts
49384
- import { existsSync as existsSync17, statSync as statSync9 } from "fs";
49385
- import { join as join31, isAbsolute as isAbsolute2 } from "path";
51594
+ import { existsSync as existsSync18, statSync as statSync9 } from "fs";
51595
+ import { join as join33, isAbsolute as isAbsolute2 } from "path";
49386
51596
 
49387
51597
  // src/pack.ts
49388
- import { existsSync as existsSync16 } from "fs";
51598
+ import { existsSync as existsSync17 } from "fs";
49389
51599
  function exampleParses(ex) {
49390
51600
  for (const code2 of [ex.bad, ex.good]) {
49391
51601
  if (!code2) continue;
@@ -49456,7 +51666,7 @@ function checkGuidance(ds, pack) {
49456
51666
  function runPackCheck(packPath, guidancePath) {
49457
51667
  const errors = [];
49458
51668
  const warnings = [];
49459
- if (!existsSync16(packPath)) return { ok: false, errors: [`pack file not found: ${packPath}`], warnings };
51669
+ if (!existsSync17(packPath)) return { ok: false, errors: [`pack file not found: ${packPath}`], warnings };
49460
51670
  let raw;
49461
51671
  try {
49462
51672
  raw = JSON.parse(readText(packPath));
@@ -49480,7 +51690,7 @@ function runPackCheck(packPath, guidancePath) {
49480
51690
  }
49481
51691
  }
49482
51692
  if (v.ok && v.pack && guidancePath) {
49483
- if (!existsSync16(guidancePath)) {
51693
+ if (!existsSync17(guidancePath)) {
49484
51694
  errors.push(`guidance file not found: ${guidancePath}`);
49485
51695
  } else {
49486
51696
  let gRaw;
@@ -49534,8 +51744,8 @@ function packScaffold() {
49534
51744
  // src/config.ts
49535
51745
  var CONFIG_FILE = ".ultra11yrc.json";
49536
51746
  function loadConfig(cwd) {
49537
- const p = join31(cwd, CONFIG_FILE);
49538
- if (!existsSync17(p)) return null;
51747
+ const p = join33(cwd, CONFIG_FILE);
51748
+ if (!existsSync18(p)) return null;
49539
51749
  let parsed;
49540
51750
  try {
49541
51751
  parsed = JSON.parse(readText(p));
@@ -49548,16 +51758,16 @@ function loadConfig(cwd) {
49548
51758
  return parsed;
49549
51759
  }
49550
51760
  function packPaths(path) {
49551
- if (!existsSync17(path)) return null;
51761
+ if (!existsSync18(path)) return null;
49552
51762
  if (statSync9(path).isDirectory()) {
49553
- const pack = join31(path, "pack.json");
49554
- if (!existsSync17(pack)) return null;
49555
- const glossary = join31(path, "glossary.json");
49556
- const guidance = join31(path, "guidance.json");
51763
+ const pack = join33(path, "pack.json");
51764
+ if (!existsSync18(pack)) return null;
51765
+ const glossary = join33(path, "glossary.json");
51766
+ const guidance = join33(path, "guidance.json");
49557
51767
  return {
49558
51768
  pack,
49559
- glossary: existsSync17(glossary) ? glossary : void 0,
49560
- guidance: existsSync17(guidance) ? guidance : void 0
51769
+ glossary: existsSync18(glossary) ? glossary : void 0,
51770
+ guidance: existsSync18(guidance) ? guidance : void 0
49561
51771
  };
49562
51772
  }
49563
51773
  return { pack: path };
@@ -49573,7 +51783,7 @@ function loadRuntimeStandards(cwd, packFlags, onWarn, override = false) {
49573
51783
  }
49574
51784
  result.defaultStandard = config?.standard;
49575
51785
  for (const raw of [...config?.packs ?? [], ...packFlags]) {
49576
- const paths = packPaths(isAbsolute2(raw) ? raw : join31(cwd, raw));
51786
+ const paths = packPaths(isAbsolute2(raw) ? raw : join33(cwd, raw));
49577
51787
  if (!paths) {
49578
51788
  result.errors.push(`--pack ${raw}: not found (expected a pack JSON file or a directory with pack.json)`);
49579
51789
  continue;
@@ -49603,7 +51813,7 @@ ${formatIssues(v.issues).join("\n")}`);
49603
51813
  result.loadedPacks.push(v.pack.key);
49604
51814
  if (paths.guidance) loadGuidanceFile(paths.guidance, result, onWarn);
49605
51815
  }
49606
- for (const g of config?.guidance ?? []) loadGuidanceFile(isAbsolute2(g) ? g : join31(cwd, g), result, onWarn);
51816
+ for (const g of config?.guidance ?? []) loadGuidanceFile(isAbsolute2(g) ? g : join33(cwd, g), result, onWarn);
49607
51817
  for (const sm of config?.secondaryMappings ?? []) {
49608
51818
  if (!sm || typeof sm.standard !== "string" || typeof sm.ruleId !== "string" || typeof sm.criterion !== "string") {
49609
51819
  result.errors.push("secondaryMappings: each entry must be { standard, ruleId, criterion, note? }");
@@ -49618,7 +51828,7 @@ ${formatIssues(v.issues).join("\n")}`);
49618
51828
  return result;
49619
51829
  }
49620
51830
  function loadGuidanceFile(path, result, onWarn) {
49621
- if (!existsSync17(path)) {
51831
+ if (!existsSync18(path)) {
49622
51832
  onWarn(`ultra11y: guidance ${path} not found \u2014 skipping.`);
49623
51833
  return;
49624
51834
  }
@@ -49656,11 +51866,11 @@ ${g.errors.map((e) => ` \u2717 ${e}`).join("\n")}`);
49656
51866
  }
49657
51867
 
49658
51868
  // src/orchestrate.ts
49659
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "fs";
49660
- import { join as join33, resolve as resolve7 } from "path";
51869
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync16, rmSync as rmSync4, writeFileSync as writeFileSync13 } from "fs";
51870
+ import { join as join35, resolve as resolve7 } from "path";
49661
51871
 
49662
51872
  // src/orchestrate-templates.ts
49663
- import { join as join32 } from "path";
51873
+ import { join as join34 } from "path";
49664
51874
  var ONE_WRITER_FOOTER = `
49665
51875
  ## Return, don't write
49666
51876
 
@@ -49685,14 +51895,21 @@ var ADJUDICATE_SCHEMA = {
49685
51895
  description: "REQUIRED (>=1, groundable) for NC",
49686
51896
  items: {
49687
51897
  type: "object",
49688
- required: ["file", "line", "message"],
51898
+ // `normativeRef` is REQUIRED by the fold (src/adjudicate.ts): an NC with none is
51899
+ // rejected. Omitting it here made every fan-out adjudication produce NC verdicts
51900
+ // that `verify --apply` then refused.
51901
+ required: ["file", "line", "message", "normativeRef"],
49689
51902
  properties: {
49690
51903
  file: { type: "string" },
49691
51904
  line: { type: "integer" },
49692
51905
  selector: { type: "string" },
49693
51906
  message: { type: "string" },
49694
51907
  snippet: { type: "string" },
49695
- severity: { enum: ["bloquant", "majeur", "mineur"] }
51908
+ severity: { enum: ["bloquant", "majeur", "mineur"] },
51909
+ normativeRef: {
51910
+ type: "string",
51911
+ description: "The precise failed test of the ACTIVE standard. WCAG core: a success-criterion id (e.g. '1.1.1'). Country standard: a test OF THIS ITEM'S OWN CRITERION (e.g. '11.2.1' on item 11.2) \u2014 a WCAG id looks alike but denotes an unrelated test and is rejected."
51912
+ }
49696
51913
  }
49697
51914
  }
49698
51915
  }
@@ -49725,7 +51942,7 @@ var PHASE_SPECS = {
49725
51942
  title: "Adjudicate",
49726
51943
  schema: ADJUDICATE_SCHEMA,
49727
51944
  description: (n) => `Adjudicate the ${n} residual judgment criterion(ia) of an ultra11y audit (fan-out, fail-closed fold)`,
49728
- applyHint: (engine, worklist, run2) => `node ${engine} verify --apply ${worklist} --in ${join32(run2, "audit-latest.json")} --out ${run2}`
51945
+ applyHint: (engine, worklist, run2) => `node ${engine} verify --apply ${worklist} --in ${join34(run2, "audit-latest.json")} --out ${run2}`
49729
51946
  },
49730
51947
  "verify-report": {
49731
51948
  role: "refuter",
@@ -49747,7 +51964,7 @@ function toBatches(ids, batchSize) {
49747
51964
  }
49748
51965
  function phaseWorkflowScript(ph, runAbs, engineAbs, batchSize) {
49749
51966
  const spec = phaseSpec(ph.name);
49750
- const scriptPath = join32(runAbs, "orchestration", `${ph.name}.workflow.mjs`);
51967
+ const scriptPath = join34(runAbs, "orchestration", `${ph.name}.workflow.mjs`);
49751
51968
  const meta2 = { name: `ultra11y-${ph.name}`, description: spec.description(ph.items), phases: [{ title: spec.title }] };
49752
51969
  return [
49753
51970
  `export const meta = ${JSON.stringify(meta2)}`,
@@ -49789,27 +52006,27 @@ function agentContracts(runAbs, engineAbs) {
49789
52006
  return {
49790
52007
  adjudicator: `# Contract: adjudicator
49791
52008
 
49792
- You adjudicate residual judgment criteria of an ultra11y WCAG audit \u2014 the success criteria the deterministic engine could not decide (alt-text relevance, link purpose in context, reading order\u2026).
52009
+ You adjudicate the residual judgment criteria of an ultra11y audit \u2014 the ones the deterministic engine could not decide (alt-text relevance, link purpose in context, reading order\u2026). The ACTIVE STANDARD is recorded in the worklist's \`standard\` field: under a country standard (e.g. \`rgaa\`) the items are that standard's OWN criteria, each carrying its numbered tests \u2014 not WCAG success criteria.
49793
52010
 
49794
- Worklist: \`${join32(runAbs, "ADJUDICATE.todo.json")}\` (an object with \`kind: "adjudication"\` and \`items[]\`). Handle ONLY the criteria whose \`criteriaId\` is named in your prompt (\`ITEMS=<id,\u2026>\`).
52011
+ Worklist: \`${join34(runAbs, "ADJUDICATE.todo.json")}\` (an object with \`kind: "adjudication"\` and \`items[]\`). Handle ONLY the criteria whose \`criteriaId\` is named in your prompt (\`ITEMS=<id,\u2026>\`).
49795
52012
 
49796
52013
  For EACH of your criteria:
49797
52014
 
49798
52015
  1. Read its worklist entry. \`evidence[]\` holds source-anchored excerpts (\`file\`, \`line\`, \`selector\`, \`snippet\`) harvested from the audited code \u2014 open the cited files at the cited lines whenever the snippet alone cannot decide.
49799
52016
  2. Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict missing its required field does not fold):
49800
52017
  - \`C\` (conforming) \u2014 REQUIRES \`justification\` explaining why the evidence satisfies the criterion.
49801
- - \`NC\` (non-conforming) \u2014 REQUIRES \`findings\`: at least one groundable \`{ file, line, selector?, message, snippet?, severity? }\` pointing at REAL source. The fold re-grounds every finding; an invented file:line is rejected.
52018
+ - \`NC\` (non-conforming) \u2014 REQUIRES \`findings\`: at least one groundable \`{ file, line, selector?, message, snippet?, severity?, normativeRef }\` pointing at REAL source. The fold re-grounds every finding; an invented file:line is rejected. \`normativeRef\` MUST cite the precise failed test \u2014 under a country standard, one of the item's OWN tests, which the worklist lists for you under \xAB tests to rule on \xBB.
49802
52019
  - \`NA\` (not applicable) \u2014 REQUIRES \`justification\`.
49803
52020
  - \`manual\` (still undecidable) \u2014 REQUIRES \`reason\`: \`needs-rendered-dom\` (only a rendered DOM can decide, e.g. computed contrast) or \`undecidable\` (the evidence cannot settle it either way).
49804
52021
  3. Never guess. A criterion you cannot decide from real evidence stays \`manual\` with a reason \u2014 that is a valid, honest verdict; the scan tier or a human picks it up.
49805
52022
 
49806
- Return (structured output): \`{ "verdicts": [{ "criteriaId", "verdict", "justification", "reason", "findings" }] }\` \u2014 your ITEMS only, every field grounded in what you actually read.
52023
+ Return (structured output): \`{ "verdicts": [{ "criteriaId", "verdict", "justification", "reason", "findings" }] }\` \u2014 your ITEMS only, every field grounded in what you actually read, every NC finding carrying its \`normativeRef\`.
49807
52024
  ${footer}`,
49808
52025
  refuter: `# Contract: refuter
49809
52026
 
49810
52027
  You are an adversarial skeptic verifying the non-conformities of an ultra11y report. Your job is to try to REFUTE each claim: assume it is wrong until the source proves it.
49811
52028
 
49812
- Worklist: \`${join32(runAbs, "VERIFY.todo.json")}\` (a JSON array; each entry has \`n\`, \`criteriaId\`, \`file\`, \`line\`, \`selector\`, \`claim\`). Handle ONLY the entries whose \`n\` is named in your prompt (\`ITEMS=<n,\u2026>\`).
52029
+ Worklist: \`${join34(runAbs, "VERIFY.todo.json")}\` (a JSON array; each entry has \`n\`, \`criteriaId\`, \`file\`, \`line\`, \`selector\`, \`claim\`). Handle ONLY the entries whose \`n\` is named in your prompt (\`ITEMS=<n,\u2026>\`).
49813
52030
 
49814
52031
  For EACH of your entries:
49815
52032
 
@@ -49845,14 +52062,14 @@ ${status}
49845
52062
 
49846
52063
  ## The loop (play every role yourself, one item at a time)
49847
52064
 
49848
- 1. **Audit** (if not done): \`${engine} audit "<globs>" --graph --out ${runAbs}\` \u2192 \`${join32(runAbs, "audit-latest.json")}\`.
49849
- 2. **Adjudicate the residual criteria** \u2014 \`${engine} verify --manual --in ${join32(runAbs, "audit-latest.json")} --out ${runAbs}\` writes \`${join32(runAbs, "ADJUDICATE.todo.json")}\`. For EVERY item, apply \`${join32(runAbs, "orchestration", "agents", "adjudicator.md")}\` yourself (read the evidence, rule C/NC/NA/manual, fill the required justification/findings/reason IN the todo file). Then fold, fail-closed: \`${engine} verify --apply ${join32(runAbs, "ADJUDICATE.todo.json")} --in ${join32(runAbs, "audit-latest.json")} --out ${runAbs}\`.
49850
- 3. **Report**: \`${engine} report --in ${join32(runAbs, "audit-latest.json")} --out ${runAbs}\`.
49851
- 4. **Verify the report's claims** \u2014 \`${engine} verify --report <the report .md> --out ${runAbs}\` writes \`${join32(runAbs, "VERIFY.todo.json")}\`. For EVERY entry, apply \`${join32(runAbs, "orchestration", "agents", "refuter.md")}\` yourself (open file:line, verdict supported/partial/refuted/unsupported + note IN the todo file). Then: \`${engine} verify --apply ${join32(runAbs, "VERIFY.todo.json")} --report <the report .md>\`.
52065
+ 1. **Audit** (if not done): \`${engine} audit "<globs>" --graph --out ${runAbs}\` \u2192 \`${join34(runAbs, "audit-latest.json")}\`.
52066
+ 2. **Adjudicate the residual criteria** \u2014 \`${engine} verify --manual --in ${join34(runAbs, "audit-latest.json")} --out ${runAbs}\` writes \`${join34(runAbs, "ADJUDICATE.todo.json")}\`. For EVERY item, apply \`${join34(runAbs, "orchestration", "agents", "adjudicator.md")}\` yourself (read the evidence, rule C/NC/NA/manual, fill the required justification/findings/reason IN the todo file). Then fold, fail-closed: \`${engine} verify --apply ${join34(runAbs, "ADJUDICATE.todo.json")} --in ${join34(runAbs, "audit-latest.json")} --out ${runAbs}\`.
52067
+ 3. **Report**: \`${engine} report --in ${join34(runAbs, "audit-latest.json")} --out ${runAbs}\`.
52068
+ 4. **Verify the report's claims** \u2014 \`${engine} verify --report <the report .md> --out ${runAbs}\` writes \`${join34(runAbs, "VERIFY.todo.json")}\`. For EVERY entry, apply \`${join34(runAbs, "orchestration", "agents", "refuter.md")}\` yourself (open file:line, verdict supported/partial/refuted/unsupported + note IN the todo file). Then: \`${engine} verify --apply ${join34(runAbs, "VERIFY.todo.json")} --report <the report .md>\`.
49852
52069
  5. **Gate**: \`${engine} check --report <the report .md> --semantic\` must exit 0 before presenting anything.
49853
52070
  6. **Fix & re-audit**: \`${engine} fix <globs> --write --iterate\`, hand-apply the judgment fixes, then loop from step 1 until the gate stays green.
49854
52071
 
49855
- With subagents available, prefer the emitted workflows instead: \`orchestrate --run ${runAbs} --phase <p>\` then \`Workflow({ scriptPath: "${join32(runAbs, "orchestration", "<p>.workflow.mjs")}" })\` \u2014 you stay the sole writer either way.
52072
+ With subagents available, prefer the emitted workflows instead: \`orchestrate --run ${runAbs} --phase <p>\` then \`Workflow({ scriptPath: "${join34(runAbs, "orchestration", "<p>.workflow.mjs")}" })\` \u2014 you stay the sole writer either way.
49856
52073
  `;
49857
52074
  }
49858
52075
 
@@ -49862,12 +52079,12 @@ var SMALL_WORKLIST = 3;
49862
52079
  var BATCH_SIZE = 8;
49863
52080
  function listPhases(runDir, engineAbs) {
49864
52081
  const run2 = resolve7(runDir);
49865
- const adjPath = join33(run2, "ADJUDICATE.todo.json");
52082
+ const adjPath = join35(run2, "ADJUDICATE.todo.json");
49866
52083
  let adjIds = [];
49867
52084
  let adjReady = false;
49868
- if (existsSync18(adjPath)) {
52085
+ if (existsSync19(adjPath)) {
49869
52086
  try {
49870
- const f = JSON.parse(readFileSync14(adjPath, "utf8"));
52087
+ const f = JSON.parse(readFileSync16(adjPath, "utf8"));
49871
52088
  if (f && f.kind === "adjudication" && Array.isArray(f.items)) {
49872
52089
  adjReady = true;
49873
52090
  adjIds = f.items.map((i2) => i2.criteriaId);
@@ -49875,12 +52092,12 @@ function listPhases(runDir, engineAbs) {
49875
52092
  } catch {
49876
52093
  }
49877
52094
  }
49878
- const verPath = join33(run2, "VERIFY.todo.json");
52095
+ const verPath = join35(run2, "VERIFY.todo.json");
49879
52096
  let verIds = [];
49880
52097
  let verReady = false;
49881
- if (existsSync18(verPath)) {
52098
+ if (existsSync19(verPath)) {
49882
52099
  try {
49883
- const items = JSON.parse(readFileSync14(verPath, "utf8"));
52100
+ const items = JSON.parse(readFileSync16(verPath, "utf8"));
49884
52101
  if (Array.isArray(items)) {
49885
52102
  verReady = true;
49886
52103
  verIds = items.map((i2) => String(i2.n));
@@ -49895,7 +52112,7 @@ function listPhases(runDir, engineAbs) {
49895
52112
  worklist: adjPath,
49896
52113
  items: adjIds.length,
49897
52114
  ids: adjIds,
49898
- prerequisite: `node ${engineAbs} verify --manual --in ${join33(run2, "audit-latest.json")} --out ${run2}`
52115
+ prerequisite: `node ${engineAbs} verify --manual --in ${join35(run2, "audit-latest.json")} --out ${run2}`
49899
52116
  },
49900
52117
  {
49901
52118
  name: "verify-report",
@@ -49909,7 +52126,7 @@ function listPhases(runDir, engineAbs) {
49909
52126
  }
49910
52127
  function orchestrateRun(runDir, engineAbs, opts = {}) {
49911
52128
  const run2 = resolve7(runDir);
49912
- if (!existsSync18(run2)) {
52129
+ if (!existsSync19(run2)) {
49913
52130
  return { exitCode: 2, written: [], notices: [], errors: [`run dir not found: ${run2}`], phases: [] };
49914
52131
  }
49915
52132
  const phases = listPhases(run2, engineAbs);
@@ -49936,22 +52153,22 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
49936
52153
  }
49937
52154
  selected = [ph];
49938
52155
  }
49939
- const orchDir = join33(run2, "orchestration");
49940
- const agentsDir = join33(orchDir, "agents");
49941
- mkdirSync9(join33(orchDir, "out"), { recursive: true });
49942
- mkdirSync9(agentsDir, { recursive: true });
52156
+ const orchDir = join35(run2, "orchestration");
52157
+ const agentsDir = join35(orchDir, "agents");
52158
+ mkdirSync10(join35(orchDir, "out"), { recursive: true });
52159
+ mkdirSync10(agentsDir, { recursive: true });
49943
52160
  const written = [];
49944
52161
  const notices = [];
49945
52162
  for (const [name2, content] of Object.entries(agentContracts(run2, engineAbs))) {
49946
- const p = join33(agentsDir, `${name2}.md`);
49947
- writeFileSync12(p, content);
52163
+ const p = join35(agentsDir, `${name2}.md`);
52164
+ writeFileSync13(p, content);
49948
52165
  written.push(p);
49949
52166
  }
49950
52167
  const emitted = new Set(opts.eco ? [] : selected.filter((p) => p.items > 0).map((p) => p.name));
49951
52168
  for (const ph of phases) {
49952
52169
  if (emitted.has(ph.name)) continue;
49953
- const stale = join33(orchDir, `${ph.name}.workflow.mjs`);
49954
- if (existsSync18(stale)) {
52170
+ const stale = join35(orchDir, `${ph.name}.workflow.mjs`);
52171
+ if (existsSync19(stale)) {
49955
52172
  rmSync4(stale, { force: true });
49956
52173
  notices.push(`phase "${ph.name}": stale workflow removed \u2014 its worklist ${ph.ready ? "is now empty" : "no longer exists"}.`);
49957
52174
  }
@@ -49965,13 +52182,13 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
49965
52182
  if (ph.items <= SMALL_WORKLIST) {
49966
52183
  notices.push(`phase "${ph.name}": only ${ph.items} item(s) \u2014 the sequential --eco path is equivalent and cheaper.`);
49967
52184
  }
49968
- const p = join33(orchDir, `${ph.name}.workflow.mjs`);
49969
- writeFileSync12(p, phaseWorkflowScript(ph, run2, engineAbs, BATCH_SIZE));
52185
+ const p = join35(orchDir, `${ph.name}.workflow.mjs`);
52186
+ writeFileSync13(p, phaseWorkflowScript(ph, run2, engineAbs, BATCH_SIZE));
49970
52187
  written.push(p);
49971
52188
  }
49972
52189
  }
49973
- const rb = join33(orchDir, "RUNBOOK.md");
49974
- writeFileSync12(rb, runbookMd(phases, run2, engineAbs));
52190
+ const rb = join35(orchDir, "RUNBOOK.md");
52191
+ writeFileSync13(rb, runbookMd(phases, run2, engineAbs));
49975
52192
  written.push(rb);
49976
52193
  return { exitCode: 0, written, notices, errors: [], phases };
49977
52194
  }
@@ -49980,8 +52197,8 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
49980
52197
  import { createInterface as createInterface2 } from "readline";
49981
52198
 
49982
52199
  // src/mcp/handlers.ts
49983
- import { existsSync as existsSync19, readFileSync as readFileSync15, realpathSync as realpathSync2, statSync as statSync10 } from "fs";
49984
- import { isAbsolute as isAbsolute3, join as join34, resolve as resolve8, sep as sep4 } from "path";
52200
+ import { existsSync as existsSync20, readFileSync as readFileSync17, realpathSync as realpathSync2, statSync as statSync10 } from "fs";
52201
+ import { isAbsolute as isAbsolute3, join as join36, resolve as resolve8, sep as sep4 } from "path";
49985
52202
 
49986
52203
  // src/project-lock.ts
49987
52204
  var chains = /* @__PURE__ */ new Map();
@@ -50029,7 +52246,7 @@ function requiredCwd(args2, defaults) {
50029
52246
  const cwd = str2(args2.cwd) ?? defaults.defaultCwd;
50030
52247
  if (!cwd) throw new ToolError("`cwd` is required: an absolute path to the project root.");
50031
52248
  const abs = resolve8(cwd);
50032
- if (!existsSync19(abs)) throw new ToolError(`project root not found: ${abs}`);
52249
+ if (!existsSync20(abs)) throw new ToolError(`project root not found: ${abs}`);
50033
52250
  if (!statSync10(abs).isDirectory()) throw new ToolError(`\`cwd\` is not a directory: ${abs}`);
50034
52251
  return abs;
50035
52252
  }
@@ -50174,13 +52391,13 @@ function handlePackCheck(args2, cwd) {
50174
52391
  return { cwd, ...res };
50175
52392
  }
50176
52393
  function handleSampleCheck(cwd) {
50177
- const file = join34(cwd, ".ultra11yrc.json");
50178
- if (!existsSync19(file)) {
52394
+ const file = join36(cwd, ".ultra11yrc.json");
52395
+ if (!existsSync20(file)) {
50179
52396
  throw new ToolError(`no .ultra11yrc.json at ${cwd} \u2014 a page sample must be declared before it can be linted.`);
50180
52397
  }
50181
52398
  let raw;
50182
52399
  try {
50183
- raw = JSON.parse(readFileSync15(file, "utf8"));
52400
+ raw = JSON.parse(readFileSync17(file, "utf8"));
50184
52401
  } catch (e) {
50185
52402
  throw new ToolError(`.ultra11yrc.json is not valid JSON: ${e.message}`);
50186
52403
  }
@@ -50218,7 +52435,7 @@ function handleInit(args2, cwd) {
50218
52435
  const enginePath = resolve8("scripts/ultra11y.mjs");
50219
52436
  const written = [];
50220
52437
  if (bool(args2.hook)) written.push(writeHook(cwd, enginePath, failOn));
50221
- if (bool(args2.ci)) written.push(join34(cwd, ".github/workflows/a11y.yml"));
52438
+ if (bool(args2.ci)) written.push(join36(cwd, ".github/workflows/a11y.yml"));
50222
52439
  if (!written.length) {
50223
52440
  throw new ToolError("nothing to install \u2014 pass hook:true and/or ci:true.");
50224
52441
  }
@@ -50238,8 +52455,8 @@ function reportText(args2, tool) {
50238
52455
  if (inline) return inline;
50239
52456
  if (!file) throw new ToolError(`\`report_text\` is required \u2014 the report markdown for ultra11y_${tool} to work on.`);
50240
52457
  if (!isAbsolute3(file)) throw new ToolError("`report_file` must be an absolute path.");
50241
- if (!existsSync19(file)) throw new ToolError(`report file not found: ${file}`);
50242
- return readFileSync15(file, "utf8");
52458
+ if (!existsSync20(file)) throw new ToolError(`report file not found: ${file}`);
52459
+ return readFileSync17(file, "utf8");
50243
52460
  }
50244
52461
  function handleCriteria(args2) {
50245
52462
  const lang = langOf(args2);
@@ -50254,7 +52471,7 @@ function handleCriteria(args2) {
50254
52471
  function handleRead(args2, cwd) {
50255
52472
  const raw = str2(args2.path);
50256
52473
  if (!raw) throw new ToolError("`path` is required \u2014 relative to the project root, or an absolute path inside it.");
50257
- const target = isAbsolute3(raw) ? raw : join34(cwd, raw);
52474
+ const target = isAbsolute3(raw) ? raw : join36(cwd, raw);
50258
52475
  let real;
50259
52476
  try {
50260
52477
  real = realpathSync2(target);
@@ -50268,7 +52485,7 @@ function handleRead(args2, cwd) {
50268
52485
  const st = statSync10(real);
50269
52486
  if (!st.isFile()) throw new ToolError(`not a file: ${raw}`);
50270
52487
  if (st.size > MAX_READ_BYTES) throw new ToolError(`file is too large to read (${st.size} bytes): ${raw}`);
50271
- const lines = readFileSync15(real, "utf8").split("\n");
52488
+ const lines = readFileSync17(real, "utf8").split("\n");
50272
52489
  const total = lines.length;
50273
52490
  const start2 = Math.max(1, Math.floor(num2(args2.start_line) ?? 1));
50274
52491
  if (start2 > total) throw new ToolError(`start_line ${start2} is past the end of the file (${total} lines).`);
@@ -50763,25 +52980,25 @@ function str3(v) {
50763
52980
  var DECLARED = new Set([...TOOLS2, ...WRITE_TOOLS].map((t2) => t2.name));
50764
52981
 
50765
52982
  // src/mcp/resources.ts
50766
- import { existsSync as existsSync20, readdirSync as readdirSync5, readFileSync as readFileSync16, realpathSync as realpathSync3, statSync as statSync11 } from "fs";
50767
- import { basename as basename3, dirname as dirname9, join as join35, resolve as resolve9, sep as sep5 } from "path";
52983
+ import { existsSync as existsSync21, readdirSync as readdirSync6, readFileSync as readFileSync18, realpathSync as realpathSync3, statSync as statSync11 } from "fs";
52984
+ import { basename as basename3, dirname as dirname10, join as join37, resolve as resolve9, sep as sep5 } from "path";
50768
52985
  import { fileURLToPath as fileURLToPath4 } from "url";
50769
52986
  var SKILL_NAME = "ultra11y";
50770
52987
  var URI_SCHEME = "skill://";
50771
52988
  function resolveSkillRoot(moduleDir) {
50772
- const here = moduleDir ?? dirname9(fileURLToPath4(import.meta.url));
52989
+ const here = moduleDir ?? dirname10(fileURLToPath4(import.meta.url));
50773
52990
  const candidates2 = [resolve9(here, ".."), resolve9(here, "..", "skills", SKILL_NAME), resolve9(here, "..", "..", "skills", SKILL_NAME)];
50774
- return candidates2.find((dir) => existsSync20(join35(dir, "SKILL.md")));
52991
+ return candidates2.find((dir) => existsSync21(join37(dir, "SKILL.md")));
50775
52992
  }
50776
52993
  function listResources(moduleDir) {
50777
52994
  const root = resolveSkillRoot(moduleDir);
50778
52995
  if (!root) return [];
50779
52996
  const out2 = [describe(root, "SKILL.md", `${SKILL_NAME}: the skill`)];
50780
- const refDir = join35(root, "references");
50781
- if (!existsSync20(refDir)) return out2;
50782
- for (const file of readdirSync5(refDir).sort()) {
52997
+ const refDir = join37(root, "references");
52998
+ if (!existsSync21(refDir)) return out2;
52999
+ for (const file of readdirSync6(refDir).sort()) {
50783
53000
  if (!file.endsWith(".md")) continue;
50784
- out2.push(describe(root, join35("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
53001
+ out2.push(describe(root, join37("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
50785
53002
  }
50786
53003
  return out2;
50787
53004
  }
@@ -50805,7 +53022,7 @@ function readResource(uri, moduleDir) {
50805
53022
  throw new ResourceError(`resource path escapes the skill root: ${uri}`);
50806
53023
  }
50807
53024
  if (!statSync11(targetReal).isFile()) throw new ResourceError(`not a file: ${uri}`);
50808
- return { uri, mimeType: "text/markdown", text: readFileSync16(targetReal, "utf8") };
53025
+ return { uri, mimeType: "text/markdown", text: readFileSync18(targetReal, "utf8") };
50809
53026
  }
50810
53027
  var ResourceError = class extends Error {
50811
53028
  };
@@ -50816,14 +53033,14 @@ function describe(root, rel, fallbackTitle) {
50816
53033
  title: fallbackTitle,
50817
53034
  mimeType: "text/markdown"
50818
53035
  };
50819
- const summary = firstProse(join35(root, rel));
53036
+ const summary = firstProse(join37(root, rel));
50820
53037
  if (summary) decl.description = summary;
50821
53038
  return decl;
50822
53039
  }
50823
53040
  function firstProse(file) {
50824
53041
  let text;
50825
53042
  try {
50826
- text = readFileSync16(file, "utf8");
53043
+ text = readFileSync18(file, "utf8");
50827
53044
  } catch {
50828
53045
  return void 0;
50829
53046
  }
@@ -50842,7 +53059,7 @@ var ERR_INVALID_REQUEST = -32600;
50842
53059
  var ERR_METHOD_NOT_FOUND = -32601;
50843
53060
  var ERR_INVALID_PARAMS = -32602;
50844
53061
  var ERR_INTERNAL = -32603;
50845
- function createServer(opts = {}) {
53062
+ function createServer2(opts = {}) {
50846
53063
  const serverInfo = { name: opts.serverName ?? "ultra11y", version: VERSION };
50847
53064
  const maxBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES2;
50848
53065
  let protocol = LATEST_PROTOCOL2;
@@ -50993,7 +53210,7 @@ async function runStdioServer(opts = {}) {
50993
53210
  process.stdout.write = original;
50994
53211
  };
50995
53212
  }
50996
- const server = createServer(opts);
53213
+ const server = createServer2(opts);
50997
53214
  const send = (msg) => {
50998
53215
  emit2(JSON.stringify(msg) + "\n");
50999
53216
  };
@@ -51142,7 +53359,7 @@ async function route(req, res, opts) {
51142
53359
  const protocol = declared ?? ASSUMED_HTTP_PROTOCOL;
51143
53360
  let raw;
51144
53361
  try {
51145
- raw = await readBody(req);
53362
+ raw = await readBody2(req);
51146
53363
  } catch (e) {
51147
53364
  if (e.message === "too large") {
51148
53365
  sendJson(res, 413, { error: `request body exceeds ${MAX_BODY_BYTES} bytes` }, origin);
@@ -51158,7 +53375,7 @@ async function route(req, res, opts) {
51158
53375
  sendJson(res, 200, { jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } }, origin);
51159
53376
  return;
51160
53377
  }
51161
- const mcp = createServer(opts);
53378
+ const mcp = createServer2(opts);
51162
53379
  mcp.setProtocolVersion(protocol);
51163
53380
  const out2 = [];
51164
53381
  const collect2 = (m) => void out2.push(m);
@@ -51189,7 +53406,7 @@ function sendJson(res, status, body2, origin, extra = {}) {
51189
53406
  res.end(text);
51190
53407
  }
51191
53408
  var DRAIN_LIMIT = MAX_BODY_BYTES * 8;
51192
- function readBody(req) {
53409
+ function readBody2(req) {
51193
53410
  return new Promise((resolve10, reject) => {
51194
53411
  const chunks = [];
51195
53412
  let size = 0;
@@ -51232,11 +53449,13 @@ non-conformities. RGAA (France) and other country standards are pluggable packs
51232
53449
  Usage:
51233
53450
  ultra11y audit <globs\u2026 | -> [--out <dir>] [--include <glob>] [--exclude <glob>] [--ext <list>] [--jsx] [--graph] [--json] [--lang auto|en|fr] [--no-default-excludes]
51234
53451
  ultra11y audit [--changed | --since <ref> | --staged] [--max-files <n>] [--dedup exact|normalized|off] [--baseline <file>] [--fail-on blocking|major|minor]
51235
- ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures: audit real HTML, gate blind-spot components)
51236
- ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--lang auto|en|fr]
53452
+ ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures + .ultra11y/pages snapshots: audit real HTML)
53453
+ ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
53454
+ ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
51237
53455
  ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--gh-issues | --gh-single] [--lang auto|en|fr]
51238
- ultra11y render [<dir>] [--scaffold | --setup | --coverage | --storybook] [--captures <dir>] [--out <file>] [--json] [--lang auto|en|fr]
53456
+ ultra11y render [<dir>] [--scaffold | --setup | --e2e | --coverage | --storybook] [--runner playwright|cypress|auto] [--captures <dir>] [--out <file>] [--json] [--lang auto|en|fr]
51239
53457
  ultra11y criteria [<sc>] [--list] [--standard <pack> [--theme <N>]] [--generate] [--json] [--lang auto|en|fr]
53458
+ ultra11y criteria --standard <pack> --glossary [<term>] (the terms the standard DEFINES \u2014 its tests depend on them)
51240
53459
  ultra11y check --report <md> [--standard <pack>] [--in <audit.json>] [--semantic [--verdicts <file>]] [--quiet] [--json]
51241
53460
  ultra11y verify --report <md> [--standard <pack>] [--semantic] [--apply <verdicts.json>] [--max-verify <n>] [--out <dir>] [--json]
51242
53461
  ultra11y verify --report <md> --in <audit.json> --manual [--out <dir>] [--json] (adjudicate the manual criteria)
@@ -51250,6 +53469,12 @@ Usage:
51250
53469
  ultra11y scan --sitemap <url> | --crawl <url> [--depth <n>] [--max <n>] [--runtime \u2026] [--cwd <dir>] [--merge <audit.json>] [--json]
51251
53470
  ultra11y scan --clean (remove the dynamic-tier Docker image + temp contexts)
51252
53471
  ultra11y sample check [--standard <pack>] [--json] (lint the .ultra11yrc.json page sample vs the standard's required page kinds)
53472
+ ultra11y snapshot write [--root <dir>] [--fail-on blocking|major|minor] [--json] (payload on stdin \u2192 .ultra11y/pages/<id>/ + audit it)
53473
+ ultra11y snapshot list [--root <dir>] [--json]
53474
+ ultra11y pages --in <audit.json> [--standard <pack>] [--json] [--lang auto|en|fr] (the per-page criterion grid)
53475
+ ultra11y mcp [--transport stdio|http] [--cwd <dir>] [--allow-write] [--port <n>] [--bind <addr>] [--allow-remote] [--allow-origin <o>] [--max-response-bytes <n>]
53476
+ ultra11y dev [--port <n>] [--root <dir>] [--standard <pack>] [--lang auto|en|fr] (dev side-car: live overlay + per-page dashboard)
53477
+ ultra11y dev --next [--port <n>] (write the Next overlay component, then wire one line into your layout)
51253
53478
 
51254
53479
  Commands:
51255
53480
  audit Run the static engine over the inputs (files/globs, or '-' for stdin)
@@ -51258,12 +53483,19 @@ Commands:
51258
53483
  (default auto: repo <html lang> \u2192 the active standard's default locale
51259
53484
  \u2192 English). The engine decides the machine-detectable criteria; the AI
51260
53485
  agent adjudicates the judgment ones (verify --manual, gated) and the
51261
- scan tier decides the needs-rendering ones.
53486
+ scan tier decides the needs-rendering ones. --format sarif|github emits
53487
+ the CI rendering instead of the summary (in --baseline gate mode it
53488
+ covers exactly the NEW findings, i.e. what the PR introduced).
51262
53489
  report Render an AuditResult into a dated WCAG 2.2 AA compliance report
51263
53490
  (audits/wcag-YYYY-MM-DD.md): metadata, per-guideline synthesis table,
51264
53491
  non-conformities by priority, conforming + not-applicable lists.
51265
53492
  --standard <pack> writes a derived report for a country standard
51266
53493
  (e.g. --standard rgaa \u2192 audits/rgaa-YYYY-MM-DD.md).
53494
+ --format renders the same result for CI instead of Markdown: 'sarif'
53495
+ (SARIF 2.1.0 for GitHub code scanning, so findings land as inline PR
53496
+ annotations) or 'github' (::error:: workflow commands on stdout plus a
53497
+ job summary appended to $GITHUB_STEP_SUMMARY). Both honour --standard,
53498
+ so this is how you get an RGAA-keyed SARIF \u2014 audit itself is WCAG-keyed.
51267
53499
  prd Turn an AuditResult into an AUDITOR conformance backlog
51268
53500
  (audits/prd-YYYY-MM-DD.md), one entry per criterion rendered with the
51269
53501
  active standard's vocabulary (RGAA "Th\xE9matique/Crit\xE8re/Test", WCAG core
@@ -51278,7 +53510,12 @@ Commands:
51278
53510
  react-dom/server SSR snapshot harness to fill in. --setup installs the
51279
53511
  zero-touch test-render capture harvester (one setupFiles line \u2192 every
51280
53512
  component your tests render is snapshotted to .ultra11y/captures and
51281
- audited). --coverage reports which components have a rendered capture vs
53513
+ audited). --setup captures COMPONENTS from unit tests; --e2e wires the
53514
+ audit into an existing Playwright/Cypress run instead, so a targeted PAGE
53515
+ is checked during the tests you already have \u2014 each checked page is
53516
+ persisted to .ultra11y/pages/<id>/ (DOM + computed styles + boxes) and can
53517
+ be re-audited later, offline, with no browser.
53518
+ --coverage reports which components have a rendered capture vs
51282
53519
  which are still opaque-source-only blind spots. --storybook attributes
51283
53520
  per-story HTML (via a storybook-static index) back to source components.
51284
53521
  Then audit the produced HTML, and use scan for the needs-rendering criteria.
@@ -51286,6 +53523,12 @@ Commands:
51286
53523
  (criteria 1.4.3) or the full list grouped by guideline (--list).
51287
53524
  --standard <pack>: a pack criterion, a pack theme (--theme N), or its
51288
53525
  theme list. Carries the WCAG\u2194pack cross-refs + automatability class.
53526
+ --glossary [<term>] looks up a term the standard DEFINES (RGAA ships
53527
+ 119). These definitions are normative \u2014 what "if necessary" or
53528
+ "relevant" mean in a test is decided there \u2014 and the adjudication
53529
+ worklist now inlines the ones each criterion's own tests cite. With no
53530
+ term, lists them all. Resolves by anchor or title, accent-insensitive;
53531
+ an unknown term errors with suggestions rather than a near-miss.
51289
53532
  check Integrity gate on a produced report: every cited criterion resolves,
51290
53533
  every NA is justified, sections + pass-rate maths are well-formed.
51291
53534
  --standard tells it which id grammar/registry to validate against.
@@ -51348,6 +53591,38 @@ Commands:
51348
53591
  page kinds (RGAA: accueil, contact, mentions l\xE9gales, d\xE9claration
51349
53592
  d'accessibilit\xE9, plan du site, aide, authentification, pages
51350
53593
  repr\xE9sentatives + \xE9l\xE9ments transverses) \u2014 advisory, never a gate.
53594
+ snapshot Persist a rendered PAGE (.ultra11y/pages/<id>/: documentElement DOM +
53595
+ computed-style digest + boxes + a11y tree) and audit it. 'snapshot write'
53596
+ reads the collected payload on stdin, so a browser-side producer (the
53597
+ render --e2e fixtures, the dev overlay) needs to know nothing about the
53598
+ on-disk format \u2014 one process per checked page. 'snapshot list' shows what
53599
+ has been captured. Because a snapshot is a FULL document (a component
53600
+ capture is a fragment), the page-scoped rules run on it: that is where
53601
+ html lang (RGAA 8.3) and page title (8.5/8.6) become decidable.
53602
+ dev Development side-car: see the defects on the page you are BUILDING,
53603
+ while you build it. It serves a per-page dashboard on
53604
+ http://127.0.0.1:4111 and an overlay the app loads; every page you
53605
+ visit is snapshotted, audited and listed in a floating panel, each
53606
+ finding linking to its file:line (Next's launch-editor endpoint).
53607
+ --next writes the overlay component to wire into your layout \u2014 it
53608
+ renders NOTHING outside development. LOOPBACK ONLY: the server
53609
+ writes files, so it never binds beyond 127.0.0.1.
53610
+ mcp Serve the engine over the Model Context Protocol, so an MCP client
53611
+ (Claude Code, an IDE) drives audit/report/criteria as tools instead of
53612
+ shelling out. --transport stdio (default) speaks over stdin/stdout;
53613
+ --transport http listens on 127.0.0.1 (--port, --bind), and stays
53614
+ loopback-only unless you pass --allow-remote. Read-only by default:
53615
+ --allow-write opts into the commands that touch files.
53616
+ pages The per-page criterion grid \u2014 RGAA is a per-page norm, the engine's
53617
+ verdict is scope-wide, and this bridges the two. One row per criterion
53618
+ (the pack's own under --standard), one column per page. Rebuilt from a
53619
+ committed audit.json alone: no snapshots on disk, no browser.
53620
+ Two honesty rules: a finding is attributed to a page only when something
53621
+ SAYS so (the snapshot it was raised on, the scanned URL, the sample page
53622
+ name, or the page's recorded source files) \u2014 anything else is reported as
53623
+ UNATTRIBUTED, never spread across pages; and "no finding here" means
53624
+ conforming only for a page whose real rendered DOM was audited. A page
53625
+ known only by source attribution keeps its undecided criteria "to assess".
51351
53626
 
51352
53627
  Options:
51353
53628
  --out <dir> output dir (report/prd/scan default: audits); for audit, persist
@@ -51390,7 +53665,19 @@ Options:
51390
53665
  --coverage render: report rendered-capture coverage (covered vs blind-spot components); with --json emits the coverage object
51391
53666
  --storybook render: attribute per-story HTML (via storybook-static index.json) into .ultra11y/captures (point the HTML dir with --captures)
51392
53667
  --captures <dir> audit/render: rendered-capture dir to ingest (default: .ultra11y/captures)
51393
- --no-captures audit: do NOT auto-detect/ingest the .ultra11y/captures dir
53668
+ --no-captures audit: do NOT auto-detect/ingest .ultra11y/captures nor .ultra11y/pages
53669
+ --e2e render: write the Playwright/Cypress fixtures into .ultra11y/e2e/
53670
+ --runner <name> render --e2e: force playwright|cypress instead of auto-detecting
53671
+ --root <dir> snapshot: project root holding .ultra11y/pages (default: .)
53672
+ --port <n> dev: port for the side-car (default: 4111); mcp: HTTP transport port
53673
+ --transport <k> mcp: stdio (default) or http
53674
+ --bind <addr> mcp --transport http: address to bind (default 127.0.0.1)
53675
+ --allow-remote mcp --transport http: accept non-loopback connections (off by default)
53676
+ --allow-origin <o> mcp --transport http: an Origin allowed to call the server
53677
+ --max-response-bytes <n> mcp: cap a tool response's size
53678
+ --allow-write mcp: allow the tools that write files (read-only otherwise)
53679
+ --glossary [<t>] criteria: look up a term the country standard defines (or list all)
53680
+ --next dev: write the Next.js overlay component instead of serving
51394
53681
  --require-captures audit: gate \u2014 fail if any opaque/control component lacks a rendered capture (implies --graph)
51395
53682
  --write fix: apply fixes to disk (default is a dry-run diff)
51396
53683
  --iterate fix: with --write, re-audit + re-apply mechanical fixes until stable (bounded)
@@ -51462,7 +53749,25 @@ Options:
51462
53749
  -v, --version print version
51463
53750
 
51464
53751
  Data: WCAG 2.2 \xA9 W3C (W3C Document License). RGAA 4.1.2 pack \xA9 DINUM, Licence Ouverte / Etalab 2.0 (see NOTICE).`;
51465
- var COMMANDS = ["audit", "report", "prd", "render", "criteria", "check", "verify", "scan", "sample", "fix", "init", "pack", "orchestrate", "mcp"];
53752
+ var COMMANDS = [
53753
+ "audit",
53754
+ "report",
53755
+ "prd",
53756
+ "render",
53757
+ "criteria",
53758
+ "check",
53759
+ "verify",
53760
+ "scan",
53761
+ "sample",
53762
+ "snapshot",
53763
+ "pages",
53764
+ "dev",
53765
+ "mcp",
53766
+ "fix",
53767
+ "init",
53768
+ "pack",
53769
+ "orchestrate"
53770
+ ];
51466
53771
  function isCommand(s) {
51467
53772
  return !!s && COMMANDS.includes(s);
51468
53773
  }
@@ -51500,10 +53805,14 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
51500
53805
  "captures",
51501
53806
  "run",
51502
53807
  "phase",
53808
+ "root",
53809
+ "runner",
53810
+ // Shared by `dev` (side-car port) and `mcp` (HTTP transport port) — one entry, both users.
53811
+ "port",
53812
+ "glossary",
51503
53813
  // `mcp` only. The flag sets are global, so these are accepted (and warned
51504
53814
  // about, never silently ignored) on every command — like --phase already is.
51505
53815
  "transport",
51506
- "port",
51507
53816
  "bind",
51508
53817
  "allow-origin",
51509
53818
  "max-response-bytes"
@@ -51526,6 +53835,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
51526
53835
  "scaffold",
51527
53836
  "storybook",
51528
53837
  "setup",
53838
+ "e2e",
53839
+ "next",
51529
53840
  "coverage",
51530
53841
  "write",
51531
53842
  "dry-run",
@@ -51632,6 +53943,36 @@ function peekMergeAudit(mergeIn) {
51632
53943
  return void 0;
51633
53944
  }
51634
53945
  }
53946
+ function parseCiFormat(v) {
53947
+ if (v === void 0) return void 0;
53948
+ if (v === "sarif") return "sarif";
53949
+ if (v === "github") return "github";
53950
+ return null;
53951
+ }
53952
+ function emitCiFormat(result, format, standard, lang, failOn) {
53953
+ if (format === "sarif") {
53954
+ console.log(JSON.stringify(toSarif(result, { standard, lang }), null, 2));
53955
+ return;
53956
+ }
53957
+ for (const line of annotations(result, { standard, lang, failOn })) console.log(line);
53958
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
53959
+ const md = stepSummary(result, { standard, lang });
53960
+ if (summaryPath) {
53961
+ try {
53962
+ appendFileSync(summaryPath, `${md}
53963
+ `);
53964
+ } catch {
53965
+ }
53966
+ } else {
53967
+ console.error(md);
53968
+ }
53969
+ if (process.env.ULTRA11Y_PR_COMMENT === "1") {
53970
+ const c2 = pushPrComment(md, standard);
53971
+ console.error(
53972
+ c2.ok ? lang === "fr" ? `ultra11y : commentaire de PR ${c2.action === "updated" ? "mis \xE0 jour" : c2.action === "created" ? "cr\xE9\xE9" : "ignor\xE9"}${c2.reason ? ` (${c2.reason})` : ""}.` : `ultra11y: PR comment ${c2.action}${c2.reason ? ` (${c2.reason})` : ""}.` : lang === "fr" ? `ultra11y : commentaire de PR impossible${c2.reason ? ` \u2014 ${c2.reason}` : ""}.` : `ultra11y: PR comment failed${c2.reason ? ` \u2014 ${c2.reason}` : ""}.`
53973
+ );
53974
+ }
53975
+ }
51635
53976
  async function cmdAudit(p) {
51636
53977
  const inputs = p.positionals.length ? p.positionals : ["."];
51637
53978
  if (inputs.length === 0) {
@@ -51646,13 +53987,19 @@ async function cmdAudit(p) {
51646
53987
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
51647
53988
  const capturesDir = capturesFlag ?? ".ultra11y/captures";
51648
53989
  const scopedToDiff = p.flags.changed === true || p.flags.staged === true || since !== void 0;
51649
- const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 || existsSync21(capturesDir));
53990
+ const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 || existsSync22(capturesDir));
51650
53991
  const useCaptures = capturesWanted && !scopedToDiff && !inputs.includes(capturesDir);
51651
- const auditInputs = useCaptures ? [...inputs, capturesDir] : inputs;
53992
+ const pagesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && existsSync22(PAGES_DIR);
53993
+ const usePages = pagesWanted && !scopedToDiff && !inputs.includes(PAGES_DIR);
53994
+ const auditInputs = [...inputs, ...useCaptures ? [capturesDir] : [], ...usePages ? [PAGES_DIR] : []];
51652
53995
  if (useCaptures)
51653
53996
  console.error(
51654
53997
  lang === "fr" ? `ultra11y audit : captures rendues ing\xE9r\xE9es depuis ${capturesDir}.` : `ultra11y audit: ingesting rendered captures from ${capturesDir}.`
51655
53998
  );
53999
+ if (usePages)
54000
+ console.error(
54001
+ lang === "fr" ? `ultra11y audit : instantan\xE9s de page ing\xE9r\xE9s depuis ${PAGES_DIR}.` : `ultra11y audit: ingesting page snapshots from ${PAGES_DIR}.`
54002
+ );
51656
54003
  const result = runAudit({
51657
54004
  inputs: auditInputs,
51658
54005
  stdin,
@@ -51673,13 +54020,20 @@ async function cmdAudit(p) {
51673
54020
  onWarn: (m) => console.error(m)
51674
54021
  });
51675
54022
  lang = resolveLang(p.flags, { audit: result });
54023
+ if (usePages) {
54024
+ const scope = pageScopesFrom(readSnapshots("."));
54025
+ if (scope.length) {
54026
+ result.scope.pages = scope;
54027
+ attributePages(result, scope);
54028
+ }
54029
+ }
51676
54030
  if (typeof p.flags.out === "string") {
51677
54031
  const out2 = p.flags.out;
51678
54032
  const asFile = out2.toLowerCase().endsWith(".json");
51679
- const target = asFile ? out2 : join36(out2, "audit-latest.json");
54033
+ const target = asFile ? out2 : join38(out2, "audit-latest.json");
51680
54034
  try {
51681
- mkdirSync10(asFile ? dirname10(out2) : out2, { recursive: true });
51682
- writeFileSync13(target, JSON.stringify(result, null, 2) + "\n");
54035
+ mkdirSync11(asFile ? dirname11(out2) : out2, { recursive: true });
54036
+ writeFileSync14(target, JSON.stringify(result, null, 2) + "\n");
51683
54037
  console.error(lang === "fr" ? `\u2192 audit \xE9crit dans ${target}` : `\u2192 audit written to ${target}`);
51684
54038
  } catch {
51685
54039
  }
@@ -51690,10 +54044,15 @@ async function cmdAudit(p) {
51690
54044
  console.error(`ultra11y audit: --fail-on must be blocking|major|minor (got "${String(failOnRaw)}").`);
51691
54045
  return 2;
51692
54046
  }
54047
+ const ciFormat = parseCiFormat(p.flags.format);
54048
+ if (ciFormat === null) {
54049
+ console.error(`ultra11y audit: --format must be sarif|github (got "${String(p.flags.format)}").`);
54050
+ return 2;
54051
+ }
51693
54052
  const baselineFlag = p.flags.baseline;
51694
54053
  if (typeof baselineFlag === "string" && baselineFlag) {
51695
54054
  let baseline = null;
51696
- if (existsSync21(baselineFlag)) {
54055
+ if (existsSync22(baselineFlag)) {
51697
54056
  try {
51698
54057
  const parsed = JSON.parse(readText(baselineFlag));
51699
54058
  if (isCurrentAudit(parsed)) baseline = parsed;
@@ -51707,7 +54066,8 @@ async function cmdAudit(p) {
51707
54066
  }
51708
54067
  const diff = diffAgainstBaseline(result, baseline, failOnParsed ?? "bloquant");
51709
54068
  const blindSpots2 = requireCaptures ? result.scope.captureCoverage?.blindSpots ?? [] : [];
51710
- if (p.flags.json)
54069
+ if (ciFormat) emitCiFormat({ ...result, findings: diff.newFindings }, ciFormat, CORE2, lang, failOnParsed ?? "bloquant");
54070
+ else if (p.flags.json)
51711
54071
  console.log(JSON.stringify(requireCaptures && result.scope.captureCoverage ? { ...diff, captureCoverage: result.scope.captureCoverage } : diff, null, 2));
51712
54072
  else {
51713
54073
  console.log(baselineSummary(diff, lang));
@@ -51719,7 +54079,8 @@ async function cmdAudit(p) {
51719
54079
  const failOn = failOnSet ? failOnParsed ?? "bloquant" : void 0;
51720
54080
  const failing = failOn ? findingsAtOrAbove(result.findings, failOn) : [];
51721
54081
  const blindSpots = requireCaptures ? result.scope.captureCoverage?.blindSpots ?? [] : [];
51722
- if (p.flags.json) console.log(JSON.stringify(result, null, 2));
54082
+ if (ciFormat) emitCiFormat(result, ciFormat, CORE2, lang, failOn);
54083
+ else if (p.flags.json) console.log(JSON.stringify(result, null, 2));
51723
54084
  else {
51724
54085
  console.log(auditSummary(result, lang));
51725
54086
  if (requireCaptures && result.scope.captureCoverage) console.error(captureCoverageSummary(result.scope.captureCoverage, lang));
@@ -51733,6 +54094,175 @@ async function cmdAudit(p) {
51733
54094
  if (!failOnSet && !requireCaptures) return 0;
51734
54095
  return failing.length || blindSpots.length ? 1 : 0;
51735
54096
  }
54097
+ async function cmdDev(p) {
54098
+ const root = typeof p.flags.root === "string" && p.flags.root ? p.flags.root : ".";
54099
+ const standard = stdOf(p, "dev");
54100
+ if (standard === null) return 2;
54101
+ const lang = resolveLang(p.flags, { standard });
54102
+ const portRaw = p.flags.port;
54103
+ const port = typeof portRaw === "string" ? Number.parseInt(portRaw, 10) : DEV_DEFAULT_PORT;
54104
+ if (!Number.isFinite(port) || port < 0 || port > 65535) {
54105
+ console.error(`ultra11y dev: --port must be a number between 0 and 65535 (got "${String(portRaw)}").`);
54106
+ return 2;
54107
+ }
54108
+ if (p.flags.next === true) {
54109
+ const dir = join38(root, ".ultra11y", "next");
54110
+ const rel = ".ultra11y/next/overlay.jsx";
54111
+ try {
54112
+ mkdirSync11(dir, { recursive: true });
54113
+ writeFileSync14(join38(dir, "overlay.jsx"), nextOverlayComponent(port));
54114
+ } catch (e) {
54115
+ console.error(`ultra11y dev: could not write ${rel}: ${e instanceof Error ? e.message : String(e)}`);
54116
+ return 1;
54117
+ }
54118
+ const fr2 = lang === "fr";
54119
+ console.log(fr2 ? `Composant overlay \xE9crit : ${rel}` : `Overlay component written: ${rel}`);
54120
+ console.log("");
54121
+ console.log(fr2 ? "Ajoutez-le \xE0 votre layout racine (app/layout.tsx) :" : "Add it to your root layout (app/layout.tsx):");
54122
+ console.log(` import { Ultra11yOverlay } from "../${rel.replace(/\.jsx$/, "")}";`);
54123
+ console.log(` <body>{children}<Ultra11yOverlay /></body>`);
54124
+ console.log("");
54125
+ console.log(
54126
+ fr2 ? `Puis lancez le side-car : ultra11y dev (tableau de bord : http://127.0.0.1:${port})` : `Then start the side-car: ultra11y dev (dashboard: http://127.0.0.1:${port})`
54127
+ );
54128
+ console.log(fr2 ? "Le composant ne rend RIEN hors d\xE9veloppement." : "The component renders NOTHING outside development.");
54129
+ return 0;
54130
+ }
54131
+ const fr = lang === "fr";
54132
+ let server;
54133
+ try {
54134
+ server = await startDevServer({ root, port, standard, lang, onLog: (m) => console.error(m) });
54135
+ } catch (e) {
54136
+ const msg = e instanceof Error ? e.message : String(e);
54137
+ console.error(
54138
+ /EADDRINUSE/.test(msg) ? `ultra11y dev: port ${port} is already in use \u2014 pass --port <n>.` : `ultra11y dev: could not start the server: ${msg}`
54139
+ );
54140
+ return 1;
54141
+ }
54142
+ console.error(fr ? `ultra11y dev : tableau de bord sur http://127.0.0.1:${server.port}` : `ultra11y dev: dashboard on http://127.0.0.1:${server.port}`);
54143
+ console.error(fr ? "Boucle locale uniquement (l'outil \xE9crit des fichiers). Ctrl-C pour arr\xEAter." : "Loopback only (the tool writes files). Ctrl-C to stop.");
54144
+ await new Promise((resolve10) => {
54145
+ const stop2 = () => {
54146
+ void server.close().then(resolve10);
54147
+ };
54148
+ process.once("SIGINT", stop2);
54149
+ process.once("SIGTERM", stop2);
54150
+ });
54151
+ return 0;
54152
+ }
54153
+ async function cmdPages(p) {
54154
+ const inFlag = p.flags.in;
54155
+ if (typeof inFlag !== "string" || !inFlag) {
54156
+ console.error("ultra11y pages: --in <audit.json> is required ('-' for stdin).");
54157
+ return 2;
54158
+ }
54159
+ const standard = stdOf(p, "pages");
54160
+ if (standard === null) return 2;
54161
+ const raw = inFlag === "-" ? await readStdin() : readInputFile(inFlag, "pages", "--in");
54162
+ if (raw === null) return 2;
54163
+ let result;
54164
+ try {
54165
+ result = JSON.parse(raw);
54166
+ } catch {
54167
+ console.error("ultra11y pages: --in is not valid JSON (expected an AuditResult).");
54168
+ return 2;
54169
+ }
54170
+ if (!isCurrentAudit(result)) {
54171
+ console.error("ultra11y pages: input is not a current ultra11y AuditResult (WCAG-keyed, schema v2). Re-run `audit`.");
54172
+ return 2;
54173
+ }
54174
+ const lang = resolveLang(p.flags, { audit: result, standard });
54175
+ const scope = pagesOf(result);
54176
+ if (!scope.length) {
54177
+ console.error(
54178
+ lang === "fr" ? "ultra11y pages : aucune page dans le p\xE9rim\xE8tre. Capturez des instantan\xE9s (render --e2e) ou scannez un \xE9chantillon (scan --sample)." : "ultra11y pages: no page in scope. Capture snapshots (render --e2e) or scan a sample (scan --sample)."
54179
+ );
54180
+ return 1;
54181
+ }
54182
+ attributePages(result, scope);
54183
+ if (p.flags.json) console.log(JSON.stringify({ pages: derivePages(result, scope), unattributed: unattributedFindings(result).length }, null, 2));
54184
+ else console.log(renderPageGrid(result, scope, standard, lang));
54185
+ return 0;
54186
+ }
54187
+ async function cmdSnapshot(p) {
54188
+ const sub = p.positionals[0];
54189
+ const root = typeof p.flags.root === "string" && p.flags.root ? p.flags.root : ".";
54190
+ const lang = resolveLang(p.flags, {});
54191
+ if (sub === "list") {
54192
+ const snaps = readSnapshots(root);
54193
+ if (p.flags.json)
54194
+ console.log(
54195
+ JSON.stringify(
54196
+ snaps.map((s) => s.meta),
54197
+ null,
54198
+ 2
54199
+ )
54200
+ );
54201
+ else if (!snaps.length) console.log(lang === "fr" ? `Aucun instantan\xE9 dans ${join38(root, PAGES_DIR)}.` : `No snapshot in ${join38(root, PAGES_DIR)}.`);
54202
+ else for (const s of snaps) console.log(`${s.meta.id} ${s.meta.name} ${s.meta.url}${s.meta.auth ? " [auth]" : ""}`);
54203
+ return 0;
54204
+ }
54205
+ if (sub !== "write") {
54206
+ console.error("ultra11y snapshot: expected `snapshot write` (payload on stdin) or `snapshot list`.");
54207
+ return 2;
54208
+ }
54209
+ const raw = await readStdin();
54210
+ if (!raw.trim()) {
54211
+ console.error("ultra11y snapshot write: no payload on stdin (expected {meta, dom, styles?, boxes?, axtree?}).");
54212
+ return 2;
54213
+ }
54214
+ let payload;
54215
+ try {
54216
+ payload = JSON.parse(raw);
54217
+ } catch {
54218
+ console.error("ultra11y snapshot write: stdin is not valid JSON.");
54219
+ return 2;
54220
+ }
54221
+ const v = validateSnapshotMeta(payload.meta);
54222
+ if (!v.ok || !v.meta) {
54223
+ for (const i2 of v.issues) console.error(`ultra11y snapshot write: ${i2.path} \u2014 ${i2.message}`);
54224
+ return 2;
54225
+ }
54226
+ if (typeof payload.dom !== "string" || !payload.dom.trim()) {
54227
+ console.error("ultra11y snapshot write: `dom` must be the serialized documentElement.outerHTML.");
54228
+ return 2;
54229
+ }
54230
+ let dir;
54231
+ try {
54232
+ dir = writeSnapshot(root, {
54233
+ meta: v.meta,
54234
+ dom: payload.dom,
54235
+ ...payload.styles ? { styles: payload.styles } : {},
54236
+ ...payload.boxes ? { boxes: payload.boxes } : {},
54237
+ ...payload.axtree ? { axtree: payload.axtree } : {},
54238
+ ...payload.css ? { css: payload.css } : {}
54239
+ });
54240
+ } catch (e) {
54241
+ console.error(`ultra11y snapshot write: could not write the snapshot: ${e instanceof Error ? e.message : String(e)}`);
54242
+ return 1;
54243
+ }
54244
+ if (typeof payload.screenshot === "string" && payload.screenshot) {
54245
+ try {
54246
+ writeFileSync14(join38(dir, "screen.png"), Buffer.from(payload.screenshot, "base64"));
54247
+ } catch {
54248
+ console.error("ultra11y snapshot write: the screenshot could not be written \u2014 the pixel tier will be skipped for this page.");
54249
+ }
54250
+ }
54251
+ const result = runAudit({ inputs: [join38(dir, "dom.html")], onWarn: (m) => console.error(m) });
54252
+ const failOnRaw = p.flags["fail-on"];
54253
+ const failOnParsed = parseFailOn(failOnRaw);
54254
+ if (failOnRaw !== void 0 && failOnParsed === null) {
54255
+ console.error(`ultra11y snapshot write: --fail-on must be blocking|major|minor (got "${String(failOnRaw)}").`);
54256
+ return 2;
54257
+ }
54258
+ const failing = failOnRaw !== void 0 ? findingsAtOrAbove(result.findings, failOnParsed ?? "bloquant").filter((f) => !f.advisory) : [];
54259
+ if (p.flags.json) console.log(JSON.stringify(result, null, 2));
54260
+ else {
54261
+ console.log(lang === "fr" ? `\u2192 instantan\xE9 \xE9crit dans ${dir}` : `\u2192 snapshot written to ${dir}`);
54262
+ console.log(auditSummary(result, lang));
54263
+ }
54264
+ return failing.length ? 1 : 0;
54265
+ }
51736
54266
  function cmdInit(p) {
51737
54267
  const root = repoRoot() ?? process.cwd();
51738
54268
  let engineRel = process.argv[1] ?? "scripts/ultra11y.mjs";
@@ -51755,9 +54285,9 @@ function cmdInit(p) {
51755
54285
  if (want.baseline) {
51756
54286
  const inputs = p.positionals.length ? p.positionals : ["."];
51757
54287
  const result = runAudit({ inputs, onWarn: (m) => console.error(m) });
51758
- mkdirSync10(join36(root, "audits"), { recursive: true });
51759
- const bp = join36(root, "audits", "baseline.json");
51760
- writeFileSync13(bp, JSON.stringify(result, null, 2) + "\n");
54288
+ mkdirSync11(join38(root, "audits"), { recursive: true });
54289
+ const bp = join38(root, "audits", "baseline.json");
54290
+ writeFileSync14(bp, JSON.stringify(result, null, 2) + "\n");
51761
54291
  wrote.push(bp);
51762
54292
  }
51763
54293
  if (want.hook) wrote.push(writeHook(root, engineRel, failOn, legacy ? "baseline" : "staged"));
@@ -51781,7 +54311,8 @@ function cmdCriteria(p) {
51781
54311
  list: p.flags.list === true,
51782
54312
  json: p.flags.json === true,
51783
54313
  lang: resolveLang(p.flags, { standard }),
51784
- standard
54314
+ standard,
54315
+ ...p.flags.glossary !== void 0 ? { glossary: p.flags.glossary } : {}
51785
54316
  });
51786
54317
  }
51787
54318
  async function cmdReport(p) {
@@ -51807,6 +54338,15 @@ async function cmdReport(p) {
51807
54338
  }
51808
54339
  const out2 = typeof p.flags.out === "string" ? p.flags.out : "audits";
51809
54340
  const lang = resolveLang(p.flags, { audit: result, standard });
54341
+ const ciFormat = parseCiFormat(p.flags.format);
54342
+ if (ciFormat === null) {
54343
+ console.error(`ultra11y report: --format must be sarif|github (got "${String(p.flags.format)}").`);
54344
+ return 2;
54345
+ }
54346
+ if (ciFormat) {
54347
+ emitCiFormat(result, ciFormat, standard, lang);
54348
+ return 0;
54349
+ }
51810
54350
  const path = writeReport(result, { out: out2, lang, standard });
51811
54351
  const untested = isCore(standard) ? [] : untestedNeedsRendering(result);
51812
54352
  const partial = untested.length > 0;
@@ -51883,13 +54423,67 @@ async function cmdPrd(p) {
51883
54423
  if (json) console.log(JSON.stringify({ paths, units: prdUnits(result, standard, lang), ...gh2 ? { gh: gh2 } : {} }, null, 2));
51884
54424
  return gh2 && gh2.failed > 0 ? 1 : 0;
51885
54425
  }
54426
+ function depsAt(root) {
54427
+ const pkgPath = join38(root, "package.json");
54428
+ if (!existsSync22(pkgPath)) return {};
54429
+ try {
54430
+ const pkg = JSON.parse(readText(pkgPath));
54431
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
54432
+ } catch {
54433
+ return {};
54434
+ }
54435
+ }
54436
+ function engineRefFor(root) {
54437
+ let ref = process.argv[1] ?? "scripts/ultra11y.mjs";
54438
+ try {
54439
+ const abs = realpathSync4(ref);
54440
+ ref = abs.startsWith(root + sep6) ? relative3(root, abs) : abs;
54441
+ } catch {
54442
+ }
54443
+ return ref;
54444
+ }
51886
54445
  function cmdRender(p) {
51887
54446
  const root = p.positionals[0] ?? ".";
51888
54447
  const lang = resolveLang(p.flags, {});
54448
+ if (p.flags.e2e === true) {
54449
+ const forced = typeof p.flags.runner === "string" ? p.flags.runner : void 0;
54450
+ if (forced !== void 0 && forced !== "playwright" && forced !== "cypress" && forced !== "auto") {
54451
+ console.error(`ultra11y render: --runner must be playwright|cypress|auto (got "${forced}").`);
54452
+ return 2;
54453
+ }
54454
+ const detected = detectE2eRunner(depsAt(root), (f) => existsSync22(join38(root, f)));
54455
+ const runners = forced && forced !== "auto" ? [forced] : detected;
54456
+ if (!runners.length) {
54457
+ console.error(e2eSetupPlan([], {}, lang));
54458
+ return 1;
54459
+ }
54460
+ const engineRef = engineRefFor(root);
54461
+ const dir = join38(root, ".ultra11y", "e2e");
54462
+ const paths = {};
54463
+ try {
54464
+ mkdirSync11(dir, { recursive: true });
54465
+ if (runners.includes("playwright")) {
54466
+ writeFileSync14(join38(dir, "playwright.mjs"), playwrightFixture(engineRef));
54467
+ paths.playwright = ".ultra11y/e2e/playwright.mjs";
54468
+ }
54469
+ if (runners.includes("cypress")) {
54470
+ writeFileSync14(join38(dir, "cypress-plugin.mjs"), cypressPlugin(engineRef));
54471
+ writeFileSync14(join38(dir, "cypress-commands.mjs"), cypressCommands());
54472
+ paths.cypressPlugin = ".ultra11y/e2e/cypress-plugin.mjs";
54473
+ paths.cypressCommands = ".ultra11y/e2e/cypress-commands.mjs";
54474
+ }
54475
+ } catch (e) {
54476
+ console.error(`ultra11y render: could not write the E2E fixtures: ${e instanceof Error ? e.message : String(e)}`);
54477
+ return 1;
54478
+ }
54479
+ if (p.flags.json) console.log(JSON.stringify({ runners, paths }, null, 2));
54480
+ else console.log(e2eSetupPlan(runners, paths, lang));
54481
+ return 0;
54482
+ }
51889
54483
  if (p.flags.scaffold === true) {
51890
54484
  const out2 = typeof p.flags.out === "string" && p.flags.out ? p.flags.out : "ultra11y-render.tsx";
51891
54485
  try {
51892
- writeFileSync13(out2, ssrHarness());
54486
+ writeFileSync14(out2, ssrHarness());
51893
54487
  } catch (e) {
51894
54488
  console.error(`ultra11y render: could not write ${out2}: ${e instanceof Error ? e.message : String(e)}`);
51895
54489
  return 1;
@@ -51903,29 +54497,29 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
51903
54497
  }
51904
54498
  if (p.flags.setup === true) {
51905
54499
  const rel = ".ultra11y/capture-setup.mjs";
51906
- const out2 = join36(root, rel);
54500
+ const out2 = join38(root, rel);
51907
54501
  try {
51908
- mkdirSync10(dirname10(out2), { recursive: true });
51909
- writeFileSync13(out2, captureSetup());
54502
+ mkdirSync11(dirname11(out2), { recursive: true });
54503
+ writeFileSync14(out2, captureSetup());
51910
54504
  } catch (e) {
51911
54505
  console.error(`ultra11y render: could not write ${out2}: ${e instanceof Error ? e.message : String(e)}`);
51912
54506
  return 1;
51913
54507
  }
51914
54508
  let setupDeps = {};
51915
- const setupPkg = join36(root, "package.json");
51916
- if (existsSync21(setupPkg)) {
54509
+ const setupPkg = join38(root, "package.json");
54510
+ if (existsSync22(setupPkg)) {
51917
54511
  try {
51918
54512
  const pkg = JSON.parse(readText(setupPkg));
51919
54513
  setupDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
51920
54514
  } catch {
51921
54515
  }
51922
54516
  }
51923
- const tr = detectTestRunner(setupDeps, (f) => existsSync21(join36(root, f)));
54517
+ const tr = detectTestRunner(setupDeps, (f) => existsSync22(join38(root, f)));
51924
54518
  console.log(captureSetupPlan(tr, rel, lang));
51925
54519
  const gaLine = ".ultra11y/captures/*.html text eol=lf linguist-generated=true";
51926
- const gaPath = join36(root, ".gitattributes");
54520
+ const gaPath = join38(root, ".gitattributes");
51927
54521
  try {
51928
- const existing = existsSync21(gaPath) ? readFileSync17(gaPath, "utf8") : "";
54522
+ const existing = existsSync22(gaPath) ? readFileSync19(gaPath, "utf8") : "";
51929
54523
  if (!existing.includes(".ultra11y/captures/")) {
51930
54524
  appendFileSync(gaPath, (existing && !existing.endsWith("\n") ? "\n" : "") + gaLine + "\n");
51931
54525
  console.log(lang === "fr" ? `.gitattributes : ajout\xE9 \xAB ${gaLine} \xBB` : `.gitattributes: added "${gaLine}"`);
@@ -51933,8 +54527,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
51933
54527
  } catch {
51934
54528
  }
51935
54529
  try {
51936
- const giPath = join36(root, ".gitignore");
51937
- if (existsSync21(giPath) && /^\s*\/?\.ultra11y(\/\**)?\/?\s*$/m.test(readFileSync17(giPath, "utf8")))
54530
+ const giPath = join38(root, ".gitignore");
54531
+ if (existsSync22(giPath) && /^\s*\/?\.ultra11y(\/\**)?\/?\s*$/m.test(readFileSync19(giPath, "utf8")))
51938
54532
  console.error(
51939
54533
  lang === "fr" ? "\u26A0\uFE0F .ultra11y semble ignor\xE9 par .gitignore \u2014 les captures doivent \xEAtre committ\xE9es pour le gate (ajoutez \xAB !.ultra11y/captures/ \xBB)." : '\u26A0\uFE0F .ultra11y appears gitignored \u2014 captures must be committed for the gate (add "!.ultra11y/captures/").'
51940
54534
  );
@@ -51944,8 +54538,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
51944
54538
  }
51945
54539
  if (p.flags.storybook === true || typeof p.flags.storybook === "string") {
51946
54540
  const sbDir = p.positionals[0] ?? "storybook-static";
51947
- const indexPath = existsSync21(join36(sbDir, "index.json")) ? join36(sbDir, "index.json") : join36(sbDir, "stories.json");
51948
- if (!existsSync21(indexPath)) {
54541
+ const indexPath = existsSync22(join38(sbDir, "index.json")) ? join38(sbDir, "index.json") : join38(sbDir, "stories.json");
54542
+ if (!existsSync22(indexPath)) {
51949
54543
  console.error(
51950
54544
  lang === "fr" ? `ultra11y render : aucun index Storybook (index.json/stories.json) dans ${sbDir}.` : `ultra11y render: no Storybook index (index.json/stories.json) in ${sbDir}.`
51951
54545
  );
@@ -51955,7 +54549,7 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
51955
54549
  const provById = new Map(stories.map((s) => [s.id, storyProvenance(s)]));
51956
54550
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
51957
54551
  const htmlDir = capturesFlag ?? sbDir;
51958
- const htmlFiles = existsSync21(htmlDir) ? discover([htmlDir]).files.filter((f) => /\.html?$/i.test(f)) : [];
54552
+ const htmlFiles = existsSync22(htmlDir) ? discover([htmlDir]).files.filter((f) => /\.html?$/i.test(f)) : [];
51959
54553
  const outDir = ".ultra11y/captures";
51960
54554
  let attributed = 0;
51961
54555
  let skipped = 0;
@@ -51977,8 +54571,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
51977
54571
  continue;
51978
54572
  }
51979
54573
  try {
51980
- mkdirSync10(outDir, { recursive: true });
51981
- writeFileSync13(join36(outDir, `${hitId}.html`), `${formatCaptureComment(prov)}
54574
+ mkdirSync11(outDir, { recursive: true });
54575
+ writeFileSync14(join38(outDir, `${hitId}.html`), `${formatCaptureComment(prov)}
51982
54576
  ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
51983
54577
  attributed++;
51984
54578
  } catch {
@@ -52000,11 +54594,11 @@ ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
52000
54594
  }
52001
54595
  if (p.flags.coverage === true) {
52002
54596
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
52003
- const capturesDir = capturesFlag ?? join36(root, ".ultra11y/captures");
54597
+ const capturesDir = capturesFlag ?? join38(root, ".ultra11y/captures");
52004
54598
  const graphExt = [...GRAPH_ONLY_EXT, ...asList(p.flags.ext) ?? []];
52005
54599
  const sourceFiles = discover([root], { include: asList(p.flags.include), exclude: asList(p.flags.exclude), ext: graphExt }).files;
52006
54600
  const graph = buildGraphStreaming(sourceFiles);
52007
- const capFiles = existsSync21(capturesDir) ? discover([capturesDir]).files : [];
54601
+ const capFiles = existsSync22(capturesDir) ? discover([capturesDir]).files : [];
52008
54602
  const entries = capFiles.map((f) => ({ file: toPosix(f), provenance: parseCaptureProvenance(readText(f)) }));
52009
54603
  const cov = computeCaptureCoverage(graph, entries);
52010
54604
  if (p.flags.json) console.log(JSON.stringify(cov, null, 2));
@@ -52012,15 +54606,15 @@ ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
52012
54606
  return 0;
52013
54607
  }
52014
54608
  let deps = {};
52015
- const pkgPath = join36(root, "package.json");
52016
- if (existsSync21(pkgPath)) {
54609
+ const pkgPath = join38(root, "package.json");
54610
+ if (existsSync22(pkgPath)) {
52017
54611
  try {
52018
54612
  const pkg = JSON.parse(readText(pkgPath));
52019
54613
  deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
52020
54614
  } catch {
52021
54615
  }
52022
54616
  }
52023
- const detection = detectFrameworks(deps, (f) => existsSync21(join36(root, f)));
54617
+ const detection = detectFrameworks(deps, (f) => existsSync22(join38(root, f)));
52024
54618
  if (p.flags.json) console.log(JSON.stringify(detection, null, 2));
52025
54619
  else console.log(renderPlan(detection, lang));
52026
54620
  return 0;
@@ -52219,9 +54813,9 @@ function applyAdjudicationFile(p, adj, lang) {
52219
54813
  return 1;
52220
54814
  }
52221
54815
  const out2 = typeof p.flags.out === "string" ? p.flags.out : ".";
52222
- mkdirSync10(out2, { recursive: true });
52223
- const auditPath = join36(out2, "audit-latest.json");
52224
- writeFileSync13(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
54816
+ mkdirSync11(out2, { recursive: true });
54817
+ const auditPath = join38(out2, "audit-latest.json");
54818
+ writeFileSync14(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
52225
54819
  if (p.flags.json) console.log(JSON.stringify({ ok: true, auditPath, applied: r.applied, stillManual: r.stillManual, grounding: r.grounding }, null, 2));
52226
54820
  else
52227
54821
  console.log(
@@ -52291,7 +54885,7 @@ async function cmdScan(p) {
52291
54885
  return 0;
52292
54886
  }
52293
54887
  for (const target of p.positionals.filter((a) => a !== "-")) {
52294
- if (/^https?:\/\//i.test(target) || existsSync21(target)) continue;
54888
+ if (/^https?:\/\//i.test(target) || existsSync22(target)) continue;
52295
54889
  console.error(
52296
54890
  lang === "fr" ? `ultra11y scan : fichier introuvable (file not found) : ${target}. Passez une URL http(s):// ou un fichier HTML existant.` : `ultra11y scan: File not found: ${target}. Pass an http(s):// URL or an existing HTML file.`
52297
54891
  );
@@ -52416,12 +55010,12 @@ async function cmdScan(p) {
52416
55010
  audit2 = parsed;
52417
55011
  }
52418
55012
  const merged = mergeDynamic(audit2, dynamic, lang);
52419
- mkdirSync10(out2, { recursive: true });
52420
- writeFileSync13(join36(out2, "audit-latest.json"), JSON.stringify(merged, null, 2) + "\n");
55013
+ mkdirSync11(out2, { recursive: true });
55014
+ writeFileSync14(join38(out2, "audit-latest.json"), JSON.stringify(merged, null, 2) + "\n");
52421
55015
  if (p.flags.json) console.log(JSON.stringify(merged, null, 2));
52422
55016
  else
52423
55017
  console.log(
52424
- lang === "fr" ? `Audit statique + dynamique fusionn\xE9 \u2192 ${join36(out2, "audit-latest.json")} (${merged.conformancePct}% r\xE9ussite, ${merged.findings.length} findings).` : `Static + dynamic audit merged \u2192 ${join36(out2, "audit-latest.json")} (${merged.conformancePct}% pass rate, ${merged.findings.length} findings).`
55018
+ lang === "fr" ? `Audit statique + dynamique fusionn\xE9 \u2192 ${join38(out2, "audit-latest.json")} (${merged.conformancePct}% r\xE9ussite, ${merged.findings.length} findings).` : `Static + dynamic audit merged \u2192 ${join38(out2, "audit-latest.json")} (${merged.conformancePct}% pass rate, ${merged.findings.length} findings).`
52425
55019
  );
52426
55020
  return 0;
52427
55021
  }
@@ -52601,7 +55195,7 @@ function cmdOrchestrate(p) {
52601
55195
  }
52602
55196
  const engineAbs = realpathSync4(fileURLToPath5(import.meta.url));
52603
55197
  if (p.flags.list === true) {
52604
- if (!existsSync21(runFlag)) {
55198
+ if (!existsSync22(runFlag)) {
52605
55199
  console.error(`ultra11y orchestrate: run dir not found: ${runFlag}.`);
52606
55200
  return 2;
52607
55201
  }
@@ -52628,7 +55222,7 @@ function cmdOrchestrate(p) {
52628
55222
  );
52629
55223
  } else {
52630
55224
  console.log(
52631
- lang === "fr" ? `Suivez ${join36(runFlag, "orchestration", "RUNBOOK.md")} s\xE9quentiellement (chemin \xE9co).` : `Follow ${join36(runFlag, "orchestration", "RUNBOOK.md")} sequentially (the eco path).`
55225
+ lang === "fr" ? `Suivez ${join38(runFlag, "orchestration", "RUNBOOK.md")} s\xE9quentiellement (chemin \xE9co).` : `Follow ${join38(runFlag, "orchestration", "RUNBOOK.md")} sequentially (the eco path).`
52632
55226
  );
52633
55227
  }
52634
55228
  if (p.flags.phase === void 0 && workflows.length === 0 && p.flags.eco !== true) {
@@ -52696,6 +55290,12 @@ async function main(argv) {
52696
55290
  return cmdScan(p);
52697
55291
  case "sample":
52698
55292
  return cmdSample(p);
55293
+ case "snapshot":
55294
+ return cmdSnapshot(p);
55295
+ case "pages":
55296
+ return cmdPages(p);
55297
+ case "dev":
55298
+ return cmdDev(p);
52699
55299
  case "fix":
52700
55300
  return cmdFix(p);
52701
55301
  case "init":