ultra11y 5.3.4 → 5.4.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.
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { realpathSync as realpathSync6, writeFileSync as writeFileSync19, mkdirSync as mkdirSync17, existsSync as existsSync34, readFileSync as readFileSync32, appendFileSync, copyFileSync as copyFileSync4 } from "fs";
5
- import { join as join50, relative as relative5, resolve as resolve15, sep as sep7, dirname as dirname14 } from "path";
4
+ import { realpathSync as realpathSync6, writeFileSync as writeFileSync20, mkdirSync as mkdirSync18, existsSync as existsSync35, readFileSync as readFileSync33, appendFileSync, copyFileSync as copyFileSync4 } from "fs";
5
+ import { join as join51, relative as relative5, resolve as resolve16, sep as sep7, dirname as dirname15 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.3.4";
9
+ var VERSION = "5.4.0";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -38491,176 +38491,795 @@ function toFinding(doc, ruleId, def, rf, ruleAdvisory) {
38491
38491
  };
38492
38492
  }
38493
38493
 
38494
- // src/name.ts
38495
- var collapse = (s) => s.replace(/\s+/g, " ").trim();
38496
- function mayInjectContent(el) {
38497
- return descendants(el).some((d) => d.tag === "slot" || d.tag !== "#fragment" && !isIntrinsic(d.tag));
38494
+ // src/rules/rendered.ts
38495
+ import { readFileSync as readFileSync15 } from "fs";
38496
+
38497
+ // src/color.ts
38498
+ var NAMED = {
38499
+ transparent: { r: 0, g: 0, b: 0, a: 0 },
38500
+ white: { r: 255, g: 255, b: 255, a: 1 },
38501
+ black: { r: 0, g: 0, b: 0, a: 1 },
38502
+ red: { r: 255, g: 0, b: 0, a: 1 },
38503
+ lime: { r: 0, g: 255, b: 0, a: 1 },
38504
+ green: { r: 0, g: 128, b: 0, a: 1 },
38505
+ blue: { r: 0, g: 0, b: 255, a: 1 },
38506
+ yellow: { r: 255, g: 255, b: 0, a: 1 },
38507
+ cyan: { r: 0, g: 255, b: 255, a: 1 },
38508
+ aqua: { r: 0, g: 255, b: 255, a: 1 },
38509
+ magenta: { r: 255, g: 0, b: 255, a: 1 },
38510
+ fuchsia: { r: 255, g: 0, b: 255, a: 1 },
38511
+ silver: { r: 192, g: 192, b: 192, a: 1 },
38512
+ gray: { r: 128, g: 128, b: 128, a: 1 },
38513
+ grey: { r: 128, g: 128, b: 128, a: 1 },
38514
+ maroon: { r: 128, g: 0, b: 0, a: 1 },
38515
+ olive: { r: 128, g: 128, b: 0, a: 1 },
38516
+ purple: { r: 128, g: 0, b: 128, a: 1 },
38517
+ teal: { r: 0, g: 128, b: 128, a: 1 },
38518
+ navy: { r: 0, g: 0, b: 128, a: 1 },
38519
+ orange: { r: 255, g: 165, b: 0, a: 1 }
38520
+ };
38521
+ function hex(part) {
38522
+ return parseInt(part.length === 1 ? part + part : part, 16);
38498
38523
  }
38499
- var HIDDEN_STYLE = /(^|;)\s*(display\s*:\s*none|visibility\s*:\s*(hidden|collapse))\s*(;|$)/i;
38500
- function isDisplayHidden(el) {
38501
- for (const node of [el, ...ancestors(el)]) {
38502
- if (hasAttr(node, "hidden")) return true;
38503
- if (HIDDEN_STYLE.test(attr(node, "style") ?? "")) return true;
38524
+ function parseColor(input) {
38525
+ const s = input.trim().toLowerCase();
38526
+ if (!s) return null;
38527
+ if (s in NAMED) return { ...NAMED[s] };
38528
+ if (s.startsWith("#")) {
38529
+ const h = s.slice(1);
38530
+ if (/^[0-9a-f]{3}$/.test(h)) return { r: hex(h[0]), g: hex(h[1]), b: hex(h[2]), a: 1 };
38531
+ if (/^[0-9a-f]{4}$/.test(h)) return { r: hex(h[0]), g: hex(h[1]), b: hex(h[2]), a: hex(h[3]) / 255 };
38532
+ if (/^[0-9a-f]{6}$/.test(h)) return { r: hex(h.slice(0, 2)), g: hex(h.slice(2, 4)), b: hex(h.slice(4, 6)), a: 1 };
38533
+ if (/^[0-9a-f]{8}$/.test(h)) return { r: hex(h.slice(0, 2)), g: hex(h.slice(2, 4)), b: hex(h.slice(4, 6)), a: hex(h.slice(6, 8)) / 255 };
38534
+ return null;
38504
38535
  }
38505
- return false;
38536
+ const m = /^rgba?\(([^)]+)\)$/.exec(s);
38537
+ if (m) {
38538
+ const parts2 = m[1].split(/[,/\s]+/).filter(Boolean);
38539
+ if (parts2.length < 3) return null;
38540
+ const chan = (p) => p.endsWith("%") ? Math.round(parseFloat(p) / 100 * 255) : parseFloat(p);
38541
+ const r = chan(parts2[0]);
38542
+ const g = chan(parts2[1]);
38543
+ const b = chan(parts2[2]);
38544
+ const a = parts2[3] !== void 0 ? parts2[3].endsWith("%") ? parseFloat(parts2[3]) / 100 : parseFloat(parts2[3]) : 1;
38545
+ if ([r, g, b, a].some((n) => Number.isNaN(n))) return null;
38546
+ return { r, g, b, a };
38547
+ }
38548
+ return null;
38506
38549
  }
38507
- function isHiddenFromAT(el) {
38508
- if (isDisplayHidden(el)) return true;
38509
- return [el, ...ancestors(el)].some((node) => attr(node, "aria-hidden") === "true");
38550
+ function channelLuminance(c2) {
38551
+ const cs = c2 / 255;
38552
+ return cs <= 0.03928 ? cs / 12.92 : ((cs + 0.055) / 1.055) ** 2.4;
38510
38553
  }
38511
- function isPresentational(el) {
38512
- return ["presentation", "none"].includes((attr(el, "role") ?? "").trim().toLowerCase());
38554
+ function relativeLuminance({ r, g, b }) {
38555
+ return 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b);
38513
38556
  }
38514
- function isNameExempt(el) {
38515
- return isHiddenFromAT(el) || isPresentational(el);
38557
+ function contrastRatio(a, b) {
38558
+ const la = relativeLuminance(a);
38559
+ const lb = relativeLuminance(b);
38560
+ const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
38561
+ return (hi + 0.05) / (lo + 0.05);
38516
38562
  }
38517
- function embeddedImageName(n, doc) {
38518
- const labelled = (attr(n, "aria-labelledby") ?? "").trim();
38519
- if (labelled) return doc ? ariaLabelledbyText(n, doc) || labelled : labelled;
38520
- return (boundAttr(n, "aria-label") ?? attr(n, "alt") ?? attr(n, "title") ?? "").trim();
38563
+ function parseInlineStyle(style) {
38564
+ const out2 = /* @__PURE__ */ new Map();
38565
+ for (const decl of style.split(";")) {
38566
+ const i2 = decl.indexOf(":");
38567
+ if (i2 === -1) continue;
38568
+ const prop = decl.slice(0, i2).trim().toLowerCase();
38569
+ const val = decl.slice(i2 + 1).trim();
38570
+ if (prop && val) out2.set(prop, val);
38571
+ }
38572
+ return out2;
38521
38573
  }
38522
- function nameFromContent(el, doc) {
38523
- let out2 = "";
38524
- const walk3 = (n) => {
38525
- if (n.type === "text") {
38526
- out2 += n.data;
38527
- return;
38574
+
38575
+ // src/pixel.ts
38576
+ import { deflateSync, inflateSync } from "zlib";
38577
+ var SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
38578
+ function paeth(a, b, c2) {
38579
+ const p = a + b - c2;
38580
+ const pa = Math.abs(p - a);
38581
+ const pb = Math.abs(p - b);
38582
+ const pc = Math.abs(p - c2);
38583
+ if (pa <= pb && pa <= pc) return a;
38584
+ return pb <= pc ? b : c2;
38585
+ }
38586
+ function decodePng(buf) {
38587
+ if (buf.length < 8 + 25) return null;
38588
+ for (let i2 = 0; i2 < SIGNATURE.length; i2++) if (buf[i2] !== SIGNATURE[i2]) return null;
38589
+ let width = 0;
38590
+ let height = 0;
38591
+ let bitDepth = 0;
38592
+ let colorType = 0;
38593
+ let interlace = 0;
38594
+ const idat = [];
38595
+ let off = 8;
38596
+ while (off + 8 <= buf.length) {
38597
+ const len = buf.readUInt32BE(off);
38598
+ const type = buf.toString("ascii", off + 4, off + 8);
38599
+ const dataStart = off + 8;
38600
+ const dataEnd = dataStart + len;
38601
+ if (dataEnd + 4 > buf.length) return null;
38602
+ if (type === "IHDR") {
38603
+ if (len < 13) return null;
38604
+ width = buf.readUInt32BE(dataStart);
38605
+ height = buf.readUInt32BE(dataStart + 4);
38606
+ bitDepth = buf[dataStart + 8] ?? 0;
38607
+ colorType = buf[dataStart + 9] ?? 0;
38608
+ interlace = buf[dataStart + 12] ?? 0;
38609
+ } else if (type === "IDAT") {
38610
+ idat.push(buf.subarray(dataStart, dataEnd));
38611
+ } else if (type === "IEND") {
38612
+ break;
38528
38613
  }
38529
- if (n.tag === "img") {
38530
- const a = embeddedImageName(n, doc);
38531
- if (a) out2 += " " + a;
38532
- return;
38614
+ off = dataEnd + 4;
38615
+ }
38616
+ if (!width || !height || !idat.length) return null;
38617
+ if (bitDepth !== 8) return null;
38618
+ if (colorType !== 2 && colorType !== 6) return null;
38619
+ if (interlace !== 0) return null;
38620
+ const channels = colorType === 6 ? 4 : 3;
38621
+ let raw;
38622
+ try {
38623
+ raw = inflateSync(Buffer.concat(idat));
38624
+ } catch {
38625
+ return null;
38626
+ }
38627
+ const stride = width * channels;
38628
+ if (raw.length < height * (stride + 1)) return null;
38629
+ const out2 = Buffer.alloc(height * stride);
38630
+ for (let y = 0; y < height; y++) {
38631
+ const filter = raw[y * (stride + 1)] ?? 0;
38632
+ const lineIn = y * (stride + 1) + 1;
38633
+ const lineOut = y * stride;
38634
+ const prevOut = lineOut - stride;
38635
+ for (let i2 = 0; i2 < stride; i2++) {
38636
+ const x = raw[lineIn + i2] ?? 0;
38637
+ const a = i2 >= channels ? out2[lineOut + i2 - channels] ?? 0 : 0;
38638
+ const b = y > 0 ? out2[prevOut + i2] ?? 0 : 0;
38639
+ const c2 = y > 0 && i2 >= channels ? out2[prevOut + i2 - channels] ?? 0 : 0;
38640
+ let v;
38641
+ switch (filter) {
38642
+ case 0:
38643
+ v = x;
38644
+ break;
38645
+ case 1:
38646
+ v = x + a;
38647
+ break;
38648
+ case 2:
38649
+ v = x + b;
38650
+ break;
38651
+ case 3:
38652
+ v = x + (a + b >> 1);
38653
+ break;
38654
+ case 4:
38655
+ v = x + paeth(a, b, c2);
38656
+ break;
38657
+ default:
38658
+ return null;
38659
+ }
38660
+ out2[lineOut + i2] = v & 255;
38533
38661
  }
38534
- if (n.tag === "svg") {
38535
- const title2 = descendants(n).find((d) => d.tag === "title");
38536
- if (title2) out2 += " " + nameFromContent(title2, doc);
38537
- return;
38662
+ }
38663
+ return {
38664
+ width,
38665
+ height,
38666
+ at(x, y) {
38667
+ if (x < 0 || y < 0 || x >= width || y >= height) return void 0;
38668
+ const p = y * stride + x * channels;
38669
+ return {
38670
+ r: out2[p] ?? 0,
38671
+ g: out2[p + 1] ?? 0,
38672
+ b: out2[p + 2] ?? 0,
38673
+ a: channels === 4 ? (out2[p + 3] ?? 255) / 255 : 1
38674
+ };
38538
38675
  }
38539
- if (attr(n, "aria-hidden") === "true") return;
38540
- for (const c2 of n.children) walk3(c2);
38541
38676
  };
38542
- for (const c2 of el.children) walk3(c2);
38543
- return collapse(out2);
38544
38677
  }
38545
- function ariaLabelledbyText(el, doc) {
38546
- const ids = attr(el, "aria-labelledby");
38547
- if (!ids) return "";
38548
- const parts2 = [];
38549
- for (const id of ids.split(/\s+/).filter(Boolean)) {
38550
- const ref = doc.byId.get(id);
38551
- if (ref) parts2.push(nameFromContent(ref) || (attr(ref, "aria-label") ?? "").trim());
38678
+ function clamp(img, r) {
38679
+ const x = Math.max(0, Math.round(r.x));
38680
+ const y = Math.max(0, Math.round(r.y));
38681
+ const x2 = Math.min(img.width, Math.round(r.x + r.w));
38682
+ const y2 = Math.min(img.height, Math.round(r.y + r.h));
38683
+ if (x2 <= x || y2 <= y) return null;
38684
+ return { x, y, w: x2 - x, h: y2 - y };
38685
+ }
38686
+ var MAX_SAMPLES = 4096;
38687
+ var DOMINANCE = 0.6;
38688
+ var BUCKET = 16;
38689
+ function dominantBackground(img, rect) {
38690
+ const r = clamp(img, rect);
38691
+ if (!r) return null;
38692
+ const step = Math.max(1, Math.floor(Math.sqrt(r.w * r.h / MAX_SAMPLES)));
38693
+ const counts = /* @__PURE__ */ new Map();
38694
+ let total = 0;
38695
+ for (let y = r.y; y < r.y + r.h; y += step) {
38696
+ for (let x = r.x; x < r.x + r.w; x += step) {
38697
+ const p = img.at(x, y);
38698
+ if (!p) continue;
38699
+ const key = Math.round(p.r / BUCKET) << 16 | Math.round(p.g / BUCKET) << 8 | Math.round(p.b / BUCKET);
38700
+ const cur = counts.get(key) ?? { n: 0, r: 0, g: 0, b: 0 };
38701
+ cur.n++;
38702
+ cur.r += p.r;
38703
+ cur.g += p.g;
38704
+ cur.b += p.b;
38705
+ counts.set(key, cur);
38706
+ total++;
38707
+ }
38552
38708
  }
38553
- return collapse(parts2.join(" "));
38709
+ if (!total) return null;
38710
+ let best = null;
38711
+ for (const c2 of counts.values()) if (!best || c2.n > best.n) best = c2;
38712
+ if (!best || best.n / total < DOMINANCE) return null;
38713
+ return { r: Math.round(best.r / best.n), g: Math.round(best.g / best.n), b: Math.round(best.b / best.n), a: 1 };
38554
38714
  }
38555
- var BUTTON_INPUT = /* @__PURE__ */ new Set(["button", "submit", "reset"]);
38556
- var NAMELESS_BY_DEFAULT = /* @__PURE__ */ new Set(["submit", "reset"]);
38557
- function accessibleName(el, doc) {
38558
- const labelledby = ariaLabelledbyText(el, doc);
38559
- if (labelledby) return labelledby;
38560
- if (hasBoundAttr(el, "aria-labelledby") && !attr(el, "aria-labelledby")) return "\xA0";
38561
- const ariaLabel = (boundAttr(el, "aria-label") ?? "").trim();
38562
- if (ariaLabel) return ariaLabel;
38563
- if (el.tag === "img" || el.tag === "area") {
38564
- return (boundAttr(el, "alt") ?? "").trim();
38715
+ var CRC_TABLE = /* @__PURE__ */ (() => {
38716
+ const t2 = new Uint32Array(256);
38717
+ for (let n = 0; n < 256; n++) {
38718
+ let c2 = n;
38719
+ for (let k = 0; k < 8; k++) c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
38720
+ t2[n] = c2 >>> 0;
38565
38721
  }
38566
- if (el.tag === "input") {
38567
- const type = (attr(el, "type") ?? "text").toLowerCase();
38568
- if (BUTTON_INPUT.has(type)) {
38569
- const value = (attr(el, "value") ?? "").trim();
38570
- if (value) return value;
38571
- if (NAMELESS_BY_DEFAULT.has(type)) return type === "submit" ? "Submit" : "Reset";
38572
- return (attr(el, "title") ?? "").trim();
38722
+ return t2;
38723
+ })();
38724
+ function crc32(buf) {
38725
+ let c2 = 4294967295;
38726
+ for (let i2 = 0; i2 < buf.length; i2++) c2 = CRC_TABLE[(c2 ^ buf[i2]) & 255] ^ c2 >>> 8;
38727
+ return (c2 ^ 4294967295) >>> 0;
38728
+ }
38729
+ function chunk(type, data2) {
38730
+ const len = Buffer.alloc(4);
38731
+ len.writeUInt32BE(data2.length);
38732
+ const body3 = Buffer.concat([Buffer.from(type, "latin1"), data2]);
38733
+ const crc = Buffer.alloc(4);
38734
+ crc.writeUInt32BE(crc32(body3));
38735
+ return Buffer.concat([len, body3, crc]);
38736
+ }
38737
+ function encodePng(r) {
38738
+ const expected = r.width * r.height * 4;
38739
+ if (r.width <= 0 || r.height <= 0) throw new Error(`encodePng: refusing a ${r.width}\xD7${r.height} raster`);
38740
+ if (r.data.length !== expected) {
38741
+ throw new Error(`encodePng: ${r.width}\xD7${r.height} needs ${expected} bytes of RGBA, got ${r.data.length}`);
38742
+ }
38743
+ const stride = r.width * 4;
38744
+ const raw = Buffer.alloc(r.height * (stride + 1));
38745
+ for (let y = 0; y < r.height; y++) {
38746
+ raw[y * (stride + 1)] = 0;
38747
+ r.data.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
38748
+ }
38749
+ const ihdr = Buffer.alloc(13);
38750
+ ihdr.writeUInt32BE(r.width, 0);
38751
+ ihdr.writeUInt32BE(r.height, 4);
38752
+ ihdr[8] = 8;
38753
+ ihdr[9] = 6;
38754
+ return Buffer.concat([Buffer.from(SIGNATURE), chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw, { level: 9 })), chunk("IEND", Buffer.alloc(0))]);
38755
+ }
38756
+ function cropImage(img, rect) {
38757
+ const r = clamp(img, rect);
38758
+ if (!r) return null;
38759
+ const data2 = Buffer.alloc(r.w * r.h * 4);
38760
+ let i2 = 0;
38761
+ for (let y = r.y; y < r.y + r.h; y++) {
38762
+ for (let x = r.x; x < r.x + r.w; x++) {
38763
+ const p = img.at(x, y);
38764
+ data2[i2] = p?.r ?? 0;
38765
+ data2[i2 + 1] = p?.g ?? 0;
38766
+ data2[i2 + 2] = p?.b ?? 0;
38767
+ data2[i2 + 3] = Math.round((p?.a ?? 1) * 255);
38768
+ i2 += 4;
38573
38769
  }
38574
38770
  }
38575
- const content = nameFromContent(el, doc);
38576
- if (content) return content;
38577
- return (attr(el, "title") ?? "").trim();
38771
+ return { raster: { width: r.w, height: r.h, data: data2 }, rect: r };
38578
38772
  }
38579
- var FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
38580
- var NON_LABELABLE_INPUT = /* @__PURE__ */ new Set(["hidden", "submit", "reset", "button", "image"]);
38581
- function isFormField(el) {
38582
- if (!FIELD_TAGS.has(el.tag)) return false;
38583
- if (hasAttr(el, "hidden")) return false;
38584
- if (el.tag === "input") {
38585
- const type = (attr(el, "type") ?? "text").toLowerCase();
38586
- return !NON_LABELABLE_INPUT.has(type);
38773
+ function blend(raster, x, y, c2) {
38774
+ if (x < 0 || y < 0 || x >= raster.width || y >= raster.height) return;
38775
+ const p = (y * raster.width + x) * 4;
38776
+ const a = Math.min(1, Math.max(0, c2.a));
38777
+ if (a >= 1) {
38778
+ raster.data[p] = c2.r;
38779
+ raster.data[p + 1] = c2.g;
38780
+ raster.data[p + 2] = c2.b;
38781
+ raster.data[p + 3] = 255;
38782
+ return;
38587
38783
  }
38588
- return true;
38784
+ const inv = 1 - a;
38785
+ raster.data[p] = Math.round(c2.r * a + raster.data[p] * inv);
38786
+ raster.data[p + 1] = Math.round(c2.g * a + raster.data[p + 1] * inv);
38787
+ raster.data[p + 2] = Math.round(c2.b * a + raster.data[p + 2] * inv);
38788
+ raster.data[p + 3] = Math.max(raster.data[p + 3], Math.round(a * 255));
38589
38789
  }
38590
- function controlLabel(el, doc) {
38591
- if (attr(el, "aria-labelledby") && ariaLabelledbyText(el, doc) || hasBoundAttr(el, "aria-labelledby") && !attr(el, "aria-labelledby"))
38592
- return { hasLabel: true, via: "aria-labelledby" };
38593
- if ((boundAttr(el, "aria-label") ?? "").trim()) return { hasLabel: true, via: "aria-label" };
38594
- const id = attr(el, "id");
38595
- if (id) {
38596
- const lbl = doc.elements.find((e) => e.tag === "label" && attr(e, "for") === id);
38597
- if (lbl) return { hasLabel: true, via: "for" };
38790
+ function fillRect(raster, rect, color) {
38791
+ const x0 = Math.max(0, Math.round(rect.x));
38792
+ const y0 = Math.max(0, Math.round(rect.y));
38793
+ const x1 = Math.min(raster.width, Math.round(rect.x + rect.w));
38794
+ const y1 = Math.min(raster.height, Math.round(rect.y + rect.h));
38795
+ for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) blend(raster, x, y, color);
38796
+ }
38797
+ function strokeRect(raster, rect, color, width, dash) {
38798
+ const t2 = Math.max(1, Math.round(width));
38799
+ const x0 = Math.round(rect.x);
38800
+ const y0 = Math.round(rect.y);
38801
+ const x1 = Math.round(rect.x + rect.w);
38802
+ const y1 = Math.round(rect.y + rect.h);
38803
+ if (x1 <= x0 || y1 <= y0) return;
38804
+ const on = (coord) => {
38805
+ if (!dash || dash <= 1) return true;
38806
+ const half = dash / 2;
38807
+ return Math.floor(coord / half) % 2 === 0;
38808
+ };
38809
+ for (let y = y0; y < y1; y++) {
38810
+ for (let x = x0; x < x1; x++) {
38811
+ const top = y < y0 + t2;
38812
+ const bottom = y >= y1 - t2;
38813
+ const left = x < x0 + t2;
38814
+ const right = x >= x1 - t2;
38815
+ if (!top && !bottom && !left && !right) continue;
38816
+ if (!on(top || bottom ? x : y)) continue;
38817
+ blend(raster, x, y, color);
38818
+ }
38598
38819
  }
38599
- if (ancestors(el).some((a) => a.tag === "label")) return { hasLabel: true, via: "wrapping" };
38600
- if ((attr(el, "title") ?? "").trim()) return { hasLabel: true, via: "title" };
38601
- return { hasLabel: false, via: null };
38602
38820
  }
38603
38821
 
38604
- // src/rules/images.ts
38605
- var isHidden = isNameExempt;
38606
- var named = (el) => !!(boundAttr(el, "aria-label") ?? "").trim() || hasBoundAttr(el, "aria-labelledby");
38607
- var imgAltMissing = {
38608
- id: "img-alt-missing",
38609
- criteria: ["1.1.1"],
38610
- severity: "bloquant",
38822
+ // src/rules/rendered.ts
38823
+ var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "head", "title", "meta", "noscript", "link", "html", "br", "wbr"]);
38824
+ function styleAt(doc, i2) {
38825
+ return doc.signals?.styles?.get(i2);
38826
+ }
38827
+ function hasDirectText(el) {
38828
+ return el.children.some((c2) => c2.type === "text" && c2.data.trim() !== "");
38829
+ }
38830
+ function invisible(css) {
38831
+ if (css.display === "none" || css.visibility === "hidden") return true;
38832
+ const op = Number.parseFloat(css.opacity ?? "1");
38833
+ return Number.isFinite(op) && op === 0;
38834
+ }
38835
+ function backdropOf(doc, index, el) {
38836
+ for (let p = el; p; p = p.parent) {
38837
+ const i2 = index.get(p);
38838
+ if (i2 === void 0) continue;
38839
+ const css = styleAt(doc, i2)?.css;
38840
+ if (!css) continue;
38841
+ const img = css.backgroundImage;
38842
+ if (img && img !== "none") return void 0;
38843
+ const c2 = parseColor(css.backgroundColor ?? "");
38844
+ if (c2 && c2.a >= 1) return { color: c2, fromImage: false };
38845
+ }
38846
+ return void 0;
38847
+ }
38848
+ function isLargeText(css) {
38849
+ const px = Number.parseFloat(css.fontSize ?? "");
38850
+ if (!Number.isFinite(px)) return false;
38851
+ const weight = Number.parseInt(css.fontWeight ?? "400", 10);
38852
+ const bold = Number.isFinite(weight) ? weight >= 700 : css.fontWeight === "bold";
38853
+ return px >= 24 || px >= 18.66 && bold;
38854
+ }
38855
+ function elementIndex(doc) {
38856
+ const m = /* @__PURE__ */ new Map();
38857
+ doc.elements.forEach((el, i2) => m.set(el, i2));
38858
+ return m;
38859
+ }
38860
+ var renderedContrast = {
38861
+ id: "rendered-contrast",
38862
+ criteria: ["1.4.3"],
38863
+ severity: "majeur",
38611
38864
  run(doc) {
38865
+ if (!doc.signals?.styles) return [];
38866
+ const index = elementIndex(doc);
38612
38867
  const out2 = [];
38613
- for (const el of doc.elements) {
38614
- const isImg = el.tag === "img" || el.tag === "area" || (attr(el, "role") ?? "") === "img";
38615
- if (!isImg) continue;
38616
- if (isHidden(el) && el.tag !== "area") continue;
38617
- const altLiteral = attr(el, "alt");
38618
- const whitespaceAlt = altLiteral !== void 0 && altLiteral !== "" && altLiteral.trim() === "";
38619
- if (hasBoundAttr(el, "alt") && !whitespaceAlt || named(el)) continue;
38620
- if (hasDynamicSpread(el)) continue;
38621
- if (el.tag !== "img" && el.tag !== "area" && accessibleName(el, doc).trim() !== "") continue;
38622
- if ((attr(el, "title") ?? "").trim() !== "") {
38623
- out2.push({
38624
- criteriaId: "1.1.1",
38625
- el,
38626
- msgId: "img-alt-missing.title-only",
38627
- params: { tag: el.tag },
38628
- advisory: true,
38629
- severity: "mineur"
38630
- });
38631
- continue;
38632
- }
38868
+ for (const [el, i2] of index) {
38869
+ if (SKIP_TAGS.has(el.tag) || !hasDirectText(el)) continue;
38870
+ const css = styleAt(doc, i2)?.css;
38871
+ if (!css || invisible(css)) continue;
38872
+ const fg = parseColor(css.color ?? "");
38873
+ if (!fg || fg.a < 1) continue;
38874
+ const bd = backdropOf(doc, index, el);
38875
+ if (!bd) continue;
38876
+ const ratio = contrastRatio(fg, bd.color);
38877
+ const large = isLargeText(css);
38878
+ const min = large ? 3 : 4.5;
38879
+ if (ratio >= min) continue;
38633
38880
  out2.push({
38634
- criteriaId: "1.1.1",
38881
+ criteriaId: "1.4.3",
38635
38882
  el,
38636
- msgId: "img-alt-missing",
38637
- params: { tag: el.tag }
38883
+ msgId: "rendered-contrast",
38884
+ params: { ratio: ratio.toFixed(2), min, textSize: large ? "large" : "normal" }
38638
38885
  });
38639
38886
  }
38640
38887
  return out2;
38641
38888
  }
38642
38889
  };
38643
- var decorativeAltMisuse = {
38644
- id: "decorative-alt-misuse",
38645
- criteria: ["1.1.1"],
38890
+ var renderedContrastPixel = {
38891
+ id: "rendered-contrast-pixel",
38892
+ criteria: ["1.4.3"],
38646
38893
  severity: "majeur",
38647
38894
  run(doc) {
38895
+ const shot = doc.signals?.screenshot;
38896
+ const boxes = doc.signals?.boxes;
38897
+ if (!shot || !boxes || !doc.signals?.styles) return [];
38898
+ let img = null;
38899
+ try {
38900
+ img = decodePng(readFileSync15(shot));
38901
+ } catch {
38902
+ return [];
38903
+ }
38904
+ if (!img) return [];
38905
+ const index = elementIndex(doc);
38648
38906
  const out2 = [];
38649
- for (const el of doc.elements) {
38650
- if (el.tag !== "img") continue;
38651
- const alt = attr(el, "alt");
38652
- const role = (attr(el, "role") ?? "").trim();
38653
- const ariaLabel = (attr(el, "aria-label") ?? "").trim();
38654
- const title2 = (attr(el, "title") ?? "").trim();
38655
- if (alt === "" && (ariaLabel || title2)) {
38656
- out2.push({
38657
- criteriaId: "1.1.1",
38658
- el,
38659
- msgId: "decorative-alt-misuse.empty-but-named"
38660
- });
38661
- } else if (["presentation", "none"].includes(role) && alt?.trim()) {
38662
- out2.push({
38663
- criteriaId: "1.1.1",
38907
+ for (const [el, i2] of index) {
38908
+ if (SKIP_TAGS.has(el.tag) || !hasDirectText(el)) continue;
38909
+ const css = styleAt(doc, i2)?.css;
38910
+ if (!css || invisible(css)) continue;
38911
+ if (backdropOf(doc, index, el)) continue;
38912
+ const fg = parseColor(css.color ?? "");
38913
+ if (!fg || fg.a < 1) continue;
38914
+ const box = boxes.get(i2);
38915
+ if (!box || box.w < 4 || box.h < 4) continue;
38916
+ const bg = dominantBackground(img, box);
38917
+ if (!bg) continue;
38918
+ const ratio = contrastRatio(fg, bg);
38919
+ const large = isLargeText(css);
38920
+ const min = large ? 3 : 4.5;
38921
+ if (ratio >= min) continue;
38922
+ out2.push({
38923
+ criteriaId: "1.4.3",
38924
+ el,
38925
+ msgId: "rendered-contrast-pixel",
38926
+ params: { ratio: ratio.toFixed(2), min, textSize: large ? "large" : "normal" }
38927
+ });
38928
+ }
38929
+ return out2;
38930
+ }
38931
+ };
38932
+ function hasNonColourAffordance(css, parentCss) {
38933
+ const deco = css.textDecorationLine ?? "";
38934
+ if (deco && deco !== "none") return true;
38935
+ if ((css.borderBottomStyle ?? "none") !== "none" && Number.parseFloat(css.borderBottomWidth ?? "0") > 0) return true;
38936
+ const bg = parseColor(css.backgroundColor ?? "");
38937
+ if (bg && bg.a > 0) return true;
38938
+ const w = Number.parseInt(css.fontWeight ?? "400", 10);
38939
+ const pw = Number.parseInt(parentCss?.fontWeight ?? "400", 10);
38940
+ if (Number.isFinite(w) && Number.isFinite(pw) && w >= pw + 200) return true;
38941
+ return false;
38942
+ }
38943
+ var TEXT_BLOCK = /* @__PURE__ */ new Set(["p", "li", "dd", "dt", "td", "th", "blockquote", "figcaption", "caption"]);
38944
+ var renderedLinkColourOnly = {
38945
+ id: "rendered-link-colour-only",
38946
+ criteria: ["1.4.1"],
38947
+ severity: "majeur",
38948
+ run(doc) {
38949
+ if (!doc.signals?.styles) return [];
38950
+ const index = elementIndex(doc);
38951
+ const out2 = [];
38952
+ for (const [el, i2] of index) {
38953
+ if (el.tag !== "a" || !hasDirectText(el)) continue;
38954
+ const parent = el.parent;
38955
+ if (!parent || !TEXT_BLOCK.has(parent.tag)) continue;
38956
+ const css = styleAt(doc, i2)?.css;
38957
+ const pi = index.get(parent);
38958
+ const parentCss = pi === void 0 ? void 0 : styleAt(doc, pi)?.css;
38959
+ if (!css || !parentCss || invisible(css)) continue;
38960
+ if (hasNonColourAffordance(css, parentCss)) continue;
38961
+ const linkColor = parseColor(css.color ?? "");
38962
+ const textColor = parseColor(parentCss.color ?? "");
38963
+ if (!linkColor || !textColor) continue;
38964
+ const distinct = contrastRatio(linkColor, textColor);
38965
+ if (distinct <= 1.02) continue;
38966
+ out2.push({
38967
+ criteriaId: "1.4.1",
38968
+ el,
38969
+ msgId: "rendered-link-colour-only",
38970
+ params: { ratio: distinct.toFixed(2) }
38971
+ });
38972
+ }
38973
+ return out2;
38974
+ }
38975
+ };
38976
+ var BOUNDED_CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea", "button"]);
38977
+ var SIDES = ["Top", "Right", "Bottom", "Left"];
38978
+ function borderColours(css) {
38979
+ const out2 = [];
38980
+ for (const s of SIDES) {
38981
+ const style = css[`border${s}Style`];
38982
+ const width = Number.parseFloat(css[`border${s}Width`] ?? "0");
38983
+ if (!style || style === "none" || style === "hidden" || !(width > 0)) continue;
38984
+ const c2 = parseColor(css[`border${s}Color`] ?? "");
38985
+ if (c2 && c2.a >= 1) out2.push(c2);
38986
+ }
38987
+ return out2;
38988
+ }
38989
+ var renderedNonTextContrast = {
38990
+ id: "rendered-nontext-contrast",
38991
+ criteria: ["1.4.11"],
38992
+ severity: "majeur",
38993
+ run(doc) {
38994
+ if (!doc.signals?.styles) return [];
38995
+ const index = elementIndex(doc);
38996
+ const out2 = [];
38997
+ for (const [el, i2] of index) {
38998
+ if (!BOUNDED_CONTROLS.has(el.tag)) continue;
38999
+ const type = (el.attribs.type ?? "").toLowerCase();
39000
+ if (el.tag === "input" && (type === "hidden" || type === "image")) continue;
39001
+ const css = styleAt(doc, i2)?.css;
39002
+ if (!css || invisible(css)) continue;
39003
+ if (css.boxShadow && css.boxShadow !== "none") continue;
39004
+ if ((css.outlineStyle ?? "none") !== "none" && Number.parseFloat(css.outlineWidth ?? "0") > 0) continue;
39005
+ const surrounding = el.parent ? backdropOf(doc, index, el.parent) : void 0;
39006
+ if (!surrounding) continue;
39007
+ const own = parseColor(css.backgroundColor ?? "");
39008
+ const fill = own && own.a >= 1 ? own : surrounding.color;
39009
+ if (contrastRatio(fill, surrounding.color) >= 3) continue;
39010
+ const borders = borderColours(css);
39011
+ if (borders.some((b) => contrastRatio(b, surrounding.color) >= 3 || contrastRatio(b, fill) >= 3)) continue;
39012
+ out2.push({
39013
+ criteriaId: "1.4.11",
39014
+ el,
39015
+ msgId: "rendered-nontext-contrast",
39016
+ params: { ratio: contrastRatio(fill, surrounding.color).toFixed(2), control: el.tag }
39017
+ });
39018
+ }
39019
+ return out2;
39020
+ }
39021
+ };
39022
+ var FOCUS_SEL = /:focus(-visible|-within)?\b/;
39023
+ function killsOutline(d) {
39024
+ const o = (d.outline ?? "").trim();
39025
+ if (o === "none" || /^0(px)?( |$)/.test(o) || /\bnone\b/.test(o)) return true;
39026
+ if ((d.outlineStyle ?? "") === "none") return true;
39027
+ return d.outlineWidth !== void 0 && Number.parseFloat(d.outlineWidth) === 0;
39028
+ }
39029
+ function restoresIndicator(d) {
39030
+ if (d.boxShadow && d.boxShadow !== "none") return true;
39031
+ if (d.outlineStyle && d.outlineStyle !== "none" && Number.parseFloat(d.outlineWidth ?? "1") > 0) return true;
39032
+ if (d.outline && !killsOutline(d)) return true;
39033
+ if (d.textDecorationLine && d.textDecorationLine !== "none") return true;
39034
+ for (const k of Object.keys(d)) {
39035
+ if (/^border(Top|Right|Bottom|Left)?(Color|Width|Style)?$/.test(k) && d[k] && d[k] !== "none") return true;
39036
+ if (/^background(Color|Image)?$/.test(k) && d[k] && d[k] !== "none" && d[k] !== "rgba(0, 0, 0, 0)") return true;
39037
+ }
39038
+ return d.filter !== void 0 || d.transform !== void 0 || d.color !== void 0;
39039
+ }
39040
+ var renderedFocusNotVisible = {
39041
+ id: "rendered-focus-not-visible",
39042
+ criteria: ["2.4.7"],
39043
+ severity: "majeur",
39044
+ scope: "page",
39045
+ run(doc) {
39046
+ const css = doc.signals?.css;
39047
+ if (!css || css.unreadable > 0 || css.truncated) return [];
39048
+ const focusRules = css.rules.filter((r) => FOCUS_SEL.test(r.selector));
39049
+ if (!focusRules.length) return [];
39050
+ if (!focusRules.some((r) => killsOutline(r.decls))) return [];
39051
+ if (focusRules.some((r) => restoresIndicator(r.decls))) return [];
39052
+ const anchor = doc.elements.find((e) => ["a", "button", "input", "select", "textarea"].includes(e.tag));
39053
+ if (!anchor) return [];
39054
+ const killer = focusRules.find((r) => killsOutline(r.decls));
39055
+ return [{ criteriaId: "2.4.7", el: doc.elements[0] ?? anchor, msgId: "rendered-focus-not-visible", params: { selector: killer?.selector ?? ":focus" } }];
39056
+ }
39057
+ };
39058
+ var DOC_SEL = /^\s*(\*|:root|html|body)\s*$/i;
39059
+ function quarterTurn(transform) {
39060
+ const m = /rotate[ZY]?\(\s*(-?[\d.]+)deg\s*\)/i.exec(transform);
39061
+ if (!m) return false;
39062
+ const deg = Math.abs(Number.parseFloat(m[1])) % 180;
39063
+ return Math.abs(deg - 90) < 1;
39064
+ }
39065
+ var renderedOrientationLock = {
39066
+ id: "rendered-orientation-lock",
39067
+ criteria: ["1.3.4"],
39068
+ severity: "majeur",
39069
+ scope: "page",
39070
+ run(doc) {
39071
+ const css = doc.signals?.css;
39072
+ if (!css || css.unreadable > 0) return [];
39073
+ for (const r of css.rules) {
39074
+ if (!r.media || !/orientation\s*:/i.test(r.media)) continue;
39075
+ if (!r.selector.split(",").some((s) => DOC_SEL.test(s))) continue;
39076
+ const t2 = r.decls.transform ?? "";
39077
+ if (!quarterTurn(t2)) continue;
39078
+ const el = doc.elements[0];
39079
+ if (!el) continue;
39080
+ return [{ criteriaId: "1.3.4", el, msgId: "rendered-orientation-lock", params: { media: r.media, transform: t2 } }];
39081
+ }
39082
+ return [];
39083
+ }
39084
+ };
39085
+ var renderedRules = [
39086
+ renderedContrast,
39087
+ renderedContrastPixel,
39088
+ renderedLinkColourOnly,
39089
+ renderedNonTextContrast,
39090
+ renderedFocusNotVisible,
39091
+ renderedOrientationLock
39092
+ ];
39093
+ var SIGNALS_REQUIRED = {
39094
+ // Computed colours, per element.
39095
+ "rendered-contrast": (s) => !!s.styles && !s.truncated,
39096
+ "rendered-link-colour-only": (s) => !!s.styles && !s.truncated,
39097
+ "rendered-nontext-contrast": (s) => !!s.styles && !s.truncated,
39098
+ // The pixel fallback needs the screenshot AND the boxes to sample it by.
39099
+ "rendered-contrast-pixel": (s) => !!s.screenshot && !!s.boxes && !s.truncated,
39100
+ // These two read the page's own stylesheets, and both decline on an unreadable sheet.
39101
+ "rendered-focus-not-visible": (s) => !!s.css && s.css.unreadable === 0 && !s.css.truncated,
39102
+ "rendered-orientation-lock": (s) => !!s.css && s.css.unreadable === 0
39103
+ };
39104
+ function renderedTestedScs(signals) {
39105
+ const scs = /* @__PURE__ */ new Set();
39106
+ for (const rule of renderedRules) {
39107
+ if (SIGNALS_REQUIRED[rule.id]?.(signals)) for (const sc of rule.criteria) scs.add(sc);
39108
+ }
39109
+ return [...scs].sort();
39110
+ }
39111
+ var RENDERED_SIGNAL_RULES = Object.keys(SIGNALS_REQUIRED);
39112
+
39113
+ // src/name.ts
39114
+ var collapse = (s) => s.replace(/\s+/g, " ").trim();
39115
+ function mayInjectContent(el) {
39116
+ return descendants(el).some((d) => d.tag === "slot" || d.tag !== "#fragment" && !isIntrinsic(d.tag));
39117
+ }
39118
+ var HIDDEN_STYLE = /(^|;)\s*(display\s*:\s*none|visibility\s*:\s*(hidden|collapse))\s*(;|$)/i;
39119
+ function isDisplayHidden(el) {
39120
+ for (const node of [el, ...ancestors(el)]) {
39121
+ if (hasAttr(node, "hidden")) return true;
39122
+ if (HIDDEN_STYLE.test(attr(node, "style") ?? "")) return true;
39123
+ }
39124
+ return false;
39125
+ }
39126
+ function isHiddenFromAT(el) {
39127
+ if (isDisplayHidden(el)) return true;
39128
+ return [el, ...ancestors(el)].some((node) => attr(node, "aria-hidden") === "true");
39129
+ }
39130
+ function isPresentational(el) {
39131
+ return ["presentation", "none"].includes((attr(el, "role") ?? "").trim().toLowerCase());
39132
+ }
39133
+ function isNameExempt(el) {
39134
+ return isHiddenFromAT(el) || isPresentational(el);
39135
+ }
39136
+ function embeddedImageName(n, doc) {
39137
+ const labelled = (attr(n, "aria-labelledby") ?? "").trim();
39138
+ if (labelled) return doc ? ariaLabelledbyText(n, doc) || labelled : labelled;
39139
+ return (boundAttr(n, "aria-label") ?? attr(n, "alt") ?? attr(n, "title") ?? "").trim();
39140
+ }
39141
+ function nameFromContent(el, doc) {
39142
+ let out2 = "";
39143
+ const walk3 = (n) => {
39144
+ if (n.type === "text") {
39145
+ out2 += n.data;
39146
+ return;
39147
+ }
39148
+ if (n.tag === "img") {
39149
+ const a = embeddedImageName(n, doc);
39150
+ if (a) out2 += " " + a;
39151
+ return;
39152
+ }
39153
+ if (n.tag === "svg") {
39154
+ const title2 = descendants(n).find((d) => d.tag === "title");
39155
+ if (title2) out2 += " " + nameFromContent(title2, doc);
39156
+ return;
39157
+ }
39158
+ if (attr(n, "aria-hidden") === "true") return;
39159
+ for (const c2 of n.children) walk3(c2);
39160
+ };
39161
+ for (const c2 of el.children) walk3(c2);
39162
+ return collapse(out2);
39163
+ }
39164
+ function ariaLabelledbyText(el, doc) {
39165
+ const ids = attr(el, "aria-labelledby");
39166
+ if (!ids) return "";
39167
+ const parts2 = [];
39168
+ for (const id of ids.split(/\s+/).filter(Boolean)) {
39169
+ const ref = doc.byId.get(id);
39170
+ if (ref) parts2.push(nameFromContent(ref) || (attr(ref, "aria-label") ?? "").trim());
39171
+ }
39172
+ return collapse(parts2.join(" "));
39173
+ }
39174
+ var BUTTON_INPUT = /* @__PURE__ */ new Set(["button", "submit", "reset"]);
39175
+ var NAMELESS_BY_DEFAULT = /* @__PURE__ */ new Set(["submit", "reset"]);
39176
+ function accessibleName(el, doc) {
39177
+ const labelledby = ariaLabelledbyText(el, doc);
39178
+ if (labelledby) return labelledby;
39179
+ if (hasBoundAttr(el, "aria-labelledby") && !attr(el, "aria-labelledby")) return "\xA0";
39180
+ const ariaLabel = (boundAttr(el, "aria-label") ?? "").trim();
39181
+ if (ariaLabel) return ariaLabel;
39182
+ if (el.tag === "img" || el.tag === "area") {
39183
+ return (boundAttr(el, "alt") ?? "").trim();
39184
+ }
39185
+ if (el.tag === "input") {
39186
+ const type = (attr(el, "type") ?? "text").toLowerCase();
39187
+ if (BUTTON_INPUT.has(type)) {
39188
+ const value = (attr(el, "value") ?? "").trim();
39189
+ if (value) return value;
39190
+ if (NAMELESS_BY_DEFAULT.has(type)) return type === "submit" ? "Submit" : "Reset";
39191
+ return (attr(el, "title") ?? "").trim();
39192
+ }
39193
+ }
39194
+ const content = nameFromContent(el, doc);
39195
+ if (content) return content;
39196
+ return (attr(el, "title") ?? "").trim();
39197
+ }
39198
+ var FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
39199
+ var NON_LABELABLE_INPUT = /* @__PURE__ */ new Set(["hidden", "submit", "reset", "button", "image"]);
39200
+ function isFormField(el) {
39201
+ if (!FIELD_TAGS.has(el.tag)) return false;
39202
+ if (hasAttr(el, "hidden")) return false;
39203
+ if (el.tag === "input") {
39204
+ const type = (attr(el, "type") ?? "text").toLowerCase();
39205
+ return !NON_LABELABLE_INPUT.has(type);
39206
+ }
39207
+ return true;
39208
+ }
39209
+ function controlLabel(el, doc) {
39210
+ if (attr(el, "aria-labelledby") && ariaLabelledbyText(el, doc) || hasBoundAttr(el, "aria-labelledby") && !attr(el, "aria-labelledby"))
39211
+ return { hasLabel: true, via: "aria-labelledby" };
39212
+ if ((boundAttr(el, "aria-label") ?? "").trim()) return { hasLabel: true, via: "aria-label" };
39213
+ const id = attr(el, "id");
39214
+ if (id) {
39215
+ const lbl = doc.elements.find((e) => e.tag === "label" && attr(e, "for") === id);
39216
+ if (lbl) return { hasLabel: true, via: "for" };
39217
+ }
39218
+ if (ancestors(el).some((a) => a.tag === "label")) return { hasLabel: true, via: "wrapping" };
39219
+ if ((attr(el, "title") ?? "").trim()) return { hasLabel: true, via: "title" };
39220
+ return { hasLabel: false, via: null };
39221
+ }
39222
+
39223
+ // src/rules/images.ts
39224
+ var isHidden = isNameExempt;
39225
+ var named = (el) => !!(boundAttr(el, "aria-label") ?? "").trim() || hasBoundAttr(el, "aria-labelledby");
39226
+ var imgAltMissing = {
39227
+ id: "img-alt-missing",
39228
+ criteria: ["1.1.1"],
39229
+ severity: "bloquant",
39230
+ run(doc) {
39231
+ const out2 = [];
39232
+ for (const el of doc.elements) {
39233
+ const isImg = el.tag === "img" || el.tag === "area" || (attr(el, "role") ?? "") === "img";
39234
+ if (!isImg) continue;
39235
+ if (isHidden(el) && el.tag !== "area") continue;
39236
+ const altLiteral = attr(el, "alt");
39237
+ const whitespaceAlt = altLiteral !== void 0 && altLiteral !== "" && altLiteral.trim() === "";
39238
+ if (hasBoundAttr(el, "alt") && !whitespaceAlt || named(el)) continue;
39239
+ if (hasDynamicSpread(el)) continue;
39240
+ if (el.tag !== "img" && el.tag !== "area" && accessibleName(el, doc).trim() !== "") continue;
39241
+ if ((attr(el, "title") ?? "").trim() !== "") {
39242
+ out2.push({
39243
+ criteriaId: "1.1.1",
39244
+ el,
39245
+ msgId: "img-alt-missing.title-only",
39246
+ params: { tag: el.tag },
39247
+ advisory: true,
39248
+ severity: "mineur"
39249
+ });
39250
+ continue;
39251
+ }
39252
+ out2.push({
39253
+ criteriaId: "1.1.1",
39254
+ el,
39255
+ msgId: "img-alt-missing",
39256
+ params: { tag: el.tag }
39257
+ });
39258
+ }
39259
+ return out2;
39260
+ }
39261
+ };
39262
+ var decorativeAltMisuse = {
39263
+ id: "decorative-alt-misuse",
39264
+ criteria: ["1.1.1"],
39265
+ severity: "majeur",
39266
+ run(doc) {
39267
+ const out2 = [];
39268
+ for (const el of doc.elements) {
39269
+ if (el.tag !== "img") continue;
39270
+ const alt = attr(el, "alt");
39271
+ const role = (attr(el, "role") ?? "").trim();
39272
+ const ariaLabel = (attr(el, "aria-label") ?? "").trim();
39273
+ const title2 = (attr(el, "title") ?? "").trim();
39274
+ if (alt === "" && (ariaLabel || title2)) {
39275
+ out2.push({
39276
+ criteriaId: "1.1.1",
39277
+ el,
39278
+ msgId: "decorative-alt-misuse.empty-but-named"
39279
+ });
39280
+ } else if (["presentation", "none"].includes(role) && alt?.trim()) {
39281
+ out2.push({
39282
+ criteriaId: "1.1.1",
38664
39283
  el,
38665
39284
  msgId: "decorative-alt-misuse.role-but-alt",
38666
39285
  params: { role }
@@ -40890,837 +41509,237 @@ var navLandmarkMissing = {
40890
41509
  }
40891
41510
  return [];
40892
41511
  }
40893
- };
40894
- var navLandmarkUnnamed = {
40895
- id: "nav-landmark-unnamed",
40896
- criteria: ["1.3.1"],
40897
- severity: "mineur",
40898
- scope: "page",
40899
- run(doc) {
40900
- const navs = navLandmarks(doc);
40901
- if (navs.length < 2) return [];
40902
- return navs.filter((n) => !navIsNamed(n)).map((el) => ({ criteriaId: "1.3.1", el, msgId: "nav-landmark-unnamed" }));
40903
- }
40904
- };
40905
- var navigationRules = [
40906
- skipLinkTargetMissing,
40907
- positiveTabindex,
40908
- missingMainLandmark,
40909
- multipleMainLandmark,
40910
- navLandmarkMissing,
40911
- navLandmarkUnnamed
40912
- ];
40913
-
40914
- // src/rules/multimedia.ts
40915
- function mutedStatically(el) {
40916
- if (!hasAttr(el, "muted")) return false;
40917
- const v = (attr(el, "muted") ?? "").trim().toLowerCase();
40918
- return v === "" || v === "muted" || v === "true" || v === "{true}";
40919
- }
40920
- var autoplayMedia = {
40921
- id: "autoplay-media",
40922
- criteria: ["1.4.2", "2.2.2"],
40923
- severity: "majeur",
40924
- run(doc) {
40925
- const out2 = [];
40926
- for (const el of doc.elements) {
40927
- if (el.tag !== "audio" && el.tag !== "video") continue;
40928
- if (!hasAttr(el, "autoplay")) continue;
40929
- if (hasAttr(el, "controls")) continue;
40930
- if (el.tag === "video" && mutedStatically(el)) {
40931
- out2.push({
40932
- criteriaId: "2.2.2",
40933
- el,
40934
- msgId: "autoplay-media.muted-video",
40935
- advisory: true
40936
- });
40937
- continue;
40938
- }
40939
- out2.push({
40940
- criteriaId: el.tag === "audio" ? "1.4.2" : "2.2.2",
40941
- el,
40942
- msgId: "autoplay-media.audible",
40943
- params: { tag: el.tag }
40944
- });
40945
- }
40946
- return out2;
40947
- }
40948
- };
40949
- var mediaNoTrack = {
40950
- id: "media-no-track",
40951
- criteria: ["1.2.2"],
40952
- severity: "majeur",
40953
- advisory: true,
40954
- run(doc) {
40955
- const out2 = [];
40956
- for (const el of doc.elements) {
40957
- if (el.tag !== "video") continue;
40958
- if (mutedStatically(el)) continue;
40959
- if (descendants(el).some((d) => d.tag === "track")) continue;
40960
- out2.push({
40961
- criteriaId: "1.2.2",
40962
- el,
40963
- msgId: "media-no-track"
40964
- });
40965
- }
40966
- return out2;
40967
- }
40968
- };
40969
- var multimediaRules = [autoplayMedia, mediaNoTrack];
40970
-
40971
- // src/rules/presentation.ts
40972
- var metaViewportZoomBlock = {
40973
- id: "meta-viewport-zoom-block",
40974
- criteria: ["1.4.4"],
40975
- severity: "majeur",
40976
- run(doc) {
40977
- const out2 = [];
40978
- for (const el of doc.elements) {
40979
- if (el.tag !== "meta" || (attr(el, "name") ?? "").toLowerCase() !== "viewport") continue;
40980
- const content = (attr(el, "content") ?? "").toLowerCase();
40981
- const pairs = /* @__PURE__ */ new Map();
40982
- for (const part of content.split(/[,;]/)) {
40983
- const [k, v] = part.split("=").map((s) => s.trim());
40984
- if (k) pairs.set(k, v ?? "");
40985
- }
40986
- const userScalable = pairs.get("user-scalable");
40987
- const maxScale = pairs.get("maximum-scale");
40988
- const maxScaleNum = maxScale !== void 0 && maxScale.trim() !== "" ? Number(maxScale) : Number.NaN;
40989
- const blocked = userScalable === "no" || userScalable === "0" || Number.isFinite(maxScaleNum) && maxScaleNum > 0 && maxScaleNum < 2;
40990
- if (!blocked) continue;
40991
- out2.push({
40992
- criteriaId: "1.4.4",
40993
- el,
40994
- msgId: "meta-viewport-zoom-block",
40995
- params: { blockedBy: userScalable === "no" || userScalable === "0" ? "user-scalable" : "maximum-scale", maxScale: maxScale ?? "" }
40996
- });
40997
- }
40998
- return out2;
40999
- }
41000
- };
41001
- var CSS_CONTENT = /content\s*:\s*(["'])((?:\\.|(?!\1).)*)\1/gi;
41002
- var INFORMATIVE_WORD = new RegExp("\\p{L}{3,}", "u");
41003
- var cssGeneratedContentInformative = {
41004
- id: "css-generated-content-informative",
41005
- criteria: ["1.3.1"],
41006
- severity: "mineur",
41007
- advisory: true,
41008
- run(doc) {
41009
- const out2 = [];
41010
- for (const el of doc.elements) {
41011
- if (el.tag !== "style") continue;
41012
- const css = textContent(el);
41013
- const seen = /* @__PURE__ */ new Set();
41014
- CSS_CONTENT.lastIndex = 0;
41015
- let m;
41016
- while (m = CSS_CONTENT.exec(css)) {
41017
- const value = m[2] ?? "";
41018
- if (value.startsWith("\\")) continue;
41019
- if (!INFORMATIVE_WORD.test(value)) continue;
41020
- if (seen.has(value)) continue;
41021
- seen.add(value);
41022
- out2.push({ criteriaId: "1.3.1", el, msgId: "css-generated-content-informative", params: { text: value.slice(0, 40) }, advisory: true });
41023
- }
41024
- }
41025
- return out2;
41026
- }
41027
- };
41028
- var presentationRules = [metaViewportZoomBlock, cssGeneratedContentInformative];
41029
-
41030
- // src/color.ts
41031
- var NAMED = {
41032
- transparent: { r: 0, g: 0, b: 0, a: 0 },
41033
- white: { r: 255, g: 255, b: 255, a: 1 },
41034
- black: { r: 0, g: 0, b: 0, a: 1 },
41035
- red: { r: 255, g: 0, b: 0, a: 1 },
41036
- lime: { r: 0, g: 255, b: 0, a: 1 },
41037
- green: { r: 0, g: 128, b: 0, a: 1 },
41038
- blue: { r: 0, g: 0, b: 255, a: 1 },
41039
- yellow: { r: 255, g: 255, b: 0, a: 1 },
41040
- cyan: { r: 0, g: 255, b: 255, a: 1 },
41041
- aqua: { r: 0, g: 255, b: 255, a: 1 },
41042
- magenta: { r: 255, g: 0, b: 255, a: 1 },
41043
- fuchsia: { r: 255, g: 0, b: 255, a: 1 },
41044
- silver: { r: 192, g: 192, b: 192, a: 1 },
41045
- gray: { r: 128, g: 128, b: 128, a: 1 },
41046
- grey: { r: 128, g: 128, b: 128, a: 1 },
41047
- maroon: { r: 128, g: 0, b: 0, a: 1 },
41048
- olive: { r: 128, g: 128, b: 0, a: 1 },
41049
- purple: { r: 128, g: 0, b: 128, a: 1 },
41050
- teal: { r: 0, g: 128, b: 128, a: 1 },
41051
- navy: { r: 0, g: 0, b: 128, a: 1 },
41052
- orange: { r: 255, g: 165, b: 0, a: 1 }
41053
- };
41054
- function hex(part) {
41055
- return parseInt(part.length === 1 ? part + part : part, 16);
41056
- }
41057
- function parseColor(input) {
41058
- const s = input.trim().toLowerCase();
41059
- if (!s) return null;
41060
- if (s in NAMED) return { ...NAMED[s] };
41061
- if (s.startsWith("#")) {
41062
- const h = s.slice(1);
41063
- if (/^[0-9a-f]{3}$/.test(h)) return { r: hex(h[0]), g: hex(h[1]), b: hex(h[2]), a: 1 };
41064
- if (/^[0-9a-f]{4}$/.test(h)) return { r: hex(h[0]), g: hex(h[1]), b: hex(h[2]), a: hex(h[3]) / 255 };
41065
- if (/^[0-9a-f]{6}$/.test(h)) return { r: hex(h.slice(0, 2)), g: hex(h.slice(2, 4)), b: hex(h.slice(4, 6)), a: 1 };
41066
- if (/^[0-9a-f]{8}$/.test(h)) return { r: hex(h.slice(0, 2)), g: hex(h.slice(2, 4)), b: hex(h.slice(4, 6)), a: hex(h.slice(6, 8)) / 255 };
41067
- return null;
41068
- }
41069
- const m = /^rgba?\(([^)]+)\)$/.exec(s);
41070
- if (m) {
41071
- const parts2 = m[1].split(/[,/\s]+/).filter(Boolean);
41072
- if (parts2.length < 3) return null;
41073
- const chan = (p) => p.endsWith("%") ? Math.round(parseFloat(p) / 100 * 255) : parseFloat(p);
41074
- const r = chan(parts2[0]);
41075
- const g = chan(parts2[1]);
41076
- const b = chan(parts2[2]);
41077
- const a = parts2[3] !== void 0 ? parts2[3].endsWith("%") ? parseFloat(parts2[3]) / 100 : parseFloat(parts2[3]) : 1;
41078
- if ([r, g, b, a].some((n) => Number.isNaN(n))) return null;
41079
- return { r, g, b, a };
41080
- }
41081
- return null;
41082
- }
41083
- function channelLuminance(c2) {
41084
- const cs = c2 / 255;
41085
- return cs <= 0.03928 ? cs / 12.92 : ((cs + 0.055) / 1.055) ** 2.4;
41086
- }
41087
- function relativeLuminance({ r, g, b }) {
41088
- return 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b);
41089
- }
41090
- function contrastRatio(a, b) {
41091
- const la = relativeLuminance(a);
41092
- const lb = relativeLuminance(b);
41093
- const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
41094
- return (hi + 0.05) / (lo + 0.05);
41095
- }
41096
- function parseInlineStyle(style) {
41097
- const out2 = /* @__PURE__ */ new Map();
41098
- for (const decl of style.split(";")) {
41099
- const i2 = decl.indexOf(":");
41100
- if (i2 === -1) continue;
41101
- const prop = decl.slice(0, i2).trim().toLowerCase();
41102
- const val = decl.slice(i2 + 1).trim();
41103
- if (prop && val) out2.set(prop, val);
41104
- }
41105
- return out2;
41106
- }
41107
-
41108
- // src/rules/colors.ts
41109
- var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "head", "title", "meta", "noscript", "link"]);
41110
- function styleOf(el) {
41111
- const s = attr(el, "style");
41112
- return s ? parseInlineStyle(s) : /* @__PURE__ */ new Map();
41113
- }
41114
- function colorFromValue(v) {
41115
- const whole = parseColor(v);
41116
- if (whole) return whole;
41117
- for (const tok of v.split(/\s+/)) {
41118
- const c2 = parseColor(tok);
41119
- if (c2) return c2;
41120
- }
41121
- return null;
41122
- }
41123
- function fgOf(el) {
41124
- for (let p = el; p; p = p.parent) {
41125
- const v = styleOf(p).get("color");
41126
- if (v) {
41127
- const c2 = colorFromValue(v);
41128
- if (c2) return c2;
41129
- }
41130
- }
41131
- return null;
41132
- }
41133
- function bgOf(el) {
41134
- for (let p = el; p; p = p.parent) {
41135
- const st = styleOf(p);
41136
- const v = st.get("background-color") ?? st.get("background");
41137
- if (v) {
41138
- const c2 = colorFromValue(v);
41139
- if (c2) return c2;
41140
- }
41141
- }
41142
- return null;
41143
- }
41144
- function fontPx(el) {
41145
- for (let p = el; p; p = p.parent) {
41146
- const v = styleOf(p).get("font-size");
41147
- if (!v) continue;
41148
- const m = /^([\d.]+)(px|pt|rem|em|%)?$/.exec(v.trim());
41149
- if (!m) return "unknown";
41150
- const n = parseFloat(m[1]);
41151
- if (Number.isNaN(n)) return "unknown";
41152
- const unit = m[2] ?? "px";
41153
- if (unit === "pt") return n * 4 / 3;
41154
- if (unit === "rem" || unit === "em") return n * 16;
41155
- if (unit === "%") return n / 100 * 16;
41156
- return n;
41157
- }
41158
- return null;
41159
- }
41160
- function isBold(el) {
41161
- for (let p = el; p; p = p.parent) {
41162
- const v = styleOf(p).get("font-weight");
41163
- if (v) return v === "bold" || Number(v) >= 700;
41164
- }
41165
- return false;
41166
- }
41167
- function isLarge(el) {
41168
- const px = fontPx(el);
41169
- if (px === null) return false;
41170
- if (px === "unknown") return true;
41171
- return px >= 24 || px >= 18.66 && isBold(el);
41172
- }
41173
- function hasDirectText(el) {
41174
- return el.children.some((c2) => c2.type === "text" && c2.data.trim() !== "");
41175
- }
41176
- var contrastLiteral = {
41177
- id: "contrast-literal",
41178
- criteria: ["1.4.3"],
41179
- severity: "majeur",
41180
- run(doc) {
41181
- const out2 = [];
41182
- for (const el of doc.elements) {
41183
- if (SKIP_TAGS.has(el.tag) || !hasDirectText(el)) continue;
41184
- const fg = fgOf(el);
41185
- const bg = bgOf(el);
41186
- if (!fg || !bg || fg.a < 1 || bg.a < 1) continue;
41187
- const ratio = contrastRatio(fg, bg);
41188
- const large = isLarge(el);
41189
- const min = large ? 3 : 4.5;
41190
- if (ratio >= min) continue;
41191
- out2.push({
41192
- criteriaId: "1.4.3",
41193
- el,
41194
- msgId: "contrast-literal",
41195
- params: { ratio: ratio.toFixed(2), min, textSize: large ? "large" : "normal" }
41196
- });
41197
- }
41198
- return out2;
41199
- }
41200
- };
41201
- var colorsRules = [contrastLiteral];
41202
-
41203
- // src/rules/rendered.ts
41204
- import { readFileSync as readFileSync15 } from "fs";
41205
-
41206
- // src/pixel.ts
41207
- import { deflateSync, inflateSync } from "zlib";
41208
- var SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
41209
- function paeth(a, b, c2) {
41210
- const p = a + b - c2;
41211
- const pa = Math.abs(p - a);
41212
- const pb = Math.abs(p - b);
41213
- const pc = Math.abs(p - c2);
41214
- if (pa <= pb && pa <= pc) return a;
41215
- return pb <= pc ? b : c2;
41216
- }
41217
- function decodePng(buf) {
41218
- if (buf.length < 8 + 25) return null;
41219
- for (let i2 = 0; i2 < SIGNATURE.length; i2++) if (buf[i2] !== SIGNATURE[i2]) return null;
41220
- let width = 0;
41221
- let height = 0;
41222
- let bitDepth = 0;
41223
- let colorType = 0;
41224
- let interlace = 0;
41225
- const idat = [];
41226
- let off = 8;
41227
- while (off + 8 <= buf.length) {
41228
- const len = buf.readUInt32BE(off);
41229
- const type = buf.toString("ascii", off + 4, off + 8);
41230
- const dataStart = off + 8;
41231
- const dataEnd = dataStart + len;
41232
- if (dataEnd + 4 > buf.length) return null;
41233
- if (type === "IHDR") {
41234
- if (len < 13) return null;
41235
- width = buf.readUInt32BE(dataStart);
41236
- height = buf.readUInt32BE(dataStart + 4);
41237
- bitDepth = buf[dataStart + 8] ?? 0;
41238
- colorType = buf[dataStart + 9] ?? 0;
41239
- interlace = buf[dataStart + 12] ?? 0;
41240
- } else if (type === "IDAT") {
41241
- idat.push(buf.subarray(dataStart, dataEnd));
41242
- } else if (type === "IEND") {
41243
- break;
41244
- }
41245
- off = dataEnd + 4;
41246
- }
41247
- if (!width || !height || !idat.length) return null;
41248
- if (bitDepth !== 8) return null;
41249
- if (colorType !== 2 && colorType !== 6) return null;
41250
- if (interlace !== 0) return null;
41251
- const channels = colorType === 6 ? 4 : 3;
41252
- let raw;
41253
- try {
41254
- raw = inflateSync(Buffer.concat(idat));
41255
- } catch {
41256
- return null;
41257
- }
41258
- const stride = width * channels;
41259
- if (raw.length < height * (stride + 1)) return null;
41260
- const out2 = Buffer.alloc(height * stride);
41261
- for (let y = 0; y < height; y++) {
41262
- const filter = raw[y * (stride + 1)] ?? 0;
41263
- const lineIn = y * (stride + 1) + 1;
41264
- const lineOut = y * stride;
41265
- const prevOut = lineOut - stride;
41266
- for (let i2 = 0; i2 < stride; i2++) {
41267
- const x = raw[lineIn + i2] ?? 0;
41268
- const a = i2 >= channels ? out2[lineOut + i2 - channels] ?? 0 : 0;
41269
- const b = y > 0 ? out2[prevOut + i2] ?? 0 : 0;
41270
- const c2 = y > 0 && i2 >= channels ? out2[prevOut + i2 - channels] ?? 0 : 0;
41271
- let v;
41272
- switch (filter) {
41273
- case 0:
41274
- v = x;
41275
- break;
41276
- case 1:
41277
- v = x + a;
41278
- break;
41279
- case 2:
41280
- v = x + b;
41281
- break;
41282
- case 3:
41283
- v = x + (a + b >> 1);
41284
- break;
41285
- case 4:
41286
- v = x + paeth(a, b, c2);
41287
- break;
41288
- default:
41289
- return null;
41290
- }
41291
- out2[lineOut + i2] = v & 255;
41292
- }
41293
- }
41294
- return {
41295
- width,
41296
- height,
41297
- at(x, y) {
41298
- if (x < 0 || y < 0 || x >= width || y >= height) return void 0;
41299
- const p = y * stride + x * channels;
41300
- return {
41301
- r: out2[p] ?? 0,
41302
- g: out2[p + 1] ?? 0,
41303
- b: out2[p + 2] ?? 0,
41304
- a: channels === 4 ? (out2[p + 3] ?? 255) / 255 : 1
41305
- };
41306
- }
41307
- };
41308
- }
41309
- function clamp(img, r) {
41310
- const x = Math.max(0, Math.round(r.x));
41311
- const y = Math.max(0, Math.round(r.y));
41312
- const x2 = Math.min(img.width, Math.round(r.x + r.w));
41313
- const y2 = Math.min(img.height, Math.round(r.y + r.h));
41314
- if (x2 <= x || y2 <= y) return null;
41315
- return { x, y, w: x2 - x, h: y2 - y };
41316
- }
41317
- var MAX_SAMPLES = 4096;
41318
- var DOMINANCE = 0.6;
41319
- var BUCKET = 16;
41320
- function dominantBackground(img, rect) {
41321
- const r = clamp(img, rect);
41322
- if (!r) return null;
41323
- const step = Math.max(1, Math.floor(Math.sqrt(r.w * r.h / MAX_SAMPLES)));
41324
- const counts = /* @__PURE__ */ new Map();
41325
- let total = 0;
41326
- for (let y = r.y; y < r.y + r.h; y += step) {
41327
- for (let x = r.x; x < r.x + r.w; x += step) {
41328
- const p = img.at(x, y);
41329
- if (!p) continue;
41330
- const key = Math.round(p.r / BUCKET) << 16 | Math.round(p.g / BUCKET) << 8 | Math.round(p.b / BUCKET);
41331
- const cur = counts.get(key) ?? { n: 0, r: 0, g: 0, b: 0 };
41332
- cur.n++;
41333
- cur.r += p.r;
41334
- cur.g += p.g;
41335
- cur.b += p.b;
41336
- counts.set(key, cur);
41337
- total++;
41338
- }
41339
- }
41340
- if (!total) return null;
41341
- let best = null;
41342
- for (const c2 of counts.values()) if (!best || c2.n > best.n) best = c2;
41343
- if (!best || best.n / total < DOMINANCE) return null;
41344
- return { r: Math.round(best.r / best.n), g: Math.round(best.g / best.n), b: Math.round(best.b / best.n), a: 1 };
41345
- }
41346
- var CRC_TABLE = /* @__PURE__ */ (() => {
41347
- const t2 = new Uint32Array(256);
41348
- for (let n = 0; n < 256; n++) {
41349
- let c2 = n;
41350
- for (let k = 0; k < 8; k++) c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
41351
- t2[n] = c2 >>> 0;
41352
- }
41353
- return t2;
41354
- })();
41355
- function crc32(buf) {
41356
- let c2 = 4294967295;
41357
- for (let i2 = 0; i2 < buf.length; i2++) c2 = CRC_TABLE[(c2 ^ buf[i2]) & 255] ^ c2 >>> 8;
41358
- return (c2 ^ 4294967295) >>> 0;
41359
- }
41360
- function chunk(type, data2) {
41361
- const len = Buffer.alloc(4);
41362
- len.writeUInt32BE(data2.length);
41363
- const body3 = Buffer.concat([Buffer.from(type, "latin1"), data2]);
41364
- const crc = Buffer.alloc(4);
41365
- crc.writeUInt32BE(crc32(body3));
41366
- return Buffer.concat([len, body3, crc]);
41367
- }
41368
- function encodePng(r) {
41369
- const expected = r.width * r.height * 4;
41370
- if (r.width <= 0 || r.height <= 0) throw new Error(`encodePng: refusing a ${r.width}\xD7${r.height} raster`);
41371
- if (r.data.length !== expected) {
41372
- throw new Error(`encodePng: ${r.width}\xD7${r.height} needs ${expected} bytes of RGBA, got ${r.data.length}`);
41373
- }
41374
- const stride = r.width * 4;
41375
- const raw = Buffer.alloc(r.height * (stride + 1));
41376
- for (let y = 0; y < r.height; y++) {
41377
- raw[y * (stride + 1)] = 0;
41378
- r.data.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
41379
- }
41380
- const ihdr = Buffer.alloc(13);
41381
- ihdr.writeUInt32BE(r.width, 0);
41382
- ihdr.writeUInt32BE(r.height, 4);
41383
- ihdr[8] = 8;
41384
- ihdr[9] = 6;
41385
- return Buffer.concat([Buffer.from(SIGNATURE), chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw, { level: 9 })), chunk("IEND", Buffer.alloc(0))]);
41386
- }
41387
- function cropImage(img, rect) {
41388
- const r = clamp(img, rect);
41389
- if (!r) return null;
41390
- const data2 = Buffer.alloc(r.w * r.h * 4);
41391
- let i2 = 0;
41392
- for (let y = r.y; y < r.y + r.h; y++) {
41393
- for (let x = r.x; x < r.x + r.w; x++) {
41394
- const p = img.at(x, y);
41395
- data2[i2] = p?.r ?? 0;
41396
- data2[i2 + 1] = p?.g ?? 0;
41397
- data2[i2 + 2] = p?.b ?? 0;
41398
- data2[i2 + 3] = Math.round((p?.a ?? 1) * 255);
41399
- i2 += 4;
41400
- }
41401
- }
41402
- return { raster: { width: r.w, height: r.h, data: data2 }, rect: r };
41403
- }
41404
- function blend(raster, x, y, c2) {
41405
- if (x < 0 || y < 0 || x >= raster.width || y >= raster.height) return;
41406
- const p = (y * raster.width + x) * 4;
41407
- const a = Math.min(1, Math.max(0, c2.a));
41408
- if (a >= 1) {
41409
- raster.data[p] = c2.r;
41410
- raster.data[p + 1] = c2.g;
41411
- raster.data[p + 2] = c2.b;
41412
- raster.data[p + 3] = 255;
41413
- return;
41414
- }
41415
- const inv = 1 - a;
41416
- raster.data[p] = Math.round(c2.r * a + raster.data[p] * inv);
41417
- raster.data[p + 1] = Math.round(c2.g * a + raster.data[p + 1] * inv);
41418
- raster.data[p + 2] = Math.round(c2.b * a + raster.data[p + 2] * inv);
41419
- raster.data[p + 3] = Math.max(raster.data[p + 3], Math.round(a * 255));
41420
- }
41421
- function fillRect(raster, rect, color) {
41422
- const x0 = Math.max(0, Math.round(rect.x));
41423
- const y0 = Math.max(0, Math.round(rect.y));
41424
- const x1 = Math.min(raster.width, Math.round(rect.x + rect.w));
41425
- const y1 = Math.min(raster.height, Math.round(rect.y + rect.h));
41426
- for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) blend(raster, x, y, color);
41427
- }
41428
- function strokeRect(raster, rect, color, width, dash) {
41429
- const t2 = Math.max(1, Math.round(width));
41430
- const x0 = Math.round(rect.x);
41431
- const y0 = Math.round(rect.y);
41432
- const x1 = Math.round(rect.x + rect.w);
41433
- const y1 = Math.round(rect.y + rect.h);
41434
- if (x1 <= x0 || y1 <= y0) return;
41435
- const on = (coord) => {
41436
- if (!dash || dash <= 1) return true;
41437
- const half = dash / 2;
41438
- return Math.floor(coord / half) % 2 === 0;
41439
- };
41440
- for (let y = y0; y < y1; y++) {
41441
- for (let x = x0; x < x1; x++) {
41442
- const top = y < y0 + t2;
41443
- const bottom = y >= y1 - t2;
41444
- const left = x < x0 + t2;
41445
- const right = x >= x1 - t2;
41446
- if (!top && !bottom && !left && !right) continue;
41447
- if (!on(top || bottom ? x : y)) continue;
41448
- blend(raster, x, y, color);
41449
- }
41450
- }
41451
- }
41452
-
41453
- // src/rules/rendered.ts
41454
- var SKIP_TAGS2 = /* @__PURE__ */ new Set(["script", "style", "head", "title", "meta", "noscript", "link", "html", "br", "wbr"]);
41455
- function styleAt(doc, i2) {
41456
- return doc.signals?.styles?.get(i2);
41457
- }
41458
- function hasDirectText2(el) {
41459
- return el.children.some((c2) => c2.type === "text" && c2.data.trim() !== "");
41460
- }
41461
- function invisible(css) {
41462
- if (css.display === "none" || css.visibility === "hidden") return true;
41463
- const op = Number.parseFloat(css.opacity ?? "1");
41464
- return Number.isFinite(op) && op === 0;
41465
- }
41466
- function backdropOf(doc, index, el) {
41467
- for (let p = el; p; p = p.parent) {
41468
- const i2 = index.get(p);
41469
- if (i2 === void 0) continue;
41470
- const css = styleAt(doc, i2)?.css;
41471
- if (!css) continue;
41472
- const img = css.backgroundImage;
41473
- if (img && img !== "none") return void 0;
41474
- const c2 = parseColor(css.backgroundColor ?? "");
41475
- if (c2 && c2.a >= 1) return { color: c2, fromImage: false };
41476
- }
41477
- return void 0;
41478
- }
41479
- function isLargeText(css) {
41480
- const px = Number.parseFloat(css.fontSize ?? "");
41481
- if (!Number.isFinite(px)) return false;
41482
- const weight = Number.parseInt(css.fontWeight ?? "400", 10);
41483
- const bold = Number.isFinite(weight) ? weight >= 700 : css.fontWeight === "bold";
41484
- return px >= 24 || px >= 18.66 && bold;
41485
- }
41486
- function elementIndex(doc) {
41487
- const m = /* @__PURE__ */ new Map();
41488
- doc.elements.forEach((el, i2) => m.set(el, i2));
41489
- return m;
41512
+ };
41513
+ var navLandmarkUnnamed = {
41514
+ id: "nav-landmark-unnamed",
41515
+ criteria: ["1.3.1"],
41516
+ severity: "mineur",
41517
+ scope: "page",
41518
+ run(doc) {
41519
+ const navs = navLandmarks(doc);
41520
+ if (navs.length < 2) return [];
41521
+ return navs.filter((n) => !navIsNamed(n)).map((el) => ({ criteriaId: "1.3.1", el, msgId: "nav-landmark-unnamed" }));
41522
+ }
41523
+ };
41524
+ var navigationRules = [
41525
+ skipLinkTargetMissing,
41526
+ positiveTabindex,
41527
+ missingMainLandmark,
41528
+ multipleMainLandmark,
41529
+ navLandmarkMissing,
41530
+ navLandmarkUnnamed
41531
+ ];
41532
+
41533
+ // src/rules/multimedia.ts
41534
+ function mutedStatically(el) {
41535
+ if (!hasAttr(el, "muted")) return false;
41536
+ const v = (attr(el, "muted") ?? "").trim().toLowerCase();
41537
+ return v === "" || v === "muted" || v === "true" || v === "{true}";
41490
41538
  }
41491
- var renderedContrast = {
41492
- id: "rendered-contrast",
41493
- criteria: ["1.4.3"],
41539
+ var autoplayMedia = {
41540
+ id: "autoplay-media",
41541
+ criteria: ["1.4.2", "2.2.2"],
41494
41542
  severity: "majeur",
41495
41543
  run(doc) {
41496
- if (!doc.signals?.styles) return [];
41497
- const index = elementIndex(doc);
41498
41544
  const out2 = [];
41499
- for (const [el, i2] of index) {
41500
- if (SKIP_TAGS2.has(el.tag) || !hasDirectText2(el)) continue;
41501
- const css = styleAt(doc, i2)?.css;
41502
- if (!css || invisible(css)) continue;
41503
- const fg = parseColor(css.color ?? "");
41504
- if (!fg || fg.a < 1) continue;
41505
- const bd = backdropOf(doc, index, el);
41506
- if (!bd) continue;
41507
- const ratio = contrastRatio(fg, bd.color);
41508
- const large = isLargeText(css);
41509
- const min = large ? 3 : 4.5;
41510
- if (ratio >= min) continue;
41545
+ for (const el of doc.elements) {
41546
+ if (el.tag !== "audio" && el.tag !== "video") continue;
41547
+ if (!hasAttr(el, "autoplay")) continue;
41548
+ if (hasAttr(el, "controls")) continue;
41549
+ if (el.tag === "video" && mutedStatically(el)) {
41550
+ out2.push({
41551
+ criteriaId: "2.2.2",
41552
+ el,
41553
+ msgId: "autoplay-media.muted-video",
41554
+ advisory: true
41555
+ });
41556
+ continue;
41557
+ }
41511
41558
  out2.push({
41512
- criteriaId: "1.4.3",
41559
+ criteriaId: el.tag === "audio" ? "1.4.2" : "2.2.2",
41513
41560
  el,
41514
- msgId: "rendered-contrast",
41515
- params: { ratio: ratio.toFixed(2), min, textSize: large ? "large" : "normal" }
41561
+ msgId: "autoplay-media.audible",
41562
+ params: { tag: el.tag }
41516
41563
  });
41517
41564
  }
41518
41565
  return out2;
41519
41566
  }
41520
41567
  };
41521
- var renderedContrastPixel = {
41522
- id: "rendered-contrast-pixel",
41523
- criteria: ["1.4.3"],
41568
+ var mediaNoTrack = {
41569
+ id: "media-no-track",
41570
+ criteria: ["1.2.2"],
41524
41571
  severity: "majeur",
41572
+ advisory: true,
41525
41573
  run(doc) {
41526
- const shot = doc.signals?.screenshot;
41527
- const boxes = doc.signals?.boxes;
41528
- if (!shot || !boxes || !doc.signals?.styles) return [];
41529
- let img = null;
41530
- try {
41531
- img = decodePng(readFileSync15(shot));
41532
- } catch {
41533
- return [];
41534
- }
41535
- if (!img) return [];
41536
- const index = elementIndex(doc);
41537
41574
  const out2 = [];
41538
- for (const [el, i2] of index) {
41539
- if (SKIP_TAGS2.has(el.tag) || !hasDirectText2(el)) continue;
41540
- const css = styleAt(doc, i2)?.css;
41541
- if (!css || invisible(css)) continue;
41542
- if (backdropOf(doc, index, el)) continue;
41543
- const fg = parseColor(css.color ?? "");
41544
- if (!fg || fg.a < 1) continue;
41545
- const box = boxes.get(i2);
41546
- if (!box || box.w < 4 || box.h < 4) continue;
41547
- const bg = dominantBackground(img, box);
41548
- if (!bg) continue;
41549
- const ratio = contrastRatio(fg, bg);
41550
- const large = isLargeText(css);
41551
- const min = large ? 3 : 4.5;
41552
- if (ratio >= min) continue;
41575
+ for (const el of doc.elements) {
41576
+ if (el.tag !== "video") continue;
41577
+ if (mutedStatically(el)) continue;
41578
+ if (descendants(el).some((d) => d.tag === "track")) continue;
41553
41579
  out2.push({
41554
- criteriaId: "1.4.3",
41580
+ criteriaId: "1.2.2",
41555
41581
  el,
41556
- msgId: "rendered-contrast-pixel",
41557
- params: { ratio: ratio.toFixed(2), min, textSize: large ? "large" : "normal" }
41582
+ msgId: "media-no-track"
41558
41583
  });
41559
41584
  }
41560
41585
  return out2;
41561
41586
  }
41562
41587
  };
41563
- function hasNonColourAffordance(css, parentCss) {
41564
- const deco = css.textDecorationLine ?? "";
41565
- if (deco && deco !== "none") return true;
41566
- if ((css.borderBottomStyle ?? "none") !== "none" && Number.parseFloat(css.borderBottomWidth ?? "0") > 0) return true;
41567
- const bg = parseColor(css.backgroundColor ?? "");
41568
- if (bg && bg.a > 0) return true;
41569
- const w = Number.parseInt(css.fontWeight ?? "400", 10);
41570
- const pw = Number.parseInt(parentCss?.fontWeight ?? "400", 10);
41571
- if (Number.isFinite(w) && Number.isFinite(pw) && w >= pw + 200) return true;
41572
- return false;
41573
- }
41574
- var TEXT_BLOCK = /* @__PURE__ */ new Set(["p", "li", "dd", "dt", "td", "th", "blockquote", "figcaption", "caption"]);
41575
- var renderedLinkColourOnly = {
41576
- id: "rendered-link-colour-only",
41577
- criteria: ["1.4.1"],
41588
+ var multimediaRules = [autoplayMedia, mediaNoTrack];
41589
+
41590
+ // src/rules/presentation.ts
41591
+ var metaViewportZoomBlock = {
41592
+ id: "meta-viewport-zoom-block",
41593
+ criteria: ["1.4.4"],
41578
41594
  severity: "majeur",
41579
41595
  run(doc) {
41580
- if (!doc.signals?.styles) return [];
41581
- const index = elementIndex(doc);
41582
41596
  const out2 = [];
41583
- for (const [el, i2] of index) {
41584
- if (el.tag !== "a" || !hasDirectText2(el)) continue;
41585
- const parent = el.parent;
41586
- if (!parent || !TEXT_BLOCK.has(parent.tag)) continue;
41587
- const css = styleAt(doc, i2)?.css;
41588
- const pi = index.get(parent);
41589
- const parentCss = pi === void 0 ? void 0 : styleAt(doc, pi)?.css;
41590
- if (!css || !parentCss || invisible(css)) continue;
41591
- if (hasNonColourAffordance(css, parentCss)) continue;
41592
- const linkColor = parseColor(css.color ?? "");
41593
- const textColor = parseColor(parentCss.color ?? "");
41594
- if (!linkColor || !textColor) continue;
41595
- const distinct = contrastRatio(linkColor, textColor);
41596
- if (distinct <= 1.02) continue;
41597
+ for (const el of doc.elements) {
41598
+ if (el.tag !== "meta" || (attr(el, "name") ?? "").toLowerCase() !== "viewport") continue;
41599
+ const content = (attr(el, "content") ?? "").toLowerCase();
41600
+ const pairs = /* @__PURE__ */ new Map();
41601
+ for (const part of content.split(/[,;]/)) {
41602
+ const [k, v] = part.split("=").map((s) => s.trim());
41603
+ if (k) pairs.set(k, v ?? "");
41604
+ }
41605
+ const userScalable = pairs.get("user-scalable");
41606
+ const maxScale = pairs.get("maximum-scale");
41607
+ const maxScaleNum = maxScale !== void 0 && maxScale.trim() !== "" ? Number(maxScale) : Number.NaN;
41608
+ const blocked = userScalable === "no" || userScalable === "0" || Number.isFinite(maxScaleNum) && maxScaleNum > 0 && maxScaleNum < 2;
41609
+ if (!blocked) continue;
41597
41610
  out2.push({
41598
- criteriaId: "1.4.1",
41611
+ criteriaId: "1.4.4",
41599
41612
  el,
41600
- msgId: "rendered-link-colour-only",
41601
- params: { ratio: distinct.toFixed(2) }
41613
+ msgId: "meta-viewport-zoom-block",
41614
+ params: { blockedBy: userScalable === "no" || userScalable === "0" ? "user-scalable" : "maximum-scale", maxScale: maxScale ?? "" }
41602
41615
  });
41603
41616
  }
41604
41617
  return out2;
41605
41618
  }
41606
41619
  };
41607
- var BOUNDED_CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea", "button"]);
41608
- var SIDES = ["Top", "Right", "Bottom", "Left"];
41609
- function borderColours(css) {
41610
- const out2 = [];
41611
- for (const s of SIDES) {
41612
- const style = css[`border${s}Style`];
41613
- const width = Number.parseFloat(css[`border${s}Width`] ?? "0");
41614
- if (!style || style === "none" || style === "hidden" || !(width > 0)) continue;
41615
- const c2 = parseColor(css[`border${s}Color`] ?? "");
41616
- if (c2 && c2.a >= 1) out2.push(c2);
41617
- }
41618
- return out2;
41619
- }
41620
- var renderedNonTextContrast = {
41621
- id: "rendered-nontext-contrast",
41622
- criteria: ["1.4.11"],
41623
- severity: "majeur",
41620
+ var CSS_CONTENT = /content\s*:\s*(["'])((?:\\.|(?!\1).)*)\1/gi;
41621
+ var INFORMATIVE_WORD = new RegExp("\\p{L}{3,}", "u");
41622
+ var cssGeneratedContentInformative = {
41623
+ id: "css-generated-content-informative",
41624
+ criteria: ["1.3.1"],
41625
+ severity: "mineur",
41626
+ advisory: true,
41624
41627
  run(doc) {
41625
- if (!doc.signals?.styles) return [];
41626
- const index = elementIndex(doc);
41627
41628
  const out2 = [];
41628
- for (const [el, i2] of index) {
41629
- if (!BOUNDED_CONTROLS.has(el.tag)) continue;
41630
- const type = (el.attribs.type ?? "").toLowerCase();
41631
- if (el.tag === "input" && (type === "hidden" || type === "image")) continue;
41632
- const css = styleAt(doc, i2)?.css;
41633
- if (!css || invisible(css)) continue;
41634
- if (css.boxShadow && css.boxShadow !== "none") continue;
41635
- if ((css.outlineStyle ?? "none") !== "none" && Number.parseFloat(css.outlineWidth ?? "0") > 0) continue;
41636
- const surrounding = el.parent ? backdropOf(doc, index, el.parent) : void 0;
41637
- if (!surrounding) continue;
41638
- const own = parseColor(css.backgroundColor ?? "");
41639
- const fill = own && own.a >= 1 ? own : surrounding.color;
41640
- if (contrastRatio(fill, surrounding.color) >= 3) continue;
41641
- const borders = borderColours(css);
41642
- if (borders.some((b) => contrastRatio(b, surrounding.color) >= 3 || contrastRatio(b, fill) >= 3)) continue;
41643
- out2.push({
41644
- criteriaId: "1.4.11",
41645
- el,
41646
- msgId: "rendered-nontext-contrast",
41647
- params: { ratio: contrastRatio(fill, surrounding.color).toFixed(2), control: el.tag }
41648
- });
41629
+ for (const el of doc.elements) {
41630
+ if (el.tag !== "style") continue;
41631
+ const css = textContent(el);
41632
+ const seen = /* @__PURE__ */ new Set();
41633
+ CSS_CONTENT.lastIndex = 0;
41634
+ let m;
41635
+ while (m = CSS_CONTENT.exec(css)) {
41636
+ const value = m[2] ?? "";
41637
+ if (value.startsWith("\\")) continue;
41638
+ if (!INFORMATIVE_WORD.test(value)) continue;
41639
+ if (seen.has(value)) continue;
41640
+ seen.add(value);
41641
+ out2.push({ criteriaId: "1.3.1", el, msgId: "css-generated-content-informative", params: { text: value.slice(0, 40) }, advisory: true });
41642
+ }
41649
41643
  }
41650
41644
  return out2;
41651
41645
  }
41652
41646
  };
41653
- var FOCUS_SEL = /:focus(-visible|-within)?\b/;
41654
- function killsOutline(d) {
41655
- const o = (d.outline ?? "").trim();
41656
- if (o === "none" || /^0(px)?( |$)/.test(o) || /\bnone\b/.test(o)) return true;
41657
- if ((d.outlineStyle ?? "") === "none") return true;
41658
- return d.outlineWidth !== void 0 && Number.parseFloat(d.outlineWidth) === 0;
41647
+ var presentationRules = [metaViewportZoomBlock, cssGeneratedContentInformative];
41648
+
41649
+ // src/rules/colors.ts
41650
+ var SKIP_TAGS2 = /* @__PURE__ */ new Set(["script", "style", "head", "title", "meta", "noscript", "link"]);
41651
+ function styleOf(el) {
41652
+ const s = attr(el, "style");
41653
+ return s ? parseInlineStyle(s) : /* @__PURE__ */ new Map();
41659
41654
  }
41660
- function restoresIndicator(d) {
41661
- if (d.boxShadow && d.boxShadow !== "none") return true;
41662
- if (d.outlineStyle && d.outlineStyle !== "none" && Number.parseFloat(d.outlineWidth ?? "1") > 0) return true;
41663
- if (d.outline && !killsOutline(d)) return true;
41664
- if (d.textDecorationLine && d.textDecorationLine !== "none") return true;
41665
- for (const k of Object.keys(d)) {
41666
- if (/^border(Top|Right|Bottom|Left)?(Color|Width|Style)?$/.test(k) && d[k] && d[k] !== "none") return true;
41667
- if (/^background(Color|Image)?$/.test(k) && d[k] && d[k] !== "none" && d[k] !== "rgba(0, 0, 0, 0)") return true;
41655
+ function colorFromValue(v) {
41656
+ const whole = parseColor(v);
41657
+ if (whole) return whole;
41658
+ for (const tok of v.split(/\s+/)) {
41659
+ const c2 = parseColor(tok);
41660
+ if (c2) return c2;
41668
41661
  }
41669
- return d.filter !== void 0 || d.transform !== void 0 || d.color !== void 0;
41662
+ return null;
41670
41663
  }
41671
- var renderedFocusNotVisible = {
41672
- id: "rendered-focus-not-visible",
41673
- criteria: ["2.4.7"],
41674
- severity: "majeur",
41675
- scope: "page",
41676
- run(doc) {
41677
- const css = doc.signals?.css;
41678
- if (!css || css.unreadable > 0 || css.truncated) return [];
41679
- const focusRules = css.rules.filter((r) => FOCUS_SEL.test(r.selector));
41680
- if (!focusRules.length) return [];
41681
- if (!focusRules.some((r) => killsOutline(r.decls))) return [];
41682
- if (focusRules.some((r) => restoresIndicator(r.decls))) return [];
41683
- const anchor = doc.elements.find((e) => ["a", "button", "input", "select", "textarea"].includes(e.tag));
41684
- if (!anchor) return [];
41685
- const killer = focusRules.find((r) => killsOutline(r.decls));
41686
- return [{ criteriaId: "2.4.7", el: doc.elements[0] ?? anchor, msgId: "rendered-focus-not-visible", params: { selector: killer?.selector ?? ":focus" } }];
41664
+ function fgOf(el) {
41665
+ for (let p = el; p; p = p.parent) {
41666
+ const v = styleOf(p).get("color");
41667
+ if (v) {
41668
+ const c2 = colorFromValue(v);
41669
+ if (c2) return c2;
41670
+ }
41687
41671
  }
41688
- };
41689
- var DOC_SEL = /^\s*(\*|:root|html|body)\s*$/i;
41690
- function quarterTurn(transform) {
41691
- const m = /rotate[ZY]?\(\s*(-?[\d.]+)deg\s*\)/i.exec(transform);
41692
- if (!m) return false;
41693
- const deg = Math.abs(Number.parseFloat(m[1])) % 180;
41694
- return Math.abs(deg - 90) < 1;
41672
+ return null;
41695
41673
  }
41696
- var renderedOrientationLock = {
41697
- id: "rendered-orientation-lock",
41698
- criteria: ["1.3.4"],
41674
+ function bgOf(el) {
41675
+ for (let p = el; p; p = p.parent) {
41676
+ const st = styleOf(p);
41677
+ const v = st.get("background-color") ?? st.get("background");
41678
+ if (v) {
41679
+ const c2 = colorFromValue(v);
41680
+ if (c2) return c2;
41681
+ }
41682
+ }
41683
+ return null;
41684
+ }
41685
+ function fontPx(el) {
41686
+ for (let p = el; p; p = p.parent) {
41687
+ const v = styleOf(p).get("font-size");
41688
+ if (!v) continue;
41689
+ const m = /^([\d.]+)(px|pt|rem|em|%)?$/.exec(v.trim());
41690
+ if (!m) return "unknown";
41691
+ const n = parseFloat(m[1]);
41692
+ if (Number.isNaN(n)) return "unknown";
41693
+ const unit = m[2] ?? "px";
41694
+ if (unit === "pt") return n * 4 / 3;
41695
+ if (unit === "rem" || unit === "em") return n * 16;
41696
+ if (unit === "%") return n / 100 * 16;
41697
+ return n;
41698
+ }
41699
+ return null;
41700
+ }
41701
+ function isBold(el) {
41702
+ for (let p = el; p; p = p.parent) {
41703
+ const v = styleOf(p).get("font-weight");
41704
+ if (v) return v === "bold" || Number(v) >= 700;
41705
+ }
41706
+ return false;
41707
+ }
41708
+ function isLarge(el) {
41709
+ const px = fontPx(el);
41710
+ if (px === null) return false;
41711
+ if (px === "unknown") return true;
41712
+ return px >= 24 || px >= 18.66 && isBold(el);
41713
+ }
41714
+ function hasDirectText2(el) {
41715
+ return el.children.some((c2) => c2.type === "text" && c2.data.trim() !== "");
41716
+ }
41717
+ var contrastLiteral = {
41718
+ id: "contrast-literal",
41719
+ criteria: ["1.4.3"],
41699
41720
  severity: "majeur",
41700
- scope: "page",
41701
41721
  run(doc) {
41702
- const css = doc.signals?.css;
41703
- if (!css || css.unreadable > 0) return [];
41704
- for (const r of css.rules) {
41705
- if (!r.media || !/orientation\s*:/i.test(r.media)) continue;
41706
- if (!r.selector.split(",").some((s) => DOC_SEL.test(s))) continue;
41707
- const t2 = r.decls.transform ?? "";
41708
- if (!quarterTurn(t2)) continue;
41709
- const el = doc.elements[0];
41710
- if (!el) continue;
41711
- return [{ criteriaId: "1.3.4", el, msgId: "rendered-orientation-lock", params: { media: r.media, transform: t2 } }];
41722
+ const out2 = [];
41723
+ for (const el of doc.elements) {
41724
+ if (SKIP_TAGS2.has(el.tag) || !hasDirectText2(el)) continue;
41725
+ const fg = fgOf(el);
41726
+ const bg = bgOf(el);
41727
+ if (!fg || !bg || fg.a < 1 || bg.a < 1) continue;
41728
+ const ratio = contrastRatio(fg, bg);
41729
+ const large = isLarge(el);
41730
+ const min = large ? 3 : 4.5;
41731
+ if (ratio >= min) continue;
41732
+ out2.push({
41733
+ criteriaId: "1.4.3",
41734
+ el,
41735
+ msgId: "contrast-literal",
41736
+ params: { ratio: ratio.toFixed(2), min, textSize: large ? "large" : "normal" }
41737
+ });
41712
41738
  }
41713
- return [];
41739
+ return out2;
41714
41740
  }
41715
41741
  };
41716
- var renderedRules = [
41717
- renderedContrast,
41718
- renderedContrastPixel,
41719
- renderedLinkColourOnly,
41720
- renderedNonTextContrast,
41721
- renderedFocusNotVisible,
41722
- renderedOrientationLock
41723
- ];
41742
+ var colorsRules = [contrastLiteral];
41724
41743
 
41725
41744
  // src/rules/timing.ts
41726
41745
  var metaRefreshRedirect = {
@@ -47390,10 +47409,84 @@ var APPLICABLE = {
47390
47409
  "3.1.1": (d) => isFullDocument(d)
47391
47410
  // Language of Page — html-lang-missing / lang-invalid
47392
47411
  };
47393
- function residualReason(automatability2) {
47412
+ var SUBJECT_MATTER = {
47413
+ // ---- Time-based media (WCAG 1.2.x → RGAA theme 4) ----
47414
+ // Captions, transcripts, audio description and sign language all presuppose a media element.
47415
+ // `track` counts on its own: a stray <track> means media is being assembled somewhere.
47416
+ "1.2.1": hasMedia,
47417
+ "1.2.2": hasMedia,
47418
+ "1.2.3": hasMedia,
47419
+ "1.2.4": hasMedia,
47420
+ "1.2.5": hasMedia,
47421
+ // ---- Motion actuation (WCAG 2.5.4) ----
47422
+ // Applicable only where device motion drives the interface, which is a source-visible API.
47423
+ "2.5.4": hasMotionApi,
47424
+ // ---- Forms (WCAG 3.3.x) ----
47425
+ // Error identification, labels and instructions, error suggestion, error prevention,
47426
+ // redundant entry and accessible authentication all presuppose user input. 3.3.4 and 3.3.8
47427
+ // are narrower still (legal/financial transactions, authentication), so "any form control"
47428
+ // is the deliberately over-inclusive predicate for them.
47429
+ "3.3.1": hasFormControl,
47430
+ "3.3.2": hasFormControl,
47431
+ "3.3.3": hasFormControl,
47432
+ "3.3.4": hasFormControl,
47433
+ "3.3.7": hasFormControl,
47434
+ "3.3.8": hasFormControl,
47435
+ // Identify Input Purpose — about autocomplete on fields collecting user data.
47436
+ "1.3.5": hasFormControl
47437
+ };
47438
+ function hasMedia(d) {
47439
+ return has(d, "audio", "video", "track", "source", "object", "embed", "iframe", "marquee", "canvas");
47440
+ }
47441
+ function hasMotionApi(d) {
47442
+ return /\b(?:devicemotion|deviceorientation|DeviceMotionEvent|DeviceOrientationEvent|Accelerometer|Gyroscope|requestPermission)\b/i.test(d.source);
47443
+ }
47444
+ function hasFormControl(d) {
47445
+ if (has(d, "input", "select", "textarea", "form", "fieldset", "output", "datalist")) return true;
47446
+ return d.elements.some((e) => {
47447
+ const role = (e.attribs.role ?? "").toLowerCase();
47448
+ if (role && /^(textbox|combobox|listbox|checkbox|radio|radiogroup|slider|spinbutton|searchbox|switch|form)$/.test(role)) return true;
47449
+ return e.attribs.contenteditable !== void 0;
47450
+ });
47451
+ }
47452
+ var RESIDUAL_TRAIL = {
47453
+ // Rendering criteria the SNAPSHOT tier decides offline, from computed styles + stylesheets.
47454
+ "1.3.4": "Decided on a page's own stylesheets (an orientation media query that rotates the document) \u2014 record a snapshot with `scan --sample` (or an E2E capture), then re-audit.",
47455
+ "1.4.1": "Decided on computed colours (a link distinguished by colour alone) \u2014 record a snapshot with `scan --sample`, then re-audit.",
47456
+ "1.4.3": "Decided on computed text/background colours, with a screenshot fallback where the backdrop is an image \u2014 record a snapshot with `scan --sample`, then re-audit.",
47457
+ "1.4.11": "Decided on computed colours of UI components and graphics \u2014 record a snapshot with `scan --sample`, then re-audit.",
47458
+ "2.4.7": "Decided on the page's own stylesheets (a `:focus` rule that removes the indicator without restoring one), or by the live focus probe \u2014 record a snapshot with `scan --sample`, then re-audit.",
47459
+ // Rendering criteria that need a LIVE browser: they are measured by acting on the page.
47460
+ "1.4.4": "Needs a live browser: the page is re-measured at 200% zoom \u2014 `scan <url> --runtime local --merge <audit.json>`.",
47461
+ "1.4.10": "Needs a live browser: the page is re-measured in a 320px viewport (reflow) \u2014 `scan <url> --merge <audit.json>`.",
47462
+ "1.4.12": "Needs a live browser: text spacing overrides are injected and the layout re-measured \u2014 `scan <url> --runtime local --merge <audit.json>`.",
47463
+ "1.4.13": "Needs a live browser: hover/focus-triggered content is opened and probed for dismiss/hover/persist \u2014 `scan <url> --runtime local --merge <audit.json>`.",
47464
+ // axe's own `target-size` rule can FAIL this on a live page, but no tier certifies it, so a
47465
+ // clean scan leaves it open rather than conforming. Say both halves.
47466
+ "2.5.8": "A live scan can fail this (axe `target-size`) but never certify it \u2014 `scan <url> --runtime local --merge <audit.json>`, then adjudicate what is left.",
47467
+ "4.1.3": "Needs a live browser WITH interaction: a status message is triggered and the live region observed \u2014 `scan <url> --runtime local --merge <audit.json>` (interactions are on by default).",
47468
+ // Rendering criteria no automated tier measures — say so, rather than pointing at `scan` and
47469
+ // letting the reader discover it changes nothing.
47470
+ "1.4.5": "No automated tier decides this: whether text is presented as an image is a reading of each image's content. Adjudicate it (`verify --manual`) against the images the audit lists.",
47471
+ "2.1.2": "No automated tier decides this: escaping a keyboard trap has to be attempted by hand, on each focusable region.",
47472
+ "2.3.1": "No automated tier decides this: flashing has to be observed over time on the rendered page.",
47473
+ "2.4.11": "No automated tier decides this: whether a focused element stays unobscured depends on the sticky headers and overlays in play on each screen."
47474
+ };
47475
+ function residualReason(automatability2, sc) {
47476
+ const trail = sc ? RESIDUAL_TRAIL[sc] : void 0;
47477
+ if (trail) return trail;
47394
47478
  return automatability2 === "needs-rendering" ? "Needs a rendered DOM (contrast, focus visibility, zoom/reflow, target size) \u2014 decide via `scan`." : "Judgement criterion \u2014 adjudicated by the agent from source/context (`verify --manual`, gated).";
47395
47479
  }
47480
+ function subjectMatterReason(sc, files) {
47481
+ const scope = `nothing in the ${files} file(s) audited is concerned`;
47482
+ if (sc.startsWith("1.2."))
47483
+ return `No time-based media in scope: ${scope} \u2014 no <audio>, <video>, <track>, <object>, <embed>, <iframe> or <canvas> element was found.`;
47484
+ if (sc === "2.5.4") return `No motion actuation in scope: ${scope} \u2014 no device-motion or device-orientation API is used.`;
47485
+ if (sc === "1.3.5" || sc.startsWith("3.3.")) return `No user input in scope: ${scope} \u2014 no form control (native, ARIA or contenteditable) was found.`;
47486
+ return `No element in scope is concerned by this success criterion (${scope}).`;
47487
+ }
47396
47488
  var STATIC_PREDS = allSC().filter((c2) => c2.automatability === "static").map((c2) => [c2.sc, APPLICABLE[c2.sc] ?? isFullDocument]);
47489
+ var SUBJECT_PREDS = allSC().filter((c2) => c2.automatability !== "static" && SUBJECT_MATTER[c2.sc] !== void 0).map((c2) => [c2.sc, SUBJECT_MATTER[c2.sc]]);
47397
47490
  function newAccum() {
47398
47491
  return {
47399
47492
  byCriterion: /* @__PURE__ */ new Map(),
@@ -47406,7 +47499,8 @@ function newAccum() {
47406
47499
  sfcFiles: 0,
47407
47500
  sfcExts: /* @__PURE__ */ new Set(),
47408
47501
  captures: [],
47409
- langCounts: /* @__PURE__ */ new Map()
47502
+ langCounts: /* @__PURE__ */ new Map(),
47503
+ renderedScs: /* @__PURE__ */ new Set()
47410
47504
  };
47411
47505
  }
47412
47506
  function primarySubtag(lang) {
@@ -47439,6 +47533,10 @@ function foldDoc(acc, doc, graph) {
47439
47533
  for (const [id, pred] of STATIC_PREDS) {
47440
47534
  if (!acc.applicable.get(id) && pred(doc)) acc.applicable.set(id, true);
47441
47535
  }
47536
+ for (const [id, pred] of SUBJECT_PREDS) {
47537
+ if (!acc.applicable.get(id) && pred(doc)) acc.applicable.set(id, true);
47538
+ }
47539
+ if (doc.signals) for (const sc of renderedTestedScs(doc.signals)) acc.renderedScs.add(sc);
47442
47540
  if (doc.opaqueComponents?.length) {
47443
47541
  for (const lib of doc.opaqueComponents) acc.opaqueLibs.add(lib);
47444
47542
  acc.opaqueFiles++;
@@ -47475,9 +47573,12 @@ function finalize(acc, inputs, extra = {}) {
47475
47573
  }
47476
47574
  } else if (normativeFs.length > 0) {
47477
47575
  status = "NC";
47576
+ } else if (SUBJECT_MATTER[c2.sc] !== void 0 && !(acc.applicable.get(c2.sc) ?? false)) {
47577
+ status = "NA";
47578
+ justification = subjectMatterReason(c2.sc, acc.fileCount);
47478
47579
  } else {
47479
47580
  status = "manual";
47480
- residualRisks.push({ criteriaId: c2.sc, reason: residualReason(c2.automatability), automatability: c2.automatability });
47581
+ residualRisks.push({ criteriaId: c2.sc, reason: residualReason(c2.automatability, c2.sc), automatability: c2.automatability });
47481
47582
  }
47482
47583
  criteria.push({ id: c2.sc, guideline: c2.guideline, status, findings: fs2, ...justification ? { justification } : {} });
47483
47584
  }
@@ -47515,6 +47616,11 @@ function finalize(acc, inputs, extra = {}) {
47515
47616
  }
47516
47617
  } : {},
47517
47618
  ...acc.langCounts.size ? { langs: [...acc.langCounts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([lang]) => lang) } : {},
47619
+ // Coverage the RENDERED tier earned from the snapshots this run ingested. Same field the
47620
+ // live-browser merge stamps (`mergeDynamic`), because it is the same claim: these criteria
47621
+ // were measured. Omitted when no snapshot carried usable signals, so a source-only audit
47622
+ // is byte-for-byte unchanged and still says the rendering criteria are untested.
47623
+ ...acc.renderedScs.size ? { scan: { testedScs: [...acc.renderedScs].sort() } } : {},
47518
47624
  // The pages this run genuinely read. Written UNCONDITIONALLY — `[]` is the whole point,
47519
47625
  // because "this audit read no page" is exactly the claim a source-only run needs to make,
47520
47626
  // and an omit-when-empty field would say nothing precisely then. See scope.pagesAudited.
@@ -47529,8 +47635,8 @@ function finalize(acc, inputs, extra = {}) {
47529
47635
  };
47530
47636
  }
47531
47637
  function hashContent(content, mode) {
47532
- const norm4 = mode === "normalized" ? content.replace(/>\s+</g, "><").trim() : content;
47533
- return createHash4("sha1").update(norm4).digest("hex");
47638
+ const norm5 = mode === "normalized" ? content.replace(/>\s+</g, "><").trim() : content;
47639
+ return createHash4("sha1").update(norm5).digest("hex");
47534
47640
  }
47535
47641
  function runAudit(opts) {
47536
47642
  const acc = newAccum();
@@ -53367,11 +53473,15 @@ var L4 = {
53367
53473
  }
53368
53474
  };
53369
53475
  var NEEDS_RENDERING = [
53476
+ { sc: "1.3.4", label: { fr: "verrou d\u2019orientation", en: "orientation lock" } },
53477
+ { sc: "1.4.1", label: { fr: "information par la couleur", en: "use of colour" } },
53478
+ { sc: "1.4.3", label: { fr: "contraste du texte", en: "text contrast" } },
53370
53479
  { sc: "1.4.4", label: { fr: "zoom 200 %", en: "200% zoom" } },
53371
53480
  { sc: "1.4.10", label: { fr: "reflow 320 px", en: "320px reflow" } },
53481
+ { sc: "1.4.11", label: { fr: "contraste des composants", en: "non-text contrast" } },
53372
53482
  { sc: "1.4.12", label: { fr: "espacement du texte", en: "text spacing" } },
53373
- { sc: "2.4.7", label: { fr: "visibilit\xE9 du focus", en: "focus visibility" } },
53374
53483
  { sc: "1.4.13", label: { fr: "contenu au survol", en: "content on hover" } },
53484
+ { sc: "2.4.7", label: { fr: "visibilit\xE9 du focus", en: "focus visibility" } },
53375
53485
  { sc: "4.1.3", label: { fr: "r\xE9gions live", en: "live regions" } }
53376
53486
  ];
53377
53487
  function untestedNeedsRendering(r) {
@@ -55475,7 +55585,7 @@ function sectionBody(md, n) {
55475
55585
 
55476
55586
  // src/adjudicate.ts
55477
55587
  import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
55478
- import { join as join35 } from "path";
55588
+ import { join as join35, resolve as resolve10 } from "path";
55479
55589
 
55480
55590
  // src/data/adjudication.json
55481
55591
  var adjudication_default = {
@@ -56422,7 +56532,7 @@ function docsForAudit(audit2, cwd) {
56422
56532
  const docs = [];
56423
56533
  for (const f of files) {
56424
56534
  try {
56425
- docs.push(parseSource(readText(cwd ? join35(cwd, f) : f), f));
56535
+ docs.push(parseSource(readText(cwd ? resolve10(cwd, f) : f), f));
56426
56536
  } catch {
56427
56537
  }
56428
56538
  }
@@ -56523,6 +56633,17 @@ function normativeRefResolves(ref, standard, itemCriterionId) {
56523
56633
  }
56524
56634
  function applyAdjudication(audit2, adj, opts = {}) {
56525
56635
  const issues = [];
56636
+ const expected = opts.residualReasons ?? {};
56637
+ const itemIssues = /* @__PURE__ */ new Map();
56638
+ const notFolded = /* @__PURE__ */ new Set();
56639
+ const blame = (criteriaId, issue) => {
56640
+ issues.push(issue);
56641
+ notFolded.add(criteriaId);
56642
+ const list = itemIssues.get(criteriaId);
56643
+ if (list) list.push(issue);
56644
+ else itemIssues.set(criteriaId, [issue]);
56645
+ };
56646
+ const uncovered = (criteriaId) => notFolded.add(criteriaId);
56526
56647
  canonicalizeAdjudication(adj);
56527
56648
  const byId2 = new Map(adj.items.map((it) => [it.criteriaId, it]));
56528
56649
  const packMode = !isCore(adj.standard);
@@ -56531,71 +56652,101 @@ function applyAdjudication(audit2, adj, opts = {}) {
56531
56652
  for (const pc of derivePackResults(audit2, adj.standard)) {
56532
56653
  if (pc.status !== "manual") continue;
56533
56654
  open.add(pc.id);
56534
- if (!byId2.has(pc.id)) issues.push(`criterion ${pc.id}: missing from the adjudication (coverage gap)`);
56655
+ if (byId2.has(pc.id)) continue;
56656
+ if (Object.hasOwn(expected, pc.id)) uncovered(pc.id);
56657
+ else blame(pc.id, `criterion ${pc.id}: missing from the adjudication (coverage gap)`);
56535
56658
  }
56536
56659
  } else {
56537
56660
  for (const r of audit2.residualRisks) {
56538
56661
  open.add(r.criteriaId);
56539
- if (!byId2.has(r.criteriaId)) issues.push(`criterion ${r.criteriaId}: missing from the adjudication (coverage gap)`);
56662
+ if (byId2.has(r.criteriaId)) continue;
56663
+ if (Object.hasOwn(expected, r.criteriaId)) uncovered(r.criteriaId);
56664
+ else blame(r.criteriaId, `criterion ${r.criteriaId}: missing from the adjudication (coverage gap)`);
56540
56665
  }
56541
56666
  }
56542
56667
  for (const it of adj.items) {
56543
56668
  if (!open.has(it.criteriaId)) {
56544
- issues.push(`criterion ${it.criteriaId}: not open for adjudication \u2014 the engine already decided it, or it is not part of ${adj.standard}`);
56669
+ blame(it.criteriaId, `criterion ${it.criteriaId}: not open for adjudication \u2014 the engine already decided it, or it is not part of ${adj.standard}`);
56545
56670
  }
56546
56671
  }
56547
- const groundInputs = [];
56672
+ const groundInputs = /* @__PURE__ */ new Map();
56673
+ const toGround = (criteriaId, g) => {
56674
+ const list = groundInputs.get(criteriaId);
56675
+ if (list) list.push(g);
56676
+ else groundInputs.set(criteriaId, [g]);
56677
+ };
56548
56678
  for (const it of adj.items) {
56549
56679
  const v = it.verdict;
56550
56680
  if (v === null) {
56551
- issues.push(`criterion ${it.criteriaId}: unadjudicated (verdict is null)`);
56681
+ blame(it.criteriaId, `criterion ${it.criteriaId}: unadjudicated (verdict is null)`);
56552
56682
  } else if (v === "C" || v === "NA") {
56553
- if (!it.justification || !it.justification.trim()) issues.push(`criterion ${it.criteriaId}: a ${v} verdict requires a justification`);
56683
+ if (!it.justification || !it.justification.trim()) blame(it.criteriaId, `criterion ${it.criteriaId}: a ${v} verdict requires a justification`);
56554
56684
  const cites = it.citations ?? [];
56555
56685
  if (it.evidence.length === 0) {
56556
56686
  if (v === "C") {
56557
- issues.push(
56687
+ blame(
56688
+ it.criteriaId,
56558
56689
  `criterion ${it.criteriaId}: a C verdict needs evidence to cite, and none was harvested for this criterion \u2014 record "manual" (reason "undecidable"), or "NA" if nothing in scope is concerned`
56559
56690
  );
56560
56691
  }
56561
56692
  } else if (cites.length === 0) {
56562
- issues.push(
56693
+ blame(
56694
+ it.criteriaId,
56563
56695
  `criterion ${it.criteriaId}: a ${v} verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, \u2026}])`
56564
56696
  );
56565
56697
  } else {
56566
56698
  const anchors = new Set(it.evidence.map((e) => `${e.file}:${e.line}`));
56567
56699
  for (const c2 of cites) {
56568
56700
  if (!anchors.has(`${c2.file}:${c2.line}`)) {
56569
- issues.push(`criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
56701
+ blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
56570
56702
  }
56571
- groundInputs.push({ file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
56703
+ toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
56572
56704
  }
56573
56705
  }
56574
56706
  } else if (v === "NC") {
56575
- if (!it.findings || it.findings.length === 0) issues.push(`criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
56707
+ if (!it.findings || it.findings.length === 0) blame(it.criteriaId, `criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
56576
56708
  for (const f of it.findings ?? []) {
56577
56709
  if (!f.normativeRef || !f.normativeRef.trim()) {
56578
- issues.push(`criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
56710
+ blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
56579
56711
  } else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
56580
- issues.push(
56712
+ blame(
56713
+ it.criteriaId,
56581
56714
  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`
56582
56715
  );
56583
56716
  }
56584
- groundInputs.push({ file: f.file, line: f.line, selector: f.selector, snippet: f.snippet });
56717
+ toGround(it.criteriaId, { file: f.file, line: f.line, selector: f.selector, snippet: f.snippet });
56585
56718
  }
56586
56719
  } else if (v === "manual") {
56587
56720
  if (!it.reason || !MANUAL_REASONS.has(it.reason))
56588
- issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`);
56721
+ blame(it.criteriaId, `criterion ${it.criteriaId}: a manual verdict requires reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`);
56589
56722
  } else {
56590
- issues.push(`criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
56723
+ blame(it.criteriaId, `criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
56724
+ }
56725
+ for (const rec of it.recommendations ?? []) toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
56726
+ }
56727
+ const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
56728
+ for (const [criteriaId, inputs] of groundInputs) {
56729
+ for (const input of inputs) {
56730
+ const r = groundFinding(input, { cwd: opts.cwd });
56731
+ if (r.ok) {
56732
+ grounding.grounded++;
56733
+ if (r.moved) grounding.moved++;
56734
+ } else {
56735
+ grounding.failed++;
56736
+ if (r.issue) {
56737
+ grounding.issues.push(r.issue);
56738
+ blame(criteriaId, r.issue);
56739
+ }
56740
+ }
56591
56741
  }
56592
- for (const rec of it.recommendations ?? []) groundInputs.push({ file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
56593
56742
  }
56594
- const grounding = groundItems(groundInputs, { cwd: opts.cwd });
56595
- for (const gi of grounding.issues) issues.push(gi);
56596
- if (issues.length) {
56597
- return { ok: false, audit: audit2, issues, applied: 0, stillManual: 0, grounding };
56743
+ if (opts.strict && issues.length) {
56744
+ return { ok: false, audit: audit2, issues, applied: 0, stillManual: 0, rejected: 0, rejectedCriteria: [], grounding };
56598
56745
  }
56746
+ const rejectedCriteria = adj.items.map((it) => it.criteriaId).filter((id) => itemIssues.has(id));
56747
+ for (const id of itemIssues.keys()) if (!rejectedCriteria.includes(id)) rejectedCriteria.push(id);
56748
+ const rejectedSet = notFolded;
56749
+ const rejectedWhy = (id) => expected[id] ?? residualRejectedReason(itemIssues.get(id)?.[0] ?? "refused by the gate");
56599
56750
  const next = structuredClone(audit2);
56600
56751
  const critById = new Map(next.criteria.map((c2) => [c2.id, c2]));
56601
56752
  const newFindings = [];
@@ -56604,6 +56755,17 @@ function applyAdjudication(audit2, adj, opts = {}) {
56604
56755
  if (packMode) {
56605
56756
  const decided = [];
56606
56757
  for (const it of adj.items) {
56758
+ if (rejectedSet.has(it.criteriaId)) {
56759
+ if (!open.has(it.criteriaId)) continue;
56760
+ decided.push({
56761
+ id: it.criteriaId,
56762
+ status: "manual",
56763
+ reason: "undecidable",
56764
+ justification: rejectedWhy(it.criteriaId),
56765
+ findings: []
56766
+ });
56767
+ continue;
56768
+ }
56607
56769
  if (it.verdict === "manual") {
56608
56770
  stillManual++;
56609
56771
  decided.push({
@@ -56628,14 +56790,19 @@ function applyAdjudication(audit2, adj, opts = {}) {
56628
56790
  decidedBy: "agent"
56629
56791
  });
56630
56792
  }
56793
+ for (const id of notFolded) {
56794
+ if (byId2.has(id) || !open.has(id)) continue;
56795
+ decided.push({ id, status: "manual", reason: "undecidable", justification: rejectedWhy(id), findings: [] });
56796
+ }
56631
56797
  next.packAdjudication = { standard: adj.standard, criteria: decided };
56632
56798
  next.findings = [...next.findings, ...newFindings];
56633
- next.adjudicated = { date: adj.auditDate, applied, stillManual };
56634
- return { ok: true, audit: next, issues, applied, stillManual, grounding };
56799
+ next.adjudicated = { date: adj.auditDate, applied, stillManual, ...rejectedCriteria.length ? { rejected: rejectedCriteria.length } : {} };
56800
+ return { ok: issues.length === 0, audit: next, issues, applied, stillManual, rejected: rejectedCriteria.length, rejectedCriteria, grounding };
56635
56801
  }
56636
56802
  for (const it of adj.items) {
56637
56803
  const c2 = critById.get(it.criteriaId);
56638
56804
  if (!c2) continue;
56805
+ if (rejectedSet.has(it.criteriaId)) continue;
56639
56806
  if (it.verdict === "manual") {
56640
56807
  c2.status = "manual";
56641
56808
  c2.decidedBy = "agent";
@@ -56656,7 +56823,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
56656
56823
  }
56657
56824
  for (const it of adj.items) {
56658
56825
  const c2 = critById.get(it.criteriaId);
56659
- if (!c2) continue;
56826
+ if (!c2 || rejectedSet.has(it.criteriaId)) continue;
56660
56827
  for (const rec of it.recommendations ?? []) {
56661
56828
  const f = agentFinding(it.criteriaId, rec, true);
56662
56829
  c2.findings.push(f);
@@ -56664,10 +56831,10 @@ function applyAdjudication(audit2, adj, opts = {}) {
56664
56831
  }
56665
56832
  }
56666
56833
  next.findings = [...next.findings, ...newFindings];
56667
- next.residualRisks = next.residualRisks.filter((r) => byId2.get(r.criteriaId)?.verdict === "manual");
56834
+ next.residualRisks = next.residualRisks.filter((r) => byId2.get(r.criteriaId)?.verdict === "manual" || rejectedSet.has(r.criteriaId)).map((r) => rejectedSet.has(r.criteriaId) ? { ...r, reason: rejectedWhy(r.criteriaId) } : r);
56668
56835
  recomputeTallies(next);
56669
- next.adjudicated = { date: adj.auditDate, applied, stillManual };
56670
- return { ok: true, audit: next, issues: [], applied, stillManual, grounding };
56836
+ next.adjudicated = { date: adj.auditDate, applied, stillManual, ...rejectedCriteria.length ? { rejected: rejectedCriteria.length } : {} };
56837
+ return { ok: issues.length === 0, audit: next, issues, applied, stillManual, rejected: rejectedCriteria.length, rejectedCriteria, grounding };
56671
56838
  }
56672
56839
  function agentFinding(criteriaId, f, advisory = false) {
56673
56840
  return {
@@ -56698,6 +56865,7 @@ function recomputeTallies(a) {
56698
56865
  }
56699
56866
  var residualScanReason = () => "Rendering criterion \u2014 decide on the rendered DOM (`scan`).";
56700
56867
  var residualUndecidableReason = () => "Left as an explicit residual risk (not decidable from the available evidence).";
56868
+ var residualRejectedReason = (why) => `Adjudication refused by the gate \u2014 ${why.replace(/^criterion \S+: /, "")}. Re-adjudicate this criterion.`;
56701
56869
  var T2 = {
56702
56870
  fr: {
56703
56871
  title: "# Adjudication des crit\xE8res \xE0 \xE9valuer (ultra11y)",
@@ -56865,6 +57033,124 @@ function hydrateAdjudication(adj, audit2, opts = {}) {
56865
57033
  }
56866
57034
  }
56867
57035
 
57036
+ // src/ledger.ts
57037
+ import { createHash as createHash6 } from "crypto";
57038
+ import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync19, writeFileSync as writeFileSync11 } from "fs";
57039
+ import { dirname as dirname10, join as join36 } from "path";
57040
+ var LEDGER_DIR = ".ultra11y/verdicts";
57041
+ function ledgerPath(standard, root = ".") {
57042
+ return join36(root, LEDGER_DIR, `${standard}.json`);
57043
+ }
57044
+ var norm3 = (s) => s.replace(/\s+/g, " ").trim();
57045
+ var anchorKey = (e) => `${e.file}|${e.selector ?? ""}|${norm3(e.snippet ?? "")}`;
57046
+ function evidenceFingerprint(evidence) {
57047
+ const keys = evidence.map(anchorKey).sort();
57048
+ return `sha256:${createHash6("sha256").update(`${keys.length}
57049
+ ${keys.join("\n")}`).digest("hex").slice(0, 32)}`;
57050
+ }
57051
+ function reanchor(stored, today2) {
57052
+ if (!stored?.length) return stored;
57053
+ const byKey2 = new Map(today2.map((e) => [anchorKey(e), e]));
57054
+ return stored.map((s) => {
57055
+ const now = byKey2.get(anchorKey(s));
57056
+ return now && now.line !== s.line ? { ...s, line: now.line } : s;
57057
+ });
57058
+ }
57059
+ function emptyLedger(standard = CORE2) {
57060
+ return { tool: "ultra11y", kind: "verdict-ledger", schemaVersion: SCHEMA_VERSION, standard, entries: [] };
57061
+ }
57062
+ function isLedger(v) {
57063
+ const o = v;
57064
+ return !!o && typeof o === "object" && o.tool === "ultra11y" && o.kind === "verdict-ledger" && Array.isArray(o.entries);
57065
+ }
57066
+ function readLedger(path) {
57067
+ if (!existsSync21(path)) return void 0;
57068
+ try {
57069
+ const parsed = JSON.parse(readFileSync19(path, "utf8"));
57070
+ return isLedger(parsed) ? parsed : void 0;
57071
+ } catch {
57072
+ return void 0;
57073
+ }
57074
+ }
57075
+ function writeLedger(path, ledger) {
57076
+ mkdirSync10(dirname10(path), { recursive: true });
57077
+ const sorted = { ...ledger, entries: [...ledger.entries].sort((a, b) => a.criteriaId.localeCompare(b.criteriaId, "en", { numeric: true })) };
57078
+ writeFileSync11(path, `${JSON.stringify(sorted, null, 2)}
57079
+ `);
57080
+ }
57081
+ function entriesFrom(adj, accepted, date) {
57082
+ const out2 = [];
57083
+ for (const it of adj.items) {
57084
+ if (!accepted.has(it.criteriaId) || it.verdict === null) continue;
57085
+ out2.push({
57086
+ criteriaId: it.criteriaId,
57087
+ verdict: it.verdict,
57088
+ ...it.justification?.trim() ? { justification: it.justification.trim() } : {},
57089
+ ...it.reason ? { reason: it.reason } : {},
57090
+ ...it.citations?.length ? { citations: it.citations } : {},
57091
+ ...it.findings?.length ? { findings: it.findings } : {},
57092
+ ...it.recommendations?.length ? { recommendations: it.recommendations } : {},
57093
+ evidenceFingerprint: evidenceFingerprint(it.evidence),
57094
+ evidenceCount: it.evidence.length,
57095
+ date,
57096
+ decidedBy: "agent"
57097
+ });
57098
+ }
57099
+ return out2;
57100
+ }
57101
+ function mergeLedger(existing, standard, fresh) {
57102
+ const base = existing && existing.standard === standard ? existing : emptyLedger(standard);
57103
+ const byId2 = new Map(base.entries.map((e) => [e.criteriaId, e]));
57104
+ for (const e of fresh) byId2.set(e.criteriaId, e);
57105
+ return { ...base, schemaVersion: SCHEMA_VERSION, standard, entries: [...byId2.values()] };
57106
+ }
57107
+ function replayLedger(audit2, ledger, opts = {}) {
57108
+ const standard = opts.standard ?? ledger.standard ?? CORE2;
57109
+ const worklist = buildAdjudicationWorklist(audit2, { cwd: opts.cwd, standard });
57110
+ const open = new Map(worklist.map((it) => [it.criteriaId, it]));
57111
+ const byId2 = new Map(ledger.entries.map((e) => [e.criteriaId, e]));
57112
+ const items = [];
57113
+ const fresh = [];
57114
+ const stale = [];
57115
+ const obsolete = [];
57116
+ const missing = [];
57117
+ const residualReasons = {};
57118
+ for (const e of ledger.entries) if (!open.has(e.criteriaId)) obsolete.push(e.criteriaId);
57119
+ for (const it of worklist) {
57120
+ const e = byId2.get(it.criteriaId);
57121
+ if (!e) {
57122
+ missing.push(it.criteriaId);
57123
+ residualReasons[it.criteriaId] = "No verdict in the ledger \u2014 this criterion has never been adjudicated. Run an adjudication pass to record one.";
57124
+ continue;
57125
+ }
57126
+ const now = evidenceFingerprint(it.evidence);
57127
+ if (now !== e.evidenceFingerprint) {
57128
+ stale.push(it.criteriaId);
57129
+ residualReasons[it.criteriaId] = `Ledger verdict is STALE \u2014 the evidence changed since it was recorded on ${e.date} (${e.evidenceCount} item(s) then, ${it.evidence.length} now). Re-adjudicate this criterion.`;
57130
+ continue;
57131
+ }
57132
+ fresh.push(it.criteriaId);
57133
+ items.push({
57134
+ ...it,
57135
+ verdict: e.verdict,
57136
+ justification: e.justification ?? "",
57137
+ reason: e.reason ?? null,
57138
+ findings: reanchor(e.findings, it.evidence) ?? [],
57139
+ ...e.citations ? { citations: reanchor(e.citations, it.evidence) ?? [] } : {},
57140
+ ...e.recommendations ? { recommendations: reanchor(e.recommendations, it.evidence) ?? [] } : {},
57141
+ decidedBy: "agent"
57142
+ });
57143
+ }
57144
+ return {
57145
+ adj: { tool: "ultra11y", kind: "adjudication", schemaVersion: SCHEMA_VERSION, standard, auditDate: audit2.date, items },
57146
+ fresh,
57147
+ stale,
57148
+ obsolete,
57149
+ missing,
57150
+ residualReasons
57151
+ };
57152
+ }
57153
+
56868
57154
  // src/llm.ts
56869
57155
  var DEFAULT_MODEL = "claude-sonnet-5";
56870
57156
  var API_VERSION = "2023-06-01";
@@ -57055,9 +57341,9 @@ function applyRawVerdicts(items, verdicts) {
57055
57341
 
57056
57342
  // src/scan.ts
57057
57343
  import { execFileSync as execFileSync6 } from "child_process";
57058
- import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync11, existsSync as existsSync22, statSync as statSync8, readdirSync as readdirSync5, rmSync as rmSync3, readFileSync as readFileSync19 } from "fs";
57344
+ import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync12, existsSync as existsSync23, statSync as statSync8, readdirSync as readdirSync5, rmSync as rmSync3, readFileSync as readFileSync20 } from "fs";
57059
57345
  import { tmpdir as tmpdir2 } from "os";
57060
- import { join as join36, resolve as resolve10 } from "path";
57346
+ import { join as join37, resolve as resolve11 } from "path";
57061
57347
  import { fileURLToPath as fileURLToPath3 } from "url";
57062
57348
 
57063
57349
  // src/axe-map.ts
@@ -57317,7 +57603,7 @@ async function crawlUrls(start2, opts) {
57317
57603
  }
57318
57604
 
57319
57605
  // src/sample.ts
57320
- import { existsSync as existsSync21 } from "fs";
57606
+ import { existsSync as existsSync22 } from "fs";
57321
57607
  function looksLikeTarget(u) {
57322
57608
  return /^https?:\/\//i.test(u) || /^(\.\.?[/\\]|[/\\])/.test(u) || /\.x?html?$/i.test(u);
57323
57609
  }
@@ -57353,7 +57639,7 @@ function validateSample(raw) {
57353
57639
  if (p.auth !== void 0 && typeof p.auth !== "boolean") err2(`sample.pages[${i2}].auth`, "auth must be a boolean");
57354
57640
  if (p.storageState !== void 0 && (typeof p.storageState !== "string" || p.storageState.trim() === "")) {
57355
57641
  err2(`sample.pages[${i2}].storageState`, "storageState must be a non-empty file path string");
57356
- } else if (typeof p.storageState === "string" && p.storageState.trim() !== "" && !existsSync21(p.storageState)) {
57642
+ } else if (typeof p.storageState === "string" && p.storageState.trim() !== "" && !existsSync22(p.storageState)) {
57357
57643
  warn(
57358
57644
  `sample.pages[${i2}].storageState`,
57359
57645
  `storageState path not found: "${p.storageState}" (resolved from the current directory) \u2014 the scan will not be able to load this session`
@@ -57400,7 +57686,7 @@ function sampleScope(sample) {
57400
57686
  ...sample.transverse?.length ? { transverse: sample.transverse } : {}
57401
57687
  };
57402
57688
  }
57403
- var norm3 = (s) => s.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").replace(/['’]/g, " ").toLowerCase();
57689
+ var norm4 = (s) => s.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").replace(/['’]/g, " ").toLowerCase();
57404
57690
  var SHORT_KEYWORD = 5;
57405
57691
  var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
57406
57692
  function keywordMatches(haystack, keyword) {
@@ -57408,12 +57694,12 @@ function keywordMatches(haystack, keyword) {
57408
57694
  return new RegExp(`(^|[^a-z0-9])${escapeRe(keyword)}($|[^a-z0-9])`).test(haystack);
57409
57695
  }
57410
57696
  function lintSample(sample, methodology) {
57411
- const haystacks = sample.pages.map((p) => norm3([p.name, p.notes ?? "", p.url, p.id].join(" ")));
57412
- const transverseHay = norm3((sample.transverse ?? []).join(" "));
57697
+ const haystacks = sample.pages.map((p) => norm4([p.name, p.notes ?? "", p.url, p.id].join(" ")));
57698
+ const transverseHay = norm4((sample.transverse ?? []).join(" "));
57413
57699
  const hasTransverse = (sample.transverse ?? []).length > 0;
57414
57700
  const missing = [];
57415
57701
  for (const kind of methodology.requiredKinds) {
57416
- const kws = kind.keywords.map(norm3).filter(Boolean);
57702
+ const kws = kind.keywords.map(norm4).filter(Boolean);
57417
57703
  const covered = haystacks.some((h) => kws.some((k) => keywordMatches(h, k))) || transverseHay !== "" && kws.some((k) => keywordMatches(transverseHay, k)) || hasTransverse && /transverse/.test(kind.id) || /representative|representatif/.test(kind.id) && sample.pages.length >= 2;
57418
57704
  if (!covered) missing.push(kind);
57419
57705
  }
@@ -57546,11 +57832,11 @@ function imageExists(tag) {
57546
57832
  }
57547
57833
  var CTX_PREFIX = "ultra11y-dyn-";
57548
57834
  function buildImage(tag = IMAGE_TAG) {
57549
- const ctx = mkdtempSync2(join36(tmpdir2(), CTX_PREFIX));
57835
+ const ctx = mkdtempSync2(join37(tmpdir2(), CTX_PREFIX));
57550
57836
  try {
57551
- writeFileSync11(join36(ctx, "runner.mjs"), RUNNER);
57552
- writeFileSync11(join36(ctx, "package.json"), PKG);
57553
- writeFileSync11(join36(ctx, "Dockerfile"), DOCKERFILE);
57837
+ writeFileSync12(join37(ctx, "runner.mjs"), RUNNER);
57838
+ writeFileSync12(join37(ctx, "package.json"), PKG);
57839
+ writeFileSync12(join37(ctx, "Dockerfile"), DOCKERFILE);
57554
57840
  execFileSync6("docker", ["build", "-t", tag, ctx], { stdio: "inherit", timeout: 9e5 });
57555
57841
  } finally {
57556
57842
  rmSync3(ctx, { recursive: true, force: true });
@@ -57561,7 +57847,7 @@ function cleanTempContexts() {
57561
57847
  const dir = tmpdir2();
57562
57848
  for (const name2 of readdirSync5(dir)) {
57563
57849
  if (!name2.startsWith(CTX_PREFIX)) continue;
57564
- rmSync3(join36(dir, name2), { recursive: true, force: true });
57850
+ rmSync3(join37(dir, name2), { recursive: true, force: true });
57565
57851
  removed++;
57566
57852
  }
57567
57853
  return removed;
@@ -57627,7 +57913,7 @@ var PROBE_FIELDS = [
57627
57913
  function runRunner(target, isFile, tag, snapshot = true) {
57628
57914
  const args2 = ["run", "--rm"];
57629
57915
  if (!snapshot) args2.push("-e", "ULTRA11Y_SNAPSHOT=0");
57630
- if (isFile) args2.push("-v", `${resolve10(target)}:${MOUNT}:ro`);
57916
+ if (isFile) args2.push("-v", `${resolve11(target)}:${MOUNT}:ro`);
57631
57917
  args2.push(tag, isFile ? MOUNT : target);
57632
57918
  let stdout;
57633
57919
  try {
@@ -57710,7 +57996,7 @@ function toDynamicResult(out2, target, lang = "en", engine = "axe-core@playwrigh
57710
57996
  var DOCKER_TESTED_SCS = ["1.4.10"];
57711
57997
  function runScan(opts) {
57712
57998
  const isUrl3 = /^https?:\/\//i.test(opts.target);
57713
- if (!isUrl3 && !existsSync22(opts.target)) {
57999
+ if (!isUrl3 && !existsSync23(opts.target)) {
57714
58000
  throw new Error(`File not found: ${opts.target}. Pass an http(s):// URL or an existing HTML file.`);
57715
58001
  }
57716
58002
  if (!dockerAvailable()) {
@@ -57718,7 +58004,7 @@ function runScan(opts) {
57718
58004
  }
57719
58005
  const tag = opts.tag ?? IMAGE_TAG;
57720
58006
  if (!imageExists(tag)) buildImage(tag);
57721
- const isFile = !isUrl3 && existsSync22(opts.target) && statSync8(opts.target).isFile();
58007
+ const isFile = !isUrl3 && existsSync23(opts.target) && statSync8(opts.target).isFile();
57722
58008
  const out2 = runRunner(opts.target, isFile, tag, Boolean(opts.snapshotRoot));
57723
58009
  const id = opts.snapshotRoot ? writeRunnerSnapshot(opts.snapshotRoot, out2, opts.target) : void 0;
57724
58010
  return { ...toDynamicResult(out2, opts.target), testedScs: [...DOCKER_TESTED_SCS], ...id ? { snapshots: [id] } : {} };
@@ -57805,10 +58091,10 @@ async function runCrawlScan(opts) {
57805
58091
  var sevRank = { bloquant: 3, majeur: 2, mineur: 1 };
57806
58092
  function resolveHostAnchor(file, snippet2) {
57807
58093
  const s = snippet2?.trim();
57808
- if (!s || !existsSync22(file)) return null;
58094
+ if (!s || !existsSync23(file)) return null;
57809
58095
  let source;
57810
58096
  try {
57811
- source = readFileSync19(file, "utf8");
58097
+ source = readFileSync20(file, "utf8");
57812
58098
  } catch {
57813
58099
  return null;
57814
58100
  }
@@ -57824,12 +58110,12 @@ function resolveHostAnchor(file, snippet2) {
57824
58110
  idx = source.indexOf(openMatch[0]);
57825
58111
  if (idx >= 0) return at(idx, openMatch[0].length);
57826
58112
  }
57827
- const norm4 = (t2) => t2.replace(/\s+/g, " ").trim();
57828
- const needle = norm4(openMatch ? openMatch[0] : s);
58113
+ const norm5 = (t2) => t2.replace(/\s+/g, " ").trim();
58114
+ const needle = norm5(openMatch ? openMatch[0] : s);
57829
58115
  if (needle) {
57830
58116
  const lines = source.split("\n");
57831
58117
  for (let i2 = 0; i2 < lines.length; i2++) {
57832
- if (norm4(lines[i2]).includes(needle)) {
58118
+ if (norm5(lines[i2]).includes(needle)) {
57833
58119
  const start2 = starts[i2] ?? 0;
57834
58120
  return { line: i2 + 1, col: 1, start: start2, end: start2 + lines[i2].length };
57835
58121
  }
@@ -57943,9 +58229,9 @@ function mergeSnapshotAudit(base, snap) {
57943
58229
  }
57944
58230
 
57945
58231
  // src/scan-local.ts
57946
- import { existsSync as existsSync23, statSync as statSync9 } from "fs";
58232
+ import { existsSync as existsSync24, statSync as statSync9 } from "fs";
57947
58233
  import { createRequire } from "module";
57948
- import { resolve as resolve11 } from "path";
58234
+ import { resolve as resolve12 } from "path";
57949
58235
  var LOCAL_ENGINE = "axe-core@playwright (local)";
57950
58236
  var LOCAL_TESTED_SCS = ["1.4.4", "1.4.10", "1.4.12", "2.4.7", "1.4.13"];
57951
58237
  function localTestedScs(interact) {
@@ -57955,7 +58241,7 @@ var PW_SPEC = "@playwright/test";
57955
58241
  var AXE_SPEC = "@axe-core/playwright";
57956
58242
  function localAvailable(cwd) {
57957
58243
  try {
57958
- const req = createRequire(resolve11(cwd, "package.json"));
58244
+ const req = createRequire(resolve12(cwd, "package.json"));
57959
58245
  req.resolve(PW_SPEC);
57960
58246
  req.resolve(AXE_SPEC);
57961
58247
  return true;
@@ -57967,7 +58253,7 @@ function resolveLocalDeps(cwd) {
57967
58253
  let chromium;
57968
58254
  let AxeBuilder;
57969
58255
  try {
57970
- const req = createRequire(resolve11(cwd, "package.json"));
58256
+ const req = createRequire(resolve12(cwd, "package.json"));
57971
58257
  const pw = req(PW_SPEC);
57972
58258
  const axeMod = req(AXE_SPEC);
57973
58259
  chromium = pw.chromium;
@@ -58466,7 +58752,7 @@ async function runOnPage(browser, AxeBuilder, target, isFile, opts) {
58466
58752
  const page = await context.newPage();
58467
58753
  const empty = [];
58468
58754
  try {
58469
- const url = isFile ? "file://" + resolve11(target) : target;
58755
+ const url = isFile ? "file://" + resolve12(target) : target;
58470
58756
  const response = await page.goto(url, { waitUntil: "load", timeout: 45e3 });
58471
58757
  await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => {
58472
58758
  });
@@ -58538,7 +58824,7 @@ async function runOnPage(browser, AxeBuilder, target, isFile, opts) {
58538
58824
  }
58539
58825
  async function runScanLocal(opts) {
58540
58826
  const isUrl3 = /^https?:\/\//i.test(opts.target);
58541
- if (!isUrl3 && !existsSync23(opts.target)) {
58827
+ if (!isUrl3 && !existsSync24(opts.target)) {
58542
58828
  throw new Error(`File not found: ${opts.target}. Pass an http(s):// URL or an existing HTML file.`);
58543
58829
  }
58544
58830
  const isFile = !isUrl3 && statSync9(opts.target).isFile();
@@ -58612,7 +58898,7 @@ async function runSampleScanLocal(pages, opts) {
58612
58898
  lang,
58613
58899
  snapshot: Boolean(opts.snapshotRoot)
58614
58900
  });
58615
- const requestedUrl = isFile ? "file://" + resolve11(page.url) : page.url;
58901
+ const requestedUrl = isFile ? "file://" + resolve12(page.url) : page.url;
58616
58902
  const landedUrl = out2.landedUrl ?? out2.url;
58617
58903
  if (!landedOnRequestedPage(requestedUrl, landedUrl)) {
58618
58904
  redirected.push({ id: page.id, name: page.name, requested: page.url, landed: landedUrl, reason: "redirect" });
@@ -58841,7 +59127,7 @@ function diffSides(left, right) {
58841
59127
  }
58842
59128
 
58843
59129
  // src/fix.ts
58844
- import { writeFileSync as writeFileSync12 } from "fs";
59130
+ import { writeFileSync as writeFileSync13 } from "fs";
58845
59131
 
58846
59132
  // src/fix/edits.ts
58847
59133
  function openTag(source, start2) {
@@ -59055,7 +59341,7 @@ function fixOne(file, source, opts, canWrite = true) {
59055
59341
  regression = after.length > findings.length || after.some((f) => !beforeKinds.has(f.ruleId));
59056
59342
  if (opts.write && !regression && file !== "<stdin>") {
59057
59343
  if (canWrite) {
59058
- writeFileSync12(file, output);
59344
+ writeFileSync13(file, output);
59059
59345
  written = true;
59060
59346
  } else {
59061
59347
  skippedPartialStage = true;
@@ -59137,9 +59423,9 @@ function fixSummary(r, lang = "fr", write = false) {
59137
59423
  }
59138
59424
 
59139
59425
  // src/init.ts
59140
- import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync10, chmodSync, realpathSync as realpathSync2 } from "fs";
59426
+ import { writeFileSync as writeFileSync14, mkdirSync as mkdirSync11, chmodSync, realpathSync as realpathSync2 } from "fs";
59141
59427
  import { execFileSync as execFileSync7 } from "child_process";
59142
- import { join as join37, relative as relative4, sep as sep4 } from "path";
59428
+ import { join as join38, relative as relative4, sep as sep4 } from "path";
59143
59429
  var EN_SEV = { bloquant: "blocking", majeur: "major", mineur: "minor" };
59144
59430
  function repoRoot() {
59145
59431
  try {
@@ -59257,24 +59543,24 @@ jobs:
59257
59543
  `;
59258
59544
  }
59259
59545
  function writeHook(root, enginePath, failOn, mode = "staged") {
59260
- const dir = join37(root, ".git", "hooks");
59261
- mkdirSync10(dir, { recursive: true });
59262
- const path = join37(dir, "pre-commit");
59263
- writeFileSync13(path, mode === "baseline" ? hookScript(enginePath, failOn) : stagedHookScript(enginePath, failOn));
59546
+ const dir = join38(root, ".git", "hooks");
59547
+ mkdirSync11(dir, { recursive: true });
59548
+ const path = join38(dir, "pre-commit");
59549
+ writeFileSync14(path, mode === "baseline" ? hookScript(enginePath, failOn) : stagedHookScript(enginePath, failOn));
59264
59550
  chmodSync(path, 493);
59265
59551
  return path;
59266
59552
  }
59267
59553
  function writeCi(root, enginePath, failOn) {
59268
- const dir = join37(root, ".github", "workflows");
59269
- mkdirSync10(dir, { recursive: true });
59270
- const path = join37(dir, "a11y.yml");
59271
- writeFileSync13(path, ciWorkflow(enginePath, failOn));
59554
+ const dir = join38(root, ".github", "workflows");
59555
+ mkdirSync11(dir, { recursive: true });
59556
+ const path = join38(dir, "a11y.yml");
59557
+ writeFileSync14(path, ciWorkflow(enginePath, failOn));
59272
59558
  return path;
59273
59559
  }
59274
59560
 
59275
59561
  // src/install/json-edit.ts
59276
- import { copyFileSync as copyFileSync2, existsSync as existsSync24, mkdirSync as mkdirSync11, readFileSync as readFileSync20, renameSync as renameSync2, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "fs";
59277
- import { dirname as dirname10 } from "path";
59562
+ import { copyFileSync as copyFileSync2, existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync21, renameSync as renameSync2, rmSync as rmSync4, writeFileSync as writeFileSync15 } from "fs";
59563
+ import { dirname as dirname11 } from "path";
59278
59564
  var SettingsParseError = class extends Error {
59279
59565
  constructor(path, cause) {
59280
59566
  super(`${path} is not valid JSON (${cause}) \u2014 fix or move it, then run install again. It has NOT been modified.`);
@@ -59287,16 +59573,16 @@ function stamp() {
59287
59573
  return (/* @__PURE__ */ new Date()).toISOString().replaceAll(/[:.]/g, "-");
59288
59574
  }
59289
59575
  function writeTextWithBackup(path, content, marker = "ultra11y") {
59290
- if (existsSync24(path) && readFileSync20(path, "utf8") === content) return { path, changed: false };
59291
- mkdirSync11(dirname10(path), { recursive: true });
59576
+ if (existsSync25(path) && readFileSync21(path, "utf8") === content) return { path, changed: false };
59577
+ mkdirSync12(dirname11(path), { recursive: true });
59292
59578
  let backup;
59293
- if (existsSync24(path)) {
59579
+ if (existsSync25(path)) {
59294
59580
  backup = `${path}.${marker}-backup-${stamp()}`;
59295
59581
  copyFileSync2(path, backup);
59296
59582
  }
59297
59583
  const tmp = `${path}.${process.pid}.tmp`;
59298
59584
  try {
59299
- writeFileSync14(tmp, content, { mode: 420 });
59585
+ writeFileSync15(tmp, content, { mode: 420 });
59300
59586
  renameSync2(tmp, path);
59301
59587
  } catch (e) {
59302
59588
  rmSync4(tmp, { force: true });
@@ -59306,8 +59592,8 @@ function writeTextWithBackup(path, content, marker = "ultra11y") {
59306
59592
  }
59307
59593
  function editJsonFile(path, mutate) {
59308
59594
  let root = {};
59309
- if (existsSync24(path)) {
59310
- const raw = readFileSync20(path, "utf8");
59595
+ if (existsSync25(path)) {
59596
+ const raw = readFileSync21(path, "utf8");
59311
59597
  if (raw.trim()) {
59312
59598
  let parsed;
59313
59599
  try {
@@ -59323,13 +59609,13 @@ function editJsonFile(path, mutate) {
59323
59609
  }
59324
59610
  const before = JSON.stringify(root);
59325
59611
  mutate(root);
59326
- if (JSON.stringify(root) === before && existsSync24(path)) return { path, changed: false };
59612
+ if (JSON.stringify(root) === before && existsSync25(path)) return { path, changed: false };
59327
59613
  return writeTextWithBackup(path, `${JSON.stringify(root, null, 2)}
59328
59614
  `);
59329
59615
  }
59330
59616
  function readJsonSafe(path) {
59331
59617
  try {
59332
- const parsed = JSON.parse(readFileSync20(path, "utf8"));
59618
+ const parsed = JSON.parse(readFileSync21(path, "utf8"));
59333
59619
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
59334
59620
  } catch {
59335
59621
  return null;
@@ -59370,7 +59656,7 @@ function claudeCodeWired(settingsPath) {
59370
59656
  }
59371
59657
 
59372
59658
  // src/install/text-edit.ts
59373
- import { existsSync as existsSync25, readFileSync as readFileSync21, rmSync as rmSync5 } from "fs";
59659
+ import { existsSync as existsSync26, readFileSync as readFileSync23, rmSync as rmSync5 } from "fs";
59374
59660
  var BLOCK_BEGIN = "<!-- BEGIN ultra11y (managed by `ultra11y install --agents-md`; edit outside this block) -->";
59375
59661
  var BLOCK_END = "<!-- END ultra11y -->";
59376
59662
  var blockRe = () => new RegExp(`${escapeRe2(BLOCK_BEGIN)}[\\s\\S]*?${escapeRe2(BLOCK_END)}\\n?`);
@@ -59382,15 +59668,15 @@ function upsertManagedBlock(path, body3) {
59382
59668
  ${body3.trim()}
59383
59669
  ${BLOCK_END}
59384
59670
  `;
59385
- if (!existsSync25(path)) return writeTextWithBackup(path, block);
59386
- const current = readFileSync21(path, "utf8");
59671
+ if (!existsSync26(path)) return writeTextWithBackup(path, block);
59672
+ const current = readFileSync23(path, "utf8");
59387
59673
  if (blockRe().test(current)) return writeTextWithBackup(path, current.replace(blockRe(), block));
59388
59674
  const sep8 = current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
59389
59675
  return writeTextWithBackup(path, `${current}${sep8}${block}`);
59390
59676
  }
59391
59677
  function removeManagedBlock(path) {
59392
- if (!existsSync25(path)) return { path, changed: false };
59393
- const current = readFileSync21(path, "utf8");
59678
+ if (!existsSync26(path)) return { path, changed: false };
59679
+ const current = readFileSync23(path, "utf8");
59394
59680
  if (!blockRe().test(current)) return { path, changed: false };
59395
59681
  const rest = current.replace(blockRe(), "");
59396
59682
  if (rest.trim() === "") {
@@ -59400,13 +59686,13 @@ function removeManagedBlock(path) {
59400
59686
  return writeTextWithBackup(path, rest.replace(/\n+$/, "\n"));
59401
59687
  }
59402
59688
  function hasManagedBlock(path) {
59403
- return existsSync25(path) && blockRe().test(readFileSync21(path, "utf8"));
59689
+ return existsSync26(path) && blockRe().test(readFileSync23(path, "utf8"));
59404
59690
  }
59405
59691
 
59406
59692
  // src/install/agents-md.ts
59407
- import { join as join38 } from "path";
59693
+ import { join as join39 } from "path";
59408
59694
  function agentsMdPath(root) {
59409
- return join38(root, "AGENTS.md");
59695
+ return join39(root, "AGENTS.md");
59410
59696
  }
59411
59697
  function agentsMdBlock(root) {
59412
59698
  const e = engineInvocation(root);
@@ -59461,25 +59747,25 @@ function agentsMdWired(root) {
59461
59747
  }
59462
59748
 
59463
59749
  // src/install/codex.ts
59464
- import { existsSync as existsSync27, readFileSync as readFileSync24 } from "fs";
59465
- import { join as join40 } from "path";
59750
+ import { existsSync as existsSync28, readFileSync as readFileSync25 } from "fs";
59751
+ import { join as join41 } from "path";
59466
59752
 
59467
59753
  // src/install/paths.ts
59468
- import { copyFileSync as copyFileSync3, cpSync, existsSync as existsSync26, mkdirSync as mkdirSync12, readFileSync as readFileSync23, realpathSync as realpathSync3, statSync as statSync10 } from "fs";
59754
+ import { copyFileSync as copyFileSync3, cpSync, existsSync as existsSync27, mkdirSync as mkdirSync13, readFileSync as readFileSync24, realpathSync as realpathSync3, statSync as statSync10 } from "fs";
59469
59755
  import { homedir as homedir2 } from "os";
59470
- import { dirname as dirname11, join as join39 } from "path";
59756
+ import { dirname as dirname12, join as join40 } from "path";
59471
59757
  function codexHome() {
59472
- return process.env.CODEX_HOME || join39(homedir2(), ".codex");
59758
+ return process.env.CODEX_HOME || join40(homedir2(), ".codex");
59473
59759
  }
59474
59760
  function opencodeConfigDir() {
59475
59761
  const xdg = process.env.XDG_CONFIG_HOME;
59476
- return join39(xdg && xdg !== "" ? xdg : join39(homedir2(), ".config"), "opencode");
59762
+ return join40(xdg && xdg !== "" ? xdg : join40(homedir2(), ".config"), "opencode");
59477
59763
  }
59478
59764
  function claudeSettingsPath(project, cwd = process.cwd()) {
59479
- return project ? join39(cwd, ".claude", "settings.json") : join39(homedir2(), ".claude", "settings.json");
59765
+ return project ? join40(cwd, ".claude", "settings.json") : join40(homedir2(), ".claude", "settings.json");
59480
59766
  }
59481
59767
  function pinnedEnginePath() {
59482
- return join39(homedir2(), ".ultra11y", "bin", "ultra11y.mjs");
59768
+ return join40(homedir2(), ".ultra11y", "bin", "ultra11y.mjs");
59483
59769
  }
59484
59770
  function installedCliCommand() {
59485
59771
  const override = process.env.ULTRA11Y_BIN;
@@ -59494,7 +59780,7 @@ function installedCliCommand() {
59494
59780
  if (source && !ephemeral) return `node ${JSON.stringify(source)}`;
59495
59781
  const pin = pinnedEnginePath();
59496
59782
  if (source) {
59497
- mkdirSync12(join39(pin, ".."), { recursive: true });
59783
+ mkdirSync13(join40(pin, ".."), { recursive: true });
59498
59784
  copyFileSync3(source, pin);
59499
59785
  }
59500
59786
  return `node ${JSON.stringify(pin)}`;
@@ -59502,37 +59788,37 @@ function installedCliCommand() {
59502
59788
  function packageRoot() {
59503
59789
  let dir;
59504
59790
  try {
59505
- dir = dirname11(realpathSync3(process.argv[1] ?? ""));
59791
+ dir = dirname12(realpathSync3(process.argv[1] ?? ""));
59506
59792
  } catch {
59507
59793
  return null;
59508
59794
  }
59509
59795
  for (let i2 = 0; i2 < 6 && dir && dir !== "/"; i2++) {
59510
- if (existsSync26(join39(dir, "skills", "review-a11y", "SKILL.md"))) return dir;
59511
- dir = dirname11(dir);
59796
+ if (existsSync27(join40(dir, "skills", "review-a11y", "SKILL.md"))) return dir;
59797
+ dir = dirname12(dir);
59512
59798
  }
59513
59799
  return null;
59514
59800
  }
59515
59801
  function bundledSkillsDir() {
59516
59802
  const root = packageRoot();
59517
- const p = root ? join39(root, "skills") : null;
59518
- return p && existsSync26(p) ? p : null;
59803
+ const p = root ? join40(root, "skills") : null;
59804
+ return p && existsSync27(p) ? p : null;
59519
59805
  }
59520
59806
  function bundledOpencodePlugin() {
59521
59807
  const root = packageRoot();
59522
- const p = root ? join39(root, ".opencode", "plugins", "ultra11y.js") : null;
59523
- return p && existsSync26(p) ? p : null;
59808
+ const p = root ? join40(root, ".opencode", "plugins", "ultra11y.js") : null;
59809
+ return p && existsSync27(p) ? p : null;
59524
59810
  }
59525
59811
  function copySkillsInto(source, dest) {
59526
59812
  const out2 = [];
59527
59813
  for (const name2 of ["ultra11y", "review-a11y"]) {
59528
- const from = join39(source, name2);
59529
- if (!existsSync26(from)) continue;
59530
- const to = join39(dest, name2);
59531
- const same = ["SKILL.md", join39("scripts", "ultra11y.mjs")].every((f) => {
59532
- const a = join39(from, f);
59533
- const b = join39(to, f);
59814
+ const from = join40(source, name2);
59815
+ if (!existsSync27(from)) continue;
59816
+ const to = join40(dest, name2);
59817
+ const same = ["SKILL.md", join40("scripts", "ultra11y.mjs")].every((f) => {
59818
+ const a = join40(from, f);
59819
+ const b = join40(to, f);
59534
59820
  try {
59535
- return existsSync26(b) && statSync10(a).size === statSync10(b).size && readFileSync23(a).equals(readFileSync23(b));
59821
+ return existsSync27(b) && statSync10(a).size === statSync10(b).size && readFileSync24(a).equals(readFileSync24(b));
59536
59822
  } catch {
59537
59823
  return false;
59538
59824
  }
@@ -59582,7 +59868,7 @@ function featuresBody(content) {
59582
59868
  function installCodex({ codexDir, command, skillsSource }) {
59583
59869
  const reports = [];
59584
59870
  reports.push(
59585
- editJsonFile(join40(codexDir, "hooks.json"), (root) => {
59871
+ editJsonFile(join41(codexDir, "hooks.json"), (root) => {
59586
59872
  const hooks = root.hooks ??= {};
59587
59873
  hooks.PreToolUse = [
59588
59874
  ...withoutOurs2(hooks.PreToolUse),
@@ -59590,11 +59876,11 @@ function installCodex({ codexDir, command, skillsSource }) {
59590
59876
  ];
59591
59877
  })
59592
59878
  );
59593
- const configPath = join40(codexDir, "config.toml");
59594
- const current = existsSync27(configPath) ? readFileSync24(configPath, "utf8") : "";
59879
+ const configPath = join41(codexDir, "config.toml");
59880
+ const current = existsSync28(configPath) ? readFileSync25(configPath, "utf8") : "";
59595
59881
  const edit = enableHooksFeature(current);
59596
59882
  if (edit.changed) reports.push(writeTextWithBackup(configPath, edit.content));
59597
- if (skillsSource) reports.push(...copySkillsInto(skillsSource, join40(codexDir, "skills")));
59883
+ if (skillsSource) reports.push(...copySkillsInto(skillsSource, join41(codexDir, "skills")));
59598
59884
  return {
59599
59885
  reports,
59600
59886
  guidance: [
@@ -59609,8 +59895,8 @@ function installCodex({ codexDir, command, skillsSource }) {
59609
59895
  }
59610
59896
  function uninstallCodex({ codexDir }) {
59611
59897
  const reports = [];
59612
- const hooksPath = join40(codexDir, "hooks.json");
59613
- if (existsSync27(hooksPath)) {
59898
+ const hooksPath = join41(codexDir, "hooks.json");
59899
+ if (existsSync28(hooksPath)) {
59614
59900
  reports.push(
59615
59901
  editJsonFile(hooksPath, (root) => {
59616
59902
  const hooks = root.hooks;
@@ -59625,33 +59911,33 @@ function uninstallCodex({ codexDir }) {
59625
59911
  return { reports, guidance: ["codex: `[features] hooks = true` was left enabled \u2014 other hooks may depend on it."] };
59626
59912
  }
59627
59913
  function codexWired(codexDir) {
59628
- const root = readJsonSafe(join40(codexDir, "hooks.json"));
59914
+ const root = readJsonSafe(join41(codexDir, "hooks.json"));
59629
59915
  const groups = root?.hooks?.PreToolUse;
59630
59916
  if (!Array.isArray(groups)) return 0;
59631
59917
  return groups.reduce((n, g) => n + (g.hooks ?? []).filter((h) => String(h.command ?? "").includes(CODEX_MARKER)).length, 0);
59632
59918
  }
59633
59919
  function codexHooksEnabled(codexDir) {
59634
- const p = join40(codexDir, "config.toml");
59635
- return existsSync27(p) && HOOKS_ON.test(featuresBody(readFileSync24(p, "utf8")));
59920
+ const p = join41(codexDir, "config.toml");
59921
+ return existsSync28(p) && HOOKS_ON.test(featuresBody(readFileSync25(p, "utf8")));
59636
59922
  }
59637
59923
 
59638
59924
  // src/install/opencode.ts
59639
- import { existsSync as existsSync28, readFileSync as readFileSync25, rmSync as rmSync6 } from "fs";
59640
- import { join as join41 } from "path";
59925
+ import { existsSync as existsSync29, readFileSync as readFileSync26, rmSync as rmSync6 } from "fs";
59926
+ import { join as join42 } from "path";
59641
59927
  var OPENCODE_MARKER = "ULTRA11Y_OPENCODE_PLUGIN";
59642
59928
  function pluginTarget(configDir) {
59643
- return join41(configDir, "plugin", "ultra11y.js");
59929
+ return join42(configDir, "plugin", "ultra11y.js");
59644
59930
  }
59645
59931
  function installOpencode({ configDir, pluginSource, skillsSource, enginePath }) {
59646
59932
  const target = pluginTarget(configDir);
59647
- if (existsSync28(target) && !readFileSync25(target, "utf8").includes(OPENCODE_MARKER)) {
59933
+ if (existsSync29(target) && !readFileSync26(target, "utf8").includes(OPENCODE_MARKER)) {
59648
59934
  throw new Error(`refusing to overwrite ${target}: it is not a file ultra11y wrote. Remove it manually if you want the plugin here.`);
59649
59935
  }
59650
- const reports = [writeTextWithBackup(target, readFileSync25(pluginSource, "utf8"))];
59651
- if (enginePath && existsSync28(enginePath)) {
59652
- reports.push(writeTextWithBackup(join41(configDir, "plugin", "ultra11y.mjs"), readFileSync25(enginePath, "utf8")));
59936
+ const reports = [writeTextWithBackup(target, readFileSync26(pluginSource, "utf8"))];
59937
+ if (enginePath && existsSync29(enginePath)) {
59938
+ reports.push(writeTextWithBackup(join42(configDir, "plugin", "ultra11y.mjs"), readFileSync26(enginePath, "utf8")));
59653
59939
  }
59654
- if (skillsSource) reports.push(...copySkillsInto(skillsSource, join41(configDir, "skills")));
59940
+ if (skillsSource) reports.push(...copySkillsInto(skillsSource, join42(configDir, "skills")));
59655
59941
  return {
59656
59942
  reports,
59657
59943
  guidance: [
@@ -59664,13 +59950,13 @@ function installOpencode({ configDir, pluginSource, skillsSource, enginePath })
59664
59950
  function uninstallOpencode({ configDir }) {
59665
59951
  const reports = [];
59666
59952
  const target = pluginTarget(configDir);
59667
- if (existsSync28(target) && readFileSync25(target, "utf8").includes(OPENCODE_MARKER)) {
59953
+ if (existsSync29(target) && readFileSync26(target, "utf8").includes(OPENCODE_MARKER)) {
59668
59954
  rmSync6(target, { force: true });
59669
- rmSync6(join41(configDir, "plugin", "ultra11y.mjs"), { force: true });
59955
+ rmSync6(join42(configDir, "plugin", "ultra11y.mjs"), { force: true });
59670
59956
  reports.push({ path: target, changed: true });
59671
59957
  }
59672
- const configPath = join41(configDir, "opencode.json");
59673
- if (existsSync28(configPath)) {
59958
+ const configPath = join42(configDir, "opencode.json");
59959
+ if (existsSync29(configPath)) {
59674
59960
  reports.push(
59675
59961
  editJsonFile(configPath, (root) => {
59676
59962
  if (!Array.isArray(root.plugin)) return;
@@ -59684,9 +59970,9 @@ function uninstallOpencode({ configDir }) {
59684
59970
  }
59685
59971
  function opencodeWired(configDir) {
59686
59972
  const target = pluginTarget(configDir);
59687
- if (existsSync28(target) && readFileSync25(target, "utf8").includes(OPENCODE_MARKER)) return true;
59973
+ if (existsSync29(target) && readFileSync26(target, "utf8").includes(OPENCODE_MARKER)) return true;
59688
59974
  try {
59689
- const cfg = JSON.parse(readFileSync25(join41(configDir, "opencode.json"), "utf8"));
59975
+ const cfg = JSON.parse(readFileSync26(join42(configDir, "opencode.json"), "utf8"));
59690
59976
  return Array.isArray(cfg.plugin) && cfg.plugin.some((p) => /^ultra11y(@|$)/.test(String(p)));
59691
59977
  } catch {
59692
59978
  return false;
@@ -59694,8 +59980,8 @@ function opencodeWired(configDir) {
59694
59980
  }
59695
59981
 
59696
59982
  // src/install/index.ts
59697
- import { existsSync as existsSync29 } from "fs";
59698
- import { join as join42 } from "path";
59983
+ import { existsSync as existsSync30 } from "fs";
59984
+ import { join as join43 } from "path";
59699
59985
  var ALL_TARGETS = ["claude-code", "codex", "opencode"];
59700
59986
  function parseTargets(flags2) {
59701
59987
  const picked = /* @__PURE__ */ new Set();
@@ -59734,7 +60020,7 @@ function installForTargets(opts) {
59734
60020
  const out3 = installCodex({ codexDir: codexHome(), command: cmd(), skillsSource: opts.skills === false ? null : bundledSkillsDir() });
59735
60021
  r.reports.push(...out3.reports);
59736
60022
  r.guidance.push(...out3.guidance);
59737
- } else r.reports.push({ path: join42(codexHome(), "hooks.json"), changed: false });
60023
+ } else r.reports.push({ path: join43(codexHome(), "hooks.json"), changed: false });
59738
60024
  break;
59739
60025
  }
59740
60026
  case "opencode": {
@@ -59743,18 +60029,18 @@ function installForTargets(opts) {
59743
60029
  r.error = "opencode: this engine was not run from a package carrying .opencode/plugins/ultra11y.js \u2014 install ultra11y from npm and retry.";
59744
60030
  break;
59745
60031
  }
59746
- const configDir = opts.project === true ? join42(cwd, ".opencode") : opencodeConfigDir();
60032
+ const configDir = opts.project === true ? join43(cwd, ".opencode") : opencodeConfigDir();
59747
60033
  if (!opts.dryRun) {
59748
- const engine = existsSync29(pinnedEnginePath()) ? pinnedEnginePath() : join42(packageRoot() ?? "", "scripts", "ultra11y.mjs");
60034
+ const engine = existsSync30(pinnedEnginePath()) ? pinnedEnginePath() : join43(packageRoot() ?? "", "scripts", "ultra11y.mjs");
59749
60035
  const out3 = installOpencode({
59750
60036
  configDir,
59751
60037
  pluginSource: source,
59752
60038
  skillsSource: opts.skills === false ? null : bundledSkillsDir(),
59753
- enginePath: existsSync29(engine) ? engine : null
60039
+ enginePath: existsSync30(engine) ? engine : null
59754
60040
  });
59755
60041
  r.reports.push(...out3.reports);
59756
60042
  r.guidance.push(...out3.guidance);
59757
- } else r.reports.push({ path: join42(configDir, "plugin", "ultra11y.js"), changed: false });
60043
+ } else r.reports.push({ path: join43(configDir, "plugin", "ultra11y.js"), changed: false });
59758
60044
  break;
59759
60045
  }
59760
60046
  case "agents-md": {
@@ -59792,7 +60078,7 @@ function uninstallForTargets(opts) {
59792
60078
  break;
59793
60079
  }
59794
60080
  case "opencode": {
59795
- const out3 = uninstallOpencode({ configDir: opts.project === true ? join42(cwd, ".opencode") : opencodeConfigDir() });
60081
+ const out3 = uninstallOpencode({ configDir: opts.project === true ? join43(cwd, ".opencode") : opencodeConfigDir() });
59796
60082
  r.reports.push(...out3.reports);
59797
60083
  r.guidance.push(...out3.guidance);
59798
60084
  break;
@@ -59813,18 +60099,18 @@ function statusReport(opts = {}) {
59813
60099
  const claudePath = claudeSettingsPath(opts.project === true, cwd);
59814
60100
  const root = projectRoot(cwd);
59815
60101
  const codexDir = codexHome();
59816
- const ocDir = opts.project === true ? join42(cwd, ".opencode") : opencodeConfigDir();
60102
+ const ocDir = opts.project === true ? join43(cwd, ".opencode") : opencodeConfigDir();
59817
60103
  const codexOn = codexWired(codexDir) > 0;
59818
60104
  return [
59819
60105
  { target: "claude-code", wired: claudeCodeWired(claudePath) > 0, path: claudePath, note: "the plugin route wires this separately and does not show here" },
59820
60106
  {
59821
60107
  target: "codex",
59822
60108
  wired: codexOn,
59823
- path: join42(codexDir, "hooks.json"),
60109
+ path: join43(codexDir, "hooks.json"),
59824
60110
  // A wired hook behind a disabled feature flag never fires and looks fine. Say so.
59825
60111
  note: codexOn && !codexHooksEnabled(codexDir) ? "WIRED BUT INERT: `[features] hooks = true` is not set in config.toml" : void 0
59826
60112
  },
59827
- { target: "opencode", wired: opencodeWired(ocDir), path: join42(ocDir, "plugin", "ultra11y.js") },
60113
+ { target: "opencode", wired: opencodeWired(ocDir), path: join43(ocDir, "plugin", "ultra11y.js") },
59828
60114
  { target: "agents-md", wired: agentsMdWired(root), path: agentsMdPath(root) }
59829
60115
  ];
59830
60116
  }
@@ -60359,9 +60645,9 @@ function pagesComment(result, opts = {}) {
60359
60645
  }
60360
60646
 
60361
60647
  // src/evidence.ts
60362
- import { createHash as createHash6 } from "crypto";
60363
- import { existsSync as existsSync30, mkdirSync as mkdirSync13, readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "fs";
60364
- import { join as join43 } from "path";
60648
+ import { createHash as createHash7 } from "crypto";
60649
+ import { existsSync as existsSync31, mkdirSync as mkdirSync14, readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "fs";
60650
+ import { join as join44 } from "path";
60365
60651
  var DEFAULT_GEOMETRY = { pad: 24, minWidth: 320, minHeight: 120, maxWidth: 960, maxHeight: 540 };
60366
60652
  var DEFAULT_CAPS = { perRule: 6, perPage: 12, total: 200 };
60367
60653
  var HALO = { r: 255, g: 255, b: 255, a: 0.92 };
@@ -60378,7 +60664,7 @@ function pageIdOfSnapshot(file) {
60378
60664
  }
60379
60665
  function readJson3(file) {
60380
60666
  try {
60381
- return JSON.parse(readFileSync26(file, "utf8"));
60667
+ return JSON.parse(readFileSync27(file, "utf8"));
60382
60668
  } catch {
60383
60669
  return void 0;
60384
60670
  }
@@ -60394,21 +60680,21 @@ function resolveScale(img, viewport, boxes) {
60394
60680
  }
60395
60681
  function loadPageEvidence(root, pageId) {
60396
60682
  const dir = snapshotDir(root, pageId);
60397
- const domFile = join43(dir, "dom.html");
60398
- const shotFile = join43(dir, "screen.png");
60399
- if (!existsSync30(domFile)) return { skip: "no-snapshot" };
60400
- if (!existsSync30(shotFile)) return { skip: "no-screenshot" };
60683
+ const domFile = join44(dir, "dom.html");
60684
+ const shotFile = join44(dir, "screen.png");
60685
+ if (!existsSync31(domFile)) return { skip: "no-snapshot" };
60686
+ if (!existsSync31(shotFile)) return { skip: "no-screenshot" };
60401
60687
  let dom;
60402
60688
  let shot;
60403
60689
  try {
60404
- dom = readFileSync26(domFile, "utf8");
60405
- shot = readFileSync26(shotFile);
60690
+ dom = readFileSync27(domFile, "utf8");
60691
+ shot = readFileSync27(shotFile);
60406
60692
  } catch {
60407
60693
  return { skip: "no-snapshot" };
60408
60694
  }
60409
60695
  const img = decodePng(shot);
60410
60696
  if (!img) return { skip: "unreadable-image" };
60411
- const digest = readJson3(join43(dir, "boxes.json"));
60697
+ const digest = readJson3(join44(dir, "boxes.json"));
60412
60698
  if (!digest) return { skip: "no-boxes" };
60413
60699
  const doc = parseHtml(dom, domFile);
60414
60700
  const boxes = alignedBoxes(dom, digest, doc);
@@ -60424,7 +60710,7 @@ function loadPageEvidence(root, pageId) {
60424
60710
  return { pageId, img, scale, viewport, ordinalOf, boxes, truncated: Boolean(digest.truncated) };
60425
60711
  }
60426
60712
  function metaOf(dir) {
60427
- const raw = readJson3(join43(dir, "meta.json"));
60713
+ const raw = readJson3(join44(dir, "meta.json"));
60428
60714
  if (raw === void 0) return void 0;
60429
60715
  const v = validateSnapshotMeta(raw);
60430
60716
  return v.ok && v.meta ? v.meta : void 0;
@@ -60556,7 +60842,7 @@ function bump(t2, reason) {
60556
60842
  t2.skipped[reason] = (t2.skipped[reason] ?? 0) + 1;
60557
60843
  }
60558
60844
  function cropName(f) {
60559
- return `${createHash6("sha1").update(findingId(f)).digest("hex").slice(0, 12)}.png`;
60845
+ return `${createHash7("sha1").update(findingId(f)).digest("hex").slice(0, 12)}.png`;
60560
60846
  }
60561
60847
  function writeEvidence(result, opts) {
60562
60848
  const root = opts.root ?? process.cwd();
@@ -60589,7 +60875,7 @@ function writeEvidence(result, opts) {
60589
60875
  }
60590
60876
  continue;
60591
60877
  }
60592
- const dir = join43(opts.outDir, "assets", pageId);
60878
+ const dir = join44(opts.outDir, "assets", pageId);
60593
60879
  let wrote = false;
60594
60880
  const perRule = /* @__PURE__ */ new Map();
60595
60881
  const seen = /* @__PURE__ */ new Set();
@@ -60614,13 +60900,13 @@ function writeEvidence(result, opts) {
60614
60900
  continue;
60615
60901
  }
60616
60902
  const name2 = cropName(f);
60617
- const path = join43(dir, name2);
60903
+ const path = join44(dir, name2);
60618
60904
  try {
60619
60905
  if (!wrote) {
60620
- mkdirSync13(dir, { recursive: true });
60906
+ mkdirSync14(dir, { recursive: true });
60621
60907
  wrote = true;
60622
60908
  }
60623
- writeFileSync15(path, crop.png);
60909
+ writeFileSync16(path, crop.png);
60624
60910
  } catch {
60625
60911
  manifest.skipped.set(id, "no-screenshot");
60626
60912
  bump(tally, "no-screenshot");
@@ -60652,8 +60938,8 @@ function writeEvidence(result, opts) {
60652
60938
  }
60653
60939
 
60654
60940
  // src/html-emit.ts
60655
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync27, statSync as statSync11, writeFileSync as writeFileSync16 } from "fs";
60656
- import { join as join44 } from "path";
60941
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync28, statSync as statSync11, writeFileSync as writeFileSync17 } from "fs";
60942
+ import { join as join45 } from "path";
60657
60943
 
60658
60944
  // src/html.ts
60659
60945
  var MARK4 = { C: "C", NC: "NC", NA: "\u2014", manual: "?" };
@@ -61215,7 +61501,7 @@ function inlineSize(path) {
61215
61501
  }
61216
61502
  function dataUri(path) {
61217
61503
  try {
61218
- return `data:image/png;base64,${readFileSync27(path).toString("base64")}`;
61504
+ return `data:image/png;base64,${readFileSync28(path).toString("base64")}`;
61219
61505
  } catch {
61220
61506
  return void 0;
61221
61507
  }
@@ -61254,7 +61540,7 @@ function writeHtml(result, opts) {
61254
61540
  const t2 = T5[lang];
61255
61541
  const budget = opts.inlineBudget ?? DEFAULT_INLINE_BUDGET;
61256
61542
  const stdKey = isCore(standard) ? "wcag" : loadPack(standard).key;
61257
- mkdirSync14(opts.outDir, { recursive: true });
61543
+ mkdirSync15(opts.outDir, { recursive: true });
61258
61544
  const crops = [...opts.evidence?.crops.values() ?? []];
61259
61545
  const shots = [...opts.screenshots?.values() ?? []];
61260
61546
  const rung = pickRung(
@@ -61286,7 +61572,7 @@ function writeHtml(result, opts) {
61286
61572
  return drawn;
61287
61573
  };
61288
61574
  const flat = opts.layout === "pages";
61289
- const sheetDir = flat ? opts.outDir : join44(opts.outDir, "pages");
61575
+ const sheetDir = flat ? opts.outDir : join45(opts.outDir, "pages");
61290
61576
  const up = flat ? "./" : "../";
61291
61577
  const compositeName = `ultra11y-${stdKey}-${result.date}.html`;
61292
61578
  const nav = flat ? [{ href: "./index.html", text: t2.pagesTitle }] : [
@@ -61305,26 +61591,26 @@ function writeHtml(result, opts) {
61305
61591
  nav: nav.map((n) => ({ ...n, current: n.href === `./${compositeName}` }))
61306
61592
  });
61307
61593
  if (notices.length) composite.blocks.unshift({ kind: "note", tone: "warn", runs: noticeRuns(notices) });
61308
- compositePath = join44(opts.outDir, compositeName);
61309
- writeFileSync16(compositePath, renderHtmlDocument(composite));
61594
+ compositePath = join45(opts.outDir, compositeName);
61595
+ writeFileSync17(compositePath, renderHtmlDocument(composite));
61310
61596
  }
61311
61597
  const fileCrops = cropLookup(opts.evidence, lang, (_p, href) => href.replace(/^\.\//, up));
61312
61598
  const indexNav = nav.map((n) => ({ ...n, current: n.href === "./index.html" }));
61313
61599
  const index = flat ? pagesIndexDoc(result, { standard, lang, nav: indexNav, sheetHref: (id) => `./page-${id}.html` }) : indexDoc(result, { standard, lang, nav: indexNav, links: nav.filter((n) => n.href !== "./index.html") });
61314
- const indexPath = join44(opts.outDir, "index.html");
61315
- writeFileSync16(indexPath, renderHtmlDocument(index));
61600
+ const indexPath = join45(opts.outDir, "index.html");
61601
+ writeFileSync17(indexPath, renderHtmlDocument(index));
61316
61602
  const sheets = [];
61317
61603
  if (opts.pages) {
61318
61604
  const derived = derivePages(result, pagesOf(result));
61319
- mkdirSync14(sheetDir, { recursive: true });
61605
+ mkdirSync15(sheetDir, { recursive: true });
61320
61606
  const sheetNav = flat ? [{ href: "./index.html", text: t2.pagesTitle }] : [
61321
61607
  { href: `${up}index.html`, text: t2.indexTitle },
61322
61608
  { href: `${up}${compositeName}`, text: t2.docTitle },
61323
61609
  { href: "./index.html", text: t2.pagesTitle }
61324
61610
  ];
61325
61611
  if (!flat) {
61326
- writeFileSync16(
61327
- join44(sheetDir, "index.html"),
61612
+ writeFileSync17(
61613
+ join45(sheetDir, "index.html"),
61328
61614
  renderHtmlDocument(
61329
61615
  pagesIndexDoc(result, {
61330
61616
  standard,
@@ -61334,7 +61620,7 @@ function writeHtml(result, opts) {
61334
61620
  })
61335
61621
  )
61336
61622
  );
61337
- sheets.push(join44(sheetDir, "index.html"));
61623
+ sheets.push(join45(sheetDir, "index.html"));
61338
61624
  }
61339
61625
  for (const p of derived) {
61340
61626
  const doc = pageDoc(result, p, {
@@ -61347,8 +61633,8 @@ function writeHtml(result, opts) {
61347
61633
  // beside the entry point, seen from wherever this sheet sits.
61348
61634
  ...opts.screenshots?.has(p.id) ? { screenshot: `${up}assets/${p.id}.png` } : {}
61349
61635
  });
61350
- const path = join44(sheetDir, `page-${p.id}.html`);
61351
- writeFileSync16(path, renderHtmlDocument(doc));
61636
+ const path = join45(sheetDir, `page-${p.id}.html`);
61637
+ writeFileSync17(path, renderHtmlDocument(doc));
61352
61638
  sheets.push(path);
61353
61639
  }
61354
61640
  }
@@ -61368,8 +61654,8 @@ function noticeRuns(notices) {
61368
61654
 
61369
61655
  // src/dev.ts
61370
61656
  import { createServer } from "http";
61371
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync28, writeFileSync as writeFileSync17 } from "fs";
61372
- import { dirname as dirname12, join as join45 } from "path";
61657
+ import { mkdirSync as mkdirSync16, readFileSync as readFileSync29, writeFileSync as writeFileSync18 } from "fs";
61658
+ import { dirname as dirname13, join as join46 } from "path";
61373
61659
  var DEV_DEFAULT_PORT = 4111;
61374
61660
  function criterionLabel3(f, standard) {
61375
61661
  if (isCore(standard)) return `WCAG ${f.criteriaId}`;
@@ -61591,13 +61877,13 @@ function auditCollected(root, payload) {
61591
61877
  ...payload.css ? { css: payload.css } : {},
61592
61878
  ...shot ? { screenshotBase64: shot } : {}
61593
61879
  });
61594
- return { ok: true, result: runAudit({ inputs: [join45(dir, "dom.html")] }) };
61880
+ return { ok: true, result: runAudit({ inputs: [join46(dir, "dom.html")] }) };
61595
61881
  }
61596
61882
  function projectPages(root) {
61597
61883
  const snaps = readSnapshots(root);
61598
61884
  if (!snaps.length) return { result: null, pages: [] };
61599
61885
  const scope = pageScopesFrom(snaps);
61600
- const result = runAudit({ inputs: [join45(root, ".ultra11y/pages")] });
61886
+ const result = runAudit({ inputs: [join46(root, ".ultra11y/pages")] });
61601
61887
  result.scope.pages = scope;
61602
61888
  attributePages(result, scope);
61603
61889
  foldRecordedAdjudication(root, result);
@@ -61611,7 +61897,7 @@ function judgePages(result) {
61611
61897
  function foldRecordedAdjudication(root, fresh) {
61612
61898
  let prior;
61613
61899
  try {
61614
- prior = JSON.parse(readFileSync28(join45(root, "audits", "audit-latest.json"), "utf8"));
61900
+ prior = JSON.parse(readFileSync29(join46(root, "audits", "audit-latest.json"), "utf8"));
61615
61901
  } catch {
61616
61902
  return;
61617
61903
  }
@@ -61726,9 +62012,9 @@ function startDevServer(opts) {
61726
62012
  });
61727
62013
  let auditPath;
61728
62014
  if (applied.ok) {
61729
- auditPath = join45(opts.root, "audits", "audit-latest.json");
61730
- mkdirSync15(dirname12(auditPath), { recursive: true });
61731
- writeFileSync17(auditPath, `${JSON.stringify(applied.audit, null, 2)}
62015
+ auditPath = join46(opts.root, "audits", "audit-latest.json");
62016
+ mkdirSync16(dirname13(auditPath), { recursive: true });
62017
+ writeFileSync18(auditPath, `${JSON.stringify(applied.audit, null, 2)}
61732
62018
  `);
61733
62019
  }
61734
62020
  res.writeHead(200, { "content-type": "application/json" }).end(
@@ -61755,10 +62041,10 @@ function startDevServer(opts) {
61755
62041
  }
61756
62042
  res.writeHead(404, { "content-type": "text/plain" }).end("not found");
61757
62043
  });
61758
- return new Promise((resolve16, reject) => {
62044
+ return new Promise((resolve17, reject) => {
61759
62045
  server.once("error", reject);
61760
62046
  server.listen(opts.port, "127.0.0.1", () => {
61761
- resolve16({
62047
+ resolve17({
61762
62048
  port: server.address().port,
61763
62049
  close: () => new Promise((r) => server.close(() => r()))
61764
62050
  });
@@ -62042,11 +62328,11 @@ function e2eSetupPlan(runners, paths, lang = "en") {
62042
62328
  }
62043
62329
 
62044
62330
  // src/orchestrate.ts
62045
- import { existsSync as existsSync31, mkdirSync as mkdirSync16, readFileSync as readFileSync29, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "fs";
62046
- import { join as join47, resolve as resolve12 } from "path";
62331
+ import { existsSync as existsSync32, mkdirSync as mkdirSync17, readFileSync as readFileSync30, rmSync as rmSync7, writeFileSync as writeFileSync19 } from "fs";
62332
+ import { join as join48, resolve as resolve13 } from "path";
62047
62333
 
62048
62334
  // src/orchestrate-templates.ts
62049
- import { join as join46 } from "path";
62335
+ import { join as join47 } from "path";
62050
62336
  var ONE_WRITER_FOOTER = `
62051
62337
  ## Return, don't write
62052
62338
 
@@ -62132,7 +62418,7 @@ var PHASE_SPECS = {
62132
62418
  title: "Adjudicate",
62133
62419
  schema: ADJUDICATE_SCHEMA,
62134
62420
  description: (n) => `Adjudicate the ${n} residual judgment criterion(ia) of an ultra11y audit (fan-out, fail-closed fold)`,
62135
- applyHint: (engine, worklist, run2) => `node ${engine} verify --apply ${worklist} --in ${join46(run2, "audit-latest.json")} --out ${run2}`
62421
+ applyHint: (engine, worklist, run2) => `node ${engine} verify --apply ${worklist} --in ${join47(run2, "audit-latest.json")} --out ${run2}`
62136
62422
  },
62137
62423
  "verify-report": {
62138
62424
  role: "refuter",
@@ -62154,7 +62440,7 @@ function toBatches(ids, batchSize) {
62154
62440
  }
62155
62441
  function phaseWorkflowScript(ph, runAbs, engineAbs, batchSize) {
62156
62442
  const spec = phaseSpec(ph.name);
62157
- const scriptPath = join46(runAbs, "orchestration", `${ph.name}.workflow.mjs`);
62443
+ const scriptPath = join47(runAbs, "orchestration", `${ph.name}.workflow.mjs`);
62158
62444
  const meta2 = { name: `ultra11y-${ph.name}`, description: spec.description(ph.items), phases: [{ title: spec.title }] };
62159
62445
  return [
62160
62446
  `export const meta = ${JSON.stringify(meta2)}`,
@@ -62198,7 +62484,7 @@ function agentContracts(runAbs, engineAbs) {
62198
62484
 
62199
62485
  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.
62200
62486
 
62201
- Worklist: \`${join46(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>\`).
62487
+ Worklist: \`${join47(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>\`).
62202
62488
 
62203
62489
  For EACH of your criteria:
62204
62490
 
@@ -62210,13 +62496,13 @@ For EACH of your criteria:
62210
62496
  - \`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).
62211
62497
  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.
62212
62498
 
62213
- Return (structured output): \`{ "verdicts": [{ "criteriaId", "verdict", "justification", "citations", "reason", "findings" }] }\` \u2014 your ITEMS only, every field grounded in what you actually read, every NC finding carrying its \`normativeRef\`, and every C (plus every evidenced NA) carrying its \`citations\`. A verdict that clears a criterion without naming what it cleared is refused by the fold, and one refusal discards the WHOLE adjudication \u2014 including every other verdict you got right.
62499
+ Return (structured output): \`{ "verdicts": [{ "criteriaId", "verdict", "justification", "citations", "reason", "findings" }] }\` \u2014 your ITEMS only, every field grounded in what you actually read, every NC finding carrying its \`normativeRef\`, and every C (plus every evidenced NA) carrying its \`citations\`. A verdict that clears a criterion without naming what it cleared is refused by the fold; the refusal costs THAT criterion \u2014 which stays \xAB to assess \xBB carrying the reason \u2014 and leaves every other verdict standing. So never guess to fill a gap: an honest \`manual\` is worth more than a verdict the gate throws away.
62214
62500
  ${footer}`,
62215
62501
  refuter: `# Contract: refuter
62216
62502
 
62217
62503
  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.
62218
62504
 
62219
- Worklist: \`${join46(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>\`).
62505
+ Worklist: \`${join47(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>\`).
62220
62506
 
62221
62507
  For EACH of your entries:
62222
62508
 
@@ -62252,14 +62538,14 @@ ${status}
62252
62538
 
62253
62539
  ## The loop (play every role yourself, one item at a time)
62254
62540
 
62255
- 1. **Audit** (if not done): \`${engine} audit "<globs>" --graph --out ${runAbs}\` \u2192 \`${join46(runAbs, "audit-latest.json")}\`.
62256
- 2. **Adjudicate the residual criteria** \u2014 \`${engine} verify --manual --in ${join46(runAbs, "audit-latest.json")} --out ${runAbs}\` writes \`${join46(runAbs, "ADJUDICATE.todo.json")}\`. For EVERY item, apply \`${join46(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 ${join46(runAbs, "ADJUDICATE.todo.json")} --in ${join46(runAbs, "audit-latest.json")} --out ${runAbs}\`.
62257
- 3. **Report**: \`${engine} report --in ${join46(runAbs, "audit-latest.json")} --out ${runAbs}\`.
62258
- 4. **Verify the report's claims** \u2014 \`${engine} verify --report <the report .md> --out ${runAbs}\` writes \`${join46(runAbs, "VERIFY.todo.json")}\`. For EVERY entry, apply \`${join46(runAbs, "orchestration", "agents", "refuter.md")}\` yourself (open file:line, verdict supported/partial/refuted/unsupported + note IN the todo file). Then: \`${engine} verify --apply ${join46(runAbs, "VERIFY.todo.json")} --report <the report .md>\`.
62541
+ 1. **Audit** (if not done): \`${engine} audit "<globs>" --graph --out ${runAbs}\` \u2192 \`${join47(runAbs, "audit-latest.json")}\`.
62542
+ 2. **Adjudicate the residual criteria** \u2014 \`${engine} verify --manual --in ${join47(runAbs, "audit-latest.json")} --out ${runAbs}\` writes \`${join47(runAbs, "ADJUDICATE.todo.json")}\`. For EVERY item, apply \`${join47(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 \u2014 gated per verdict, so a refusal costs its own criterion and no other: \`${engine} verify --apply ${join47(runAbs, "ADJUDICATE.todo.json")} --in ${join47(runAbs, "audit-latest.json")} --out ${runAbs}\`. Add \`--ledger .ultra11y/verdicts/<standard>.json\` to RECORD the verdicts that landed, so CI can replay them without a model; \`--strict\` restores the old all-or-nothing fold.
62543
+ 3. **Report**: \`${engine} report --in ${join47(runAbs, "audit-latest.json")} --out ${runAbs}\`.
62544
+ 4. **Verify the report's claims** \u2014 \`${engine} verify --report <the report .md> --out ${runAbs}\` writes \`${join47(runAbs, "VERIFY.todo.json")}\`. For EVERY entry, apply \`${join47(runAbs, "orchestration", "agents", "refuter.md")}\` yourself (open file:line, verdict supported/partial/refuted/unsupported + note IN the todo file). Then: \`${engine} verify --apply ${join47(runAbs, "VERIFY.todo.json")} --report <the report .md>\`.
62259
62545
  5. **Gate**: \`${engine} check --report <the report .md> --semantic\` must exit 0 before presenting anything.
62260
62546
  6. **Fix & re-audit**: \`${engine} fix <globs> --write --iterate\`, hand-apply the judgment fixes, then loop from step 1 until the gate stays green.
62261
62547
 
62262
- With subagents available, prefer the emitted workflows instead: \`orchestrate --run ${runAbs} --phase <p>\` then \`Workflow({ scriptPath: "${join46(runAbs, "orchestration", "<p>.workflow.mjs")}" })\` \u2014 you stay the sole writer either way.
62548
+ With subagents available, prefer the emitted workflows instead: \`orchestrate --run ${runAbs} --phase <p>\` then \`Workflow({ scriptPath: "${join47(runAbs, "orchestration", "<p>.workflow.mjs")}" })\` \u2014 you stay the sole writer either way.
62263
62549
  `;
62264
62550
  }
62265
62551
 
@@ -62268,13 +62554,13 @@ var PHASES = ["adjudicate", "verify-report"];
62268
62554
  var SMALL_WORKLIST = 3;
62269
62555
  var BATCH_SIZE2 = 8;
62270
62556
  function listPhases(runDir, engineAbs) {
62271
- const run2 = resolve12(runDir);
62272
- const adjPath = join47(run2, "ADJUDICATE.todo.json");
62557
+ const run2 = resolve13(runDir);
62558
+ const adjPath = join48(run2, "ADJUDICATE.todo.json");
62273
62559
  let adjIds = [];
62274
62560
  let adjReady = false;
62275
- if (existsSync31(adjPath)) {
62561
+ if (existsSync32(adjPath)) {
62276
62562
  try {
62277
- const f = JSON.parse(readFileSync29(adjPath, "utf8"));
62563
+ const f = JSON.parse(readFileSync30(adjPath, "utf8"));
62278
62564
  if (f && f.kind === "adjudication" && Array.isArray(f.items)) {
62279
62565
  adjReady = true;
62280
62566
  adjIds = f.items.map((i2) => i2.criteriaId);
@@ -62282,12 +62568,12 @@ function listPhases(runDir, engineAbs) {
62282
62568
  } catch {
62283
62569
  }
62284
62570
  }
62285
- const verPath = join47(run2, "VERIFY.todo.json");
62571
+ const verPath = join48(run2, "VERIFY.todo.json");
62286
62572
  let verIds = [];
62287
62573
  let verReady = false;
62288
- if (existsSync31(verPath)) {
62574
+ if (existsSync32(verPath)) {
62289
62575
  try {
62290
- const items = JSON.parse(readFileSync29(verPath, "utf8"));
62576
+ const items = JSON.parse(readFileSync30(verPath, "utf8"));
62291
62577
  if (Array.isArray(items)) {
62292
62578
  verReady = true;
62293
62579
  verIds = items.map((i2) => String(i2.n));
@@ -62302,7 +62588,7 @@ function listPhases(runDir, engineAbs) {
62302
62588
  worklist: adjPath,
62303
62589
  items: adjIds.length,
62304
62590
  ids: adjIds,
62305
- prerequisite: `node ${engineAbs} verify --manual --in ${join47(run2, "audit-latest.json")} --out ${run2}`
62591
+ prerequisite: `node ${engineAbs} verify --manual --in ${join48(run2, "audit-latest.json")} --out ${run2}`
62306
62592
  },
62307
62593
  {
62308
62594
  name: "verify-report",
@@ -62315,8 +62601,8 @@ function listPhases(runDir, engineAbs) {
62315
62601
  ];
62316
62602
  }
62317
62603
  function orchestrateRun(runDir, engineAbs, opts = {}) {
62318
- const run2 = resolve12(runDir);
62319
- if (!existsSync31(run2)) {
62604
+ const run2 = resolve13(runDir);
62605
+ if (!existsSync32(run2)) {
62320
62606
  return { exitCode: 2, written: [], notices: [], errors: [`run dir not found: ${run2}`], phases: [] };
62321
62607
  }
62322
62608
  const phases = listPhases(run2, engineAbs);
@@ -62343,22 +62629,22 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
62343
62629
  }
62344
62630
  selected = [ph];
62345
62631
  }
62346
- const orchDir = join47(run2, "orchestration");
62347
- const agentsDir = join47(orchDir, "agents");
62348
- mkdirSync16(join47(orchDir, "out"), { recursive: true });
62349
- mkdirSync16(agentsDir, { recursive: true });
62632
+ const orchDir = join48(run2, "orchestration");
62633
+ const agentsDir = join48(orchDir, "agents");
62634
+ mkdirSync17(join48(orchDir, "out"), { recursive: true });
62635
+ mkdirSync17(agentsDir, { recursive: true });
62350
62636
  const written = [];
62351
62637
  const notices = [];
62352
62638
  for (const [name2, content] of Object.entries(agentContracts(run2, engineAbs))) {
62353
- const p = join47(agentsDir, `${name2}.md`);
62354
- writeFileSync18(p, content);
62639
+ const p = join48(agentsDir, `${name2}.md`);
62640
+ writeFileSync19(p, content);
62355
62641
  written.push(p);
62356
62642
  }
62357
62643
  const emitted = new Set(opts.eco ? [] : selected.filter((p) => p.items > 0).map((p) => p.name));
62358
62644
  for (const ph of phases) {
62359
62645
  if (emitted.has(ph.name)) continue;
62360
- const stale = join47(orchDir, `${ph.name}.workflow.mjs`);
62361
- if (existsSync31(stale)) {
62646
+ const stale = join48(orchDir, `${ph.name}.workflow.mjs`);
62647
+ if (existsSync32(stale)) {
62362
62648
  rmSync7(stale, { force: true });
62363
62649
  notices.push(`phase "${ph.name}": stale workflow removed \u2014 its worklist ${ph.ready ? "is now empty" : "no longer exists"}.`);
62364
62650
  }
@@ -62372,13 +62658,13 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
62372
62658
  if (ph.items <= SMALL_WORKLIST) {
62373
62659
  notices.push(`phase "${ph.name}": only ${ph.items} item(s) \u2014 the sequential --eco path is equivalent and cheaper.`);
62374
62660
  }
62375
- const p = join47(orchDir, `${ph.name}.workflow.mjs`);
62376
- writeFileSync18(p, phaseWorkflowScript(ph, run2, engineAbs, BATCH_SIZE2));
62661
+ const p = join48(orchDir, `${ph.name}.workflow.mjs`);
62662
+ writeFileSync19(p, phaseWorkflowScript(ph, run2, engineAbs, BATCH_SIZE2));
62377
62663
  written.push(p);
62378
62664
  }
62379
62665
  }
62380
- const rb = join47(orchDir, "RUNBOOK.md");
62381
- writeFileSync18(rb, runbookMd(phases, run2, engineAbs));
62666
+ const rb = join48(orchDir, "RUNBOOK.md");
62667
+ writeFileSync19(rb, runbookMd(phases, run2, engineAbs));
62382
62668
  written.push(rb);
62383
62669
  return { exitCode: 0, written, notices, errors: [], phases };
62384
62670
  }
@@ -62387,8 +62673,8 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
62387
62673
  import { createInterface as createInterface2 } from "readline";
62388
62674
 
62389
62675
  // src/mcp/handlers.ts
62390
- import { existsSync as existsSync32, readFileSync as readFileSync30, realpathSync as realpathSync4, statSync as statSync12 } from "fs";
62391
- import { isAbsolute as isAbsolute3, join as join48, resolve as resolve13, sep as sep5 } from "path";
62676
+ import { existsSync as existsSync33, readFileSync as readFileSync31, realpathSync as realpathSync4, statSync as statSync12 } from "fs";
62677
+ import { isAbsolute as isAbsolute3, join as join49, resolve as resolve14, sep as sep5 } from "path";
62392
62678
 
62393
62679
  // src/criteria-view.ts
62394
62680
  var CriteriaLookupError = class extends Error {
@@ -62865,8 +63151,8 @@ function positive(v, key) {
62865
63151
  function requiredCwd(args2, defaults) {
62866
63152
  const cwd = str2(args2.cwd) ?? defaults.defaultCwd;
62867
63153
  if (!cwd) throw new ToolError("`cwd` is required: an absolute path to the project root.");
62868
- const abs = resolve13(cwd);
62869
- if (!existsSync32(abs)) throw new ToolError(`project root not found: ${abs}`);
63154
+ const abs = resolve14(cwd);
63155
+ if (!existsSync33(abs)) throw new ToolError(`project root not found: ${abs}`);
62870
63156
  if (!statSync12(abs).isDirectory()) throw new ToolError(`\`cwd\` is not a directory: ${abs}`);
62871
63157
  return abs;
62872
63158
  }
@@ -62977,7 +63263,7 @@ function audit(args2, cwd) {
62977
63263
  process.chdir(cwd);
62978
63264
  const inputs = strArray2(args2.globs) ?? DEFAULT_GLOBS;
62979
63265
  const scopedToDiff = bool(args2.changed) || bool(args2.staged) || str2(args2.since) !== void 0;
62980
- const extra = [CAPTURES_DIR, PAGES_DIR].filter((d) => !scopedToDiff && existsSync32(d) && !inputs.includes(d));
63266
+ const extra = [CAPTURES_DIR, PAGES_DIR].filter((d) => !scopedToDiff && existsSync33(d) && !inputs.includes(d));
62981
63267
  const result = runAudit({
62982
63268
  inputs: [...inputs, ...extra],
62983
63269
  include: strArray2(args2.include),
@@ -63096,13 +63382,13 @@ function handlePackCheck(args2, cwd) {
63096
63382
  return { cwd, ...res };
63097
63383
  }
63098
63384
  function handleSampleCheck(cwd, standard) {
63099
- const file = join48(cwd, ".ultra11yrc.json");
63100
- if (!existsSync32(file)) {
63385
+ const file = join49(cwd, ".ultra11yrc.json");
63386
+ if (!existsSync33(file)) {
63101
63387
  throw new ToolError(`no .ultra11yrc.json at ${cwd} \u2014 a page sample must be declared before it can be linted.`);
63102
63388
  }
63103
63389
  let raw;
63104
63390
  try {
63105
- raw = JSON.parse(readFileSync30(file, "utf8"));
63391
+ raw = JSON.parse(readFileSync31(file, "utf8"));
63106
63392
  } catch (e) {
63107
63393
  throw new ToolError(`.ultra11yrc.json is not valid JSON: ${e.message}`);
63108
63394
  }
@@ -63193,10 +63479,10 @@ function handleInit(args2, cwd) {
63193
63479
  throw new ToolError(`\`fail_on\` must be one of: error, warning, notice (got "${failOnRaw}")`);
63194
63480
  }
63195
63481
  const failOn = failOnRaw;
63196
- const enginePath = resolve13("scripts/ultra11y.mjs");
63482
+ const enginePath = resolve14("scripts/ultra11y.mjs");
63197
63483
  const written = [];
63198
63484
  if (bool(args2.hook)) written.push(writeHook(cwd, enginePath, failOn));
63199
- if (bool(args2.ci)) written.push(join48(cwd, ".github/workflows/a11y.yml"));
63485
+ if (bool(args2.ci)) written.push(join49(cwd, ".github/workflows/a11y.yml"));
63200
63486
  if (!written.length) {
63201
63487
  throw new ToolError("nothing to install \u2014 pass hook:true and/or ci:true.");
63202
63488
  }
@@ -63216,8 +63502,8 @@ function reportText(args2, tool) {
63216
63502
  if (inline) return inline;
63217
63503
  if (!file) throw new ToolError(`\`report_text\` is required \u2014 the report markdown for ultra11y_${tool} to work on.`);
63218
63504
  if (!isAbsolute3(file)) throw new ToolError("`report_file` must be an absolute path.");
63219
- if (!existsSync32(file)) throw new ToolError(`report file not found: ${file}`);
63220
- return readFileSync30(file, "utf8");
63505
+ if (!existsSync33(file)) throw new ToolError(`report file not found: ${file}`);
63506
+ return readFileSync31(file, "utf8");
63221
63507
  }
63222
63508
  function handleCriteria(args2) {
63223
63509
  const lang = langOf(args2);
@@ -63284,7 +63570,7 @@ function handleMethod(args2) {
63284
63570
  function handleRead(args2, cwd) {
63285
63571
  const raw = str2(args2.path);
63286
63572
  if (!raw) throw new ToolError("`path` is required \u2014 relative to the project root, or an absolute path inside it.");
63287
- const target = isAbsolute3(raw) ? raw : join48(cwd, raw);
63573
+ const target = isAbsolute3(raw) ? raw : join49(cwd, raw);
63288
63574
  let real;
63289
63575
  try {
63290
63576
  real = realpathSync4(target);
@@ -63298,7 +63584,7 @@ function handleRead(args2, cwd) {
63298
63584
  const st = statSync12(real);
63299
63585
  if (!st.isFile()) throw new ToolError(`not a file: ${raw}`);
63300
63586
  if (st.size > MAX_READ_BYTES) throw new ToolError(`file is too large to read (${st.size} bytes): ${raw}`);
63301
- const lines = readFileSync30(real, "utf8").split("\n");
63587
+ const lines = readFileSync31(real, "utf8").split("\n");
63302
63588
  const total = lines.length;
63303
63589
  const start2 = Math.max(1, Math.floor(num2(args2.start_line) ?? 1));
63304
63590
  if (start2 > total) throw new ToolError(`start_line ${start2} is past the end of the file (${total} lines).`);
@@ -63953,25 +64239,25 @@ function str3(v) {
63953
64239
  var DECLARED = new Set([...TOOLS2, ...WRITE_TOOLS].map((t2) => t2.name));
63954
64240
 
63955
64241
  // src/mcp/resources.ts
63956
- import { existsSync as existsSync33, readdirSync as readdirSync6, readFileSync as readFileSync31, realpathSync as realpathSync5, statSync as statSync13 } from "fs";
63957
- import { basename as basename3, dirname as dirname13, join as join49, resolve as resolve14, sep as sep6 } from "path";
64242
+ import { existsSync as existsSync34, readdirSync as readdirSync6, readFileSync as readFileSync32, realpathSync as realpathSync5, statSync as statSync13 } from "fs";
64243
+ import { basename as basename3, dirname as dirname14, join as join50, resolve as resolve15, sep as sep6 } from "path";
63958
64244
  import { fileURLToPath as fileURLToPath4 } from "url";
63959
64245
  var SKILL_NAME = "ultra11y";
63960
64246
  var URI_SCHEME = "skill://";
63961
64247
  function resolveSkillRoot(moduleDir) {
63962
- const here = moduleDir ?? dirname13(fileURLToPath4(import.meta.url));
63963
- const candidates2 = [resolve14(here, ".."), resolve14(here, "..", "skills", SKILL_NAME), resolve14(here, "..", "..", "skills", SKILL_NAME)];
63964
- return candidates2.find((dir) => existsSync33(join49(dir, "SKILL.md")));
64248
+ const here = moduleDir ?? dirname14(fileURLToPath4(import.meta.url));
64249
+ const candidates2 = [resolve15(here, ".."), resolve15(here, "..", "skills", SKILL_NAME), resolve15(here, "..", "..", "skills", SKILL_NAME)];
64250
+ return candidates2.find((dir) => existsSync34(join50(dir, "SKILL.md")));
63965
64251
  }
63966
64252
  function listResources(moduleDir) {
63967
64253
  const root = resolveSkillRoot(moduleDir);
63968
64254
  if (!root) return [];
63969
64255
  const out2 = [describe(root, "SKILL.md", `${SKILL_NAME}: the skill`)];
63970
- const refDir = join49(root, "references");
63971
- if (!existsSync33(refDir)) return out2;
64256
+ const refDir = join50(root, "references");
64257
+ if (!existsSync34(refDir)) return out2;
63972
64258
  for (const file of readdirSync6(refDir).sort()) {
63973
64259
  if (!file.endsWith(".md")) continue;
63974
- out2.push(describe(root, join49("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
64260
+ out2.push(describe(root, join50("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
63975
64261
  }
63976
64262
  return out2;
63977
64263
  }
@@ -63983,7 +64269,7 @@ function readResource(uri, moduleDir) {
63983
64269
  if (!root) throw new ResourceError("no skill payload found next to this build \u2014 nothing to read");
63984
64270
  const rel2 = uri.slice(URI_SCHEME.length);
63985
64271
  if (!rel2) throw new ResourceError("empty resource path");
63986
- const target = resolve14(root, rel2);
64272
+ const target = resolve15(root, rel2);
63987
64273
  const rootReal = realpathSync5(root);
63988
64274
  let targetReal;
63989
64275
  try {
@@ -63995,7 +64281,7 @@ function readResource(uri, moduleDir) {
63995
64281
  throw new ResourceError(`resource path escapes the skill root: ${uri}`);
63996
64282
  }
63997
64283
  if (!statSync13(targetReal).isFile()) throw new ResourceError(`not a file: ${uri}`);
63998
- return { uri, mimeType: "text/markdown", text: readFileSync31(targetReal, "utf8") };
64284
+ return { uri, mimeType: "text/markdown", text: readFileSync32(targetReal, "utf8") };
63999
64285
  }
64000
64286
  var ResourceError = class extends Error {
64001
64287
  };
@@ -64006,14 +64292,14 @@ function describe(root, rel2, fallbackTitle) {
64006
64292
  title: fallbackTitle,
64007
64293
  mimeType: "text/markdown"
64008
64294
  };
64009
- const summary = firstProse(join49(root, rel2));
64295
+ const summary = firstProse(join50(root, rel2));
64010
64296
  if (summary) decl.description = summary;
64011
64297
  return decl;
64012
64298
  }
64013
64299
  function firstProse(file) {
64014
64300
  let text;
64015
64301
  try {
64016
- text = readFileSync31(file, "utf8");
64302
+ text = readFileSync32(file, "utf8");
64017
64303
  } catch {
64018
64304
  return void 0;
64019
64305
  }
@@ -64395,14 +64681,14 @@ function startHttpServer(opts = {}) {
64395
64681
  server.requestTimeout = 0;
64396
64682
  server.headersTimeout = 6e4;
64397
64683
  server.keepAliveTimeout = 12e4;
64398
- return new Promise((resolve16, reject) => {
64684
+ return new Promise((resolve17, reject) => {
64399
64685
  server.once("error", reject);
64400
64686
  server.listen(opts.port ?? 0, bind, () => {
64401
64687
  server.removeListener("error", reject);
64402
64688
  const addr2 = server.address();
64403
64689
  const port = typeof addr2 === "object" && addr2 ? addr2.port : opts.port ?? 0;
64404
64690
  const host = bind.includes(":") ? `[${bind}]` : bind;
64405
- resolve16({
64691
+ resolve17({
64406
64692
  server,
64407
64693
  port,
64408
64694
  url: `http://${host}:${port}${MCP_PATH}`,
@@ -64511,7 +64797,7 @@ function sendJson(res, status, body3, origin, extra = {}) {
64511
64797
  }
64512
64798
  var DRAIN_LIMIT = MAX_BODY_BYTES * 8;
64513
64799
  function readBody2(req) {
64514
- return new Promise((resolve16, reject) => {
64800
+ return new Promise((resolve17, reject) => {
64515
64801
  const chunks = [];
64516
64802
  let size = 0;
64517
64803
  let over = false;
@@ -64535,7 +64821,7 @@ function readBody2(req) {
64535
64821
  });
64536
64822
  req.on("end", () => {
64537
64823
  if (over) reject(new Error("too large"));
64538
- else resolve16(Buffer.concat(chunks).toString("utf8"));
64824
+ else resolve17(Buffer.concat(chunks).toString("utf8"));
64539
64825
  });
64540
64826
  req.on("error", reject);
64541
64827
  req.on("aborted", () => reject(new Error("client aborted the request")));
@@ -64837,6 +65123,13 @@ Options:
64837
65123
  --generate criteria: emit the bundled references/criteria.md (WCAG 2.2 AA)
64838
65124
  --apply <file> verify: reduce a filled verdicts file to a pass/fail gate
64839
65125
  (requires --report \u2014 coverage is re-derived from the report, uncapped)
65126
+ Also accepts an ADJUDICATION (--in <audit.json>), or a VERDICT LEDGER,
65127
+ which it REPLAYS onto the audit with no model in the loop
65128
+ --ledger <file> verify --apply / judge --apply: record the verdicts the fold ACCEPTED,
65129
+ so a later run replays them for free (default:
65130
+ .ultra11y/verdicts/<standard>.json). Replay re-derives the evidence
65131
+ and re-runs the same gate; a verdict whose evidence changed is
65132
+ dropped as stale and its criterion says so
64840
65133
  --max-verify <n> verify: cap the worklist size; 0 = no cap (default: 40)
64841
65134
  --verdicts <file> check --semantic: the adjudicated verdicts artifact
64842
65135
  (default: VERIFY.todo.json next to the report)
@@ -64899,6 +65192,10 @@ Options:
64899
65192
  --apply judge: fold the verdicts straight into the audit, through the SAME
64900
65193
  fail-closed gate an agent's verdicts pass (no unjustified C/NA, no
64901
65194
  ungroundable NC, no unadjudicated criterion)
65195
+ --strict verify --apply / judge --apply: all-or-nothing fold \u2014 one refused
65196
+ verdict discards the whole adjudication. The default folds PER
65197
+ VERDICT: a refusal costs its own criterion, which stays to assess
65198
+ carrying the refusal, and every verdict that proved itself lands
64902
65199
  --semantic verify: fold the support-check into one pass
64903
65200
  check: engage the semantic gate \u2014 requires an adjudicated verdicts
64904
65201
  artifact (fails closed when absent) and re-grounds every passing
@@ -64973,6 +65270,10 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
64973
65270
  "dedup",
64974
65271
  "only",
64975
65272
  "standard",
65273
+ // `verify --apply` / `judge --apply`: where to RECORD the verdicts the fold accepted, so a
65274
+ // later run can replay them without a model (src/ledger.ts). A path; empty falls back to the
65275
+ // standard's default location under .ultra11y/verdicts/.
65276
+ "ledger",
64976
65277
  "baseline",
64977
65278
  "fail-on",
64978
65279
  "split",
@@ -65044,6 +65345,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
65044
65345
  "generate",
65045
65346
  "semantic",
65046
65347
  "manual",
65348
+ // `verify --apply` / `judge --apply`: restore the all-or-nothing fold, where one refused
65349
+ // verdict discards the whole adjudication. The default is per-verdict.
65350
+ "strict",
65047
65351
  "no-technical",
65048
65352
  "override",
65049
65353
  "local",
@@ -65268,9 +65572,9 @@ async function cmdAudit(p) {
65268
65572
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
65269
65573
  const capturesDir = capturesFlag ?? CAPTURES_DIR;
65270
65574
  const scopedToDiff = p.flags.changed === true || p.flags.staged === true || since !== void 0;
65271
- const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 || existsSync34(capturesDir));
65575
+ const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 || existsSync35(capturesDir));
65272
65576
  const useCaptures = capturesWanted && !scopedToDiff && !inputs.includes(capturesDir);
65273
- const pagesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && existsSync34(PAGES_DIR);
65577
+ const pagesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && existsSync35(PAGES_DIR);
65274
65578
  const usePages = pagesWanted && !scopedToDiff && !inputs.includes(PAGES_DIR);
65275
65579
  const pagesInScope = pagesWanted && !scopedToDiff;
65276
65580
  const auditInputs = [...inputs, ...useCaptures ? [capturesDir] : [], ...usePages ? [PAGES_DIR] : []];
@@ -65312,10 +65616,10 @@ async function cmdAudit(p) {
65312
65616
  if (typeof p.flags.out === "string") {
65313
65617
  const out2 = p.flags.out;
65314
65618
  const asFile = out2.toLowerCase().endsWith(".json");
65315
- const target = asFile ? out2 : join50(out2, "audit-latest.json");
65619
+ const target = asFile ? out2 : join51(out2, "audit-latest.json");
65316
65620
  try {
65317
- mkdirSync17(asFile ? dirname14(out2) : out2, { recursive: true });
65318
- writeFileSync19(target, JSON.stringify(result, null, 2) + "\n");
65621
+ mkdirSync18(asFile ? dirname15(out2) : out2, { recursive: true });
65622
+ writeFileSync20(target, JSON.stringify(result, null, 2) + "\n");
65319
65623
  console.error(lang === "fr" ? `\u2192 audit \xE9crit dans ${target}` : `\u2192 audit written to ${target}`);
65320
65624
  } catch {
65321
65625
  }
@@ -65334,7 +65638,7 @@ async function cmdAudit(p) {
65334
65638
  const baselineFlag = p.flags.baseline;
65335
65639
  if (typeof baselineFlag === "string" && baselineFlag) {
65336
65640
  let baseline = null;
65337
- if (existsSync34(baselineFlag)) {
65641
+ if (existsSync35(baselineFlag)) {
65338
65642
  try {
65339
65643
  const parsed = JSON.parse(readText(baselineFlag));
65340
65644
  if (isCurrentAudit(parsed)) baseline = parsed;
@@ -65388,11 +65692,11 @@ async function cmdDev(p) {
65388
65692
  return 2;
65389
65693
  }
65390
65694
  if (p.flags.next === true) {
65391
- const dir = join50(root, ".ultra11y", "next");
65695
+ const dir = join51(root, ".ultra11y", "next");
65392
65696
  const rel2 = ".ultra11y/next/overlay.jsx";
65393
65697
  try {
65394
- mkdirSync17(dir, { recursive: true });
65395
- writeFileSync19(join50(dir, "overlay.jsx"), nextOverlayComponent(port));
65698
+ mkdirSync18(dir, { recursive: true });
65699
+ writeFileSync20(join51(dir, "overlay.jsx"), nextOverlayComponent(port));
65396
65700
  } catch (e) {
65397
65701
  console.error(`ultra11y dev: could not write ${rel2}: ${e instanceof Error ? e.message : String(e)}`);
65398
65702
  return 1;
@@ -65423,9 +65727,9 @@ async function cmdDev(p) {
65423
65727
  }
65424
65728
  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}`);
65425
65729
  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.");
65426
- await new Promise((resolve16) => {
65730
+ await new Promise((resolve17) => {
65427
65731
  const stop2 = () => {
65428
- void server.close().then(resolve16);
65732
+ void server.close().then(resolve17);
65429
65733
  };
65430
65734
  process.once("SIGINT", stop2);
65431
65735
  process.once("SIGTERM", stop2);
@@ -65441,7 +65745,7 @@ async function cmdPagesDiscover(p) {
65441
65745
  const snaps = sampleFromSnapshots(readSnapshots(root));
65442
65746
  if (!snaps.length) {
65443
65747
  console.error(
65444
- lang === "fr" ? `ultra11y pages discover : aucun instantan\xE9 sous ${join50(root, PAGES_DIR)} \u2014 lancez d'abord vos tests E2E avec checkA11y, ou \`scan --sample\`.` : `ultra11y pages discover: no snapshot under ${join50(root, PAGES_DIR)} \u2014 run your E2E tests with checkA11y first, or \`scan --sample\`.`
65748
+ lang === "fr" ? `ultra11y pages discover : aucun instantan\xE9 sous ${join51(root, PAGES_DIR)} \u2014 lancez d'abord vos tests E2E avec checkA11y, ou \`scan --sample\`.` : `ultra11y pages discover: no snapshot under ${join51(root, PAGES_DIR)} \u2014 run your E2E tests with checkA11y first, or \`scan --sample\`.`
65445
65749
  );
65446
65750
  return 1;
65447
65751
  }
@@ -65521,9 +65825,9 @@ ${found} page(s) discovered, ${merged.added.length} new. Nothing was written \u2
65521
65825
  }
65522
65826
  return 0;
65523
65827
  }
65524
- const file = join50(process.cwd(), ".ultra11yrc.json");
65828
+ const file = join51(process.cwd(), ".ultra11yrc.json");
65525
65829
  let doc = {};
65526
- if (existsSync34(file)) {
65830
+ if (existsSync35(file)) {
65527
65831
  try {
65528
65832
  doc = JSON.parse(readText(file));
65529
65833
  } catch {
@@ -65532,7 +65836,7 @@ ${found} page(s) discovered, ${merged.added.length} new. Nothing was written \u2
65532
65836
  }
65533
65837
  }
65534
65838
  doc.sample = v.sample;
65535
- writeFileSync19(file, `${JSON.stringify(doc, null, 2)}
65839
+ writeFileSync20(file, `${JSON.stringify(doc, null, 2)}
65536
65840
  `);
65537
65841
  if (!p.flags.json)
65538
65842
  console.log(
@@ -65592,27 +65896,27 @@ async function cmdPages(p) {
65592
65896
  const derived = derivePages(result, scope);
65593
65897
  const split = p.flags.split === "page";
65594
65898
  const outDir = typeof p.flags.out === "string" && p.flags.out ? p.flags.out : void 0;
65595
- const shotOf = (id) => join50(PAGES_DIR, id, "screen.png");
65899
+ const shotOf = (id) => join51(PAGES_DIR, id, "screen.png");
65596
65900
  const shotsRelative = (fileDir) => {
65597
65901
  const m = /* @__PURE__ */ new Map();
65598
65902
  for (const pg of derived) {
65599
- if (existsSync34(shotOf(pg.id))) m.set(pg.id, relative5(fileDir, shotOf(pg.id)).split("\\").join("/"));
65903
+ if (existsSync35(shotOf(pg.id))) m.set(pg.id, relative5(fileDir, shotOf(pg.id)).split("\\").join("/"));
65600
65904
  }
65601
65905
  return m;
65602
65906
  };
65603
65907
  const shotPaths = (pages) => {
65604
65908
  const m = /* @__PURE__ */ new Map();
65605
- for (const pg of pages) if (existsSync34(shotOf(pg.id))) m.set(pg.id, shotOf(pg.id));
65909
+ for (const pg of pages) if (existsSync35(shotOf(pg.id))) m.set(pg.id, shotOf(pg.id));
65606
65910
  return m;
65607
65911
  };
65608
65912
  const shotsCopiedInto = (dir) => {
65609
65913
  const m = /* @__PURE__ */ new Map();
65610
- const assets = join50(dir, "assets");
65914
+ const assets = join51(dir, "assets");
65611
65915
  for (const pg of derived) {
65612
65916
  const src = shotOf(pg.id);
65613
- if (!existsSync34(src)) continue;
65614
- mkdirSync17(assets, { recursive: true });
65615
- copyFileSync4(src, join50(assets, `${pg.id}.png`));
65917
+ if (!existsSync35(src)) continue;
65918
+ mkdirSync18(assets, { recursive: true });
65919
+ copyFileSync4(src, join51(assets, `${pg.id}.png`));
65616
65920
  m.set(pg.id, `./assets/${pg.id}.png`);
65617
65921
  }
65618
65922
  return m;
@@ -65629,7 +65933,7 @@ async function cmdPages(p) {
65629
65933
  console.log(renderPagesDocument(result, derived, { standard, lang, screenshots: shotsRelative(".") }));
65630
65934
  return 0;
65631
65935
  }
65632
- mkdirSync17(outDir, { recursive: true });
65936
+ mkdirSync18(outDir, { recursive: true });
65633
65937
  const manifest = wantEvidence ? writeEvidence(result, { outDir, ...evidenceCapsOf(p) }) : void 0;
65634
65938
  const cropFor = manifest ? (f) => {
65635
65939
  const c2 = manifest.crops.get(findingId(f));
@@ -65638,7 +65942,7 @@ async function cmdPages(p) {
65638
65942
  if (manifest) {
65639
65943
  const total = evidenceNotice(manifest, null, lang);
65640
65944
  console.error(
65641
- lang === "fr" ? `ultra11y : ${manifest.totals.imaged} vignette(s) \xE9crite(s) dans ${join50(outDir, "assets")}.` : `ultra11y: ${manifest.totals.imaged} crop(s) written to ${join50(outDir, "assets")}.`
65945
+ lang === "fr" ? `ultra11y : ${manifest.totals.imaged} vignette(s) \xE9crite(s) dans ${join51(outDir, "assets")}.` : `ultra11y: ${manifest.totals.imaged} crop(s) written to ${join51(outDir, "assets")}.`
65642
65946
  );
65643
65947
  for (const line of total) console.error(line);
65644
65948
  }
@@ -65647,8 +65951,8 @@ async function cmdPages(p) {
65647
65951
  ...manifest ? { evidenceNotice: (id) => evidenceNotice(manifest, id, lang) } : {}
65648
65952
  };
65649
65953
  if (!split) {
65650
- const file = join50(outDir, `pages-${result.date}.md`);
65651
- writeFileSync19(file, `${renderPagesDocument(result, derived, { standard, lang, screenshots: shotsCopiedInto(outDir), ...evidenceOpts })}
65954
+ const file = join51(outDir, `pages-${result.date}.md`);
65955
+ writeFileSync20(file, `${renderPagesDocument(result, derived, { standard, lang, screenshots: shotsCopiedInto(outDir), ...evidenceOpts })}
65652
65956
  `);
65653
65957
  const html2 = wantHtml ? emitHtml(result, {
65654
65958
  outDir,
@@ -65667,11 +65971,11 @@ async function cmdPages(p) {
65667
65971
  const sheet = (id) => `page-${id}.md`;
65668
65972
  const hrefs = new Map(derived.map((pg) => [pg.id, `./${sheet(pg.id)}`]));
65669
65973
  for (const pg of derived) {
65670
- writeFileSync19(join50(outDir, sheet(pg.id)), `${renderPageDocument(result, pg, { standard, lang, screenshots: shots, ...evidenceOpts })}
65974
+ writeFileSync20(join51(outDir, sheet(pg.id)), `${renderPageDocument(result, pg, { standard, lang, screenshots: shots, ...evidenceOpts })}
65671
65975
  `);
65672
65976
  }
65673
- const index = join50(outDir, "index.md");
65674
- writeFileSync19(index, `${renderPagesIndex(result, derived, { standard, lang, hrefs })}
65977
+ const index = join51(outDir, "index.md");
65978
+ writeFileSync20(index, `${renderPagesIndex(result, derived, { standard, lang, hrefs })}
65675
65979
  `);
65676
65980
  const html = wantHtml ? emitHtml(result, {
65677
65981
  outDir,
@@ -65722,7 +66026,7 @@ async function cmdSnapshot(p) {
65722
66026
  2
65723
66027
  )
65724
66028
  );
65725
- else if (!snaps.length) console.log(lang === "fr" ? `Aucun instantan\xE9 dans ${join50(root, PAGES_DIR)}.` : `No snapshot in ${join50(root, PAGES_DIR)}.`);
66029
+ else if (!snaps.length) console.log(lang === "fr" ? `Aucun instantan\xE9 dans ${join51(root, PAGES_DIR)}.` : `No snapshot in ${join51(root, PAGES_DIR)}.`);
65726
66030
  else for (const s of snaps) console.log(`${s.meta.id} ${s.meta.name} ${s.meta.url}${s.meta.auth ? " [auth]" : ""}`);
65727
66031
  return 0;
65728
66032
  }
@@ -65769,7 +66073,7 @@ async function cmdSnapshot(p) {
65769
66073
  console.error(`ultra11y snapshot write: could not write the snapshot: ${e instanceof Error ? e.message : String(e)}`);
65770
66074
  return 1;
65771
66075
  }
65772
- const result = runAudit({ inputs: [join50(dir, "dom.html")], onWarn: (m) => console.error(m) });
66076
+ const result = runAudit({ inputs: [join51(dir, "dom.html")], onWarn: (m) => console.error(m) });
65773
66077
  const failOnRaw = p.flags["fail-on"];
65774
66078
  const failOnParsed = parseFailOn(failOnRaw);
65775
66079
  if (failOnRaw !== void 0 && failOnParsed === null) {
@@ -65801,9 +66105,9 @@ function cmdInit(p) {
65801
66105
  if (want.baseline) {
65802
66106
  const inputs = p.positionals.length ? p.positionals : ["."];
65803
66107
  const result = runAudit({ inputs, onWarn: (m) => console.error(m) });
65804
- mkdirSync17(join50(root, "audits"), { recursive: true });
65805
- const bp = join50(root, "audits", "baseline.json");
65806
- writeFileSync19(bp, JSON.stringify(result, null, 2) + "\n");
66108
+ mkdirSync18(join51(root, "audits"), { recursive: true });
66109
+ const bp = join51(root, "audits", "baseline.json");
66110
+ writeFileSync20(bp, JSON.stringify(result, null, 2) + "\n");
65807
66111
  wrote.push(bp);
65808
66112
  }
65809
66113
  if (want.hook) wrote.push(writeHook(root, engineRel, failOn, legacy ? "baseline" : "staged"));
@@ -66102,8 +66406,8 @@ async function cmdTickets(p) {
66102
66406
  const ticketsOut = typeof p.flags.out === "string" && p.flags.out ? p.flags.out : void 0;
66103
66407
  let setPath;
66104
66408
  if (ticketsOut) {
66105
- mkdirSync17(ticketsOut, { recursive: true });
66106
- setPath = join50(ticketsOut, `issues-${result.date}.json`);
66409
+ mkdirSync18(ticketsOut, { recursive: true });
66410
+ setPath = join51(ticketsOut, `issues-${result.date}.json`);
66107
66411
  const payload = {
66108
66412
  tool: "ultra11y",
66109
66413
  kind: "issues",
@@ -66114,7 +66418,7 @@ async function cmdTickets(p) {
66114
66418
  count: plan.tickets.length,
66115
66419
  issues: plan.tickets
66116
66420
  };
66117
- writeFileSync19(setPath, `${JSON.stringify(payload, null, 2)}
66421
+ writeFileSync20(setPath, `${JSON.stringify(payload, null, 2)}
66118
66422
  `);
66119
66423
  if (!json) console.log(setPath);
66120
66424
  }
@@ -66204,8 +66508,8 @@ async function cmdTickets(p) {
66204
66508
  return pushed.failed > 0 ? 1 : 0;
66205
66509
  }
66206
66510
  function depsAt(root) {
66207
- const pkgPath = join50(root, "package.json");
66208
- if (!existsSync34(pkgPath)) return {};
66511
+ const pkgPath = join51(root, "package.json");
66512
+ if (!existsSync35(pkgPath)) return {};
66209
66513
  try {
66210
66514
  const pkg = JSON.parse(readText(pkgPath));
66211
66515
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
@@ -66231,24 +66535,24 @@ function cmdRender(p) {
66231
66535
  console.error(`ultra11y render: --runner must be playwright|cypress|auto (got "${forced}").`);
66232
66536
  return 2;
66233
66537
  }
66234
- const detected = detectE2eRunner(depsAt(root), (f) => existsSync34(join50(root, f)));
66538
+ const detected = detectE2eRunner(depsAt(root), (f) => existsSync35(join51(root, f)));
66235
66539
  const runners = forced && forced !== "auto" ? [forced] : detected;
66236
66540
  if (!runners.length) {
66237
66541
  console.error(e2eSetupPlan([], {}, lang));
66238
66542
  return 1;
66239
66543
  }
66240
66544
  const engineRef = engineRefFor(root);
66241
- const dir = join50(root, ".ultra11y", "e2e");
66545
+ const dir = join51(root, ".ultra11y", "e2e");
66242
66546
  const paths = {};
66243
66547
  try {
66244
- mkdirSync17(dir, { recursive: true });
66548
+ mkdirSync18(dir, { recursive: true });
66245
66549
  if (runners.includes("playwright")) {
66246
- writeFileSync19(join50(dir, "playwright.mjs"), playwrightFixture(engineRef));
66550
+ writeFileSync20(join51(dir, "playwright.mjs"), playwrightFixture(engineRef));
66247
66551
  paths.playwright = ".ultra11y/e2e/playwright.mjs";
66248
66552
  }
66249
66553
  if (runners.includes("cypress")) {
66250
- writeFileSync19(join50(dir, "cypress-plugin.mjs"), cypressPlugin(engineRef));
66251
- writeFileSync19(join50(dir, "cypress-commands.mjs"), cypressCommands());
66554
+ writeFileSync20(join51(dir, "cypress-plugin.mjs"), cypressPlugin(engineRef));
66555
+ writeFileSync20(join51(dir, "cypress-commands.mjs"), cypressCommands());
66252
66556
  paths.cypressPlugin = ".ultra11y/e2e/cypress-plugin.mjs";
66253
66557
  paths.cypressCommands = ".ultra11y/e2e/cypress-commands.mjs";
66254
66558
  }
@@ -66263,7 +66567,7 @@ function cmdRender(p) {
66263
66567
  if (p.flags.scaffold === true) {
66264
66568
  const out2 = typeof p.flags.out === "string" && p.flags.out ? p.flags.out : "ultra11y-render.tsx";
66265
66569
  try {
66266
- writeFileSync19(out2, ssrHarness());
66570
+ writeFileSync20(out2, ssrHarness());
66267
66571
  } catch (e) {
66268
66572
  console.error(`ultra11y render: could not write ${out2}: ${e instanceof Error ? e.message : String(e)}`);
66269
66573
  return 1;
@@ -66277,29 +66581,29 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
66277
66581
  }
66278
66582
  if (p.flags.setup === true) {
66279
66583
  const rel2 = ".ultra11y/capture-setup.mjs";
66280
- const out2 = join50(root, rel2);
66584
+ const out2 = join51(root, rel2);
66281
66585
  try {
66282
- mkdirSync17(dirname14(out2), { recursive: true });
66283
- writeFileSync19(out2, captureSetup());
66586
+ mkdirSync18(dirname15(out2), { recursive: true });
66587
+ writeFileSync20(out2, captureSetup());
66284
66588
  } catch (e) {
66285
66589
  console.error(`ultra11y render: could not write ${out2}: ${e instanceof Error ? e.message : String(e)}`);
66286
66590
  return 1;
66287
66591
  }
66288
66592
  let setupDeps = {};
66289
- const setupPkg = join50(root, "package.json");
66290
- if (existsSync34(setupPkg)) {
66593
+ const setupPkg = join51(root, "package.json");
66594
+ if (existsSync35(setupPkg)) {
66291
66595
  try {
66292
66596
  const pkg = JSON.parse(readText(setupPkg));
66293
66597
  setupDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
66294
66598
  } catch {
66295
66599
  }
66296
66600
  }
66297
- const tr = detectTestRunner(setupDeps, (f) => existsSync34(join50(root, f)));
66601
+ const tr = detectTestRunner(setupDeps, (f) => existsSync35(join51(root, f)));
66298
66602
  console.log(captureSetupPlan(tr, rel2, lang));
66299
66603
  const gaLine = ".ultra11y/captures/*.html text eol=lf linguist-generated=true";
66300
- const gaPath = join50(root, ".gitattributes");
66604
+ const gaPath = join51(root, ".gitattributes");
66301
66605
  try {
66302
- const existing = existsSync34(gaPath) ? readFileSync32(gaPath, "utf8") : "";
66606
+ const existing = existsSync35(gaPath) ? readFileSync33(gaPath, "utf8") : "";
66303
66607
  if (!existing.includes(".ultra11y/captures/")) {
66304
66608
  appendFileSync(gaPath, (existing && !existing.endsWith("\n") ? "\n" : "") + gaLine + "\n");
66305
66609
  console.log(lang === "fr" ? `.gitattributes : ajout\xE9 \xAB ${gaLine} \xBB` : `.gitattributes: added "${gaLine}"`);
@@ -66307,8 +66611,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
66307
66611
  } catch {
66308
66612
  }
66309
66613
  try {
66310
- const giPath = join50(root, ".gitignore");
66311
- if (existsSync34(giPath) && /^\s*\/?\.ultra11y(\/\**)?\/?\s*$/m.test(readFileSync32(giPath, "utf8")))
66614
+ const giPath = join51(root, ".gitignore");
66615
+ if (existsSync35(giPath) && /^\s*\/?\.ultra11y(\/\**)?\/?\s*$/m.test(readFileSync33(giPath, "utf8")))
66312
66616
  console.error(
66313
66617
  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/").'
66314
66618
  );
@@ -66318,8 +66622,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
66318
66622
  }
66319
66623
  if (p.flags.storybook === true || typeof p.flags.storybook === "string") {
66320
66624
  const sbDir = p.positionals[0] ?? "storybook-static";
66321
- const indexPath = existsSync34(join50(sbDir, "index.json")) ? join50(sbDir, "index.json") : join50(sbDir, "stories.json");
66322
- if (!existsSync34(indexPath)) {
66625
+ const indexPath = existsSync35(join51(sbDir, "index.json")) ? join51(sbDir, "index.json") : join51(sbDir, "stories.json");
66626
+ if (!existsSync35(indexPath)) {
66323
66627
  console.error(
66324
66628
  lang === "fr" ? `ultra11y render : aucun index Storybook (index.json/stories.json) dans ${sbDir}.` : `ultra11y render: no Storybook index (index.json/stories.json) in ${sbDir}.`
66325
66629
  );
@@ -66329,7 +66633,7 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
66329
66633
  const provById = new Map(stories.map((s) => [s.id, storyProvenance(s)]));
66330
66634
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
66331
66635
  const htmlDir = capturesFlag ?? sbDir;
66332
- const htmlFiles = existsSync34(htmlDir) ? discover([htmlDir]).files.filter((f) => /\.html?$/i.test(f)) : [];
66636
+ const htmlFiles = existsSync35(htmlDir) ? discover([htmlDir]).files.filter((f) => /\.html?$/i.test(f)) : [];
66333
66637
  const outDir = ".ultra11y/captures";
66334
66638
  let attributed = 0;
66335
66639
  let skipped = 0;
@@ -66351,8 +66655,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
66351
66655
  continue;
66352
66656
  }
66353
66657
  try {
66354
- mkdirSync17(outDir, { recursive: true });
66355
- writeFileSync19(join50(outDir, `${hitId}.html`), `${formatCaptureComment(prov)}
66658
+ mkdirSync18(outDir, { recursive: true });
66659
+ writeFileSync20(join51(outDir, `${hitId}.html`), `${formatCaptureComment(prov)}
66356
66660
  ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
66357
66661
  attributed++;
66358
66662
  } catch {
@@ -66374,11 +66678,11 @@ ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
66374
66678
  }
66375
66679
  if (p.flags.coverage === true) {
66376
66680
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
66377
- const capturesDir = capturesFlag ?? join50(root, ".ultra11y/captures");
66681
+ const capturesDir = capturesFlag ?? join51(root, ".ultra11y/captures");
66378
66682
  const graphExt = [...GRAPH_ONLY_EXT, ...asList(p.flags.ext) ?? []];
66379
66683
  const sourceFiles = discover([root], { include: asList(p.flags.include), exclude: asList(p.flags.exclude), ext: graphExt }).files;
66380
66684
  const graph = buildGraphStreaming(sourceFiles);
66381
- const capFiles = existsSync34(capturesDir) ? discover([capturesDir]).files : [];
66685
+ const capFiles = existsSync35(capturesDir) ? discover([capturesDir]).files : [];
66382
66686
  const entries = capFiles.map((f) => ({ file: toPosix(f), provenance: parseCaptureProvenance(readText(f)) }));
66383
66687
  const cov = computeCaptureCoverage(graph, entries);
66384
66688
  if (p.flags.json) console.log(JSON.stringify(cov, null, 2));
@@ -66386,15 +66690,15 @@ ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
66386
66690
  return 0;
66387
66691
  }
66388
66692
  let deps = {};
66389
- const pkgPath = join50(root, "package.json");
66390
- if (existsSync34(pkgPath)) {
66693
+ const pkgPath = join51(root, "package.json");
66694
+ if (existsSync35(pkgPath)) {
66391
66695
  try {
66392
66696
  const pkg = JSON.parse(readText(pkgPath));
66393
66697
  deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
66394
66698
  } catch {
66395
66699
  }
66396
66700
  }
66397
- const detection = detectFrameworks(deps, (f) => existsSync34(join50(root, f)));
66701
+ const detection = detectFrameworks(deps, (f) => existsSync35(join51(root, f)));
66398
66702
  if (p.flags.json) console.log(JSON.stringify(detection, null, 2));
66399
66703
  else console.log(renderPlan(detection, lang));
66400
66704
  return 0;
@@ -66467,6 +66771,9 @@ function cmdVerify(p) {
66467
66771
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.kind === "adjudication") {
66468
66772
  return applyAdjudicationFile(p, parsed, lang);
66469
66773
  }
66774
+ if (isLedger(parsed)) {
66775
+ return replayLedgerFile(p, parsed, lang);
66776
+ }
66470
66777
  if (!Array.isArray(parsed)) {
66471
66778
  console.error("ultra11y verify: --apply must be a JSON array of verdicts, or an adjudication object.");
66472
66779
  return 2;
@@ -66568,6 +66875,84 @@ function cmdVerify(p) {
66568
66875
  );
66569
66876
  return 0;
66570
66877
  }
66878
+ function ledgerTarget(p, standard) {
66879
+ const flag = p.flags.ledger;
66880
+ if (flag === void 0 || flag === false) return void 0;
66881
+ return typeof flag === "string" && flag ? flag : ledgerPath(standard);
66882
+ }
66883
+ function replayLedgerFile(p, ledger, lang) {
66884
+ const inFlag = p.flags.in;
66885
+ if (typeof inFlag !== "string" || !inFlag) {
66886
+ console.error(
66887
+ lang === "fr" ? "ultra11y verify : --apply <registre> exige --in <audit.json> (l'audit \xE0 mettre \xE0 jour)." : "ultra11y verify: --apply <ledger> requires --in <audit.json> (the audit to update)."
66888
+ );
66889
+ return 2;
66890
+ }
66891
+ let audit2;
66892
+ try {
66893
+ audit2 = JSON.parse(readText(inFlag));
66894
+ } catch {
66895
+ console.error(`ultra11y verify: --in file not found or not valid JSON: ${inFlag}.`);
66896
+ return 2;
66897
+ }
66898
+ const cwd = typeof p.flags.cwd === "string" ? p.flags.cwd : void 0;
66899
+ const standard = resolveStandard(p.flags.standard) ?? ledger.standard;
66900
+ const rp = replayLedger(audit2, ledger, { cwd, standard });
66901
+ const r = applyAdjudication(audit2, rp.adj, { cwd, strict: p.flags.strict === true, residualReasons: rp.residualReasons });
66902
+ if (!r.ok && (p.flags.strict === true || r.applied + r.stillManual === 0)) {
66903
+ if (p.flags.json) console.log(JSON.stringify({ ...r, replay: rp }, null, 2));
66904
+ else {
66905
+ console.error(lang === "fr" ? `\u2717 Registre rejet\xE9 (${r.issues.length} probl\xE8me(s)) :` : `\u2717 Ledger rejected (${r.issues.length} issue(s)):`);
66906
+ for (const i2 of r.issues) console.error(` \u2717 ${i2}`);
66907
+ }
66908
+ return 1;
66909
+ }
66910
+ const out2 = typeof p.flags.out === "string" ? p.flags.out : ".";
66911
+ mkdirSync18(out2, { recursive: true });
66912
+ const auditPath = join51(out2, "audit-latest.json");
66913
+ writeFileSync20(auditPath, `${JSON.stringify(r.audit, null, 2)}
66914
+ `);
66915
+ if (p.flags.json)
66916
+ console.log(
66917
+ JSON.stringify(
66918
+ {
66919
+ ok: true,
66920
+ auditPath,
66921
+ applied: r.applied,
66922
+ stillManual: r.stillManual,
66923
+ rejected: r.rejected,
66924
+ rejectedCriteria: r.rejectedCriteria,
66925
+ replayed: rp.fresh.length,
66926
+ stale: rp.stale,
66927
+ obsolete: rp.obsolete,
66928
+ missing: rp.missing,
66929
+ issues: r.issues,
66930
+ conformancePct: r.audit.conformancePct,
66931
+ grounding: r.grounding
66932
+ },
66933
+ null,
66934
+ 2
66935
+ )
66936
+ );
66937
+ else {
66938
+ console.log(
66939
+ lang === "fr" ? `\u2713 ${r.applied} verdict(s) rejou\xE9(s) depuis le registre, ${r.stillManual} laiss\xE9(s) en r\xE9siduel${r.rejected ? `, ${r.rejected} refus\xE9(s)` : ""} \u2192 ${auditPath}` : `\u2713 ${r.applied} verdict(s) replayed from the ledger, ${r.stillManual} left residual${r.rejected ? `, ${r.rejected} refused` : ""} \u2192 ${auditPath}`
66940
+ );
66941
+ if (rp.stale.length)
66942
+ console.error(
66943
+ lang === "fr" ? `\u26A0 ${rp.stale.length} verdict(s) p\xE9rim\xE9(s) (l'\xE9vidence a chang\xE9) : ${rp.stale.join(", ")}` : `\u26A0 ${rp.stale.length} stale verdict(s) (the evidence changed): ${rp.stale.join(", ")}`
66944
+ );
66945
+ if (rp.missing.length)
66946
+ console.error(
66947
+ lang === "fr" ? `\u26A0 ${rp.missing.length} crit\xE8re(s) absent(s) du registre : ${rp.missing.join(", ")}` : `\u26A0 ${rp.missing.length} criterion(ia) absent from the ledger: ${rp.missing.join(", ")}`
66948
+ );
66949
+ if (rp.obsolete.length)
66950
+ console.error(
66951
+ lang === "fr" ? `\u2139 ${rp.obsolete.length} entr\xE9e(s) obsol\xE8te(s) (le moteur les d\xE9cide d\xE9sormais) : ${rp.obsolete.join(", ")}` : `\u2139 ${rp.obsolete.length} obsolete entry(ies) (the engine now decides them): ${rp.obsolete.join(", ")}`
66952
+ );
66953
+ }
66954
+ return 0;
66955
+ }
66571
66956
  function applyAdjudicationFile(p, adj, lang) {
66572
66957
  const inFlag = p.flags.in;
66573
66958
  if (typeof inFlag !== "string" || !inFlag) {
@@ -66583,9 +66968,11 @@ function applyAdjudicationFile(p, adj, lang) {
66583
66968
  console.error(`ultra11y verify: --in file not found or not valid JSON: ${inFlag}.`);
66584
66969
  return 2;
66585
66970
  }
66586
- hydrateAdjudication(adj, audit2, { cwd: typeof p.flags.cwd === "string" ? p.flags.cwd : void 0 });
66587
- const r = applyAdjudication(audit2, adj);
66588
- if (!r.ok) {
66971
+ const cwd = typeof p.flags.cwd === "string" ? p.flags.cwd : void 0;
66972
+ hydrateAdjudication(adj, audit2, { cwd });
66973
+ const strict = p.flags.strict === true;
66974
+ const r = applyAdjudication(audit2, adj, { cwd, strict });
66975
+ if (!r.ok && (strict || r.applied + r.stillManual === 0)) {
66589
66976
  if (p.flags.json) console.log(JSON.stringify(r, null, 2));
66590
66977
  else {
66591
66978
  console.error(lang === "fr" ? `\u2717 Adjudication rejet\xE9e (${r.issues.length} probl\xE8me(s)) :` : `\u2717 Adjudication rejected (${r.issues.length} issue(s)):`);
@@ -66593,10 +66980,27 @@ function applyAdjudicationFile(p, adj, lang) {
66593
66980
  }
66594
66981
  return 1;
66595
66982
  }
66983
+ if (r.rejected > 0 && !p.flags.json) {
66984
+ console.error(
66985
+ lang === "fr" ? `\u26A0 ${r.rejected} crit\xE8re(s) refus\xE9(s) par la porte \u2014 ils restent \xE0 \xE9valuer, avec le motif du refus :` : `\u26A0 ${r.rejected} criterion(ia) refused by the gate \u2014 they stay to assess, carrying the refusal:`
66986
+ );
66987
+ for (const i2 of r.issues) console.error(` \u2717 ${i2}`);
66988
+ }
66596
66989
  const out2 = typeof p.flags.out === "string" ? p.flags.out : ".";
66597
- mkdirSync17(out2, { recursive: true });
66598
- const auditPath = join50(out2, "audit-latest.json");
66599
- writeFileSync19(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
66990
+ mkdirSync18(out2, { recursive: true });
66991
+ const auditPath = join51(out2, "audit-latest.json");
66992
+ writeFileSync20(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
66993
+ const ledgerOut = ledgerTarget(p, adj.standard);
66994
+ if (ledgerOut) {
66995
+ const refused = new Set(r.rejectedCriteria);
66996
+ const accepted = new Set(adj.items.map((it) => it.criteriaId).filter((id) => !refused.has(id)));
66997
+ const fresh = entriesFrom(adj, accepted, r.audit.date);
66998
+ writeLedger(ledgerOut, mergeLedger(readLedger(ledgerOut), adj.standard, fresh));
66999
+ if (!p.flags.json)
67000
+ console.log(
67001
+ lang === "fr" ? `\u2713 ${fresh.length} verdict(s) enregistr\xE9(s) au registre \u2192 ${ledgerOut}` : `\u2713 ${fresh.length} verdict(s) recorded in the ledger \u2192 ${ledgerOut}`
67002
+ );
67003
+ }
66600
67004
  if (p.flags.json)
66601
67005
  console.log(
66602
67006
  JSON.stringify(
@@ -66605,6 +67009,9 @@ function applyAdjudicationFile(p, adj, lang) {
66605
67009
  auditPath,
66606
67010
  applied: r.applied,
66607
67011
  stillManual: r.stillManual,
67012
+ rejected: r.rejected,
67013
+ rejectedCriteria: r.rejectedCriteria,
67014
+ issues: r.issues,
66608
67015
  conformancePct: r.audit.conformancePct,
66609
67016
  findings: r.audit.findings.length,
66610
67017
  grounding: r.grounding
@@ -66615,7 +67022,7 @@ function applyAdjudicationFile(p, adj, lang) {
66615
67022
  );
66616
67023
  else
66617
67024
  console.log(
66618
- lang === "fr" ? `\u2713 ${r.applied} crit\xE8re(s) adjug\xE9(s), ${r.stillManual} laiss\xE9(s) en r\xE9siduel \u2192 ${auditPath}` : `\u2713 ${r.applied} criterion(ia) adjudicated, ${r.stillManual} left residual \u2192 ${auditPath}`
67025
+ lang === "fr" ? `\u2713 ${r.applied} crit\xE8re(s) adjug\xE9(s), ${r.stillManual} laiss\xE9(s) en r\xE9siduel${r.rejected ? `, ${r.rejected} refus\xE9(s)` : ""} \u2192 ${auditPath}` : `\u2713 ${r.applied} criterion(ia) adjudicated, ${r.stillManual} left residual${r.rejected ? `, ${r.rejected} refused` : ""} \u2192 ${auditPath}`
66619
67026
  );
66620
67027
  return 0;
66621
67028
  }
@@ -66654,11 +67061,11 @@ async function cmdJudge(p) {
66654
67061
  }
66655
67062
  const max = typeof p.flags.max === "string" ? Number(p.flags.max) : void 0;
66656
67063
  const truncated = max !== void 0 && Number.isFinite(max) && max > 0 && items.length > max;
66657
- if (truncated && p.flags.apply === true) {
67064
+ if (truncated && p.flags.apply === true && p.flags.strict === true) {
66658
67065
  console.error(
66659
- lang === "fr" ? `ultra11y judge : --max ${max} ne couvre que ${max} des ${items.length} crit\xE8res, et --apply exige une adjudication COMPL\xC8TE (le gate refuse une couverture partielle).
66660
- Retirez --max pour tout adjuger, ou retirez --apply pour produire la worklist et l'appliquer plus tard.` : `ultra11y judge: --max ${max} covers only ${max} of ${items.length} criteria, and --apply requires a COMPLETE adjudication (the gate refuses partial coverage).
66661
- Drop --max to adjudicate everything, or drop --apply to produce the worklist and apply it later.`
67066
+ lang === "fr" ? `ultra11y judge : --max ${max} ne couvre que ${max} des ${items.length} crit\xE8res, et --apply --strict exige une adjudication COMPL\xC8TE (le fold tout-ou-rien refuse une couverture partielle).
67067
+ Retirez --strict pour appliquer ce qui est couvert, retirez --max pour tout adjuger, ou retirez --apply pour produire la worklist et l'appliquer plus tard.` : `ultra11y judge: --max ${max} covers only ${max} of ${items.length} criteria, and --apply --strict requires a COMPLETE adjudication (the all-or-nothing fold refuses partial coverage).
67068
+ Drop --strict to apply what is covered, drop --max to adjudicate everything, or drop --apply to produce the worklist and apply it later.`
66662
67069
  );
66663
67070
  return 2;
66664
67071
  }
@@ -66697,19 +67104,36 @@ async function cmdJudge(p) {
66697
67104
  return 0;
66698
67105
  }
66699
67106
  const adj = JSON.parse(readText(w.todoPath));
66700
- const r = applyAdjudication(audit2, adj);
66701
- if (!r.ok) {
67107
+ const r = applyAdjudication(audit2, adj, { strict: p.flags.strict === true });
67108
+ if (!r.ok && (p.flags.strict === true || r.applied + r.stillManual === 0)) {
66702
67109
  console.error(lang === "fr" ? `\u2717 Adjudication rejet\xE9e (${r.issues.length} probl\xE8me(s)) :` : `\u2717 Adjudication rejected (${r.issues.length} issue(s)):`);
66703
67110
  for (const i2 of r.issues.slice(0, 40)) console.error(` \u2717 ${i2}`);
66704
67111
  if (r.issues.length > 40) console.error(` \u2026 +${r.issues.length - 40}`);
66705
67112
  return 1;
66706
67113
  }
66707
- mkdirSync17(out2, { recursive: true });
66708
- const auditPath = join50(out2, "audit-latest.json");
66709
- writeFileSync19(auditPath, `${JSON.stringify(r.audit, null, 2)}
67114
+ if (r.rejected > 0) {
67115
+ console.error(
67116
+ lang === "fr" ? `\u26A0 ${r.rejected} crit\xE8re(s) refus\xE9(s) par la porte \u2014 ils restent \xE0 \xE9valuer, avec le motif du refus :` : `\u26A0 ${r.rejected} criterion(ia) refused by the gate \u2014 they stay to assess, carrying the refusal:`
67117
+ );
67118
+ for (const i2 of r.issues.slice(0, 40)) console.error(` \u2717 ${i2}`);
67119
+ if (r.issues.length > 40) console.error(` \u2026 +${r.issues.length - 40}`);
67120
+ }
67121
+ mkdirSync18(out2, { recursive: true });
67122
+ const auditPath = join51(out2, "audit-latest.json");
67123
+ writeFileSync20(auditPath, `${JSON.stringify(r.audit, null, 2)}
66710
67124
  `);
67125
+ const ledgerOut = ledgerTarget(p, adj.standard);
67126
+ if (ledgerOut) {
67127
+ const refused = new Set(r.rejectedCriteria);
67128
+ const accepted = new Set(adj.items.map((it) => it.criteriaId).filter((id) => !refused.has(id)));
67129
+ const fresh = entriesFrom(adj, accepted, r.audit.date);
67130
+ writeLedger(ledgerOut, mergeLedger(readLedger(ledgerOut), adj.standard, fresh));
67131
+ console.log(
67132
+ lang === "fr" ? `\u2713 ${fresh.length} verdict(s) enregistr\xE9(s) au registre \u2192 ${ledgerOut}` : `\u2713 ${fresh.length} verdict(s) recorded in the ledger \u2192 ${ledgerOut}`
67133
+ );
67134
+ }
66711
67135
  console.log(
66712
- lang === "fr" ? `\u2713 ${r.applied} crit\xE8re(s) adjug\xE9(s), ${r.stillManual} laiss\xE9(s) en r\xE9siduel \u2192 ${auditPath}` : `\u2713 ${r.applied} criterion(ia) adjudicated, ${r.stillManual} left residual \u2192 ${auditPath}`
67136
+ lang === "fr" ? `\u2713 ${r.applied} crit\xE8re(s) adjug\xE9(s), ${r.stillManual} laiss\xE9(s) en r\xE9siduel${r.rejected ? `, ${r.rejected} refus\xE9(s)` : ""} \u2192 ${auditPath}` : `\u2713 ${r.applied} criterion(ia) adjudicated, ${r.stillManual} left residual${r.rejected ? `, ${r.rejected} refused` : ""} \u2192 ${auditPath}`
66713
67137
  );
66714
67138
  return 0;
66715
67139
  }
@@ -66775,7 +67199,7 @@ async function cmdScan(p) {
66775
67199
  return 0;
66776
67200
  }
66777
67201
  for (const target of p.positionals.filter((a) => a !== "-")) {
66778
- if (/^https?:\/\//i.test(target) || existsSync34(target)) continue;
67202
+ if (/^https?:\/\//i.test(target) || existsSync35(target)) continue;
66779
67203
  console.error(
66780
67204
  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.`
66781
67205
  );
@@ -66917,7 +67341,7 @@ async function cmdScan(p) {
66917
67341
  }
66918
67342
  let merged = mergeDynamic(audit2, dynamic, lang);
66919
67343
  if (dynamic.snapshots?.length) {
66920
- const doms = dynamic.snapshots.map((id) => join50(snapshotRoot ?? ".", PAGES_DIR, id, "dom.html")).filter((f) => existsSync34(f));
67344
+ const doms = dynamic.snapshots.map((id) => join51(snapshotRoot ?? ".", PAGES_DIR, id, "dom.html")).filter((f) => existsSync35(f));
66921
67345
  if (doms.length) {
66922
67346
  const snapAudit = runAudit({ inputs: doms, onWarn: (m) => console.error(m) });
66923
67347
  merged = mergeSnapshotAudit(merged, snapAudit);
@@ -66928,12 +67352,12 @@ async function cmdScan(p) {
66928
67352
  attributePages(merged, scope);
66929
67353
  }
66930
67354
  }
66931
- mkdirSync17(out2, { recursive: true });
66932
- writeFileSync19(join50(out2, "audit-latest.json"), JSON.stringify(merged, null, 2) + "\n");
67355
+ mkdirSync18(out2, { recursive: true });
67356
+ writeFileSync20(join51(out2, "audit-latest.json"), JSON.stringify(merged, null, 2) + "\n");
66933
67357
  if (p.flags.json) console.log(JSON.stringify(merged, null, 2));
66934
67358
  else {
66935
67359
  console.log(
66936
- lang === "fr" ? `Audit statique + dynamique fusionn\xE9 \u2192 ${join50(out2, "audit-latest.json")} (${merged.conformancePct}% r\xE9ussite, ${merged.findings.length} findings).` : `Static + dynamic audit merged \u2192 ${join50(out2, "audit-latest.json")} (${merged.conformancePct}% pass rate, ${merged.findings.length} findings).`
67360
+ lang === "fr" ? `Audit statique + dynamique fusionn\xE9 \u2192 ${join51(out2, "audit-latest.json")} (${merged.conformancePct}% r\xE9ussite, ${merged.findings.length} findings).` : `Static + dynamic audit merged \u2192 ${join51(out2, "audit-latest.json")} (${merged.conformancePct}% pass rate, ${merged.findings.length} findings).`
66937
67361
  );
66938
67362
  if (dynamic.snapshots?.length)
66939
67363
  console.log(
@@ -67175,9 +67599,9 @@ async function cmdImport(p) {
67175
67599
  return 1;
67176
67600
  }
67177
67601
  if (outDir) {
67178
- mkdirSync17(outDir, { recursive: true });
67179
- const rawFile = join50(outDir, `external-${adapterId}-${arg}.raw.json`);
67180
- writeFileSync19(rawFile, rawText.endsWith("\n") ? rawText : `${rawText}
67602
+ mkdirSync18(outDir, { recursive: true });
67603
+ const rawFile = join51(outDir, `external-${adapterId}-${arg}.raw.json`);
67604
+ writeFileSync20(rawFile, rawText.endsWith("\n") ? rawText : `${rawText}
67181
67605
  `);
67182
67606
  if (!p.flags.json) console.log(rawFile);
67183
67607
  }
@@ -67211,9 +67635,9 @@ async function cmdImport(p) {
67211
67635
  return 0;
67212
67636
  }
67213
67637
  if (outDir) {
67214
- mkdirSync17(outDir, { recursive: true });
67215
- const file = join50(outDir, "external-latest.json");
67216
- writeFileSync19(file, `${JSON.stringify(audit2, null, 2)}
67638
+ mkdirSync18(outDir, { recursive: true });
67639
+ const file = join51(outDir, "external-latest.json");
67640
+ writeFileSync20(file, `${JSON.stringify(audit2, null, 2)}
67217
67641
  `);
67218
67642
  console.log(file);
67219
67643
  } else {
@@ -67318,7 +67742,7 @@ function cmdOrchestrate(p) {
67318
67742
  }
67319
67743
  const engineAbs = realpathSync6(fileURLToPath5(import.meta.url));
67320
67744
  if (p.flags.list === true) {
67321
- if (!existsSync34(runFlag)) {
67745
+ if (!existsSync35(runFlag)) {
67322
67746
  console.error(`ultra11y orchestrate: run dir not found: ${runFlag}.`);
67323
67747
  return 2;
67324
67748
  }
@@ -67345,7 +67769,7 @@ function cmdOrchestrate(p) {
67345
67769
  );
67346
67770
  } else {
67347
67771
  console.log(
67348
- lang === "fr" ? `Suivez ${join50(runFlag, "orchestration", "RUNBOOK.md")} s\xE9quentiellement (chemin \xE9co).` : `Follow ${join50(runFlag, "orchestration", "RUNBOOK.md")} sequentially (the eco path).`
67772
+ lang === "fr" ? `Suivez ${join51(runFlag, "orchestration", "RUNBOOK.md")} s\xE9quentiellement (chemin \xE9co).` : `Follow ${join51(runFlag, "orchestration", "RUNBOOK.md")} sequentially (the eco path).`
67349
67773
  );
67350
67774
  }
67351
67775
  if (p.flags.phase === void 0 && workflows.length === 0 && p.flags.eco !== true) {
@@ -67400,7 +67824,7 @@ async function main(argv) {
67400
67824
  if (typeof v === "string" && v && !allowed.includes(v)) console.error(`ultra11y: --${flag} "${v}" is not one of ${allowed.join("|")} \u2014 using the default.`);
67401
67825
  }
67402
67826
  const packList = typeof p.flags.pack === "string" ? p.flags.pack.split(",").map((s) => s.trim()).filter(Boolean) : [];
67403
- const configRoot = p.command === "mcp" && typeof p.flags.cwd === "string" && p.flags.cwd ? resolve15(p.flags.cwd) : process.cwd();
67827
+ const configRoot = p.command === "mcp" && typeof p.flags.cwd === "string" && p.flags.cwd ? resolve16(p.flags.cwd) : process.cwd();
67404
67828
  const loaded2 = loadRuntimeStandards(configRoot, packList, (m) => console.error(m), p.flags.override === true);
67405
67829
  if (loaded2.errors.length) {
67406
67830
  for (const e of loaded2.errors) console.error(`ultra11y: ${e}`);