ultra11y 5.6.0 → 5.7.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.
@@ -10,7 +10,7 @@
10
10
  "name": "ultra11y",
11
11
  "source": "./",
12
12
  "description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
13
- "version": "5.6.0",
13
+ "version": "5.7.0",
14
14
  "author": { "name": "maxgfr" },
15
15
  "homepage": "https://github.com/maxgfr/ultra11y",
16
16
  "repository": "https://github.com/maxgfr/ultra11y",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ultra11y",
3
3
  "description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
4
- "version": "5.6.0",
4
+ "version": "5.7.0",
5
5
  "author": { "name": "maxgfr" },
6
6
  "homepage": "https://github.com/maxgfr/ultra11y",
7
7
  "repository": "https://github.com/maxgfr/ultra11y",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ultra11y",
3
3
  "description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
4
- "version": "5.6.0",
4
+ "version": "5.7.0",
5
5
  "author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
6
6
  "homepage": "https://github.com/maxgfr/ultra11y",
7
7
  "repository": "https://github.com/maxgfr/ultra11y",
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
27
27
  // Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
28
28
  // does not carry it, so a user's own ultra11y.js is never clobbered.
29
29
  const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
30
- const ULTRA11Y_PLUGIN_VERSION = "5.6.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.7.0";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
@@ -1,4 +1,4 @@
1
- import { S as SnapshotPayload, F as FindingLike } from './payload-BshGXnOO.js';
1
+ import { S as SnapshotPayload, F as FindingLike } from './payload-D8dvqLcc.js';
2
2
 
3
3
  type On = (event: string, handler: any) => void;
4
4
  interface TaskPayload extends SnapshotPayload {
package/dist/cypress.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CheckOptions } from './payload-BshGXnOO.js';
2
- export { s as slugify } from './payload-BshGXnOO.js';
1
+ import { C as CheckOptions } from './payload-D8dvqLcc.js';
2
+ export { s as slugify } from './payload-D8dvqLcc.js';
3
3
 
4
4
  interface CypressCheckOptions extends CheckOptions {
5
5
  /** Capture a viewport screenshot so the pixel tier can run. On by default — the plugin
@@ -0,0 +1,77 @@
1
+ interface FindingLike {
2
+ ruleId: string;
3
+ criteriaId: string;
4
+ file: string;
5
+ line: number;
6
+ severity: string;
7
+ message: string;
8
+ advisory?: boolean;
9
+ origin?: {
10
+ sourceFile?: string;
11
+ sourceLine?: number;
12
+ };
13
+ }
14
+ interface AuditLike {
15
+ findings?: FindingLike[];
16
+ }
17
+ interface SnapshotPayload {
18
+ meta: Record<string, unknown>;
19
+ dom: string;
20
+ styles?: unknown;
21
+ boxes?: unknown;
22
+ css?: unknown;
23
+ screenshot?: string;
24
+ /** What the live probes measured, when the caller ran them. */
25
+ probes?: unknown;
26
+ }
27
+ /** Same slug rule as the engine (src/snapshot.ts slugifyPageId): accent-folded URL path. */
28
+ declare function slugify(url: string): string;
29
+ interface CheckOptions {
30
+ /** The page id (directory name). Defaults to the slugified URL path. */
31
+ as?: string;
32
+ /** Human page name for the report. Defaults to the document title. */
33
+ name?: string;
34
+ /** Severity at which the check fails, or `false` to record without ever failing. */
35
+ failOn?: "blocking" | "major" | "minor" | false;
36
+ /** Mark the page as sitting behind authentication. */
37
+ auth?: boolean;
38
+ /** Source files that rendered it — findings are then reported against YOUR code. */
39
+ sources?: string[];
40
+ /** Reproduction notes carried into the auditor ticket. */
41
+ notes?: string;
42
+ /** Capture a viewport screenshot for the pixel tier (Playwright: on by default). */
43
+ screenshot?: boolean;
44
+ /** The path you NAVIGATED to. Given it, the fixture refuses to record a page the browser
45
+ * did not stay on — a guarded route that redirected, a session that expired — instead of
46
+ * filing that other screen under this page's `as`/`name`. Nothing about such a snapshot
47
+ * looks wrong, which is what makes it the worst thing an accessibility report can carry.
48
+ * Compared by path (a query or fragment the app appends to its own route is the same
49
+ * page); a full URL works too. `scan --sample` applies the same rule. */
50
+ expectPath?: string;
51
+ /** Run the LIVE PROBES on this page after collecting it: 200% zoom, 320px reflow, the
52
+ * text-spacing override, focus visibility and content-on-hover.
53
+ *
54
+ * These are the measurements a recorded snapshot can never settle, because they are
55
+ * properties of a page being acted on rather than of a page as it stands. `scan` runs them
56
+ * in its own browser, which is exactly why they were undecidable on any screen behind a
57
+ * login or a state machine — the scanner arrives cold and gets a redirect. Your suite has
58
+ * the page in the right state; this is how it says so.
59
+ *
60
+ * Off by default: the probes press Tab, hover, resize the viewport and inject a stylesheet,
61
+ * and a suite should opt into that. Everything is restored before it returns, and the
62
+ * snapshot has already been collected, so what they measure cannot alter what was recorded.
63
+ * Pass an array to run only some of them (by criterion: "1.4.4", "1.4.10", "1.4.12",
64
+ * "1.4.13", "2.4.7"), or an object to also set the bounds — how wide the reflow probe
65
+ * narrows to, how many focusables are worth tabbing through, how many hits are worth
66
+ * recording. 320px is normative and stays the default; the other two are judgements about
67
+ * YOUR pages, and a screen with 400 focusables should be able to say so rather than being
68
+ * quietly cut off at 120. */
69
+ probes?: boolean | string[] | {
70
+ only?: string[];
71
+ reflowWidth?: number;
72
+ maxFocusables?: number;
73
+ maxHits?: number;
74
+ };
75
+ }
76
+
77
+ export { type AuditLike as A, type CheckOptions as C, type FindingLike as F, type SnapshotPayload as S, slugify as s };
@@ -1,4 +1,4 @@
1
- import { C as CheckOptions, A as AuditLike } from './payload-BshGXnOO.js';
1
+ import { C as CheckOptions, A as AuditLike } from './payload-D8dvqLcc.js';
2
2
 
3
3
  interface PlaywrightPage {
4
4
  evaluate(script: string): Promise<unknown>;
@@ -154,6 +154,7 @@ const __vis = (e) => {
154
154
  };
155
155
  const __html = (e) => (e.outerHTML || '').slice(0, 160);
156
156
  `;
157
+ var PROBE_DEFAULTS = { reflowWidth: 320, maxFocusables: 120, maxHits: 20 };
157
158
  var REFLOW_PROBE = `(() => {
158
159
  const el = document.scrollingElement || document.documentElement;
159
160
  return { horizontalScroll: el.scrollWidth > el.clientWidth + 2 };
@@ -193,7 +194,7 @@ var TEXT_SPACING_PROBE = `(() => { ${PRELUDE}
193
194
  }
194
195
  return hits;
195
196
  })()`;
196
- function focusSetupExpr(scope = "") {
197
+ function focusSetupExpr(scope = "", maxFocusables = PROBE_DEFAULTS.maxFocusables) {
197
198
  const rootExpr = scope ? `document.querySelectorAll(${JSON.stringify(scope)})` : `[document.documentElement]`;
198
199
  return `(() => { ${PRELUDE}
199
200
  const sel = 'a[href],button:not([disabled]),input:not([type=hidden]):not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"]),[role=button]:not([disabled])';
@@ -227,7 +228,7 @@ function focusSetupExpr(scope = "") {
227
228
  proxy.setAttribute('data-u11y-fp', key);
228
229
  window.__u11yF[key] = { rest: snap(proxy), sel: __sel(proxy), html: __html(proxy) };
229
230
  n++;
230
- if (n >= 120) break;
231
+ if (n >= ${maxFocusables}) break;
231
232
  }
232
233
  return n;
233
234
  })()`;
@@ -266,12 +267,12 @@ function hoverVisibleExpr(id, wantHidden = false) {
266
267
  const j = JSON.stringify(id);
267
268
  return `(() => { const t = document.getElementById(${j}); if (!t) return ${wantHidden ? "true" : "false"}; const s = getComputedStyle(t); const shown = s.display !== 'none' && s.visibility !== 'hidden' && t.getBoundingClientRect().height > 0; return ${wantHidden ? "!shown" : "shown"}; })()`;
268
269
  }
269
- async function probeFocusVisible(page, scope = "") {
270
- const count = await page.evaluate(focusSetupExpr(scope));
270
+ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS) {
271
+ const count = await page.evaluate(focusSetupExpr(scope, limits.maxFocusables));
271
272
  if (!count) return [];
272
273
  const hits = [];
273
274
  const seen = /* @__PURE__ */ new Set();
274
- const limit = Math.min(count + 2, 130);
275
+ const limit = Math.min(count + 2, limits.maxFocusables + 10);
275
276
  for (let i = 0; i < limit; i++) {
276
277
  await page.keyboard.press("Tab");
277
278
  const r = await page.evaluate(FOCUS_CHECK_PROBE);
@@ -289,7 +290,7 @@ async function probeFocusVisible(page, scope = "") {
289
290
  }
290
291
  return hits;
291
292
  }
292
- async function probeHover(page) {
293
+ async function probeHover(page, limits = PROBE_DEFAULTS) {
293
294
  const triggers = await page.evaluate(HOVER_SETUP_PROBE);
294
295
  const hits = [];
295
296
  for (const tr of triggers) {
@@ -313,44 +314,88 @@ async function probeHover(page) {
313
314
  detail: `Le contenu r\xE9v\xE9l\xE9 au survol (aria-describedby #${tr.target}) ne se masque pas avec \xC9chap \u2014 Contenu au survol ou au focus (1.4.13).`
314
315
  });
315
316
  }
316
- if (hits.length >= 8) break;
317
+ if (hits.length >= Math.min(limits.maxHits, 8)) break;
317
318
  }
318
319
  return hits;
319
320
  }
320
321
  async function runLiveProbes(page, opts = {}) {
322
+ const limits = { ...PROBE_DEFAULTS, ...opts.limits };
321
323
  const only = opts.only?.length ? new Set(opts.only) : null;
322
324
  const want = (id) => only === null || only.has(id);
323
- const size = page.viewportSize?.() ?? null;
325
+ const canResize = typeof page.setViewportSize === "function" && typeof page.viewportSize === "function";
326
+ const canType = !!page.keyboard && typeof page.keyboard.press === "function";
327
+ const canHover = typeof page.hover === "function" && typeof page.waitForTimeout === "function";
328
+ const canStyle = typeof page.addStyleTag === "function";
329
+ const size = canResize ? page.viewportSize() ?? null : null;
324
330
  const restore = size ?? { width: 1280, height: 900 };
325
- const out = { focusVisible: [], hover: [], reflowZoom: [], textSpacing: [], reflow: { horizontalScroll: false }, probed: [] };
326
- if (want("2.4.7")) {
327
- out.focusVisible = await probeFocusVisible(page).catch(() => []);
328
- out.probed.push("2.4.7");
331
+ const out = { focusVisible: [], hover: [], reflowZoom: [], textSpacing: [], reflow: { horizontalScroll: false }, probed: [], skipped: [] };
332
+ const skip = (sc, why) => {
333
+ out.skipped?.push({ sc, why });
334
+ };
335
+ if (!canResize) skip("1.4.10", "the page object cannot resize its viewport");
336
+ if (!canType) skip("2.4.7", "the page object exposes no keyboard");
337
+ if (!canHover) skip("1.4.13", "the page object cannot hover");
338
+ if (!canStyle) skip("1.4.12", "the page object cannot inject a stylesheet");
339
+ if (want("2.4.7") && canType) {
340
+ const r = await probeFocusVisible(page, "", limits).catch((e) => {
341
+ skip("2.4.7", String(e?.message ?? e).slice(0, 160));
342
+ return null;
343
+ });
344
+ if (r) {
345
+ out.focusVisible = r;
346
+ out.probed.push("2.4.7");
347
+ }
329
348
  }
330
- if (want("1.4.13")) {
331
- out.hover = await probeHover(page).catch(() => []);
332
- out.probed.push("1.4.13");
349
+ if (want("1.4.13") && canHover && canType) {
350
+ const r = await probeHover(page, limits).catch((e) => {
351
+ skip("1.4.13", String(e?.message ?? e).slice(0, 160));
352
+ return null;
353
+ });
354
+ if (r) {
355
+ out.hover = r;
356
+ out.probed.push("1.4.13");
357
+ }
333
358
  }
334
359
  if (want("1.4.4")) {
335
360
  out.reflowZoom = await page.evaluate(REFLOW_ZOOM_PROBE).catch(() => []) ?? [];
336
361
  out.probed.push("1.4.4");
337
362
  }
338
- if (want("1.4.10")) {
339
- await page.setViewportSize({ width: 320, height: restore.height }).catch(() => {
340
- });
341
- out.reflow = await page.evaluate(REFLOW_PROBE).catch(() => ({ horizontalScroll: false })) ?? {
342
- horizontalScroll: false
343
- };
344
- await page.setViewportSize(restore).catch(() => {
363
+ if (want("1.4.10") && canResize) {
364
+ let narrowed = true;
365
+ await page.setViewportSize({ width: limits.reflowWidth, height: restore.height }).catch((e) => {
366
+ narrowed = false;
367
+ skip("1.4.10", String(e?.message ?? e).slice(0, 160));
345
368
  });
346
- out.probed.push("1.4.10");
369
+ if (narrowed) {
370
+ const r = await page.evaluate(REFLOW_PROBE).catch((e) => {
371
+ skip("1.4.10", String(e?.message ?? e).slice(0, 160));
372
+ return null;
373
+ });
374
+ await page.setViewportSize(restore).catch(() => {
375
+ });
376
+ if (r) {
377
+ out.reflow = r;
378
+ out.probed.push("1.4.10");
379
+ }
380
+ }
347
381
  }
348
- if (want("1.4.12")) {
349
- const handle = await page.addStyleTag({ content: TEXT_SPACING_CSS }).catch(() => null);
350
- out.textSpacing = await page.evaluate(TEXT_SPACING_PROBE).catch(() => []) ?? [];
351
- if (handle) await page.evaluate(REMOVE_TEXT_SPACING_STEP).catch(() => {
382
+ if (want("1.4.12") && canStyle) {
383
+ const handle = await page.addStyleTag({ content: TEXT_SPACING_CSS }).catch((e) => {
384
+ skip("1.4.12", String(e?.message ?? e).slice(0, 160));
385
+ return null;
352
386
  });
353
- out.probed.push("1.4.12");
387
+ if (handle) {
388
+ const r = await page.evaluate(TEXT_SPACING_PROBE).catch((e) => {
389
+ skip("1.4.12", String(e?.message ?? e).slice(0, 160));
390
+ return null;
391
+ });
392
+ await page.evaluate(REMOVE_TEXT_SPACING_STEP).catch(() => {
393
+ });
394
+ if (r) {
395
+ out.textSpacing = r;
396
+ out.probed.push("1.4.12");
397
+ }
398
+ }
354
399
  }
355
400
  return out;
356
401
  }
@@ -482,6 +527,23 @@ function writePagesReport(opts = {}, engine = enginePath()) {
482
527
  }
483
528
 
484
529
  // src/integrations/playwright.ts
530
+ function probeOptions(p) {
531
+ if (p === true || p === void 0) return fromConfig();
532
+ if (p === false) return {};
533
+ if (Array.isArray(p)) return { only: p, ...fromConfig() };
534
+ const { only, ...limits } = p;
535
+ return { ...only ? { only } : {}, limits };
536
+ }
537
+ function fromConfig() {
538
+ try {
539
+ const cfg = JSON.parse(readFileSync(join(process.cwd(), ".ultra11yrc.json"), "utf8"));
540
+ if (!cfg.probes) return {};
541
+ const { only, ...limits } = cfg.probes;
542
+ return { ...only ? { only } : {}, limits };
543
+ } catch {
544
+ return {};
545
+ }
546
+ }
485
547
  async function checkA11y(page, opts = {}) {
486
548
  const collected = await page.evaluate(COLLECT_SNAPSHOT);
487
549
  let shot;
@@ -500,8 +562,11 @@ async function checkA11y(page, opts = {}) {
500
562
  let probes;
501
563
  if (opts.probes) {
502
564
  try {
503
- probes = await runLiveProbes(page, Array.isArray(opts.probes) ? { only: opts.probes } : {});
504
- } catch {
565
+ probes = await runLiveProbes(page, probeOptions(opts.probes));
566
+ } catch (e) {
567
+ console.warn(
568
+ `ultra11y: the live probes failed on this page \u2014 ${e instanceof Error ? e.message : String(e)}. The snapshot was still recorded; the criteria they decide stay to assess.`
569
+ );
505
570
  }
506
571
  }
507
572
  const payload = buildPayload(collected, url, "playwright", opts, shot, probes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.6.0",
3
+ "version": "5.7.0",
4
4
  "description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.0",
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.6.0";
9
+ var VERSION = "5.7.0";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -57421,6 +57421,7 @@ function adjudicationText(sc, lang) {
57421
57421
  // src/adjudicate.ts
57422
57422
  var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
57423
57423
  var ALSO_AT_SHOWN = 8;
57424
+ var CITE_DRIFT_DEFAULT = 10;
57424
57425
  function adjudicationContract() {
57425
57426
  return {
57426
57427
  verdicts: [...VERDICTS],
@@ -57448,7 +57449,23 @@ function docsForAudit(audit2, cwd) {
57448
57449
  }
57449
57450
  return docs;
57450
57451
  }
57451
- function collapse2(harvested) {
57452
+ var ADJUDICATION_DEFAULTS = {
57453
+ maxClasses: ADJUDICATE_MAX_EVIDENCE_CLASSES,
57454
+ showAlsoAt: ALSO_AT_SHOWN,
57455
+ citationDrift: CITE_DRIFT_DEFAULT
57456
+ };
57457
+ function adjudicationLimits(cwd) {
57458
+ const cfg = loadConfig(cwd ?? ".")?.adjudication;
57459
+ if (!cfg) return ADJUDICATION_DEFAULTS;
57460
+ const positive2 = (v, fallback) => typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : fallback;
57461
+ return {
57462
+ maxClasses: positive2(cfg.maxClasses, ADJUDICATION_DEFAULTS.maxClasses),
57463
+ showAlsoAt: positive2(cfg.showAlsoAt, ADJUDICATION_DEFAULTS.showAlsoAt),
57464
+ // 0 is meaningful here (exact-anchor matching), so it is allowed through.
57465
+ citationDrift: typeof cfg.citationDrift === "number" && Number.isFinite(cfg.citationDrift) && cfg.citationDrift >= 0 ? Math.floor(cfg.citationDrift) : ADJUDICATION_DEFAULTS.citationDrift
57466
+ };
57467
+ }
57468
+ function collapse2(harvested, limits) {
57452
57469
  const byClass = /* @__PURE__ */ new Map();
57453
57470
  for (const item of harvested) {
57454
57471
  const g = byClass.get(item.cls);
@@ -57456,7 +57473,7 @@ function collapse2(harvested) {
57456
57473
  else byClass.set(item.cls, [item]);
57457
57474
  }
57458
57475
  const groups = [...byClass.values()];
57459
- const evidence = groups.slice(0, ADJUDICATE_MAX_EVIDENCE_CLASSES).map((group) => {
57476
+ const evidence = groups.slice(0, limits.maxClasses).map((group) => {
57460
57477
  const rep = group.find((x) => isSnapshotFile(x.ev.file)) ?? group[0];
57461
57478
  const others = group.filter((x) => x !== rep);
57462
57479
  const pages = [...new Set(group.map((x) => pageOfDoc(x.ev.file)).filter((x) => x !== void 0))];
@@ -57475,11 +57492,11 @@ function collapse2(harvested) {
57475
57492
  files: new Set(harvested.map((x) => x.ev.file)).size,
57476
57493
  pages: new Set(harvested.map((x) => pageOfDoc(x.ev.file)).filter((x) => x !== void 0)).size
57477
57494
  },
57478
- complete: groups.length <= ADJUDICATE_MAX_EVIDENCE_CLASSES
57495
+ complete: groups.length <= limits.maxClasses
57479
57496
  };
57480
57497
  }
57481
- function blankItem(criteriaId, automatability2, title2, harvested) {
57482
- const { evidence, population, complete } = collapse2(harvested);
57498
+ function blankItem(criteriaId, automatability2, title2, harvested, limits) {
57499
+ const { evidence, population, complete } = collapse2(harvested, limits);
57483
57500
  return {
57484
57501
  criteriaId,
57485
57502
  automatability: automatability2,
@@ -57507,6 +57524,7 @@ function subjectsForPackCriterion(standard, id, scs) {
57507
57524
  function buildAdjudicationWorklist(audit2, opts = {}) {
57508
57525
  const docs = docsForAudit(audit2, opts.cwd);
57509
57526
  const standard = opts.standard;
57527
+ const limits = adjudicationLimits(opts.cwd);
57510
57528
  if (standard !== void 0 && !isCore(standard)) {
57511
57529
  const pack = loadPack(standard);
57512
57530
  return derivePackResults(audit2, standard).filter((pc) => pc.status === "manual").map((pc) => {
@@ -57518,12 +57536,13 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
57518
57536
  pc.id,
57519
57537
  automatability2,
57520
57538
  crit ? titlePlain(pack, crit, "fr") : void 0,
57521
- harvestSubjects(subjectsForPackCriterion(standard, pc.id, scs), docs)
57539
+ harvestSubjects(subjectsForPackCriterion(standard, pc.id, scs), docs),
57540
+ limits
57522
57541
  );
57523
57542
  });
57524
57543
  }
57525
57544
  return audit2.residualRisks.map(
57526
- (r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs))
57545
+ (r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs), limits)
57527
57546
  );
57528
57547
  }
57529
57548
  function readCitation(c2) {
@@ -57544,14 +57563,13 @@ function auditFiles(audit2, cwd) {
57544
57563
  return /* @__PURE__ */ new Set();
57545
57564
  }
57546
57565
  }
57547
- var CITE_DRIFT = 10;
57548
- function cites0(anchors, c2) {
57566
+ function cites0(anchors, c2, drift) {
57549
57567
  for (const a of anchors) {
57550
57568
  const at = a.lastIndexOf(":");
57551
57569
  if (at < 0) continue;
57552
57570
  if (a.slice(0, at) !== c2.file) continue;
57553
57571
  const line = Number.parseInt(a.slice(at + 1), 10);
57554
- if (Number.isFinite(line) && Math.abs(line - c2.line) <= CITE_DRIFT) return true;
57572
+ if (Number.isFinite(line) && Math.abs(line - c2.line) <= drift) return true;
57555
57573
  }
57556
57574
  return false;
57557
57575
  }
@@ -57642,6 +57660,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
57642
57660
  }
57643
57661
  const groundInputs = /* @__PURE__ */ new Map();
57644
57662
  let scopeCache;
57663
+ const { citationDrift } = adjudicationLimits(opts.cwd);
57645
57664
  const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
57646
57665
  const toGround = (criteriaId, g) => {
57647
57666
  const list = groundInputs.get(criteriaId);
@@ -57676,7 +57695,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
57676
57695
  } else if (cites.length > 0) {
57677
57696
  const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]);
57678
57697
  for (const c2 of cites) {
57679
- if (!cites0(anchors, c2) && !scopeFiles().has(c2.file)) {
57698
+ if (!cites0(anchors, c2, citationDrift) && !scopeFiles().has(c2.file)) {
57680
57699
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
57681
57700
  }
57682
57701
  toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
@@ -57934,6 +57953,7 @@ function plainTest(s) {
57934
57953
  }
57935
57954
  function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
57936
57955
  const s = T2[lang];
57956
+ const { showAlsoAt: shown } = adjudicationLimits(opts.cwd);
57937
57957
  const pack = isCore(standard) ? void 0 : loadPack(standard);
57938
57958
  const out2 = opts.preamble === false ? [] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
57939
57959
  if (pack && opts.preamble !== false) out2.push(`> ${s.packIntro(pack.name)}`, "");
@@ -57948,7 +57968,7 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
57948
57968
  const extra = [
57949
57969
  e.occurrences && e.occurrences > 1 ? `\xD7${e.occurrences}` : "",
57950
57970
  e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : "",
57951
- e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "\u2026" : ""}` : ""
57971
+ e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, shown).join(", ")}${e.alsoAt.length > shown ? "\u2026" : ""}` : ""
57952
57972
  ].filter(Boolean).join(" ");
57953
57973
  out2.push(`- \`${e.file}:${e.line}\` (\`${e.selector}\`)${extra ? ` [${extra}]` : ""}${e.note ? ` \u2014 ${e.note}` : ""}`);
57954
57974
  }
@@ -59079,6 +59099,7 @@ const __vis = (e) => {
59079
59099
  };
59080
59100
  const __html = (e) => (e.outerHTML || '').slice(0, 160);
59081
59101
  `;
59102
+ var PROBE_DEFAULTS = { reflowWidth: 320, maxFocusables: 120, maxHits: 20 };
59082
59103
  var REFLOW_PROBE = `(() => {
59083
59104
  const el = document.scrollingElement || document.documentElement;
59084
59105
  return { horizontalScroll: el.scrollWidth > el.clientWidth + 2 };
@@ -59118,7 +59139,7 @@ var TEXT_SPACING_PROBE = `(() => { ${PRELUDE}
59118
59139
  }
59119
59140
  return hits;
59120
59141
  })()`;
59121
- function focusSetupExpr(scope = "") {
59142
+ function focusSetupExpr(scope = "", maxFocusables = PROBE_DEFAULTS.maxFocusables) {
59122
59143
  const rootExpr = scope ? `document.querySelectorAll(${JSON.stringify(scope)})` : `[document.documentElement]`;
59123
59144
  return `(() => { ${PRELUDE}
59124
59145
  const sel = 'a[href],button:not([disabled]),input:not([type=hidden]):not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"]),[role=button]:not([disabled])';
@@ -59152,7 +59173,7 @@ function focusSetupExpr(scope = "") {
59152
59173
  proxy.setAttribute('data-u11y-fp', key);
59153
59174
  window.__u11yF[key] = { rest: snap(proxy), sel: __sel(proxy), html: __html(proxy) };
59154
59175
  n++;
59155
- if (n >= 120) break;
59176
+ if (n >= ${maxFocusables}) break;
59156
59177
  }
59157
59178
  return n;
59158
59179
  })()`;
@@ -59191,12 +59212,12 @@ function hoverVisibleExpr(id, wantHidden = false) {
59191
59212
  const j = JSON.stringify(id);
59192
59213
  return `(() => { const t = document.getElementById(${j}); if (!t) return ${wantHidden ? "true" : "false"}; const s = getComputedStyle(t); const shown = s.display !== 'none' && s.visibility !== 'hidden' && t.getBoundingClientRect().height > 0; return ${wantHidden ? "!shown" : "shown"}; })()`;
59193
59214
  }
59194
- async function probeFocusVisible(page, scope = "") {
59195
- const count = await page.evaluate(focusSetupExpr(scope));
59215
+ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS) {
59216
+ const count = await page.evaluate(focusSetupExpr(scope, limits.maxFocusables));
59196
59217
  if (!count) return [];
59197
59218
  const hits = [];
59198
59219
  const seen = /* @__PURE__ */ new Set();
59199
- const limit = Math.min(count + 2, 130);
59220
+ const limit = Math.min(count + 2, limits.maxFocusables + 10);
59200
59221
  for (let i2 = 0; i2 < limit; i2++) {
59201
59222
  await page.keyboard.press("Tab");
59202
59223
  const r = await page.evaluate(FOCUS_CHECK_PROBE);
@@ -59214,7 +59235,7 @@ async function probeFocusVisible(page, scope = "") {
59214
59235
  }
59215
59236
  return hits;
59216
59237
  }
59217
- async function probeHover(page) {
59238
+ async function probeHover(page, limits = PROBE_DEFAULTS) {
59218
59239
  const triggers = await page.evaluate(HOVER_SETUP_PROBE);
59219
59240
  const hits = [];
59220
59241
  for (const tr of triggers) {
@@ -59238,7 +59259,7 @@ async function probeHover(page) {
59238
59259
  detail: `Le contenu r\xE9v\xE9l\xE9 au survol (aria-describedby #${tr.target}) ne se masque pas avec \xC9chap \u2014 Contenu au survol ou au focus (1.4.13).`
59239
59260
  });
59240
59261
  }
59241
- if (hits.length >= 8) break;
59262
+ if (hits.length >= Math.min(limits.maxHits, 8)) break;
59242
59263
  }
59243
59264
  return hits;
59244
59265
  }
@@ -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: 5.6.0
8
+ version: 5.7.0
9
9
  ---
10
10
 
11
11
  # review-a11y — accessibility review of changed code