ultra11y 2.24.0 → 2.26.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.
- package/.claude-plugin/marketplace.json +22 -0
- package/.claude-plugin/plugin.json +9 -0
- package/README.md +46 -1
- package/hooks/hooks.json +16 -0
- package/hooks/pre-tool-use.mjs +47 -0
- package/package.json +4 -2
- package/scripts/sync-version.mjs +11 -1
- package/scripts/ultra11y.mjs +3815 -1625
- package/scripts/vendor/wcag-2.2-fr.json +1 -1
- package/scripts/vendor/wcag-2.2-sc-universe.json +1 -1
- package/scripts/verify-skill-bundle.mjs +104 -7
- package/skills/review-a11y/SKILL.md +9 -3
- package/skills/review-a11y/scripts/ultra11y.mjs +3815 -1625
- package/skills/ultra11y/SKILL.md +5 -2
- package/skills/ultra11y/references/automation.md +37 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +3815 -1625
- package/src/cli.ts +31 -0
- package/src/config.ts +4 -0
- package/src/data/wcag-universe.json +1 -1
- package/src/hook.ts +0 -0
- package/src/types.ts +1 -1
- package/src/vendor/codeindex-engine.d.mts +227 -31
- package/src/vendor/codeindex-engine.mjs +3045 -1033
- package/src/vendor/engine.meta.json +5 -5
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"wcagVersion": "2.2",
|
|
3
3
|
"source": "https://www.w3.org/TR/WCAG22/",
|
|
4
4
|
"criteriaSource": "https://github.com/w3c/wcag",
|
|
5
|
-
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-07-
|
|
5
|
+
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-07-31 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
6
6
|
"principles": [
|
|
7
7
|
{
|
|
8
8
|
"number": 1,
|
|
@@ -15,9 +15,47 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
|
|
18
|
-
// Claude Code
|
|
19
|
-
//
|
|
20
|
-
|
|
18
|
+
// Claude Code truncates the COMBINED `description` + `when_to_use` text at 1536
|
|
19
|
+
// chars in the skill listing — that listing is what the model matches a task
|
|
20
|
+
// against, so anything past the cut is invisible and the skill silently stops
|
|
21
|
+
// auto-invoking. 1500 leaves a safety margin. NOTE: the budget is shared, not
|
|
22
|
+
// per-field: growing `description` eats into `when_to_use` and vice versa.
|
|
23
|
+
const LISTING_MAX = 1500;
|
|
24
|
+
|
|
25
|
+
// Frontmatter keys Claude Code understands, plus the two this repo carries
|
|
26
|
+
// (`license`, `metadata`). An unknown key at the root is almost always a typo
|
|
27
|
+
// — `when-to-use` for `when_to_use` costs the whole automatic trigger and
|
|
28
|
+
// otherwise fails silently, so it is a hard error here.
|
|
29
|
+
const KNOWN_KEYS = new Set([
|
|
30
|
+
"name",
|
|
31
|
+
"description",
|
|
32
|
+
"when_to_use",
|
|
33
|
+
"argument-hint",
|
|
34
|
+
"arguments",
|
|
35
|
+
"disable-model-invocation",
|
|
36
|
+
"user-invocable",
|
|
37
|
+
"allowed-tools",
|
|
38
|
+
"disallowed-tools",
|
|
39
|
+
"model",
|
|
40
|
+
"effort",
|
|
41
|
+
"context",
|
|
42
|
+
"agent",
|
|
43
|
+
"background",
|
|
44
|
+
"hooks",
|
|
45
|
+
"paths",
|
|
46
|
+
"shell",
|
|
47
|
+
"license",
|
|
48
|
+
"metadata",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/** A single-line, optionally quoted frontmatter scalar. The frontmatter here is
|
|
52
|
+
* deliberately kept single-line-per-key (no folded `>-` blocks) so this regex
|
|
53
|
+
* read stays honest — `sync-version.mjs` relies on the same shape. */
|
|
54
|
+
const scalar = (fm, key) =>
|
|
55
|
+
fm
|
|
56
|
+
.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]
|
|
57
|
+
?.trim()
|
|
58
|
+
.replace(/^["']|["']$/g, "") ?? null;
|
|
21
59
|
|
|
22
60
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
61
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
@@ -55,13 +93,24 @@ for (const name of skillNames) {
|
|
|
55
93
|
if (!fm) bad(`skills/${name}/SKILL.md has no frontmatter block`);
|
|
56
94
|
else {
|
|
57
95
|
ok("packaged SKILL.md present with frontmatter");
|
|
58
|
-
const nameLine = fm[1]
|
|
96
|
+
const nameLine = scalar(fm[1], "name");
|
|
59
97
|
nameLine === name ? ok(`frontmatter name "${name}" matches its directory`) : bad(`frontmatter name "${nameLine}" != skill directory "${name}"`);
|
|
60
|
-
|
|
98
|
+
|
|
99
|
+
// Only root-level keys: `metadata:`'s children are indented, so ^ under /m skips them.
|
|
100
|
+
const unknown = [...fm[1].matchAll(/^([A-Za-z][\w-]*):/gm)].map((m) => m[1]).filter((k) => !KNOWN_KEYS.has(k));
|
|
101
|
+
unknown.length === 0
|
|
102
|
+
? ok("frontmatter keys are all recognised")
|
|
103
|
+
: bad(`skills/${name}: unknown frontmatter key(s) ${unknown.map((k) => `"${k}"`).join(", ")} — a typo here fails silently (e.g. "when-to-use" instead of "when_to_use")`);
|
|
104
|
+
|
|
105
|
+
const desc = scalar(fm[1], "description");
|
|
106
|
+
const when = scalar(fm[1], "when_to_use");
|
|
61
107
|
if (!desc) bad(`skills/${name}: frontmatter has no description`);
|
|
62
108
|
else {
|
|
63
|
-
const
|
|
64
|
-
|
|
109
|
+
const total = desc.length + (when?.length ?? 0);
|
|
110
|
+
const parts = `description ${desc.length}${when ? ` + when_to_use ${when.length}` : ""} = ${total}`;
|
|
111
|
+
total <= LISTING_MAX
|
|
112
|
+
? ok(`${parts} chars (<= ${LISTING_MAX} safety cap)`)
|
|
113
|
+
: bad(`skills/${name}: ${parts} chars exceeds the ${LISTING_MAX}-char safety cap — Claude Code truncates the listing at 1536 and the tail stops matching`);
|
|
65
114
|
}
|
|
66
115
|
}
|
|
67
116
|
|
|
@@ -84,6 +133,54 @@ for (const name of skillNames) {
|
|
|
84
133
|
: bad(`skills/${name}/${engine} differs from ${engine} — run \`node scripts/copy-bundle.mjs\` and commit`);
|
|
85
134
|
}
|
|
86
135
|
|
|
136
|
+
// 5. The Claude Code PLUGIN shape, which is what carries the hooks. `skills add`
|
|
137
|
+
// installs skills only; the automatic pre-PR review needs the plugin manifests
|
|
138
|
+
// and the hook guard to ship together, so their absence is a hard failure.
|
|
139
|
+
console.log("plugin:");
|
|
140
|
+
const readJson = (rel) => {
|
|
141
|
+
const p = join(root, rel);
|
|
142
|
+
if (!existsSync(p)) {
|
|
143
|
+
bad(`missing ${rel} — the plugin would not install (no hooks, so no automatic review)`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
148
|
+
} catch (e) {
|
|
149
|
+
bad(`${rel} is not valid JSON: ${e.message}`);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const plugin = readJson(".claude-plugin/plugin.json");
|
|
155
|
+
if (plugin) {
|
|
156
|
+
plugin.name === pkg.name ? ok(`plugin.json name "${plugin.name}" matches the package`) : bad(`.claude-plugin/plugin.json name "${plugin.name}" != package name "${pkg.name}"`);
|
|
157
|
+
plugin.version === pkg.version
|
|
158
|
+
? ok(`plugin.json version ${plugin.version} matches the package`)
|
|
159
|
+
: bad(`.claude-plugin/plugin.json version "${plugin.version}" != package version "${pkg.version}" — add it to scripts/sync-version.mjs`);
|
|
160
|
+
plugin.description ? ok("plugin.json has a description") : bad(".claude-plugin/plugin.json has no description (required)");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const marketplace = readJson(".claude-plugin/marketplace.json");
|
|
164
|
+
if (marketplace) {
|
|
165
|
+
Array.isArray(marketplace.plugins) && marketplace.plugins.some((p) => p.name === pkg.name)
|
|
166
|
+
? ok(`marketplace.json lists the "${pkg.name}" plugin`)
|
|
167
|
+
: bad(`.claude-plugin/marketplace.json does not list a plugin named "${pkg.name}"`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const hooksJson = readJson("hooks/hooks.json");
|
|
171
|
+
if (hooksJson) {
|
|
172
|
+
Array.isArray(hooksJson.hooks?.PreToolUse) && hooksJson.hooks.PreToolUse.length > 0
|
|
173
|
+
? ok("hooks.json declares a PreToolUse hook")
|
|
174
|
+
: bad("hooks/hooks.json declares no PreToolUse hook — nothing would trigger the review before a commit/push/PR");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
existsSync(join(root, "hooks/pre-tool-use.mjs"))
|
|
178
|
+
? ok("hooks/pre-tool-use.mjs present")
|
|
179
|
+
: bad("missing hooks/pre-tool-use.mjs — hooks.json points at a guard that does not exist");
|
|
180
|
+
|
|
181
|
+
for (const f of [".claude-plugin", "hooks"])
|
|
182
|
+
(pkg.files ?? []).includes(f) ? ok(`package.json files[] carries ${f}/`) : bad(`package.json "files" is missing "${f}" — the npm tarball would ship without it`);
|
|
183
|
+
|
|
87
184
|
if (errors.length) {
|
|
88
185
|
console.error(`\nverify-skill-bundle: ${errors.length} problem(s) — a published skill would not install correctly.`);
|
|
89
186
|
process.exit(1);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review-a11y
|
|
3
3
|
description: "Use to REVIEW code changes for accessibility — '/review-a11y', 'review my changes/PR/branch for a11y', 'accessibility review of this diff'. Scopes to exactly the code under change (staged files, working diff, or branch vs merge-base), runs the bundled install-free ultra11y engine (node scripts/ultra11y.mjs, no keys; static WCAG 2.2 AA checks tied to success criteria, cross-file JSX/TSX via --graph), then adds YOUR judgment: refute false positives (SFC/library-source findings are preliminary), decide alt relevance and link purpose, name the rendering criteria (contrast, focus, zoom) as residual risks — never invent a non-conformity. Output: a severity-ranked review (blocking → major → minor) with file:line, concrete fixes, residual risks and a one-line verdict, in the conversation's language (technical tokens stay in English). Companion of the ultra11y skill (full audits, reports, PRD, RGAA packs). Triggers: 'review a11y', 'a11y review', 'accessibility review'."
|
|
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
|
|
4
6
|
license: MIT
|
|
5
7
|
metadata:
|
|
6
|
-
version: 2.
|
|
8
|
+
version: 2.26.0
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -112,8 +114,12 @@ rendered DOM; residual risks listed above.
|
|
|
112
114
|
- **Formal deliverables** (dated report, auditor conformance blocks, PRD backlog, GitHub
|
|
113
115
|
issues, RGAA/country-standard vocabulary): that is the `ultra11y` skill — same engine,
|
|
114
116
|
same findings (`report`, `prd`, `check`/`verify` gates).
|
|
115
|
-
- **Make it automatic**:
|
|
116
|
-
|
|
117
|
+
- **Make it automatic**: installed as a **Claude Code plugin**, a `PreToolUse` hook stops a
|
|
118
|
+
pending `git commit` / `git push` / `gh pr create` that carries findings and hands them
|
|
119
|
+
over — that is what invokes this skill without anyone asking. It never fires twice for the
|
|
120
|
+
same findings, so a retry lands. For a gate that does not need an agent at all, the
|
|
121
|
+
`ultra11y` skill's `init --hook` installs a mechanical pre-commit run of this same staged
|
|
122
|
+
audit.
|
|
117
123
|
|
|
118
124
|
## Do not
|
|
119
125
|
|