ultra11y 5.5.1 → 5.5.3
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 +45 -12
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +45 -12
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +45 -12
- package/src/adjudicate-subjects.ts +25 -2
- package/src/adjudicate.ts +91 -18
- 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.5.
|
|
13
|
+
"version": "5.5.3",
|
|
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.5.
|
|
4
|
+
"version": "5.5.3",
|
|
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.5.
|
|
4
|
+
"version": "5.5.3",
|
|
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.5.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.5.3";
|
|
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.5.
|
|
3
|
+
"version": "5.5.3",
|
|
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.5.
|
|
9
|
+
var VERSION = "5.5.3";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -55758,6 +55758,13 @@ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
|
55758
55758
|
import { join as join35, resolve as resolve10 } from "path";
|
|
55759
55759
|
|
|
55760
55760
|
// src/adjudicate-subjects.ts
|
|
55761
|
+
var VOLATILE = [
|
|
55762
|
+
[/\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g, "<date>"],
|
|
55763
|
+
[/\b\d{4}-\d{2}-\d{2}\b/g, "<date>"],
|
|
55764
|
+
[/\b\d{1,2}:\d{2}(?::\d{2})?\b/g, "<time>"],
|
|
55765
|
+
[/\b\d{6,}\b/g, "<num>"]
|
|
55766
|
+
];
|
|
55767
|
+
var stable = (s) => VOLATILE.reduce((acc, [re, to]) => acc.replace(re, to), s);
|
|
55761
55768
|
var selectorFor = (el) => {
|
|
55762
55769
|
const id = el.attribs.id ? `#${el.attribs.id}` : "";
|
|
55763
55770
|
const cls = el.attribs.class ? `.${el.attribs.class.trim().split(/\s+/)[0]}` : "";
|
|
@@ -55766,14 +55773,14 @@ var selectorFor = (el) => {
|
|
|
55766
55773
|
function h(doc, el, note, cls) {
|
|
55767
55774
|
return {
|
|
55768
55775
|
ev: { file: doc.file, line: el.line, selector: selectorFor(el), snippet: snippet(doc, el, 160), note },
|
|
55769
|
-
cls: cls ?? `${el.tag}|${note}
|
|
55776
|
+
cls: stable(cls ?? `${el.tag}|${note}`),
|
|
55770
55777
|
at: el.start
|
|
55771
55778
|
};
|
|
55772
55779
|
}
|
|
55773
55780
|
function hAt(doc, line, selector, note, cls) {
|
|
55774
55781
|
return {
|
|
55775
55782
|
ev: { file: doc.file, line, selector, snippet: (doc.source.split("\n")[line - 1] ?? "").trim().slice(0, 160), note },
|
|
55776
|
-
cls,
|
|
55783
|
+
cls: stable(cls),
|
|
55777
55784
|
at: line
|
|
55778
55785
|
};
|
|
55779
55786
|
}
|
|
@@ -57174,8 +57181,8 @@ function adjudicationText(sc, lang) {
|
|
|
57174
57181
|
}
|
|
57175
57182
|
|
|
57176
57183
|
// src/adjudicate.ts
|
|
57177
|
-
var ADJUDICATE_MAX_EVIDENCE_CLASSES =
|
|
57178
|
-
var
|
|
57184
|
+
var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
|
|
57185
|
+
var ALSO_AT_SHOWN = 8;
|
|
57179
57186
|
function adjudicationContract() {
|
|
57180
57187
|
return {
|
|
57181
57188
|
verdicts: [...VERDICTS],
|
|
@@ -57218,7 +57225,7 @@ function collapse2(harvested) {
|
|
|
57218
57225
|
return {
|
|
57219
57226
|
...rep.ev,
|
|
57220
57227
|
...group.length > 1 ? { occurrences: group.length } : {},
|
|
57221
|
-
...others.length ? { alsoAt: others.
|
|
57228
|
+
...others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
|
|
57222
57229
|
...pages.length ? { pages } : {}
|
|
57223
57230
|
};
|
|
57224
57231
|
});
|
|
@@ -57281,6 +57288,27 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
57281
57288
|
(r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs))
|
|
57282
57289
|
);
|
|
57283
57290
|
}
|
|
57291
|
+
function auditFiles(audit2, cwd) {
|
|
57292
|
+
const inputs = audit2.scope.inputs.filter((i2) => i2 !== "-" && i2 !== "<stdin>");
|
|
57293
|
+
if (!inputs.length) return /* @__PURE__ */ new Set();
|
|
57294
|
+
try {
|
|
57295
|
+
void cwd;
|
|
57296
|
+
return new Set(discover(inputs, {}).files);
|
|
57297
|
+
} catch {
|
|
57298
|
+
return /* @__PURE__ */ new Set();
|
|
57299
|
+
}
|
|
57300
|
+
}
|
|
57301
|
+
var CITE_DRIFT = 10;
|
|
57302
|
+
function cites0(anchors, c2) {
|
|
57303
|
+
for (const a of anchors) {
|
|
57304
|
+
const at = a.lastIndexOf(":");
|
|
57305
|
+
if (at < 0) continue;
|
|
57306
|
+
if (a.slice(0, at) !== c2.file) continue;
|
|
57307
|
+
const line = Number.parseInt(a.slice(at + 1), 10);
|
|
57308
|
+
if (Number.isFinite(line) && Math.abs(line - c2.line) <= CITE_DRIFT) return true;
|
|
57309
|
+
}
|
|
57310
|
+
return false;
|
|
57311
|
+
}
|
|
57284
57312
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
57285
57313
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
57286
57314
|
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
@@ -57367,6 +57395,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57367
57395
|
}
|
|
57368
57396
|
}
|
|
57369
57397
|
const groundInputs = /* @__PURE__ */ new Map();
|
|
57398
|
+
let scopeCache;
|
|
57399
|
+
const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
|
|
57370
57400
|
const toGround = (criteriaId, g) => {
|
|
57371
57401
|
const list = groundInputs.get(criteriaId);
|
|
57372
57402
|
if (list) list.push(g);
|
|
@@ -57392,15 +57422,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57392
57422
|
`criterion ${it.criteriaId}: a C verdict needs evidence to cite, and none was harvested for this criterion \u2014 record "manual" (reason "undecidable"), or "NA" if nothing in scope is concerned`
|
|
57393
57423
|
);
|
|
57394
57424
|
}
|
|
57395
|
-
} else if (cites.length === 0) {
|
|
57425
|
+
} else if (cites.length === 0 && v === "C") {
|
|
57396
57426
|
blame(
|
|
57397
57427
|
it.criteriaId,
|
|
57398
|
-
`criterion ${it.criteriaId}: a
|
|
57428
|
+
`criterion ${it.criteriaId}: a C verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, \u2026}])`
|
|
57399
57429
|
);
|
|
57400
|
-
} else {
|
|
57401
|
-
const anchors =
|
|
57430
|
+
} else if (cites.length > 0) {
|
|
57431
|
+
const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]);
|
|
57402
57432
|
for (const c2 of cites) {
|
|
57403
|
-
if (!anchors.has(
|
|
57433
|
+
if (!cites0(anchors, c2) && !scopeFiles().has(c2.file)) {
|
|
57404
57434
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
57405
57435
|
}
|
|
57406
57436
|
toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
|
|
@@ -57586,6 +57616,7 @@ var T2 = {
|
|
|
57586
57616
|
occurrences: "occurrences",
|
|
57587
57617
|
pagesWord: "page(s)",
|
|
57588
57618
|
on: "sur",
|
|
57619
|
+
alsoAt: "aussi en",
|
|
57589
57620
|
incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
|
|
57590
57621
|
none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
|
|
57591
57622
|
questions: "\xC0 v\xE9rifier manuellement",
|
|
@@ -57614,6 +57645,7 @@ var T2 = {
|
|
|
57614
57645
|
occurrences: "occurrences",
|
|
57615
57646
|
pagesWord: "page(s)",
|
|
57616
57647
|
on: "on",
|
|
57648
|
+
alsoAt: "also at",
|
|
57617
57649
|
incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
|
|
57618
57650
|
none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
|
|
57619
57651
|
questions: "To verify manually",
|
|
@@ -57669,7 +57701,8 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
|
|
|
57669
57701
|
for (const e of it.evidence) {
|
|
57670
57702
|
const extra = [
|
|
57671
57703
|
e.occurrences && e.occurrences > 1 ? `\xD7${e.occurrences}` : "",
|
|
57672
|
-
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : ""
|
|
57704
|
+
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : "",
|
|
57705
|
+
e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "\u2026" : ""}` : ""
|
|
57673
57706
|
].filter(Boolean).join(" ");
|
|
57674
57707
|
out2.push(`- \`${e.file}:${e.line}\` (\`${e.selector}\`)${extra ? ` [${extra}]` : ""}${e.note ? ` \u2014 ${e.note}` : ""}`);
|
|
57675
57708
|
}
|
|
@@ -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.5.
|
|
8
|
+
version: 5.5.3
|
|
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.5.
|
|
9
|
+
var VERSION = "5.5.3";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -55758,6 +55758,13 @@ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
|
55758
55758
|
import { join as join35, resolve as resolve10 } from "path";
|
|
55759
55759
|
|
|
55760
55760
|
// src/adjudicate-subjects.ts
|
|
55761
|
+
var VOLATILE = [
|
|
55762
|
+
[/\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g, "<date>"],
|
|
55763
|
+
[/\b\d{4}-\d{2}-\d{2}\b/g, "<date>"],
|
|
55764
|
+
[/\b\d{1,2}:\d{2}(?::\d{2})?\b/g, "<time>"],
|
|
55765
|
+
[/\b\d{6,}\b/g, "<num>"]
|
|
55766
|
+
];
|
|
55767
|
+
var stable = (s) => VOLATILE.reduce((acc, [re, to]) => acc.replace(re, to), s);
|
|
55761
55768
|
var selectorFor = (el) => {
|
|
55762
55769
|
const id = el.attribs.id ? `#${el.attribs.id}` : "";
|
|
55763
55770
|
const cls = el.attribs.class ? `.${el.attribs.class.trim().split(/\s+/)[0]}` : "";
|
|
@@ -55766,14 +55773,14 @@ var selectorFor = (el) => {
|
|
|
55766
55773
|
function h(doc, el, note, cls) {
|
|
55767
55774
|
return {
|
|
55768
55775
|
ev: { file: doc.file, line: el.line, selector: selectorFor(el), snippet: snippet(doc, el, 160), note },
|
|
55769
|
-
cls: cls ?? `${el.tag}|${note}
|
|
55776
|
+
cls: stable(cls ?? `${el.tag}|${note}`),
|
|
55770
55777
|
at: el.start
|
|
55771
55778
|
};
|
|
55772
55779
|
}
|
|
55773
55780
|
function hAt(doc, line, selector, note, cls) {
|
|
55774
55781
|
return {
|
|
55775
55782
|
ev: { file: doc.file, line, selector, snippet: (doc.source.split("\n")[line - 1] ?? "").trim().slice(0, 160), note },
|
|
55776
|
-
cls,
|
|
55783
|
+
cls: stable(cls),
|
|
55777
55784
|
at: line
|
|
55778
55785
|
};
|
|
55779
55786
|
}
|
|
@@ -57174,8 +57181,8 @@ function adjudicationText(sc, lang) {
|
|
|
57174
57181
|
}
|
|
57175
57182
|
|
|
57176
57183
|
// src/adjudicate.ts
|
|
57177
|
-
var ADJUDICATE_MAX_EVIDENCE_CLASSES =
|
|
57178
|
-
var
|
|
57184
|
+
var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
|
|
57185
|
+
var ALSO_AT_SHOWN = 8;
|
|
57179
57186
|
function adjudicationContract() {
|
|
57180
57187
|
return {
|
|
57181
57188
|
verdicts: [...VERDICTS],
|
|
@@ -57218,7 +57225,7 @@ function collapse2(harvested) {
|
|
|
57218
57225
|
return {
|
|
57219
57226
|
...rep.ev,
|
|
57220
57227
|
...group.length > 1 ? { occurrences: group.length } : {},
|
|
57221
|
-
...others.length ? { alsoAt: others.
|
|
57228
|
+
...others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
|
|
57222
57229
|
...pages.length ? { pages } : {}
|
|
57223
57230
|
};
|
|
57224
57231
|
});
|
|
@@ -57281,6 +57288,27 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
57281
57288
|
(r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs))
|
|
57282
57289
|
);
|
|
57283
57290
|
}
|
|
57291
|
+
function auditFiles(audit2, cwd) {
|
|
57292
|
+
const inputs = audit2.scope.inputs.filter((i2) => i2 !== "-" && i2 !== "<stdin>");
|
|
57293
|
+
if (!inputs.length) return /* @__PURE__ */ new Set();
|
|
57294
|
+
try {
|
|
57295
|
+
void cwd;
|
|
57296
|
+
return new Set(discover(inputs, {}).files);
|
|
57297
|
+
} catch {
|
|
57298
|
+
return /* @__PURE__ */ new Set();
|
|
57299
|
+
}
|
|
57300
|
+
}
|
|
57301
|
+
var CITE_DRIFT = 10;
|
|
57302
|
+
function cites0(anchors, c2) {
|
|
57303
|
+
for (const a of anchors) {
|
|
57304
|
+
const at = a.lastIndexOf(":");
|
|
57305
|
+
if (at < 0) continue;
|
|
57306
|
+
if (a.slice(0, at) !== c2.file) continue;
|
|
57307
|
+
const line = Number.parseInt(a.slice(at + 1), 10);
|
|
57308
|
+
if (Number.isFinite(line) && Math.abs(line - c2.line) <= CITE_DRIFT) return true;
|
|
57309
|
+
}
|
|
57310
|
+
return false;
|
|
57311
|
+
}
|
|
57284
57312
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
57285
57313
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
57286
57314
|
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
@@ -57367,6 +57395,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57367
57395
|
}
|
|
57368
57396
|
}
|
|
57369
57397
|
const groundInputs = /* @__PURE__ */ new Map();
|
|
57398
|
+
let scopeCache;
|
|
57399
|
+
const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
|
|
57370
57400
|
const toGround = (criteriaId, g) => {
|
|
57371
57401
|
const list = groundInputs.get(criteriaId);
|
|
57372
57402
|
if (list) list.push(g);
|
|
@@ -57392,15 +57422,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57392
57422
|
`criterion ${it.criteriaId}: a C verdict needs evidence to cite, and none was harvested for this criterion \u2014 record "manual" (reason "undecidable"), or "NA" if nothing in scope is concerned`
|
|
57393
57423
|
);
|
|
57394
57424
|
}
|
|
57395
|
-
} else if (cites.length === 0) {
|
|
57425
|
+
} else if (cites.length === 0 && v === "C") {
|
|
57396
57426
|
blame(
|
|
57397
57427
|
it.criteriaId,
|
|
57398
|
-
`criterion ${it.criteriaId}: a
|
|
57428
|
+
`criterion ${it.criteriaId}: a C verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, \u2026}])`
|
|
57399
57429
|
);
|
|
57400
|
-
} else {
|
|
57401
|
-
const anchors =
|
|
57430
|
+
} else if (cites.length > 0) {
|
|
57431
|
+
const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]);
|
|
57402
57432
|
for (const c2 of cites) {
|
|
57403
|
-
if (!anchors.has(
|
|
57433
|
+
if (!cites0(anchors, c2) && !scopeFiles().has(c2.file)) {
|
|
57404
57434
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
57405
57435
|
}
|
|
57406
57436
|
toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
|
|
@@ -57586,6 +57616,7 @@ var T2 = {
|
|
|
57586
57616
|
occurrences: "occurrences",
|
|
57587
57617
|
pagesWord: "page(s)",
|
|
57588
57618
|
on: "sur",
|
|
57619
|
+
alsoAt: "aussi en",
|
|
57589
57620
|
incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
|
|
57590
57621
|
none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
|
|
57591
57622
|
questions: "\xC0 v\xE9rifier manuellement",
|
|
@@ -57614,6 +57645,7 @@ var T2 = {
|
|
|
57614
57645
|
occurrences: "occurrences",
|
|
57615
57646
|
pagesWord: "page(s)",
|
|
57616
57647
|
on: "on",
|
|
57648
|
+
alsoAt: "also at",
|
|
57617
57649
|
incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
|
|
57618
57650
|
none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
|
|
57619
57651
|
questions: "To verify manually",
|
|
@@ -57669,7 +57701,8 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
|
|
|
57669
57701
|
for (const e of it.evidence) {
|
|
57670
57702
|
const extra = [
|
|
57671
57703
|
e.occurrences && e.occurrences > 1 ? `\xD7${e.occurrences}` : "",
|
|
57672
|
-
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : ""
|
|
57704
|
+
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : "",
|
|
57705
|
+
e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "\u2026" : ""}` : ""
|
|
57673
57706
|
].filter(Boolean).join(" ");
|
|
57674
57707
|
out2.push(`- \`${e.file}:${e.line}\` (\`${e.selector}\`)${extra ? ` [${extra}]` : ""}${e.note ? ` \u2014 ${e.note}` : ""}`);
|
|
57675
57708
|
}
|
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.5.
|
|
7
|
+
version: 5.5.3
|
|
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.5.
|
|
9
|
+
var VERSION = "5.5.3";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -55758,6 +55758,13 @@ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
|
55758
55758
|
import { join as join35, resolve as resolve10 } from "path";
|
|
55759
55759
|
|
|
55760
55760
|
// src/adjudicate-subjects.ts
|
|
55761
|
+
var VOLATILE = [
|
|
55762
|
+
[/\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g, "<date>"],
|
|
55763
|
+
[/\b\d{4}-\d{2}-\d{2}\b/g, "<date>"],
|
|
55764
|
+
[/\b\d{1,2}:\d{2}(?::\d{2})?\b/g, "<time>"],
|
|
55765
|
+
[/\b\d{6,}\b/g, "<num>"]
|
|
55766
|
+
];
|
|
55767
|
+
var stable = (s) => VOLATILE.reduce((acc, [re, to]) => acc.replace(re, to), s);
|
|
55761
55768
|
var selectorFor = (el) => {
|
|
55762
55769
|
const id = el.attribs.id ? `#${el.attribs.id}` : "";
|
|
55763
55770
|
const cls = el.attribs.class ? `.${el.attribs.class.trim().split(/\s+/)[0]}` : "";
|
|
@@ -55766,14 +55773,14 @@ var selectorFor = (el) => {
|
|
|
55766
55773
|
function h(doc, el, note, cls) {
|
|
55767
55774
|
return {
|
|
55768
55775
|
ev: { file: doc.file, line: el.line, selector: selectorFor(el), snippet: snippet(doc, el, 160), note },
|
|
55769
|
-
cls: cls ?? `${el.tag}|${note}
|
|
55776
|
+
cls: stable(cls ?? `${el.tag}|${note}`),
|
|
55770
55777
|
at: el.start
|
|
55771
55778
|
};
|
|
55772
55779
|
}
|
|
55773
55780
|
function hAt(doc, line, selector, note, cls) {
|
|
55774
55781
|
return {
|
|
55775
55782
|
ev: { file: doc.file, line, selector, snippet: (doc.source.split("\n")[line - 1] ?? "").trim().slice(0, 160), note },
|
|
55776
|
-
cls,
|
|
55783
|
+
cls: stable(cls),
|
|
55777
55784
|
at: line
|
|
55778
55785
|
};
|
|
55779
55786
|
}
|
|
@@ -57174,8 +57181,8 @@ function adjudicationText(sc, lang) {
|
|
|
57174
57181
|
}
|
|
57175
57182
|
|
|
57176
57183
|
// src/adjudicate.ts
|
|
57177
|
-
var ADJUDICATE_MAX_EVIDENCE_CLASSES =
|
|
57178
|
-
var
|
|
57184
|
+
var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
|
|
57185
|
+
var ALSO_AT_SHOWN = 8;
|
|
57179
57186
|
function adjudicationContract() {
|
|
57180
57187
|
return {
|
|
57181
57188
|
verdicts: [...VERDICTS],
|
|
@@ -57218,7 +57225,7 @@ function collapse2(harvested) {
|
|
|
57218
57225
|
return {
|
|
57219
57226
|
...rep.ev,
|
|
57220
57227
|
...group.length > 1 ? { occurrences: group.length } : {},
|
|
57221
|
-
...others.length ? { alsoAt: others.
|
|
57228
|
+
...others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
|
|
57222
57229
|
...pages.length ? { pages } : {}
|
|
57223
57230
|
};
|
|
57224
57231
|
});
|
|
@@ -57281,6 +57288,27 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
57281
57288
|
(r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs))
|
|
57282
57289
|
);
|
|
57283
57290
|
}
|
|
57291
|
+
function auditFiles(audit2, cwd) {
|
|
57292
|
+
const inputs = audit2.scope.inputs.filter((i2) => i2 !== "-" && i2 !== "<stdin>");
|
|
57293
|
+
if (!inputs.length) return /* @__PURE__ */ new Set();
|
|
57294
|
+
try {
|
|
57295
|
+
void cwd;
|
|
57296
|
+
return new Set(discover(inputs, {}).files);
|
|
57297
|
+
} catch {
|
|
57298
|
+
return /* @__PURE__ */ new Set();
|
|
57299
|
+
}
|
|
57300
|
+
}
|
|
57301
|
+
var CITE_DRIFT = 10;
|
|
57302
|
+
function cites0(anchors, c2) {
|
|
57303
|
+
for (const a of anchors) {
|
|
57304
|
+
const at = a.lastIndexOf(":");
|
|
57305
|
+
if (at < 0) continue;
|
|
57306
|
+
if (a.slice(0, at) !== c2.file) continue;
|
|
57307
|
+
const line = Number.parseInt(a.slice(at + 1), 10);
|
|
57308
|
+
if (Number.isFinite(line) && Math.abs(line - c2.line) <= CITE_DRIFT) return true;
|
|
57309
|
+
}
|
|
57310
|
+
return false;
|
|
57311
|
+
}
|
|
57284
57312
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
57285
57313
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
57286
57314
|
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
@@ -57367,6 +57395,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57367
57395
|
}
|
|
57368
57396
|
}
|
|
57369
57397
|
const groundInputs = /* @__PURE__ */ new Map();
|
|
57398
|
+
let scopeCache;
|
|
57399
|
+
const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
|
|
57370
57400
|
const toGround = (criteriaId, g) => {
|
|
57371
57401
|
const list = groundInputs.get(criteriaId);
|
|
57372
57402
|
if (list) list.push(g);
|
|
@@ -57392,15 +57422,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57392
57422
|
`criterion ${it.criteriaId}: a C verdict needs evidence to cite, and none was harvested for this criterion \u2014 record "manual" (reason "undecidable"), or "NA" if nothing in scope is concerned`
|
|
57393
57423
|
);
|
|
57394
57424
|
}
|
|
57395
|
-
} else if (cites.length === 0) {
|
|
57425
|
+
} else if (cites.length === 0 && v === "C") {
|
|
57396
57426
|
blame(
|
|
57397
57427
|
it.criteriaId,
|
|
57398
|
-
`criterion ${it.criteriaId}: a
|
|
57428
|
+
`criterion ${it.criteriaId}: a C verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, \u2026}])`
|
|
57399
57429
|
);
|
|
57400
|
-
} else {
|
|
57401
|
-
const anchors =
|
|
57430
|
+
} else if (cites.length > 0) {
|
|
57431
|
+
const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]);
|
|
57402
57432
|
for (const c2 of cites) {
|
|
57403
|
-
if (!anchors.has(
|
|
57433
|
+
if (!cites0(anchors, c2) && !scopeFiles().has(c2.file)) {
|
|
57404
57434
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
57405
57435
|
}
|
|
57406
57436
|
toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
|
|
@@ -57586,6 +57616,7 @@ var T2 = {
|
|
|
57586
57616
|
occurrences: "occurrences",
|
|
57587
57617
|
pagesWord: "page(s)",
|
|
57588
57618
|
on: "sur",
|
|
57619
|
+
alsoAt: "aussi en",
|
|
57589
57620
|
incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
|
|
57590
57621
|
none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
|
|
57591
57622
|
questions: "\xC0 v\xE9rifier manuellement",
|
|
@@ -57614,6 +57645,7 @@ var T2 = {
|
|
|
57614
57645
|
occurrences: "occurrences",
|
|
57615
57646
|
pagesWord: "page(s)",
|
|
57616
57647
|
on: "on",
|
|
57648
|
+
alsoAt: "also at",
|
|
57617
57649
|
incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
|
|
57618
57650
|
none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
|
|
57619
57651
|
questions: "To verify manually",
|
|
@@ -57669,7 +57701,8 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
|
|
|
57669
57701
|
for (const e of it.evidence) {
|
|
57670
57702
|
const extra = [
|
|
57671
57703
|
e.occurrences && e.occurrences > 1 ? `\xD7${e.occurrences}` : "",
|
|
57672
|
-
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : ""
|
|
57704
|
+
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : "",
|
|
57705
|
+
e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "\u2026" : ""}` : ""
|
|
57673
57706
|
].filter(Boolean).join(" ");
|
|
57674
57707
|
out2.push(`- \`${e.file}:${e.line}\` (\`${e.selector}\`)${extra ? ` [${extra}]` : ""}${e.note ? ` \u2014 ${e.note}` : ""}`);
|
|
57675
57708
|
}
|
|
@@ -27,6 +27,29 @@ import { type Doc, type El, ancestors, attr, elementsByTag, snippet as elSnippet
|
|
|
27
27
|
import { parseInlineStyle } from "./color.js";
|
|
28
28
|
import type { Evidence } from "./adjudicate.js";
|
|
29
29
|
|
|
30
|
+
/** Mask the parts of a text that change between runs without the code changing.
|
|
31
|
+
*
|
|
32
|
+
* Only the class IDENTITY is masked — the note and the snippet keep the real text, so the
|
|
33
|
+
* adjudicator still reads what the page says. What this decides is whether two anchors are
|
|
34
|
+
* "the same thing": a row rendered at 13:04 and the same row rendered at 14:12 are one
|
|
35
|
+
* decision about accessibility, not two.
|
|
36
|
+
*
|
|
37
|
+
* It matters beyond tidiness. The verdict ledger fingerprints the evidence a criterion was
|
|
38
|
+
* ruled against, and a fingerprint that moves every run makes a stored verdict stale on
|
|
39
|
+
* arrival — the criterion returns to « to assess » and the ledger stops paying for itself.
|
|
40
|
+
* Measured on a real capture set: four criteria carried a run timestamp in their evidence.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately narrow. Generated ids that are stable for a given tree (React `useId`) and
|
|
43
|
+
* content hashes that are stable for given source are NOT masked: when those move, the code
|
|
44
|
+
* moved, and a verdict about it SHOULD be re-examined. */
|
|
45
|
+
const VOLATILE: [RegExp, string][] = [
|
|
46
|
+
[/\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g, "<date>"],
|
|
47
|
+
[/\b\d{4}-\d{2}-\d{2}\b/g, "<date>"],
|
|
48
|
+
[/\b\d{1,2}:\d{2}(?::\d{2})?\b/g, "<time>"],
|
|
49
|
+
[/\b\d{6,}\b/g, "<num>"],
|
|
50
|
+
];
|
|
51
|
+
const stable = (s: string): string => VOLATILE.reduce((acc, [re, to]) => acc.replace(re, to), s);
|
|
52
|
+
|
|
30
53
|
/** One harvested anchor, plus the content class it belongs to. Two anchors sharing a class
|
|
31
54
|
* are the same thing rendered twice — a header link on 38 pages, not 38 link problems. */
|
|
32
55
|
export interface Harvested {
|
|
@@ -52,7 +75,7 @@ const selectorFor = (el: El): string => {
|
|
|
52
75
|
function h(doc: Doc, el: El, note: string, cls?: string): Harvested {
|
|
53
76
|
return {
|
|
54
77
|
ev: { file: doc.file, line: el.line, selector: selectorFor(el), snippet: elSnippet(doc, el, 160), note },
|
|
55
|
-
cls: cls ?? `${el.tag}|${note}
|
|
78
|
+
cls: stable(cls ?? `${el.tag}|${note}`),
|
|
56
79
|
at: el.start,
|
|
57
80
|
};
|
|
58
81
|
}
|
|
@@ -62,7 +85,7 @@ function h(doc: Doc, el: El, note: string, cls?: string): Harvested {
|
|
|
62
85
|
function hAt(doc: Doc, line: number, selector: string, note: string, cls: string): Harvested {
|
|
63
86
|
return {
|
|
64
87
|
ev: { file: doc.file, line, selector, snippet: (doc.source.split("\n")[line - 1] ?? "").trim().slice(0, 160), note },
|
|
65
|
-
cls,
|
|
88
|
+
cls: stable(cls),
|
|
66
89
|
at: line,
|
|
67
90
|
};
|
|
68
91
|
}
|
package/src/adjudicate.ts
CHANGED
|
@@ -47,19 +47,24 @@ import { guidanceExampleBlock } from "./prd.js";
|
|
|
47
47
|
* 887 links over 38 captured pages are 97 distinct (text, href) pairs. When even the class
|
|
48
48
|
* count exceeds this, `evidenceComplete` goes false and the fold refuses a `C` outright —
|
|
49
49
|
* an incomplete reading may still find a real failure, it may never clear one. */
|
|
50
|
-
export const ADJUDICATE_MAX_EVIDENCE_CLASSES =
|
|
50
|
+
export const ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
52
|
+
// The number is set by what a real application actually contains, measured, and then given
|
|
53
|
+
// room to grow. On a 338-file product with 38 captured pages the largest per-criterion
|
|
54
|
+
// populations run to 592 classes (what survives when CSS is off), 531 (reading order) and 487
|
|
55
|
+
// (structure) — and those ARE the populations, not samples of them.
|
|
56
|
+
//
|
|
57
|
+
// The headroom is the point. A cap sitting just above today's largest criterion is a gate
|
|
58
|
+
// that flips the day someone adds a heading: the criterion becomes unclearable for reasons of
|
|
59
|
+
// VOLUME rather than of uncertainty, which is the wrong reason to refuse a verdict and an
|
|
60
|
+
// especially confusing one, since nothing about the code got worse. Refusing to conclude is
|
|
61
|
+
// honest only when something really was not looked at.
|
|
59
62
|
|
|
60
|
-
/** Sibling anchors
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
+
/** Sibling anchors RENDERED per class. The data keeps them all — the citation gate reads that
|
|
64
|
+
* list to decide whether an anchor belongs to the criterion, so a bound here would make the
|
|
65
|
+
* gate refuse real occurrences for being ninth. Measured on a real run: 31 of 37 refusals
|
|
66
|
+
* were citations of elements the criterion genuinely carried. Only the reading is bounded. */
|
|
67
|
+
const ALSO_AT_SHOWN = 8;
|
|
63
68
|
|
|
64
69
|
/** How much there actually was to look at, so a reader can tell a complete reading from a
|
|
65
70
|
* glance at the first few. */
|
|
@@ -81,7 +86,9 @@ export interface Evidence {
|
|
|
81
86
|
/** Anchors sharing this content class — the same header link on 38 pages is one decision,
|
|
82
87
|
* not 38. Absent when the class has a single anchor. */
|
|
83
88
|
occurrences?: number;
|
|
84
|
-
/**
|
|
89
|
+
/** Every other anchor of this class, as `file:line`. Complete on purpose: the citation gate
|
|
90
|
+
* reads it to decide membership, so a bound here would refuse real occurrences. The
|
|
91
|
+
* worklist renders only the first few (ALSO_AT_SHOWN). */
|
|
85
92
|
alsoAt?: string[];
|
|
86
93
|
/** Page ids this class appears on, when it was harvested from rendered captures. */
|
|
87
94
|
pages?: string[];
|
|
@@ -237,7 +244,7 @@ function collapse(harvested: Harvested[]): { evidence: Evidence[]; population: E
|
|
|
237
244
|
return {
|
|
238
245
|
...rep.ev,
|
|
239
246
|
...(group.length > 1 ? { occurrences: group.length } : {}),
|
|
240
|
-
...(others.length ? { alsoAt: others.
|
|
247
|
+
...(others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {}),
|
|
241
248
|
...(pages.length ? { pages } : {}),
|
|
242
249
|
};
|
|
243
250
|
});
|
|
@@ -327,6 +334,44 @@ export function buildAdjudicationWorklist(audit: AuditResult, opts: { cwd?: stri
|
|
|
327
334
|
);
|
|
328
335
|
}
|
|
329
336
|
|
|
337
|
+
/** The files this audit actually read. Computed from the audit's own scope inputs, so it is
|
|
338
|
+
* the same set the harvest walked — `discover` only globs, it does not parse, so this costs
|
|
339
|
+
* little and is memoised per fold. */
|
|
340
|
+
function auditFiles(audit: AuditResult, cwd?: string): Set<string> {
|
|
341
|
+
const inputs = audit.scope.inputs.filter((i) => i !== "-" && i !== "<stdin>");
|
|
342
|
+
if (!inputs.length) return new Set();
|
|
343
|
+
try {
|
|
344
|
+
void cwd; // `discover` returns the same repo-relative paths the harvest anchors use
|
|
345
|
+
return new Set(discover(inputs, {}).files);
|
|
346
|
+
} catch {
|
|
347
|
+
return new Set();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Does this citation name evidence the criterion actually carries?
|
|
352
|
+
*
|
|
353
|
+
* Same file, and within the drift `groundFinding` already tolerates. Exact line equality was
|
|
354
|
+
* too literal to be useful: the harvest anchors a <table> at line 19, the agent cites the
|
|
355
|
+
* <caption> at line 20 — which is the very thing RGAA 5.4 asks about, inside the element it
|
|
356
|
+
* was shown — and the gate called it fabricated. Measured on a real run, that class of
|
|
357
|
+
* refusal cost more criteria than every other cause combined.
|
|
358
|
+
*
|
|
359
|
+
* What the check still catches is what it was written for: a citation pointing at a file the
|
|
360
|
+
* criterion was never given. And it is only half the gate — `groundFinding` still has to find
|
|
361
|
+
* the cited content in the real source, so a plausible-looking file:line in the right
|
|
362
|
+
* neighbourhood proves nothing on its own. */
|
|
363
|
+
const CITE_DRIFT = 10;
|
|
364
|
+
function cites0(anchors: string[], c: { file: string; line: number }): boolean {
|
|
365
|
+
for (const a of anchors) {
|
|
366
|
+
const at = a.lastIndexOf(":");
|
|
367
|
+
if (at < 0) continue;
|
|
368
|
+
if (a.slice(0, at) !== c.file) continue;
|
|
369
|
+
const line = Number.parseInt(a.slice(at + 1), 10);
|
|
370
|
+
if (Number.isFinite(line) && Math.abs(line - c.line) <= CITE_DRIFT) return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
330
375
|
export interface ApplyAdjudicationResult {
|
|
331
376
|
ok: boolean;
|
|
332
377
|
audit: AuditResult;
|
|
@@ -518,6 +563,9 @@ export function applyAdjudication(
|
|
|
518
563
|
// flat list) for the same reason as `blame`: a failed citation has to condemn its own
|
|
519
564
|
// criterion and no other.
|
|
520
565
|
const groundInputs = new Map<string, { file: string; line: number; selector?: string; snippet?: string }[]>();
|
|
566
|
+
// Memoised: only a citation that missed its criterion's own anchors ever asks for it.
|
|
567
|
+
let scopeCache: Set<string> | undefined;
|
|
568
|
+
const scopeFiles = (): Set<string> => (scopeCache ??= auditFiles(audit, opts.cwd));
|
|
521
569
|
const toGround = (criteriaId: string, g: { file: string; line: number; selector?: string; snippet?: string }) => {
|
|
522
570
|
const list = groundInputs.get(criteriaId);
|
|
523
571
|
if (list) list.push(g);
|
|
@@ -554,12 +602,21 @@ export function applyAdjudication(
|
|
|
554
602
|
`criterion ${it.criteriaId}: a C verdict needs evidence to cite, and none was harvested for this criterion — record "manual" (reason "undecidable"), or "NA" if nothing in scope is concerned`,
|
|
555
603
|
);
|
|
556
604
|
}
|
|
557
|
-
} else if (cites.length === 0) {
|
|
605
|
+
} else if (cites.length === 0 && v === "C") {
|
|
606
|
+
// Only a `C` must cite. A `C` says "everything here conforms", so it has to name what
|
|
607
|
+
// it cleared; an `NA` says "none of this is what the criterion is about", and there is
|
|
608
|
+
// nothing to clear — asking it to cite the very elements it just ruled out of scope is
|
|
609
|
+
// a contradiction, and one the engine does not hold ITSELF to: an engine-proved NA
|
|
610
|
+
// (src/audit.ts subjectMatterReason) carries a justification naming what was searched
|
|
611
|
+
// for, and no citations at all. Measured on a real run, six criteria were refused for
|
|
612
|
+
// this alone, each of them an honest NA.
|
|
613
|
+
//
|
|
614
|
+
// The justification is what keeps an NA falsifiable, and it is still required above.
|
|
558
615
|
blame(
|
|
559
616
|
it.criteriaId,
|
|
560
|
-
`criterion ${it.criteriaId}: a
|
|
617
|
+
`criterion ${it.criteriaId}: a C verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, …}])`,
|
|
561
618
|
);
|
|
562
|
-
} else {
|
|
619
|
+
} else if (cites.length > 0) {
|
|
563
620
|
// Each citation must name evidence THIS item carried. Grounding alone would only
|
|
564
621
|
// prove the anchor exists somewhere in the tree; the pairing proves the agent ruled
|
|
565
622
|
// on what it was actually given.
|
|
@@ -573,9 +630,22 @@ export function applyAdjudication(
|
|
|
573
630
|
// exactly how RGAA 5.8, 9.2 and 11.1 were refused. Membership was the defect;
|
|
574
631
|
// grounding — which proves the citation resolves against real source — is unchanged
|
|
575
632
|
// and still runs on every one of them.
|
|
576
|
-
const anchors =
|
|
633
|
+
const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...(e.alsoAt ?? [])]);
|
|
577
634
|
for (const c of cites) {
|
|
578
|
-
|
|
635
|
+
// Two ways to belong, and the second one is what a good adjudicator does.
|
|
636
|
+
//
|
|
637
|
+
// The harvest anchors a rendered page (`.ultra11y/pages/<id>/dom.html`) because that
|
|
638
|
+
// is what proves the browser's output — but the place a reader goes to FIX it is the
|
|
639
|
+
// component that produced it, and that is what the agent naturally cites. Measured
|
|
640
|
+
// on a real run: 15 of the 16 citations still refused after the drift tolerance were
|
|
641
|
+
// in files this audit had read, pointing at the source behind the evidence. Refusing
|
|
642
|
+
// them called correct work fabricated.
|
|
643
|
+
//
|
|
644
|
+
// So a citation also belongs when it lands in a file the audit read. That is a real
|
|
645
|
+
// bound — a file outside the audited scope is still refused — and it is not the last
|
|
646
|
+
// one: `groundFinding` below still has to find the cited content at that file:line in
|
|
647
|
+
// the real source, which is what "not fabricated" actually means.
|
|
648
|
+
if (!cites0(anchors, c) && !scopeFiles().has(c.file)) {
|
|
579
649
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c.file}:${c.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
580
650
|
}
|
|
581
651
|
toGround(it.criteriaId, { file: c.file, line: c.line, selector: c.selector, snippet: c.snippet });
|
|
@@ -826,6 +896,7 @@ const T = {
|
|
|
826
896
|
occurrences: "occurrences",
|
|
827
897
|
pagesWord: "page(s)",
|
|
828
898
|
on: "sur",
|
|
899
|
+
alsoAt: "aussi en",
|
|
829
900
|
incomplete: "LECTURE INCOMPLÈTE — un « C » sera refusé sur ce critère",
|
|
830
901
|
none: "(aucune évidence automatique — décidez depuis la source, ou laissez `manual` avec une raison)",
|
|
831
902
|
questions: "À vérifier manuellement",
|
|
@@ -855,6 +926,7 @@ const T = {
|
|
|
855
926
|
occurrences: "occurrences",
|
|
856
927
|
pagesWord: "page(s)",
|
|
857
928
|
on: "on",
|
|
929
|
+
alsoAt: "also at",
|
|
858
930
|
incomplete: "INCOMPLETE READING — a C will be refused on this criterion",
|
|
859
931
|
none: "(no automatic evidence — decide from source, or leave `manual` with a reason)",
|
|
860
932
|
questions: "To verify manually",
|
|
@@ -1000,6 +1072,7 @@ export function formatAdjudication(items: AdjudicationItem[], lang: Lang = "en",
|
|
|
1000
1072
|
const extra = [
|
|
1001
1073
|
e.occurrences && e.occurrences > 1 ? `×${e.occurrences}` : "",
|
|
1002
1074
|
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "…" : ""}` : "",
|
|
1075
|
+
e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "…" : ""}` : "",
|
|
1003
1076
|
]
|
|
1004
1077
|
.filter(Boolean)
|
|
1005
1078
|
.join(" ");
|
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.5.
|
|
3
|
+
export const VERSION = "5.5.3";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|