ultra11y 5.14.0 → 5.14.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/dist/playwright.mjs +12 -6
- package/package.json +4 -1
- package/scripts/ultra11y.mjs +19 -3
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +19 -3
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +19 -3
- package/src/adjudicate.ts +21 -1
- package/src/integrations/playwright.ts +19 -7
- package/src/scan-local.ts +18 -3
- package/src/types.ts +1 -1
- package/src/util.ts +7 -0
|
@@ -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.14.
|
|
13
|
+
"version": "5.14.2",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
16
16
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.14.
|
|
4
|
+
"version": "5.14.2",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.14.
|
|
4
|
+
"version": "5.14.2",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
// Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
|
|
28
28
|
// does not carry it, so a user's own ultra11y.js is never clobbered.
|
|
29
29
|
const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
|
|
30
|
-
const ULTRA11Y_PLUGIN_VERSION = "5.14.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.14.2";
|
|
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.14.
|
|
3
|
+
"version": "5.14.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",
|
|
@@ -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.14.
|
|
9
|
+
var VERSION = "5.14.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -19918,6 +19918,7 @@ function isUrlPath(file) {
|
|
|
19918
19918
|
return /^https?:\/\//i.test(file);
|
|
19919
19919
|
}
|
|
19920
19920
|
function repoRelative(file, baseDir) {
|
|
19921
|
+
if (typeof file !== "string" || !file) return "";
|
|
19921
19922
|
const posix3 = file.split("\\").join("/").replace(/^\.\//, "");
|
|
19922
19923
|
const base = baseDir.split("\\").join("/").replace(/\/+$/, "");
|
|
19923
19924
|
if (base && posix3.startsWith(`${base}/`)) return posix3.slice(base.length + 1);
|
|
@@ -58072,6 +58073,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
58072
58073
|
} else if (v === "NC") {
|
|
58073
58074
|
if (!it.findings || it.findings.length === 0) blame(it.criteriaId, `criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
|
|
58074
58075
|
for (const f of it.findings ?? []) {
|
|
58076
|
+
if (typeof f.file !== "string" || !f.file.trim()) {
|
|
58077
|
+
blame(
|
|
58078
|
+
it.criteriaId,
|
|
58079
|
+
`criterion ${it.criteriaId}: an NC finding must name the file it was observed in \u2014 nobody can act on a non-conformity with no location`
|
|
58080
|
+
);
|
|
58081
|
+
continue;
|
|
58082
|
+
}
|
|
58075
58083
|
if (!f.normativeRef || !f.normativeRef.trim()) {
|
|
58076
58084
|
blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
|
|
58077
58085
|
} else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
|
|
@@ -58088,7 +58096,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
58088
58096
|
} else {
|
|
58089
58097
|
blame(it.criteriaId, `criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
|
|
58090
58098
|
}
|
|
58091
|
-
for (const rec of it.recommendations ?? [])
|
|
58099
|
+
for (const rec of it.recommendations ?? []) {
|
|
58100
|
+
if (typeof rec.file !== "string" || !rec.file.trim()) {
|
|
58101
|
+
blame(it.criteriaId, `criterion ${it.criteriaId}: a recommendation must name the file it was observed in`);
|
|
58102
|
+
continue;
|
|
58103
|
+
}
|
|
58104
|
+
toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
|
|
58105
|
+
}
|
|
58092
58106
|
}
|
|
58093
58107
|
const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
58094
58108
|
for (const [criteriaId, inputs] of groundInputs) {
|
|
@@ -59694,7 +59708,9 @@ function localAvailable(cwd) {
|
|
|
59694
59708
|
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59695
59709
|
req.resolve(PW_SPEC);
|
|
59696
59710
|
req.resolve(AXE_SPEC);
|
|
59697
|
-
|
|
59711
|
+
const pw = req(PW_SPEC);
|
|
59712
|
+
const bin = pw.chromium?.executablePath?.();
|
|
59713
|
+
return typeof bin !== "string" || bin.length === 0 ? true : existsSync24(bin);
|
|
59698
59714
|
} catch {
|
|
59699
59715
|
return false;
|
|
59700
59716
|
}
|
|
@@ -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.14.
|
|
8
|
+
version: 5.14.2
|
|
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.14.
|
|
9
|
+
var VERSION = "5.14.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -19918,6 +19918,7 @@ function isUrlPath(file) {
|
|
|
19918
19918
|
return /^https?:\/\//i.test(file);
|
|
19919
19919
|
}
|
|
19920
19920
|
function repoRelative(file, baseDir) {
|
|
19921
|
+
if (typeof file !== "string" || !file) return "";
|
|
19921
19922
|
const posix3 = file.split("\\").join("/").replace(/^\.\//, "");
|
|
19922
19923
|
const base = baseDir.split("\\").join("/").replace(/\/+$/, "");
|
|
19923
19924
|
if (base && posix3.startsWith(`${base}/`)) return posix3.slice(base.length + 1);
|
|
@@ -58072,6 +58073,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
58072
58073
|
} else if (v === "NC") {
|
|
58073
58074
|
if (!it.findings || it.findings.length === 0) blame(it.criteriaId, `criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
|
|
58074
58075
|
for (const f of it.findings ?? []) {
|
|
58076
|
+
if (typeof f.file !== "string" || !f.file.trim()) {
|
|
58077
|
+
blame(
|
|
58078
|
+
it.criteriaId,
|
|
58079
|
+
`criterion ${it.criteriaId}: an NC finding must name the file it was observed in \u2014 nobody can act on a non-conformity with no location`
|
|
58080
|
+
);
|
|
58081
|
+
continue;
|
|
58082
|
+
}
|
|
58075
58083
|
if (!f.normativeRef || !f.normativeRef.trim()) {
|
|
58076
58084
|
blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
|
|
58077
58085
|
} else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
|
|
@@ -58088,7 +58096,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
58088
58096
|
} else {
|
|
58089
58097
|
blame(it.criteriaId, `criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
|
|
58090
58098
|
}
|
|
58091
|
-
for (const rec of it.recommendations ?? [])
|
|
58099
|
+
for (const rec of it.recommendations ?? []) {
|
|
58100
|
+
if (typeof rec.file !== "string" || !rec.file.trim()) {
|
|
58101
|
+
blame(it.criteriaId, `criterion ${it.criteriaId}: a recommendation must name the file it was observed in`);
|
|
58102
|
+
continue;
|
|
58103
|
+
}
|
|
58104
|
+
toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
|
|
58105
|
+
}
|
|
58092
58106
|
}
|
|
58093
58107
|
const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
58094
58108
|
for (const [criteriaId, inputs] of groundInputs) {
|
|
@@ -59694,7 +59708,9 @@ function localAvailable(cwd) {
|
|
|
59694
59708
|
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59695
59709
|
req.resolve(PW_SPEC);
|
|
59696
59710
|
req.resolve(AXE_SPEC);
|
|
59697
|
-
|
|
59711
|
+
const pw = req(PW_SPEC);
|
|
59712
|
+
const bin = pw.chromium?.executablePath?.();
|
|
59713
|
+
return typeof bin !== "string" || bin.length === 0 ? true : existsSync24(bin);
|
|
59698
59714
|
} catch {
|
|
59699
59715
|
return false;
|
|
59700
59716
|
}
|
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.14.
|
|
7
|
+
version: 5.14.2
|
|
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.14.
|
|
9
|
+
var VERSION = "5.14.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -19918,6 +19918,7 @@ function isUrlPath(file) {
|
|
|
19918
19918
|
return /^https?:\/\//i.test(file);
|
|
19919
19919
|
}
|
|
19920
19920
|
function repoRelative(file, baseDir) {
|
|
19921
|
+
if (typeof file !== "string" || !file) return "";
|
|
19921
19922
|
const posix3 = file.split("\\").join("/").replace(/^\.\//, "");
|
|
19922
19923
|
const base = baseDir.split("\\").join("/").replace(/\/+$/, "");
|
|
19923
19924
|
if (base && posix3.startsWith(`${base}/`)) return posix3.slice(base.length + 1);
|
|
@@ -58072,6 +58073,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
58072
58073
|
} else if (v === "NC") {
|
|
58073
58074
|
if (!it.findings || it.findings.length === 0) blame(it.criteriaId, `criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
|
|
58074
58075
|
for (const f of it.findings ?? []) {
|
|
58076
|
+
if (typeof f.file !== "string" || !f.file.trim()) {
|
|
58077
|
+
blame(
|
|
58078
|
+
it.criteriaId,
|
|
58079
|
+
`criterion ${it.criteriaId}: an NC finding must name the file it was observed in \u2014 nobody can act on a non-conformity with no location`
|
|
58080
|
+
);
|
|
58081
|
+
continue;
|
|
58082
|
+
}
|
|
58075
58083
|
if (!f.normativeRef || !f.normativeRef.trim()) {
|
|
58076
58084
|
blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
|
|
58077
58085
|
} else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
|
|
@@ -58088,7 +58096,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
58088
58096
|
} else {
|
|
58089
58097
|
blame(it.criteriaId, `criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
|
|
58090
58098
|
}
|
|
58091
|
-
for (const rec of it.recommendations ?? [])
|
|
58099
|
+
for (const rec of it.recommendations ?? []) {
|
|
58100
|
+
if (typeof rec.file !== "string" || !rec.file.trim()) {
|
|
58101
|
+
blame(it.criteriaId, `criterion ${it.criteriaId}: a recommendation must name the file it was observed in`);
|
|
58102
|
+
continue;
|
|
58103
|
+
}
|
|
58104
|
+
toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
|
|
58105
|
+
}
|
|
58092
58106
|
}
|
|
58093
58107
|
const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
58094
58108
|
for (const [criteriaId, inputs] of groundInputs) {
|
|
@@ -59694,7 +59708,9 @@ function localAvailable(cwd) {
|
|
|
59694
59708
|
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59695
59709
|
req.resolve(PW_SPEC);
|
|
59696
59710
|
req.resolve(AXE_SPEC);
|
|
59697
|
-
|
|
59711
|
+
const pw = req(PW_SPEC);
|
|
59712
|
+
const bin = pw.chromium?.executablePath?.();
|
|
59713
|
+
return typeof bin !== "string" || bin.length === 0 ? true : existsSync24(bin);
|
|
59698
59714
|
} catch {
|
|
59699
59715
|
return false;
|
|
59700
59716
|
}
|
package/src/adjudicate.ts
CHANGED
|
@@ -879,6 +879,18 @@ export function applyAdjudication(
|
|
|
879
879
|
} else if (v === "NC") {
|
|
880
880
|
if (!it.findings || it.findings.length === 0) blame(it.criteriaId, `criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
|
|
881
881
|
for (const f of it.findings ?? []) {
|
|
882
|
+
// AN NC NOBODY CAN OPEN IS NOT A FINDING. Checked before the normativeRef, because a
|
|
883
|
+
// finding with no file cannot be grounded at all — there is nothing to go and read.
|
|
884
|
+
// Measured once: a model returned an NC with no `file`, the fold minted it anyway, and
|
|
885
|
+
// it reached `repoRelative`, which crashed on `undefined.split` and took SARIF, the
|
|
886
|
+
// annotations, the comment, the report, the HTML and the artifact upload with it.
|
|
887
|
+
if (typeof f.file !== "string" || !f.file.trim()) {
|
|
888
|
+
blame(
|
|
889
|
+
it.criteriaId,
|
|
890
|
+
`criterion ${it.criteriaId}: an NC finding must name the file it was observed in — nobody can act on a non-conformity with no location`,
|
|
891
|
+
);
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
882
894
|
// FAIL-CLOSED: every NC finding must cite a precise, resolvable test of the active
|
|
883
895
|
// standard. A good practice with no normative test is a recommendation, not an NC.
|
|
884
896
|
if (!f.normativeRef || !f.normativeRef.trim()) {
|
|
@@ -905,7 +917,15 @@ export function applyAdjudication(
|
|
|
905
917
|
// Recommendations are independent of the verdict (a C criterion may still carry a good
|
|
906
918
|
// practice) and are grounded exactly like an NC finding — no normativeRef required, as
|
|
907
919
|
// a recommendation has no normative test by definition.
|
|
908
|
-
for (const rec of it.recommendations ?? [])
|
|
920
|
+
for (const rec of it.recommendations ?? []) {
|
|
921
|
+
// Same contract, same reason: a recommendation is rendered through the very surfaces an
|
|
922
|
+
// NC is, so one with no location breaks them just as thoroughly.
|
|
923
|
+
if (typeof rec.file !== "string" || !rec.file.trim()) {
|
|
924
|
+
blame(it.criteriaId, `criterion ${it.criteriaId}: a recommendation must name the file it was observed in`);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
|
|
928
|
+
}
|
|
909
929
|
}
|
|
910
930
|
|
|
911
931
|
// Content-level grounding of every agent NC finding, every C/NA citation, and every
|
|
@@ -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.14.
|
|
3
|
+
export const VERSION = "5.14.2";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|
package/src/util.ts
CHANGED
|
@@ -36,6 +36,13 @@ export function isUrlPath(file: string): boolean {
|
|
|
36
36
|
* under `baseDir`; leave it as-is when it does not, rather than inventing a path that
|
|
37
37
|
* resolves nowhere. */
|
|
38
38
|
export function repoRelative(file: string, baseDir: string): string {
|
|
39
|
+
// A finding with no file at all. It should never reach here — the adjudication fold refuses
|
|
40
|
+
// an NC that names none — but an AuditResult can also be hand-written or produced by an
|
|
41
|
+
// external tool's adapter, and neither goes through that gate. Measured once: one such
|
|
42
|
+
// finding threw here and took SARIF, the annotations, the pull-request comment, the report,
|
|
43
|
+
// the HTML and the artifact upload with it, because every one of those steps runs after.
|
|
44
|
+
// A rendering surface that explodes on bad input tells its reader nothing at all.
|
|
45
|
+
if (typeof file !== "string" || !file) return "";
|
|
39
46
|
const posix = file.split("\\").join("/").replace(/^\.\//, "");
|
|
40
47
|
const base = baseDir.split("\\").join("/").replace(/\/+$/, "");
|
|
41
48
|
if (base && posix.startsWith(`${base}/`)) return posix.slice(base.length + 1);
|