ultra11y 5.25.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/dist/playwright.mjs +1 -1
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +64 -23
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +64 -23
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +64 -23
- package/src/adjudicate-subjects.ts +79 -7
- package/src/adjudicate.ts +2 -2
- package/src/audit.ts +1 -1
- package/src/dev.ts +1 -1
- package/src/glob.ts +2 -2
- package/src/graph/imports.ts +1 -1
- package/src/integrations/cypress-plugin.ts +1 -1
- package/src/integrations/cypress.ts +2 -2
- package/src/integrations/playwright.ts +5 -5
- package/src/mcp/handlers.ts +1 -1
- package/src/orchestrate-templates.ts +4 -1
- package/src/orchestrate.ts +1 -1
- package/src/rules/images.ts +1 -1
- package/src/rules/tables.ts +2 -2
- package/src/scan.ts +1 -1
- package/src/standards/derive.ts +33 -1
- package/src/standards/pack-rules.ts +1 -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/dist/playwright.mjs
CHANGED
|
@@ -720,7 +720,7 @@ var test = (() => {
|
|
|
720
720
|
const base = load(from).test;
|
|
721
721
|
if (!base?.extend) continue;
|
|
722
722
|
return base.extend({
|
|
723
|
-
//
|
|
723
|
+
// `any` for the same reason.
|
|
724
724
|
ultra11y: async ({ page }, use) => {
|
|
725
725
|
await use((opts) => checkA11y(page, opts));
|
|
726
726
|
}
|
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
|
|
|
@@ -36908,7 +36908,7 @@ function makeFilter(opts = {}) {
|
|
|
36908
36908
|
const rel2 = toPosix(file);
|
|
36909
36909
|
if (include && !include(rel2)) return false;
|
|
36910
36910
|
if (exclude?.(rel2)) return false;
|
|
36911
|
-
if (!opts.noDefaultExcludes && isTestArtifact(rel2) && !
|
|
36911
|
+
if (!opts.noDefaultExcludes && isTestArtifact(rel2) && !include?.(rel2)) return false;
|
|
36912
36912
|
return true;
|
|
36913
36913
|
};
|
|
36914
36914
|
}
|
|
@@ -36974,7 +36974,7 @@ function expandInputs(inputs, opts = {}) {
|
|
|
36974
36974
|
const before = list.length;
|
|
36975
36975
|
list = list.filter((f) => {
|
|
36976
36976
|
const rel2 = toPosix(f);
|
|
36977
|
-
return !isTestArtifact(rel2) || explicit.has(f) || include
|
|
36977
|
+
return !isTestArtifact(rel2) || explicit.has(f) || include?.(rel2);
|
|
36978
36978
|
});
|
|
36979
36979
|
const dropped = before - list.length;
|
|
36980
36980
|
if (dropped) opts.onWarn?.(`ultra11y: skipped ${dropped} test/spec/story file(s) \u2014 pass --no-default-excludes to audit them.`);
|
|
@@ -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
|
),
|
|
@@ -45436,9 +45464,12 @@ var EXISTENCE_SUBJECTS = /* @__PURE__ */ new Set([
|
|
|
45436
45464
|
"autocomplete",
|
|
45437
45465
|
"errors",
|
|
45438
45466
|
"frames",
|
|
45439
|
-
"downloadDocs"
|
|
45467
|
+
"downloadDocs",
|
|
45468
|
+
"newWindow",
|
|
45469
|
+
"contextChange"
|
|
45440
45470
|
]);
|
|
45441
45471
|
var subjectsForSc = (sc) => SC_SUBJECTS[sc] ?? [];
|
|
45472
|
+
var ownPackSubjects = (standard, id) => PACK_SUBJECTS[standard]?.[id];
|
|
45442
45473
|
function subjectsForPackCriterion(standard, id, scs) {
|
|
45443
45474
|
const own = PACK_SUBJECTS[standard]?.[id];
|
|
45444
45475
|
if (own) return own;
|
|
@@ -46968,7 +46999,7 @@ var tableEmptyDataCell = {
|
|
|
46968
46999
|
return out2;
|
|
46969
47000
|
}
|
|
46970
47001
|
};
|
|
46971
|
-
function ownerTable(el
|
|
47002
|
+
function ownerTable(el) {
|
|
46972
47003
|
return ancestors(el).find((a) => a.tag === "table");
|
|
46973
47004
|
}
|
|
46974
47005
|
var headersAttrDangling = {
|
|
@@ -46981,7 +47012,7 @@ var headersAttrDangling = {
|
|
|
46981
47012
|
if (el.tag !== "td" && el.tag !== "th") continue;
|
|
46982
47013
|
const raw = (attr(el, "headers") ?? "").trim();
|
|
46983
47014
|
if (!raw || raw.includes("{")) continue;
|
|
46984
|
-
const table = ownerTable(el
|
|
47015
|
+
const table = ownerTable(el);
|
|
46985
47016
|
if (!table) continue;
|
|
46986
47017
|
const inTable = new Map(
|
|
46987
47018
|
descendants(table).filter((d) => d.tag === "th" || d.tag === "td").map((d) => [attr(d, "id") ?? "", d])
|
|
@@ -48784,7 +48815,7 @@ function matchNode(el, node, depth) {
|
|
|
48784
48815
|
if (node.has || node.lacks) {
|
|
48785
48816
|
const desc = descendants(el);
|
|
48786
48817
|
if (node.has && !node.has.every((sub) => desc.some((d) => matchNode(d, sub, depth + 1)))) return false;
|
|
48787
|
-
if (node.lacks
|
|
48818
|
+
if (node.lacks?.some((sub) => desc.some((d) => matchNode(d, sub, depth + 1)))) return false;
|
|
48788
48819
|
}
|
|
48789
48820
|
return true;
|
|
48790
48821
|
}
|
|
@@ -49000,7 +49031,7 @@ function extractGraphNode(ast, doc, file, opts = {}) {
|
|
|
49000
49031
|
if (ast) {
|
|
49001
49032
|
for (const stmt of asNodes((asNode(ast.program) ?? ast).body)) {
|
|
49002
49033
|
const vd = stmt.type === "VariableDeclaration" ? stmt : stmt.type === "ExportNamedDeclaration" ? asNode(stmt.declaration) : void 0;
|
|
49003
|
-
if (
|
|
49034
|
+
if (vd?.type !== "VariableDeclaration") continue;
|
|
49004
49035
|
for (const d of asNodes(vd.declarations)) {
|
|
49005
49036
|
const idn = asNode(d.id);
|
|
49006
49037
|
const init2 = asNode(d.init);
|
|
@@ -49353,7 +49384,7 @@ function hasMedia(d) {
|
|
|
49353
49384
|
return d.elements.some((el) => {
|
|
49354
49385
|
if (el.tag === "object" || el.tag === "embed") {
|
|
49355
49386
|
const type = (el.attribs.type ?? "").toLowerCase();
|
|
49356
|
-
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/")
|
|
49387
|
+
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/");
|
|
49357
49388
|
}
|
|
49358
49389
|
if (el.tag !== "iframe") return false;
|
|
49359
49390
|
const hay = `${el.attribs.src ?? ""} ${el.attribs.title ?? ""} ${el.attribs.allow ?? ""}`;
|
|
@@ -54407,6 +54438,13 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54407
54438
|
const overrides = pack.overrides;
|
|
54408
54439
|
const seen = audit2.scope.subjectsSeen;
|
|
54409
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
|
+
};
|
|
54410
54448
|
const ownRuleIds = new Set((pack.rules ?? []).map((r) => `pack:${packKey}:${r.id}`));
|
|
54411
54449
|
const deriveBase = (pc) => {
|
|
54412
54450
|
const outOfScope = pc.wcag.every((sc) => {
|
|
@@ -54443,6 +54481,9 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54443
54481
|
return { id: pc.id, theme: pc.theme, status: INAPPLICABLE_STATUS, findings, scs: pc.wcag, inapplicable: true };
|
|
54444
54482
|
}
|
|
54445
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
|
+
}
|
|
54446
54487
|
return { id: pc.id, theme: pc.theme, status, findings, scs: pc.wcag, ...inapplicable ? { inapplicable: true } : {} };
|
|
54447
54488
|
};
|
|
54448
54489
|
const enabledSecondary = (pack.secondaryMappings ?? []).filter((m) => m.enabled === true);
|
|
@@ -59219,7 +59260,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59219
59260
|
if (v === null) {
|
|
59220
59261
|
blame(it.criteriaId, `criterion ${it.criteriaId}: unadjudicated (verdict is null)`);
|
|
59221
59262
|
} else if (v === "C" || v === "NA") {
|
|
59222
|
-
if (!it.justification
|
|
59263
|
+
if (!it.justification?.trim()) blame(it.criteriaId, `criterion ${it.criteriaId}: a ${v} verdict requires a justification`);
|
|
59223
59264
|
const cites = (it.citations ?? []).map(readCitation).filter((c2) => c2 !== null);
|
|
59224
59265
|
if (v === "C" && it.evidenceComplete === false) {
|
|
59225
59266
|
blame(
|
|
@@ -59272,7 +59313,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59272
59313
|
);
|
|
59273
59314
|
continue;
|
|
59274
59315
|
}
|
|
59275
|
-
if (!f.normativeRef
|
|
59316
|
+
if (!f.normativeRef?.trim()) {
|
|
59276
59317
|
blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
|
|
59277
59318
|
} else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
|
|
59278
59319
|
blame(
|
|
@@ -60873,7 +60914,7 @@ function mergeSnapshotAudit(base, snap) {
|
|
|
60873
60914
|
for (const c2 of merged.criteria) {
|
|
60874
60915
|
if (!c2.inapplicable) continue;
|
|
60875
60916
|
const measured = snapById.get(c2.id);
|
|
60876
|
-
if (
|
|
60917
|
+
if (measured?.status !== "C" || measured.inapplicable) continue;
|
|
60877
60918
|
c2.status = "C";
|
|
60878
60919
|
delete c2.inapplicable;
|
|
60879
60920
|
if (measured.justification) c2.justification = measured.justification;
|
|
@@ -64916,7 +64957,7 @@ function foldRecordedAdjudication(root, fresh) {
|
|
|
64916
64957
|
for (const c2 of prior.criteria) {
|
|
64917
64958
|
if (c2.decidedBy !== "agent") continue;
|
|
64918
64959
|
const target = byId2.get(c2.id);
|
|
64919
|
-
if (
|
|
64960
|
+
if (target?.status !== "manual") continue;
|
|
64920
64961
|
target.status = c2.status;
|
|
64921
64962
|
target.decidedBy = "agent";
|
|
64922
64963
|
if (c2.justification) target.justification = c2.justification;
|
|
@@ -65541,7 +65582,7 @@ For EACH entry:
|
|
|
65541
65582
|
Then fold: \`ultra11y verify --apply ${join47(runAbs, "VERIFY.todo.json")} --report <the report .md>\`. Without a shell, leave the fold to whoever gave you the file.
|
|
65542
65583
|
`;
|
|
65543
65584
|
}
|
|
65544
|
-
function agentContracts(runAbs,
|
|
65585
|
+
function agentContracts(runAbs, opts = {}) {
|
|
65545
65586
|
const footer = ONE_WRITER_FOOTER.replaceAll("<RUN>", runAbs);
|
|
65546
65587
|
if (opts.eco) return { adjudicator: ecoAdjudicatorContract(runAbs), refuter: ecoRefuterContract(runAbs) };
|
|
65547
65588
|
return {
|
|
@@ -65724,7 +65765,7 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
|
|
|
65724
65765
|
mkdirSync17(agentsDir, { recursive: true });
|
|
65725
65766
|
const written = [];
|
|
65726
65767
|
const notices = [];
|
|
65727
|
-
for (const [name2, content] of Object.entries(agentContracts(run2,
|
|
65768
|
+
for (const [name2, content] of Object.entries(agentContracts(run2, { eco: opts.eco === true }))) {
|
|
65728
65769
|
const p = join48(agentsDir, `${name2}.md`);
|
|
65729
65770
|
writeFileSync19(p, content);
|
|
65730
65771
|
written.push(p);
|
|
@@ -66230,7 +66271,7 @@ function bool(v) {
|
|
|
66230
66271
|
}
|
|
66231
66272
|
function strArray2(v) {
|
|
66232
66273
|
const a = Array.isArray(v) && v.every((x) => typeof x === "string") ? v : void 0;
|
|
66233
|
-
return a
|
|
66274
|
+
return a?.length ? a : void 0;
|
|
66234
66275
|
}
|
|
66235
66276
|
function positive(v, key) {
|
|
66236
66277
|
const n = num2(v);
|
|
@@ -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
|
|
|
@@ -36908,7 +36908,7 @@ function makeFilter(opts = {}) {
|
|
|
36908
36908
|
const rel2 = toPosix(file);
|
|
36909
36909
|
if (include && !include(rel2)) return false;
|
|
36910
36910
|
if (exclude?.(rel2)) return false;
|
|
36911
|
-
if (!opts.noDefaultExcludes && isTestArtifact(rel2) && !
|
|
36911
|
+
if (!opts.noDefaultExcludes && isTestArtifact(rel2) && !include?.(rel2)) return false;
|
|
36912
36912
|
return true;
|
|
36913
36913
|
};
|
|
36914
36914
|
}
|
|
@@ -36974,7 +36974,7 @@ function expandInputs(inputs, opts = {}) {
|
|
|
36974
36974
|
const before = list.length;
|
|
36975
36975
|
list = list.filter((f) => {
|
|
36976
36976
|
const rel2 = toPosix(f);
|
|
36977
|
-
return !isTestArtifact(rel2) || explicit.has(f) || include
|
|
36977
|
+
return !isTestArtifact(rel2) || explicit.has(f) || include?.(rel2);
|
|
36978
36978
|
});
|
|
36979
36979
|
const dropped = before - list.length;
|
|
36980
36980
|
if (dropped) opts.onWarn?.(`ultra11y: skipped ${dropped} test/spec/story file(s) \u2014 pass --no-default-excludes to audit them.`);
|
|
@@ -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
|
),
|
|
@@ -45436,9 +45464,12 @@ var EXISTENCE_SUBJECTS = /* @__PURE__ */ new Set([
|
|
|
45436
45464
|
"autocomplete",
|
|
45437
45465
|
"errors",
|
|
45438
45466
|
"frames",
|
|
45439
|
-
"downloadDocs"
|
|
45467
|
+
"downloadDocs",
|
|
45468
|
+
"newWindow",
|
|
45469
|
+
"contextChange"
|
|
45440
45470
|
]);
|
|
45441
45471
|
var subjectsForSc = (sc) => SC_SUBJECTS[sc] ?? [];
|
|
45472
|
+
var ownPackSubjects = (standard, id) => PACK_SUBJECTS[standard]?.[id];
|
|
45442
45473
|
function subjectsForPackCriterion(standard, id, scs) {
|
|
45443
45474
|
const own = PACK_SUBJECTS[standard]?.[id];
|
|
45444
45475
|
if (own) return own;
|
|
@@ -46968,7 +46999,7 @@ var tableEmptyDataCell = {
|
|
|
46968
46999
|
return out2;
|
|
46969
47000
|
}
|
|
46970
47001
|
};
|
|
46971
|
-
function ownerTable(el
|
|
47002
|
+
function ownerTable(el) {
|
|
46972
47003
|
return ancestors(el).find((a) => a.tag === "table");
|
|
46973
47004
|
}
|
|
46974
47005
|
var headersAttrDangling = {
|
|
@@ -46981,7 +47012,7 @@ var headersAttrDangling = {
|
|
|
46981
47012
|
if (el.tag !== "td" && el.tag !== "th") continue;
|
|
46982
47013
|
const raw = (attr(el, "headers") ?? "").trim();
|
|
46983
47014
|
if (!raw || raw.includes("{")) continue;
|
|
46984
|
-
const table = ownerTable(el
|
|
47015
|
+
const table = ownerTable(el);
|
|
46985
47016
|
if (!table) continue;
|
|
46986
47017
|
const inTable = new Map(
|
|
46987
47018
|
descendants(table).filter((d) => d.tag === "th" || d.tag === "td").map((d) => [attr(d, "id") ?? "", d])
|
|
@@ -48784,7 +48815,7 @@ function matchNode(el, node, depth) {
|
|
|
48784
48815
|
if (node.has || node.lacks) {
|
|
48785
48816
|
const desc = descendants(el);
|
|
48786
48817
|
if (node.has && !node.has.every((sub) => desc.some((d) => matchNode(d, sub, depth + 1)))) return false;
|
|
48787
|
-
if (node.lacks
|
|
48818
|
+
if (node.lacks?.some((sub) => desc.some((d) => matchNode(d, sub, depth + 1)))) return false;
|
|
48788
48819
|
}
|
|
48789
48820
|
return true;
|
|
48790
48821
|
}
|
|
@@ -49000,7 +49031,7 @@ function extractGraphNode(ast, doc, file, opts = {}) {
|
|
|
49000
49031
|
if (ast) {
|
|
49001
49032
|
for (const stmt of asNodes((asNode(ast.program) ?? ast).body)) {
|
|
49002
49033
|
const vd = stmt.type === "VariableDeclaration" ? stmt : stmt.type === "ExportNamedDeclaration" ? asNode(stmt.declaration) : void 0;
|
|
49003
|
-
if (
|
|
49034
|
+
if (vd?.type !== "VariableDeclaration") continue;
|
|
49004
49035
|
for (const d of asNodes(vd.declarations)) {
|
|
49005
49036
|
const idn = asNode(d.id);
|
|
49006
49037
|
const init2 = asNode(d.init);
|
|
@@ -49353,7 +49384,7 @@ function hasMedia(d) {
|
|
|
49353
49384
|
return d.elements.some((el) => {
|
|
49354
49385
|
if (el.tag === "object" || el.tag === "embed") {
|
|
49355
49386
|
const type = (el.attribs.type ?? "").toLowerCase();
|
|
49356
|
-
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/")
|
|
49387
|
+
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/");
|
|
49357
49388
|
}
|
|
49358
49389
|
if (el.tag !== "iframe") return false;
|
|
49359
49390
|
const hay = `${el.attribs.src ?? ""} ${el.attribs.title ?? ""} ${el.attribs.allow ?? ""}`;
|
|
@@ -54407,6 +54438,13 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54407
54438
|
const overrides = pack.overrides;
|
|
54408
54439
|
const seen = audit2.scope.subjectsSeen;
|
|
54409
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
|
+
};
|
|
54410
54448
|
const ownRuleIds = new Set((pack.rules ?? []).map((r) => `pack:${packKey}:${r.id}`));
|
|
54411
54449
|
const deriveBase = (pc) => {
|
|
54412
54450
|
const outOfScope = pc.wcag.every((sc) => {
|
|
@@ -54443,6 +54481,9 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54443
54481
|
return { id: pc.id, theme: pc.theme, status: INAPPLICABLE_STATUS, findings, scs: pc.wcag, inapplicable: true };
|
|
54444
54482
|
}
|
|
54445
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
|
+
}
|
|
54446
54487
|
return { id: pc.id, theme: pc.theme, status, findings, scs: pc.wcag, ...inapplicable ? { inapplicable: true } : {} };
|
|
54447
54488
|
};
|
|
54448
54489
|
const enabledSecondary = (pack.secondaryMappings ?? []).filter((m) => m.enabled === true);
|
|
@@ -59219,7 +59260,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59219
59260
|
if (v === null) {
|
|
59220
59261
|
blame(it.criteriaId, `criterion ${it.criteriaId}: unadjudicated (verdict is null)`);
|
|
59221
59262
|
} else if (v === "C" || v === "NA") {
|
|
59222
|
-
if (!it.justification
|
|
59263
|
+
if (!it.justification?.trim()) blame(it.criteriaId, `criterion ${it.criteriaId}: a ${v} verdict requires a justification`);
|
|
59223
59264
|
const cites = (it.citations ?? []).map(readCitation).filter((c2) => c2 !== null);
|
|
59224
59265
|
if (v === "C" && it.evidenceComplete === false) {
|
|
59225
59266
|
blame(
|
|
@@ -59272,7 +59313,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59272
59313
|
);
|
|
59273
59314
|
continue;
|
|
59274
59315
|
}
|
|
59275
|
-
if (!f.normativeRef
|
|
59316
|
+
if (!f.normativeRef?.trim()) {
|
|
59276
59317
|
blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
|
|
59277
59318
|
} else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
|
|
59278
59319
|
blame(
|
|
@@ -60873,7 +60914,7 @@ function mergeSnapshotAudit(base, snap) {
|
|
|
60873
60914
|
for (const c2 of merged.criteria) {
|
|
60874
60915
|
if (!c2.inapplicable) continue;
|
|
60875
60916
|
const measured = snapById.get(c2.id);
|
|
60876
|
-
if (
|
|
60917
|
+
if (measured?.status !== "C" || measured.inapplicable) continue;
|
|
60877
60918
|
c2.status = "C";
|
|
60878
60919
|
delete c2.inapplicable;
|
|
60879
60920
|
if (measured.justification) c2.justification = measured.justification;
|
|
@@ -64916,7 +64957,7 @@ function foldRecordedAdjudication(root, fresh) {
|
|
|
64916
64957
|
for (const c2 of prior.criteria) {
|
|
64917
64958
|
if (c2.decidedBy !== "agent") continue;
|
|
64918
64959
|
const target = byId2.get(c2.id);
|
|
64919
|
-
if (
|
|
64960
|
+
if (target?.status !== "manual") continue;
|
|
64920
64961
|
target.status = c2.status;
|
|
64921
64962
|
target.decidedBy = "agent";
|
|
64922
64963
|
if (c2.justification) target.justification = c2.justification;
|
|
@@ -65541,7 +65582,7 @@ For EACH entry:
|
|
|
65541
65582
|
Then fold: \`ultra11y verify --apply ${join47(runAbs, "VERIFY.todo.json")} --report <the report .md>\`. Without a shell, leave the fold to whoever gave you the file.
|
|
65542
65583
|
`;
|
|
65543
65584
|
}
|
|
65544
|
-
function agentContracts(runAbs,
|
|
65585
|
+
function agentContracts(runAbs, opts = {}) {
|
|
65545
65586
|
const footer = ONE_WRITER_FOOTER.replaceAll("<RUN>", runAbs);
|
|
65546
65587
|
if (opts.eco) return { adjudicator: ecoAdjudicatorContract(runAbs), refuter: ecoRefuterContract(runAbs) };
|
|
65547
65588
|
return {
|
|
@@ -65724,7 +65765,7 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
|
|
|
65724
65765
|
mkdirSync17(agentsDir, { recursive: true });
|
|
65725
65766
|
const written = [];
|
|
65726
65767
|
const notices = [];
|
|
65727
|
-
for (const [name2, content] of Object.entries(agentContracts(run2,
|
|
65768
|
+
for (const [name2, content] of Object.entries(agentContracts(run2, { eco: opts.eco === true }))) {
|
|
65728
65769
|
const p = join48(agentsDir, `${name2}.md`);
|
|
65729
65770
|
writeFileSync19(p, content);
|
|
65730
65771
|
written.push(p);
|
|
@@ -66230,7 +66271,7 @@ function bool(v) {
|
|
|
66230
66271
|
}
|
|
66231
66272
|
function strArray2(v) {
|
|
66232
66273
|
const a = Array.isArray(v) && v.every((x) => typeof x === "string") ? v : void 0;
|
|
66233
|
-
return a
|
|
66274
|
+
return a?.length ? a : void 0;
|
|
66234
66275
|
}
|
|
66235
66276
|
function positive(v, key) {
|
|
66236
66277
|
const n = num2(v);
|
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
|
|
|
@@ -36908,7 +36908,7 @@ function makeFilter(opts = {}) {
|
|
|
36908
36908
|
const rel2 = toPosix(file);
|
|
36909
36909
|
if (include && !include(rel2)) return false;
|
|
36910
36910
|
if (exclude?.(rel2)) return false;
|
|
36911
|
-
if (!opts.noDefaultExcludes && isTestArtifact(rel2) && !
|
|
36911
|
+
if (!opts.noDefaultExcludes && isTestArtifact(rel2) && !include?.(rel2)) return false;
|
|
36912
36912
|
return true;
|
|
36913
36913
|
};
|
|
36914
36914
|
}
|
|
@@ -36974,7 +36974,7 @@ function expandInputs(inputs, opts = {}) {
|
|
|
36974
36974
|
const before = list.length;
|
|
36975
36975
|
list = list.filter((f) => {
|
|
36976
36976
|
const rel2 = toPosix(f);
|
|
36977
|
-
return !isTestArtifact(rel2) || explicit.has(f) || include
|
|
36977
|
+
return !isTestArtifact(rel2) || explicit.has(f) || include?.(rel2);
|
|
36978
36978
|
});
|
|
36979
36979
|
const dropped = before - list.length;
|
|
36980
36980
|
if (dropped) opts.onWarn?.(`ultra11y: skipped ${dropped} test/spec/story file(s) \u2014 pass --no-default-excludes to audit them.`);
|
|
@@ -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
|
),
|
|
@@ -45436,9 +45464,12 @@ var EXISTENCE_SUBJECTS = /* @__PURE__ */ new Set([
|
|
|
45436
45464
|
"autocomplete",
|
|
45437
45465
|
"errors",
|
|
45438
45466
|
"frames",
|
|
45439
|
-
"downloadDocs"
|
|
45467
|
+
"downloadDocs",
|
|
45468
|
+
"newWindow",
|
|
45469
|
+
"contextChange"
|
|
45440
45470
|
]);
|
|
45441
45471
|
var subjectsForSc = (sc) => SC_SUBJECTS[sc] ?? [];
|
|
45472
|
+
var ownPackSubjects = (standard, id) => PACK_SUBJECTS[standard]?.[id];
|
|
45442
45473
|
function subjectsForPackCriterion(standard, id, scs) {
|
|
45443
45474
|
const own = PACK_SUBJECTS[standard]?.[id];
|
|
45444
45475
|
if (own) return own;
|
|
@@ -46968,7 +46999,7 @@ var tableEmptyDataCell = {
|
|
|
46968
46999
|
return out2;
|
|
46969
47000
|
}
|
|
46970
47001
|
};
|
|
46971
|
-
function ownerTable(el
|
|
47002
|
+
function ownerTable(el) {
|
|
46972
47003
|
return ancestors(el).find((a) => a.tag === "table");
|
|
46973
47004
|
}
|
|
46974
47005
|
var headersAttrDangling = {
|
|
@@ -46981,7 +47012,7 @@ var headersAttrDangling = {
|
|
|
46981
47012
|
if (el.tag !== "td" && el.tag !== "th") continue;
|
|
46982
47013
|
const raw = (attr(el, "headers") ?? "").trim();
|
|
46983
47014
|
if (!raw || raw.includes("{")) continue;
|
|
46984
|
-
const table = ownerTable(el
|
|
47015
|
+
const table = ownerTable(el);
|
|
46985
47016
|
if (!table) continue;
|
|
46986
47017
|
const inTable = new Map(
|
|
46987
47018
|
descendants(table).filter((d) => d.tag === "th" || d.tag === "td").map((d) => [attr(d, "id") ?? "", d])
|
|
@@ -48784,7 +48815,7 @@ function matchNode(el, node, depth) {
|
|
|
48784
48815
|
if (node.has || node.lacks) {
|
|
48785
48816
|
const desc = descendants(el);
|
|
48786
48817
|
if (node.has && !node.has.every((sub) => desc.some((d) => matchNode(d, sub, depth + 1)))) return false;
|
|
48787
|
-
if (node.lacks
|
|
48818
|
+
if (node.lacks?.some((sub) => desc.some((d) => matchNode(d, sub, depth + 1)))) return false;
|
|
48788
48819
|
}
|
|
48789
48820
|
return true;
|
|
48790
48821
|
}
|
|
@@ -49000,7 +49031,7 @@ function extractGraphNode(ast, doc, file, opts = {}) {
|
|
|
49000
49031
|
if (ast) {
|
|
49001
49032
|
for (const stmt of asNodes((asNode(ast.program) ?? ast).body)) {
|
|
49002
49033
|
const vd = stmt.type === "VariableDeclaration" ? stmt : stmt.type === "ExportNamedDeclaration" ? asNode(stmt.declaration) : void 0;
|
|
49003
|
-
if (
|
|
49034
|
+
if (vd?.type !== "VariableDeclaration") continue;
|
|
49004
49035
|
for (const d of asNodes(vd.declarations)) {
|
|
49005
49036
|
const idn = asNode(d.id);
|
|
49006
49037
|
const init2 = asNode(d.init);
|
|
@@ -49353,7 +49384,7 @@ function hasMedia(d) {
|
|
|
49353
49384
|
return d.elements.some((el) => {
|
|
49354
49385
|
if (el.tag === "object" || el.tag === "embed") {
|
|
49355
49386
|
const type = (el.attribs.type ?? "").toLowerCase();
|
|
49356
|
-
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/")
|
|
49387
|
+
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/");
|
|
49357
49388
|
}
|
|
49358
49389
|
if (el.tag !== "iframe") return false;
|
|
49359
49390
|
const hay = `${el.attribs.src ?? ""} ${el.attribs.title ?? ""} ${el.attribs.allow ?? ""}`;
|
|
@@ -54407,6 +54438,13 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54407
54438
|
const overrides = pack.overrides;
|
|
54408
54439
|
const seen = audit2.scope.subjectsSeen;
|
|
54409
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
|
+
};
|
|
54410
54448
|
const ownRuleIds = new Set((pack.rules ?? []).map((r) => `pack:${packKey}:${r.id}`));
|
|
54411
54449
|
const deriveBase = (pc) => {
|
|
54412
54450
|
const outOfScope = pc.wcag.every((sc) => {
|
|
@@ -54443,6 +54481,9 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
54443
54481
|
return { id: pc.id, theme: pc.theme, status: INAPPLICABLE_STATUS, findings, scs: pc.wcag, inapplicable: true };
|
|
54444
54482
|
}
|
|
54445
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
|
+
}
|
|
54446
54487
|
return { id: pc.id, theme: pc.theme, status, findings, scs: pc.wcag, ...inapplicable ? { inapplicable: true } : {} };
|
|
54447
54488
|
};
|
|
54448
54489
|
const enabledSecondary = (pack.secondaryMappings ?? []).filter((m) => m.enabled === true);
|
|
@@ -59219,7 +59260,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59219
59260
|
if (v === null) {
|
|
59220
59261
|
blame(it.criteriaId, `criterion ${it.criteriaId}: unadjudicated (verdict is null)`);
|
|
59221
59262
|
} else if (v === "C" || v === "NA") {
|
|
59222
|
-
if (!it.justification
|
|
59263
|
+
if (!it.justification?.trim()) blame(it.criteriaId, `criterion ${it.criteriaId}: a ${v} verdict requires a justification`);
|
|
59223
59264
|
const cites = (it.citations ?? []).map(readCitation).filter((c2) => c2 !== null);
|
|
59224
59265
|
if (v === "C" && it.evidenceComplete === false) {
|
|
59225
59266
|
blame(
|
|
@@ -59272,7 +59313,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
59272
59313
|
);
|
|
59273
59314
|
continue;
|
|
59274
59315
|
}
|
|
59275
|
-
if (!f.normativeRef
|
|
59316
|
+
if (!f.normativeRef?.trim()) {
|
|
59276
59317
|
blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
|
|
59277
59318
|
} else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
|
|
59278
59319
|
blame(
|
|
@@ -60873,7 +60914,7 @@ function mergeSnapshotAudit(base, snap) {
|
|
|
60873
60914
|
for (const c2 of merged.criteria) {
|
|
60874
60915
|
if (!c2.inapplicable) continue;
|
|
60875
60916
|
const measured = snapById.get(c2.id);
|
|
60876
|
-
if (
|
|
60917
|
+
if (measured?.status !== "C" || measured.inapplicable) continue;
|
|
60877
60918
|
c2.status = "C";
|
|
60878
60919
|
delete c2.inapplicable;
|
|
60879
60920
|
if (measured.justification) c2.justification = measured.justification;
|
|
@@ -64916,7 +64957,7 @@ function foldRecordedAdjudication(root, fresh) {
|
|
|
64916
64957
|
for (const c2 of prior.criteria) {
|
|
64917
64958
|
if (c2.decidedBy !== "agent") continue;
|
|
64918
64959
|
const target = byId2.get(c2.id);
|
|
64919
|
-
if (
|
|
64960
|
+
if (target?.status !== "manual") continue;
|
|
64920
64961
|
target.status = c2.status;
|
|
64921
64962
|
target.decidedBy = "agent";
|
|
64922
64963
|
if (c2.justification) target.justification = c2.justification;
|
|
@@ -65541,7 +65582,7 @@ For EACH entry:
|
|
|
65541
65582
|
Then fold: \`ultra11y verify --apply ${join47(runAbs, "VERIFY.todo.json")} --report <the report .md>\`. Without a shell, leave the fold to whoever gave you the file.
|
|
65542
65583
|
`;
|
|
65543
65584
|
}
|
|
65544
|
-
function agentContracts(runAbs,
|
|
65585
|
+
function agentContracts(runAbs, opts = {}) {
|
|
65545
65586
|
const footer = ONE_WRITER_FOOTER.replaceAll("<RUN>", runAbs);
|
|
65546
65587
|
if (opts.eco) return { adjudicator: ecoAdjudicatorContract(runAbs), refuter: ecoRefuterContract(runAbs) };
|
|
65547
65588
|
return {
|
|
@@ -65724,7 +65765,7 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
|
|
|
65724
65765
|
mkdirSync17(agentsDir, { recursive: true });
|
|
65725
65766
|
const written = [];
|
|
65726
65767
|
const notices = [];
|
|
65727
|
-
for (const [name2, content] of Object.entries(agentContracts(run2,
|
|
65768
|
+
for (const [name2, content] of Object.entries(agentContracts(run2, { eco: opts.eco === true }))) {
|
|
65728
65769
|
const p = join48(agentsDir, `${name2}.md`);
|
|
65729
65770
|
writeFileSync19(p, content);
|
|
65730
65771
|
written.push(p);
|
|
@@ -66230,7 +66271,7 @@ function bool(v) {
|
|
|
66230
66271
|
}
|
|
66231
66272
|
function strArray2(v) {
|
|
66232
66273
|
const a = Array.isArray(v) && v.every((x) => typeof x === "string") ? v : void 0;
|
|
66233
|
-
return a
|
|
66274
|
+
return a?.length ? a : void 0;
|
|
66234
66275
|
}
|
|
66235
66276
|
function positive(v, key) {
|
|
66236
66277
|
const n = num2(v);
|
|
@@ -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
|
),
|
|
@@ -1184,6 +1212,38 @@ export const PACK_SUBJECTS: Record<string, Record<string, string[]>> = {
|
|
|
1184
1212
|
// about nothing when there is nothing to download. Its recall is not perfect — a document
|
|
1185
1213
|
// served from an extensionless URL is missed — which is true of every subject here and is why
|
|
1186
1214
|
// absence is only ever read as « nothing of that kind is in scope », never as conformance.
|
|
1215
|
+
//
|
|
1216
|
+
// `newWindow` earns its place the same way, and it is worth spelling out because the criterion
|
|
1217
|
+
// it closes has a single test. RGAA 13.2.1 asks one thing — « vérifier qu'à l'ouverture du
|
|
1218
|
+
// document, aucune nouvelle fenêtre n'est ouverte » — and a browser has exactly three ways to
|
|
1219
|
+
// do it: a `target` that leaves this browsing context, a `<meta http-equiv="refresh">`, and a
|
|
1220
|
+
// literal `window.open(`. Two are markup and the third survives minification, so a page that
|
|
1221
|
+
// FAILS 13.2 cannot harvest nothing. Its recall is imperfect in the same way every subject here
|
|
1222
|
+
// is — `open` reached through an alias is missed — which is why the absence still reads only as
|
|
1223
|
+
// « nothing of that kind is in scope », never as conformance.
|
|
1224
|
+
//
|
|
1225
|
+
// Measured on tests/fixtures/realworld, crawled and scanned, engine v5.25.0: 13.2 was one of
|
|
1226
|
+
// only two criteria still reaching the adjudicator with `evidenceComplete` and a population of
|
|
1227
|
+
// zero. The engine already knew the answer and was billing a model to restate it — on the run
|
|
1228
|
+
// this comment was written from, both were ruled NA on the first pass with no gate refusal,
|
|
1229
|
+
// which is the definition of a paid turn that decided nothing.
|
|
1230
|
+
//
|
|
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.
|
|
1187
1247
|
export const EXISTENCE_SUBJECTS: ReadonlySet<string> = new Set([
|
|
1188
1248
|
"images",
|
|
1189
1249
|
"tables",
|
|
@@ -1194,6 +1254,8 @@ export const EXISTENCE_SUBJECTS: ReadonlySet<string> = new Set([
|
|
|
1194
1254
|
"errors",
|
|
1195
1255
|
"frames",
|
|
1196
1256
|
"downloadDocs",
|
|
1257
|
+
"newWindow",
|
|
1258
|
+
"contextChange",
|
|
1197
1259
|
]);
|
|
1198
1260
|
|
|
1199
1261
|
/** The subjects that decide a success criterion. Empty ⇒ the criterion has none declared,
|
|
@@ -1204,6 +1266,16 @@ export const subjectsForSc = (sc: string): string[] => SC_SUBJECTS[sc] ?? [];
|
|
|
1204
1266
|
* of its mapped success criteria's. The override is what stops RGAA 11.1 (are the fields
|
|
1205
1267
|
* labelled?) from being handed the page's heading outline because 1.3.1 happens to come
|
|
1206
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
|
+
|
|
1207
1279
|
export function subjectsForPackCriterion(standard: string, id: string, scs: string[]): string[] {
|
|
1208
1280
|
const own = PACK_SUBJECTS[standard]?.[id];
|
|
1209
1281
|
if (own) return own;
|
package/src/adjudicate.ts
CHANGED
|
@@ -799,7 +799,7 @@ export function applyAdjudication(
|
|
|
799
799
|
if (v === null) {
|
|
800
800
|
blame(it.criteriaId, `criterion ${it.criteriaId}: unadjudicated (verdict is null)`);
|
|
801
801
|
} else if (v === "C" || v === "NA") {
|
|
802
|
-
if (!it.justification
|
|
802
|
+
if (!it.justification?.trim()) blame(it.criteriaId, `criterion ${it.criteriaId}: a ${v} verdict requires a justification`);
|
|
803
803
|
// A clearing verdict is gated exactly like an accusing one. Before this, the only
|
|
804
804
|
// check was "the justification is a non-empty string", so `"x"` cleared a criterion —
|
|
805
805
|
// and a model answering C to everything published a conformance nobody had assessed.
|
|
@@ -942,7 +942,7 @@ export function applyAdjudication(
|
|
|
942
942
|
}
|
|
943
943
|
// FAIL-CLOSED: every NC finding must cite a precise, resolvable test of the active
|
|
944
944
|
// standard. A good practice with no normative test is a recommendation, not an NC.
|
|
945
|
-
if (!f.normativeRef
|
|
945
|
+
if (!f.normativeRef?.trim()) {
|
|
946
946
|
blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
|
|
947
947
|
} else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? undefined : it.criteriaId)) {
|
|
948
948
|
blame(
|
package/src/audit.ts
CHANGED
|
@@ -184,7 +184,7 @@ function hasMedia(d: Doc): boolean {
|
|
|
184
184
|
if (el.tag === "object" || el.tag === "embed") {
|
|
185
185
|
const type = (el.attribs.type ?? "").toLowerCase();
|
|
186
186
|
// A declared non-media type rules it out; anything else is uncertain, so it counts.
|
|
187
|
-
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/")
|
|
187
|
+
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/");
|
|
188
188
|
}
|
|
189
189
|
if (el.tag !== "iframe") return false;
|
|
190
190
|
const hay = `${el.attribs.src ?? ""} ${el.attribs.title ?? ""} ${el.attribs.allow ?? ""}`;
|
package/src/dev.ts
CHANGED
|
@@ -356,7 +356,7 @@ function foldRecordedAdjudication(root: string, fresh: AuditResult): void {
|
|
|
356
356
|
const target = byId.get(c.id);
|
|
357
357
|
// A criterion the fresh run decided by itself is authoritative: the page moved, and a
|
|
358
358
|
// stale ruling must never override a live measurement.
|
|
359
|
-
if (
|
|
359
|
+
if (target?.status !== "manual") continue;
|
|
360
360
|
target.status = c.status;
|
|
361
361
|
target.decidedBy = "agent";
|
|
362
362
|
if (c.justification) target.justification = c.justification;
|
package/src/glob.ts
CHANGED
|
@@ -89,7 +89,7 @@ export function makeFilter(opts: ExpandOpts = {}): (file: string) => boolean {
|
|
|
89
89
|
if (include && !include(rel)) return false;
|
|
90
90
|
if (exclude?.(rel)) return false;
|
|
91
91
|
// Default test-artifact exclusion (re-admitted by an explicit --include match).
|
|
92
|
-
if (!opts.noDefaultExcludes && isTestArtifact(rel) && !
|
|
92
|
+
if (!opts.noDefaultExcludes && isTestArtifact(rel) && !include?.(rel)) return false;
|
|
93
93
|
return true;
|
|
94
94
|
};
|
|
95
95
|
}
|
|
@@ -197,7 +197,7 @@ export function expandInputs(inputs: string[], opts: ExpandOpts = {}): string[]
|
|
|
197
197
|
const before = list.length;
|
|
198
198
|
list = list.filter((f) => {
|
|
199
199
|
const rel = toPosix(f);
|
|
200
|
-
return !isTestArtifact(rel) || explicit.has(f) ||
|
|
200
|
+
return !isTestArtifact(rel) || explicit.has(f) || include?.(rel);
|
|
201
201
|
});
|
|
202
202
|
const dropped = before - list.length;
|
|
203
203
|
if (dropped) opts.onWarn?.(`ultra11y: skipped ${dropped} test/spec/story file(s) — pass --no-default-excludes to audit them.`);
|
package/src/graph/imports.ts
CHANGED
|
@@ -225,7 +225,7 @@ export function extractGraphNode(ast: AstFile | null, doc: Doc, file: string, op
|
|
|
225
225
|
if (ast) {
|
|
226
226
|
for (const stmt of asNodes((asNode(ast.program) ?? ast).body)) {
|
|
227
227
|
const vd = stmt.type === "VariableDeclaration" ? stmt : stmt.type === "ExportNamedDeclaration" ? asNode(stmt.declaration) : undefined;
|
|
228
|
-
if (
|
|
228
|
+
if (vd?.type !== "VariableDeclaration") continue;
|
|
229
229
|
for (const d of asNodes(vd.declarations)) {
|
|
230
230
|
const idn = asNode(d.id);
|
|
231
231
|
const init = asNode(d.init);
|
|
@@ -22,7 +22,7 @@ interface ScreenshotDetails {
|
|
|
22
22
|
path: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// `any`: Cypress's `on` is not a dependency of this package.
|
|
26
26
|
type On = (event: string, handler: any) => void;
|
|
27
27
|
|
|
28
28
|
export interface TaskPayload extends SnapshotPayload {
|
|
@@ -14,10 +14,10 @@ import { type CheckOptions, buildPayload, slugify, stayedOnPage } from "./payloa
|
|
|
14
14
|
|
|
15
15
|
// Cypress's globals are not a dependency of this package; they exist at run time only.
|
|
16
16
|
declare const Cypress: {
|
|
17
|
-
//
|
|
17
|
+
// `any`: Cypress's own command signature.
|
|
18
18
|
Commands: { add(name: string, fn: (...args: any[]) => any): void };
|
|
19
19
|
};
|
|
20
|
-
//
|
|
20
|
+
// `any` for the same reason.
|
|
21
21
|
declare const cy: any;
|
|
22
22
|
|
|
23
23
|
export interface CypressCheckOptions extends CheckOptions {
|
|
@@ -88,7 +88,7 @@ function fromConfig(): { only?: string[]; limits?: Omit<ProbeTuning, "only"> } {
|
|
|
88
88
|
async function runAxe(page: PlaywrightPage, mode: boolean | "auto" | undefined): Promise<{ ran: true; violations: unknown[] } | undefined> {
|
|
89
89
|
if (mode === false) return undefined;
|
|
90
90
|
const demanded = mode === true;
|
|
91
|
-
//
|
|
91
|
+
// `any`: the builder's shape is axe's, which is not a dependency here.
|
|
92
92
|
let AxeBuilder: any;
|
|
93
93
|
// PROJECT FIRST, then relative to this module — the same order the Playwright fixture above
|
|
94
94
|
// and `scan --runtime local` use, and for the same reason: module-relative alone works for an
|
|
@@ -204,16 +204,16 @@ export async function checkA11y(page: PlaywrightPage, opts: PlaywrightCheckOptio
|
|
|
204
204
|
*
|
|
205
205
|
* Resolution is lazy and guarded either way, so importing this in a Cypress-only repo
|
|
206
206
|
* gives `undefined` rather than a crash. */
|
|
207
|
-
//
|
|
207
|
+
// `any`: the shape is Playwright's, which is not a dependency here.
|
|
208
208
|
export const test: any = (() => {
|
|
209
|
-
//
|
|
209
|
+
// `any` for the same reason.
|
|
210
210
|
const load = (from: string): any => createRequire(from)("@playwright/test");
|
|
211
211
|
for (const from of [`${process.cwd()}/package.json`, import.meta.url]) {
|
|
212
212
|
try {
|
|
213
213
|
const base = load(from).test;
|
|
214
214
|
if (!base?.extend) continue;
|
|
215
215
|
return base.extend({
|
|
216
|
-
//
|
|
216
|
+
// `any` for the same reason.
|
|
217
217
|
ultra11y: async ({ page }: any, use: any) => {
|
|
218
218
|
await use((opts: PlaywrightCheckOptions) => checkA11y(page, opts));
|
|
219
219
|
},
|
|
@@ -241,7 +241,7 @@ export interface SweepOptions {
|
|
|
241
241
|
* `waitForLoadState`, `waitForFunction`, locators — and Playwright is not a dependency of
|
|
242
242
|
* this package, so the narrow structural shape the collector needs would reject the very
|
|
243
243
|
* function this option exists to take. Same reasoning as `test` below. */
|
|
244
|
-
//
|
|
244
|
+
// `any`: the shape is Playwright's, which is not a dependency here.
|
|
245
245
|
settle?: (page: any) => Promise<void> | void;
|
|
246
246
|
/** Passed to every `checkA11y`. `failOn: false` (the default here) records without
|
|
247
247
|
* asserting: the durable output of a sweep is the snapshot, not a red test. */
|
package/src/mcp/handlers.ts
CHANGED
|
@@ -80,7 +80,7 @@ function bool(v: unknown): boolean {
|
|
|
80
80
|
|
|
81
81
|
function strArray(v: unknown): string[] | undefined {
|
|
82
82
|
const a = Array.isArray(v) && v.every((x) => typeof x === "string") ? (v as string[]) : undefined;
|
|
83
|
-
return a
|
|
83
|
+
return a?.length ? a : undefined;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function positive(v: unknown, key: string): number | undefined {
|
|
@@ -260,7 +260,10 @@ Then fold: \`ultra11y verify --apply ${join(runAbs, "VERIFY.todo.json")} --repor
|
|
|
260
260
|
`;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
|
|
263
|
+
// `engineAbs` is deliberately NOT a parameter: the emitted workflow injects `ENGINE` as a
|
|
264
|
+
// constant and the contract refers to it by name, so a second copy of the path here could only
|
|
265
|
+
// ever disagree with the one the agent is actually handed.
|
|
266
|
+
export function agentContracts(runAbs: string, opts: { eco?: boolean } = {}): Record<string, string> {
|
|
264
267
|
const footer = ONE_WRITER_FOOTER.replaceAll("<RUN>", runAbs);
|
|
265
268
|
if (opts.eco) return { adjudicator: ecoAdjudicatorContract(runAbs), refuter: ecoRefuterContract(runAbs) };
|
|
266
269
|
return {
|
package/src/orchestrate.ts
CHANGED
|
@@ -165,7 +165,7 @@ export function orchestrateRun(runDir: string, engineAbs: string, opts: Orchestr
|
|
|
165
165
|
// exists on the sequential path — least of all in CI, where the adjudicator has no shell and
|
|
166
166
|
// writes a file. Emitting the fan-out text there and then telling an agent to obey it VERBATIM
|
|
167
167
|
// is two contradictory sets of instructions, which is what a small model breaks on.
|
|
168
|
-
for (const [name, content] of Object.entries(agentContracts(run,
|
|
168
|
+
for (const [name, content] of Object.entries(agentContracts(run, { eco: opts.eco === true }))) {
|
|
169
169
|
const p = join(agentsDir, `${name}.md`);
|
|
170
170
|
writeFileSync(p, content);
|
|
171
171
|
written.push(p);
|
package/src/rules/images.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Theme 1 — Images.
|
|
2
2
|
import type { Doc, El } from "../parse/html.js";
|
|
3
|
-
import { attr,
|
|
3
|
+
import { attr, hasBoundAttr, boundAttr, hasDynamicSpread, visibleText, ancestors, descendants } from "../parse/html.js";
|
|
4
4
|
import { accessibleName, isNameExempt } from "../name.js";
|
|
5
5
|
import type { Rule, RuleFinding } from "./rule.js";
|
|
6
6
|
|
package/src/rules/tables.ts
CHANGED
|
@@ -181,7 +181,7 @@ const tableEmptyDataCell: Rule = {
|
|
|
181
181
|
};
|
|
182
182
|
|
|
183
183
|
/** The <table> a cell belongs to (nearest table ancestor), or undefined. */
|
|
184
|
-
function ownerTable(el: El
|
|
184
|
+
function ownerTable(el: El): El | undefined {
|
|
185
185
|
return ancestors(el).find((a) => a.tag === "table");
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -198,7 +198,7 @@ const headersAttrDangling: Rule = {
|
|
|
198
198
|
if (el.tag !== "td" && el.tag !== "th") continue;
|
|
199
199
|
const raw = (attr(el, "headers") ?? "").trim();
|
|
200
200
|
if (!raw || raw.includes("{")) continue;
|
|
201
|
-
const table = ownerTable(el
|
|
201
|
+
const table = ownerTable(el);
|
|
202
202
|
if (!table) continue;
|
|
203
203
|
const inTable = new Map(
|
|
204
204
|
descendants(table)
|
package/src/scan.ts
CHANGED
|
@@ -852,7 +852,7 @@ export function mergeSnapshotAudit(base: AuditResult, snap: AuditResult): AuditR
|
|
|
852
852
|
const measured = snapById.get(c.id);
|
|
853
853
|
// Only a snapshot that reached `C` WITHOUT the flag has falsified "nothing of that kind
|
|
854
854
|
// here" — that one looked at the rendered page and found the subject.
|
|
855
|
-
if (
|
|
855
|
+
if (measured?.status !== "C" || measured.inapplicable) continue;
|
|
856
856
|
c.status = "C";
|
|
857
857
|
delete c.inapplicable;
|
|
858
858
|
// Keep the claim citable: adopt what the measuring half said, rather than leaving a `C`
|
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
|
|
|
@@ -60,7 +60,7 @@ function matchNode(el: El, node: MatchNode, depth: number): boolean {
|
|
|
60
60
|
const desc = descendants(el);
|
|
61
61
|
if (node.has && !node.has.every((sub) => desc.some((d) => matchNode(d, sub, depth + 1)))) return false;
|
|
62
62
|
// `lacks`: NO listed sub-condition may match ANY descendant.
|
|
63
|
-
if (node.lacks
|
|
63
|
+
if (node.lacks?.some((sub) => desc.some((d) => matchNode(d, sub, depth + 1)))) return false;
|
|
64
64
|
}
|
|
65
65
|
return true;
|
|
66
66
|
}
|
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,
|