ultra11y 5.5.2 → 5.5.4
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 +47 -11
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +47 -11
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +47 -11
- package/src/adjudicate.ts +104 -12
- package/src/ledger.ts +4 -2
- 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.4",
|
|
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.4",
|
|
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.4",
|
|
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.4";
|
|
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.4",
|
|
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.4";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -57182,7 +57182,7 @@ function adjudicationText(sc, lang) {
|
|
|
57182
57182
|
|
|
57183
57183
|
// src/adjudicate.ts
|
|
57184
57184
|
var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
|
|
57185
|
-
var
|
|
57185
|
+
var ALSO_AT_SHOWN = 8;
|
|
57186
57186
|
function adjudicationContract() {
|
|
57187
57187
|
return {
|
|
57188
57188
|
verdicts: [...VERDICTS],
|
|
@@ -57225,7 +57225,7 @@ function collapse2(harvested) {
|
|
|
57225
57225
|
return {
|
|
57226
57226
|
...rep.ev,
|
|
57227
57227
|
...group.length > 1 ? { occurrences: group.length } : {},
|
|
57228
|
-
...others.length ? { alsoAt: others.
|
|
57228
|
+
...others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
|
|
57229
57229
|
...pages.length ? { pages } : {}
|
|
57230
57230
|
};
|
|
57231
57231
|
});
|
|
@@ -57288,6 +57288,35 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
57288
57288
|
(r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs))
|
|
57289
57289
|
);
|
|
57290
57290
|
}
|
|
57291
|
+
function readCitation(c2) {
|
|
57292
|
+
if (typeof c2 !== "string") return c2;
|
|
57293
|
+
const at = c2.lastIndexOf(":");
|
|
57294
|
+
if (at <= 0) return null;
|
|
57295
|
+
const line = Number.parseInt(c2.slice(at + 1), 10);
|
|
57296
|
+
if (!Number.isFinite(line)) return null;
|
|
57297
|
+
return { file: c2.slice(0, at), line, selector: "", snippet: "" };
|
|
57298
|
+
}
|
|
57299
|
+
function auditFiles(audit2, cwd) {
|
|
57300
|
+
const inputs = audit2.scope.inputs.filter((i2) => i2 !== "-" && i2 !== "<stdin>");
|
|
57301
|
+
if (!inputs.length) return /* @__PURE__ */ new Set();
|
|
57302
|
+
try {
|
|
57303
|
+
void cwd;
|
|
57304
|
+
return new Set(discover(inputs, {}).files);
|
|
57305
|
+
} catch {
|
|
57306
|
+
return /* @__PURE__ */ new Set();
|
|
57307
|
+
}
|
|
57308
|
+
}
|
|
57309
|
+
var CITE_DRIFT = 10;
|
|
57310
|
+
function cites0(anchors, c2) {
|
|
57311
|
+
for (const a of anchors) {
|
|
57312
|
+
const at = a.lastIndexOf(":");
|
|
57313
|
+
if (at < 0) continue;
|
|
57314
|
+
if (a.slice(0, at) !== c2.file) continue;
|
|
57315
|
+
const line = Number.parseInt(a.slice(at + 1), 10);
|
|
57316
|
+
if (Number.isFinite(line) && Math.abs(line - c2.line) <= CITE_DRIFT) return true;
|
|
57317
|
+
}
|
|
57318
|
+
return false;
|
|
57319
|
+
}
|
|
57291
57320
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
57292
57321
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
57293
57322
|
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
@@ -57374,6 +57403,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57374
57403
|
}
|
|
57375
57404
|
}
|
|
57376
57405
|
const groundInputs = /* @__PURE__ */ new Map();
|
|
57406
|
+
let scopeCache;
|
|
57407
|
+
const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
|
|
57377
57408
|
const toGround = (criteriaId, g) => {
|
|
57378
57409
|
const list = groundInputs.get(criteriaId);
|
|
57379
57410
|
if (list) list.push(g);
|
|
@@ -57385,7 +57416,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57385
57416
|
blame(it.criteriaId, `criterion ${it.criteriaId}: unadjudicated (verdict is null)`);
|
|
57386
57417
|
} else if (v === "C" || v === "NA") {
|
|
57387
57418
|
if (!it.justification || !it.justification.trim()) blame(it.criteriaId, `criterion ${it.criteriaId}: a ${v} verdict requires a justification`);
|
|
57388
|
-
const cites = it.citations ?? [];
|
|
57419
|
+
const cites = (it.citations ?? []).map(readCitation).filter((c2) => c2 !== null);
|
|
57389
57420
|
if (v === "C" && it.evidenceComplete === false) {
|
|
57390
57421
|
blame(
|
|
57391
57422
|
it.criteriaId,
|
|
@@ -57399,15 +57430,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57399
57430
|
`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`
|
|
57400
57431
|
);
|
|
57401
57432
|
}
|
|
57402
|
-
} else if (cites.length === 0) {
|
|
57433
|
+
} else if (cites.length === 0 && v === "C") {
|
|
57403
57434
|
blame(
|
|
57404
57435
|
it.criteriaId,
|
|
57405
|
-
`criterion ${it.criteriaId}: a
|
|
57436
|
+
`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}])`
|
|
57406
57437
|
);
|
|
57407
|
-
} else {
|
|
57408
|
-
const anchors =
|
|
57438
|
+
} else if (cites.length > 0) {
|
|
57439
|
+
const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]);
|
|
57409
57440
|
for (const c2 of cites) {
|
|
57410
|
-
if (!anchors.has(
|
|
57441
|
+
if (!cites0(anchors, c2) && !scopeFiles().has(c2.file)) {
|
|
57411
57442
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
57412
57443
|
}
|
|
57413
57444
|
toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
|
|
@@ -57593,6 +57624,7 @@ var T2 = {
|
|
|
57593
57624
|
occurrences: "occurrences",
|
|
57594
57625
|
pagesWord: "page(s)",
|
|
57595
57626
|
on: "sur",
|
|
57627
|
+
alsoAt: "aussi en",
|
|
57596
57628
|
incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
|
|
57597
57629
|
none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
|
|
57598
57630
|
questions: "\xC0 v\xE9rifier manuellement",
|
|
@@ -57621,6 +57653,7 @@ var T2 = {
|
|
|
57621
57653
|
occurrences: "occurrences",
|
|
57622
57654
|
pagesWord: "page(s)",
|
|
57623
57655
|
on: "on",
|
|
57656
|
+
alsoAt: "also at",
|
|
57624
57657
|
incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
|
|
57625
57658
|
none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
|
|
57626
57659
|
questions: "To verify manually",
|
|
@@ -57676,7 +57709,8 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
|
|
|
57676
57709
|
for (const e of it.evidence) {
|
|
57677
57710
|
const extra = [
|
|
57678
57711
|
e.occurrences && e.occurrences > 1 ? `\xD7${e.occurrences}` : "",
|
|
57679
|
-
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : ""
|
|
57712
|
+
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : "",
|
|
57713
|
+
e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "\u2026" : ""}` : ""
|
|
57680
57714
|
].filter(Boolean).join(" ");
|
|
57681
57715
|
out2.push(`- \`${e.file}:${e.line}\` (\`${e.selector}\`)${extra ? ` [${extra}]` : ""}${e.note ? ` \u2014 ${e.note}` : ""}`);
|
|
57682
57716
|
}
|
|
@@ -57817,7 +57851,9 @@ function entriesFrom(adj, accepted, date) {
|
|
|
57817
57851
|
verdict: it.verdict,
|
|
57818
57852
|
...it.justification?.trim() ? { justification: it.justification.trim() } : {},
|
|
57819
57853
|
...it.reason ? { reason: it.reason } : {},
|
|
57820
|
-
|
|
57854
|
+
// Normalised here too: a citation may have arrived as a bare "file:line" string, and a
|
|
57855
|
+
// ledger entry has to hold the object form so a replay can re-anchor and re-ground it.
|
|
57856
|
+
...it.citations?.length ? { citations: it.citations.map(readCitation).filter((c2) => c2 !== null) } : {},
|
|
57821
57857
|
...it.findings?.length ? { findings: it.findings } : {},
|
|
57822
57858
|
...it.recommendations?.length ? { recommendations: it.recommendations } : {},
|
|
57823
57859
|
evidenceFingerprint: evidenceFingerprint(it.evidence),
|
|
@@ -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.4
|
|
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.4";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -57182,7 +57182,7 @@ function adjudicationText(sc, lang) {
|
|
|
57182
57182
|
|
|
57183
57183
|
// src/adjudicate.ts
|
|
57184
57184
|
var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
|
|
57185
|
-
var
|
|
57185
|
+
var ALSO_AT_SHOWN = 8;
|
|
57186
57186
|
function adjudicationContract() {
|
|
57187
57187
|
return {
|
|
57188
57188
|
verdicts: [...VERDICTS],
|
|
@@ -57225,7 +57225,7 @@ function collapse2(harvested) {
|
|
|
57225
57225
|
return {
|
|
57226
57226
|
...rep.ev,
|
|
57227
57227
|
...group.length > 1 ? { occurrences: group.length } : {},
|
|
57228
|
-
...others.length ? { alsoAt: others.
|
|
57228
|
+
...others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
|
|
57229
57229
|
...pages.length ? { pages } : {}
|
|
57230
57230
|
};
|
|
57231
57231
|
});
|
|
@@ -57288,6 +57288,35 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
57288
57288
|
(r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs))
|
|
57289
57289
|
);
|
|
57290
57290
|
}
|
|
57291
|
+
function readCitation(c2) {
|
|
57292
|
+
if (typeof c2 !== "string") return c2;
|
|
57293
|
+
const at = c2.lastIndexOf(":");
|
|
57294
|
+
if (at <= 0) return null;
|
|
57295
|
+
const line = Number.parseInt(c2.slice(at + 1), 10);
|
|
57296
|
+
if (!Number.isFinite(line)) return null;
|
|
57297
|
+
return { file: c2.slice(0, at), line, selector: "", snippet: "" };
|
|
57298
|
+
}
|
|
57299
|
+
function auditFiles(audit2, cwd) {
|
|
57300
|
+
const inputs = audit2.scope.inputs.filter((i2) => i2 !== "-" && i2 !== "<stdin>");
|
|
57301
|
+
if (!inputs.length) return /* @__PURE__ */ new Set();
|
|
57302
|
+
try {
|
|
57303
|
+
void cwd;
|
|
57304
|
+
return new Set(discover(inputs, {}).files);
|
|
57305
|
+
} catch {
|
|
57306
|
+
return /* @__PURE__ */ new Set();
|
|
57307
|
+
}
|
|
57308
|
+
}
|
|
57309
|
+
var CITE_DRIFT = 10;
|
|
57310
|
+
function cites0(anchors, c2) {
|
|
57311
|
+
for (const a of anchors) {
|
|
57312
|
+
const at = a.lastIndexOf(":");
|
|
57313
|
+
if (at < 0) continue;
|
|
57314
|
+
if (a.slice(0, at) !== c2.file) continue;
|
|
57315
|
+
const line = Number.parseInt(a.slice(at + 1), 10);
|
|
57316
|
+
if (Number.isFinite(line) && Math.abs(line - c2.line) <= CITE_DRIFT) return true;
|
|
57317
|
+
}
|
|
57318
|
+
return false;
|
|
57319
|
+
}
|
|
57291
57320
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
57292
57321
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
57293
57322
|
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
@@ -57374,6 +57403,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57374
57403
|
}
|
|
57375
57404
|
}
|
|
57376
57405
|
const groundInputs = /* @__PURE__ */ new Map();
|
|
57406
|
+
let scopeCache;
|
|
57407
|
+
const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
|
|
57377
57408
|
const toGround = (criteriaId, g) => {
|
|
57378
57409
|
const list = groundInputs.get(criteriaId);
|
|
57379
57410
|
if (list) list.push(g);
|
|
@@ -57385,7 +57416,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57385
57416
|
blame(it.criteriaId, `criterion ${it.criteriaId}: unadjudicated (verdict is null)`);
|
|
57386
57417
|
} else if (v === "C" || v === "NA") {
|
|
57387
57418
|
if (!it.justification || !it.justification.trim()) blame(it.criteriaId, `criterion ${it.criteriaId}: a ${v} verdict requires a justification`);
|
|
57388
|
-
const cites = it.citations ?? [];
|
|
57419
|
+
const cites = (it.citations ?? []).map(readCitation).filter((c2) => c2 !== null);
|
|
57389
57420
|
if (v === "C" && it.evidenceComplete === false) {
|
|
57390
57421
|
blame(
|
|
57391
57422
|
it.criteriaId,
|
|
@@ -57399,15 +57430,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57399
57430
|
`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`
|
|
57400
57431
|
);
|
|
57401
57432
|
}
|
|
57402
|
-
} else if (cites.length === 0) {
|
|
57433
|
+
} else if (cites.length === 0 && v === "C") {
|
|
57403
57434
|
blame(
|
|
57404
57435
|
it.criteriaId,
|
|
57405
|
-
`criterion ${it.criteriaId}: a
|
|
57436
|
+
`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}])`
|
|
57406
57437
|
);
|
|
57407
|
-
} else {
|
|
57408
|
-
const anchors =
|
|
57438
|
+
} else if (cites.length > 0) {
|
|
57439
|
+
const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]);
|
|
57409
57440
|
for (const c2 of cites) {
|
|
57410
|
-
if (!anchors.has(
|
|
57441
|
+
if (!cites0(anchors, c2) && !scopeFiles().has(c2.file)) {
|
|
57411
57442
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
57412
57443
|
}
|
|
57413
57444
|
toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
|
|
@@ -57593,6 +57624,7 @@ var T2 = {
|
|
|
57593
57624
|
occurrences: "occurrences",
|
|
57594
57625
|
pagesWord: "page(s)",
|
|
57595
57626
|
on: "sur",
|
|
57627
|
+
alsoAt: "aussi en",
|
|
57596
57628
|
incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
|
|
57597
57629
|
none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
|
|
57598
57630
|
questions: "\xC0 v\xE9rifier manuellement",
|
|
@@ -57621,6 +57653,7 @@ var T2 = {
|
|
|
57621
57653
|
occurrences: "occurrences",
|
|
57622
57654
|
pagesWord: "page(s)",
|
|
57623
57655
|
on: "on",
|
|
57656
|
+
alsoAt: "also at",
|
|
57624
57657
|
incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
|
|
57625
57658
|
none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
|
|
57626
57659
|
questions: "To verify manually",
|
|
@@ -57676,7 +57709,8 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
|
|
|
57676
57709
|
for (const e of it.evidence) {
|
|
57677
57710
|
const extra = [
|
|
57678
57711
|
e.occurrences && e.occurrences > 1 ? `\xD7${e.occurrences}` : "",
|
|
57679
|
-
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : ""
|
|
57712
|
+
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : "",
|
|
57713
|
+
e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "\u2026" : ""}` : ""
|
|
57680
57714
|
].filter(Boolean).join(" ");
|
|
57681
57715
|
out2.push(`- \`${e.file}:${e.line}\` (\`${e.selector}\`)${extra ? ` [${extra}]` : ""}${e.note ? ` \u2014 ${e.note}` : ""}`);
|
|
57682
57716
|
}
|
|
@@ -57817,7 +57851,9 @@ function entriesFrom(adj, accepted, date) {
|
|
|
57817
57851
|
verdict: it.verdict,
|
|
57818
57852
|
...it.justification?.trim() ? { justification: it.justification.trim() } : {},
|
|
57819
57853
|
...it.reason ? { reason: it.reason } : {},
|
|
57820
|
-
|
|
57854
|
+
// Normalised here too: a citation may have arrived as a bare "file:line" string, and a
|
|
57855
|
+
// ledger entry has to hold the object form so a replay can re-anchor and re-ground it.
|
|
57856
|
+
...it.citations?.length ? { citations: it.citations.map(readCitation).filter((c2) => c2 !== null) } : {},
|
|
57821
57857
|
...it.findings?.length ? { findings: it.findings } : {},
|
|
57822
57858
|
...it.recommendations?.length ? { recommendations: it.recommendations } : {},
|
|
57823
57859
|
evidenceFingerprint: evidenceFingerprint(it.evidence),
|
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.4
|
|
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.4";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -57182,7 +57182,7 @@ function adjudicationText(sc, lang) {
|
|
|
57182
57182
|
|
|
57183
57183
|
// src/adjudicate.ts
|
|
57184
57184
|
var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
|
|
57185
|
-
var
|
|
57185
|
+
var ALSO_AT_SHOWN = 8;
|
|
57186
57186
|
function adjudicationContract() {
|
|
57187
57187
|
return {
|
|
57188
57188
|
verdicts: [...VERDICTS],
|
|
@@ -57225,7 +57225,7 @@ function collapse2(harvested) {
|
|
|
57225
57225
|
return {
|
|
57226
57226
|
...rep.ev,
|
|
57227
57227
|
...group.length > 1 ? { occurrences: group.length } : {},
|
|
57228
|
-
...others.length ? { alsoAt: others.
|
|
57228
|
+
...others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
|
|
57229
57229
|
...pages.length ? { pages } : {}
|
|
57230
57230
|
};
|
|
57231
57231
|
});
|
|
@@ -57288,6 +57288,35 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
57288
57288
|
(r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs))
|
|
57289
57289
|
);
|
|
57290
57290
|
}
|
|
57291
|
+
function readCitation(c2) {
|
|
57292
|
+
if (typeof c2 !== "string") return c2;
|
|
57293
|
+
const at = c2.lastIndexOf(":");
|
|
57294
|
+
if (at <= 0) return null;
|
|
57295
|
+
const line = Number.parseInt(c2.slice(at + 1), 10);
|
|
57296
|
+
if (!Number.isFinite(line)) return null;
|
|
57297
|
+
return { file: c2.slice(0, at), line, selector: "", snippet: "" };
|
|
57298
|
+
}
|
|
57299
|
+
function auditFiles(audit2, cwd) {
|
|
57300
|
+
const inputs = audit2.scope.inputs.filter((i2) => i2 !== "-" && i2 !== "<stdin>");
|
|
57301
|
+
if (!inputs.length) return /* @__PURE__ */ new Set();
|
|
57302
|
+
try {
|
|
57303
|
+
void cwd;
|
|
57304
|
+
return new Set(discover(inputs, {}).files);
|
|
57305
|
+
} catch {
|
|
57306
|
+
return /* @__PURE__ */ new Set();
|
|
57307
|
+
}
|
|
57308
|
+
}
|
|
57309
|
+
var CITE_DRIFT = 10;
|
|
57310
|
+
function cites0(anchors, c2) {
|
|
57311
|
+
for (const a of anchors) {
|
|
57312
|
+
const at = a.lastIndexOf(":");
|
|
57313
|
+
if (at < 0) continue;
|
|
57314
|
+
if (a.slice(0, at) !== c2.file) continue;
|
|
57315
|
+
const line = Number.parseInt(a.slice(at + 1), 10);
|
|
57316
|
+
if (Number.isFinite(line) && Math.abs(line - c2.line) <= CITE_DRIFT) return true;
|
|
57317
|
+
}
|
|
57318
|
+
return false;
|
|
57319
|
+
}
|
|
57291
57320
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
57292
57321
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
57293
57322
|
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
@@ -57374,6 +57403,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57374
57403
|
}
|
|
57375
57404
|
}
|
|
57376
57405
|
const groundInputs = /* @__PURE__ */ new Map();
|
|
57406
|
+
let scopeCache;
|
|
57407
|
+
const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
|
|
57377
57408
|
const toGround = (criteriaId, g) => {
|
|
57378
57409
|
const list = groundInputs.get(criteriaId);
|
|
57379
57410
|
if (list) list.push(g);
|
|
@@ -57385,7 +57416,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57385
57416
|
blame(it.criteriaId, `criterion ${it.criteriaId}: unadjudicated (verdict is null)`);
|
|
57386
57417
|
} else if (v === "C" || v === "NA") {
|
|
57387
57418
|
if (!it.justification || !it.justification.trim()) blame(it.criteriaId, `criterion ${it.criteriaId}: a ${v} verdict requires a justification`);
|
|
57388
|
-
const cites = it.citations ?? [];
|
|
57419
|
+
const cites = (it.citations ?? []).map(readCitation).filter((c2) => c2 !== null);
|
|
57389
57420
|
if (v === "C" && it.evidenceComplete === false) {
|
|
57390
57421
|
blame(
|
|
57391
57422
|
it.criteriaId,
|
|
@@ -57399,15 +57430,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
57399
57430
|
`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`
|
|
57400
57431
|
);
|
|
57401
57432
|
}
|
|
57402
|
-
} else if (cites.length === 0) {
|
|
57433
|
+
} else if (cites.length === 0 && v === "C") {
|
|
57403
57434
|
blame(
|
|
57404
57435
|
it.criteriaId,
|
|
57405
|
-
`criterion ${it.criteriaId}: a
|
|
57436
|
+
`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}])`
|
|
57406
57437
|
);
|
|
57407
|
-
} else {
|
|
57408
|
-
const anchors =
|
|
57438
|
+
} else if (cites.length > 0) {
|
|
57439
|
+
const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]);
|
|
57409
57440
|
for (const c2 of cites) {
|
|
57410
|
-
if (!anchors.has(
|
|
57441
|
+
if (!cites0(anchors, c2) && !scopeFiles().has(c2.file)) {
|
|
57411
57442
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
57412
57443
|
}
|
|
57413
57444
|
toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
|
|
@@ -57593,6 +57624,7 @@ var T2 = {
|
|
|
57593
57624
|
occurrences: "occurrences",
|
|
57594
57625
|
pagesWord: "page(s)",
|
|
57595
57626
|
on: "sur",
|
|
57627
|
+
alsoAt: "aussi en",
|
|
57596
57628
|
incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
|
|
57597
57629
|
none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
|
|
57598
57630
|
questions: "\xC0 v\xE9rifier manuellement",
|
|
@@ -57621,6 +57653,7 @@ var T2 = {
|
|
|
57621
57653
|
occurrences: "occurrences",
|
|
57622
57654
|
pagesWord: "page(s)",
|
|
57623
57655
|
on: "on",
|
|
57656
|
+
alsoAt: "also at",
|
|
57624
57657
|
incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
|
|
57625
57658
|
none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
|
|
57626
57659
|
questions: "To verify manually",
|
|
@@ -57676,7 +57709,8 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
|
|
|
57676
57709
|
for (const e of it.evidence) {
|
|
57677
57710
|
const extra = [
|
|
57678
57711
|
e.occurrences && e.occurrences > 1 ? `\xD7${e.occurrences}` : "",
|
|
57679
|
-
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : ""
|
|
57712
|
+
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : "",
|
|
57713
|
+
e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "\u2026" : ""}` : ""
|
|
57680
57714
|
].filter(Boolean).join(" ");
|
|
57681
57715
|
out2.push(`- \`${e.file}:${e.line}\` (\`${e.selector}\`)${extra ? ` [${extra}]` : ""}${e.note ? ` \u2014 ${e.note}` : ""}`);
|
|
57682
57716
|
}
|
|
@@ -57817,7 +57851,9 @@ function entriesFrom(adj, accepted, date) {
|
|
|
57817
57851
|
verdict: it.verdict,
|
|
57818
57852
|
...it.justification?.trim() ? { justification: it.justification.trim() } : {},
|
|
57819
57853
|
...it.reason ? { reason: it.reason } : {},
|
|
57820
|
-
|
|
57854
|
+
// Normalised here too: a citation may have arrived as a bare "file:line" string, and a
|
|
57855
|
+
// ledger entry has to hold the object form so a replay can re-anchor and re-ground it.
|
|
57856
|
+
...it.citations?.length ? { citations: it.citations.map(readCitation).filter((c2) => c2 !== null) } : {},
|
|
57821
57857
|
...it.findings?.length ? { findings: it.findings } : {},
|
|
57822
57858
|
...it.recommendations?.length ? { recommendations: it.recommendations } : {},
|
|
57823
57859
|
evidenceFingerprint: evidenceFingerprint(it.evidence),
|
package/src/adjudicate.ts
CHANGED
|
@@ -60,9 +60,11 @@ export const ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
|
|
|
60
60
|
// especially confusing one, since nothing about the code got worse. Refusing to conclude is
|
|
61
61
|
// honest only when something really was not looked at.
|
|
62
62
|
|
|
63
|
-
/** Sibling anchors
|
|
64
|
-
*
|
|
65
|
-
|
|
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;
|
|
66
68
|
|
|
67
69
|
/** How much there actually was to look at, so a reader can tell a complete reading from a
|
|
68
70
|
* glance at the first few. */
|
|
@@ -84,7 +86,9 @@ export interface Evidence {
|
|
|
84
86
|
/** Anchors sharing this content class — the same header link on 38 pages is one decision,
|
|
85
87
|
* not 38. Absent when the class has a single anchor. */
|
|
86
88
|
occurrences?: number;
|
|
87
|
-
/**
|
|
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). */
|
|
88
92
|
alsoAt?: string[];
|
|
89
93
|
/** Page ids this class appears on, when it was harvested from rendered captures. */
|
|
90
94
|
pages?: string[];
|
|
@@ -135,7 +139,10 @@ export interface AdjudicationItem {
|
|
|
135
139
|
// is what turns "cite something real" into "cite the evidence you were shown". When the
|
|
136
140
|
// harvester found no evidence at all there is nothing to clear, and the honest verdicts
|
|
137
141
|
// are `manual` or `NA`, never `C`.
|
|
138
|
-
|
|
142
|
+
// Objects are what the contract asks for; a bare `"file:line"` string is accepted too,
|
|
143
|
+
// because that is the shape the worklist's own `alsoAt` uses and a real run reached for it.
|
|
144
|
+
// See `readCitation` — the gate is identical either way, a string just carries no snippet.
|
|
145
|
+
citations?: (Evidence | string)[];
|
|
139
146
|
// Non-normative good practices the agent noted on this criterion — folded back into the
|
|
140
147
|
// audit as ADVISORY findings (grounded exactly like an NC finding, but never affecting
|
|
141
148
|
// status: they cannot flip the criterion to NC nor enter conformancePct). Optional.
|
|
@@ -240,7 +247,7 @@ function collapse(harvested: Harvested[]): { evidence: Evidence[]; population: E
|
|
|
240
247
|
return {
|
|
241
248
|
...rep.ev,
|
|
242
249
|
...(group.length > 1 ? { occurrences: group.length } : {}),
|
|
243
|
-
...(others.length ? { alsoAt: others.
|
|
250
|
+
...(others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {}),
|
|
244
251
|
...(pages.length ? { pages } : {}),
|
|
245
252
|
};
|
|
246
253
|
});
|
|
@@ -330,6 +337,63 @@ export function buildAdjudicationWorklist(audit: AuditResult, opts: { cwd?: stri
|
|
|
330
337
|
);
|
|
331
338
|
}
|
|
332
339
|
|
|
340
|
+
/** Read a citation whichever way it was written.
|
|
341
|
+
*
|
|
342
|
+
* The contract asks for `{file, line, …}`, and a real run wrote `"src/Foo.tsx:15"` instead —
|
|
343
|
+
* 63 verdicts refused for a shape, not for a claim. It is an unambiguous form and the
|
|
344
|
+
* worklist itself is full of it (`alsoAt` is exactly `file:line`), so an adjudicator
|
|
345
|
+
* reaching for it is being consistent, not careless.
|
|
346
|
+
*
|
|
347
|
+
* Nothing about the gate moves: membership and grounding run on the result either way. A
|
|
348
|
+
* string simply carries no snippet, so grounding falls back to its selector/line checks —
|
|
349
|
+
* weaker evidence, and the reason the object form stays what the contract asks for. */
|
|
350
|
+
export function readCitation(c: Evidence | string): Evidence | null {
|
|
351
|
+
if (typeof c !== "string") return c;
|
|
352
|
+
const at = c.lastIndexOf(":");
|
|
353
|
+
if (at <= 0) return null;
|
|
354
|
+
const line = Number.parseInt(c.slice(at + 1), 10);
|
|
355
|
+
if (!Number.isFinite(line)) return null;
|
|
356
|
+
return { file: c.slice(0, at), line, selector: "", snippet: "" };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** The files this audit actually read. Computed from the audit's own scope inputs, so it is
|
|
360
|
+
* the same set the harvest walked — `discover` only globs, it does not parse, so this costs
|
|
361
|
+
* little and is memoised per fold. */
|
|
362
|
+
function auditFiles(audit: AuditResult, cwd?: string): Set<string> {
|
|
363
|
+
const inputs = audit.scope.inputs.filter((i) => i !== "-" && i !== "<stdin>");
|
|
364
|
+
if (!inputs.length) return new Set();
|
|
365
|
+
try {
|
|
366
|
+
void cwd; // `discover` returns the same repo-relative paths the harvest anchors use
|
|
367
|
+
return new Set(discover(inputs, {}).files);
|
|
368
|
+
} catch {
|
|
369
|
+
return new Set();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Does this citation name evidence the criterion actually carries?
|
|
374
|
+
*
|
|
375
|
+
* Same file, and within the drift `groundFinding` already tolerates. Exact line equality was
|
|
376
|
+
* too literal to be useful: the harvest anchors a <table> at line 19, the agent cites the
|
|
377
|
+
* <caption> at line 20 — which is the very thing RGAA 5.4 asks about, inside the element it
|
|
378
|
+
* was shown — and the gate called it fabricated. Measured on a real run, that class of
|
|
379
|
+
* refusal cost more criteria than every other cause combined.
|
|
380
|
+
*
|
|
381
|
+
* What the check still catches is what it was written for: a citation pointing at a file the
|
|
382
|
+
* criterion was never given. And it is only half the gate — `groundFinding` still has to find
|
|
383
|
+
* the cited content in the real source, so a plausible-looking file:line in the right
|
|
384
|
+
* neighbourhood proves nothing on its own. */
|
|
385
|
+
const CITE_DRIFT = 10;
|
|
386
|
+
function cites0(anchors: string[], c: { file: string; line: number }): boolean {
|
|
387
|
+
for (const a of anchors) {
|
|
388
|
+
const at = a.lastIndexOf(":");
|
|
389
|
+
if (at < 0) continue;
|
|
390
|
+
if (a.slice(0, at) !== c.file) continue;
|
|
391
|
+
const line = Number.parseInt(a.slice(at + 1), 10);
|
|
392
|
+
if (Number.isFinite(line) && Math.abs(line - c.line) <= CITE_DRIFT) return true;
|
|
393
|
+
}
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
333
397
|
export interface ApplyAdjudicationResult {
|
|
334
398
|
ok: boolean;
|
|
335
399
|
audit: AuditResult;
|
|
@@ -521,6 +585,9 @@ export function applyAdjudication(
|
|
|
521
585
|
// flat list) for the same reason as `blame`: a failed citation has to condemn its own
|
|
522
586
|
// criterion and no other.
|
|
523
587
|
const groundInputs = new Map<string, { file: string; line: number; selector?: string; snippet?: string }[]>();
|
|
588
|
+
// Memoised: only a citation that missed its criterion's own anchors ever asks for it.
|
|
589
|
+
let scopeCache: Set<string> | undefined;
|
|
590
|
+
const scopeFiles = (): Set<string> => (scopeCache ??= auditFiles(audit, opts.cwd));
|
|
524
591
|
const toGround = (criteriaId: string, g: { file: string; line: number; selector?: string; snippet?: string }) => {
|
|
525
592
|
const list = groundInputs.get(criteriaId);
|
|
526
593
|
if (list) list.push(g);
|
|
@@ -535,7 +602,7 @@ export function applyAdjudication(
|
|
|
535
602
|
// A clearing verdict is gated exactly like an accusing one. Before this, the only
|
|
536
603
|
// check was "the justification is a non-empty string", so `"x"` cleared a criterion —
|
|
537
604
|
// and a model answering C to everything published a conformance nobody had assessed.
|
|
538
|
-
const cites = it.citations ?? [];
|
|
605
|
+
const cites = (it.citations ?? []).map(readCitation).filter((c): c is Evidence => c !== null);
|
|
539
606
|
// An INCOMPLETE reading may report a failure it saw; it may never clear what it never
|
|
540
607
|
// looked at. Before class-based harvesting the evidence was a 30-anchor sample of a
|
|
541
608
|
// population that ran to thousands, so a `C` was routinely a conformity claim over
|
|
@@ -557,12 +624,21 @@ export function applyAdjudication(
|
|
|
557
624
|
`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`,
|
|
558
625
|
);
|
|
559
626
|
}
|
|
560
|
-
} else if (cites.length === 0) {
|
|
627
|
+
} else if (cites.length === 0 && v === "C") {
|
|
628
|
+
// Only a `C` must cite. A `C` says "everything here conforms", so it has to name what
|
|
629
|
+
// it cleared; an `NA` says "none of this is what the criterion is about", and there is
|
|
630
|
+
// nothing to clear — asking it to cite the very elements it just ruled out of scope is
|
|
631
|
+
// a contradiction, and one the engine does not hold ITSELF to: an engine-proved NA
|
|
632
|
+
// (src/audit.ts subjectMatterReason) carries a justification naming what was searched
|
|
633
|
+
// for, and no citations at all. Measured on a real run, six criteria were refused for
|
|
634
|
+
// this alone, each of them an honest NA.
|
|
635
|
+
//
|
|
636
|
+
// The justification is what keeps an NA falsifiable, and it is still required above.
|
|
561
637
|
blame(
|
|
562
638
|
it.criteriaId,
|
|
563
|
-
`criterion ${it.criteriaId}: a
|
|
639
|
+
`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, …}])`,
|
|
564
640
|
);
|
|
565
|
-
} else {
|
|
641
|
+
} else if (cites.length > 0) {
|
|
566
642
|
// Each citation must name evidence THIS item carried. Grounding alone would only
|
|
567
643
|
// prove the anchor exists somewhere in the tree; the pairing proves the agent ruled
|
|
568
644
|
// on what it was actually given.
|
|
@@ -576,9 +652,22 @@ export function applyAdjudication(
|
|
|
576
652
|
// exactly how RGAA 5.8, 9.2 and 11.1 were refused. Membership was the defect;
|
|
577
653
|
// grounding — which proves the citation resolves against real source — is unchanged
|
|
578
654
|
// and still runs on every one of them.
|
|
579
|
-
const anchors =
|
|
655
|
+
const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...(e.alsoAt ?? [])]);
|
|
580
656
|
for (const c of cites) {
|
|
581
|
-
|
|
657
|
+
// Two ways to belong, and the second one is what a good adjudicator does.
|
|
658
|
+
//
|
|
659
|
+
// The harvest anchors a rendered page (`.ultra11y/pages/<id>/dom.html`) because that
|
|
660
|
+
// is what proves the browser's output — but the place a reader goes to FIX it is the
|
|
661
|
+
// component that produced it, and that is what the agent naturally cites. Measured
|
|
662
|
+
// on a real run: 15 of the 16 citations still refused after the drift tolerance were
|
|
663
|
+
// in files this audit had read, pointing at the source behind the evidence. Refusing
|
|
664
|
+
// them called correct work fabricated.
|
|
665
|
+
//
|
|
666
|
+
// So a citation also belongs when it lands in a file the audit read. That is a real
|
|
667
|
+
// bound — a file outside the audited scope is still refused — and it is not the last
|
|
668
|
+
// one: `groundFinding` below still has to find the cited content at that file:line in
|
|
669
|
+
// the real source, which is what "not fabricated" actually means.
|
|
670
|
+
if (!cites0(anchors, c) && !scopeFiles().has(c.file)) {
|
|
582
671
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c.file}:${c.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
583
672
|
}
|
|
584
673
|
toGround(it.criteriaId, { file: c.file, line: c.line, selector: c.selector, snippet: c.snippet });
|
|
@@ -829,6 +918,7 @@ const T = {
|
|
|
829
918
|
occurrences: "occurrences",
|
|
830
919
|
pagesWord: "page(s)",
|
|
831
920
|
on: "sur",
|
|
921
|
+
alsoAt: "aussi en",
|
|
832
922
|
incomplete: "LECTURE INCOMPLÈTE — un « C » sera refusé sur ce critère",
|
|
833
923
|
none: "(aucune évidence automatique — décidez depuis la source, ou laissez `manual` avec une raison)",
|
|
834
924
|
questions: "À vérifier manuellement",
|
|
@@ -858,6 +948,7 @@ const T = {
|
|
|
858
948
|
occurrences: "occurrences",
|
|
859
949
|
pagesWord: "page(s)",
|
|
860
950
|
on: "on",
|
|
951
|
+
alsoAt: "also at",
|
|
861
952
|
incomplete: "INCOMPLETE READING — a C will be refused on this criterion",
|
|
862
953
|
none: "(no automatic evidence — decide from source, or leave `manual` with a reason)",
|
|
863
954
|
questions: "To verify manually",
|
|
@@ -1003,6 +1094,7 @@ export function formatAdjudication(items: AdjudicationItem[], lang: Lang = "en",
|
|
|
1003
1094
|
const extra = [
|
|
1004
1095
|
e.occurrences && e.occurrences > 1 ? `×${e.occurrences}` : "",
|
|
1005
1096
|
e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "…" : ""}` : "",
|
|
1097
|
+
e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "…" : ""}` : "",
|
|
1006
1098
|
]
|
|
1007
1099
|
.filter(Boolean)
|
|
1008
1100
|
.join(" ");
|
package/src/ledger.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { createHash } from "node:crypto";
|
|
|
30
30
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
31
31
|
import { dirname, join } from "node:path";
|
|
32
32
|
|
|
33
|
-
import { type AdjudicationFile, type AdjudicationItem, type AgentFinding, buildAdjudicationWorklist, type Evidence } from "./adjudicate.js";
|
|
33
|
+
import { type AdjudicationFile, type AdjudicationItem, type AgentFinding, buildAdjudicationWorklist, type Evidence, readCitation } from "./adjudicate.js";
|
|
34
34
|
import { CORE, type StandardId } from "./standards/index.js";
|
|
35
35
|
import type { AuditResult } from "./types.js";
|
|
36
36
|
import { SCHEMA_VERSION } from "./types.js";
|
|
@@ -148,7 +148,9 @@ export function entriesFrom(adj: AdjudicationFile, accepted: ReadonlySet<string>
|
|
|
148
148
|
verdict: it.verdict,
|
|
149
149
|
...(it.justification?.trim() ? { justification: it.justification.trim() } : {}),
|
|
150
150
|
...(it.reason ? { reason: it.reason } : {}),
|
|
151
|
-
|
|
151
|
+
// Normalised here too: a citation may have arrived as a bare "file:line" string, and a
|
|
152
|
+
// ledger entry has to hold the object form so a replay can re-anchor and re-ground it.
|
|
153
|
+
...(it.citations?.length ? { citations: it.citations.map(readCitation).filter((c): c is Evidence => c !== null) } : {}),
|
|
152
154
|
...(it.findings?.length ? { findings: it.findings } : {}),
|
|
153
155
|
...(it.recommendations?.length ? { recommendations: it.recommendations } : {}),
|
|
154
156
|
evidenceFingerprint: evidenceFingerprint(it.evidence),
|
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.4";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|