ultra11y 5.26.0 → 5.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +49 -9
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +49 -9
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +49 -9
- package/src/adjudicate-subjects.ts +62 -14
- package/src/standards/derive.ts +33 -1
- 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.27.0",
|
|
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.27.0",
|
|
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.27.0",
|
|
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.27.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.27.0",
|
|
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 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.27.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -45061,14 +45061,42 @@ var SUBJECTS = {
|
|
|
45061
45061
|
(e) => h(d, e, `<${e.tag} autoplay=${attr(e, "autoplay") !== void 0} controls=${attr(e, "controls") !== void 0}>`, `media|${e.tag}`)
|
|
45062
45062
|
)
|
|
45063
45063
|
]),
|
|
45064
|
-
// A change of context
|
|
45065
|
-
//
|
|
45066
|
-
//
|
|
45067
|
-
//
|
|
45068
|
-
//
|
|
45069
|
-
//
|
|
45070
|
-
//
|
|
45064
|
+
// A change of context initiated by a script. RGAA 7.4 / WCAG 3.2.1-3.2.2.
|
|
45065
|
+
//
|
|
45066
|
+
// THE SUBJECT IS THE SCRIPT, and getting that wrong cost a release. This used to be a list of
|
|
45067
|
+
// CONSEQUENCES — onFocus/onBlur/onChange, `.submit(`, `location`, `router.push`,
|
|
45068
|
+
// `window.open` — on the theory that a context change is one of them. It is, but the list can
|
|
45069
|
+
// only ever be the ways we have already seen a script change context, and a criterion whose
|
|
45070
|
+
// subject is « each script » cannot be answered from a catalogue of endings.
|
|
45071
|
+
//
|
|
45072
|
+
// Measured on tests/fixtures/realworld, which ships LoginForm.tsx whose form carries
|
|
45073
|
+
// `onSubmit={(e) => { e.preventDefault(); … }}`: 7.4 reached the adjudicator with population
|
|
45074
|
+
// ZERO. A script was right there, doing the thing the criterion asks about, and the harvest
|
|
45075
|
+
// walked past it because `onSubmit` was not on the list. The adjudicator was told « no
|
|
45076
|
+
// evidence » and billed to guess between NA and manual — on a page it should have been handed
|
|
45077
|
+
// twenty lines of React to read.
|
|
45078
|
+
//
|
|
45079
|
+
// So it collects the script FIRST: any `<script>`, any `on*` binding whatever the event, and
|
|
45080
|
+
// then the consequences it always looked for, which stay because they are what an adjudicator
|
|
45081
|
+
// most needs pointed out. Two things follow. A page with a script always arrives with it, so
|
|
45082
|
+
// the verdict is made on the code rather than on its absence. And a page with no script at all
|
|
45083
|
+
// harvests nothing — which is now a FACT about the code, not a gap in a pattern list, so 7.4
|
|
45084
|
+
// and 3.2.1/3.2.2 close as « not applicable » with no model in the loop (EXISTENCE_SUBJECTS).
|
|
45071
45085
|
contextChange: (docs) => docs.flatMap((d) => [
|
|
45086
|
+
// The script itself — the subject, before anything it goes on to do.
|
|
45087
|
+
...elementsByTag(d, "script").map(
|
|
45088
|
+
(e) => h(
|
|
45089
|
+
d,
|
|
45090
|
+
e,
|
|
45091
|
+
`<script${attr(e, "src") ? ` src="${attr(e, "src")}"` : ""}> \u2014 does anything it runs change context, and is the user warned or in control?`,
|
|
45092
|
+
`script|${attr(e, "src") ?? "inline"}`
|
|
45093
|
+
)
|
|
45094
|
+
),
|
|
45095
|
+
// Any handler binding, whatever the event: `onSubmit` and `onClick` change context as
|
|
45096
|
+
// readily as `onChange`, and a criterion about scripts has no business enumerating events.
|
|
45097
|
+
...linesOf(d, /\bon[A-Z][A-Za-z]*\s*=|\son[a-z]+\s*=\s*["']/).map(
|
|
45098
|
+
(l) => hAt(d, l.line, "handler", `handler \u2014 does it change context (navigate, submit, open a window, move focus)? ${l.text}`, `handler|${l.text}`)
|
|
45099
|
+
),
|
|
45072
45100
|
...linesOf(d, /\bon(?:Focus|Blur)\s*=/).map(
|
|
45073
45101
|
(l) => hAt(d, l.line, "handler", `focus handler \u2014 does it change context (navigate, submit, move focus)? ${l.text}`, `onfocus|${l.text}`)
|
|
45074
45102
|
),
|
|
@@ -45437,9 +45465,11 @@ var EXISTENCE_SUBJECTS = /* @__PURE__ */ new Set([
|
|
|
45437
45465
|
"errors",
|
|
45438
45466
|
"frames",
|
|
45439
45467
|
"downloadDocs",
|
|
45440
|
-
"newWindow"
|
|
45468
|
+
"newWindow",
|
|
45469
|
+
"contextChange"
|
|
45441
45470
|
]);
|
|
45442
45471
|
var subjectsForSc = (sc) => SC_SUBJECTS[sc] ?? [];
|
|
45472
|
+
var ownPackSubjects = (standard, id) => PACK_SUBJECTS[standard]?.[id];
|
|
45443
45473
|
function subjectsForPackCriterion(standard, id, scs) {
|
|
45444
45474
|
const own = PACK_SUBJECTS[standard]?.[id];
|
|
45445
45475
|
if (own) return own;
|
|
@@ -54408,6 +54438,13 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54408
54438
|
const overrides = pack.overrides;
|
|
54409
54439
|
const seen = audit2.scope.subjectsSeen;
|
|
54410
54440
|
const subjectAbsent = (pc) => seen !== void 0 && subjectsAbsent(subjectsForPackCriterion(packKey, pc.id, pc.wcag), new Set(seen));
|
|
54441
|
+
const ownSubjectSeen = (pc) => {
|
|
54442
|
+
if (seen === void 0) return false;
|
|
54443
|
+
const own = ownPackSubjects(packKey, pc.id);
|
|
54444
|
+
if (!own?.length) return false;
|
|
54445
|
+
const present = new Set(seen);
|
|
54446
|
+
return own.some((id) => present.has(id));
|
|
54447
|
+
};
|
|
54411
54448
|
const ownRuleIds = new Set((pack.rules ?? []).map((r) => `pack:${packKey}:${r.id}`));
|
|
54412
54449
|
const deriveBase = (pc) => {
|
|
54413
54450
|
const outOfScope = pc.wcag.every((sc) => {
|
|
@@ -54444,6 +54481,9 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54444
54481
|
return { id: pc.id, theme: pc.theme, status: INAPPLICABLE_STATUS, findings, scs: pc.wcag, inapplicable: true };
|
|
54445
54482
|
}
|
|
54446
54483
|
const inapplicable = status === INAPPLICABLE_STATUS && (scResults.length === 0 || scResults.every((r) => r.inapplicable));
|
|
54484
|
+
if (inapplicable && ownSubjectSeen(pc)) {
|
|
54485
|
+
return { id: pc.id, theme: pc.theme, status: "manual", findings, scs: pc.wcag };
|
|
54486
|
+
}
|
|
54447
54487
|
return { id: pc.id, theme: pc.theme, status, findings, scs: pc.wcag, ...inapplicable ? { inapplicable: true } : {} };
|
|
54448
54488
|
};
|
|
54449
54489
|
const enabledSecondary = (pack.secondaryMappings ?? []).filter((m) => m.enabled === true);
|
|
@@ -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.27.0
|
|
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.27.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -45061,14 +45061,42 @@ var SUBJECTS = {
|
|
|
45061
45061
|
(e) => h(d, e, `<${e.tag} autoplay=${attr(e, "autoplay") !== void 0} controls=${attr(e, "controls") !== void 0}>`, `media|${e.tag}`)
|
|
45062
45062
|
)
|
|
45063
45063
|
]),
|
|
45064
|
-
// A change of context
|
|
45065
|
-
//
|
|
45066
|
-
//
|
|
45067
|
-
//
|
|
45068
|
-
//
|
|
45069
|
-
//
|
|
45070
|
-
//
|
|
45064
|
+
// A change of context initiated by a script. RGAA 7.4 / WCAG 3.2.1-3.2.2.
|
|
45065
|
+
//
|
|
45066
|
+
// THE SUBJECT IS THE SCRIPT, and getting that wrong cost a release. This used to be a list of
|
|
45067
|
+
// CONSEQUENCES — onFocus/onBlur/onChange, `.submit(`, `location`, `router.push`,
|
|
45068
|
+
// `window.open` — on the theory that a context change is one of them. It is, but the list can
|
|
45069
|
+
// only ever be the ways we have already seen a script change context, and a criterion whose
|
|
45070
|
+
// subject is « each script » cannot be answered from a catalogue of endings.
|
|
45071
|
+
//
|
|
45072
|
+
// Measured on tests/fixtures/realworld, which ships LoginForm.tsx whose form carries
|
|
45073
|
+
// `onSubmit={(e) => { e.preventDefault(); … }}`: 7.4 reached the adjudicator with population
|
|
45074
|
+
// ZERO. A script was right there, doing the thing the criterion asks about, and the harvest
|
|
45075
|
+
// walked past it because `onSubmit` was not on the list. The adjudicator was told « no
|
|
45076
|
+
// evidence » and billed to guess between NA and manual — on a page it should have been handed
|
|
45077
|
+
// twenty lines of React to read.
|
|
45078
|
+
//
|
|
45079
|
+
// So it collects the script FIRST: any `<script>`, any `on*` binding whatever the event, and
|
|
45080
|
+
// then the consequences it always looked for, which stay because they are what an adjudicator
|
|
45081
|
+
// most needs pointed out. Two things follow. A page with a script always arrives with it, so
|
|
45082
|
+
// the verdict is made on the code rather than on its absence. And a page with no script at all
|
|
45083
|
+
// harvests nothing — which is now a FACT about the code, not a gap in a pattern list, so 7.4
|
|
45084
|
+
// and 3.2.1/3.2.2 close as « not applicable » with no model in the loop (EXISTENCE_SUBJECTS).
|
|
45071
45085
|
contextChange: (docs) => docs.flatMap((d) => [
|
|
45086
|
+
// The script itself — the subject, before anything it goes on to do.
|
|
45087
|
+
...elementsByTag(d, "script").map(
|
|
45088
|
+
(e) => h(
|
|
45089
|
+
d,
|
|
45090
|
+
e,
|
|
45091
|
+
`<script${attr(e, "src") ? ` src="${attr(e, "src")}"` : ""}> \u2014 does anything it runs change context, and is the user warned or in control?`,
|
|
45092
|
+
`script|${attr(e, "src") ?? "inline"}`
|
|
45093
|
+
)
|
|
45094
|
+
),
|
|
45095
|
+
// Any handler binding, whatever the event: `onSubmit` and `onClick` change context as
|
|
45096
|
+
// readily as `onChange`, and a criterion about scripts has no business enumerating events.
|
|
45097
|
+
...linesOf(d, /\bon[A-Z][A-Za-z]*\s*=|\son[a-z]+\s*=\s*["']/).map(
|
|
45098
|
+
(l) => hAt(d, l.line, "handler", `handler \u2014 does it change context (navigate, submit, open a window, move focus)? ${l.text}`, `handler|${l.text}`)
|
|
45099
|
+
),
|
|
45072
45100
|
...linesOf(d, /\bon(?:Focus|Blur)\s*=/).map(
|
|
45073
45101
|
(l) => hAt(d, l.line, "handler", `focus handler \u2014 does it change context (navigate, submit, move focus)? ${l.text}`, `onfocus|${l.text}`)
|
|
45074
45102
|
),
|
|
@@ -45437,9 +45465,11 @@ var EXISTENCE_SUBJECTS = /* @__PURE__ */ new Set([
|
|
|
45437
45465
|
"errors",
|
|
45438
45466
|
"frames",
|
|
45439
45467
|
"downloadDocs",
|
|
45440
|
-
"newWindow"
|
|
45468
|
+
"newWindow",
|
|
45469
|
+
"contextChange"
|
|
45441
45470
|
]);
|
|
45442
45471
|
var subjectsForSc = (sc) => SC_SUBJECTS[sc] ?? [];
|
|
45472
|
+
var ownPackSubjects = (standard, id) => PACK_SUBJECTS[standard]?.[id];
|
|
45443
45473
|
function subjectsForPackCriterion(standard, id, scs) {
|
|
45444
45474
|
const own = PACK_SUBJECTS[standard]?.[id];
|
|
45445
45475
|
if (own) return own;
|
|
@@ -54408,6 +54438,13 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54408
54438
|
const overrides = pack.overrides;
|
|
54409
54439
|
const seen = audit2.scope.subjectsSeen;
|
|
54410
54440
|
const subjectAbsent = (pc) => seen !== void 0 && subjectsAbsent(subjectsForPackCriterion(packKey, pc.id, pc.wcag), new Set(seen));
|
|
54441
|
+
const ownSubjectSeen = (pc) => {
|
|
54442
|
+
if (seen === void 0) return false;
|
|
54443
|
+
const own = ownPackSubjects(packKey, pc.id);
|
|
54444
|
+
if (!own?.length) return false;
|
|
54445
|
+
const present = new Set(seen);
|
|
54446
|
+
return own.some((id) => present.has(id));
|
|
54447
|
+
};
|
|
54411
54448
|
const ownRuleIds = new Set((pack.rules ?? []).map((r) => `pack:${packKey}:${r.id}`));
|
|
54412
54449
|
const deriveBase = (pc) => {
|
|
54413
54450
|
const outOfScope = pc.wcag.every((sc) => {
|
|
@@ -54444,6 +54481,9 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54444
54481
|
return { id: pc.id, theme: pc.theme, status: INAPPLICABLE_STATUS, findings, scs: pc.wcag, inapplicable: true };
|
|
54445
54482
|
}
|
|
54446
54483
|
const inapplicable = status === INAPPLICABLE_STATUS && (scResults.length === 0 || scResults.every((r) => r.inapplicable));
|
|
54484
|
+
if (inapplicable && ownSubjectSeen(pc)) {
|
|
54485
|
+
return { id: pc.id, theme: pc.theme, status: "manual", findings, scs: pc.wcag };
|
|
54486
|
+
}
|
|
54447
54487
|
return { id: pc.id, theme: pc.theme, status, findings, scs: pc.wcag, ...inapplicable ? { inapplicable: true } : {} };
|
|
54448
54488
|
};
|
|
54449
54489
|
const enabledSecondary = (pack.secondaryMappings ?? []).filter((m) => m.enabled === true);
|
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.27.0
|
|
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.27.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -45061,14 +45061,42 @@ var SUBJECTS = {
|
|
|
45061
45061
|
(e) => h(d, e, `<${e.tag} autoplay=${attr(e, "autoplay") !== void 0} controls=${attr(e, "controls") !== void 0}>`, `media|${e.tag}`)
|
|
45062
45062
|
)
|
|
45063
45063
|
]),
|
|
45064
|
-
// A change of context
|
|
45065
|
-
//
|
|
45066
|
-
//
|
|
45067
|
-
//
|
|
45068
|
-
//
|
|
45069
|
-
//
|
|
45070
|
-
//
|
|
45064
|
+
// A change of context initiated by a script. RGAA 7.4 / WCAG 3.2.1-3.2.2.
|
|
45065
|
+
//
|
|
45066
|
+
// THE SUBJECT IS THE SCRIPT, and getting that wrong cost a release. This used to be a list of
|
|
45067
|
+
// CONSEQUENCES — onFocus/onBlur/onChange, `.submit(`, `location`, `router.push`,
|
|
45068
|
+
// `window.open` — on the theory that a context change is one of them. It is, but the list can
|
|
45069
|
+
// only ever be the ways we have already seen a script change context, and a criterion whose
|
|
45070
|
+
// subject is « each script » cannot be answered from a catalogue of endings.
|
|
45071
|
+
//
|
|
45072
|
+
// Measured on tests/fixtures/realworld, which ships LoginForm.tsx whose form carries
|
|
45073
|
+
// `onSubmit={(e) => { e.preventDefault(); … }}`: 7.4 reached the adjudicator with population
|
|
45074
|
+
// ZERO. A script was right there, doing the thing the criterion asks about, and the harvest
|
|
45075
|
+
// walked past it because `onSubmit` was not on the list. The adjudicator was told « no
|
|
45076
|
+
// evidence » and billed to guess between NA and manual — on a page it should have been handed
|
|
45077
|
+
// twenty lines of React to read.
|
|
45078
|
+
//
|
|
45079
|
+
// So it collects the script FIRST: any `<script>`, any `on*` binding whatever the event, and
|
|
45080
|
+
// then the consequences it always looked for, which stay because they are what an adjudicator
|
|
45081
|
+
// most needs pointed out. Two things follow. A page with a script always arrives with it, so
|
|
45082
|
+
// the verdict is made on the code rather than on its absence. And a page with no script at all
|
|
45083
|
+
// harvests nothing — which is now a FACT about the code, not a gap in a pattern list, so 7.4
|
|
45084
|
+
// and 3.2.1/3.2.2 close as « not applicable » with no model in the loop (EXISTENCE_SUBJECTS).
|
|
45071
45085
|
contextChange: (docs) => docs.flatMap((d) => [
|
|
45086
|
+
// The script itself — the subject, before anything it goes on to do.
|
|
45087
|
+
...elementsByTag(d, "script").map(
|
|
45088
|
+
(e) => h(
|
|
45089
|
+
d,
|
|
45090
|
+
e,
|
|
45091
|
+
`<script${attr(e, "src") ? ` src="${attr(e, "src")}"` : ""}> \u2014 does anything it runs change context, and is the user warned or in control?`,
|
|
45092
|
+
`script|${attr(e, "src") ?? "inline"}`
|
|
45093
|
+
)
|
|
45094
|
+
),
|
|
45095
|
+
// Any handler binding, whatever the event: `onSubmit` and `onClick` change context as
|
|
45096
|
+
// readily as `onChange`, and a criterion about scripts has no business enumerating events.
|
|
45097
|
+
...linesOf(d, /\bon[A-Z][A-Za-z]*\s*=|\son[a-z]+\s*=\s*["']/).map(
|
|
45098
|
+
(l) => hAt(d, l.line, "handler", `handler \u2014 does it change context (navigate, submit, open a window, move focus)? ${l.text}`, `handler|${l.text}`)
|
|
45099
|
+
),
|
|
45072
45100
|
...linesOf(d, /\bon(?:Focus|Blur)\s*=/).map(
|
|
45073
45101
|
(l) => hAt(d, l.line, "handler", `focus handler \u2014 does it change context (navigate, submit, move focus)? ${l.text}`, `onfocus|${l.text}`)
|
|
45074
45102
|
),
|
|
@@ -45437,9 +45465,11 @@ var EXISTENCE_SUBJECTS = /* @__PURE__ */ new Set([
|
|
|
45437
45465
|
"errors",
|
|
45438
45466
|
"frames",
|
|
45439
45467
|
"downloadDocs",
|
|
45440
|
-
"newWindow"
|
|
45468
|
+
"newWindow",
|
|
45469
|
+
"contextChange"
|
|
45441
45470
|
]);
|
|
45442
45471
|
var subjectsForSc = (sc) => SC_SUBJECTS[sc] ?? [];
|
|
45472
|
+
var ownPackSubjects = (standard, id) => PACK_SUBJECTS[standard]?.[id];
|
|
45443
45473
|
function subjectsForPackCriterion(standard, id, scs) {
|
|
45444
45474
|
const own = PACK_SUBJECTS[standard]?.[id];
|
|
45445
45475
|
if (own) return own;
|
|
@@ -54408,6 +54438,13 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54408
54438
|
const overrides = pack.overrides;
|
|
54409
54439
|
const seen = audit2.scope.subjectsSeen;
|
|
54410
54440
|
const subjectAbsent = (pc) => seen !== void 0 && subjectsAbsent(subjectsForPackCriterion(packKey, pc.id, pc.wcag), new Set(seen));
|
|
54441
|
+
const ownSubjectSeen = (pc) => {
|
|
54442
|
+
if (seen === void 0) return false;
|
|
54443
|
+
const own = ownPackSubjects(packKey, pc.id);
|
|
54444
|
+
if (!own?.length) return false;
|
|
54445
|
+
const present = new Set(seen);
|
|
54446
|
+
return own.some((id) => present.has(id));
|
|
54447
|
+
};
|
|
54411
54448
|
const ownRuleIds = new Set((pack.rules ?? []).map((r) => `pack:${packKey}:${r.id}`));
|
|
54412
54449
|
const deriveBase = (pc) => {
|
|
54413
54450
|
const outOfScope = pc.wcag.every((sc) => {
|
|
@@ -54444,6 +54481,9 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54444
54481
|
return { id: pc.id, theme: pc.theme, status: INAPPLICABLE_STATUS, findings, scs: pc.wcag, inapplicable: true };
|
|
54445
54482
|
}
|
|
54446
54483
|
const inapplicable = status === INAPPLICABLE_STATUS && (scResults.length === 0 || scResults.every((r) => r.inapplicable));
|
|
54484
|
+
if (inapplicable && ownSubjectSeen(pc)) {
|
|
54485
|
+
return { id: pc.id, theme: pc.theme, status: "manual", findings, scs: pc.wcag };
|
|
54486
|
+
}
|
|
54447
54487
|
return { id: pc.id, theme: pc.theme, status, findings, scs: pc.wcag, ...inapplicable ? { inapplicable: true } : {} };
|
|
54448
54488
|
};
|
|
54449
54489
|
const enabledSecondary = (pack.secondaryMappings ?? []).filter((m) => m.enabled === true);
|
|
@@ -706,15 +706,43 @@ export const SUBJECTS: Record<string, Subject> = {
|
|
|
706
706
|
),
|
|
707
707
|
]),
|
|
708
708
|
|
|
709
|
-
// A change of context
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
709
|
+
// A change of context initiated by a script. RGAA 7.4 / WCAG 3.2.1-3.2.2.
|
|
710
|
+
//
|
|
711
|
+
// THE SUBJECT IS THE SCRIPT, and getting that wrong cost a release. This used to be a list of
|
|
712
|
+
// CONSEQUENCES — onFocus/onBlur/onChange, `.submit(`, `location`, `router.push`,
|
|
713
|
+
// `window.open` — on the theory that a context change is one of them. It is, but the list can
|
|
714
|
+
// only ever be the ways we have already seen a script change context, and a criterion whose
|
|
715
|
+
// subject is « each script » cannot be answered from a catalogue of endings.
|
|
716
|
+
//
|
|
717
|
+
// Measured on tests/fixtures/realworld, which ships LoginForm.tsx whose form carries
|
|
718
|
+
// `onSubmit={(e) => { e.preventDefault(); … }}`: 7.4 reached the adjudicator with population
|
|
719
|
+
// ZERO. A script was right there, doing the thing the criterion asks about, and the harvest
|
|
720
|
+
// walked past it because `onSubmit` was not on the list. The adjudicator was told « no
|
|
721
|
+
// evidence » and billed to guess between NA and manual — on a page it should have been handed
|
|
722
|
+
// twenty lines of React to read.
|
|
723
|
+
//
|
|
724
|
+
// So it collects the script FIRST: any `<script>`, any `on*` binding whatever the event, and
|
|
725
|
+
// then the consequences it always looked for, which stay because they are what an adjudicator
|
|
726
|
+
// most needs pointed out. Two things follow. A page with a script always arrives with it, so
|
|
727
|
+
// the verdict is made on the code rather than on its absence. And a page with no script at all
|
|
728
|
+
// harvests nothing — which is now a FACT about the code, not a gap in a pattern list, so 7.4
|
|
729
|
+
// and 3.2.1/3.2.2 close as « not applicable » with no model in the loop (EXISTENCE_SUBJECTS).
|
|
716
730
|
contextChange: (docs) =>
|
|
717
731
|
docs.flatMap((d) => [
|
|
732
|
+
// The script itself — the subject, before anything it goes on to do.
|
|
733
|
+
...elementsByTag(d, "script").map((e) =>
|
|
734
|
+
h(
|
|
735
|
+
d,
|
|
736
|
+
e,
|
|
737
|
+
`<script${attr(e, "src") ? ` src="${attr(e, "src")}"` : ""}> — does anything it runs change context, and is the user warned or in control?`,
|
|
738
|
+
`script|${attr(e, "src") ?? "inline"}`,
|
|
739
|
+
),
|
|
740
|
+
),
|
|
741
|
+
// Any handler binding, whatever the event: `onSubmit` and `onClick` change context as
|
|
742
|
+
// readily as `onChange`, and a criterion about scripts has no business enumerating events.
|
|
743
|
+
...linesOf(d, /\bon[A-Z][A-Za-z]*\s*=|\son[a-z]+\s*=\s*["']/).map((l) =>
|
|
744
|
+
hAt(d, l.line, "handler", `handler — does it change context (navigate, submit, open a window, move focus)? ${l.text}`, `handler|${l.text}`),
|
|
745
|
+
),
|
|
718
746
|
...linesOf(d, /\bon(?:Focus|Blur)\s*=/).map((l) =>
|
|
719
747
|
hAt(d, l.line, "handler", `focus handler — does it change context (navigate, submit, move focus)? ${l.text}`, `onfocus|${l.text}`),
|
|
720
748
|
),
|
|
@@ -1200,13 +1228,22 @@ export const PACK_SUBJECTS: Record<string, Record<string, string[]>> = {
|
|
|
1200
1228
|
// this comment was written from, both were ruled NA on the first pass with no gate refusal,
|
|
1201
1229
|
// which is the definition of a paid turn that decided nothing.
|
|
1202
1230
|
//
|
|
1203
|
-
//
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1206
|
-
//
|
|
1207
|
-
//
|
|
1208
|
-
//
|
|
1209
|
-
//
|
|
1231
|
+
// `contextChange` is here too NOW, and it was not before — the earlier refusal was right about
|
|
1232
|
+
// the rule and wrong about the subject, so it is worth writing down which changed.
|
|
1233
|
+
//
|
|
1234
|
+
// It used to enumerate CONSEQUENCES: `.submit(`, `location.href`, `router.push`, a handful of
|
|
1235
|
+
// handler attributes. Read that way it is a heuristic, its silence is what a failing page looks
|
|
1236
|
+
// like, and admitting it would have bought a cheap NA at the price of a hidden non-conformity.
|
|
1237
|
+
// That refusal stood, and it also let a real defect stand: on tests/fixtures/realworld, 7.4
|
|
1238
|
+
// arrived with population ZERO from a page shipping `onSubmit={(e) => …}` — a script the harvest
|
|
1239
|
+
// simply did not look for.
|
|
1240
|
+
//
|
|
1241
|
+
// The subject is the SCRIPT now, not the endings: any `<script>`, any `on*` binding. RGAA 7.4
|
|
1242
|
+
// asks about « chaque script qui initie un changement de contexte », so a page that FAILS it runs
|
|
1243
|
+
// a script, and a scope with no script anywhere has nothing for the criterion to be about. That
|
|
1244
|
+
// is an element species, and its silence is a fact a reader can check — the same test
|
|
1245
|
+
// `downloadDocs` and `newWindow` pass. The heuristics stay in the harvest as POINTERS for
|
|
1246
|
+
// whoever reads it; they are no longer what decides whether the subject exists.
|
|
1210
1247
|
export const EXISTENCE_SUBJECTS: ReadonlySet<string> = new Set([
|
|
1211
1248
|
"images",
|
|
1212
1249
|
"tables",
|
|
@@ -1218,6 +1255,7 @@ export const EXISTENCE_SUBJECTS: ReadonlySet<string> = new Set([
|
|
|
1218
1255
|
"frames",
|
|
1219
1256
|
"downloadDocs",
|
|
1220
1257
|
"newWindow",
|
|
1258
|
+
"contextChange",
|
|
1221
1259
|
]);
|
|
1222
1260
|
|
|
1223
1261
|
/** The subjects that decide a success criterion. Empty ⇒ the criterion has none declared,
|
|
@@ -1228,6 +1266,16 @@ export const subjectsForSc = (sc: string): string[] => SC_SUBJECTS[sc] ?? [];
|
|
|
1228
1266
|
* of its mapped success criteria's. The override is what stops RGAA 11.1 (are the fields
|
|
1229
1267
|
* labelled?) from being handed the page's heading outline because 1.3.1 happens to come
|
|
1230
1268
|
* first in its `wcag` list. */
|
|
1269
|
+
/** The subjects a pack criterion declares FOR ITSELF, as opposed to the union it inherits from
|
|
1270
|
+
* its WCAG mapping. Undefined when it declares none.
|
|
1271
|
+
*
|
|
1272
|
+
* The distinction decides whether an INHERITED absence may close the criterion. RGAA 13.2 maps
|
|
1273
|
+
* onto WCAG 3.2.1, whose subject is a script; 13.2's own subject is a window opening. On a page
|
|
1274
|
+
* carrying `target="_blank"` and no script at all, 3.2.1 is legitimately « nothing of that kind
|
|
1275
|
+
* in scope » — and 13.2 inherited that closure and published a conformity over an element
|
|
1276
|
+
* nobody had looked at. Its own subject was right there in the harvest. */
|
|
1277
|
+
export const ownPackSubjects = (standard: string, id: string): string[] | undefined => PACK_SUBJECTS[standard]?.[id];
|
|
1278
|
+
|
|
1231
1279
|
export function subjectsForPackCriterion(standard: string, id: string, scs: string[]): string[] {
|
|
1232
1280
|
const own = PACK_SUBJECTS[standard]?.[id];
|
|
1233
1281
|
if (own) return own;
|
package/src/standards/derive.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// presentation-only WCAG view (former src/standard.ts). For each pack criterion, gather
|
|
3
3
|
// the results of the WCAG SCs it maps to and fold them with the same NC-dominates rule.
|
|
4
4
|
// Presentation-only: the canonical, gated verdict lives on the WCAG core.
|
|
5
|
-
import { subjectsAbsent, subjectsForPackCriterion } from "../adjudicate-subjects.js";
|
|
5
|
+
import { ownPackSubjects, subjectsAbsent, subjectsForPackCriterion } from "../adjudicate-subjects.js";
|
|
6
6
|
import { coverageFor, criterionMeasuredOn, unionCoverage } from "../coverage.js";
|
|
7
7
|
import type { AuditResult, CriterionResult, PageCoverage, Status, Finding, Severity } from "../types.js";
|
|
8
8
|
import { CORE_KEY, loadPack } from "./registry.js";
|
|
@@ -264,6 +264,16 @@ export function derivePackResults(audit: AuditResult, packKey: string, pageId?:
|
|
|
264
264
|
// scope that read no file.
|
|
265
265
|
const seen = audit.scope.subjectsSeen;
|
|
266
266
|
const subjectAbsent = (pc: PackCriterion): boolean => seen !== undefined && subjectsAbsent(subjectsForPackCriterion(packKey, pc.id, pc.wcag), new Set(seen));
|
|
267
|
+
// Did the criterion's OWN subject turn up? Only its own counts: a criterion that declares none
|
|
268
|
+
// has nothing to defend against an inherited closure, and the union it borrows from its WCAG
|
|
269
|
+
// mapping is exactly where that closure came from.
|
|
270
|
+
const ownSubjectSeen = (pc: PackCriterion): boolean => {
|
|
271
|
+
if (seen === undefined) return false;
|
|
272
|
+
const own = ownPackSubjects(packKey, pc.id);
|
|
273
|
+
if (!own?.length) return false;
|
|
274
|
+
const present = new Set(seen);
|
|
275
|
+
return own.some((id) => present.has(id));
|
|
276
|
+
};
|
|
267
277
|
|
|
268
278
|
// Rules the PACK ITSELF brings, by finding id. A criterion whose `appliesTo` names one of
|
|
269
279
|
// these has an instrument regardless of what its WCAG mapping is worth — which is the whole
|
|
@@ -361,6 +371,28 @@ export function derivePackResults(audit: AuditResult, packKey: string, pageId?:
|
|
|
361
371
|
return { id: pc.id, theme: pc.theme, status: INAPPLICABLE_STATUS as Status, findings, scs: pc.wcag, inapplicable: true };
|
|
362
372
|
}
|
|
363
373
|
const inapplicable = status === INAPPLICABLE_STATUS && (scResults.length === 0 || scResults.every((r) => r.inapplicable));
|
|
374
|
+
// AN INHERITED ABSENCE CANNOT CLOSE A CRITERION WHOSE OWN SUBJECT IS PRESENT.
|
|
375
|
+
//
|
|
376
|
+
// `inapplicable` here means every mapped success criterion came back « nothing of that kind
|
|
377
|
+
// is in scope ». That is a statement about THEIR subject, and a pack criterion frequently
|
|
378
|
+
// asks about a different one — which is the entire reason PACK_SUBJECTS exists. Publishing
|
|
379
|
+
// their absence as our conformity is the precise shape of « conforming because nobody
|
|
380
|
+
// looked », and it is worse than leaving the criterion open: in the deliverable it reads
|
|
381
|
+
// identically to a verified conformity.
|
|
382
|
+
//
|
|
383
|
+
// Measured on RGAA 13.2 (« l'ouverture d'une nouvelle fenêtre ne doit pas être déclenchée
|
|
384
|
+
// sans action de l'utilisateur »), which maps onto WCAG 3.2.1, whose subject is a script. On
|
|
385
|
+
// a page carrying `target="_blank"` and no script, 3.2.1 is honestly inapplicable — and 13.2
|
|
386
|
+
// inherited that and reported « conforme faute de sujet » over a link sitting in its own
|
|
387
|
+
// harvest. It surfaced the day `contextChange` became an existence subject, which is what
|
|
388
|
+
// made 3.2.1 closable at all; the flaw predates that and was merely unreachable.
|
|
389
|
+
//
|
|
390
|
+
// The answer is « undecided », never a conformity: our subject is here and nobody has ruled
|
|
391
|
+
// on it. NC is already excluded above, and a criterion declaring no subject of its own is
|
|
392
|
+
// untouched.
|
|
393
|
+
if (inapplicable && ownSubjectSeen(pc)) {
|
|
394
|
+
return { id: pc.id, theme: pc.theme, status: "manual" as Status, findings, scs: pc.wcag };
|
|
395
|
+
}
|
|
364
396
|
return { id: pc.id, theme: pc.theme, status, findings, scs: pc.wcag, ...(inapplicable ? { inapplicable: true } : {}) };
|
|
365
397
|
};
|
|
366
398
|
|
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.27.0";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|