ultra11y 5.34.0 → 5.34.2
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/package.json +1 -1
- package/scripts/ultra11y.mjs +17 -8
- package/scripts/verify-skill-bundle.mjs +15 -16
- package/skills/review-a11y/SKILL.md +2 -4
- package/skills/review-a11y/scripts/ultra11y.mjs +17 -8
- package/skills/ultra11y/SKILL.md +7 -5
- package/skills/ultra11y/scripts/ultra11y.mjs +17 -8
- package/src/adjudicate.ts +50 -6
- package/src/types.ts +1 -1
- package/src/verdict-rules.ts +3 -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.34.
|
|
13
|
+
"version": "5.34.2",
|
|
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.34.
|
|
4
|
+
"version": "5.34.2",
|
|
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.34.
|
|
4
|
+
"version": "5.34.2",
|
|
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.34.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.34.2";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.34.
|
|
3
|
+
"version": "5.34.2",
|
|
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 join52, 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.34.
|
|
9
|
+
var VERSION = "5.34.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -59367,6 +59367,12 @@ function auditFiles(audit2, cwd) {
|
|
|
59367
59367
|
return /* @__PURE__ */ new Set();
|
|
59368
59368
|
}
|
|
59369
59369
|
}
|
|
59370
|
+
function offHarvestHint(criteriaId, evidence) {
|
|
59371
|
+
const anchors = [...new Set(evidence.map((e) => e.selector?.trim() || `${e.file}:${e.line}`).filter(Boolean))];
|
|
59372
|
+
const shown = anchors.slice(0, 4).join(", ");
|
|
59373
|
+
const rest = anchors.length > 4 ? `, +${anchors.length - 4}` : "";
|
|
59374
|
+
return anchors.length ? ` \u2014 this anchor is not among the evidence harvested for ${criteriaId}, so the snippet was verified literally rather than vouched for by the harvest. Cite one of: ${shown}${rest}.` : ` \u2014 ${criteriaId} was harvested no evidence, so nothing can vouch for a transcription here. If the criterion has no subject in scope, the verdict is NA with a justification and no citation to ground.`;
|
|
59375
|
+
}
|
|
59370
59376
|
function tagOf2(x) {
|
|
59371
59377
|
const fromSnippet = /<\s*([a-zA-Z][\w-]*)/.exec(x.snippet ?? "");
|
|
59372
59378
|
if (fromSnippet) return fromSnippet[1].toLowerCase();
|
|
@@ -59538,8 +59544,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59538
59544
|
}
|
|
59539
59545
|
return engineNcCache.get(key2) ?? EMPTY_IDS;
|
|
59540
59546
|
};
|
|
59541
|
-
const toGround = (criteriaId, g, fallback) => {
|
|
59542
|
-
const entry = { g, ...fallback ? { fallback } : {} };
|
|
59547
|
+
const toGround = (criteriaId, g, fallback, offHarvest) => {
|
|
59548
|
+
const entry = { g, ...fallback ? { fallback } : {}, ...offHarvest ? { offHarvest } : {} };
|
|
59543
59549
|
const list = groundInputs.get(criteriaId);
|
|
59544
59550
|
if (list) list.push(entry);
|
|
59545
59551
|
else groundInputs.set(criteriaId, [entry]);
|
|
@@ -59584,7 +59590,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59584
59590
|
anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
|
|
59585
59591
|
);
|
|
59586
59592
|
} else {
|
|
59587
|
-
toGround(it.criteriaId, cite);
|
|
59593
|
+
toGround(it.criteriaId, cite, void 0, anchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
|
|
59588
59594
|
}
|
|
59589
59595
|
}
|
|
59590
59596
|
}
|
|
@@ -59644,7 +59650,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59644
59650
|
}
|
|
59645
59651
|
const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
59646
59652
|
for (const [criteriaId, inputs] of groundInputs) {
|
|
59647
|
-
for (const { g, fallback } of inputs) {
|
|
59653
|
+
for (const { g, fallback, offHarvest } of inputs) {
|
|
59648
59654
|
let r = groundFinding(g, { cwd: opts.cwd });
|
|
59649
59655
|
if (!r.ok && fallback) {
|
|
59650
59656
|
const viaAnchor = groundFinding(fallback, { cwd: opts.cwd });
|
|
@@ -59656,8 +59662,9 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59656
59662
|
} else {
|
|
59657
59663
|
grounding.failed++;
|
|
59658
59664
|
if (r.issue) {
|
|
59659
|
-
|
|
59660
|
-
|
|
59665
|
+
const issue = offHarvest && r.issue.startsWith("cited snippet not found") ? `${r.issue}${offHarvest}` : r.issue;
|
|
59666
|
+
grounding.issues.push(issue);
|
|
59667
|
+
blame(criteriaId, issue);
|
|
59661
59668
|
}
|
|
59662
59669
|
}
|
|
59663
59670
|
}
|
|
@@ -60378,7 +60385,9 @@ var VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict mis
|
|
|
60378
60385
|
- \`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.
|
|
60379
60386
|
- \`NA\` (not applicable) \u2014 REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
|
|
60380
60387
|
- \`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).`;
|
|
60381
|
-
var ABSENCE_RULE = `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
|
|
60388
|
+
var ABSENCE_RULE = `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\`.
|
|
60389
|
+
|
|
60390
|
+
AND CITE FROM THIS CRITERION'S OWN ANCHORS, NOT THE THING YOU ARE RULING OUT. An absence pulls you toward the element you are arguing ABOUT \u2014 the search form that is not a site search engine, the menu that is not a second navigation system \u2014 and that element is, precisely because it is off-topic, absent from what this criterion was harvested. A citation on one of the criterion's own anchors is vouched for by the harvest; one outside it has its snippet verified LITERALLY, character for character, and a retyping then fails. Cite the region you inspected \u2014 the \`header\`, the \`nav\`, the \`footer\` the brief listed \u2014 and say in the justification what you did not find in it.`;
|
|
60382
60391
|
var CAPTURE_RULE = `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.`;
|
|
60383
60392
|
var NEVER_GUESS_RULE = `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.`;
|
|
60384
60393
|
var SCOPE_RULE = `Rule ONLY on the criteria presented. Never introduce another \u2014 a verdict for a criterion nobody asked about is dropped, and under the fold it would otherwise overwrite what the deterministic engine already decided.`;
|
|
@@ -16,17 +16,14 @@ import { spawnSync } from "node:child_process";
|
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
// (`license`, `metadata`). An unknown key at the root is almost always a typo
|
|
28
|
-
// — `when-to-use` for `when_to_use` costs the whole automatic trigger and
|
|
29
|
-
// otherwise fails silently, so it is a hard error here.
|
|
19
|
+
// Codex accepts descriptions up to 1024 characters. Keep the gate at the
|
|
20
|
+
// official limit so the same frontmatter remains discoverable across hosts.
|
|
21
|
+
const DESCRIPTION_MAX = 1024;
|
|
22
|
+
const CLAUDE_LISTING_MAX = 1500;
|
|
23
|
+
|
|
24
|
+
// Accept the union of the Codex keys and the Claude Code keys supported by this
|
|
25
|
+
// multi-host repository. The installable skills themselves are tested below
|
|
26
|
+
// against the stricter portable subset.
|
|
30
27
|
const KNOWN_KEYS = new Set([
|
|
31
28
|
"name",
|
|
32
29
|
"description",
|
|
@@ -107,11 +104,13 @@ for (const name of skillNames) {
|
|
|
107
104
|
const when = scalar(fm[1], "when_to_use");
|
|
108
105
|
if (!desc) bad(`skills/${name}: frontmatter has no description`);
|
|
109
106
|
else {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
desc.length <= DESCRIPTION_MAX
|
|
108
|
+
? ok(`description ${desc.length} chars (<= ${DESCRIPTION_MAX} Codex limit)`)
|
|
109
|
+
: bad(`skills/${name}: description ${desc.length} chars exceeds the ${DESCRIPTION_MAX}-char Codex limit`);
|
|
110
|
+
const listingLength = desc.length + (when?.length ?? 0);
|
|
111
|
+
listingLength <= CLAUDE_LISTING_MAX
|
|
112
|
+
? ok(`Claude listing ${listingLength} chars (<= ${CLAUDE_LISTING_MAX} safety cap)`)
|
|
113
|
+
: bad(`skills/${name}: Claude listing ${listingLength} chars exceeds the ${CLAUDE_LISTING_MAX}-char safety cap`);
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review-a11y
|
|
3
|
-
description: "Use to REVIEW code
|
|
4
|
-
when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should be checked: the user just finished or modified a component, template, page or stylesheet; is about to commit, push or open a pull request; asks 'is this accessible?' or 'anything to fix before merge?'; a diff, branch or PR is under discussion; or a hook or CI reports accessibility findings on the change. NOT for a whole-repo audit, a dated conformance report, an RGAA/Section 508 deliverable or a PRD backlog — that is the `ultra11y` skill."
|
|
5
|
-
allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
|
|
3
|
+
description: "Use to REVIEW changed frontend code for accessibility: staged files, a working diff, branch, or PR; prompts such as 'review a11y', 'is this accessible?', or 'anything to fix before merge?'; and hook or CI findings on a change. Run the bundled install-free engine with cross-file JSX/TSX analysis, then adjudicate candidates and return a severity-ranked WCAG 2.2 AA review with file:line fixes, residual rendering risks, and a verdict. Use `ultra11y` instead for whole-repo audits, dated conformance reports, country standards, PRD backlogs, or authoring accessible markup."
|
|
6
4
|
license: MIT
|
|
7
5
|
metadata:
|
|
8
|
-
version: 5.34.
|
|
6
|
+
version: 5.34.2
|
|
9
7
|
---
|
|
10
8
|
|
|
11
9
|
# review-a11y — accessibility review of changed code
|
|
@@ -6,7 +6,7 @@ import { join as join52, 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.34.
|
|
9
|
+
var VERSION = "5.34.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -59367,6 +59367,12 @@ function auditFiles(audit2, cwd) {
|
|
|
59367
59367
|
return /* @__PURE__ */ new Set();
|
|
59368
59368
|
}
|
|
59369
59369
|
}
|
|
59370
|
+
function offHarvestHint(criteriaId, evidence) {
|
|
59371
|
+
const anchors = [...new Set(evidence.map((e) => e.selector?.trim() || `${e.file}:${e.line}`).filter(Boolean))];
|
|
59372
|
+
const shown = anchors.slice(0, 4).join(", ");
|
|
59373
|
+
const rest = anchors.length > 4 ? `, +${anchors.length - 4}` : "";
|
|
59374
|
+
return anchors.length ? ` \u2014 this anchor is not among the evidence harvested for ${criteriaId}, so the snippet was verified literally rather than vouched for by the harvest. Cite one of: ${shown}${rest}.` : ` \u2014 ${criteriaId} was harvested no evidence, so nothing can vouch for a transcription here. If the criterion has no subject in scope, the verdict is NA with a justification and no citation to ground.`;
|
|
59375
|
+
}
|
|
59370
59376
|
function tagOf2(x) {
|
|
59371
59377
|
const fromSnippet = /<\s*([a-zA-Z][\w-]*)/.exec(x.snippet ?? "");
|
|
59372
59378
|
if (fromSnippet) return fromSnippet[1].toLowerCase();
|
|
@@ -59538,8 +59544,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59538
59544
|
}
|
|
59539
59545
|
return engineNcCache.get(key2) ?? EMPTY_IDS;
|
|
59540
59546
|
};
|
|
59541
|
-
const toGround = (criteriaId, g, fallback) => {
|
|
59542
|
-
const entry = { g, ...fallback ? { fallback } : {} };
|
|
59547
|
+
const toGround = (criteriaId, g, fallback, offHarvest) => {
|
|
59548
|
+
const entry = { g, ...fallback ? { fallback } : {}, ...offHarvest ? { offHarvest } : {} };
|
|
59543
59549
|
const list = groundInputs.get(criteriaId);
|
|
59544
59550
|
if (list) list.push(entry);
|
|
59545
59551
|
else groundInputs.set(criteriaId, [entry]);
|
|
@@ -59584,7 +59590,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59584
59590
|
anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
|
|
59585
59591
|
);
|
|
59586
59592
|
} else {
|
|
59587
|
-
toGround(it.criteriaId, cite);
|
|
59593
|
+
toGround(it.criteriaId, cite, void 0, anchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
|
|
59588
59594
|
}
|
|
59589
59595
|
}
|
|
59590
59596
|
}
|
|
@@ -59644,7 +59650,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59644
59650
|
}
|
|
59645
59651
|
const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
59646
59652
|
for (const [criteriaId, inputs] of groundInputs) {
|
|
59647
|
-
for (const { g, fallback } of inputs) {
|
|
59653
|
+
for (const { g, fallback, offHarvest } of inputs) {
|
|
59648
59654
|
let r = groundFinding(g, { cwd: opts.cwd });
|
|
59649
59655
|
if (!r.ok && fallback) {
|
|
59650
59656
|
const viaAnchor = groundFinding(fallback, { cwd: opts.cwd });
|
|
@@ -59656,8 +59662,9 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59656
59662
|
} else {
|
|
59657
59663
|
grounding.failed++;
|
|
59658
59664
|
if (r.issue) {
|
|
59659
|
-
|
|
59660
|
-
|
|
59665
|
+
const issue = offHarvest && r.issue.startsWith("cited snippet not found") ? `${r.issue}${offHarvest}` : r.issue;
|
|
59666
|
+
grounding.issues.push(issue);
|
|
59667
|
+
blame(criteriaId, issue);
|
|
59661
59668
|
}
|
|
59662
59669
|
}
|
|
59663
59670
|
}
|
|
@@ -60378,7 +60385,9 @@ var VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict mis
|
|
|
60378
60385
|
- \`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.
|
|
60379
60386
|
- \`NA\` (not applicable) \u2014 REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
|
|
60380
60387
|
- \`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).`;
|
|
60381
|
-
var ABSENCE_RULE = `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
|
|
60388
|
+
var ABSENCE_RULE = `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\`.
|
|
60389
|
+
|
|
60390
|
+
AND CITE FROM THIS CRITERION'S OWN ANCHORS, NOT THE THING YOU ARE RULING OUT. An absence pulls you toward the element you are arguing ABOUT \u2014 the search form that is not a site search engine, the menu that is not a second navigation system \u2014 and that element is, precisely because it is off-topic, absent from what this criterion was harvested. A citation on one of the criterion's own anchors is vouched for by the harvest; one outside it has its snippet verified LITERALLY, character for character, and a retyping then fails. Cite the region you inspected \u2014 the \`header\`, the \`nav\`, the \`footer\` the brief listed \u2014 and say in the justification what you did not find in it.`;
|
|
60382
60391
|
var CAPTURE_RULE = `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.`;
|
|
60383
60392
|
var NEVER_GUESS_RULE = `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.`;
|
|
60384
60393
|
var SCOPE_RULE = `Rule ONLY on the criteria presented. Never introduce another \u2014 a verdict for a criterion nobody asked about is dropped, and under the fold it would otherwise overwrite what the deterministic engine already decided.`;
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ultra11y
|
|
3
|
-
description: "Use to AUDIT
|
|
4
|
-
when_to_use: "Invoke when the user asks for an accessibility AUDIT or a formal deliverable: audit a repo, site or page against WCAG 2.2 AA or a country standard (RGAA, Section 508, EN 301 549); produce a dated conformance report, a criterion grid, a PRD backlog, or tickets (GitHub/GitLab/Jira); author accessible markup from scratch; wire the repo gate (init --hook/--ci) or a rendered-DOM capture pipeline. For a review of the code UNDER CHANGE (diff, branch, PR), use the `review-a11y` skill instead."
|
|
3
|
+
description: "Use to AUDIT a repository, site, or page against WCAG 2.2 AA or a country standard such as RGAA; produce a dated conformance report, criterion grid, PRD backlog, or tickets; AUTHOR accessible markup; fix accessibility; or wire a repo gate and rendered-DOM capture pipeline. The bundled install-free engine runs 81 static checks tied to success criteria, while the agent adjudicates judgment criteria and routes rendering criteria to browser scans; check/verify reject invented non-conformities. Use `review-a11y` instead for a review scoped only to a diff, branch, or PR."
|
|
5
4
|
license: MIT
|
|
6
5
|
metadata:
|
|
7
|
-
version: 5.34.
|
|
6
|
+
version: 5.34.2
|
|
8
7
|
---
|
|
9
8
|
|
|
10
9
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -14,7 +13,7 @@ that with a **division of labour**: the deterministic, install-free engine
|
|
|
14
13
|
(`node scripts/ultra11y.mjs <command>` — no `npm install`, no key; the JSX/TSX parser
|
|
15
14
|
is embedded in the bundle) does the *mechanical* work — detect the machine-checkable
|
|
16
15
|
non-conformities and tie each to the right **WCAG success criterion** — and **the AI agent**
|
|
17
|
-
(
|
|
16
|
+
(the AI agent running this skill) *adjudicates the judgment criteria itself* — alt relevance, link
|
|
18
17
|
purpose in context, reading order — statically, from the evidence the engine harvests
|
|
19
18
|
(`verify --manual`), each verdict gated by `verify`/`check`. Only the truly **rendered-DOM**
|
|
20
19
|
criteria (computed contrast, visible focus, zoom/reflow, content-on-hover) fall to the `scan`
|
|
@@ -411,8 +410,11 @@ This skill does the analysis end to end: audit, render, adjudicate, fix, re-audi
|
|
|
411
410
|
When it has produced fixes, **dispatch `review-a11y` on the code under change as a subagent**,
|
|
412
411
|
and report what it returns.
|
|
413
412
|
|
|
413
|
+
Use the host's native subagent capability: Codex dispatches a subagent with the prompt below;
|
|
414
|
+
Claude Code can use `Agent(...)`. If the host exposes neither, follow the sequential route below.
|
|
415
|
+
|
|
414
416
|
```
|
|
415
|
-
|
|
417
|
+
Use the review-a11y skill on the working diff. Return its report verbatim.
|
|
416
418
|
```
|
|
417
419
|
|
|
418
420
|
Two skills, two scopes, and the split is not cosmetic:
|
|
@@ -6,7 +6,7 @@ import { join as join52, 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.34.
|
|
9
|
+
var VERSION = "5.34.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -59367,6 +59367,12 @@ function auditFiles(audit2, cwd) {
|
|
|
59367
59367
|
return /* @__PURE__ */ new Set();
|
|
59368
59368
|
}
|
|
59369
59369
|
}
|
|
59370
|
+
function offHarvestHint(criteriaId, evidence) {
|
|
59371
|
+
const anchors = [...new Set(evidence.map((e) => e.selector?.trim() || `${e.file}:${e.line}`).filter(Boolean))];
|
|
59372
|
+
const shown = anchors.slice(0, 4).join(", ");
|
|
59373
|
+
const rest = anchors.length > 4 ? `, +${anchors.length - 4}` : "";
|
|
59374
|
+
return anchors.length ? ` \u2014 this anchor is not among the evidence harvested for ${criteriaId}, so the snippet was verified literally rather than vouched for by the harvest. Cite one of: ${shown}${rest}.` : ` \u2014 ${criteriaId} was harvested no evidence, so nothing can vouch for a transcription here. If the criterion has no subject in scope, the verdict is NA with a justification and no citation to ground.`;
|
|
59375
|
+
}
|
|
59370
59376
|
function tagOf2(x) {
|
|
59371
59377
|
const fromSnippet = /<\s*([a-zA-Z][\w-]*)/.exec(x.snippet ?? "");
|
|
59372
59378
|
if (fromSnippet) return fromSnippet[1].toLowerCase();
|
|
@@ -59538,8 +59544,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59538
59544
|
}
|
|
59539
59545
|
return engineNcCache.get(key2) ?? EMPTY_IDS;
|
|
59540
59546
|
};
|
|
59541
|
-
const toGround = (criteriaId, g, fallback) => {
|
|
59542
|
-
const entry = { g, ...fallback ? { fallback } : {} };
|
|
59547
|
+
const toGround = (criteriaId, g, fallback, offHarvest) => {
|
|
59548
|
+
const entry = { g, ...fallback ? { fallback } : {}, ...offHarvest ? { offHarvest } : {} };
|
|
59543
59549
|
const list = groundInputs.get(criteriaId);
|
|
59544
59550
|
if (list) list.push(entry);
|
|
59545
59551
|
else groundInputs.set(criteriaId, [entry]);
|
|
@@ -59584,7 +59590,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59584
59590
|
anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
|
|
59585
59591
|
);
|
|
59586
59592
|
} else {
|
|
59587
|
-
toGround(it.criteriaId, cite);
|
|
59593
|
+
toGround(it.criteriaId, cite, void 0, anchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
|
|
59588
59594
|
}
|
|
59589
59595
|
}
|
|
59590
59596
|
}
|
|
@@ -59644,7 +59650,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59644
59650
|
}
|
|
59645
59651
|
const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
59646
59652
|
for (const [criteriaId, inputs] of groundInputs) {
|
|
59647
|
-
for (const { g, fallback } of inputs) {
|
|
59653
|
+
for (const { g, fallback, offHarvest } of inputs) {
|
|
59648
59654
|
let r = groundFinding(g, { cwd: opts.cwd });
|
|
59649
59655
|
if (!r.ok && fallback) {
|
|
59650
59656
|
const viaAnchor = groundFinding(fallback, { cwd: opts.cwd });
|
|
@@ -59656,8 +59662,9 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59656
59662
|
} else {
|
|
59657
59663
|
grounding.failed++;
|
|
59658
59664
|
if (r.issue) {
|
|
59659
|
-
|
|
59660
|
-
|
|
59665
|
+
const issue = offHarvest && r.issue.startsWith("cited snippet not found") ? `${r.issue}${offHarvest}` : r.issue;
|
|
59666
|
+
grounding.issues.push(issue);
|
|
59667
|
+
blame(criteriaId, issue);
|
|
59661
59668
|
}
|
|
59662
59669
|
}
|
|
59663
59670
|
}
|
|
@@ -60378,7 +60385,9 @@ var VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict mis
|
|
|
60378
60385
|
- \`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.
|
|
60379
60386
|
- \`NA\` (not applicable) \u2014 REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
|
|
60380
60387
|
- \`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).`;
|
|
60381
|
-
var ABSENCE_RULE = `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
|
|
60388
|
+
var ABSENCE_RULE = `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\`.
|
|
60389
|
+
|
|
60390
|
+
AND CITE FROM THIS CRITERION'S OWN ANCHORS, NOT THE THING YOU ARE RULING OUT. An absence pulls you toward the element you are arguing ABOUT \u2014 the search form that is not a site search engine, the menu that is not a second navigation system \u2014 and that element is, precisely because it is off-topic, absent from what this criterion was harvested. A citation on one of the criterion's own anchors is vouched for by the harvest; one outside it has its snippet verified LITERALLY, character for character, and a retyping then fails. Cite the region you inspected \u2014 the \`header\`, the \`nav\`, the \`footer\` the brief listed \u2014 and say in the justification what you did not find in it.`;
|
|
60382
60391
|
var CAPTURE_RULE = `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.`;
|
|
60383
60392
|
var NEVER_GUESS_RULE = `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.`;
|
|
60384
60393
|
var SCOPE_RULE = `Rule ONLY on the criteria presented. Never introduce another \u2014 a verdict for a criterion nobody asked about is dropped, and under the fold it would otherwise overwrite what the deterministic engine already decided.`;
|
package/src/adjudicate.ts
CHANGED
|
@@ -484,6 +484,22 @@ function auditFiles(audit: AuditResult, cwd?: string): Set<string> {
|
|
|
484
484
|
* criterion was never given. And it is only half the gate — `groundFinding` still has to find
|
|
485
485
|
* the cited content in the real source, so a plausible-looking file:line in the right
|
|
486
486
|
* neighbourhood proves nothing on its own. */
|
|
487
|
+
/** Why a citation was held to the literal check, and what to cite instead.
|
|
488
|
+
*
|
|
489
|
+
* Appended to a `cited snippet not found` refusal when the citation missed THIS criterion's
|
|
490
|
+
* harvest. Without it the message names a symptom — a transcription that did not match — and
|
|
491
|
+
* hides the cause, which is that the citation left the evidence the criterion was shown. The
|
|
492
|
+
* two are fixed differently: one asks the adjudicator to copy more carefully, the other asks
|
|
493
|
+
* it to cite something else entirely. */
|
|
494
|
+
export function offHarvestHint(criteriaId: string, evidence: Evidence[]): string {
|
|
495
|
+
const anchors = [...new Set(evidence.map((e) => e.selector?.trim() || `${e.file}:${e.line}`).filter(Boolean))];
|
|
496
|
+
const shown = anchors.slice(0, 4).join(", ");
|
|
497
|
+
const rest = anchors.length > 4 ? `, +${anchors.length - 4}` : "";
|
|
498
|
+
return anchors.length
|
|
499
|
+
? ` — this anchor is not among the evidence harvested for ${criteriaId}, so the snippet was verified literally rather than vouched for by the harvest. Cite one of: ${shown}${rest}.`
|
|
500
|
+
: ` — ${criteriaId} was harvested no evidence, so nothing can vouch for a transcription here. If the criterion has no subject in scope, the verdict is NA with a justification and no citation to ground.`;
|
|
501
|
+
}
|
|
502
|
+
|
|
487
503
|
/** The tag a citation or an anchor is about, from its snippet first and its selector second.
|
|
488
504
|
* Lowercased — HTML tag names are case-insensitive. Undefined when neither says. */
|
|
489
505
|
function tagOf(x: { snippet?: string; selector?: string }): string | undefined {
|
|
@@ -789,7 +805,7 @@ export function applyAdjudication(
|
|
|
789
805
|
// flat list) for the same reason as `blame`: a failed citation has to condemn its own
|
|
790
806
|
// criterion and no other.
|
|
791
807
|
type Ground = { file: string; line: number; selector?: string; snippet?: string };
|
|
792
|
-
const groundInputs = new Map<string, { g: Ground; fallback?: Ground }[]>();
|
|
808
|
+
const groundInputs = new Map<string, { g: Ground; fallback?: Ground; offHarvest?: string }[]>();
|
|
793
809
|
// Memoised: only a citation that missed its criterion's own anchors ever asks for it.
|
|
794
810
|
let scopeCache: Set<string> | undefined;
|
|
795
811
|
const { citationDrift } = adjudicationLimits(opts.cwd);
|
|
@@ -819,8 +835,9 @@ export function applyAdjudication(
|
|
|
819
835
|
criteriaId: string,
|
|
820
836
|
g: { file: string; line: number; selector?: string; snippet?: string },
|
|
821
837
|
fallback?: { file: string; line: number; selector?: string; snippet?: string },
|
|
838
|
+
offHarvest?: string,
|
|
822
839
|
) => {
|
|
823
|
-
const entry = { g, ...(fallback ? { fallback } : {}) };
|
|
840
|
+
const entry = { g, ...(fallback ? { fallback } : {}), ...(offHarvest ? { offHarvest } : {}) };
|
|
824
841
|
const list = groundInputs.get(criteriaId);
|
|
825
842
|
if (list) list.push(entry);
|
|
826
843
|
else groundInputs.set(criteriaId, [entry]);
|
|
@@ -948,7 +965,30 @@ export function applyAdjudication(
|
|
|
948
965
|
: { file: c.file, line: c.line, selector: anchor.at.selector ?? c.selector },
|
|
949
966
|
);
|
|
950
967
|
} else {
|
|
951
|
-
|
|
968
|
+
// WHY THE STRICT PATH APPLIED, said at the point it applies.
|
|
969
|
+
//
|
|
970
|
+
// The citation belongs (it is in a file this audit read) but it is not on an anchor
|
|
971
|
+
// THIS criterion was shown, so the paragraph above cannot let the harvest vouch for
|
|
972
|
+
// the transcription — and the refusal that follows says only `cited snippet not
|
|
973
|
+
// found`, which reads as « you mistyped » when the actionable fact is « you cited
|
|
974
|
+
// outside this criterion's evidence, so your retyping was verified literally ».
|
|
975
|
+
//
|
|
976
|
+
// Measured: that message sent a reader diagnosing RGAA 12.5 to the wrong conclusion
|
|
977
|
+
// — a fold bug — with this source open. The risk is not the lost criterion, it is
|
|
978
|
+
// the « fix » that would follow: relaxing the strict path is exactly the hole the
|
|
979
|
+
// two bounds above exist to close. So the cause travels with the refusal.
|
|
980
|
+
//
|
|
981
|
+
// It bites hardest where the criterion's subject is an ABSENCE. 12.1 and 12.5 are
|
|
982
|
+
// the repeat offenders the adjudicator prompt already names: asked to prove there is
|
|
983
|
+
// no search engine, a model cites the search form it is arguing about — which is, by
|
|
984
|
+
// definition, not among the anchors harvested for a criterion about navigation.
|
|
985
|
+
// ONLY when the citation is genuinely off-harvest. Landing on an anchor and then
|
|
986
|
+
// claiming a different KIND of element (an <img> cited as a <video>) also reaches
|
|
987
|
+
// this branch, and telling that reader « not among the harvested evidence » would
|
|
988
|
+
// be false: the anchor is right there, it is the element that is wrong. That case
|
|
989
|
+
// keeps the bare refusal, which already says the honest thing — this markup is not
|
|
990
|
+
// in the file.
|
|
991
|
+
toGround(it.criteriaId, cite, undefined, anchor ? undefined : offHarvestHint(it.criteriaId, it.evidence));
|
|
952
992
|
}
|
|
953
993
|
}
|
|
954
994
|
}
|
|
@@ -1069,7 +1109,7 @@ export function applyAdjudication(
|
|
|
1069
1109
|
// flat `groundItems` produced.
|
|
1070
1110
|
const grounding: GroundingSummary = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
1071
1111
|
for (const [criteriaId, inputs] of groundInputs) {
|
|
1072
|
-
for (const { g, fallback } of inputs) {
|
|
1112
|
+
for (const { g, fallback, offHarvest } of inputs) {
|
|
1073
1113
|
let r = groundFinding(g, { cwd: opts.cwd });
|
|
1074
1114
|
// The harvested anchor vouches for a citation the file could not confirm on its own —
|
|
1075
1115
|
// an adjudicator's retyping of an element it did read. `moved` either way: what
|
|
@@ -1084,8 +1124,12 @@ export function applyAdjudication(
|
|
|
1084
1124
|
} else {
|
|
1085
1125
|
grounding.failed++;
|
|
1086
1126
|
if (r.issue) {
|
|
1087
|
-
|
|
1088
|
-
|
|
1127
|
+
// The cause travels with the refusal, and only when it IS the cause: a citation that
|
|
1128
|
+
// never reached the literal check (a missing file, a line out of range) is not an
|
|
1129
|
+
// off-harvest problem, and saying so would send the reader chasing the wrong thing.
|
|
1130
|
+
const issue = offHarvest && r.issue.startsWith("cited snippet not found") ? `${r.issue}${offHarvest}` : r.issue;
|
|
1131
|
+
grounding.issues.push(issue);
|
|
1132
|
+
blame(criteriaId, issue);
|
|
1089
1133
|
}
|
|
1090
1134
|
}
|
|
1091
1135
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Single source of truth for shared types + the embedded version. sync-version.mjs
|
|
2
2
|
// rewrites VERSION at release time (kept in lockstep with package.json + SKILL.md).
|
|
3
|
-
export const VERSION = "5.34.
|
|
3
|
+
export const VERSION = "5.34.2";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|
package/src/verdict-rules.ts
CHANGED
|
@@ -27,7 +27,9 @@ export const VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED — a verdi
|
|
|
27
27
|
- \`manual\` (still undecidable) — 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).`;
|
|
28
28
|
|
|
29
29
|
/** An absence is observed somewhere. Measured: 12.1 and 12.5 lost to this on one run. */
|
|
30
|
-
export const ABSENCE_RULE = `AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. « No second navigation system », « no search engine », « no error message suggests the expected format » — 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
|
|
30
|
+
export const ABSENCE_RULE = `AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. « No second navigation system », « no search engine », « no error message suggests the expected format » — 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\`.
|
|
31
|
+
|
|
32
|
+
AND CITE FROM THIS CRITERION'S OWN ANCHORS, NOT THE THING YOU ARE RULING OUT. An absence pulls you toward the element you are arguing ABOUT — the search form that is not a site search engine, the menu that is not a second navigation system — and that element is, precisely because it is off-topic, absent from what this criterion was harvested. A citation on one of the criterion's own anchors is vouched for by the harvest; one outside it has its snippet verified LITERALLY, character for character, and a retyping then fails. Cite the region you inspected — the \`header\`, the \`nav\`, the \`footer\` the brief listed — and say in the justification what you did not find in it.`;
|
|
31
33
|
|
|
32
34
|
/** `needs-rendered-dom` is a claim about the RUN, not a way out of a hard criterion. */
|
|
33
35
|
export const CAPTURE_RULE = `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 — decide it from those files, or answer \`undecidable\` and say what the capture does not settle.`;
|