ultra11y 5.21.0 → 5.22.1
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/dist/playwright.mjs +60 -2
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +228 -18
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +228 -18
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +228 -18
- package/src/adjudicate-subjects.ts +108 -3
- package/src/adjudicate.ts +66 -16
- package/src/audit.ts +4 -2
- package/src/axe-map.ts +8 -0
- package/src/coverage.ts +1 -1
- package/src/orchestrate-templates.ts +84 -1
- package/src/orchestrate.ts +6 -2
- package/src/probes.ts +104 -2
- package/src/report.ts +3 -1
- package/src/scan-local.ts +12 -1
- package/src/scan.ts +2 -0
- package/src/types.ts +2 -1
|
@@ -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.
|
|
13
|
+
"version": "5.22.1",
|
|
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.
|
|
4
|
+
"version": "5.22.1",
|
|
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.
|
|
4
|
+
"version": "5.22.1",
|
|
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.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.22.1";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/dist/playwright.mjs
CHANGED
|
@@ -306,6 +306,48 @@ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, dead
|
|
|
306
306
|
}
|
|
307
307
|
return hits;
|
|
308
308
|
}
|
|
309
|
+
var FOCUS_WHERE_PROBE = `(() => { ${PRELUDE}
|
|
310
|
+
const e = document.activeElement;
|
|
311
|
+
if (!e || e === document.body || e === document.documentElement) return null;
|
|
312
|
+
const key = e.getAttribute && e.getAttribute('data-u11y-f');
|
|
313
|
+
return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e) };
|
|
314
|
+
})()`;
|
|
315
|
+
async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
316
|
+
const count = await page.evaluate(focusSetupExpr("", limits.maxFocusables));
|
|
317
|
+
if (!count || count < 2) return [];
|
|
318
|
+
const hits = [];
|
|
319
|
+
const seen = /* @__PURE__ */ new Set();
|
|
320
|
+
const confirmPresses = 2;
|
|
321
|
+
const limit = Math.min(count + 2, limits.maxFocusables + 10);
|
|
322
|
+
let prev = null;
|
|
323
|
+
for (let i = 0; i < limit; i++) {
|
|
324
|
+
if (deadline?.out()) break;
|
|
325
|
+
await page.keyboard.press("Tab");
|
|
326
|
+
const now = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
327
|
+
if (!now) break;
|
|
328
|
+
if (prev?.tagged && now.tagged && now.key === prev.key) {
|
|
329
|
+
let stuck = true;
|
|
330
|
+
for (let k = 0; k < confirmPresses && stuck; k++) {
|
|
331
|
+
if (deadline?.out()) break;
|
|
332
|
+
await page.keyboard.press("Tab");
|
|
333
|
+
const again = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
334
|
+
stuck = again !== null && again.tagged === true && again.key === now.key;
|
|
335
|
+
}
|
|
336
|
+
if (stuck) {
|
|
337
|
+
hits.push({
|
|
338
|
+
selector: now.selector,
|
|
339
|
+
html: now.html,
|
|
340
|
+
detail: `Le focus reste sur cet \xE9l\xE9ment apr\xE8s ${1 + confirmPresses} appuis sur Tab, alors que la page compte ${count} \xE9l\xE9ments focalisables \u2014 pi\xE8ge au clavier (2.1.2).`
|
|
341
|
+
});
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (seen.has(now.key)) break;
|
|
346
|
+
seen.add(now.key);
|
|
347
|
+
prev = now;
|
|
348
|
+
}
|
|
349
|
+
return hits;
|
|
350
|
+
}
|
|
309
351
|
async function probeHover(page, limits = PROBE_DEFAULTS, deadline) {
|
|
310
352
|
const triggers = await page.evaluate(HOVER_SETUP_PROBE);
|
|
311
353
|
const hits = [];
|
|
@@ -346,7 +388,16 @@ async function runLiveProbes(page, opts = {}) {
|
|
|
346
388
|
const canStyle = typeof page.addStyleTag === "function";
|
|
347
389
|
const size = canResize ? page.viewportSize() ?? null : null;
|
|
348
390
|
const restore = size ?? { width: 1280, height: 900 };
|
|
349
|
-
const out = {
|
|
391
|
+
const out = {
|
|
392
|
+
focusVisible: [],
|
|
393
|
+
hover: [],
|
|
394
|
+
keyboardTrap: [],
|
|
395
|
+
reflowZoom: [],
|
|
396
|
+
textSpacing: [],
|
|
397
|
+
reflow: { horizontalScroll: false },
|
|
398
|
+
probed: [],
|
|
399
|
+
skipped: []
|
|
400
|
+
};
|
|
350
401
|
const skip = (sc, why) => {
|
|
351
402
|
out.skipped?.push({ sc, why });
|
|
352
403
|
};
|
|
@@ -380,7 +431,7 @@ async function runLiveProbes(page, opts = {}) {
|
|
|
380
431
|
}
|
|
381
432
|
};
|
|
382
433
|
if (!canResize) skip("1.4.10", "the page object cannot resize its viewport");
|
|
383
|
-
if (!canType)
|
|
434
|
+
if (!canType) for (const sc of ["2.4.7", "2.1.2"]) skip(sc, "the page object exposes no keyboard");
|
|
384
435
|
if (!canHover) skip("1.4.13", "the page object cannot hover");
|
|
385
436
|
if (!canStyle) skip("1.4.12", "the page object cannot inject a stylesheet");
|
|
386
437
|
if (want("1.4.4")) {
|
|
@@ -424,6 +475,13 @@ async function runLiveProbes(page, opts = {}) {
|
|
|
424
475
|
out.probed.push("2.4.7");
|
|
425
476
|
}
|
|
426
477
|
}
|
|
478
|
+
if (want("2.1.2") && canType) {
|
|
479
|
+
const r = await bounded("2.1.2", () => probeKeyboardTrap(page, limits, deadline));
|
|
480
|
+
if (r) {
|
|
481
|
+
out.keyboardTrap = r;
|
|
482
|
+
out.probed.push("2.1.2");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
427
485
|
if (want("1.4.13") && canHover && canType) {
|
|
428
486
|
const r = await bounded("1.4.13", () => probeHover(page, limits, deadline));
|
|
429
487
|
if (r) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.22.1",
|
|
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",
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -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.
|
|
9
|
+
var VERSION = "5.22.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -39175,6 +39175,11 @@ var PROBE_WCAG = {
|
|
|
39175
39175
|
// Text Spacing — clipping/overlap under the WCAG spacing override
|
|
39176
39176
|
hover: "1.4.13",
|
|
39177
39177
|
// Content on Hover or Focus — not dismissible/hoverable/persistent
|
|
39178
|
+
// No Keyboard Trap. Until this probe existed, 2.1.2 was one of the five needs-rendering
|
|
39179
|
+
// criteria NO tier measured (src/report.ts NEEDS_RENDERING) — so RGAA 12.9 reached a paid
|
|
39180
|
+
// adjudicator on every run, carrying whatever `preventDefault(` happened to be in the source.
|
|
39181
|
+
// A trap is a property of the tab ring, and the tab ring only exists in a browser.
|
|
39182
|
+
"keyboard-trap": "2.1.2",
|
|
39178
39183
|
// Stateful input-overflow probes — a FILLED input whose typed value is clipped/unreadable
|
|
39179
39184
|
// under a stress. Same defect, one SC per stress (so each folds onto the SC that stress
|
|
39180
39185
|
// actually evidences), so they stay unambiguous cross-standard (RGAA 10.11/10.4/10.12).
|
|
@@ -39192,6 +39197,9 @@ var PROBE_SEVERITY = {
|
|
|
39192
39197
|
"reflow-zoom": "majeur",
|
|
39193
39198
|
"text-spacing": "mineur",
|
|
39194
39199
|
hover: "mineur",
|
|
39200
|
+
// Not heuristic and not minor: the probe pressed Tab three times and focus did not move on a
|
|
39201
|
+
// page with other focusables. A keyboard user cannot get out of it — that is a blocker.
|
|
39202
|
+
"keyboard-trap": "bloquant",
|
|
39195
39203
|
// Input-overflow is a directly OBSERVED loss of content (the value the user typed is no
|
|
39196
39204
|
// longer visible), not a heuristic — the human auditor confirmed the real instance class
|
|
39197
39205
|
// — so it is majeur on every stress (incl. text-spacing, unlike the generic text-spacing
|
|
@@ -39876,6 +39884,92 @@ var SUBJECTS = {
|
|
|
39876
39884
|
)
|
|
39877
39885
|
)
|
|
39878
39886
|
),
|
|
39887
|
+
// ADDITIONAL CONTENT — the thing that appears when a component is hovered, focused or
|
|
39888
|
+
// activated, and the subject RGAA 10.14 and 12.11 are entirely about.
|
|
39889
|
+
//
|
|
39890
|
+
// It had no harvester at all, and the two criteria were mapped onto `focusables` +
|
|
39891
|
+
// `pointerHandlers` instead. On a page whose tooltip is pure CSS that harvests exactly
|
|
39892
|
+
// nothing to do with the question: measured on tests/fixtures/realworld, both criteria
|
|
39893
|
+
// reached a paid adjudicator carrying one anchor between them — `e.preventDefault();` from a
|
|
39894
|
+
// React submit handler — and were asked whether the site's hover content is keyboard
|
|
39895
|
+
// reachable. No model can answer that, and the honest verdict (`undecidable`) leaves the
|
|
39896
|
+
// criterion « à évaluer » forever.
|
|
39897
|
+
//
|
|
39898
|
+
// So harvest the CANDIDATES, from the four shapes additional content actually takes, and let
|
|
39899
|
+
// the adjudicator read the rendered page for the rest:
|
|
39900
|
+
// - an explicit `role="tooltip"`, and whatever describes a control (`aria-describedby`,
|
|
39901
|
+
// `aria-details`) — the trigger/target pair `probeHover` also keys on;
|
|
39902
|
+
// - a disclosure: `aria-expanded` on the trigger, `aria-controls` naming the panel;
|
|
39903
|
+
// - the native pair, `<details>`/`<summary>`;
|
|
39904
|
+
// - the popover API (`popover`, `popovertarget`).
|
|
39905
|
+
// The TRIGGER goes in beside its target, because 12.11 asks whether the component can be
|
|
39906
|
+
// reached at all and 10.14 asks whether activating it reveals the content — both questions
|
|
39907
|
+
// about the trigger, neither answerable from the panel alone.
|
|
39908
|
+
additionalContent: (docs) => docs.flatMap((d) => {
|
|
39909
|
+
const byId2 = /* @__PURE__ */ new Map();
|
|
39910
|
+
for (const e of d.elements) {
|
|
39911
|
+
const id = attr(e, "id");
|
|
39912
|
+
if (id && !byId2.has(id)) byId2.set(id, e);
|
|
39913
|
+
}
|
|
39914
|
+
const out2 = [];
|
|
39915
|
+
const seen = /* @__PURE__ */ new Set();
|
|
39916
|
+
const push = (e, note, cls) => {
|
|
39917
|
+
if (seen.has(e.start)) return;
|
|
39918
|
+
seen.add(e.start);
|
|
39919
|
+
out2.push(h(d, e, note, cls));
|
|
39920
|
+
};
|
|
39921
|
+
for (const e of d.elements) {
|
|
39922
|
+
if (attr(e, "role") === "tooltip")
|
|
39923
|
+
push(e, `<${e.tag}> role="tooltip" text="${t(e, 60)}" \u2014 revealed how, and reachable with the keyboard?`, `tooltip|${t(e, 40)}`);
|
|
39924
|
+
if (attr(e, "popover") !== void 0) push(e, `<${e.tag}> popover="${attr(e, "popover") ?? ""}" text="${t(e, 60)}"`, `popover|${t(e, 40)}`);
|
|
39925
|
+
}
|
|
39926
|
+
for (const e of d.elements) {
|
|
39927
|
+
for (const rel2 of ["aria-describedby", "aria-details", "aria-controls", "popovertarget"]) {
|
|
39928
|
+
const ref = (attr(e, rel2) ?? "").trim().split(/\s+/)[0];
|
|
39929
|
+
if (!ref) continue;
|
|
39930
|
+
const target = byId2.get(ref);
|
|
39931
|
+
if (!target) continue;
|
|
39932
|
+
const expanded = attr(e, "aria-expanded");
|
|
39933
|
+
push(
|
|
39934
|
+
e,
|
|
39935
|
+
`<${e.tag}> ${rel2}="${ref}"${expanded !== void 0 ? ` aria-expanded="${expanded}"` : ""} text="${t(e, 40)}" \u2014 the component whose hover/focus/activation reveals #${ref}`,
|
|
39936
|
+
`trigger|${e.tag}|${rel2}|${ref}`
|
|
39937
|
+
);
|
|
39938
|
+
push(
|
|
39939
|
+
target,
|
|
39940
|
+
`<${target.tag}> #${ref} \u2014 additional content pointed at by a ${rel2} on <${e.tag}>: visible on hover only, or on focus too?`,
|
|
39941
|
+
`revealed|${ref}`
|
|
39942
|
+
);
|
|
39943
|
+
}
|
|
39944
|
+
if (attr(e, "aria-expanded") !== void 0) {
|
|
39945
|
+
push(e, `<${e.tag}> aria-expanded="${attr(e, "aria-expanded") ?? ""}" text="${t(e, 40)}" \u2014 disclosure trigger`, `disclosure|${e.tag}|${t(e, 40)}`);
|
|
39946
|
+
}
|
|
39947
|
+
}
|
|
39948
|
+
for (const e of elementsByTag(d, "details", "summary")) {
|
|
39949
|
+
push(e, `<${e.tag}> text="${t(e, 60)}" \u2014 native disclosure`, `details|${e.tag}|${t(e, 40)}`);
|
|
39950
|
+
}
|
|
39951
|
+
out2.push(
|
|
39952
|
+
...linesOf(d, /:(?:hover|focus|focus-within|focus-visible)\b/).map(
|
|
39953
|
+
(l) => hAt(d, l.line, "css", `CSS rule keyed on a pointer/keyboard state \u2014 does the hover half have a focus twin? ${l.text}`, `cssstate|${l.text}`)
|
|
39954
|
+
)
|
|
39955
|
+
);
|
|
39956
|
+
if (out2.length) {
|
|
39957
|
+
for (const link of elementsByTag(d, "link")) {
|
|
39958
|
+
const rel2 = (attr(link, "rel") ?? "").toLowerCase();
|
|
39959
|
+
if (!rel2.split(/\s+/).includes("stylesheet")) continue;
|
|
39960
|
+
const href = attr(link, "href") ?? "";
|
|
39961
|
+
out2.push(
|
|
39962
|
+
h(
|
|
39963
|
+
d,
|
|
39964
|
+
link,
|
|
39965
|
+
`stylesheet "${href}" \u2014 this page reveals additional content and the rule that does it is NOT in this file. Open the stylesheet and check the \`:hover\` selectors for a \`:focus\`/\`:focus-within\` twin.`,
|
|
39966
|
+
`stylesheet|${href}`
|
|
39967
|
+
)
|
|
39968
|
+
);
|
|
39969
|
+
}
|
|
39970
|
+
}
|
|
39971
|
+
return out2;
|
|
39972
|
+
}),
|
|
39879
39973
|
// Content pinned over the page — what can obscure a focused element.
|
|
39880
39974
|
stickies: (docs) => docs.flatMap((d) => [
|
|
39881
39975
|
...d.elements.filter((e) => /position\s*:\s*(?:fixed|sticky)/.test(attr(e, "style") ?? "")).map((e) => h(d, e, `pinned element (inline): ${(attr(e, "style") ?? "").slice(0, 60)} \u2014 can it cover a focused element?`, `sticky|${selectorFor(e)}`)),
|
|
@@ -39909,7 +40003,9 @@ var SC_SUBJECTS = {
|
|
|
39909
40003
|
"1.4.10": ["readingOrder"],
|
|
39910
40004
|
"1.4.11": ["colourPairs"],
|
|
39911
40005
|
"1.4.12": ["readingOrder"],
|
|
39912
|
-
|
|
40006
|
+
// Content on Hover or Focus. Its subject IS the additional content — `aria` and `stickies`
|
|
40007
|
+
// were standing in for a harvester that did not exist yet.
|
|
40008
|
+
"1.4.13": ["additionalContent", "aria", "stickies"],
|
|
39913
40009
|
"2.1.1": ["pointerHandlers", "focusables"],
|
|
39914
40010
|
"2.1.2": ["focusables", "pointerHandlers"],
|
|
39915
40011
|
"2.1.4": ["shortcuts"],
|
|
@@ -40002,7 +40098,7 @@ var PACK_SUBJECTS = {
|
|
|
40002
40098
|
"12.8": ["focusOrder"],
|
|
40003
40099
|
"12.9": ["focusables", "pointerHandlers"],
|
|
40004
40100
|
"12.10": ["shortcuts"],
|
|
40005
|
-
"12.11": ["pointerHandlers", "focusables"],
|
|
40101
|
+
"12.11": ["additionalContent", "pointerHandlers", "focusables"],
|
|
40006
40102
|
// Theme 4 — multimedia. 4.10 (is automatically-triggered sound controllable?) maps onto
|
|
40007
40103
|
// WCAG 1.4.2, which is `static` and therefore has no subject of its own — but the pack
|
|
40008
40104
|
// flags 4.10 `judgment`, so judgmentGuard reopens it and it would arrive with nothing.
|
|
@@ -40029,7 +40125,7 @@ var PACK_SUBJECTS = {
|
|
|
40029
40125
|
"10.11": ["readingOrder"],
|
|
40030
40126
|
"10.12": ["readingOrder"],
|
|
40031
40127
|
"10.13": ["aria", "stickies"],
|
|
40032
|
-
"10.14": ["focusables", "pointerHandlers"],
|
|
40128
|
+
"10.14": ["additionalContent", "focusables", "pointerHandlers"],
|
|
40033
40129
|
// Theme 13 — consultation.
|
|
40034
40130
|
"13.1": ["timers"],
|
|
40035
40131
|
"13.3": ["downloadDocs"],
|
|
@@ -48486,7 +48582,7 @@ var RESIDUAL_TRAIL = {
|
|
|
48486
48582
|
// Rendering criteria no automated tier measures — say so, rather than pointing at `scan` and
|
|
48487
48583
|
// letting the reader discover it changes nothing.
|
|
48488
48584
|
"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.",
|
|
48489
|
-
"2.1.2": "
|
|
48585
|
+
"2.1.2": "Needs a live browser: the tab ring is walked and every focusable is checked for one Tab cannot move off \u2014 `scan <url> --runtime local --merge <audit.json>`. A region only a pointer can open (a custom widget behind a click) still has to be attempted by hand.",
|
|
48490
48586
|
"2.3.1": "No automated tier decides this: flashing has to be observed over time on the rendered page.",
|
|
48491
48587
|
"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."
|
|
48492
48588
|
};
|
|
@@ -55074,6 +55170,7 @@ var NEEDS_RENDERING = [
|
|
|
55074
55170
|
{ sc: "1.4.11", label: { fr: "contraste des composants", en: "non-text contrast" } },
|
|
55075
55171
|
{ sc: "1.4.12", label: { fr: "espacement du texte", en: "text spacing" } },
|
|
55076
55172
|
{ sc: "1.4.13", label: { fr: "contenu au survol", en: "content on hover" } },
|
|
55173
|
+
{ sc: "2.1.2", label: { fr: "pi\xE8ge au clavier", en: "keyboard trap" } },
|
|
55077
55174
|
{ sc: "2.4.7", label: { fr: "visibilit\xE9 du focus", en: "focus visibility" } },
|
|
55078
55175
|
{ sc: "4.1.3", label: { fr: "r\xE9gions live", en: "live regions" } }
|
|
55079
55176
|
];
|
|
@@ -57939,8 +58036,8 @@ function adjudicationContract() {
|
|
|
57939
58036
|
manualReasons: [...MANUAL_REASON_VALUES],
|
|
57940
58037
|
requires: {
|
|
57941
58038
|
C: "a non-empty justification AND citations[] naming the harvested evidence it cleared (each anchor resolvable and drawn from this criterion's own evidence, and about the same kind of element the harvest recorded there \u2014 copy the evidence's own `snippet` rather than retyping the element); a criterion with no harvested evidence cannot be C at all",
|
|
57942
|
-
NA: "a non-empty justification",
|
|
57943
|
-
NC: "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
|
|
58039
|
+
NA: "a non-empty justification, and citations[] whenever evidence was presented \u2014 a criterion whose subject exists NOWHERE in the audited scope is NA, never NC",
|
|
58040
|
+
NC: "at least one groundable finding, each naming the `file` it was observed in (an NC with no location is refused as flatly as an uncited C \u2014 and a non-conformity that rests on an ABSENCE is still observed on an element of a page, so cite that element) and each citing a normativeRef that resolves against the active standard",
|
|
57944
58041
|
manual: `a reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`
|
|
57945
58042
|
}
|
|
57946
58043
|
};
|
|
@@ -58285,12 +58382,16 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
58285
58382
|
}
|
|
58286
58383
|
}
|
|
58287
58384
|
} else if (v === "NC") {
|
|
58288
|
-
if (!it.findings || it.findings.length === 0)
|
|
58385
|
+
if (!it.findings || it.findings.length === 0)
|
|
58386
|
+
blame(
|
|
58387
|
+
it.criteriaId,
|
|
58388
|
+
`criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding \u2014 { file, line, message, snippet, normativeRef } pointing at a real anchor from this criterion's own evidence`
|
|
58389
|
+
);
|
|
58289
58390
|
for (const f of it.findings ?? []) {
|
|
58290
58391
|
if (typeof f.file !== "string" || !f.file.trim()) {
|
|
58291
58392
|
blame(
|
|
58292
58393
|
it.criteriaId,
|
|
58293
|
-
`criterion ${it.criteriaId}: an NC finding must name the file it was observed in \u2014 nobody can act on a non-conformity with no location
|
|
58394
|
+
`criterion ${it.criteriaId}: an NC finding must name the file it was observed in \u2014 nobody can act on a non-conformity with no location. An absence is still observed somewhere: cite the element and the page you observed it on, with the file, line and snippet copied from this criterion's own evidence. If the subject exists nowhere in scope, the verdict is NA with a justification, not NC.`
|
|
58294
58395
|
);
|
|
58295
58396
|
continue;
|
|
58296
58397
|
}
|
|
@@ -58317,7 +58418,10 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
58317
58418
|
}
|
|
58318
58419
|
for (const rec of it.recommendations ?? []) {
|
|
58319
58420
|
if (typeof rec.file !== "string" || !rec.file.trim()) {
|
|
58320
|
-
blame(
|
|
58421
|
+
blame(
|
|
58422
|
+
it.criteriaId,
|
|
58423
|
+
`criterion ${it.criteriaId}: a recommendation must name the file it was observed in \u2014 copy a file and line from this criterion's own evidence, or drop the recommendation`
|
|
58424
|
+
);
|
|
58321
58425
|
continue;
|
|
58322
58426
|
}
|
|
58323
58427
|
toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
|
|
@@ -58508,6 +58612,8 @@ var T2 = {
|
|
|
58508
58612
|
snippetLabel: "`snippet` \xE0 copier dans la citation",
|
|
58509
58613
|
renderedAvailable: "**RENDU DISPONIBLE.** Cet audit a ing\xE9r\xE9 des captures de page : le rendu de la page est sur le disque, sous `.ultra11y/pages/<id>/` \u2014 `dom.html` (le DOM s\xE9rialis\xE9 par le navigateur), `styles.json` (les styles calcul\xE9s), `boxes.json` (les bo\xEEtes et positions), `axtree.json` (l'arbre d'accessibilit\xE9) et `screen.png`. Un crit\xE8re \xAB \xE0 restituer \xBB \u2014 information par la couleur, op\xE9rabilit\xE9 clavier d'un script, geste au pointeur \u2014 se tranche DEPUIS CES FICHIERS : lisez-les comme vous liriez la source. `needs-rendered-dom` reste la bonne r\xE9ponse pour un crit\xE8re dont aucune capture ne porte le sujet, et pour lui seul.",
|
|
58510
58614
|
nothingRendered: (ids) => `**AUCUN RENDU DANS CETTE PORT\xC9E.** ${ids.length} crit\xE8re(s) exigent une page rendue, et aucune page n'a \xE9t\xE9 instantan\xE9e ici : personne ne peut les trancher depuis la source, et \`needs-rendered-dom\` est pour eux la seule r\xE9ponse honn\xEAte. Rendez AVANT d'adjuger \u2014 \`ultra11y scan <url> --merge <audit.json>\` (ou \`scan --sample\`) \u2014 puis reconstruisez cette liste : la mesure en ferme la plupart sans mod\xE8le, donc sans facture. Concern\xE9s : ${ids.map((id) => `\`${id}\``).join(" \xB7 ")}`,
|
|
58615
|
+
briefContract: "> **CONTRAT DE VERDICT** \u2014 le pli est FERM\xC9 : un verdict auquel il manque son champ obligatoire est refus\xE9, et son crit\xE8re retourne \xAB \xE0 \xE9valuer \xBB en portant le refus. Renseignez le verdict de CE crit\xE8re, ici :",
|
|
58616
|
+
absenceRule: "> **UNE NC EN FORME D'ABSENCE S'ANCRE QUAND M\xCAME.** \xAB Il n'y a pas de second syst\xE8me de navigation \xBB, \xAB il n'y a pas de moteur de recherche \xBB, \xAB aucun message d'erreur ne sugg\xE8re le format attendu \xBB : une absence se CONSTATE quelque part. Citez l'\xE9l\xE9ment et la page o\xF9 vous l'avez constat\xE9e \u2014 le `<nav>`, le `<header>`, le formulaire \u2014 avec son `file`, sa `line` et son `snippet`. Une NC sans `file` est refus\xE9e aussi s\xFBrement qu'un `C` sans citations. Et si le sujet du crit\xE8re n'existe nulle part dans le p\xE9rim\xE8tre audit\xE9, le verdict n'est pas `NC` : c'est `NA`, avec sa justification.",
|
|
58511
58617
|
incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
|
|
58512
58618
|
none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
|
|
58513
58619
|
questions: "\xC0 v\xE9rifier manuellement",
|
|
@@ -58540,6 +58646,8 @@ var T2 = {
|
|
|
58540
58646
|
snippetLabel: "`snippet` to copy into the citation",
|
|
58541
58647
|
renderedAvailable: "**THE RENDERED PAGE IS AVAILABLE.** This audit ingested page captures: the rendered page is on disk under `.ultra11y/pages/<id>/` \u2014 `dom.html` (the DOM the browser serialized), `styles.json` (computed styles), `boxes.json` (boxes and positions), `axtree.json` (the accessibility tree) and `screen.png`. A needs-rendering criterion \u2014 information by colour, keyboard operability of a script, a pointer gesture \u2014 is decided FROM THOSE FILES: read them as you would read the source. `needs-rendered-dom` stays the right answer for a criterion no capture carries the subject of, and for that alone.",
|
|
58542
58648
|
nothingRendered: (ids) => `**NOTHING WAS RENDERED IN THIS SCOPE.** ${ids.length} criteria need a rendered page, and no page was snapshotted here: nobody can settle them from source, and \`needs-rendered-dom\` is the only honest answer for them. Render BEFORE adjudicating \u2014 \`ultra11y scan <url> --merge <audit.json>\` (or \`scan --sample\`) \u2014 then rebuild this worklist: the measurement closes most of them with no model in the loop, and so with no bill. Affected: ${ids.map((id) => `\`${id}\``).join(" \xB7 ")}`,
|
|
58649
|
+
briefContract: "> **VERDICT CONTRACT** \u2014 the fold is FAIL-CLOSED: a verdict missing its required field is refused, and its criterion goes back to \xAB to assess \xBB carrying the refusal. Record THIS criterion's verdict, here:",
|
|
58650
|
+
absenceRule: "> **AN NC SHAPED LIKE AN ABSENCE STILL HAS TO BE ANCHORED.** \xAB There is no second navigation system \xBB, \xAB there is no search engine \xBB, \xAB no error message suggests the expected format \xBB: an absence is OBSERVED somewhere. Cite the element and the page you observed it on \u2014 the `<nav>`, the `<header>`, the form \u2014 with its `file`, `line` and `snippet`. An NC with no `file` is refused exactly as surely as a `C` with no citations. And when the criterion's subject exists nowhere in the audited scope, the verdict is not `NC`: it is `NA`, with its justification.",
|
|
58543
58651
|
incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
|
|
58544
58652
|
none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
|
|
58545
58653
|
questions: "To verify manually",
|
|
@@ -58584,14 +58692,14 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
|
|
|
58584
58692
|
const s = T2[lang];
|
|
58585
58693
|
const { showAlsoAt: shown } = adjudicationLimits(opts.cwd);
|
|
58586
58694
|
const pack = isCore(standard) ? void 0 : loadPack(standard);
|
|
58587
|
-
const out2 = opts.preamble === false ? [] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
|
|
58588
|
-
if (
|
|
58695
|
+
const out2 = opts.preamble === false ? [s.briefContract, "", ...s.verdicts, "", s.rule, "", s.absenceRule, ""] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.absenceRule, "", s.then, ""];
|
|
58696
|
+
if (items.some((it) => it.evidence.some((e) => isSnapshotFile(e.file)))) {
|
|
58589
58697
|
out2.push(`> ${s.renderedAvailable}`, "");
|
|
58590
58698
|
}
|
|
58591
|
-
if (opts.
|
|
58699
|
+
if (opts.unrendered?.length) {
|
|
58592
58700
|
out2.push(`> ${s.nothingRendered(opts.unrendered)}`, "");
|
|
58593
58701
|
}
|
|
58594
|
-
if (pack
|
|
58702
|
+
if (pack) out2.push(`> ${s.packIntro(pack.name)}`, "");
|
|
58595
58703
|
for (const it of items) {
|
|
58596
58704
|
out2.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
|
|
58597
58705
|
const pop = it.population;
|
|
@@ -58669,8 +58777,13 @@ function writeAdjudication(items, outDir, opts) {
|
|
|
58669
58777
|
writeFileSync10(verdictsPath, JSON.stringify({ ...file, items: slimAdjudicationItems(items) }, null, 2) + "\n");
|
|
58670
58778
|
const itemsDir = join35(outDir, "adjudicate");
|
|
58671
58779
|
mkdirSync9(itemsDir, { recursive: true });
|
|
58780
|
+
const unrendered = new Set(opts.unrendered ?? []);
|
|
58672
58781
|
for (const it of items) {
|
|
58673
|
-
|
|
58782
|
+
const mine = unrendered.has(it.criteriaId) ? [it.criteriaId] : [];
|
|
58783
|
+
writeFileSync10(
|
|
58784
|
+
join35(itemsDir, `${it.criteriaId}.md`),
|
|
58785
|
+
formatAdjudication([it], opts.lang ?? "en", opts.standard, { preamble: false, ...mine.length ? { unrendered: mine } : {} })
|
|
58786
|
+
);
|
|
58674
58787
|
}
|
|
58675
58788
|
return { todoPath, mdPath, verdictsPath, itemsDir, count: items.length };
|
|
58676
58789
|
}
|
|
@@ -59442,6 +59555,7 @@ var PROBE_FIELDS = [
|
|
|
59442
59555
|
{ key: "reflowZoom", engine: "reflow-zoom" },
|
|
59443
59556
|
{ key: "textSpacing", engine: "text-spacing" },
|
|
59444
59557
|
{ key: "hover", engine: "hover" },
|
|
59558
|
+
{ key: "keyboardTrap", engine: "keyboard-trap" },
|
|
59445
59559
|
{ key: "inputOverflowReflow", engine: "input-overflow-reflow" },
|
|
59446
59560
|
{ key: "inputOverflowZoom", engine: "input-overflow-zoom" },
|
|
59447
59561
|
{ key: "inputOverflowSpacing", engine: "input-overflow-spacing" },
|
|
@@ -59970,6 +60084,48 @@ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, dead
|
|
|
59970
60084
|
}
|
|
59971
60085
|
return hits;
|
|
59972
60086
|
}
|
|
60087
|
+
var FOCUS_WHERE_PROBE = `(() => { ${PRELUDE}
|
|
60088
|
+
const e = document.activeElement;
|
|
60089
|
+
if (!e || e === document.body || e === document.documentElement) return null;
|
|
60090
|
+
const key = e.getAttribute && e.getAttribute('data-u11y-f');
|
|
60091
|
+
return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e) };
|
|
60092
|
+
})()`;
|
|
60093
|
+
async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
60094
|
+
const count = await page.evaluate(focusSetupExpr("", limits.maxFocusables));
|
|
60095
|
+
if (!count || count < 2) return [];
|
|
60096
|
+
const hits = [];
|
|
60097
|
+
const seen = /* @__PURE__ */ new Set();
|
|
60098
|
+
const confirmPresses = 2;
|
|
60099
|
+
const limit = Math.min(count + 2, limits.maxFocusables + 10);
|
|
60100
|
+
let prev = null;
|
|
60101
|
+
for (let i2 = 0; i2 < limit; i2++) {
|
|
60102
|
+
if (deadline?.out()) break;
|
|
60103
|
+
await page.keyboard.press("Tab");
|
|
60104
|
+
const now = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
60105
|
+
if (!now) break;
|
|
60106
|
+
if (prev?.tagged && now.tagged && now.key === prev.key) {
|
|
60107
|
+
let stuck = true;
|
|
60108
|
+
for (let k = 0; k < confirmPresses && stuck; k++) {
|
|
60109
|
+
if (deadline?.out()) break;
|
|
60110
|
+
await page.keyboard.press("Tab");
|
|
60111
|
+
const again = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
60112
|
+
stuck = again !== null && again.tagged === true && again.key === now.key;
|
|
60113
|
+
}
|
|
60114
|
+
if (stuck) {
|
|
60115
|
+
hits.push({
|
|
60116
|
+
selector: now.selector,
|
|
60117
|
+
html: now.html,
|
|
60118
|
+
detail: `Le focus reste sur cet \xE9l\xE9ment apr\xE8s ${1 + confirmPresses} appuis sur Tab, alors que la page compte ${count} \xE9l\xE9ments focalisables \u2014 pi\xE8ge au clavier (2.1.2).`
|
|
60119
|
+
});
|
|
60120
|
+
break;
|
|
60121
|
+
}
|
|
60122
|
+
}
|
|
60123
|
+
if (seen.has(now.key)) break;
|
|
60124
|
+
seen.add(now.key);
|
|
60125
|
+
prev = now;
|
|
60126
|
+
}
|
|
60127
|
+
return hits;
|
|
60128
|
+
}
|
|
59973
60129
|
async function probeHover(page, limits = PROBE_DEFAULTS, deadline) {
|
|
59974
60130
|
const triggers = await page.evaluate(HOVER_SETUP_PROBE);
|
|
59975
60131
|
const hits = [];
|
|
@@ -60002,7 +60158,7 @@ async function probeHover(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
60002
60158
|
|
|
60003
60159
|
// src/scan-local.ts
|
|
60004
60160
|
var LOCAL_ENGINE = "axe-core@playwright (local)";
|
|
60005
|
-
var LOCAL_TESTED_SCS = ["1.4.4", "1.4.10", "1.4.12", "2.4.7", "1.4.13"];
|
|
60161
|
+
var LOCAL_TESTED_SCS = ["1.4.4", "1.4.10", "1.4.12", "2.4.7", "1.4.13", "2.1.2"];
|
|
60006
60162
|
function localTestedScs(interact) {
|
|
60007
60163
|
return interact ? [...LOCAL_TESTED_SCS, "4.1.3"] : [...LOCAL_TESTED_SCS];
|
|
60008
60164
|
}
|
|
@@ -60410,6 +60566,7 @@ async function runOnPage(browser, AxeBuilder, target, isFile, opts) {
|
|
|
60410
60566
|
}
|
|
60411
60567
|
};
|
|
60412
60568
|
const focusVisible = await ran("2.4.7", empty, () => probeFocusVisible(page));
|
|
60569
|
+
const keyboardTrap = await ran("2.1.2", empty, () => probeKeyboardTrap(page));
|
|
60413
60570
|
const hover = await ran("1.4.13", empty, () => probeHover(page));
|
|
60414
60571
|
const l = opts.lang;
|
|
60415
60572
|
if (opts.interact) await page.evaluate(FILL_INPUTS_STEP).catch(() => {
|
|
@@ -60440,6 +60597,7 @@ async function runOnPage(browser, AxeBuilder, target, isFile, opts) {
|
|
|
60440
60597
|
reflow,
|
|
60441
60598
|
focusVisible: dialogFocus.length ? [...focusVisible, ...dialogFocus] : focusVisible,
|
|
60442
60599
|
hover,
|
|
60600
|
+
keyboardTrap,
|
|
60443
60601
|
reflowZoom,
|
|
60444
60602
|
textSpacing,
|
|
60445
60603
|
inputOverflowReflow,
|
|
@@ -64355,8 +64513,60 @@ function phaseWorkflowScript(ph, runAbs, engineAbs, batchSize) {
|
|
|
64355
64513
|
``
|
|
64356
64514
|
].join("\n");
|
|
64357
64515
|
}
|
|
64358
|
-
|
|
64516
|
+
var VERDICT_RULES = `2. Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict missing its required field does not fold, and its criterion goes back to \xAB to assess \xBB carrying the refusal):
|
|
64517
|
+
- \`C\` (conforming) \u2014 REQUIRES \`justification\` explaining why the evidence satisfies the criterion, AND \`citations[]\` naming the evidence you cleared (\`file\`/\`line\` copied VERBATIM from this criterion's own evidence; an anchor that is not in that list is treated as fabricated). A criterion presented with NO evidence at all cannot be \`C\` \u2014 it is \`manual\` (\`undecidable\`), or \`NA\` if nothing in scope is concerned.
|
|
64518
|
+
- \`NC\` (non-conforming) \u2014 REQUIRES \`findings\`: at least one groundable \`{ file, line, selector?, message, snippet?, severity?, normativeRef }\` pointing at REAL source. The fold re-grounds every finding; an invented file:line is rejected, and so is a finding with no \`file\` at all. \`normativeRef\` MUST cite the precise failed test \u2014 under a country standard, one of the criterion's OWN numbered tests, listed in its brief under \xAB tests to rule on \xBB. A WCAG id looks alike, denotes an unrelated test, and is rejected.
|
|
64519
|
+
- \`NA\` (not applicable) \u2014 REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
|
|
64520
|
+
- \`manual\` (still undecidable) \u2014 REQUIRES \`reason\`: \`needs-rendered-dom\` (only a rendered DOM can decide it, and no capture in this run carries its subject) or \`undecidable\` (the evidence cannot settle it either way).
|
|
64521
|
+
3. AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. \xAB No second navigation system \xBB, \xAB no search engine \xBB, \xAB no error message suggests the expected format \xBB \u2014 an absence is OBSERVED somewhere: cite the element and the page you observed it on. And when the criterion's subject exists nowhere in the audited scope, the verdict is \`NA\` with its justification, never \`NC\`.
|
|
64522
|
+
4. THE RENDERED PAGE MAY BE ON DISK. When a criterion's evidence is anchored under \`.ultra11y/pages/<id>/\`, the browser already ran: \`dom.html\`, \`styles.json\`, \`boxes.json\`, \`axtree.json\` and \`screen.png\` are there to read. \`needs-rendered-dom\` is refused on such a criterion \u2014 decide it from those files, or answer \`undecidable\` and say what the capture does not settle.
|
|
64523
|
+
5. Never guess. A criterion you cannot decide from real evidence stays \`manual\` with its reason \u2014 that is a valid, honest verdict, and it is worth more than a verdict the gate throws away.`;
|
|
64524
|
+
function ecoAdjudicatorContract(runAbs) {
|
|
64525
|
+
return `# Contract: adjudicator (sequential / --eco)
|
|
64526
|
+
|
|
64527
|
+
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.
|
|
64528
|
+
|
|
64529
|
+
There is no fan-out here and no ITEMS selection: you handle EVERY criterion, one at a time, in order.
|
|
64530
|
+
|
|
64531
|
+
## Which files \u2014 your prompt decides, and it wins over this document
|
|
64532
|
+
|
|
64533
|
+
- **With a shell.** Read \`${join47(runAbs, "ADJUDICATE.todo.json")}\`, fill each item's verdict in place, then fold: \`ultra11y verify --apply ${join47(runAbs, "ADJUDICATE.todo.json")} --in ${join47(runAbs, "audit-latest.json")} --out ${runAbs}\`.
|
|
64534
|
+
- **Without a shell** (CI: Read, Grep, Glob, Edit, Write only). Do NOT open \`ADJUDICATE.todo.json\` or \`ADJUDICATE.md\` \u2014 they run to hundreds of kilobytes and will swamp your context. Read \`${join47(runAbs, "adjudicate")}/<criteriaId>.md\`, one small brief per criterion carrying its evidence, its decision protocol, its numbered tests and this contract in short form. Write your verdicts into \`${join47(runAbs, "ADJUDICATE.verdicts.json")}\` \u2014 the ONLY file you write. Someone else folds; you never run the engine.
|
|
64535
|
+
|
|
64536
|
+
## For EACH criterion
|
|
64537
|
+
|
|
64538
|
+
1. Read its brief. \`evidence[]\` holds source-anchored excerpts (\`file\`, \`line\`, \`selector\`, \`snippet\`) \u2014 open the cited files at the cited lines whenever the snippet alone cannot decide. Copy the \`snippet\` from the brief rather than retyping it.
|
|
64539
|
+
${VERDICT_RULES}
|
|
64540
|
+
|
|
64541
|
+
Every item comes back with a verdict. Each one stands or falls on its own: a refusal costs THAT criterion and leaves every other verdict standing \u2014 so work through the list steadily, and never guess to fill a gap.
|
|
64542
|
+
|
|
64543
|
+
Do not edit any other file, do not touch the audited source, do not commit, and do not comment on any pull request.
|
|
64544
|
+
`;
|
|
64545
|
+
}
|
|
64546
|
+
function ecoRefuterContract(runAbs) {
|
|
64547
|
+
return `# Contract: refuter (sequential / --eco)
|
|
64548
|
+
|
|
64549
|
+
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.
|
|
64550
|
+
|
|
64551
|
+
There is no fan-out here and no ITEMS selection: you handle EVERY entry of \`${join47(runAbs, "VERIFY.todo.json")}\` (a JSON array; each entry has \`n\`, \`criteriaId\`, \`file\`, \`line\`, \`selector\`, \`claim\`), one at a time, writing your verdict into that same file.
|
|
64552
|
+
|
|
64553
|
+
For EACH entry:
|
|
64554
|
+
|
|
64555
|
+
1. Open \`file\` at \`line\` and read the cited element (\`selector\`) in its real context.
|
|
64556
|
+
2. Judge the claim against the source:
|
|
64557
|
+
- \`supported\` \u2014 the cited code violates the criterion exactly as claimed.
|
|
64558
|
+
- \`partial\` \u2014 a real issue, but the claim overstates it (wrong element, wrong scope, exaggerated count).
|
|
64559
|
+
- \`unsupported\` \u2014 the source does not establish the claim.
|
|
64560
|
+
- \`refuted\` \u2014 the source contradicts the claim.
|
|
64561
|
+
When unsure, choose the HARSHER verdict \u2014 a false pass is worse than a false fail.
|
|
64562
|
+
3. \`note\` is REQUIRED \u2014 one line grounded in what you read (quote or paraphrase the decisive code).
|
|
64563
|
+
|
|
64564
|
+
Then fold: \`ultra11y verify --apply ${join47(runAbs, "VERIFY.todo.json")} --report <the report .md>\`. Without a shell, leave the fold to whoever gave you the file.
|
|
64565
|
+
`;
|
|
64566
|
+
}
|
|
64567
|
+
function agentContracts(runAbs, engineAbs, opts = {}) {
|
|
64359
64568
|
const footer = ONE_WRITER_FOOTER.replaceAll("<RUN>", runAbs);
|
|
64569
|
+
if (opts.eco) return { adjudicator: ecoAdjudicatorContract(runAbs), refuter: ecoRefuterContract(runAbs) };
|
|
64360
64570
|
return {
|
|
64361
64571
|
adjudicator: `# Contract: adjudicator
|
|
64362
64572
|
|
|
@@ -64536,7 +64746,7 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
|
|
|
64536
64746
|
mkdirSync17(agentsDir, { recursive: true });
|
|
64537
64747
|
const written = [];
|
|
64538
64748
|
const notices = [];
|
|
64539
|
-
for (const [name2, content] of Object.entries(agentContracts(run2, engineAbs))) {
|
|
64749
|
+
for (const [name2, content] of Object.entries(agentContracts(run2, engineAbs, { eco: opts.eco === true }))) {
|
|
64540
64750
|
const p = join48(agentsDir, `${name2}.md`);
|
|
64541
64751
|
writeFileSync19(p, content);
|
|
64542
64752
|
written.push(p);
|
|
@@ -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.
|
|
8
|
+
version: 5.22.1
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|