ultra11y 2.31.0 → 2.31.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/README.md +6 -1
- package/dist/cypress-plugin.d.ts +6 -0
- package/dist/cypress-plugin.mjs +13 -0
- package/dist/cypress.d.ts +11 -0
- package/dist/cypress.mjs +1 -0
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +79 -4
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +79 -4
- package/skills/ultra11y/SKILL.md +24 -1
- package/skills/ultra11y/references/ci.md +34 -0
- package/skills/ultra11y/references/e2e.md +6 -3
- package/skills/ultra11y/references/extension.md +1 -1
- package/skills/ultra11y/references/judgment.md +7 -2
- package/skills/ultra11y/scripts/ultra11y.mjs +79 -4
- package/src/cli.ts +13 -0
- package/src/dev.ts +11 -5
- package/src/integrations/cypress-plugin.ts +4 -1
- package/src/integrations/cypress.ts +8 -0
- package/src/mcp/handlers.ts +60 -2
- package/src/mcp/tools.ts +26 -0
- package/src/types.ts +1 -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": "2.31.
|
|
13
|
+
"version": "2.31.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": "2.31.
|
|
4
|
+
"version": "2.31.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": "2.31.
|
|
4
|
+
"version": "2.31.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 = "2.31.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "2.31.2";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -195,7 +195,7 @@ the one output an accessibility tool must never produce.
|
|
|
195
195
|
|
|
196
196
|
### Tools
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
Eleven read tools. `ultra11y_audit` is the entry point:
|
|
199
199
|
|
|
200
200
|
| Tool | What it does |
|
|
201
201
|
|------|--------------|
|
|
@@ -208,6 +208,7 @@ Ten read tools. `ultra11y_audit` is the entry point:
|
|
|
208
208
|
| `ultra11y_verify` | Claim↔evidence worklist |
|
|
209
209
|
| `ultra11y_pack_check` | Validate a country standards pack against what it really ships |
|
|
210
210
|
| `ultra11y_sample_check` | Lint the normative page sample |
|
|
211
|
+
| `ultra11y_pages` | The per-page view: the criterion × page grid, or one dossier per page |
|
|
211
212
|
| `ultra11y_read` | A file, or a line range, from the project |
|
|
212
213
|
|
|
213
214
|
`--allow-write` additionally exposes `ultra11y_fix` (safe codemods, dry-run by
|
|
@@ -216,6 +217,10 @@ project. `ultra11y_scan` is declared but declines over MCP: it drives a headless
|
|
|
216
217
|
browser and may pull a Docker image, which is not a subprocess lifecycle a
|
|
217
218
|
long-lived server should own. It tells you to run it from the CLI and re-audit.
|
|
218
219
|
|
|
220
|
+
Every tool audits exactly what the CLI would: the rendered captures and the page
|
|
221
|
+
snapshots under `.ultra11y/` are ingested here too, so the server never reports a
|
|
222
|
+
smaller audit than the same command run by hand.
|
|
223
|
+
|
|
219
224
|
Pass `--cwd <dir>` at startup to dedicate the server to one project — `cwd` then
|
|
220
225
|
becomes optional on every tool.
|
|
221
226
|
|
package/dist/cypress-plugin.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ interface TaskPayload extends SnapshotPayload {
|
|
|
5
5
|
failOn?: string | false;
|
|
6
6
|
/** The name `cy.screenshot()` was called with, when the browser half took one. */
|
|
7
7
|
screenshotName?: string;
|
|
8
|
+
/** Forwarded from `cy.ultra11y({ report })` — the browser cannot write files, this side can. */
|
|
9
|
+
report?: boolean | {
|
|
10
|
+
out?: string;
|
|
11
|
+
standard?: string;
|
|
12
|
+
lang?: string;
|
|
13
|
+
};
|
|
8
14
|
}
|
|
9
15
|
interface TaskResult {
|
|
10
16
|
findings: FindingLike[];
|
package/dist/cypress-plugin.mjs
CHANGED
|
@@ -48,6 +48,18 @@ ${res.stderr || ""}`);
|
|
|
48
48
|
${res.stdout.slice(0, 500)}`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
function writePagesReport(opts = {}, engine = enginePath()) {
|
|
52
|
+
const args = [engine, "pages", "--in", "-", "--format", "report"];
|
|
53
|
+
if (opts.split !== false) args.push("--split", "page");
|
|
54
|
+
args.push("--out", opts.out ?? "audits/pages");
|
|
55
|
+
if (opts.standard) args.push("--standard", opts.standard);
|
|
56
|
+
if (opts.lang) args.push("--lang", opts.lang);
|
|
57
|
+
const audit = spawnSync(process.execPath, [engine, "audit", ".ultra11y/pages", "--json"], { encoding: "utf8", maxBuffer: 192 * 1024 * 1024 });
|
|
58
|
+
if (audit.error || !audit.stdout) return void 0;
|
|
59
|
+
const res = spawnSync(process.execPath, args, { input: audit.stdout, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
60
|
+
if (res.error || res.status !== 0) return void 0;
|
|
61
|
+
return res.stdout.trim() || void 0;
|
|
62
|
+
}
|
|
51
63
|
|
|
52
64
|
// src/integrations/cypress-plugin.ts
|
|
53
65
|
function register(on) {
|
|
@@ -67,6 +79,7 @@ function register(on) {
|
|
|
67
79
|
}
|
|
68
80
|
}
|
|
69
81
|
const result = auditSnapshot(withShot);
|
|
82
|
+
if (payload.report) writePagesReport(typeof payload.report === "object" ? payload.report : {});
|
|
70
83
|
const failOn = payload.failOn === void 0 ? "blocking" : payload.failOn;
|
|
71
84
|
const failing = failOn === false ? [] : failingFindings(result, failOn);
|
|
72
85
|
return {
|
package/dist/cypress.d.ts
CHANGED
|
@@ -5,6 +5,17 @@ interface CypressCheckOptions extends CheckOptions {
|
|
|
5
5
|
/** Capture a viewport screenshot so the pixel tier can run. On by default — the plugin
|
|
6
6
|
* reads it back through Cypress's `after:screenshot` event. */
|
|
7
7
|
screenshot?: boolean;
|
|
8
|
+
/** Also write the per-page report once this page is recorded. Off by default: a report per
|
|
9
|
+
* checked page would be wasteful in a suite, so turn it on in a final test.
|
|
10
|
+
*
|
|
11
|
+
* Cypress test code runs in the browser and cannot write files, so the option is only
|
|
12
|
+
* DECLARED here and forwarded — the Node half does the work. Same option, same behaviour
|
|
13
|
+
* as Playwright's; a runner that silently ignored it would be worse than one that lacked it. */
|
|
14
|
+
report?: boolean | {
|
|
15
|
+
out?: string;
|
|
16
|
+
standard?: string;
|
|
17
|
+
lang?: string;
|
|
18
|
+
};
|
|
8
19
|
}
|
|
9
20
|
declare function registerUltra11yCommand(): void;
|
|
10
21
|
|
package/dist/cypress.mjs
CHANGED
|
@@ -154,6 +154,7 @@ function registerUltra11yCommand() {
|
|
|
154
154
|
const payload = {
|
|
155
155
|
...buildPayload(collected, url, "cypress", opts),
|
|
156
156
|
failOn: opts.failOn,
|
|
157
|
+
...opts.report ? { report: opts.report } : {},
|
|
157
158
|
...shotName ? { screenshotName: shotName } : {}
|
|
158
159
|
};
|
|
159
160
|
return cy.task("ultra11ySnapshot", payload, { log: false }).then((res) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "2.31.
|
|
3
|
+
"version": "2.31.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 join45, relative as relative5, sep as sep7, dirname as dirname1
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "2.31.
|
|
9
|
+
var VERSION = "2.31.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -55414,7 +55414,15 @@ function startDevServer(opts) {
|
|
|
55414
55414
|
}
|
|
55415
55415
|
if (url.pathname === "/health") {
|
|
55416
55416
|
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(
|
|
55417
|
-
JSON.stringify({
|
|
55417
|
+
JSON.stringify({
|
|
55418
|
+
ok: true,
|
|
55419
|
+
tool: "ultra11y",
|
|
55420
|
+
version: VERSION,
|
|
55421
|
+
port: opts.port,
|
|
55422
|
+
standard: opts.standard,
|
|
55423
|
+
lang: opts.lang,
|
|
55424
|
+
pages: readSnapshots(opts.root).length
|
|
55425
|
+
})
|
|
55418
55426
|
);
|
|
55419
55427
|
return;
|
|
55420
55428
|
}
|
|
@@ -56150,6 +56158,7 @@ function noop() {
|
|
|
56150
56158
|
}
|
|
56151
56159
|
|
|
56152
56160
|
// src/mcp/handlers.ts
|
|
56161
|
+
var CAPTURES_DIR = ".ultra11y/captures";
|
|
56153
56162
|
var ToolError = class extends Error {
|
|
56154
56163
|
};
|
|
56155
56164
|
var MAX_READ_LINES = 2e3;
|
|
@@ -56220,6 +56229,8 @@ async function dispatch(name2, args2, cwd) {
|
|
|
56220
56229
|
return handlePackCheck(args2, cwd);
|
|
56221
56230
|
case "ultra11y_sample_check":
|
|
56222
56231
|
return handleSampleCheck(cwd);
|
|
56232
|
+
case "ultra11y_pages":
|
|
56233
|
+
return handlePages(args2, cwd);
|
|
56223
56234
|
case "ultra11y_read":
|
|
56224
56235
|
return handleRead(args2, cwd);
|
|
56225
56236
|
case "ultra11y_fix":
|
|
@@ -56240,8 +56251,11 @@ function audit(args2, cwd) {
|
|
|
56240
56251
|
const warnings = [];
|
|
56241
56252
|
try {
|
|
56242
56253
|
process.chdir(cwd);
|
|
56243
|
-
|
|
56244
|
-
|
|
56254
|
+
const inputs = strArray2(args2.globs) ?? DEFAULT_GLOBS;
|
|
56255
|
+
const scopedToDiff = bool(args2.changed) || bool(args2.staged) || str2(args2.since) !== void 0;
|
|
56256
|
+
const extra = [CAPTURES_DIR, PAGES_DIR].filter((d) => !scopedToDiff && existsSync29(d) && !inputs.includes(d));
|
|
56257
|
+
const result = runAudit({
|
|
56258
|
+
inputs: [...inputs, ...extra],
|
|
56245
56259
|
include: strArray2(args2.include),
|
|
56246
56260
|
exclude: strArray2(args2.exclude),
|
|
56247
56261
|
forceJsx: bool(args2.jsx),
|
|
@@ -56252,6 +56266,14 @@ function audit(args2, cwd) {
|
|
|
56252
56266
|
maxFiles: positive(args2.max_files, "max_files"),
|
|
56253
56267
|
onWarn: (m) => warnings.push(m)
|
|
56254
56268
|
});
|
|
56269
|
+
if (extra.includes(PAGES_DIR)) {
|
|
56270
|
+
const scope = pageScopesFrom(readSnapshots("."));
|
|
56271
|
+
if (scope.length) {
|
|
56272
|
+
result.scope.pages = scope;
|
|
56273
|
+
attributePages(result, scope);
|
|
56274
|
+
}
|
|
56275
|
+
}
|
|
56276
|
+
return result;
|
|
56255
56277
|
} finally {
|
|
56256
56278
|
process.chdir(prev);
|
|
56257
56279
|
}
|
|
@@ -56337,6 +56359,30 @@ function handleSampleCheck(cwd) {
|
|
|
56337
56359
|
}
|
|
56338
56360
|
return { cwd, config: raw, next: "Check the sample covers every page kind the methodology requires, not just the convenient ones." };
|
|
56339
56361
|
}
|
|
56362
|
+
function handlePages(args2, cwd) {
|
|
56363
|
+
const standard = standardOf(args2);
|
|
56364
|
+
const lang = langOf(args2);
|
|
56365
|
+
const r = audit(args2, cwd);
|
|
56366
|
+
const scope = pagesOf(r);
|
|
56367
|
+
if (!scope.length) {
|
|
56368
|
+
throw new ToolError(
|
|
56369
|
+
"no page in scope: this audit covers no snapshot (.ultra11y/pages) and no scanned sample. Capture pages with the E2E plugins, `ultra11y dev`, or `scan` (which snapshots every page it visits), then re-run."
|
|
56370
|
+
);
|
|
56371
|
+
}
|
|
56372
|
+
attributePages(r, scope);
|
|
56373
|
+
const derived = derivePages(r, scope);
|
|
56374
|
+
const format = typeof args2.format === "string" ? args2.format : "grid";
|
|
56375
|
+
const markdown = format === "report" ? renderPagesDocument(r, derived, { standard, lang }) : renderPageGrid(r, scope, standard, lang);
|
|
56376
|
+
return {
|
|
56377
|
+
cwd,
|
|
56378
|
+
standard,
|
|
56379
|
+
lang,
|
|
56380
|
+
markdown,
|
|
56381
|
+
pages: derived.map((p) => ({ id: p.id, name: p.name, url: p.url, basis: p.basis, conformancePct: p.conformancePct, findings: p.findings.length })),
|
|
56382
|
+
unattributed: unattributedFindings(r).length,
|
|
56383
|
+
next: 'A page whose basis is "attributed" has no snapshot: absence of a finding there is NOT conformity. Its undecided criteria stay yours to adjudicate.'
|
|
56384
|
+
};
|
|
56385
|
+
}
|
|
56340
56386
|
function handleFix(args2, cwd) {
|
|
56341
56387
|
const prev = process.cwd();
|
|
56342
56388
|
try {
|
|
@@ -56667,6 +56713,26 @@ var TOOLS2 = [
|
|
|
56667
56713
|
description: "Validate a project's `.ultra11yrc.json` page sample against the methodology: a conformance claim covering only the pages that happened to be easy is not a conformance claim. Reads the config; changes nothing.",
|
|
56668
56714
|
inputSchema: { type: "object", properties: { cwd: cwdProp }, required: ["cwd"] }
|
|
56669
56715
|
},
|
|
56716
|
+
{
|
|
56717
|
+
name: "ultra11y_pages",
|
|
56718
|
+
title: "The per-page view of the audit",
|
|
56719
|
+
description: "RGAA \u2014 like every country standard \u2014 is a PER-PAGE norm, but the engine's verdict is scope-wide. This projects the audit onto the pages it covers: the criterion \xD7 page grid, or the per-page report (one dossier per page, every criterion of the standard with its status on THAT page). Two rules make it trustworthy: a finding belongs to a page only when something says so \u2014 never spread across pages \u2014 and a criterion is conforming by silence only on a page whose real rendered DOM was audited, and only for the criteria the engine can actually decide. Everything else stays \xAB to assess \xBB, which is what it is. Pages come from snapshots (.ultra11y/pages) or a scanned sample; with neither, there is nothing to project.",
|
|
56720
|
+
inputSchema: {
|
|
56721
|
+
type: "object",
|
|
56722
|
+
properties: {
|
|
56723
|
+
cwd: cwdProp,
|
|
56724
|
+
globs: globsProp,
|
|
56725
|
+
standard: standardProp,
|
|
56726
|
+
lang: langProp,
|
|
56727
|
+
format: {
|
|
56728
|
+
type: "string",
|
|
56729
|
+
enum: ["grid", "report"],
|
|
56730
|
+
description: "`grid` (default) is the criterion \xD7 page matrix; `report` is the per-page dossiers."
|
|
56731
|
+
}
|
|
56732
|
+
},
|
|
56733
|
+
required: ["cwd"]
|
|
56734
|
+
}
|
|
56735
|
+
},
|
|
56670
56736
|
{
|
|
56671
56737
|
name: "ultra11y_read",
|
|
56672
56738
|
title: "Read a file from the project",
|
|
@@ -56754,6 +56820,7 @@ var TOOL_META2 = {
|
|
|
56754
56820
|
ultra11y_adjudicate: { openWorld: false },
|
|
56755
56821
|
ultra11y_pack_check: { openWorld: false },
|
|
56756
56822
|
ultra11y_sample_check: { openWorld: false },
|
|
56823
|
+
ultra11y_pages: { openWorld: false },
|
|
56757
56824
|
ultra11y_read: { openWorld: false },
|
|
56758
56825
|
// Rewrites source files. Not destructive in the delete sense, and it verifies
|
|
56759
56826
|
// its own work — but it is the user's code.
|
|
@@ -59052,6 +59119,14 @@ async function cmdJudge(p) {
|
|
|
59052
59119
|
}
|
|
59053
59120
|
const max = typeof p.flags.max === "string" ? Number(p.flags.max) : void 0;
|
|
59054
59121
|
const truncated = max !== void 0 && Number.isFinite(max) && max > 0 && items.length > max;
|
|
59122
|
+
if (truncated && p.flags.apply === true) {
|
|
59123
|
+
console.error(
|
|
59124
|
+
lang === "fr" ? `ultra11y judge : --max ${max} ne couvre que ${max} des ${items.length} crit\xE8res, et --apply exige une adjudication COMPL\xC8TE (le gate refuse une couverture partielle).
|
|
59125
|
+
Retirez --max pour tout adjuger, ou retirez --apply pour produire la worklist et l'appliquer plus tard.` : `ultra11y judge: --max ${max} covers only ${max} of ${items.length} criteria, and --apply requires a COMPLETE adjudication (the gate refuses partial coverage).
|
|
59126
|
+
Drop --max to adjudicate everything, or drop --apply to produce the worklist and apply it later.`
|
|
59127
|
+
);
|
|
59128
|
+
return 2;
|
|
59129
|
+
}
|
|
59055
59130
|
if (truncated) items = items.slice(0, max);
|
|
59056
59131
|
const batches = [];
|
|
59057
59132
|
for (let i2 = 0; i2 < items.length; i2 += BATCH_SIZE) {
|
|
@@ -5,7 +5,7 @@ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should
|
|
|
5
5
|
allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
|
|
6
6
|
license: MIT
|
|
7
7
|
metadata:
|
|
8
|
-
version: 2.31.
|
|
8
|
+
version: 2.31.2
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -6,7 +6,7 @@ import { join as join45, relative as relative5, sep as sep7, dirname as dirname1
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "2.31.
|
|
9
|
+
var VERSION = "2.31.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -55414,7 +55414,15 @@ function startDevServer(opts) {
|
|
|
55414
55414
|
}
|
|
55415
55415
|
if (url.pathname === "/health") {
|
|
55416
55416
|
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(
|
|
55417
|
-
JSON.stringify({
|
|
55417
|
+
JSON.stringify({
|
|
55418
|
+
ok: true,
|
|
55419
|
+
tool: "ultra11y",
|
|
55420
|
+
version: VERSION,
|
|
55421
|
+
port: opts.port,
|
|
55422
|
+
standard: opts.standard,
|
|
55423
|
+
lang: opts.lang,
|
|
55424
|
+
pages: readSnapshots(opts.root).length
|
|
55425
|
+
})
|
|
55418
55426
|
);
|
|
55419
55427
|
return;
|
|
55420
55428
|
}
|
|
@@ -56150,6 +56158,7 @@ function noop() {
|
|
|
56150
56158
|
}
|
|
56151
56159
|
|
|
56152
56160
|
// src/mcp/handlers.ts
|
|
56161
|
+
var CAPTURES_DIR = ".ultra11y/captures";
|
|
56153
56162
|
var ToolError = class extends Error {
|
|
56154
56163
|
};
|
|
56155
56164
|
var MAX_READ_LINES = 2e3;
|
|
@@ -56220,6 +56229,8 @@ async function dispatch(name2, args2, cwd) {
|
|
|
56220
56229
|
return handlePackCheck(args2, cwd);
|
|
56221
56230
|
case "ultra11y_sample_check":
|
|
56222
56231
|
return handleSampleCheck(cwd);
|
|
56232
|
+
case "ultra11y_pages":
|
|
56233
|
+
return handlePages(args2, cwd);
|
|
56223
56234
|
case "ultra11y_read":
|
|
56224
56235
|
return handleRead(args2, cwd);
|
|
56225
56236
|
case "ultra11y_fix":
|
|
@@ -56240,8 +56251,11 @@ function audit(args2, cwd) {
|
|
|
56240
56251
|
const warnings = [];
|
|
56241
56252
|
try {
|
|
56242
56253
|
process.chdir(cwd);
|
|
56243
|
-
|
|
56244
|
-
|
|
56254
|
+
const inputs = strArray2(args2.globs) ?? DEFAULT_GLOBS;
|
|
56255
|
+
const scopedToDiff = bool(args2.changed) || bool(args2.staged) || str2(args2.since) !== void 0;
|
|
56256
|
+
const extra = [CAPTURES_DIR, PAGES_DIR].filter((d) => !scopedToDiff && existsSync29(d) && !inputs.includes(d));
|
|
56257
|
+
const result = runAudit({
|
|
56258
|
+
inputs: [...inputs, ...extra],
|
|
56245
56259
|
include: strArray2(args2.include),
|
|
56246
56260
|
exclude: strArray2(args2.exclude),
|
|
56247
56261
|
forceJsx: bool(args2.jsx),
|
|
@@ -56252,6 +56266,14 @@ function audit(args2, cwd) {
|
|
|
56252
56266
|
maxFiles: positive(args2.max_files, "max_files"),
|
|
56253
56267
|
onWarn: (m) => warnings.push(m)
|
|
56254
56268
|
});
|
|
56269
|
+
if (extra.includes(PAGES_DIR)) {
|
|
56270
|
+
const scope = pageScopesFrom(readSnapshots("."));
|
|
56271
|
+
if (scope.length) {
|
|
56272
|
+
result.scope.pages = scope;
|
|
56273
|
+
attributePages(result, scope);
|
|
56274
|
+
}
|
|
56275
|
+
}
|
|
56276
|
+
return result;
|
|
56255
56277
|
} finally {
|
|
56256
56278
|
process.chdir(prev);
|
|
56257
56279
|
}
|
|
@@ -56337,6 +56359,30 @@ function handleSampleCheck(cwd) {
|
|
|
56337
56359
|
}
|
|
56338
56360
|
return { cwd, config: raw, next: "Check the sample covers every page kind the methodology requires, not just the convenient ones." };
|
|
56339
56361
|
}
|
|
56362
|
+
function handlePages(args2, cwd) {
|
|
56363
|
+
const standard = standardOf(args2);
|
|
56364
|
+
const lang = langOf(args2);
|
|
56365
|
+
const r = audit(args2, cwd);
|
|
56366
|
+
const scope = pagesOf(r);
|
|
56367
|
+
if (!scope.length) {
|
|
56368
|
+
throw new ToolError(
|
|
56369
|
+
"no page in scope: this audit covers no snapshot (.ultra11y/pages) and no scanned sample. Capture pages with the E2E plugins, `ultra11y dev`, or `scan` (which snapshots every page it visits), then re-run."
|
|
56370
|
+
);
|
|
56371
|
+
}
|
|
56372
|
+
attributePages(r, scope);
|
|
56373
|
+
const derived = derivePages(r, scope);
|
|
56374
|
+
const format = typeof args2.format === "string" ? args2.format : "grid";
|
|
56375
|
+
const markdown = format === "report" ? renderPagesDocument(r, derived, { standard, lang }) : renderPageGrid(r, scope, standard, lang);
|
|
56376
|
+
return {
|
|
56377
|
+
cwd,
|
|
56378
|
+
standard,
|
|
56379
|
+
lang,
|
|
56380
|
+
markdown,
|
|
56381
|
+
pages: derived.map((p) => ({ id: p.id, name: p.name, url: p.url, basis: p.basis, conformancePct: p.conformancePct, findings: p.findings.length })),
|
|
56382
|
+
unattributed: unattributedFindings(r).length,
|
|
56383
|
+
next: 'A page whose basis is "attributed" has no snapshot: absence of a finding there is NOT conformity. Its undecided criteria stay yours to adjudicate.'
|
|
56384
|
+
};
|
|
56385
|
+
}
|
|
56340
56386
|
function handleFix(args2, cwd) {
|
|
56341
56387
|
const prev = process.cwd();
|
|
56342
56388
|
try {
|
|
@@ -56667,6 +56713,26 @@ var TOOLS2 = [
|
|
|
56667
56713
|
description: "Validate a project's `.ultra11yrc.json` page sample against the methodology: a conformance claim covering only the pages that happened to be easy is not a conformance claim. Reads the config; changes nothing.",
|
|
56668
56714
|
inputSchema: { type: "object", properties: { cwd: cwdProp }, required: ["cwd"] }
|
|
56669
56715
|
},
|
|
56716
|
+
{
|
|
56717
|
+
name: "ultra11y_pages",
|
|
56718
|
+
title: "The per-page view of the audit",
|
|
56719
|
+
description: "RGAA \u2014 like every country standard \u2014 is a PER-PAGE norm, but the engine's verdict is scope-wide. This projects the audit onto the pages it covers: the criterion \xD7 page grid, or the per-page report (one dossier per page, every criterion of the standard with its status on THAT page). Two rules make it trustworthy: a finding belongs to a page only when something says so \u2014 never spread across pages \u2014 and a criterion is conforming by silence only on a page whose real rendered DOM was audited, and only for the criteria the engine can actually decide. Everything else stays \xAB to assess \xBB, which is what it is. Pages come from snapshots (.ultra11y/pages) or a scanned sample; with neither, there is nothing to project.",
|
|
56720
|
+
inputSchema: {
|
|
56721
|
+
type: "object",
|
|
56722
|
+
properties: {
|
|
56723
|
+
cwd: cwdProp,
|
|
56724
|
+
globs: globsProp,
|
|
56725
|
+
standard: standardProp,
|
|
56726
|
+
lang: langProp,
|
|
56727
|
+
format: {
|
|
56728
|
+
type: "string",
|
|
56729
|
+
enum: ["grid", "report"],
|
|
56730
|
+
description: "`grid` (default) is the criterion \xD7 page matrix; `report` is the per-page dossiers."
|
|
56731
|
+
}
|
|
56732
|
+
},
|
|
56733
|
+
required: ["cwd"]
|
|
56734
|
+
}
|
|
56735
|
+
},
|
|
56670
56736
|
{
|
|
56671
56737
|
name: "ultra11y_read",
|
|
56672
56738
|
title: "Read a file from the project",
|
|
@@ -56754,6 +56820,7 @@ var TOOL_META2 = {
|
|
|
56754
56820
|
ultra11y_adjudicate: { openWorld: false },
|
|
56755
56821
|
ultra11y_pack_check: { openWorld: false },
|
|
56756
56822
|
ultra11y_sample_check: { openWorld: false },
|
|
56823
|
+
ultra11y_pages: { openWorld: false },
|
|
56757
56824
|
ultra11y_read: { openWorld: false },
|
|
56758
56825
|
// Rewrites source files. Not destructive in the delete sense, and it verifies
|
|
56759
56826
|
// its own work — but it is the user's code.
|
|
@@ -59052,6 +59119,14 @@ async function cmdJudge(p) {
|
|
|
59052
59119
|
}
|
|
59053
59120
|
const max = typeof p.flags.max === "string" ? Number(p.flags.max) : void 0;
|
|
59054
59121
|
const truncated = max !== void 0 && Number.isFinite(max) && max > 0 && items.length > max;
|
|
59122
|
+
if (truncated && p.flags.apply === true) {
|
|
59123
|
+
console.error(
|
|
59124
|
+
lang === "fr" ? `ultra11y judge : --max ${max} ne couvre que ${max} des ${items.length} crit\xE8res, et --apply exige une adjudication COMPL\xC8TE (le gate refuse une couverture partielle).
|
|
59125
|
+
Retirez --max pour tout adjuger, ou retirez --apply pour produire la worklist et l'appliquer plus tard.` : `ultra11y judge: --max ${max} covers only ${max} of ${items.length} criteria, and --apply requires a COMPLETE adjudication (the gate refuses partial coverage).
|
|
59126
|
+
Drop --max to adjudicate everything, or drop --apply to produce the worklist and apply it later.`
|
|
59127
|
+
);
|
|
59128
|
+
return 2;
|
|
59129
|
+
}
|
|
59055
59130
|
if (truncated) items = items.slice(0, max);
|
|
59056
59131
|
const batches = [];
|
|
59057
59132
|
for (let i2 = 0; i2 < items.length; i2 += BATCH_SIZE) {
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibili
|
|
|
4
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 remediation backlog or GitHub issues; 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."
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: 2.31.
|
|
7
|
+
version: 2.31.2
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -189,6 +189,29 @@ Domain knowledge first, then the tooling. Read the one that matches the question
|
|
|
189
189
|
something SAYS so (else it is reported as unattributed, never spread), and `C` by silence is
|
|
190
190
|
earned only by a page whose real DOM was audited — a source-only page keeps its undecided
|
|
191
191
|
criteria « à évaluer »; read **`references/pages.md`**.
|
|
192
|
+
- **"I want a report PER PAGE, not a matrix"** → `pages --in audits/audit-latest.json
|
|
193
|
+
--standard rgaa --format report --split page --out audits/pages`: one dossier per page —
|
|
194
|
+
identity, its screenshot, its rate, EVERY criterion of the standard with its status on that
|
|
195
|
+
page, then each non-conformity as the ordinary auditor block. Nothing is re-decided (the
|
|
196
|
+
same projection as the grid) and no format is invented (the same auditor block as the
|
|
197
|
+
report), so a page sheet and the compliance report cannot disagree. `check` gates it against
|
|
198
|
+
invented criteria like any other report; read **`references/pages.md`**.
|
|
199
|
+
- **"Which pages should I even audit?"** → `pages discover --crawl http://localhost:3000
|
|
200
|
+
--write` (or `--sitemap <url>`) writes the `sample.pages` block for you, taking each page's
|
|
201
|
+
name from its served `<title>` and NEVER overwriting the auth/storageState/notes already
|
|
202
|
+
declared. Then `sample check` lints the coverage and `scan --sample` scans and snapshots it.
|
|
203
|
+
A client-rendered SPA exposes no routes in its served HTML — use a sitemap there; read
|
|
204
|
+
**`references/dynamic.md`**.
|
|
205
|
+
- **"Audit these URLs page by page, in CI"** → the shipped Action takes `urls`, `sitemap`,
|
|
206
|
+
`crawl` or `sample`, snapshots every page it visits, writes the per-page dossiers into its
|
|
207
|
+
artifact and adds a per-page scoreboard to the job summary and the PR comment; read
|
|
208
|
+
**`references/ci.md`**.
|
|
209
|
+
- **"Nobody is here to rule on the judgment criteria"** → `judge --in audits/audit-latest.json
|
|
210
|
+
--standard rgaa [--apply]` adjudicates them with a model, for a run with no coding agent in
|
|
211
|
+
the loop (CI, the extension, an E2E run). It is a CALLER, not a second judge: same worklist,
|
|
212
|
+
same evidence, same prompt, and the verdicts pass the same fail-closed gate yours do. The
|
|
213
|
+
only command that takes an API key — inside an agent, use `verify --manual` instead and rule
|
|
214
|
+
yourself; read **`references/judgment.md`**.
|
|
192
215
|
- **"Show the findings ON the pull request, not just a red job"** → `--format sarif` (upload
|
|
193
216
|
to code scanning → inline annotations at the right file:line) or `--format github`
|
|
194
217
|
(`::error::` workflow commands + a `$GITHUB_STEP_SUMMARY` table) — from `report
|
|
@@ -196,3 +196,37 @@ Two surfaces come out of it:
|
|
|
196
196
|
standard with its status on that page, and each non-conformity as the ordinary auditor block.
|
|
197
197
|
|
|
198
198
|
A run with no page in scope is not a failure: the report step says so and the job carries on.
|
|
199
|
+
|
|
200
|
+
## The action is executed in ITS OWN CI, not just parsed
|
|
201
|
+
|
|
202
|
+
`tests/action.test.ts` reads `action.yml` and gates its shape. That cannot prove the thing
|
|
203
|
+
works: a composite action is bash — arrays, `set -e`, quoting — and the whole file could stay
|
|
204
|
+
green while the action was broken for every consumer.
|
|
205
|
+
|
|
206
|
+
So `ci.yml` has an `action` job that `uses: ./` for real, twice: report-only over a
|
|
207
|
+
non-conforming fixture (asserting the declared outputs, the written report, and that the
|
|
208
|
+
per-page step DEGRADES rather than failing when nothing was scanned), then the page-by-page
|
|
209
|
+
path — a served two-page site, crawled, scanned, snapshotted, folded in and rendered as
|
|
210
|
+
per-page dossiers, asserting that each crawled page is snapshot-based and that the finding on
|
|
211
|
+
one page did not leak onto the clean one. Both run with `sarif: false`, so the job needs no
|
|
212
|
+
`security-events: write` and works on a fork's pull request.
|
|
213
|
+
|
|
214
|
+
Two traps are worth naming, because running it is what found them.
|
|
215
|
+
|
|
216
|
+
`[ cond ] && cmd` returns 1 when the condition is false, and `-e` turns that into a dead job.
|
|
217
|
+
A test now forbids the form in `action.yml` **and** in every CI workflow.
|
|
218
|
+
|
|
219
|
+
And **artifact names are unique per workflow RUN, not per step**. Using the action twice in
|
|
220
|
+
one job — the code diff, then the served pages — died on a `409 Conflict`, with the report
|
|
221
|
+
written and never uploaded. Pass `artifact-name` on each invocation:
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
- uses: maxgfr/ultra11y@v2
|
|
225
|
+
with: { since: auto, artifact-name: a11y-code }
|
|
226
|
+
- uses: maxgfr/ultra11y@v2
|
|
227
|
+
with: { crawl: http://localhost:3000, artifact-name: a11y-pages }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Left unset it keeps the historical `ultra11y-<standard>`, so a single-invocation workflow is
|
|
231
|
+
unaffected.
|
|
232
|
+
|
|
@@ -114,10 +114,13 @@ between the two.
|
|
|
114
114
|
|
|
115
115
|
## A report straight out of the test run
|
|
116
116
|
|
|
117
|
-
`checkA11y(page, { report: true })` (
|
|
118
|
-
recorded — `audits/pages/index.md` plus one sheet per page, screenshots included. Off by
|
|
117
|
+
`checkA11y(page, { report: true })` — and `cy.ultra11y({ report: true })` — writes the per-page
|
|
118
|
+
report once the page is recorded — `audits/pages/index.md` plus one sheet per page, screenshots included. Off by
|
|
119
119
|
default: a report per checked page would be wasteful in a suite, so turn it on in a final
|
|
120
|
-
test, or run `pages --format report` yourself afterwards.
|
|
120
|
+
test, or run `pages --format report` yourself afterwards. Cypress test code cannot write
|
|
121
|
+
files, so there the option is declared in the browser and honoured by the Node plugin — same
|
|
122
|
+
option, same behaviour, because a runner that silently ignored it would be worse than one
|
|
123
|
+
that lacked it.
|
|
121
124
|
|
|
122
125
|
## Why the artefact matters more than the assertion
|
|
123
126
|
|
|
@@ -18,7 +18,7 @@ node scripts/ultra11y.mjs dev --standard rgaa # the side-car (loopback only
|
|
|
18
18
|
|---|---|
|
|
19
19
|
| **Audit this page** | collects the page with the engine's own collector, posts it to the side-car, which persists it as a snapshot and audits it — the findings come back and are listed |
|
|
20
20
|
| **Adjudicate with AI** | asks the side-car to rule on the judgment criteria (see `judgment.md`) |
|
|
21
|
-
| **Page-by-page grid** | opens the dashboard, where every page you audited accumulates |
|
|
21
|
+
| **Page-by-page grid** | opens the dashboard, where every page you audited accumulates — on the port the server actually answered on, not a default |
|
|
22
22
|
|
|
23
23
|
Because the page is persisted as an ordinary snapshot, everything else already works on it:
|
|
24
24
|
`ultra11y audit` re-audits it offline, `pages --format report` writes its dossier, and it
|
|
@@ -149,5 +149,10 @@ gate refuses:
|
|
|
149
149
|
A rejected adjudication leaves `audit-latest.json` untouched.
|
|
150
150
|
|
|
151
151
|
**Strictly opt-in.** With no key the command explains itself and exits 2; nothing else in the
|
|
152
|
-
engine changes.
|
|
153
|
-
|
|
152
|
+
engine changes.
|
|
153
|
+
|
|
154
|
+
`--max` bounds the spend and says out loud which criteria it did not submit. It is **refused
|
|
155
|
+
together with `--apply`**, before any call is made: the gate rejects an incomplete
|
|
156
|
+
adjudication by design, so the pair could only ever bill you for a guaranteed failure. Drop
|
|
157
|
+
`--max` to adjudicate everything, or drop `--apply` to produce the worklist now and fold it
|
|
158
|
+
in later.
|
|
@@ -6,7 +6,7 @@ import { join as join45, relative as relative5, sep as sep7, dirname as dirname1
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "2.31.
|
|
9
|
+
var VERSION = "2.31.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -55414,7 +55414,15 @@ function startDevServer(opts) {
|
|
|
55414
55414
|
}
|
|
55415
55415
|
if (url.pathname === "/health") {
|
|
55416
55416
|
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(
|
|
55417
|
-
JSON.stringify({
|
|
55417
|
+
JSON.stringify({
|
|
55418
|
+
ok: true,
|
|
55419
|
+
tool: "ultra11y",
|
|
55420
|
+
version: VERSION,
|
|
55421
|
+
port: opts.port,
|
|
55422
|
+
standard: opts.standard,
|
|
55423
|
+
lang: opts.lang,
|
|
55424
|
+
pages: readSnapshots(opts.root).length
|
|
55425
|
+
})
|
|
55418
55426
|
);
|
|
55419
55427
|
return;
|
|
55420
55428
|
}
|
|
@@ -56150,6 +56158,7 @@ function noop() {
|
|
|
56150
56158
|
}
|
|
56151
56159
|
|
|
56152
56160
|
// src/mcp/handlers.ts
|
|
56161
|
+
var CAPTURES_DIR = ".ultra11y/captures";
|
|
56153
56162
|
var ToolError = class extends Error {
|
|
56154
56163
|
};
|
|
56155
56164
|
var MAX_READ_LINES = 2e3;
|
|
@@ -56220,6 +56229,8 @@ async function dispatch(name2, args2, cwd) {
|
|
|
56220
56229
|
return handlePackCheck(args2, cwd);
|
|
56221
56230
|
case "ultra11y_sample_check":
|
|
56222
56231
|
return handleSampleCheck(cwd);
|
|
56232
|
+
case "ultra11y_pages":
|
|
56233
|
+
return handlePages(args2, cwd);
|
|
56223
56234
|
case "ultra11y_read":
|
|
56224
56235
|
return handleRead(args2, cwd);
|
|
56225
56236
|
case "ultra11y_fix":
|
|
@@ -56240,8 +56251,11 @@ function audit(args2, cwd) {
|
|
|
56240
56251
|
const warnings = [];
|
|
56241
56252
|
try {
|
|
56242
56253
|
process.chdir(cwd);
|
|
56243
|
-
|
|
56244
|
-
|
|
56254
|
+
const inputs = strArray2(args2.globs) ?? DEFAULT_GLOBS;
|
|
56255
|
+
const scopedToDiff = bool(args2.changed) || bool(args2.staged) || str2(args2.since) !== void 0;
|
|
56256
|
+
const extra = [CAPTURES_DIR, PAGES_DIR].filter((d) => !scopedToDiff && existsSync29(d) && !inputs.includes(d));
|
|
56257
|
+
const result = runAudit({
|
|
56258
|
+
inputs: [...inputs, ...extra],
|
|
56245
56259
|
include: strArray2(args2.include),
|
|
56246
56260
|
exclude: strArray2(args2.exclude),
|
|
56247
56261
|
forceJsx: bool(args2.jsx),
|
|
@@ -56252,6 +56266,14 @@ function audit(args2, cwd) {
|
|
|
56252
56266
|
maxFiles: positive(args2.max_files, "max_files"),
|
|
56253
56267
|
onWarn: (m) => warnings.push(m)
|
|
56254
56268
|
});
|
|
56269
|
+
if (extra.includes(PAGES_DIR)) {
|
|
56270
|
+
const scope = pageScopesFrom(readSnapshots("."));
|
|
56271
|
+
if (scope.length) {
|
|
56272
|
+
result.scope.pages = scope;
|
|
56273
|
+
attributePages(result, scope);
|
|
56274
|
+
}
|
|
56275
|
+
}
|
|
56276
|
+
return result;
|
|
56255
56277
|
} finally {
|
|
56256
56278
|
process.chdir(prev);
|
|
56257
56279
|
}
|
|
@@ -56337,6 +56359,30 @@ function handleSampleCheck(cwd) {
|
|
|
56337
56359
|
}
|
|
56338
56360
|
return { cwd, config: raw, next: "Check the sample covers every page kind the methodology requires, not just the convenient ones." };
|
|
56339
56361
|
}
|
|
56362
|
+
function handlePages(args2, cwd) {
|
|
56363
|
+
const standard = standardOf(args2);
|
|
56364
|
+
const lang = langOf(args2);
|
|
56365
|
+
const r = audit(args2, cwd);
|
|
56366
|
+
const scope = pagesOf(r);
|
|
56367
|
+
if (!scope.length) {
|
|
56368
|
+
throw new ToolError(
|
|
56369
|
+
"no page in scope: this audit covers no snapshot (.ultra11y/pages) and no scanned sample. Capture pages with the E2E plugins, `ultra11y dev`, or `scan` (which snapshots every page it visits), then re-run."
|
|
56370
|
+
);
|
|
56371
|
+
}
|
|
56372
|
+
attributePages(r, scope);
|
|
56373
|
+
const derived = derivePages(r, scope);
|
|
56374
|
+
const format = typeof args2.format === "string" ? args2.format : "grid";
|
|
56375
|
+
const markdown = format === "report" ? renderPagesDocument(r, derived, { standard, lang }) : renderPageGrid(r, scope, standard, lang);
|
|
56376
|
+
return {
|
|
56377
|
+
cwd,
|
|
56378
|
+
standard,
|
|
56379
|
+
lang,
|
|
56380
|
+
markdown,
|
|
56381
|
+
pages: derived.map((p) => ({ id: p.id, name: p.name, url: p.url, basis: p.basis, conformancePct: p.conformancePct, findings: p.findings.length })),
|
|
56382
|
+
unattributed: unattributedFindings(r).length,
|
|
56383
|
+
next: 'A page whose basis is "attributed" has no snapshot: absence of a finding there is NOT conformity. Its undecided criteria stay yours to adjudicate.'
|
|
56384
|
+
};
|
|
56385
|
+
}
|
|
56340
56386
|
function handleFix(args2, cwd) {
|
|
56341
56387
|
const prev = process.cwd();
|
|
56342
56388
|
try {
|
|
@@ -56667,6 +56713,26 @@ var TOOLS2 = [
|
|
|
56667
56713
|
description: "Validate a project's `.ultra11yrc.json` page sample against the methodology: a conformance claim covering only the pages that happened to be easy is not a conformance claim. Reads the config; changes nothing.",
|
|
56668
56714
|
inputSchema: { type: "object", properties: { cwd: cwdProp }, required: ["cwd"] }
|
|
56669
56715
|
},
|
|
56716
|
+
{
|
|
56717
|
+
name: "ultra11y_pages",
|
|
56718
|
+
title: "The per-page view of the audit",
|
|
56719
|
+
description: "RGAA \u2014 like every country standard \u2014 is a PER-PAGE norm, but the engine's verdict is scope-wide. This projects the audit onto the pages it covers: the criterion \xD7 page grid, or the per-page report (one dossier per page, every criterion of the standard with its status on THAT page). Two rules make it trustworthy: a finding belongs to a page only when something says so \u2014 never spread across pages \u2014 and a criterion is conforming by silence only on a page whose real rendered DOM was audited, and only for the criteria the engine can actually decide. Everything else stays \xAB to assess \xBB, which is what it is. Pages come from snapshots (.ultra11y/pages) or a scanned sample; with neither, there is nothing to project.",
|
|
56720
|
+
inputSchema: {
|
|
56721
|
+
type: "object",
|
|
56722
|
+
properties: {
|
|
56723
|
+
cwd: cwdProp,
|
|
56724
|
+
globs: globsProp,
|
|
56725
|
+
standard: standardProp,
|
|
56726
|
+
lang: langProp,
|
|
56727
|
+
format: {
|
|
56728
|
+
type: "string",
|
|
56729
|
+
enum: ["grid", "report"],
|
|
56730
|
+
description: "`grid` (default) is the criterion \xD7 page matrix; `report` is the per-page dossiers."
|
|
56731
|
+
}
|
|
56732
|
+
},
|
|
56733
|
+
required: ["cwd"]
|
|
56734
|
+
}
|
|
56735
|
+
},
|
|
56670
56736
|
{
|
|
56671
56737
|
name: "ultra11y_read",
|
|
56672
56738
|
title: "Read a file from the project",
|
|
@@ -56754,6 +56820,7 @@ var TOOL_META2 = {
|
|
|
56754
56820
|
ultra11y_adjudicate: { openWorld: false },
|
|
56755
56821
|
ultra11y_pack_check: { openWorld: false },
|
|
56756
56822
|
ultra11y_sample_check: { openWorld: false },
|
|
56823
|
+
ultra11y_pages: { openWorld: false },
|
|
56757
56824
|
ultra11y_read: { openWorld: false },
|
|
56758
56825
|
// Rewrites source files. Not destructive in the delete sense, and it verifies
|
|
56759
56826
|
// its own work — but it is the user's code.
|
|
@@ -59052,6 +59119,14 @@ async function cmdJudge(p) {
|
|
|
59052
59119
|
}
|
|
59053
59120
|
const max = typeof p.flags.max === "string" ? Number(p.flags.max) : void 0;
|
|
59054
59121
|
const truncated = max !== void 0 && Number.isFinite(max) && max > 0 && items.length > max;
|
|
59122
|
+
if (truncated && p.flags.apply === true) {
|
|
59123
|
+
console.error(
|
|
59124
|
+
lang === "fr" ? `ultra11y judge : --max ${max} ne couvre que ${max} des ${items.length} crit\xE8res, et --apply exige une adjudication COMPL\xC8TE (le gate refuse une couverture partielle).
|
|
59125
|
+
Retirez --max pour tout adjuger, ou retirez --apply pour produire la worklist et l'appliquer plus tard.` : `ultra11y judge: --max ${max} covers only ${max} of ${items.length} criteria, and --apply requires a COMPLETE adjudication (the gate refuses partial coverage).
|
|
59126
|
+
Drop --max to adjudicate everything, or drop --apply to produce the worklist and apply it later.`
|
|
59127
|
+
);
|
|
59128
|
+
return 2;
|
|
59129
|
+
}
|
|
59055
59130
|
if (truncated) items = items.slice(0, max);
|
|
59056
59131
|
const batches = [];
|
|
59057
59132
|
for (let i2 = 0; i2 < items.length; i2 += BATCH_SIZE) {
|
package/src/cli.ts
CHANGED
|
@@ -2124,6 +2124,19 @@ async function cmdJudge(p: ParsedArgs): Promise<number> {
|
|
|
2124
2124
|
}
|
|
2125
2125
|
const max = typeof p.flags.max === "string" ? Number(p.flags.max) : undefined;
|
|
2126
2126
|
const truncated = max !== undefined && Number.isFinite(max) && max > 0 && items.length > max;
|
|
2127
|
+
// `--max` bounds the spend; `--apply` folds the verdicts through a gate that REFUSES an
|
|
2128
|
+
// incomplete adjudication. Together they can never succeed — so say so before spending
|
|
2129
|
+
// anything, instead of billing a full run and failing on coverage at the end.
|
|
2130
|
+
if (truncated && p.flags.apply === true) {
|
|
2131
|
+
console.error(
|
|
2132
|
+
lang === "fr"
|
|
2133
|
+
? `ultra11y judge : --max ${max} ne couvre que ${max} des ${items.length} critères, et --apply exige une adjudication COMPLÈTE (le gate refuse une couverture partielle).\n` +
|
|
2134
|
+
" Retirez --max pour tout adjuger, ou retirez --apply pour produire la worklist et l'appliquer plus tard."
|
|
2135
|
+
: `ultra11y judge: --max ${max} covers only ${max} of ${items.length} criteria, and --apply requires a COMPLETE adjudication (the gate refuses partial coverage).\n` +
|
|
2136
|
+
" Drop --max to adjudicate everything, or drop --apply to produce the worklist and apply it later.",
|
|
2137
|
+
);
|
|
2138
|
+
return 2;
|
|
2139
|
+
}
|
|
2127
2140
|
if (truncated) items = items.slice(0, max);
|
|
2128
2141
|
|
|
2129
2142
|
// Batch, and render EACH batch through the very worklist formatter the agent reads. There
|
package/src/dev.ts
CHANGED
|
@@ -424,11 +424,17 @@ export function startDevServer(opts: DevOptions): Promise<DevServer> {
|
|
|
424
424
|
}
|
|
425
425
|
// A liveness probe, so a client can say "run `ultra11y dev`" instead of failing silently.
|
|
426
426
|
if (url.pathname === "/health") {
|
|
427
|
-
res
|
|
428
|
-
.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
427
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(
|
|
428
|
+
JSON.stringify({
|
|
429
|
+
ok: true,
|
|
430
|
+
tool: "ultra11y",
|
|
431
|
+
version: VERSION,
|
|
432
|
+
port: opts.port,
|
|
433
|
+
standard: opts.standard,
|
|
434
|
+
lang: opts.lang,
|
|
435
|
+
pages: readSnapshots(opts.root).length,
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
432
438
|
return;
|
|
433
439
|
}
|
|
434
440
|
if (url.pathname === "/snapshot" && req.method === "POST") {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// If that event never fires, the page is recorded without a screenshot — the criterion stays
|
|
16
16
|
// undecided, exactly as before, and is never guessed.
|
|
17
17
|
import { readFileSync } from "node:fs";
|
|
18
|
-
import { type AuditLike, type FindingLike, type SnapshotPayload, auditSnapshot, failingFindings, formatFailure } from "./core.js";
|
|
18
|
+
import { type AuditLike, type FindingLike, type SnapshotPayload, auditSnapshot, failingFindings, formatFailure, writePagesReport } from "./core.js";
|
|
19
19
|
|
|
20
20
|
interface ScreenshotDetails {
|
|
21
21
|
name?: string;
|
|
@@ -29,6 +29,8 @@ export interface TaskPayload extends SnapshotPayload {
|
|
|
29
29
|
failOn?: string | false;
|
|
30
30
|
/** The name `cy.screenshot()` was called with, when the browser half took one. */
|
|
31
31
|
screenshotName?: string;
|
|
32
|
+
/** Forwarded from `cy.ultra11y({ report })` — the browser cannot write files, this side can. */
|
|
33
|
+
report?: boolean | { out?: string; standard?: string; lang?: string };
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export interface TaskResult {
|
|
@@ -61,6 +63,7 @@ export default function register(on: On): void {
|
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
const result: AuditLike = auditSnapshot(withShot);
|
|
66
|
+
if (payload.report) writePagesReport(typeof payload.report === "object" ? payload.report : {});
|
|
64
67
|
const failOn = payload.failOn === undefined ? "blocking" : payload.failOn;
|
|
65
68
|
const failing = failOn === false ? [] : failingFindings(result, failOn);
|
|
66
69
|
// Return rather than throw: the browser half raises the assertion, so the failure is
|
|
@@ -24,6 +24,13 @@ export interface CypressCheckOptions extends CheckOptions {
|
|
|
24
24
|
/** Capture a viewport screenshot so the pixel tier can run. On by default — the plugin
|
|
25
25
|
* reads it back through Cypress's `after:screenshot` event. */
|
|
26
26
|
screenshot?: boolean;
|
|
27
|
+
/** Also write the per-page report once this page is recorded. Off by default: a report per
|
|
28
|
+
* checked page would be wasteful in a suite, so turn it on in a final test.
|
|
29
|
+
*
|
|
30
|
+
* Cypress test code runs in the browser and cannot write files, so the option is only
|
|
31
|
+
* DECLARED here and forwarded — the Node half does the work. Same option, same behaviour
|
|
32
|
+
* as Playwright's; a runner that silently ignored it would be worse than one that lacked it. */
|
|
33
|
+
report?: boolean | { out?: string; standard?: string; lang?: string };
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export function registerUltra11yCommand(): void {
|
|
@@ -41,6 +48,7 @@ export function registerUltra11yCommand(): void {
|
|
|
41
48
|
const payload = {
|
|
42
49
|
...buildPayload(collected, url, "cypress", opts),
|
|
43
50
|
failOn: opts.failOn,
|
|
51
|
+
...(opts.report ? { report: opts.report } : {}),
|
|
44
52
|
...(shotName ? { screenshotName: shotName } : {}),
|
|
45
53
|
};
|
|
46
54
|
return cy.task("ultra11ySnapshot", payload, { log: false }).then((res: { failing?: unknown[]; message?: string }) => {
|
package/src/mcp/handlers.ts
CHANGED
|
@@ -12,6 +12,13 @@ import { renderReport, renderPackReport } from "../report.js";
|
|
|
12
12
|
import { buildWorklist, formatWorklist } from "../verify.js";
|
|
13
13
|
import { allSC, getSC } from "../wcag.js";
|
|
14
14
|
import { loadPack } from "../standards/index.js";
|
|
15
|
+
import { attributePages, derivePages, pageScopesFrom, pagesOf, renderPageGrid, unattributedFindings } from "../pages.js";
|
|
16
|
+
import { PAGES_DIR, readSnapshots } from "../snapshot.js";
|
|
17
|
+
|
|
18
|
+
// The CLI's default capture directory (`--captures` overrides it there; the MCP surface has
|
|
19
|
+
// no such flag, so the default is the contract).
|
|
20
|
+
const CAPTURES_DIR = ".ultra11y/captures";
|
|
21
|
+
import { renderPagesDocument } from "../pages-report.js";
|
|
15
22
|
import { withProjectLock } from "../project-lock.js";
|
|
16
23
|
import type { AuditResult, Lang } from "../types.js";
|
|
17
24
|
import type { StandardId } from "../standards/index.js";
|
|
@@ -132,6 +139,8 @@ async function dispatch(name: string, args: Record<string, unknown>, cwd: string
|
|
|
132
139
|
return handlePackCheck(args, cwd);
|
|
133
140
|
case "ultra11y_sample_check":
|
|
134
141
|
return handleSampleCheck(cwd);
|
|
142
|
+
case "ultra11y_pages":
|
|
143
|
+
return handlePages(args, cwd);
|
|
135
144
|
case "ultra11y_read":
|
|
136
145
|
return handleRead(args, cwd);
|
|
137
146
|
case "ultra11y_fix":
|
|
@@ -161,8 +170,18 @@ function audit(args: Record<string, unknown>, cwd: string): AuditResult {
|
|
|
161
170
|
const warnings: string[] = [];
|
|
162
171
|
try {
|
|
163
172
|
process.chdir(cwd);
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
const inputs = strArray(args.globs) ?? DEFAULT_GLOBS;
|
|
174
|
+
const scopedToDiff = bool(args.changed) || bool(args.staged) || str(args.since) !== undefined;
|
|
175
|
+
|
|
176
|
+
// RENDERED ARTEFACTS. `cmdAudit` appends `.ultra11y/captures` and `.ultra11y/pages` to the
|
|
177
|
+
// inputs and records the page scope; this helper did not, so every MCP tool audited LESS
|
|
178
|
+
// than the same command on the CLI: no rendered tier, none of the page-scoped rules
|
|
179
|
+
// (lang, title, main landmark), and no page dimension at all. An agent driving the server
|
|
180
|
+
// was quietly getting a smaller audit than the one it would have run itself.
|
|
181
|
+
const extra = [CAPTURES_DIR, PAGES_DIR].filter((d) => !scopedToDiff && existsSync(d) && !inputs.includes(d));
|
|
182
|
+
|
|
183
|
+
const result = runAudit({
|
|
184
|
+
inputs: [...inputs, ...extra],
|
|
166
185
|
include: strArray(args.include),
|
|
167
186
|
exclude: strArray(args.exclude),
|
|
168
187
|
forceJsx: bool(args.jsx),
|
|
@@ -173,6 +192,17 @@ function audit(args: Record<string, unknown>, cwd: string): AuditResult {
|
|
|
173
192
|
maxFiles: positive(args.max_files, "max_files"),
|
|
174
193
|
onWarn: (m) => warnings.push(m),
|
|
175
194
|
});
|
|
195
|
+
|
|
196
|
+
// Record the pages in scope so the projection can rebuild from this result alone — the
|
|
197
|
+
// same step cmdAudit takes right after runAudit.
|
|
198
|
+
if (extra.includes(PAGES_DIR)) {
|
|
199
|
+
const scope = pageScopesFrom(readSnapshots("."));
|
|
200
|
+
if (scope.length) {
|
|
201
|
+
result.scope.pages = scope;
|
|
202
|
+
attributePages(result, scope);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
176
206
|
} finally {
|
|
177
207
|
process.chdir(prev);
|
|
178
208
|
}
|
|
@@ -271,6 +301,34 @@ function handleSampleCheck(cwd: string): unknown {
|
|
|
271
301
|
return { cwd, config: raw, next: "Check the sample covers every page kind the methodology requires, not just the convenient ones." };
|
|
272
302
|
}
|
|
273
303
|
|
|
304
|
+
/** The page dimension. Everything it needs is already on the AuditResult, so it re-measures
|
|
305
|
+
* nothing — the same projection the report and the CLI use. */
|
|
306
|
+
function handlePages(args: Record<string, unknown>, cwd: string): unknown {
|
|
307
|
+
const standard = standardOf(args);
|
|
308
|
+
const lang = langOf(args);
|
|
309
|
+
const r = audit(args, cwd);
|
|
310
|
+
const scope = pagesOf(r);
|
|
311
|
+
if (!scope.length) {
|
|
312
|
+
throw new ToolError(
|
|
313
|
+
"no page in scope: this audit covers no snapshot (.ultra11y/pages) and no scanned sample. " +
|
|
314
|
+
"Capture pages with the E2E plugins, `ultra11y dev`, or `scan` (which snapshots every page it visits), then re-run.",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
attributePages(r, scope);
|
|
318
|
+
const derived = derivePages(r, scope);
|
|
319
|
+
const format = typeof args.format === "string" ? args.format : "grid";
|
|
320
|
+
const markdown = format === "report" ? renderPagesDocument(r, derived, { standard, lang }) : renderPageGrid(r, scope, standard, lang);
|
|
321
|
+
return {
|
|
322
|
+
cwd,
|
|
323
|
+
standard,
|
|
324
|
+
lang,
|
|
325
|
+
markdown,
|
|
326
|
+
pages: derived.map((p) => ({ id: p.id, name: p.name, url: p.url, basis: p.basis, conformancePct: p.conformancePct, findings: p.findings.length })),
|
|
327
|
+
unattributed: unattributedFindings(r).length,
|
|
328
|
+
next: 'A page whose basis is "attributed" has no snapshot: absence of a finding there is NOT conformity. Its undecided criteria stay yours to adjudicate.',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
274
332
|
function handleFix(args: Record<string, unknown>, cwd: string): unknown {
|
|
275
333
|
const prev = process.cwd();
|
|
276
334
|
try {
|
package/src/mcp/tools.ts
CHANGED
|
@@ -178,6 +178,31 @@ export const TOOLS: ToolDecl[] = [
|
|
|
178
178
|
"is not a conformance claim. Reads the config; changes nothing.",
|
|
179
179
|
inputSchema: { type: "object", properties: { cwd: cwdProp }, required: ["cwd"] },
|
|
180
180
|
},
|
|
181
|
+
{
|
|
182
|
+
name: "ultra11y_pages",
|
|
183
|
+
title: "The per-page view of the audit",
|
|
184
|
+
description:
|
|
185
|
+
"RGAA — like every country standard — is a PER-PAGE norm, but the engine's verdict is scope-wide. This projects the audit onto the pages it covers: " +
|
|
186
|
+
"the criterion × page grid, or the per-page report (one dossier per page, every criterion of the standard with its status on THAT page). " +
|
|
187
|
+
"Two rules make it trustworthy: a finding belongs to a page only when something says so — never spread across pages — and a criterion is conforming " +
|
|
188
|
+
"by silence only on a page whose real rendered DOM was audited, and only for the criteria the engine can actually decide. Everything else stays " +
|
|
189
|
+
"« to assess », which is what it is. Pages come from snapshots (.ultra11y/pages) or a scanned sample; with neither, there is nothing to project.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
cwd: cwdProp,
|
|
194
|
+
globs: globsProp,
|
|
195
|
+
standard: standardProp,
|
|
196
|
+
lang: langProp,
|
|
197
|
+
format: {
|
|
198
|
+
type: "string",
|
|
199
|
+
enum: ["grid", "report"],
|
|
200
|
+
description: "`grid` (default) is the criterion × page matrix; `report` is the per-page dossiers.",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
required: ["cwd"],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
181
206
|
{
|
|
182
207
|
name: "ultra11y_read",
|
|
183
208
|
title: "Read a file from the project",
|
|
@@ -283,6 +308,7 @@ export const TOOL_META: Record<string, { write?: boolean; destructive?: boolean;
|
|
|
283
308
|
ultra11y_adjudicate: { openWorld: false },
|
|
284
309
|
ultra11y_pack_check: { openWorld: false },
|
|
285
310
|
ultra11y_sample_check: { openWorld: false },
|
|
311
|
+
ultra11y_pages: { openWorld: false },
|
|
286
312
|
ultra11y_read: { openWorld: false },
|
|
287
313
|
// Rewrites source files. Not destructive in the delete sense, and it verifies
|
|
288
314
|
// its own work — but it is the user's code.
|
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 = "2.31.
|
|
3
|
+
export const VERSION = "2.31.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,
|