ultra11y 2.31.2 → 2.31.3

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 writeFileSync17, mkdirSync as mkdirSync15, existsSync as existsSync31, readFileSync as readFileSync27, appendFileSync } from "fs";
4
+ import { realpathSync as realpathSync6, writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, existsSync as existsSync31, readFileSync as readFileSync27, appendFileSync, copyFileSync as copyFileSync4 } from "fs";
5
5
  import { join as join45, relative as relative5, sep as sep7, dirname as dirname14 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "2.31.2";
9
+ var VERSION = "2.31.3";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -1197,7 +1197,7 @@ var wcag_universe_default = {
1197
1197
  wcagVersion: "2.2",
1198
1198
  source: "https://www.w3.org/TR/WCAG22/",
1199
1199
  criteriaSource: "https://github.com/w3c/wcag",
1200
- provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-11 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
1200
+ provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-12 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
1201
1201
  criteria: [
1202
1202
  {
1203
1203
  id: "1.1.1",
@@ -41345,7 +41345,8 @@ var rgaa_default = {
41345
41345
  wcag: ["1.4.2"],
41346
41346
  appliesTo: {
41347
41347
  ruleIds: ["autoplay-media", "axe:no-autoplay-audio"]
41348
- }
41348
+ },
41349
+ judgment: true
41349
41350
  },
41350
41351
  {
41351
41352
  id: "4.11",
@@ -41984,7 +41985,8 @@ var rgaa_default = {
41984
41985
  wcag: ["3.1.1"],
41985
41986
  appliesTo: {
41986
41987
  ruleIds: ["axe:html-lang-valid", "lang-invalid"]
41987
- }
41988
+ },
41989
+ judgment: true
41988
41990
  },
41989
41991
  {
41990
41992
  id: "8.5",
@@ -42020,7 +42022,8 @@ var rgaa_default = {
42020
42022
  wcag: ["2.4.2"],
42021
42023
  appliesTo: {
42022
42024
  ruleIds: []
42023
- }
42025
+ },
42026
+ judgment: true
42024
42027
  },
42025
42028
  {
42026
42029
  id: "8.7",
@@ -43414,7 +43417,8 @@ var rgaa_default = {
43414
43417
  wcag: ["1.1.1", "1.3.1", "1.3.2", "2.4.1", "2.4.3", "3.1.1", "4.1.2"],
43415
43418
  appliesTo: {
43416
43419
  ruleIds: []
43417
- }
43420
+ },
43421
+ judgment: true
43418
43422
  },
43419
43423
  {
43420
43424
  id: "13.4",
@@ -43436,7 +43440,8 @@ var rgaa_default = {
43436
43440
  wcag: ["1.1.1", "1.3.1", "1.3.2", "2.4.1", "2.4.3", "3.1.1", "4.1.2"],
43437
43441
  appliesTo: {
43438
43442
  ruleIds: []
43439
- }
43443
+ },
43444
+ judgment: true
43440
43445
  },
43441
43446
  {
43442
43447
  id: "13.5",
@@ -45444,6 +45449,10 @@ function title(pack, c2, lang) {
45444
45449
  function titlePlain(pack, c2, lang) {
45445
45450
  return localize(pack, c2.titlePlain, lang);
45446
45451
  }
45452
+ function packTestIds(pack, id) {
45453
+ const tests = getCriterion(pack, id)?.tests;
45454
+ return tests ? Object.keys(tests).map((k) => `${id}.${k}`) : [];
45455
+ }
45447
45456
  function resolveGlossary(packKey, anchor) {
45448
45457
  return packGlossary(packKey)?.[anchor];
45449
45458
  }
@@ -48357,7 +48366,7 @@ function decide(payload, deps = {}) {
48357
48366
  }
48358
48367
 
48359
48368
  // src/report.ts
48360
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
48369
+ import { copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
48361
48370
  import { join as join29, relative as relative3 } from "path";
48362
48371
 
48363
48372
  // src/prd.ts
@@ -48384,7 +48393,7 @@ function packCriteriaForFinding(pack, finding) {
48384
48393
  return pack.criteria.filter((pc) => pc.wcag.includes(finding.criteriaId) && (!pc.appliesTo || ruleMatches(finding.ruleId, pc.appliesTo.ruleIds))).map((pc) => pc.id);
48385
48394
  }
48386
48395
  function packConformancePct(derived) {
48387
- const c2 = derived.filter((d) => d.status === "C").length;
48396
+ const c2 = derived.filter((d) => d.status === "C" && d.decidedBy !== "agent").length;
48388
48397
  const nc = derived.filter((d) => d.status === "NC").length;
48389
48398
  return c2 + nc === 0 ? 100 : Math.round(c2 / (c2 + nc) * 100);
48390
48399
  }
@@ -48413,6 +48422,10 @@ function applySecondaryMappings(base, pc, enabled, sources, defaultLocale) {
48413
48422
  if (added.some((f) => !f.advisory)) return { id: pc.id, theme: pc.theme, status: aggregate2([base.status, "NC"]), findings, scs: base.scs };
48414
48423
  return { ...base, findings };
48415
48424
  }
48425
+ function judgmentGuard(r, pc) {
48426
+ if (!pc.judgment || r.status !== "C") return r;
48427
+ return { ...r, status: "manual", judgment: true };
48428
+ }
48416
48429
  function derivePackResults(audit2, packKey) {
48417
48430
  const pack = loadPack(packKey);
48418
48431
  const byScId = new Map(audit2.criteria.map((c2) => [c2.id, c2]));
@@ -48460,7 +48473,7 @@ function derivePackResults(audit2, packKey) {
48460
48473
  decidedBy: "agent"
48461
48474
  };
48462
48475
  }
48463
- const base = deriveBase(pc);
48476
+ const base = judgmentGuard(deriveBase(pc), pc);
48464
48477
  return enabledSecondary.length ? applySecondaryMappings(base, pc, enabledSecondary, secondarySources, pack.defaultLocale) : base;
48465
48478
  });
48466
48479
  }
@@ -48654,7 +48667,7 @@ function renderAuditorUnit(unit, standard, lang, opts = {}) {
48654
48667
  const pc = pack.criteria.find((c2) => c2.id === unit.criteriaId);
48655
48668
  if (pc) out2.push(`**${v.theme}** : ${pc.theme}. ${themeName(pack, pc.theme, lang) ?? ""}`.trimEnd());
48656
48669
  out2.push(`**${v.criterion}** : ${unit.criteriaId} \u2014 ${unit.title}`);
48657
- const testNums = pc?.tests ? Object.keys(pc.tests).map((k) => `${unit.criteriaId}.${k}`) : [];
48670
+ const testNums = packTestIds(pack, unit.criteriaId);
48658
48671
  if (testNums.length) out2.push(`**${v.test}(s)** : ${testNums.join(" \xB7 ")}`);
48659
48672
  if (unit.refs.length) out2.push(`**WCAG** : ${unit.refs.map((sc) => `${sc}${scLevel(sc)}`).join(" \xB7 ")}`);
48660
48673
  }
@@ -49066,8 +49079,8 @@ function toRuleOnSection(r, standard, lang) {
49066
49079
  for (const pc of manual) {
49067
49080
  const crit = pack.criteria.find((c2) => c2.id === pc.id);
49068
49081
  const title2 = crit ? titlePlain(pack, crit, lang) : pc.id;
49069
- const tests = Object.keys(crit?.tests ?? {});
49070
- out2.push(`- [ ] **${pack.name} ${pc.id}** \u2014 ${title2}${tests.length ? ` \xB7 ${s.tests}: ${tests.map((k) => `\`${pc.id}.${k}\``).join(" ")}` : ""}`);
49082
+ const tests = packTestIds(pack, pc.id);
49083
+ out2.push(`- [ ] **${pack.name} ${pc.id}** \u2014 ${title2}${tests.length ? ` \xB7 ${s.tests}: ${tests.map((t2) => `\`${t2}\``).join(" ")}` : ""}`);
49071
49084
  }
49072
49085
  out2.push("");
49073
49086
  return out2;
@@ -49340,6 +49353,8 @@ var L4 = {
49340
49353
  sev: { bloquant: "Bloquant", majeur: "Majeur", mineur: "Mineur" },
49341
49354
  none: "Aucune non-conformit\xE9 d\xE9tect\xE9e par le moteur statique.",
49342
49355
  cTitle: "3. Crit\xE8res conformes (C)",
49356
+ cAgentTitle: "Conformes par adjudication de l'agent (jugement, non prouv\xE9 par le moteur)",
49357
+ cAgentNote: "Ces crit\xE8res ont \xE9t\xE9 tranch\xE9s par l'agent IA \xE0 partir des \xE9vidences cit\xE9es, non d\xE9cid\xE9s par le moteur d\xE9terministe. Ils sont gat\xE9s (chaque verdict cite une \xE9vidence r\xE9solvable) mais restent un jugement : ils ne sont pas compt\xE9s dans le taux de r\xE9ussite automatique ci-dessus.",
49343
49358
  naTitle: "4. Crit\xE8res non applicables (NA)",
49344
49359
  manualTitle: "5. Crit\xE8res \xE0 adjuger (jugement / rendu) \u2014 non d\xE9cid\xE9s par le moteur statique",
49345
49360
  manualWarn: "Adjugez-les avec `verify --manual` (l'agent d\xE9cide depuis la source, de fa\xE7on gat\xE9e) ; les crit\xE8res de rendu passent par `scan`. Aucun ne doit \xEAtre marqu\xE9 \xAB conforme \xBB sans justification enregistr\xE9e et gat\xE9e.",
@@ -49347,6 +49362,7 @@ var L4 = {
49347
49362
  manualHowTo: "G\xE9n\xE9rez la worklist : `verify --manual --in <audit.json> --standard <pack> --out <dir>`. Chaque item y porte l'\xE9nonc\xE9 complet de ses tests, sa note technique, ses cas particuliers, sa guidance et les termes que le r\xE9f\xE9rentiel d\xE9finit.",
49348
49363
  outOfScope: "Hors p\xE9rim\xE8tre moteur \u2014 mapp\xE9 sur des SC hors WCAG 2.2 AA ; v\xE9rification manuelle.",
49349
49364
  scopedOut: "Les non-conformit\xE9s WCAG relev\xE9es concernent des \xE9l\xE9ments hors du p\xE9rim\xE8tre de ce crit\xE8re \u2014 \xE0 \xE9valuer s\xE9par\xE9ment.",
49365
+ judgment: "L\u2019\xE9nonc\xE9 du crit\xE8re demande davantage que les CS WCAG auxquels il est rattach\xE9 \u2014 le moteur n\u2019y a pas r\xE9pondu, \xE0 trancher.",
49350
49366
  nothing: "Aucun.",
49351
49367
  dedup: "D\xE9dup",
49352
49368
  canonical: "fichier(s) canonique(s) audit\xE9(s)",
@@ -49392,6 +49408,8 @@ var L4 = {
49392
49408
  sev: { bloquant: "Blocking", majeur: "Major", mineur: "Minor" },
49393
49409
  none: "No non-conformity detected by the static engine.",
49394
49410
  cTitle: "3. Conforming criteria (C)",
49411
+ cAgentTitle: "Conforming by agent adjudication (judgement, not proven by the engine)",
49412
+ cAgentNote: "These criteria were ruled on by the AI agent from the evidence it cited, not decided by the deterministic engine. They are gated (every verdict cites resolvable evidence) but remain a judgement: they are not counted in the automatic pass rate above.",
49395
49413
  naTitle: "4. Not-applicable criteria (NA)",
49396
49414
  manualTitle: "5. Criteria to adjudicate (judgment / rendering) \u2014 not decided by the static engine",
49397
49415
  manualWarn: "Adjudicate these with `verify --manual` (the agent decides from source, gated); rendering criteria go to `scan`. None may be marked \u201Cconforming\u201D without a recorded, gated justification.",
@@ -49399,6 +49417,7 @@ var L4 = {
49399
49417
  manualHowTo: "Generate the worklist: `verify --manual --in <audit.json> --standard <pack> --out <dir>`. Each item carries the full wording of its tests, its technical note, its particular cases, its guidance and the terms the standard defines.",
49400
49418
  outOfScope: "Out of engine scope \u2014 mapped to SCs outside WCAG 2.2 AA; manual verification.",
49401
49419
  scopedOut: "The WCAG failures found concern elements outside this criterion's scope \u2014 assess separately.",
49420
+ judgment: "The criterion asks more than the WCAG SCs it maps to \u2014 the engine did not answer it; rule on it.",
49402
49421
  nothing: "None.",
49403
49422
  dedup: "Dedup",
49404
49423
  canonical: "canonical file(s) audited",
@@ -49507,11 +49526,18 @@ function render(r, lang, opts) {
49507
49526
  const notes = pageScope.find((x) => x.id === pg.id)?.notes;
49508
49527
  if (notes) out2.push(`- _${notes}_`);
49509
49528
  const shot = join29(PAGES_DIR, pg.id, "screen.png");
49510
- if (existsSync16(shot))
49511
- out2.push(
49512
- "",
49513
- `![${s.screenshotAlt(pg.name)}](${relative3(opts.outDir ?? ".", shot).split("\\").join("/")})`
49514
- );
49529
+ if (existsSync16(shot)) {
49530
+ let href = relative3(opts.outDir ?? ".", shot).split("\\").join("/");
49531
+ if (opts.outDir) {
49532
+ try {
49533
+ mkdirSync7(join29(opts.outDir, "assets"), { recursive: true });
49534
+ copyFileSync(shot, join29(opts.outDir, "assets", `${pg.id}.png`));
49535
+ href = `./assets/${pg.id}.png`;
49536
+ } catch {
49537
+ }
49538
+ }
49539
+ out2.push("", `![${s.screenshotAlt(pg.name)}](${href})`);
49540
+ }
49515
49541
  for (const f of nc.slice(0, 30)) {
49516
49542
  const crits = pack ? packCriteriaForFinding(pack, f) : [];
49517
49543
  const label = crits.length ? crits.join(", ") : f.criteriaId;
@@ -49522,7 +49548,16 @@ function render(r, lang, opts) {
49522
49548
  }
49523
49549
  out2.push(`## ${s.cTitle}`, "");
49524
49550
  const conform = rows.filter((x) => x.status === "C");
49525
- out2.push(conform.length ? conform.map((x) => `- ${x.label}`).join("\n") : s.nothing, "");
49551
+ const byEngine = conform.filter((x) => x.decidedBy !== "agent");
49552
+ const byAgent = conform.filter((x) => x.decidedBy === "agent");
49553
+ if (!conform.length) out2.push(s.nothing, "");
49554
+ else {
49555
+ if (byEngine.length) out2.push(...byEngine.map((x) => `- ${x.label}`), "");
49556
+ if (byAgent.length) {
49557
+ out2.push(`### ${s.cAgentTitle}`, "", `> ${s.cAgentNote}`, "");
49558
+ out2.push(...byAgent.map((x) => `- ${x.label}${x.justification ? ` \u2014 _${x.justification}_` : ""}`), "");
49559
+ }
49560
+ }
49526
49561
  out2.push(`## ${s.naTitle}`, "");
49527
49562
  const na = rows.filter((x) => x.status === "NA");
49528
49563
  out2.push(na.length ? na.map((x) => `- ${x.label}${x.justification ? ` \u2014 _${x.justification}_` : ""}`).join("\n") : s.nothing, "");
@@ -49532,8 +49567,8 @@ function render(r, lang, opts) {
49532
49567
  else {
49533
49568
  const pack5 = isCore(opts.standard) ? void 0 : loadPack(opts.standard);
49534
49569
  for (const x of manual) {
49535
- const tests = pack5 ? Object.keys(getCriterion(pack5, x.id)?.tests ?? {}) : [];
49536
- const testRef = tests.length ? ` \u2014 ${s.testsToRule}: ${tests.map((k) => `\`${x.id}.${k}\``).join(" \xB7 ")}` : "";
49570
+ const tests = pack5 ? packTestIds(pack5, x.id) : [];
49571
+ const testRef = tests.length ? ` \u2014 ${s.testsToRule}: ${tests.map((t2) => `\`${t2}\``).join(" \xB7 ")}` : "";
49537
49572
  out2.push(`- ${x.label}${x.justification ? ` \u2014 _${x.justification}_` : ""}${testRef}`);
49538
49573
  }
49539
49574
  out2.push("", `> ${s.manualHowTo}`, "");
@@ -49545,7 +49580,14 @@ function renderReport(r, lang = "en", outDir) {
49545
49580
  const byGuideline = /* @__PURE__ */ new Map();
49546
49581
  for (const c2 of r.criteria) {
49547
49582
  const title2 = scTitle(c2.id, lang);
49548
- const row = { id: c2.id, label: title2 ? `${c2.id} \u2014 ${title2}` : c2.id, status: c2.status, findings: c2.findings, justification: c2.justification };
49583
+ const row = {
49584
+ id: c2.id,
49585
+ label: title2 ? `${c2.id} \u2014 ${title2}` : c2.id,
49586
+ status: c2.status,
49587
+ findings: c2.findings,
49588
+ justification: c2.justification,
49589
+ decidedBy: c2.decidedBy
49590
+ };
49549
49591
  (byGuideline.get(c2.guideline) ?? byGuideline.set(c2.guideline, []).get(c2.guideline)).push(row);
49550
49592
  }
49551
49593
  const groups = r.guidelines.map((g) => ({ key: g.key, title: guidelineTitle(g.key, lang) ?? g.title, rows: byGuideline.get(g.key) ?? [] }));
@@ -49564,9 +49606,10 @@ function renderPackReport(r, pack, lang = "en", outDir) {
49564
49606
  label: `${pack.name} ${pr.id} \u2014 ${title(pack, pc, lang)}`,
49565
49607
  status: pr.status,
49566
49608
  findings: pr.findings,
49609
+ ...pr.decidedBy ? { decidedBy: pr.decidedBy } : {},
49567
49610
  // outOfScope / scopedOut criteria are "manual" (not NA) with their own dedicated
49568
49611
  // justification — never mixed with the ordinary NA reason (see the manual section above).
49569
- ...pr.outOfScope ? { justification: s.outOfScope } : pr.scopedOut ? { justification: s.scopedOut } : pr.status === "NA" ? { justification: naReason } : {}
49612
+ ...pr.outOfScope ? { justification: s.outOfScope } : pr.scopedOut ? { justification: s.scopedOut } : pr.judgment ? { justification: s.judgment } : pr.status === "NA" ? { justification: naReason } : {}
49570
49613
  };
49571
49614
  (byTheme.get(pr.theme) ?? byTheme.set(pr.theme, []).get(pr.theme)).push(row);
49572
49615
  }
@@ -49578,7 +49621,11 @@ function renderPackReport(r, pack, lang = "en", outDir) {
49578
49621
  derivedOf: std,
49579
49622
  standard: pack.key,
49580
49623
  partialAudit: untestedNeedsRendering(r),
49581
- headerRatePct: packConformancePct(derived)
49624
+ headerRatePct: packConformancePct(derived),
49625
+ // Forwarded, unlike before: without it the per-page screenshots resolved against the
49626
+ // CWD instead of the report's own directory, so a pack report written to `audits/`
49627
+ // carried links that only worked when read from the repo root.
49628
+ outDir
49582
49629
  });
49583
49630
  }
49584
49631
  function writeReport(r, opts) {
@@ -50462,9 +50509,12 @@ var L6 = {
50462
50509
  viewport: "Fen\xEAtre",
50463
50510
  producer: "Producteur",
50464
50511
  auth: "Authentification requise",
50465
- rate: "Taux de conformit\xE9",
50512
+ rate: "Taux de r\xE9ussite automatique (v\xE9rifications statiques)",
50466
50513
  rateNote: "sous-ensemble d\xE9cidable : C \xF7 (C + NC)",
50467
50514
  tally: (c2, nc, na, m) => `${c2} conforme(s) \xB7 ${nc} non conforme(s) \xB7 ${na} non applicable(s) \xB7 ${m} \xE0 \xE9valuer`,
50515
+ coverage: (decided, total) => `Couverture : ${decided}/${total} crit\xE8re(s) \xE9valu\xE9(s) \u2014 le taux ci-dessus ne porte que sur eux, et ne dit rien des ${total - decided} autres.`,
50516
+ tests: "Tests",
50517
+ agentMark: "`C*` : conformit\xE9 tranch\xE9e par l'agent IA \xE0 partir des \xE9vidences cit\xE9es (gat\xE9), et non prouv\xE9e par le moteur d\xE9terministe.",
50468
50518
  screenshotAlt: (n) => `Capture d'\xE9cran de la page ${n}`,
50469
50519
  noScreenshot: "Aucune capture d'\xE9cran pour cette page (le producteur n'en a pas fourni) \u2014 le tier pixel est donc inactif ici.",
50470
50520
  gridTitle: "Grille des crit\xE8res",
@@ -50499,9 +50549,12 @@ var L6 = {
50499
50549
  viewport: "Viewport",
50500
50550
  producer: "Producer",
50501
50551
  auth: "Authentication required",
50502
- rate: "Conformance rate",
50552
+ rate: "Automatic static-check pass rate",
50503
50553
  rateNote: "decidable subset: C \xF7 (C + NC)",
50504
50554
  tally: (c2, nc, na, m) => `${c2} conforming \xB7 ${nc} non-conforming \xB7 ${na} not applicable \xB7 ${m} to assess`,
50555
+ coverage: (decided, total) => `Coverage: ${decided}/${total} criteria assessed \u2014 the rate above covers only those, and says nothing about the other ${total - decided}.`,
50556
+ tests: "Tests",
50557
+ agentMark: "`C*`: conformity ruled by the AI agent from the evidence it cited (gated), not proven by the deterministic engine.",
50505
50558
  screenshotAlt: (n) => `Screenshot of the ${n} page`,
50506
50559
  noScreenshot: "No screenshot for this page (the producer supplied none) \u2014 the pixel tier is therefore inactive here.",
50507
50560
  gridTitle: "Criteria grid",
@@ -50524,15 +50577,24 @@ var L6 = {
50524
50577
  };
50525
50578
  function rowsFor(result, page, standard, lang) {
50526
50579
  if (isCore(standard)) {
50527
- return [...page.criteria].sort((a, b) => compareSC(a.id, b.id)).map((c2) => ({ id: c2.id, label: `${c2.id} ${scTitle(c2.id, lang) ?? ""}`.trim(), group: c2.guideline, status: c2.status }));
50580
+ return [...page.criteria].sort((a, b) => compareSC(a.id, b.id)).map((c2) => ({
50581
+ id: c2.id,
50582
+ label: `${c2.id} ${scTitle(c2.id, lang) ?? ""}`.trim(),
50583
+ group: c2.guideline,
50584
+ status: c2.status,
50585
+ tests: [],
50586
+ decidedBy: c2.decidedBy
50587
+ }));
50528
50588
  }
50529
50589
  const pack = loadPack(standard);
50530
- const byId2 = new Map(derivePackResults(pageView(result, page), standard).map((r) => [r.id, r.status]));
50590
+ const byId2 = new Map(derivePackResults(pageView(result, page), standard).map((r) => [r.id, r]));
50531
50591
  return pack.criteria.map((pc) => ({
50532
50592
  id: pc.id,
50533
50593
  label: `${pc.id} \u2014 ${titlePlain(pack, pc, lang)}`,
50534
50594
  group: `${pc.theme}. ${themeName(pack, pc.theme, lang) ?? ""}`.trim(),
50535
- status: byId2.get(pc.id) ?? "manual"
50595
+ status: byId2.get(pc.id)?.status ?? "manual",
50596
+ tests: packTestIds(pack, pc.id),
50597
+ decidedBy: byId2.get(pc.id)?.decidedBy
50536
50598
  }));
50537
50599
  }
50538
50600
  function tally(rows) {
@@ -50562,22 +50624,28 @@ function renderPageReport(result, page, opts = {}) {
50562
50624
  const rows = rowsFor(result, page, standard, lang);
50563
50625
  const t2 = tally(rows);
50564
50626
  out2.push(`- **${s.rate}** : **${ratePct(rows)} %** _(${s.rateNote})_`);
50565
- out2.push(`- ${s.tally(t2.c, t2.nc, t2.na, t2.manual)}`, "");
50627
+ out2.push(`- ${s.tally(t2.c, t2.nc, t2.na, t2.manual)}`);
50628
+ out2.push(`- ${s.coverage(t2.c + t2.nc, rows.length)}`, "");
50566
50629
  if (page.basis !== "snapshot") out2.push(`> \u26A0\uFE0F ${s.sourceWarn}`, "");
50567
50630
  const shot = opts.screenshots?.get(page.id);
50568
50631
  if (shot) out2.push(`![${s.screenshotAlt(page.name)}](${shot})`, "");
50569
50632
  else out2.push(`_${s.noScreenshot}_`, "");
50633
+ const withTests = rows.some((r) => r.tests.length);
50570
50634
  out2.push(`${h}# ${s.gridTitle}`, "", `> ${s.gridNote}`, "");
50571
- out2.push(`| ${s.criterion} | ${s.status} |`, "| --- | --- |");
50635
+ out2.push(withTests ? `| ${s.criterion} | ${s.tests} | ${s.status} |` : `| ${s.criterion} | ${s.status} |`);
50636
+ out2.push(withTests ? "| --- | --- | --- |" : "| --- | --- |");
50572
50637
  let group = "";
50573
50638
  for (const row of rows) {
50574
50639
  if (row.group !== group) {
50575
50640
  group = row.group;
50576
- out2.push(`| **${group}** | |`);
50641
+ out2.push(withTests ? `| **${group}** | | |` : `| **${group}** | |`);
50577
50642
  }
50578
- out2.push(`| ${row.label} | ${MARK2[row.status]} |`);
50643
+ const mark = row.decidedBy === "agent" && row.status === "C" ? `${MARK2[row.status]}*` : MARK2[row.status];
50644
+ if (withTests) out2.push(`| ${row.label} | ${row.tests.map((t3) => `\`${t3}\``).join(" ")} | ${mark} |`);
50645
+ else out2.push(`| ${row.label} | ${mark} |`);
50579
50646
  }
50580
50647
  out2.push("", `> ${s.manualWarn}`, "");
50648
+ if (rows.some((r) => r.decidedBy === "agent" && r.status === "C")) out2.push(`> ${s.agentMark}`, "");
50581
50649
  const units = prdUnits(pageView(result, page), standard, lang);
50582
50650
  const ncUnits = units.filter((u) => !u.advisory);
50583
50651
  const advUnits = units.filter((u) => u.advisory);
@@ -51786,12 +51854,23 @@ function applyAdjudication(audit2, adj, opts = {}) {
51786
51854
  const issues = [];
51787
51855
  const byId2 = new Map(adj.items.map((it) => [it.criteriaId, it]));
51788
51856
  const packMode = !isCore(adj.standard);
51857
+ const open = /* @__PURE__ */ new Set();
51789
51858
  if (packMode) {
51790
51859
  for (const pc of derivePackResults(audit2, adj.standard)) {
51791
- if (pc.status === "manual" && !byId2.has(pc.id)) issues.push(`criterion ${pc.id}: missing from the adjudication (coverage gap)`);
51860
+ if (pc.status !== "manual") continue;
51861
+ open.add(pc.id);
51862
+ if (!byId2.has(pc.id)) issues.push(`criterion ${pc.id}: missing from the adjudication (coverage gap)`);
51792
51863
  }
51793
51864
  } else {
51794
- for (const r of audit2.residualRisks) if (!byId2.has(r.criteriaId)) issues.push(`criterion ${r.criteriaId}: missing from the adjudication (coverage gap)`);
51865
+ for (const r of audit2.residualRisks) {
51866
+ open.add(r.criteriaId);
51867
+ if (!byId2.has(r.criteriaId)) issues.push(`criterion ${r.criteriaId}: missing from the adjudication (coverage gap)`);
51868
+ }
51869
+ }
51870
+ for (const it of adj.items) {
51871
+ if (!open.has(it.criteriaId)) {
51872
+ issues.push(`criterion ${it.criteriaId}: not open for adjudication \u2014 the engine already decided it, or it is not part of ${adj.standard}`);
51873
+ }
51795
51874
  }
51796
51875
  const groundInputs = [];
51797
51876
  for (const it of adj.items) {
@@ -51800,6 +51879,26 @@ function applyAdjudication(audit2, adj, opts = {}) {
51800
51879
  issues.push(`criterion ${it.criteriaId}: unadjudicated (verdict is null)`);
51801
51880
  } else if (v === "C" || v === "NA") {
51802
51881
  if (!it.justification || !it.justification.trim()) issues.push(`criterion ${it.criteriaId}: a ${v} verdict requires a justification`);
51882
+ const cites = it.citations ?? [];
51883
+ if (it.evidence.length === 0) {
51884
+ if (v === "C") {
51885
+ issues.push(
51886
+ `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`
51887
+ );
51888
+ }
51889
+ } else if (cites.length === 0) {
51890
+ issues.push(
51891
+ `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}])`
51892
+ );
51893
+ } else {
51894
+ const anchors = new Set(it.evidence.map((e) => `${e.file}:${e.line}`));
51895
+ for (const c2 of cites) {
51896
+ if (!anchors.has(`${c2.file}:${c2.line}`)) {
51897
+ issues.push(`criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
51898
+ }
51899
+ groundInputs.push({ file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
51900
+ }
51901
+ }
51803
51902
  } else if (v === "NC") {
51804
51903
  if (!it.findings || it.findings.length === 0) issues.push(`criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
51805
51904
  for (const f of it.findings ?? []) {
@@ -51921,7 +52020,7 @@ function recomputeTallies(a) {
51921
52020
  g.na = inG.filter((c2) => c2.status === "NA").length;
51922
52021
  g.manual = inG.filter((c2) => c2.status === "manual").length;
51923
52022
  }
51924
- const decided = a.criteria.filter((c2) => c2.status === "C" || c2.status === "NC");
52023
+ const decided = a.criteria.filter((c2) => c2.status === "NC" || c2.status === "C" && c2.decidedBy !== "agent");
51925
52024
  const conform = decided.filter((c2) => c2.status === "C").length;
51926
52025
  a.conformancePct = decided.length === 0 ? 100 : Math.round(conform / decided.length * 100);
51927
52026
  }
@@ -51932,13 +52031,13 @@ var T2 = {
51932
52031
  title: "# Adjudication des crit\xE8res \xE0 \xE9valuer (ultra11y)",
51933
52032
  intro: "Pour CHAQUE crit\xE8re, lisez les \xE9vidences ci-dessous (extraites de la source audit\xE9e) et attribuez un verdict dans `ADJUDICATE.todo.json` (champ `verdict`) :",
51934
52033
  verdicts: [
51935
- "- `C` \u2014 conforme (renseignez `justification`) ;",
52034
+ "- `C` \u2014 conforme (renseignez `justification` ET `citations[]` : les \xE9vidences que vous avez lev\xE9es, `file`/`line` recopi\xE9s depuis la liste du crit\xE8re) ;",
51936
52035
  "- `NC` \u2014 non conforme (ajoutez au moins un `findings[]` : file/line/message, avec un `snippet` groundable ET un `normativeRef` citant le test pr\xE9cis \xE9chou\xE9) ;",
51937
- "- `NA` \u2014 non applicable (renseignez `justification`) ;",
52036
+ "- `NA` \u2014 non applicable (renseignez `justification` ; si des \xE9vidences sont pr\xE9sent\xE9es, citez-les aussi dans `citations[]` pour dire lesquelles sortent du p\xE9rim\xE8tre) ;",
51938
52037
  "- `manual` \u2014 ind\xE9cidable statiquement (renseignez `reason` : `needs-rendered-dom` \u2192 `scan`, ou `undecidable`)."
51939
52038
  ],
51940
- rule: "> Ne signalez une NC que si un test pr\xE9cis du r\xE9f\xE9rentiel actif \xE9choue \u2014 citez-le (`normativeRef`). Une bonne pratique sans test normatif est une recommandation (`recommendations[]`, non normative). Une simple pr\xE9occupation UX n'est ni l'un ni l'autre.",
51941
- then: "Puis : `ultra11y verify --apply ADJUDICATE.todo.json --in <audit.json> --out <dir>` (\xE9choue si un verdict manque une justification/finding/reason).",
52039
+ rule: "> Ne signalez une NC que si un test pr\xE9cis du r\xE9f\xE9rentiel actif \xE9choue \u2014 citez-le (`normativeRef`). Une bonne pratique sans test normatif est une recommandation (`recommendations[]`, non normative). Une simple pr\xE9occupation UX n'est ni l'un ni l'autre.\n>\n> Sym\xE9triquement, un `C` se cite comme une NC : il faut nommer dans `citations[]` les \xE9vidences lev\xE9es. **Un crit\xE8re pr\xE9sent\xE9 sans aucune \xE9vidence ne peut pas \xEAtre `C`** \u2014 c'est `manual` (`undecidable`), ou `NA` si rien n'est concern\xE9.",
52040
+ then: "Puis : `ultra11y verify --apply ADJUDICATE.todo.json --in <audit.json> --out <dir>` (\xE9choue si un verdict manque sa justification, ses citations, son finding ou sa raison).",
51942
52041
  evidence: "\xC9vidences",
51943
52042
  none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
51944
52043
  questions: "\xC0 v\xE9rifier manuellement",
@@ -51955,13 +52054,13 @@ var T2 = {
51955
52054
  title: "# Criteria adjudication (ultra11y)",
51956
52055
  intro: "For EACH criterion, read the evidence below (harvested from the audited source) and set a verdict in `ADJUDICATE.todo.json` (field `verdict`):",
51957
52056
  verdicts: [
51958
- "- `C` \u2014 conformant (fill `justification`);",
52057
+ "- `C` \u2014 conformant (fill `justification` AND `citations[]`: the evidence you cleared, `file`/`line` copied from the criterion's own list);",
51959
52058
  "- `NC` \u2014 non-conformant (add at least one `findings[]`: file/line/message, with a groundable `snippet` AND a `normativeRef` citing the precise failed test);",
51960
- "- `NA` \u2014 not applicable (fill `justification`);",
52059
+ "- `NA` \u2014 not applicable (fill `justification`; when evidence is presented, cite it too in `citations[]` to say which items are out of scope);",
51961
52060
  "- `manual` \u2014 not statically decidable (fill `reason`: `needs-rendered-dom` \u2192 `scan`, or `undecidable`)."
51962
52061
  ],
51963
- rule: "> Report NC only if a precise test of the active standard fails \u2014 cite it (`normativeRef`). A good practice without a normative test is a recommendation (`recommendations[]`, non-normative). A purely UX concern is neither.",
51964
- then: "Then: `ultra11y verify --apply ADJUDICATE.todo.json --in <audit.json> --out <dir>` (fails if any verdict lacks its justification/finding/reason).",
52062
+ rule: "> Report NC only if a precise test of the active standard fails \u2014 cite it (`normativeRef`). A good practice without a normative test is a recommendation (`recommendations[]`, non-normative). A purely UX concern is neither.\n>\n> A `C` is cited the same way an NC is: name the evidence you cleared in `citations[]`. **A criterion presented with no evidence at all cannot be `C`** \u2014 record `manual` (`undecidable`), or `NA` if nothing in scope is concerned.",
52063
+ then: "Then: `ultra11y verify --apply ADJUDICATE.todo.json --in <audit.json> --out <dir>` (fails if any verdict lacks its justification, citations, finding or reason).",
51965
52064
  evidence: "Evidence",
51966
52065
  none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
51967
52066
  questions: "To verify manually",
@@ -52135,6 +52234,20 @@ var VERDICT_TOOL = {
52135
52234
  required: ["file", "line", "message", "normativeRef"]
52136
52235
  }
52137
52236
  },
52237
+ citations: {
52238
+ type: "array",
52239
+ description: "Required for C and NA when evidence was presented: the evidence items you cleared (or ruled out of scope). Copy `file`, `line` and `snippet` VERBATIM from the evidence list of this criterion \u2014 a citation that is not among them, or that no longer matches the source, is rejected.",
52240
+ items: {
52241
+ type: "object",
52242
+ properties: {
52243
+ file: { type: "string" },
52244
+ line: { type: "number" },
52245
+ selector: { type: "string" },
52246
+ snippet: { type: "string" }
52247
+ },
52248
+ required: ["file", "line"]
52249
+ }
52250
+ },
52138
52251
  recommendations: {
52139
52252
  type: "array",
52140
52253
  description: "Non-normative good practices. They never change a status.",
@@ -52163,7 +52276,7 @@ var SYSTEM = `You are an accessibility auditor ruling on the criteria a static e
52163
52276
  Rules, in order of importance:
52164
52277
  1. NEVER assert conformity you did not verify. "manual" with a reason is always available and is a correct answer.
52165
52278
  2. An NC must cite a real file:line taken from the evidence you were given, and the criterion's OWN numbered test as normativeRef. A citation that does not resolve against the real source is rejected downstream and wastes the whole batch.
52166
- 3. C and NA require a justification that says what you saw, not that you checked.
52279
+ 3. C and NA require a justification that says what you saw, AND a "citations" array naming the evidence items you cleared \u2014 file and line copied verbatim from the evidence presented for that criterion. A criterion presented with NO evidence cannot be C: record "manual" (reason "undecidable"), or NA if nothing in scope is concerned.
52167
52280
  4. A criterion that needs a rendered page (computed contrast, visible focus, zoom, reflow) and was given only source evidence is "manual" with reason "needs-rendered-dom".
52168
52281
  5. Rule only on the criteria presented. Never introduce another.`;
52169
52282
  var sleep = (ms) => ms <= 0 ? Promise.resolve() : new Promise((r) => setTimeout(r, ms));
@@ -52247,6 +52360,8 @@ function applyRawVerdicts(items, verdicts) {
52247
52360
  item.justification = v.justification ?? "";
52248
52361
  item.reason = v.verdict === "manual" ? v.reason ?? null : null;
52249
52362
  item.findings = v.verdict === "NC" ? v.findings ?? [] : [];
52363
+ if (v.verdict === "C" || v.verdict === "NA") item.citations = v.citations ?? [];
52364
+ else delete item.citations;
52250
52365
  if (v.recommendations?.length) item.recommendations = v.recommendations;
52251
52366
  filled++;
52252
52367
  }
@@ -54213,7 +54328,7 @@ function writeCi(root, enginePath, failOn) {
54213
54328
  }
54214
54329
 
54215
54330
  // src/install/json-edit.ts
54216
- import { copyFileSync, existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync17, renameSync as renameSync2, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "fs";
54331
+ import { copyFileSync as copyFileSync2, existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync17, renameSync as renameSync2, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "fs";
54217
54332
  import { dirname as dirname10 } from "path";
54218
54333
  var SettingsParseError = class extends Error {
54219
54334
  constructor(path, cause) {
@@ -54232,7 +54347,7 @@ function writeTextWithBackup(path, content, marker = "ultra11y") {
54232
54347
  let backup;
54233
54348
  if (existsSync22(path)) {
54234
54349
  backup = `${path}.${marker}-backup-${stamp()}`;
54235
- copyFileSync(path, backup);
54350
+ copyFileSync2(path, backup);
54236
54351
  }
54237
54352
  const tmp = `${path}.${process.pid}.tmp`;
54238
54353
  try {
@@ -54405,7 +54520,7 @@ import { existsSync as existsSync25, readFileSync as readFileSync20 } from "fs";
54405
54520
  import { join as join37 } from "path";
54406
54521
 
54407
54522
  // src/install/paths.ts
54408
- import { copyFileSync as copyFileSync2, cpSync, existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync19, realpathSync as realpathSync3, statSync as statSync10 } from "fs";
54523
+ import { copyFileSync as copyFileSync3, cpSync, existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync19, realpathSync as realpathSync3, statSync as statSync10 } from "fs";
54409
54524
  import { homedir as homedir2 } from "os";
54410
54525
  import { dirname as dirname11, join as join36 } from "path";
54411
54526
  function codexHome() {
@@ -54435,7 +54550,7 @@ function installedCliCommand() {
54435
54550
  const pin = pinnedEnginePath();
54436
54551
  if (source) {
54437
54552
  mkdirSync12(join36(pin, ".."), { recursive: true });
54438
- copyFileSync2(source, pin);
54553
+ copyFileSync3(source, pin);
54439
54554
  }
54440
54555
  return `node ${JSON.stringify(pin)}`;
54441
54556
  }
@@ -58351,27 +58466,39 @@ async function cmdPages(p) {
58351
58466
  const derived = derivePages(result, scope);
58352
58467
  const split = p.flags.split === "page";
58353
58468
  const outDir = typeof p.flags.out === "string" && p.flags.out ? p.flags.out : void 0;
58354
- const shotsFor = (fileDir) => {
58469
+ const shotOf = (id) => join45(PAGES_DIR, id, "screen.png");
58470
+ const shotsRelative = (fileDir) => {
58471
+ const m = /* @__PURE__ */ new Map();
58472
+ for (const pg of derived) {
58473
+ if (existsSync31(shotOf(pg.id))) m.set(pg.id, relative5(fileDir, shotOf(pg.id)).split("\\").join("/"));
58474
+ }
58475
+ return m;
58476
+ };
58477
+ const shotsCopiedInto = (dir) => {
58355
58478
  const m = /* @__PURE__ */ new Map();
58479
+ const assets = join45(dir, "assets");
58356
58480
  for (const pg of derived) {
58357
- const shot = join45(PAGES_DIR, pg.id, "screen.png");
58358
- if (existsSync31(shot)) m.set(pg.id, relative5(fileDir, shot).split("\\").join("/"));
58481
+ const src = shotOf(pg.id);
58482
+ if (!existsSync31(src)) continue;
58483
+ mkdirSync15(assets, { recursive: true });
58484
+ copyFileSync4(src, join45(assets, `${pg.id}.png`));
58485
+ m.set(pg.id, `./assets/${pg.id}.png`);
58359
58486
  }
58360
58487
  return m;
58361
58488
  };
58362
58489
  if (!outDir) {
58363
- console.log(renderPagesDocument(result, derived, { standard, lang, screenshots: shotsFor(".") }));
58490
+ console.log(renderPagesDocument(result, derived, { standard, lang, screenshots: shotsRelative(".") }));
58364
58491
  return 0;
58365
58492
  }
58366
58493
  mkdirSync15(outDir, { recursive: true });
58367
58494
  if (!split) {
58368
58495
  const file = join45(outDir, `pages-${result.date}.md`);
58369
- writeFileSync17(file, `${renderPagesDocument(result, derived, { standard, lang, screenshots: shotsFor(outDir) })}
58496
+ writeFileSync17(file, `${renderPagesDocument(result, derived, { standard, lang, screenshots: shotsCopiedInto(outDir) })}
58370
58497
  `);
58371
58498
  console.log(file);
58372
58499
  return 0;
58373
58500
  }
58374
- const shots = shotsFor(outDir);
58501
+ const shots = shotsCopiedInto(outDir);
58375
58502
  const sheet = (id) => `page-${id}.md`;
58376
58503
  const hrefs = new Map(derived.map((pg) => [pg.id, `./${sheet(pg.id)}`]));
58377
58504
  for (const pg of derived) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "https://www.w3.org/Translations/WCAG22-fr/",
3
- "fetchedAt": "2026-08-11",
3
+ "fetchedAt": "2026-08-12",
4
4
  "principles": {
5
5
  "1": "Perceptible",
6
6
  "2": "Utilisable",
@@ -2,7 +2,7 @@
2
2
  "wcagVersion": "2.2",
3
3
  "source": "https://www.w3.org/TR/WCAG22/",
4
4
  "criteriaSource": "https://github.com/w3c/wcag",
5
- "provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-11 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
5
+ "provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-12 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
6
6
  "principles": [
7
7
  {
8
8
  "number": 1,
@@ -5,7 +5,7 @@ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should
5
5
  allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
6
6
  license: MIT
7
7
  metadata:
8
- version: 2.31.2
8
+ version: 2.31.3
9
9
  ---
10
10
 
11
11
  # review-a11y — accessibility review of changed code