ultra11y 5.13.1 → 5.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/dist/playwright.mjs +12 -6
- package/package.json +4 -1
- package/scripts/ultra11y.mjs +52 -11
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +52 -11
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +52 -11
- package/src/check.ts +73 -1
- package/src/cli.ts +26 -5
- package/src/integrations/playwright.ts +19 -7
- package/src/scan-local.ts +18 -3
- 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": "5.
|
|
13
|
+
"version": "5.14.1",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
16
16
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.14.1",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.14.1",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
// Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
|
|
28
28
|
// does not carry it, so a user's own ultra11y.js is never clobbered.
|
|
29
29
|
const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
|
|
30
|
-
const ULTRA11Y_PLUGIN_VERSION = "5.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.14.1";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/dist/playwright.mjs
CHANGED
|
@@ -595,14 +595,20 @@ async function runAxe(page, mode) {
|
|
|
595
595
|
if (mode === false) return void 0;
|
|
596
596
|
const demanded = mode === true;
|
|
597
597
|
let AxeBuilder;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
598
|
+
let lastError;
|
|
599
|
+
for (const from of [join(process.cwd(), "package.json"), import.meta.url]) {
|
|
600
|
+
try {
|
|
601
|
+
const mod = createRequire2(from)("@axe-core/playwright");
|
|
602
|
+
AxeBuilder = mod.default ?? mod;
|
|
603
|
+
break;
|
|
604
|
+
} catch (e) {
|
|
605
|
+
lastError = e;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (!AxeBuilder) {
|
|
603
609
|
if (demanded) {
|
|
604
610
|
console.warn(
|
|
605
|
-
`ultra11y: axe was requested but @axe-core/playwright could not be resolved from ${process.cwd()} \u2014 ${
|
|
611
|
+
`ultra11y: axe was requested but @axe-core/playwright could not be resolved from ${process.cwd()} nor from this package \u2014 ${lastError instanceof Error ? lastError.message : String(lastError)}. Install it, or set \`axe: false\`. The criteria axe decides stay to assess.`
|
|
606
612
|
);
|
|
607
613
|
}
|
|
608
614
|
return void 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.1",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"test:watch": "vitest",
|
|
49
49
|
"test:coverage": "vitest run --coverage",
|
|
50
50
|
"test:perf": "vitest run --config vitest.perf.config.ts",
|
|
51
|
+
"test:browser": "vitest run --config vitest.browser.config.ts",
|
|
51
52
|
"bench": "node scripts/bench.mjs",
|
|
52
53
|
"typecheck": "tsc --noEmit",
|
|
53
54
|
"lint": "biome ci .",
|
|
@@ -81,7 +82,9 @@
|
|
|
81
82
|
"htmlparser2": "^12.0.0"
|
|
82
83
|
},
|
|
83
84
|
"devDependencies": {
|
|
85
|
+
"@axe-core/playwright": "^4.13.0",
|
|
84
86
|
"@biomejs/biome": "^2.5.1",
|
|
87
|
+
"@playwright/test": "^1.62.1",
|
|
85
88
|
"@semantic-release/changelog": "^6.0.3",
|
|
86
89
|
"@semantic-release/exec": "^7.1.0",
|
|
87
90
|
"@semantic-release/git": "^10.0.1",
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.
|
|
9
|
+
var VERSION = "5.14.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -56439,7 +56439,7 @@ function renderCriteriaReference() {
|
|
|
56439
56439
|
}
|
|
56440
56440
|
|
|
56441
56441
|
// src/check.ts
|
|
56442
|
-
import { existsSync as existsSync20, readFileSync as readFileSync18 } from "fs";
|
|
56442
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "fs";
|
|
56443
56443
|
import { dirname as dirname9, join as join34 } from "path";
|
|
56444
56444
|
|
|
56445
56445
|
// src/verify.ts
|
|
@@ -56860,6 +56860,33 @@ function sectionBody(md, n) {
|
|
|
56860
56860
|
const next = /^##\s+/m.exec(md.slice(from));
|
|
56861
56861
|
return next ? md.slice(from, from + next.index) : md.slice(from);
|
|
56862
56862
|
}
|
|
56863
|
+
function checkSampleCaptured(root = ".", lang = "en") {
|
|
56864
|
+
const fr = lang === "fr";
|
|
56865
|
+
let declared = [];
|
|
56866
|
+
try {
|
|
56867
|
+
const cfg = JSON.parse(readFileSync18(join34(root, ".ultra11yrc.json"), "utf8"));
|
|
56868
|
+
declared = cfg.sample?.pages ?? [];
|
|
56869
|
+
} catch {
|
|
56870
|
+
return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
|
|
56871
|
+
}
|
|
56872
|
+
if (!declared.length) return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
|
|
56873
|
+
let captured = /* @__PURE__ */ new Set();
|
|
56874
|
+
try {
|
|
56875
|
+
captured = new Set(
|
|
56876
|
+
readdirSync5(join34(root, PAGES_DIR), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).filter((id) => existsSync20(join34(root, PAGES_DIR, id, "dom.html")))
|
|
56877
|
+
);
|
|
56878
|
+
} catch {
|
|
56879
|
+
}
|
|
56880
|
+
const missing = declared.filter((p) => !captured.has(p.id)).map((p) => ({ id: p.id, name: p.name ?? p.id, url: p.url ?? "" }));
|
|
56881
|
+
const issues = [];
|
|
56882
|
+
if (missing.length) {
|
|
56883
|
+
const kept = declared.length - missing.length;
|
|
56884
|
+
issues.push(
|
|
56885
|
+
fr ? `${kept}/${declared.length} page(s) d\xE9clar\xE9e(s) ont \xE9t\xE9 captur\xE9es. Aucune capture pour : ${missing.map((m) => `${m.name} (${m.id})`).join(", ")}. Une page d\xE9clar\xE9e sans capture n'est pas une page conforme, c'est une page que personne n'a regard\xE9e.` : `${kept}/${declared.length} declared page(s) were captured. No capture for: ${missing.map((m) => `${m.name} (${m.id})`).join(", ")}. A declared page with no capture is not a page that passed \u2014 it is a page nobody looked at.`
|
|
56886
|
+
);
|
|
56887
|
+
}
|
|
56888
|
+
return { ok: issues.length === 0, issues, missing, declared: declared.length, captured: captured.size };
|
|
56889
|
+
}
|
|
56863
56890
|
function byCriterionId(a, b) {
|
|
56864
56891
|
const pa = a.split(".").map(Number);
|
|
56865
56892
|
const pb = b.split(".").map(Number);
|
|
@@ -58740,7 +58767,7 @@ function applyRawVerdicts(items, verdicts) {
|
|
|
58740
58767
|
|
|
58741
58768
|
// src/scan.ts
|
|
58742
58769
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
58743
|
-
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync12, existsSync as existsSync23, statSync as statSync8, readdirSync as
|
|
58770
|
+
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync12, existsSync as existsSync23, statSync as statSync8, readdirSync as readdirSync6, rmSync as rmSync3, readFileSync as readFileSync20 } from "fs";
|
|
58744
58771
|
import { tmpdir as tmpdir2 } from "os";
|
|
58745
58772
|
import { join as join37, resolve as resolve11 } from "path";
|
|
58746
58773
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
@@ -59064,7 +59091,7 @@ function buildImage(tag = IMAGE_TAG) {
|
|
|
59064
59091
|
function cleanTempContexts() {
|
|
59065
59092
|
let removed = 0;
|
|
59066
59093
|
const dir = tmpdir2();
|
|
59067
|
-
for (const name2 of
|
|
59094
|
+
for (const name2 of readdirSync6(dir)) {
|
|
59068
59095
|
if (!name2.startsWith(CTX_PREFIX)) continue;
|
|
59069
59096
|
rmSync3(join37(dir, name2), { recursive: true, force: true });
|
|
59070
59097
|
removed++;
|
|
@@ -59667,7 +59694,9 @@ function localAvailable(cwd) {
|
|
|
59667
59694
|
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59668
59695
|
req.resolve(PW_SPEC);
|
|
59669
59696
|
req.resolve(AXE_SPEC);
|
|
59670
|
-
|
|
59697
|
+
const pw = req(PW_SPEC);
|
|
59698
|
+
const bin = pw.chromium?.executablePath?.();
|
|
59699
|
+
return typeof bin !== "string" || bin.length === 0 ? true : existsSync24(bin);
|
|
59671
59700
|
} catch {
|
|
59672
59701
|
return false;
|
|
59673
59702
|
}
|
|
@@ -65563,7 +65592,7 @@ function str3(v) {
|
|
|
65563
65592
|
var DECLARED = new Set([...TOOLS2, ...WRITE_TOOLS].map((t3) => t3.name));
|
|
65564
65593
|
|
|
65565
65594
|
// src/mcp/resources.ts
|
|
65566
|
-
import { existsSync as existsSync34, readdirSync as
|
|
65595
|
+
import { existsSync as existsSync34, readdirSync as readdirSync7, readFileSync as readFileSync32, realpathSync as realpathSync5, statSync as statSync13 } from "fs";
|
|
65567
65596
|
import { basename as basename3, dirname as dirname14, join as join50, resolve as resolve15, sep as sep6 } from "path";
|
|
65568
65597
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
65569
65598
|
var SKILL_NAME = "ultra11y";
|
|
@@ -65579,7 +65608,7 @@ function listResources(moduleDir) {
|
|
|
65579
65608
|
const out2 = [describe(root, "SKILL.md", `${SKILL_NAME}: the skill`)];
|
|
65580
65609
|
const refDir = join50(root, "references");
|
|
65581
65610
|
if (!existsSync34(refDir)) return out2;
|
|
65582
|
-
for (const file of
|
|
65611
|
+
for (const file of readdirSync7(refDir).sort()) {
|
|
65583
65612
|
if (!file.endsWith(".md")) continue;
|
|
65584
65613
|
out2.push(describe(root, join50("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
|
|
65585
65614
|
}
|
|
@@ -66457,6 +66486,10 @@ Options:
|
|
|
66457
66486
|
dropped as stale and its criterion says so
|
|
66458
66487
|
--max-verify <n> verify: cap the worklist size; 0 = no cap (default: 40)
|
|
66459
66488
|
--verdicts <file> check --semantic: the adjudicated verdicts artifact
|
|
66489
|
+
--require-sample check: fail while a page DECLARED in .ultra11yrc.json has no capture under
|
|
66490
|
+
.ultra11y/pages/. Coverage, one level below --require-decided: a sweep that
|
|
66491
|
+
loses pages produces a report that is merely SHORTER, and a shorter
|
|
66492
|
+
deliverable reads exactly like a complete one.
|
|
66460
66493
|
--require-decided check: fail while ANY criterion of the standard is still \xAB to assess \xBB.
|
|
66461
66494
|
Needs --in. A green job does not otherwise mean the grid was filled:
|
|
66462
66495
|
--fail-on governs non-conformities, and an adjudication that lands
|
|
@@ -66683,6 +66716,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
66683
66716
|
"semantic",
|
|
66684
66717
|
// `check`: fail while any criterion of the standard is still « to assess ».
|
|
66685
66718
|
"require-decided",
|
|
66719
|
+
"require-sample",
|
|
66686
66720
|
"manual",
|
|
66687
66721
|
// `verify --apply` / `judge --apply`: restore the all-or-nothing fold, where one refused
|
|
66688
66722
|
// verdict discards the whole adjudication. The default is per-verdict.
|
|
@@ -68059,9 +68093,10 @@ function cmdCheck(p) {
|
|
|
68059
68093
|
const standard = stdOf(p, "check");
|
|
68060
68094
|
if (standard === null) return 2;
|
|
68061
68095
|
const requireDecided = p.flags["require-decided"] === true;
|
|
68096
|
+
const requireSample = p.flags["require-sample"] === true;
|
|
68062
68097
|
const rep = p.flags.report;
|
|
68063
68098
|
if (typeof rep !== "string" || !rep) {
|
|
68064
|
-
if (!requireDecided) {
|
|
68099
|
+
if (!requireDecided && !requireSample) {
|
|
68065
68100
|
console.error("ultra11y check: --report <md> is required.");
|
|
68066
68101
|
return 2;
|
|
68067
68102
|
}
|
|
@@ -68106,6 +68141,7 @@ function cmdCheck(p) {
|
|
|
68106
68141
|
}
|
|
68107
68142
|
}
|
|
68108
68143
|
const decided = requireDecided && audit2 ? checkDecided(audit2, standard, lang, { allow }) : null;
|
|
68144
|
+
const covered = requireSample ? checkSampleCaptured(".", lang) : null;
|
|
68109
68145
|
const res = md ? checkReport(md, standard, lang, { audit: audit2 }) : { ok: true, issues: [] };
|
|
68110
68146
|
const sem = p.flags.semantic === true && typeof rep === "string" && rep ? checkSemantic(md, {
|
|
68111
68147
|
reportPath: rep,
|
|
@@ -68113,9 +68149,9 @@ function cmdCheck(p) {
|
|
|
68113
68149
|
standard,
|
|
68114
68150
|
lang
|
|
68115
68151
|
}) : null;
|
|
68116
|
-
const ok = res.ok && (sem === null || sem.ok) && (decided === null || decided.ok);
|
|
68152
|
+
const ok = res.ok && (sem === null || sem.ok) && (decided === null || decided.ok) && (covered === null || covered.ok);
|
|
68117
68153
|
if (p.flags.json) {
|
|
68118
|
-
console.log(JSON.stringify({ ...res, ok, ...sem ? { semantic: sem } : {}, ...decided ? { decided } : {} }, null, 2));
|
|
68154
|
+
console.log(JSON.stringify({ ...res, ok, ...sem ? { semantic: sem } : {}, ...decided ? { decided } : {}, ...covered ? { covered } : {} }, null, 2));
|
|
68119
68155
|
} else if (!p.flags.quiet) {
|
|
68120
68156
|
if (decided) {
|
|
68121
68157
|
const allowedNote = decided.allowed.length ? lang === "fr" ? ` (${decided.allowed.length} crit\xE8re(s) d\xE9clar\xE9(s) ind\xE9cidable(s) : ${decided.allowed.map((a) => `${a.criteriaId} \u2014 ${a.reason}`).join(" \xB7 ")})` : ` (${decided.allowed.length} criterion(ia) declared undecidable: ${decided.allowed.map((a) => `${a.criteriaId} \u2014 ${a.reason}`).join(" \xB7 ")})` : "";
|
|
@@ -68124,11 +68160,16 @@ function cmdCheck(p) {
|
|
|
68124
68160
|
lang === "fr" ? `\u2713 Grille compl\xE8te : les ${decided.total} crit\xE8res portent un verdict${allowedNote}.` : `\u2713 Complete grid: all ${decided.total} criteria carry a verdict${allowedNote}.`
|
|
68125
68161
|
);
|
|
68126
68162
|
}
|
|
68163
|
+
if (covered?.ok) {
|
|
68164
|
+
console.log(
|
|
68165
|
+
lang === "fr" ? `\u2713 \xC9chantillon couvert : les ${covered.declared} page(s) d\xE9clar\xE9e(s) ont une capture.` : `\u2713 Sample covered: all ${covered.declared} declared page(s) have a capture.`
|
|
68166
|
+
);
|
|
68167
|
+
}
|
|
68127
68168
|
if (ok)
|
|
68128
68169
|
console.log(
|
|
68129
68170
|
sem ? lang === "fr" ? `\u2713 Rapport valide + gate s\xE9mantique engag\xE9e : ${sem.total} verdict(s), ${sem.grounded} ancr\xE9(s) dans la source${sem.moved ? ` (${sem.moved} d\xE9plac\xE9(s))` : ""}.` : `\u2713 Report valid + semantic gate engaged: ${sem.total} verdict(s), ${sem.grounded} grounded in source${sem.moved ? ` (${sem.moved} moved)` : ""}.` : lang === "fr" ? "\u2713 Rapport valide : sections, crit\xE8res cit\xE9s et justifications NA coh\xE9rents." : "\u2713 Report valid: sections, cited criteria and NA justifications are consistent."
|
|
68130
68171
|
);
|
|
68131
|
-
else for (const i2 of [...res.issues, ...sem?.issues ?? [], ...decided?.issues ?? []]) console.error(`\u2717 ${i2}`);
|
|
68172
|
+
else for (const i2 of [...res.issues, ...sem?.issues ?? [], ...decided?.issues ?? [], ...covered?.issues ?? []]) console.error(`\u2717 ${i2}`);
|
|
68132
68173
|
}
|
|
68133
68174
|
return ok ? 0 : 1;
|
|
68134
68175
|
}
|
|
@@ -5,7 +5,7 @@ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should
|
|
|
5
5
|
allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
|
|
6
6
|
license: MIT
|
|
7
7
|
metadata:
|
|
8
|
-
version: 5.
|
|
8
|
+
version: 5.14.1
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.
|
|
9
|
+
var VERSION = "5.14.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -56439,7 +56439,7 @@ function renderCriteriaReference() {
|
|
|
56439
56439
|
}
|
|
56440
56440
|
|
|
56441
56441
|
// src/check.ts
|
|
56442
|
-
import { existsSync as existsSync20, readFileSync as readFileSync18 } from "fs";
|
|
56442
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "fs";
|
|
56443
56443
|
import { dirname as dirname9, join as join34 } from "path";
|
|
56444
56444
|
|
|
56445
56445
|
// src/verify.ts
|
|
@@ -56860,6 +56860,33 @@ function sectionBody(md, n) {
|
|
|
56860
56860
|
const next = /^##\s+/m.exec(md.slice(from));
|
|
56861
56861
|
return next ? md.slice(from, from + next.index) : md.slice(from);
|
|
56862
56862
|
}
|
|
56863
|
+
function checkSampleCaptured(root = ".", lang = "en") {
|
|
56864
|
+
const fr = lang === "fr";
|
|
56865
|
+
let declared = [];
|
|
56866
|
+
try {
|
|
56867
|
+
const cfg = JSON.parse(readFileSync18(join34(root, ".ultra11yrc.json"), "utf8"));
|
|
56868
|
+
declared = cfg.sample?.pages ?? [];
|
|
56869
|
+
} catch {
|
|
56870
|
+
return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
|
|
56871
|
+
}
|
|
56872
|
+
if (!declared.length) return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
|
|
56873
|
+
let captured = /* @__PURE__ */ new Set();
|
|
56874
|
+
try {
|
|
56875
|
+
captured = new Set(
|
|
56876
|
+
readdirSync5(join34(root, PAGES_DIR), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).filter((id) => existsSync20(join34(root, PAGES_DIR, id, "dom.html")))
|
|
56877
|
+
);
|
|
56878
|
+
} catch {
|
|
56879
|
+
}
|
|
56880
|
+
const missing = declared.filter((p) => !captured.has(p.id)).map((p) => ({ id: p.id, name: p.name ?? p.id, url: p.url ?? "" }));
|
|
56881
|
+
const issues = [];
|
|
56882
|
+
if (missing.length) {
|
|
56883
|
+
const kept = declared.length - missing.length;
|
|
56884
|
+
issues.push(
|
|
56885
|
+
fr ? `${kept}/${declared.length} page(s) d\xE9clar\xE9e(s) ont \xE9t\xE9 captur\xE9es. Aucune capture pour : ${missing.map((m) => `${m.name} (${m.id})`).join(", ")}. Une page d\xE9clar\xE9e sans capture n'est pas une page conforme, c'est une page que personne n'a regard\xE9e.` : `${kept}/${declared.length} declared page(s) were captured. No capture for: ${missing.map((m) => `${m.name} (${m.id})`).join(", ")}. A declared page with no capture is not a page that passed \u2014 it is a page nobody looked at.`
|
|
56886
|
+
);
|
|
56887
|
+
}
|
|
56888
|
+
return { ok: issues.length === 0, issues, missing, declared: declared.length, captured: captured.size };
|
|
56889
|
+
}
|
|
56863
56890
|
function byCriterionId(a, b) {
|
|
56864
56891
|
const pa = a.split(".").map(Number);
|
|
56865
56892
|
const pb = b.split(".").map(Number);
|
|
@@ -58740,7 +58767,7 @@ function applyRawVerdicts(items, verdicts) {
|
|
|
58740
58767
|
|
|
58741
58768
|
// src/scan.ts
|
|
58742
58769
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
58743
|
-
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync12, existsSync as existsSync23, statSync as statSync8, readdirSync as
|
|
58770
|
+
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync12, existsSync as existsSync23, statSync as statSync8, readdirSync as readdirSync6, rmSync as rmSync3, readFileSync as readFileSync20 } from "fs";
|
|
58744
58771
|
import { tmpdir as tmpdir2 } from "os";
|
|
58745
58772
|
import { join as join37, resolve as resolve11 } from "path";
|
|
58746
58773
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
@@ -59064,7 +59091,7 @@ function buildImage(tag = IMAGE_TAG) {
|
|
|
59064
59091
|
function cleanTempContexts() {
|
|
59065
59092
|
let removed = 0;
|
|
59066
59093
|
const dir = tmpdir2();
|
|
59067
|
-
for (const name2 of
|
|
59094
|
+
for (const name2 of readdirSync6(dir)) {
|
|
59068
59095
|
if (!name2.startsWith(CTX_PREFIX)) continue;
|
|
59069
59096
|
rmSync3(join37(dir, name2), { recursive: true, force: true });
|
|
59070
59097
|
removed++;
|
|
@@ -59667,7 +59694,9 @@ function localAvailable(cwd) {
|
|
|
59667
59694
|
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59668
59695
|
req.resolve(PW_SPEC);
|
|
59669
59696
|
req.resolve(AXE_SPEC);
|
|
59670
|
-
|
|
59697
|
+
const pw = req(PW_SPEC);
|
|
59698
|
+
const bin = pw.chromium?.executablePath?.();
|
|
59699
|
+
return typeof bin !== "string" || bin.length === 0 ? true : existsSync24(bin);
|
|
59671
59700
|
} catch {
|
|
59672
59701
|
return false;
|
|
59673
59702
|
}
|
|
@@ -65563,7 +65592,7 @@ function str3(v) {
|
|
|
65563
65592
|
var DECLARED = new Set([...TOOLS2, ...WRITE_TOOLS].map((t3) => t3.name));
|
|
65564
65593
|
|
|
65565
65594
|
// src/mcp/resources.ts
|
|
65566
|
-
import { existsSync as existsSync34, readdirSync as
|
|
65595
|
+
import { existsSync as existsSync34, readdirSync as readdirSync7, readFileSync as readFileSync32, realpathSync as realpathSync5, statSync as statSync13 } from "fs";
|
|
65567
65596
|
import { basename as basename3, dirname as dirname14, join as join50, resolve as resolve15, sep as sep6 } from "path";
|
|
65568
65597
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
65569
65598
|
var SKILL_NAME = "ultra11y";
|
|
@@ -65579,7 +65608,7 @@ function listResources(moduleDir) {
|
|
|
65579
65608
|
const out2 = [describe(root, "SKILL.md", `${SKILL_NAME}: the skill`)];
|
|
65580
65609
|
const refDir = join50(root, "references");
|
|
65581
65610
|
if (!existsSync34(refDir)) return out2;
|
|
65582
|
-
for (const file of
|
|
65611
|
+
for (const file of readdirSync7(refDir).sort()) {
|
|
65583
65612
|
if (!file.endsWith(".md")) continue;
|
|
65584
65613
|
out2.push(describe(root, join50("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
|
|
65585
65614
|
}
|
|
@@ -66457,6 +66486,10 @@ Options:
|
|
|
66457
66486
|
dropped as stale and its criterion says so
|
|
66458
66487
|
--max-verify <n> verify: cap the worklist size; 0 = no cap (default: 40)
|
|
66459
66488
|
--verdicts <file> check --semantic: the adjudicated verdicts artifact
|
|
66489
|
+
--require-sample check: fail while a page DECLARED in .ultra11yrc.json has no capture under
|
|
66490
|
+
.ultra11y/pages/. Coverage, one level below --require-decided: a sweep that
|
|
66491
|
+
loses pages produces a report that is merely SHORTER, and a shorter
|
|
66492
|
+
deliverable reads exactly like a complete one.
|
|
66460
66493
|
--require-decided check: fail while ANY criterion of the standard is still \xAB to assess \xBB.
|
|
66461
66494
|
Needs --in. A green job does not otherwise mean the grid was filled:
|
|
66462
66495
|
--fail-on governs non-conformities, and an adjudication that lands
|
|
@@ -66683,6 +66716,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
66683
66716
|
"semantic",
|
|
66684
66717
|
// `check`: fail while any criterion of the standard is still « to assess ».
|
|
66685
66718
|
"require-decided",
|
|
66719
|
+
"require-sample",
|
|
66686
66720
|
"manual",
|
|
66687
66721
|
// `verify --apply` / `judge --apply`: restore the all-or-nothing fold, where one refused
|
|
66688
66722
|
// verdict discards the whole adjudication. The default is per-verdict.
|
|
@@ -68059,9 +68093,10 @@ function cmdCheck(p) {
|
|
|
68059
68093
|
const standard = stdOf(p, "check");
|
|
68060
68094
|
if (standard === null) return 2;
|
|
68061
68095
|
const requireDecided = p.flags["require-decided"] === true;
|
|
68096
|
+
const requireSample = p.flags["require-sample"] === true;
|
|
68062
68097
|
const rep = p.flags.report;
|
|
68063
68098
|
if (typeof rep !== "string" || !rep) {
|
|
68064
|
-
if (!requireDecided) {
|
|
68099
|
+
if (!requireDecided && !requireSample) {
|
|
68065
68100
|
console.error("ultra11y check: --report <md> is required.");
|
|
68066
68101
|
return 2;
|
|
68067
68102
|
}
|
|
@@ -68106,6 +68141,7 @@ function cmdCheck(p) {
|
|
|
68106
68141
|
}
|
|
68107
68142
|
}
|
|
68108
68143
|
const decided = requireDecided && audit2 ? checkDecided(audit2, standard, lang, { allow }) : null;
|
|
68144
|
+
const covered = requireSample ? checkSampleCaptured(".", lang) : null;
|
|
68109
68145
|
const res = md ? checkReport(md, standard, lang, { audit: audit2 }) : { ok: true, issues: [] };
|
|
68110
68146
|
const sem = p.flags.semantic === true && typeof rep === "string" && rep ? checkSemantic(md, {
|
|
68111
68147
|
reportPath: rep,
|
|
@@ -68113,9 +68149,9 @@ function cmdCheck(p) {
|
|
|
68113
68149
|
standard,
|
|
68114
68150
|
lang
|
|
68115
68151
|
}) : null;
|
|
68116
|
-
const ok = res.ok && (sem === null || sem.ok) && (decided === null || decided.ok);
|
|
68152
|
+
const ok = res.ok && (sem === null || sem.ok) && (decided === null || decided.ok) && (covered === null || covered.ok);
|
|
68117
68153
|
if (p.flags.json) {
|
|
68118
|
-
console.log(JSON.stringify({ ...res, ok, ...sem ? { semantic: sem } : {}, ...decided ? { decided } : {} }, null, 2));
|
|
68154
|
+
console.log(JSON.stringify({ ...res, ok, ...sem ? { semantic: sem } : {}, ...decided ? { decided } : {}, ...covered ? { covered } : {} }, null, 2));
|
|
68119
68155
|
} else if (!p.flags.quiet) {
|
|
68120
68156
|
if (decided) {
|
|
68121
68157
|
const allowedNote = decided.allowed.length ? lang === "fr" ? ` (${decided.allowed.length} crit\xE8re(s) d\xE9clar\xE9(s) ind\xE9cidable(s) : ${decided.allowed.map((a) => `${a.criteriaId} \u2014 ${a.reason}`).join(" \xB7 ")})` : ` (${decided.allowed.length} criterion(ia) declared undecidable: ${decided.allowed.map((a) => `${a.criteriaId} \u2014 ${a.reason}`).join(" \xB7 ")})` : "";
|
|
@@ -68124,11 +68160,16 @@ function cmdCheck(p) {
|
|
|
68124
68160
|
lang === "fr" ? `\u2713 Grille compl\xE8te : les ${decided.total} crit\xE8res portent un verdict${allowedNote}.` : `\u2713 Complete grid: all ${decided.total} criteria carry a verdict${allowedNote}.`
|
|
68125
68161
|
);
|
|
68126
68162
|
}
|
|
68163
|
+
if (covered?.ok) {
|
|
68164
|
+
console.log(
|
|
68165
|
+
lang === "fr" ? `\u2713 \xC9chantillon couvert : les ${covered.declared} page(s) d\xE9clar\xE9e(s) ont une capture.` : `\u2713 Sample covered: all ${covered.declared} declared page(s) have a capture.`
|
|
68166
|
+
);
|
|
68167
|
+
}
|
|
68127
68168
|
if (ok)
|
|
68128
68169
|
console.log(
|
|
68129
68170
|
sem ? lang === "fr" ? `\u2713 Rapport valide + gate s\xE9mantique engag\xE9e : ${sem.total} verdict(s), ${sem.grounded} ancr\xE9(s) dans la source${sem.moved ? ` (${sem.moved} d\xE9plac\xE9(s))` : ""}.` : `\u2713 Report valid + semantic gate engaged: ${sem.total} verdict(s), ${sem.grounded} grounded in source${sem.moved ? ` (${sem.moved} moved)` : ""}.` : lang === "fr" ? "\u2713 Rapport valide : sections, crit\xE8res cit\xE9s et justifications NA coh\xE9rents." : "\u2713 Report valid: sections, cited criteria and NA justifications are consistent."
|
|
68130
68171
|
);
|
|
68131
|
-
else for (const i2 of [...res.issues, ...sem?.issues ?? [], ...decided?.issues ?? []]) console.error(`\u2717 ${i2}`);
|
|
68172
|
+
else for (const i2 of [...res.issues, ...sem?.issues ?? [], ...decided?.issues ?? [], ...covered?.issues ?? []]) console.error(`\u2717 ${i2}`);
|
|
68132
68173
|
}
|
|
68133
68174
|
return ok ? 0 : 1;
|
|
68134
68175
|
}
|
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 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."
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: 5.
|
|
7
|
+
version: 5.14.1
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.
|
|
9
|
+
var VERSION = "5.14.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -56439,7 +56439,7 @@ function renderCriteriaReference() {
|
|
|
56439
56439
|
}
|
|
56440
56440
|
|
|
56441
56441
|
// src/check.ts
|
|
56442
|
-
import { existsSync as existsSync20, readFileSync as readFileSync18 } from "fs";
|
|
56442
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "fs";
|
|
56443
56443
|
import { dirname as dirname9, join as join34 } from "path";
|
|
56444
56444
|
|
|
56445
56445
|
// src/verify.ts
|
|
@@ -56860,6 +56860,33 @@ function sectionBody(md, n) {
|
|
|
56860
56860
|
const next = /^##\s+/m.exec(md.slice(from));
|
|
56861
56861
|
return next ? md.slice(from, from + next.index) : md.slice(from);
|
|
56862
56862
|
}
|
|
56863
|
+
function checkSampleCaptured(root = ".", lang = "en") {
|
|
56864
|
+
const fr = lang === "fr";
|
|
56865
|
+
let declared = [];
|
|
56866
|
+
try {
|
|
56867
|
+
const cfg = JSON.parse(readFileSync18(join34(root, ".ultra11yrc.json"), "utf8"));
|
|
56868
|
+
declared = cfg.sample?.pages ?? [];
|
|
56869
|
+
} catch {
|
|
56870
|
+
return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
|
|
56871
|
+
}
|
|
56872
|
+
if (!declared.length) return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
|
|
56873
|
+
let captured = /* @__PURE__ */ new Set();
|
|
56874
|
+
try {
|
|
56875
|
+
captured = new Set(
|
|
56876
|
+
readdirSync5(join34(root, PAGES_DIR), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).filter((id) => existsSync20(join34(root, PAGES_DIR, id, "dom.html")))
|
|
56877
|
+
);
|
|
56878
|
+
} catch {
|
|
56879
|
+
}
|
|
56880
|
+
const missing = declared.filter((p) => !captured.has(p.id)).map((p) => ({ id: p.id, name: p.name ?? p.id, url: p.url ?? "" }));
|
|
56881
|
+
const issues = [];
|
|
56882
|
+
if (missing.length) {
|
|
56883
|
+
const kept = declared.length - missing.length;
|
|
56884
|
+
issues.push(
|
|
56885
|
+
fr ? `${kept}/${declared.length} page(s) d\xE9clar\xE9e(s) ont \xE9t\xE9 captur\xE9es. Aucune capture pour : ${missing.map((m) => `${m.name} (${m.id})`).join(", ")}. Une page d\xE9clar\xE9e sans capture n'est pas une page conforme, c'est une page que personne n'a regard\xE9e.` : `${kept}/${declared.length} declared page(s) were captured. No capture for: ${missing.map((m) => `${m.name} (${m.id})`).join(", ")}. A declared page with no capture is not a page that passed \u2014 it is a page nobody looked at.`
|
|
56886
|
+
);
|
|
56887
|
+
}
|
|
56888
|
+
return { ok: issues.length === 0, issues, missing, declared: declared.length, captured: captured.size };
|
|
56889
|
+
}
|
|
56863
56890
|
function byCriterionId(a, b) {
|
|
56864
56891
|
const pa = a.split(".").map(Number);
|
|
56865
56892
|
const pb = b.split(".").map(Number);
|
|
@@ -58740,7 +58767,7 @@ function applyRawVerdicts(items, verdicts) {
|
|
|
58740
58767
|
|
|
58741
58768
|
// src/scan.ts
|
|
58742
58769
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
58743
|
-
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync12, existsSync as existsSync23, statSync as statSync8, readdirSync as
|
|
58770
|
+
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync12, existsSync as existsSync23, statSync as statSync8, readdirSync as readdirSync6, rmSync as rmSync3, readFileSync as readFileSync20 } from "fs";
|
|
58744
58771
|
import { tmpdir as tmpdir2 } from "os";
|
|
58745
58772
|
import { join as join37, resolve as resolve11 } from "path";
|
|
58746
58773
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
@@ -59064,7 +59091,7 @@ function buildImage(tag = IMAGE_TAG) {
|
|
|
59064
59091
|
function cleanTempContexts() {
|
|
59065
59092
|
let removed = 0;
|
|
59066
59093
|
const dir = tmpdir2();
|
|
59067
|
-
for (const name2 of
|
|
59094
|
+
for (const name2 of readdirSync6(dir)) {
|
|
59068
59095
|
if (!name2.startsWith(CTX_PREFIX)) continue;
|
|
59069
59096
|
rmSync3(join37(dir, name2), { recursive: true, force: true });
|
|
59070
59097
|
removed++;
|
|
@@ -59667,7 +59694,9 @@ function localAvailable(cwd) {
|
|
|
59667
59694
|
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59668
59695
|
req.resolve(PW_SPEC);
|
|
59669
59696
|
req.resolve(AXE_SPEC);
|
|
59670
|
-
|
|
59697
|
+
const pw = req(PW_SPEC);
|
|
59698
|
+
const bin = pw.chromium?.executablePath?.();
|
|
59699
|
+
return typeof bin !== "string" || bin.length === 0 ? true : existsSync24(bin);
|
|
59671
59700
|
} catch {
|
|
59672
59701
|
return false;
|
|
59673
59702
|
}
|
|
@@ -65563,7 +65592,7 @@ function str3(v) {
|
|
|
65563
65592
|
var DECLARED = new Set([...TOOLS2, ...WRITE_TOOLS].map((t3) => t3.name));
|
|
65564
65593
|
|
|
65565
65594
|
// src/mcp/resources.ts
|
|
65566
|
-
import { existsSync as existsSync34, readdirSync as
|
|
65595
|
+
import { existsSync as existsSync34, readdirSync as readdirSync7, readFileSync as readFileSync32, realpathSync as realpathSync5, statSync as statSync13 } from "fs";
|
|
65567
65596
|
import { basename as basename3, dirname as dirname14, join as join50, resolve as resolve15, sep as sep6 } from "path";
|
|
65568
65597
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
65569
65598
|
var SKILL_NAME = "ultra11y";
|
|
@@ -65579,7 +65608,7 @@ function listResources(moduleDir) {
|
|
|
65579
65608
|
const out2 = [describe(root, "SKILL.md", `${SKILL_NAME}: the skill`)];
|
|
65580
65609
|
const refDir = join50(root, "references");
|
|
65581
65610
|
if (!existsSync34(refDir)) return out2;
|
|
65582
|
-
for (const file of
|
|
65611
|
+
for (const file of readdirSync7(refDir).sort()) {
|
|
65583
65612
|
if (!file.endsWith(".md")) continue;
|
|
65584
65613
|
out2.push(describe(root, join50("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
|
|
65585
65614
|
}
|
|
@@ -66457,6 +66486,10 @@ Options:
|
|
|
66457
66486
|
dropped as stale and its criterion says so
|
|
66458
66487
|
--max-verify <n> verify: cap the worklist size; 0 = no cap (default: 40)
|
|
66459
66488
|
--verdicts <file> check --semantic: the adjudicated verdicts artifact
|
|
66489
|
+
--require-sample check: fail while a page DECLARED in .ultra11yrc.json has no capture under
|
|
66490
|
+
.ultra11y/pages/. Coverage, one level below --require-decided: a sweep that
|
|
66491
|
+
loses pages produces a report that is merely SHORTER, and a shorter
|
|
66492
|
+
deliverable reads exactly like a complete one.
|
|
66460
66493
|
--require-decided check: fail while ANY criterion of the standard is still \xAB to assess \xBB.
|
|
66461
66494
|
Needs --in. A green job does not otherwise mean the grid was filled:
|
|
66462
66495
|
--fail-on governs non-conformities, and an adjudication that lands
|
|
@@ -66683,6 +66716,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
66683
66716
|
"semantic",
|
|
66684
66717
|
// `check`: fail while any criterion of the standard is still « to assess ».
|
|
66685
66718
|
"require-decided",
|
|
66719
|
+
"require-sample",
|
|
66686
66720
|
"manual",
|
|
66687
66721
|
// `verify --apply` / `judge --apply`: restore the all-or-nothing fold, where one refused
|
|
66688
66722
|
// verdict discards the whole adjudication. The default is per-verdict.
|
|
@@ -68059,9 +68093,10 @@ function cmdCheck(p) {
|
|
|
68059
68093
|
const standard = stdOf(p, "check");
|
|
68060
68094
|
if (standard === null) return 2;
|
|
68061
68095
|
const requireDecided = p.flags["require-decided"] === true;
|
|
68096
|
+
const requireSample = p.flags["require-sample"] === true;
|
|
68062
68097
|
const rep = p.flags.report;
|
|
68063
68098
|
if (typeof rep !== "string" || !rep) {
|
|
68064
|
-
if (!requireDecided) {
|
|
68099
|
+
if (!requireDecided && !requireSample) {
|
|
68065
68100
|
console.error("ultra11y check: --report <md> is required.");
|
|
68066
68101
|
return 2;
|
|
68067
68102
|
}
|
|
@@ -68106,6 +68141,7 @@ function cmdCheck(p) {
|
|
|
68106
68141
|
}
|
|
68107
68142
|
}
|
|
68108
68143
|
const decided = requireDecided && audit2 ? checkDecided(audit2, standard, lang, { allow }) : null;
|
|
68144
|
+
const covered = requireSample ? checkSampleCaptured(".", lang) : null;
|
|
68109
68145
|
const res = md ? checkReport(md, standard, lang, { audit: audit2 }) : { ok: true, issues: [] };
|
|
68110
68146
|
const sem = p.flags.semantic === true && typeof rep === "string" && rep ? checkSemantic(md, {
|
|
68111
68147
|
reportPath: rep,
|
|
@@ -68113,9 +68149,9 @@ function cmdCheck(p) {
|
|
|
68113
68149
|
standard,
|
|
68114
68150
|
lang
|
|
68115
68151
|
}) : null;
|
|
68116
|
-
const ok = res.ok && (sem === null || sem.ok) && (decided === null || decided.ok);
|
|
68152
|
+
const ok = res.ok && (sem === null || sem.ok) && (decided === null || decided.ok) && (covered === null || covered.ok);
|
|
68117
68153
|
if (p.flags.json) {
|
|
68118
|
-
console.log(JSON.stringify({ ...res, ok, ...sem ? { semantic: sem } : {}, ...decided ? { decided } : {} }, null, 2));
|
|
68154
|
+
console.log(JSON.stringify({ ...res, ok, ...sem ? { semantic: sem } : {}, ...decided ? { decided } : {}, ...covered ? { covered } : {} }, null, 2));
|
|
68119
68155
|
} else if (!p.flags.quiet) {
|
|
68120
68156
|
if (decided) {
|
|
68121
68157
|
const allowedNote = decided.allowed.length ? lang === "fr" ? ` (${decided.allowed.length} crit\xE8re(s) d\xE9clar\xE9(s) ind\xE9cidable(s) : ${decided.allowed.map((a) => `${a.criteriaId} \u2014 ${a.reason}`).join(" \xB7 ")})` : ` (${decided.allowed.length} criterion(ia) declared undecidable: ${decided.allowed.map((a) => `${a.criteriaId} \u2014 ${a.reason}`).join(" \xB7 ")})` : "";
|
|
@@ -68124,11 +68160,16 @@ function cmdCheck(p) {
|
|
|
68124
68160
|
lang === "fr" ? `\u2713 Grille compl\xE8te : les ${decided.total} crit\xE8res portent un verdict${allowedNote}.` : `\u2713 Complete grid: all ${decided.total} criteria carry a verdict${allowedNote}.`
|
|
68125
68161
|
);
|
|
68126
68162
|
}
|
|
68163
|
+
if (covered?.ok) {
|
|
68164
|
+
console.log(
|
|
68165
|
+
lang === "fr" ? `\u2713 \xC9chantillon couvert : les ${covered.declared} page(s) d\xE9clar\xE9e(s) ont une capture.` : `\u2713 Sample covered: all ${covered.declared} declared page(s) have a capture.`
|
|
68166
|
+
);
|
|
68167
|
+
}
|
|
68127
68168
|
if (ok)
|
|
68128
68169
|
console.log(
|
|
68129
68170
|
sem ? lang === "fr" ? `\u2713 Rapport valide + gate s\xE9mantique engag\xE9e : ${sem.total} verdict(s), ${sem.grounded} ancr\xE9(s) dans la source${sem.moved ? ` (${sem.moved} d\xE9plac\xE9(s))` : ""}.` : `\u2713 Report valid + semantic gate engaged: ${sem.total} verdict(s), ${sem.grounded} grounded in source${sem.moved ? ` (${sem.moved} moved)` : ""}.` : lang === "fr" ? "\u2713 Rapport valide : sections, crit\xE8res cit\xE9s et justifications NA coh\xE9rents." : "\u2713 Report valid: sections, cited criteria and NA justifications are consistent."
|
|
68130
68171
|
);
|
|
68131
|
-
else for (const i2 of [...res.issues, ...sem?.issues ?? [], ...decided?.issues ?? []]) console.error(`\u2717 ${i2}`);
|
|
68172
|
+
else for (const i2 of [...res.issues, ...sem?.issues ?? [], ...decided?.issues ?? [], ...covered?.issues ?? []]) console.error(`\u2717 ${i2}`);
|
|
68132
68173
|
}
|
|
68133
68174
|
return ok ? 0 : 1;
|
|
68134
68175
|
}
|
package/src/check.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// The canonical WCAG report is keyed by 3-segment success criteria (1.4.3); a pack
|
|
6
6
|
// report by the pack's own 2-segment ids (RGAA 8.3) — the id grammar is per-standard
|
|
7
7
|
// so the version token "WCAG 2.2 —" can never be mistaken for a criterion.
|
|
8
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import type { AuditResult, Lang } from "./types.js";
|
|
11
11
|
import { hasSC } from "./wcag.js";
|
|
@@ -13,6 +13,7 @@ import { type StandardId, CORE, criterionCoverage, isCore, loadPack, hasId, idCa
|
|
|
13
13
|
import { buildWorklist, applyVerdicts, type VerifyItem } from "./verify.js";
|
|
14
14
|
import { groundItems } from "./grounding.js";
|
|
15
15
|
import { isPagesReport } from "./pages-report.js";
|
|
16
|
+
import { PAGES_DIR } from "./snapshot.js";
|
|
16
17
|
|
|
17
18
|
export interface CheckResult {
|
|
18
19
|
ok: boolean;
|
|
@@ -325,6 +326,77 @@ function sectionBody(md: string, n: number): string {
|
|
|
325
326
|
// threshold would hide. An entry states which criterion and why; an entry with no reason
|
|
326
327
|
// fails the gate, because "documented" has to mean something.
|
|
327
328
|
|
|
329
|
+
// ---- the COVERAGE gate ------------------------------------------------------------------
|
|
330
|
+
//
|
|
331
|
+
// `--require-decided` asks whether every criterion carries a verdict. This asks the question one
|
|
332
|
+
// level below it, and it is the one that went unasked for a long time: a sweep that loses pages
|
|
333
|
+
// produces a report that is simply SHORTER, and a shorter deliverable reads exactly like a
|
|
334
|
+
// complete one. Measured on a real run — a hung probe killed two specs, a serial group took
|
|
335
|
+
// fifteen more with it, and the RGAA report shipped with 20 of the 35 declared pages, green,
|
|
336
|
+
// with nothing anywhere naming the missing fifteen or saying that any were missing.
|
|
337
|
+
//
|
|
338
|
+
// A declared page with no capture is not a page that passed. It is a page nobody looked at.
|
|
339
|
+
|
|
340
|
+
/** One declared page that produced no capture. */
|
|
341
|
+
export interface UncapturedPage {
|
|
342
|
+
id: string;
|
|
343
|
+
name: string;
|
|
344
|
+
url: string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface SampleCoverage {
|
|
348
|
+
ok: boolean;
|
|
349
|
+
issues: string[];
|
|
350
|
+
/** Declared pages with no snapshot under `.ultra11y/pages/`. */
|
|
351
|
+
missing: UncapturedPage[];
|
|
352
|
+
declared: number;
|
|
353
|
+
captured: number;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Hold the sample a repository DECLARES against the snapshots a run actually produced.
|
|
357
|
+
*
|
|
358
|
+
* Reads `.ultra11yrc.json` for the declaration and the pages directory for the captures, so it
|
|
359
|
+
* answers for the working tree rather than for an audit JSON that may have been produced
|
|
360
|
+
* elsewhere. A repository that declares no sample has nothing to answer — a gate that fails on
|
|
361
|
+
* its own absence is a gate that gets turned off. */
|
|
362
|
+
export function checkSampleCaptured(root = ".", lang: Lang = "en"): SampleCoverage {
|
|
363
|
+
const fr = lang === "fr";
|
|
364
|
+
let declared: { id: string; name?: string; url?: string }[] = [];
|
|
365
|
+
try {
|
|
366
|
+
const cfg = JSON.parse(readFileSync(join(root, ".ultra11yrc.json"), "utf8")) as {
|
|
367
|
+
sample?: { pages?: { id: string; name?: string; url?: string }[] };
|
|
368
|
+
};
|
|
369
|
+
declared = cfg.sample?.pages ?? [];
|
|
370
|
+
} catch {
|
|
371
|
+
return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
|
|
372
|
+
}
|
|
373
|
+
if (!declared.length) return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
|
|
374
|
+
|
|
375
|
+
let captured = new Set<string>();
|
|
376
|
+
try {
|
|
377
|
+
captured = new Set(
|
|
378
|
+
readdirSync(join(root, PAGES_DIR), { withFileTypes: true })
|
|
379
|
+
.filter((d) => d.isDirectory())
|
|
380
|
+
.map((d) => d.name)
|
|
381
|
+
.filter((id) => existsSync(join(root, PAGES_DIR, id, "dom.html"))),
|
|
382
|
+
);
|
|
383
|
+
} catch {
|
|
384
|
+
/* no pages directory at all — every declared page is missing, which is what we report */
|
|
385
|
+
}
|
|
386
|
+
const missing = declared.filter((p) => !captured.has(p.id)).map((p) => ({ id: p.id, name: p.name ?? p.id, url: p.url ?? "" }));
|
|
387
|
+
|
|
388
|
+
const issues: string[] = [];
|
|
389
|
+
if (missing.length) {
|
|
390
|
+
const kept = declared.length - missing.length;
|
|
391
|
+
issues.push(
|
|
392
|
+
fr
|
|
393
|
+
? `${kept}/${declared.length} page(s) déclarée(s) ont été capturées. Aucune capture pour : ${missing.map((m) => `${m.name} (${m.id})`).join(", ")}. Une page déclarée sans capture n'est pas une page conforme, c'est une page que personne n'a regardée.`
|
|
394
|
+
: `${kept}/${declared.length} declared page(s) were captured. No capture for: ${missing.map((m) => `${m.name} (${m.id})`).join(", ")}. A declared page with no capture is not a page that passed — it is a page nobody looked at.`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
return { ok: issues.length === 0, issues, missing, declared: declared.length, captured: captured.size };
|
|
398
|
+
}
|
|
399
|
+
|
|
328
400
|
/** Numeric-segment order, so 2.1 comes before 10.1. */
|
|
329
401
|
function byCriterionId(a: string, b: string): number {
|
|
330
402
|
const pa = a.split(".").map(Number);
|
package/src/cli.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { buildGraphStreaming } from "./graph/build.js";
|
|
|
39
39
|
import { discover } from "./discover.js";
|
|
40
40
|
import { toPosix, GRAPH_ONLY_EXT } from "./glob.js";
|
|
41
41
|
import { runCriteria, renderCriteriaReference } from "./criteria.js";
|
|
42
|
-
import { checkDecided, checkReport, checkSemantic, isUndecidedFile, type UndecidedFile } from "./check.js";
|
|
42
|
+
import { checkSampleCaptured, checkDecided, checkReport, checkSemantic, isUndecidedFile, type UndecidedFile } from "./check.js";
|
|
43
43
|
import { buildWorklist, writeWorklist, applyVerdicts, VERIFY_MAX, type VerifyItem } from "./verify.js";
|
|
44
44
|
import { groundItems } from "./grounding.js";
|
|
45
45
|
import {
|
|
@@ -397,6 +397,10 @@ Options:
|
|
|
397
397
|
dropped as stale and its criterion says so
|
|
398
398
|
--max-verify <n> verify: cap the worklist size; 0 = no cap (default: 40)
|
|
399
399
|
--verdicts <file> check --semantic: the adjudicated verdicts artifact
|
|
400
|
+
--require-sample check: fail while a page DECLARED in .ultra11yrc.json has no capture under
|
|
401
|
+
.ultra11y/pages/. Coverage, one level below --require-decided: a sweep that
|
|
402
|
+
loses pages produces a report that is merely SHORTER, and a shorter
|
|
403
|
+
deliverable reads exactly like a complete one.
|
|
400
404
|
--require-decided check: fail while ANY criterion of the standard is still « to assess ».
|
|
401
405
|
Needs --in. A green job does not otherwise mean the grid was filled:
|
|
402
406
|
--fail-on governs non-conformities, and an adjudication that lands
|
|
@@ -640,6 +644,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
640
644
|
"semantic",
|
|
641
645
|
// `check`: fail while any criterion of the standard is still « to assess ».
|
|
642
646
|
"require-decided",
|
|
647
|
+
"require-sample",
|
|
643
648
|
"manual",
|
|
644
649
|
// `verify --apply` / `judge --apply`: restore the all-or-nothing fold, where one refused
|
|
645
650
|
// verdict discards the whole adjudication. The default is per-verdict.
|
|
@@ -2487,9 +2492,12 @@ function cmdCheck(p: ParsedArgs): number {
|
|
|
2487
2492
|
// active standard carries a verdict. It therefore needs --in and not --report, and stands on
|
|
2488
2493
|
// its own — a project can demand a complete grid long before it publishes a deliverable.
|
|
2489
2494
|
const requireDecided = p.flags["require-decided"] === true;
|
|
2495
|
+
// Same reasoning for coverage: whether the sweep captured every declared page is a fact
|
|
2496
|
+
// about the working tree, answerable long before there is a deliverable to validate.
|
|
2497
|
+
const requireSample = p.flags["require-sample"] === true;
|
|
2490
2498
|
const rep = p.flags.report;
|
|
2491
2499
|
if (typeof rep !== "string" || !rep) {
|
|
2492
|
-
if (!requireDecided) {
|
|
2500
|
+
if (!requireDecided && !requireSample) {
|
|
2493
2501
|
console.error("ultra11y check: --report <md> is required.");
|
|
2494
2502
|
return 2;
|
|
2495
2503
|
}
|
|
@@ -2539,6 +2547,10 @@ function cmdCheck(p: ParsedArgs): number {
|
|
|
2539
2547
|
}
|
|
2540
2548
|
}
|
|
2541
2549
|
const decided = requireDecided && audit ? checkDecided(audit, standard, lang, { allow }) : null;
|
|
2550
|
+
// COVERAGE, one level below completeness: did the run capture every page it says it audits?
|
|
2551
|
+
// Needs no audit JSON — it holds the repository's declared sample against the snapshots on
|
|
2552
|
+
// disk, which is what a sweep either produced or did not.
|
|
2553
|
+
const covered = requireSample ? checkSampleCaptured(".", lang) : null;
|
|
2542
2554
|
const res = md ? checkReport(md, standard, lang, { audit }) : { ok: true, issues: [] };
|
|
2543
2555
|
// --semantic: the support-level gate ON TOP of the structural check. Fails closed —
|
|
2544
2556
|
// a green exit must always mean the gate engaged (family P0: never green-but-inactive).
|
|
@@ -2552,9 +2564,9 @@ function cmdCheck(p: ParsedArgs): number {
|
|
|
2552
2564
|
lang,
|
|
2553
2565
|
})
|
|
2554
2566
|
: null;
|
|
2555
|
-
const ok = res.ok && (sem === null || sem.ok) && (decided === null || decided.ok);
|
|
2567
|
+
const ok = res.ok && (sem === null || sem.ok) && (decided === null || decided.ok) && (covered === null || covered.ok);
|
|
2556
2568
|
if (p.flags.json) {
|
|
2557
|
-
console.log(JSON.stringify({ ...res, ok, ...(sem ? { semantic: sem } : {}), ...(decided ? { decided } : {}) }, null, 2));
|
|
2569
|
+
console.log(JSON.stringify({ ...res, ok, ...(sem ? { semantic: sem } : {}), ...(decided ? { decided } : {}), ...(covered ? { covered } : {}) }, null, 2));
|
|
2558
2570
|
} else if (!p.flags.quiet) {
|
|
2559
2571
|
if (decided) {
|
|
2560
2572
|
// Say what the gate engaged on, green or red. A completeness gate that reports only its
|
|
@@ -2571,6 +2583,15 @@ function cmdCheck(p: ParsedArgs): number {
|
|
|
2571
2583
|
: `✓ Complete grid: all ${decided.total} criteria carry a verdict${allowedNote}.`,
|
|
2572
2584
|
);
|
|
2573
2585
|
}
|
|
2586
|
+
if (covered?.ok) {
|
|
2587
|
+
// Green or red, say what it engaged on — a gate that reports only its failures cannot be
|
|
2588
|
+
// told apart from one that never ran.
|
|
2589
|
+
console.log(
|
|
2590
|
+
lang === "fr"
|
|
2591
|
+
? `✓ Échantillon couvert : les ${covered.declared} page(s) déclarée(s) ont une capture.`
|
|
2592
|
+
: `✓ Sample covered: all ${covered.declared} declared page(s) have a capture.`,
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2574
2595
|
if (ok)
|
|
2575
2596
|
console.log(
|
|
2576
2597
|
sem
|
|
@@ -2581,7 +2602,7 @@ function cmdCheck(p: ParsedArgs): number {
|
|
|
2581
2602
|
? "✓ Rapport valide : sections, critères cités et justifications NA cohérents."
|
|
2582
2603
|
: "✓ Report valid: sections, cited criteria and NA justifications are consistent.",
|
|
2583
2604
|
);
|
|
2584
|
-
else for (const i of [...res.issues, ...(sem?.issues ?? []), ...(decided?.issues ?? [])]) console.error(`✗ ${i}`);
|
|
2605
|
+
else for (const i of [...res.issues, ...(sem?.issues ?? []), ...(decided?.issues ?? []), ...(covered?.issues ?? [])]) console.error(`✗ ${i}`);
|
|
2585
2606
|
}
|
|
2586
2607
|
return ok ? 0 : 1;
|
|
2587
2608
|
}
|
|
@@ -90,15 +90,27 @@ async function runAxe(page: PlaywrightPage, mode: boolean | "auto" | undefined):
|
|
|
90
90
|
const demanded = mode === true;
|
|
91
91
|
// biome-ignore lint/suspicious/noExplicitAny: the builder's shape is axe's, which is not a dependency here
|
|
92
92
|
let AxeBuilder: any;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
// PROJECT FIRST, then relative to this module — the same order the Playwright fixture above
|
|
94
|
+
// and `scan --runtime local` use, and for the same reason: module-relative alone works for an
|
|
95
|
+
// ordinary `node_modules/ultra11y` install but yields nothing whenever the package is linked,
|
|
96
|
+
// pnpm-hoisted elsewhere, or in a monorepo. Anchoring on the cwd ALONE is just as wrong in the
|
|
97
|
+
// other direction — a suite that changes directory to write its snapshot then resolves nothing
|
|
98
|
+
// at all, and the pass evaporates silently because the default is `auto`.
|
|
99
|
+
let lastError: unknown;
|
|
100
|
+
for (const from of [join(process.cwd(), "package.json"), import.meta.url]) {
|
|
101
|
+
try {
|
|
102
|
+
const mod = createRequire(from)("@axe-core/playwright");
|
|
103
|
+
AxeBuilder = mod.default ?? mod;
|
|
104
|
+
break;
|
|
105
|
+
} catch (e) {
|
|
106
|
+
lastError = e;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!AxeBuilder) {
|
|
98
110
|
if (demanded) {
|
|
99
111
|
console.warn(
|
|
100
|
-
`ultra11y: axe was requested but @axe-core/playwright could not be resolved from ${process.cwd()} — ${
|
|
101
|
-
|
|
112
|
+
`ultra11y: axe was requested but @axe-core/playwright could not be resolved from ${process.cwd()} nor from this package — ${
|
|
113
|
+
lastError instanceof Error ? lastError.message : String(lastError)
|
|
102
114
|
}. Install it, or set \`axe: false\`. The criteria axe decides stay to assess.`,
|
|
103
115
|
);
|
|
104
116
|
}
|
package/src/scan-local.ts
CHANGED
|
@@ -59,14 +59,29 @@ interface LocalDeps {
|
|
|
59
59
|
const PW_SPEC = "@playwright/test";
|
|
60
60
|
const AXE_SPEC = "@axe-core/playwright";
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
*
|
|
62
|
+
/** Is the LOCAL tier actually usable from `cwd`? Drives `runtime: auto`.
|
|
63
|
+
*
|
|
64
|
+
* Both packages must resolve — and the browser BINARY must be on disk. The second half is not
|
|
65
|
+
* belt-and-braces: `npm i @playwright/test` installs the package, `npx playwright install`
|
|
66
|
+
* installs the browsers, and the two are separate steps that CI images routinely do only one
|
|
67
|
+
* of. Resolution alone therefore says "local is available" on a machine that cannot launch
|
|
68
|
+
* anything, and `auto` — whose whole contract is to degrade to Docker when the local tier is
|
|
69
|
+
* not there — instead picks it and dies on the launch.
|
|
70
|
+
*
|
|
71
|
+
* Measured on this repository the day Playwright became a devDependency: three `--runtime
|
|
72
|
+
* local` tests and the action's own crawl job switched themselves on in jobs that install no
|
|
73
|
+
* browser, and failed rather than degrading. `executablePath()` is a pure path computation, so
|
|
74
|
+
* this stays cheap: no launch, no download, one `stat`. */
|
|
64
75
|
export function localAvailable(cwd: string): boolean {
|
|
65
76
|
try {
|
|
66
77
|
const req = createRequire(resolve(cwd, "package.json"));
|
|
67
78
|
req.resolve(PW_SPEC);
|
|
68
79
|
req.resolve(AXE_SPEC);
|
|
69
|
-
|
|
80
|
+
const pw = req(PW_SPEC) as { chromium?: { executablePath?: () => string } };
|
|
81
|
+
const bin = pw.chromium?.executablePath?.();
|
|
82
|
+
// No path at all ⇒ a Playwright that cannot tell us; treat as available and let the launch
|
|
83
|
+
// report its own failure, rather than refusing a tier that might work.
|
|
84
|
+
return typeof bin !== "string" || bin.length === 0 ? true : existsSync(bin);
|
|
70
85
|
} catch {
|
|
71
86
|
return false;
|
|
72
87
|
}
|
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.
|
|
3
|
+
export const VERSION = "5.14.1";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|