ultra11y 5.43.0 → 5.43.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +63 -15
- package/scripts/vendor/wcag-2.2-fr.json +1 -1
- package/scripts/vendor/wcag-2.2-sc-universe.json +1 -1
- package/scripts/vendor/wcag-2.2-techniques.json +1 -1
- package/scripts/vendor/wcag-2.2-text.json +1 -1
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +63 -15
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/references/orchestrators.md +44 -16
- package/skills/ultra11y/references/verify.md +12 -3
- package/skills/ultra11y/scripts/ultra11y.mjs +63 -15
- package/skills/ultra11y/templates/gitlab-ci.yml +1 -1
- package/src/adjudicate.ts +64 -10
- package/src/cli.ts +53 -2
- package/src/data/wcag-universe.json +1 -1
- package/src/grounding.ts +18 -3
- package/src/types.ts +1 -1
- package/src/verify.ts +12 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "ultra11y",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Automatic accessibility review before commit, push and pull request, plus detailed evidenced Markdown, HTML and per-page WCAG 2.2 AA / RGAA reports in Claude Code.",
|
|
13
|
-
"version": "5.43.
|
|
13
|
+
"version": "5.43.2",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/SocialGouv/ultra11y",
|
|
16
16
|
"repository": "https://github.com/SocialGouv/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): automatic review before commit, push and pull request, plus detailed evidenced Markdown, HTML and per-page conformance reports in interactive agent sessions.",
|
|
4
|
-
"version": "5.43.
|
|
4
|
+
"version": "5.43.2",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/SocialGouv/ultra11y",
|
|
7
7
|
"repository": "https://github.com/SocialGouv/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): automatic review before commit, push and pull request, plus detailed evidenced Markdown, HTML and per-page conformance reports in interactive agent sessions.",
|
|
4
|
-
"version": "5.43.
|
|
4
|
+
"version": "5.43.2",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/SocialGouv/ultra11y",
|
|
7
7
|
"repository": "https://github.com/SocialGouv/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.43.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.43.2";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -577,7 +577,7 @@ pnpm run build:icons # re-generate the browser extension's icons from their
|
|
|
577
577
|
```
|
|
578
578
|
|
|
579
579
|
Releases are cut automatically by **semantic-release** on push to `main` (GitHub release +
|
|
580
|
-
npm publish via OIDC trusted publishing, plus the moving `
|
|
580
|
+
npm publish via OIDC trusted publishing, plus the moving `v5` major tag). The version bump is
|
|
581
581
|
decided by **Conventional Commits**: a commit whose subject carries no `feat:`/`fix:` prefix
|
|
582
582
|
is analysed as *no release*, so a change that must ship needs at least one conventional commit
|
|
583
583
|
in the range — otherwise the major alias and npm keep serving the previous version while `main`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.43.
|
|
3
|
+
"version": "5.43.2",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join as join52, relative as relative6, resolve as resolve17, 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.43.
|
|
9
|
+
var VERSION = "5.43.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -2298,7 +2298,7 @@ var wcag_universe_default = {
|
|
|
2298
2298
|
wcagVersion: "2.2",
|
|
2299
2299
|
source: "https://www.w3.org/TR/WCAG22/",
|
|
2300
2300
|
criteriaSource: "https://github.com/w3c/wcag",
|
|
2301
|
-
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-10-
|
|
2301
|
+
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-10-19 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
2302
2302
|
criteria: [
|
|
2303
2303
|
{
|
|
2304
2304
|
id: "1.1.1",
|
|
@@ -62643,9 +62643,12 @@ var resumableItemKey = (it) => {
|
|
|
62643
62643
|
return `${it.kind ?? "nc"}|${it.criteriaId}|${canonicalFile(it.file)}|${it.line}|${it.selector}|${it.claim}|${citationSetKey(it)}`;
|
|
62644
62644
|
};
|
|
62645
62645
|
var legacyConformityKey = (criteriaId, file, line, selector, claim) => `${criteriaId}|${canonicalFile(file)}|${line}|${selector}|${claim}`;
|
|
62646
|
-
function verifyGroundingInputs(items) {
|
|
62646
|
+
function verifyGroundingInputs(items, fallbackFor) {
|
|
62647
62647
|
return items.flatMap(
|
|
62648
|
-
(it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) =>
|
|
62648
|
+
(it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) => {
|
|
62649
|
+
const fallback = fallbackFor?.(it, cite);
|
|
62650
|
+
return { file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet, ...fallback ? { fallback } : {} };
|
|
62651
|
+
}) : [{ file: it.file, line: it.line, selector: it.selector, snippet: it.snippet }]
|
|
62649
62652
|
);
|
|
62650
62653
|
}
|
|
62651
62654
|
var PASSING = /* @__PURE__ */ new Set(["supported", "partial"]);
|
|
@@ -62733,7 +62736,7 @@ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
|
|
|
62733
62736
|
import { resolve as resolve10 } from "path";
|
|
62734
62737
|
var WINDOW = 10;
|
|
62735
62738
|
var norm2 = (s) => s.normalize("NFKC").replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, " ").replace(/>\s+/g, ">").replace(/\s+</g, "<").trim();
|
|
62736
|
-
var isUnresolvable = (file) => !file || file === "-" || file === "<stdin>" || file === "stdin";
|
|
62739
|
+
var isUnresolvable = (file) => !file || file === "-" || file === "<stdin>" || file === "stdin" || /^[a-z][a-z0-9+.-]*:\/\//.test(file);
|
|
62737
62740
|
var SEMANTIC_SELECTORS = /* @__PURE__ */ new Set(["handler"]);
|
|
62738
62741
|
function selectorProbes(selector) {
|
|
62739
62742
|
if (SEMANTIC_SELECTORS.has(selector.trim().toLowerCase())) return [];
|
|
@@ -62783,7 +62786,11 @@ function groundFinding(g, opts = {}) {
|
|
|
62783
62786
|
function groundItems(items, opts = {}) {
|
|
62784
62787
|
const s = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
62785
62788
|
for (const it of items) {
|
|
62786
|
-
|
|
62789
|
+
let r = groundFinding(it, opts);
|
|
62790
|
+
if (!r.ok && it.fallback) {
|
|
62791
|
+
const viaAnchor = groundFinding(it.fallback, opts);
|
|
62792
|
+
if (viaAnchor.ok) r = { ok: true, moved: true };
|
|
62793
|
+
}
|
|
62787
62794
|
if (r.ok) {
|
|
62788
62795
|
s.grounded++;
|
|
62789
62796
|
if (r.moved) s.moved++;
|
|
@@ -64210,6 +64217,23 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
64210
64217
|
(r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc2(r.criteriaId), docs), limits)
|
|
64211
64218
|
);
|
|
64212
64219
|
}
|
|
64220
|
+
function evidenceForCriteria(audit2, criteriaIds, opts = {}) {
|
|
64221
|
+
const limits = adjudicationLimits(opts.cwd);
|
|
64222
|
+
const docs = docsForAudit(audit2, opts.cwd);
|
|
64223
|
+
const standard = opts.standard;
|
|
64224
|
+
const pack = standard !== void 0 && !isCore(standard) ? loadPack(standard) : void 0;
|
|
64225
|
+
const candidates2 = new Map(pack && standard ? derivePackResults(audit2, standard).map((pc) => [pc.id, pc.candidateFindings ?? []]) : []);
|
|
64226
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
64227
|
+
for (const id of criteriaIds) {
|
|
64228
|
+
if (out2.has(id)) continue;
|
|
64229
|
+
const crit = pack ? getCriterion(pack, id) : void 0;
|
|
64230
|
+
const subjects = pack && standard ? subjectsForPackCriterion2(standard, id, crit?.wcag ?? []) : subjectsForSc2(id);
|
|
64231
|
+
let evidence = collapse2(harvestSubjects(subjects, docs), limits).evidence;
|
|
64232
|
+
if (!evidence.length) evidence = candidateFindingEvidence(candidates2.get(id) ?? [], limits.maxClasses);
|
|
64233
|
+
out2.set(id, evidence);
|
|
64234
|
+
}
|
|
64235
|
+
return out2;
|
|
64236
|
+
}
|
|
64213
64237
|
function unrenderedResidual(audit2, items) {
|
|
64214
64238
|
const audited = audit2.scope.pagesAudited;
|
|
64215
64239
|
const readSomePage = audited === void 0 ? (audit2.scope.pages ?? []).length > 0 : audited.length > 0;
|
|
@@ -64336,6 +64360,14 @@ function cites0(anchors, c2, drift) {
|
|
|
64336
64360
|
}
|
|
64337
64361
|
return false;
|
|
64338
64362
|
}
|
|
64363
|
+
function evidenceFallback(evidence, c2, drift) {
|
|
64364
|
+
const anchor = anchorFor(evidence, c2, drift);
|
|
64365
|
+
if (!anchor || !recognisablySame(c2, anchor.at)) return { onAnchor: anchor !== void 0 };
|
|
64366
|
+
return {
|
|
64367
|
+
onAnchor: true,
|
|
64368
|
+
fallback: anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
|
|
64369
|
+
};
|
|
64370
|
+
}
|
|
64339
64371
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
64340
64372
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
64341
64373
|
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
@@ -64485,16 +64517,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
64485
64517
|
if (!cites0(anchors, c2, citationDrift) && !inAuditScope(c2.file)) {
|
|
64486
64518
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
64487
64519
|
}
|
|
64488
|
-
const anchor = anchorFor(it.evidence, c2, citationDrift);
|
|
64489
64520
|
const cite = { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet };
|
|
64490
|
-
|
|
64491
|
-
|
|
64492
|
-
|
|
64493
|
-
cite,
|
|
64494
|
-
anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
|
|
64495
|
-
);
|
|
64521
|
+
const { fallback, onAnchor } = evidenceFallback(it.evidence, c2, citationDrift);
|
|
64522
|
+
if (fallback) {
|
|
64523
|
+
toGround(it.criteriaId, cite, fallback);
|
|
64496
64524
|
} else {
|
|
64497
|
-
toGround(it.criteriaId, cite, void 0,
|
|
64525
|
+
toGround(it.criteriaId, cite, void 0, onAnchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
|
|
64498
64526
|
}
|
|
64499
64527
|
}
|
|
64500
64528
|
}
|
|
@@ -74398,6 +74426,11 @@ Options:
|
|
|
74398
74426
|
(requires --report \u2014 coverage is re-derived from the report, uncapped)
|
|
74399
74427
|
Also accepts an ADJUDICATION (--in <audit.json>), or a VERDICT LEDGER,
|
|
74400
74428
|
which it REPLAYS onto the audit with no model in the loop
|
|
74429
|
+
Retained conformities are grounded the way the fold grounded them:
|
|
74430
|
+
a citation that is one of its criterion's own harvested anchors
|
|
74431
|
+
(--in <audit.json>) falls back to that anchor, so a retyping the
|
|
74432
|
+
fold accepted is not refused again at trial; without the audit the
|
|
74433
|
+
cited snippet must match the source literally
|
|
74401
74434
|
--ledger <file> verify --apply / judge --apply: record the verdicts the fold ACCEPTED,
|
|
74402
74435
|
so a later run replays them for free (default:
|
|
74403
74436
|
.ultra11y/verdicts/<standard>.json). Replay re-derives the evidence
|
|
@@ -76267,7 +76300,7 @@ function cmdVerify(p) {
|
|
|
76267
76300
|
const expected = [...expectedNc, ...buildConformityWorklist(conformityClaimsFor(p, standard, lang), expectedNc.length, Number.POSITIVE_INFINITY)];
|
|
76268
76301
|
const r = applyVerdicts(items, expected);
|
|
76269
76302
|
const passing = items.filter((it) => typeof it.verdict === "string" && ["supported", "partial"].includes(it.verdict.trim().toLowerCase()));
|
|
76270
|
-
const grounding = groundItems(verifyGroundingInputs(passing));
|
|
76303
|
+
const grounding = groundItems(verifyGroundingInputs(passing, trialAnchorFallbacks(p, standard, passing)));
|
|
76271
76304
|
const pruned = p.flags.prune === true ? applyPrune(p, standard, items, lang) : void 0;
|
|
76272
76305
|
const ok = (pruned ? r.unadjudicated + r.invalid + r.missing === 0 : r.ok) && grounding.failed === 0;
|
|
76273
76306
|
if (p.flags.json) console.log(JSON.stringify({ ...r, ok, grounding, ...pruned ? { pruned } : {} }, null, 2));
|
|
@@ -76425,6 +76458,21 @@ function cmdVerifyWorklist(p, langIn) {
|
|
|
76425
76458
|
);
|
|
76426
76459
|
return 0;
|
|
76427
76460
|
}
|
|
76461
|
+
function trialAnchorFallbacks(p, standard, passing) {
|
|
76462
|
+
const ids = [...new Set(passing.filter((it) => it.kind === "c").map((it) => it.criteriaId))];
|
|
76463
|
+
const inFlag = p.flags.in;
|
|
76464
|
+
if (!ids.length || typeof inFlag !== "string" || !inFlag || inFlag === "-") return void 0;
|
|
76465
|
+
let audit2;
|
|
76466
|
+
try {
|
|
76467
|
+
audit2 = unwrapAudit(JSON.parse(readText(inFlag)));
|
|
76468
|
+
} catch {
|
|
76469
|
+
return void 0;
|
|
76470
|
+
}
|
|
76471
|
+
const cwd = typeof p.flags.cwd === "string" ? p.flags.cwd : void 0;
|
|
76472
|
+
const evidence = evidenceForCriteria(audit2, ids, { cwd, standard });
|
|
76473
|
+
const { citationDrift } = adjudicationLimits(cwd);
|
|
76474
|
+
return (it, cite) => evidenceFallback(evidence.get(it.criteriaId) ?? [], cite, citationDrift).fallback;
|
|
76475
|
+
}
|
|
76428
76476
|
function conformityClaimsFor(p, standard, lang) {
|
|
76429
76477
|
if (p.flags.conformities === false || p.flags["no-conformities"] === true) return [];
|
|
76430
76478
|
const named3 = typeof p.flags.conformities === "string" && p.flags.conformities ? p.flags.conformities : void 0;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"wcagVersion": "2.2",
|
|
3
3
|
"source": "https://www.w3.org/TR/WCAG22/",
|
|
4
4
|
"criteriaSource": "https://github.com/w3c/wcag",
|
|
5
|
-
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-10-
|
|
5
|
+
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-10-19 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
6
6
|
"principles": [
|
|
7
7
|
{
|
|
8
8
|
"number": 1,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"source": "https://github.com/w3c/wcag",
|
|
3
3
|
"understanding": "https://www.w3.org/WAI/WCAG22/Understanding/",
|
|
4
4
|
"license": "W3C Document License (code samples: W3C Software License)",
|
|
5
|
-
"fetchedAt": "2026-10-
|
|
5
|
+
"fetchedAt": "2026-10-19",
|
|
6
6
|
"criteria": {
|
|
7
7
|
"1.1.1": {
|
|
8
8
|
"slug": "non-text-content",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "https://github.com/w3c/wcag",
|
|
3
3
|
"license": "W3C Document License",
|
|
4
|
-
"fetchedAt": "2026-10-
|
|
4
|
+
"fetchedAt": "2026-10-19",
|
|
5
5
|
"criteria": {
|
|
6
6
|
"1.1.1": "All non-text content that is presented to the user has a text alternative that serves the equivalent purpose, except for the situations listed below.\nControls, Input:\nIf non-text content is a control or accepts user input, then it has a name that describes its purpose. (Refer to Success Criterion 4.1.2 Name, Role, Value for additional requirements for controls and content that accepts user input.)\nTime-Based Media:\nIf non-text content is time-based media, then text alternatives at least provide descriptive identification of the non-text content. (Refer to Guideline 1.2 for additional requirements for media.)\nTest:\nIf non-text content is a test or exercise that would be invalid if presented in text, then text alternatives at least provide descriptive identification of the non-text content.\nSensory:\nIf non-text content is primarily intended to create a specific sensory experience, then text alternatives at least provide descriptive identification of the non-text content.\nCAPTCHA:\nIf the purpose of non-text content is to confirm that content is being accessed by a person rather than a computer, then text alternatives that identify and describe the purpose of the non-text content are provided, and alternative forms of CAPTCHA using output modes for different types of sensory perception are provided to accommodate different disabilities.\nDecoration, Formatting, Invisible:\nIf non-text content is pure decoration, is used only for visual formatting, or is not presented to users, then it is implemented in a way that it can be ignored by assistive technology.",
|
|
7
7
|
"1.2.1": "For prerecorded audio-only and prerecorded video-only media, the following are true, except when the audio or video is a media alternative for text and is clearly labeled as such:\nPrerecorded Audio-only:\nAn alternative for time-based media is provided that presents equivalent information for prerecorded audio-only content.\nPrerecorded Video-only:\nEither an alternative for time-based media or an audio track is provided that presents equivalent information for prerecorded video-only content.",
|
|
@@ -3,7 +3,7 @@ name: review-a11y
|
|
|
3
3
|
description: "Use to REVIEW changed frontend code for accessibility: staged files, a working diff, branch, or PR; prompts such as 'review a11y', 'is this accessible?', or 'anything to fix before merge?'; and hook or CI findings on a change. Run the bundled install-free engine with cross-file JSX/TSX analysis, then adjudicate candidates and return a severity-ranked WCAG 2.2 AA review with file:line fixes, residual rendering risks, and a verdict. Use `ultra11y` instead for whole-repo audits, dated conformance reports, country standards, PRD backlogs, or authoring accessible markup."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: 5.43.
|
|
6
|
+
version: 5.43.2
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# review-a11y — accessibility review of changed code
|
|
@@ -6,7 +6,7 @@ import { join as join52, relative as relative6, resolve as resolve17, 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.43.
|
|
9
|
+
var VERSION = "5.43.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -2298,7 +2298,7 @@ var wcag_universe_default = {
|
|
|
2298
2298
|
wcagVersion: "2.2",
|
|
2299
2299
|
source: "https://www.w3.org/TR/WCAG22/",
|
|
2300
2300
|
criteriaSource: "https://github.com/w3c/wcag",
|
|
2301
|
-
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-10-
|
|
2301
|
+
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-10-19 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
2302
2302
|
criteria: [
|
|
2303
2303
|
{
|
|
2304
2304
|
id: "1.1.1",
|
|
@@ -62643,9 +62643,12 @@ var resumableItemKey = (it) => {
|
|
|
62643
62643
|
return `${it.kind ?? "nc"}|${it.criteriaId}|${canonicalFile(it.file)}|${it.line}|${it.selector}|${it.claim}|${citationSetKey(it)}`;
|
|
62644
62644
|
};
|
|
62645
62645
|
var legacyConformityKey = (criteriaId, file, line, selector, claim) => `${criteriaId}|${canonicalFile(file)}|${line}|${selector}|${claim}`;
|
|
62646
|
-
function verifyGroundingInputs(items) {
|
|
62646
|
+
function verifyGroundingInputs(items, fallbackFor) {
|
|
62647
62647
|
return items.flatMap(
|
|
62648
|
-
(it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) =>
|
|
62648
|
+
(it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) => {
|
|
62649
|
+
const fallback = fallbackFor?.(it, cite);
|
|
62650
|
+
return { file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet, ...fallback ? { fallback } : {} };
|
|
62651
|
+
}) : [{ file: it.file, line: it.line, selector: it.selector, snippet: it.snippet }]
|
|
62649
62652
|
);
|
|
62650
62653
|
}
|
|
62651
62654
|
var PASSING = /* @__PURE__ */ new Set(["supported", "partial"]);
|
|
@@ -62733,7 +62736,7 @@ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
|
|
|
62733
62736
|
import { resolve as resolve10 } from "path";
|
|
62734
62737
|
var WINDOW = 10;
|
|
62735
62738
|
var norm2 = (s) => s.normalize("NFKC").replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, " ").replace(/>\s+/g, ">").replace(/\s+</g, "<").trim();
|
|
62736
|
-
var isUnresolvable = (file) => !file || file === "-" || file === "<stdin>" || file === "stdin";
|
|
62739
|
+
var isUnresolvable = (file) => !file || file === "-" || file === "<stdin>" || file === "stdin" || /^[a-z][a-z0-9+.-]*:\/\//.test(file);
|
|
62737
62740
|
var SEMANTIC_SELECTORS = /* @__PURE__ */ new Set(["handler"]);
|
|
62738
62741
|
function selectorProbes(selector) {
|
|
62739
62742
|
if (SEMANTIC_SELECTORS.has(selector.trim().toLowerCase())) return [];
|
|
@@ -62783,7 +62786,11 @@ function groundFinding(g, opts = {}) {
|
|
|
62783
62786
|
function groundItems(items, opts = {}) {
|
|
62784
62787
|
const s = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
62785
62788
|
for (const it of items) {
|
|
62786
|
-
|
|
62789
|
+
let r = groundFinding(it, opts);
|
|
62790
|
+
if (!r.ok && it.fallback) {
|
|
62791
|
+
const viaAnchor = groundFinding(it.fallback, opts);
|
|
62792
|
+
if (viaAnchor.ok) r = { ok: true, moved: true };
|
|
62793
|
+
}
|
|
62787
62794
|
if (r.ok) {
|
|
62788
62795
|
s.grounded++;
|
|
62789
62796
|
if (r.moved) s.moved++;
|
|
@@ -64210,6 +64217,23 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
64210
64217
|
(r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc2(r.criteriaId), docs), limits)
|
|
64211
64218
|
);
|
|
64212
64219
|
}
|
|
64220
|
+
function evidenceForCriteria(audit2, criteriaIds, opts = {}) {
|
|
64221
|
+
const limits = adjudicationLimits(opts.cwd);
|
|
64222
|
+
const docs = docsForAudit(audit2, opts.cwd);
|
|
64223
|
+
const standard = opts.standard;
|
|
64224
|
+
const pack = standard !== void 0 && !isCore(standard) ? loadPack(standard) : void 0;
|
|
64225
|
+
const candidates2 = new Map(pack && standard ? derivePackResults(audit2, standard).map((pc) => [pc.id, pc.candidateFindings ?? []]) : []);
|
|
64226
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
64227
|
+
for (const id of criteriaIds) {
|
|
64228
|
+
if (out2.has(id)) continue;
|
|
64229
|
+
const crit = pack ? getCriterion(pack, id) : void 0;
|
|
64230
|
+
const subjects = pack && standard ? subjectsForPackCriterion2(standard, id, crit?.wcag ?? []) : subjectsForSc2(id);
|
|
64231
|
+
let evidence = collapse2(harvestSubjects(subjects, docs), limits).evidence;
|
|
64232
|
+
if (!evidence.length) evidence = candidateFindingEvidence(candidates2.get(id) ?? [], limits.maxClasses);
|
|
64233
|
+
out2.set(id, evidence);
|
|
64234
|
+
}
|
|
64235
|
+
return out2;
|
|
64236
|
+
}
|
|
64213
64237
|
function unrenderedResidual(audit2, items) {
|
|
64214
64238
|
const audited = audit2.scope.pagesAudited;
|
|
64215
64239
|
const readSomePage = audited === void 0 ? (audit2.scope.pages ?? []).length > 0 : audited.length > 0;
|
|
@@ -64336,6 +64360,14 @@ function cites0(anchors, c2, drift) {
|
|
|
64336
64360
|
}
|
|
64337
64361
|
return false;
|
|
64338
64362
|
}
|
|
64363
|
+
function evidenceFallback(evidence, c2, drift) {
|
|
64364
|
+
const anchor = anchorFor(evidence, c2, drift);
|
|
64365
|
+
if (!anchor || !recognisablySame(c2, anchor.at)) return { onAnchor: anchor !== void 0 };
|
|
64366
|
+
return {
|
|
64367
|
+
onAnchor: true,
|
|
64368
|
+
fallback: anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
|
|
64369
|
+
};
|
|
64370
|
+
}
|
|
64339
64371
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
64340
64372
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
64341
64373
|
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
@@ -64485,16 +64517,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
64485
64517
|
if (!cites0(anchors, c2, citationDrift) && !inAuditScope(c2.file)) {
|
|
64486
64518
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
64487
64519
|
}
|
|
64488
|
-
const anchor = anchorFor(it.evidence, c2, citationDrift);
|
|
64489
64520
|
const cite = { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet };
|
|
64490
|
-
|
|
64491
|
-
|
|
64492
|
-
|
|
64493
|
-
cite,
|
|
64494
|
-
anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
|
|
64495
|
-
);
|
|
64521
|
+
const { fallback, onAnchor } = evidenceFallback(it.evidence, c2, citationDrift);
|
|
64522
|
+
if (fallback) {
|
|
64523
|
+
toGround(it.criteriaId, cite, fallback);
|
|
64496
64524
|
} else {
|
|
64497
|
-
toGround(it.criteriaId, cite, void 0,
|
|
64525
|
+
toGround(it.criteriaId, cite, void 0, onAnchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
|
|
64498
64526
|
}
|
|
64499
64527
|
}
|
|
64500
64528
|
}
|
|
@@ -74398,6 +74426,11 @@ Options:
|
|
|
74398
74426
|
(requires --report \u2014 coverage is re-derived from the report, uncapped)
|
|
74399
74427
|
Also accepts an ADJUDICATION (--in <audit.json>), or a VERDICT LEDGER,
|
|
74400
74428
|
which it REPLAYS onto the audit with no model in the loop
|
|
74429
|
+
Retained conformities are grounded the way the fold grounded them:
|
|
74430
|
+
a citation that is one of its criterion's own harvested anchors
|
|
74431
|
+
(--in <audit.json>) falls back to that anchor, so a retyping the
|
|
74432
|
+
fold accepted is not refused again at trial; without the audit the
|
|
74433
|
+
cited snippet must match the source literally
|
|
74401
74434
|
--ledger <file> verify --apply / judge --apply: record the verdicts the fold ACCEPTED,
|
|
74402
74435
|
so a later run replays them for free (default:
|
|
74403
74436
|
.ultra11y/verdicts/<standard>.json). Replay re-derives the evidence
|
|
@@ -76267,7 +76300,7 @@ function cmdVerify(p) {
|
|
|
76267
76300
|
const expected = [...expectedNc, ...buildConformityWorklist(conformityClaimsFor(p, standard, lang), expectedNc.length, Number.POSITIVE_INFINITY)];
|
|
76268
76301
|
const r = applyVerdicts(items, expected);
|
|
76269
76302
|
const passing = items.filter((it) => typeof it.verdict === "string" && ["supported", "partial"].includes(it.verdict.trim().toLowerCase()));
|
|
76270
|
-
const grounding = groundItems(verifyGroundingInputs(passing));
|
|
76303
|
+
const grounding = groundItems(verifyGroundingInputs(passing, trialAnchorFallbacks(p, standard, passing)));
|
|
76271
76304
|
const pruned = p.flags.prune === true ? applyPrune(p, standard, items, lang) : void 0;
|
|
76272
76305
|
const ok = (pruned ? r.unadjudicated + r.invalid + r.missing === 0 : r.ok) && grounding.failed === 0;
|
|
76273
76306
|
if (p.flags.json) console.log(JSON.stringify({ ...r, ok, grounding, ...pruned ? { pruned } : {} }, null, 2));
|
|
@@ -76425,6 +76458,21 @@ function cmdVerifyWorklist(p, langIn) {
|
|
|
76425
76458
|
);
|
|
76426
76459
|
return 0;
|
|
76427
76460
|
}
|
|
76461
|
+
function trialAnchorFallbacks(p, standard, passing) {
|
|
76462
|
+
const ids = [...new Set(passing.filter((it) => it.kind === "c").map((it) => it.criteriaId))];
|
|
76463
|
+
const inFlag = p.flags.in;
|
|
76464
|
+
if (!ids.length || typeof inFlag !== "string" || !inFlag || inFlag === "-") return void 0;
|
|
76465
|
+
let audit2;
|
|
76466
|
+
try {
|
|
76467
|
+
audit2 = unwrapAudit(JSON.parse(readText(inFlag)));
|
|
76468
|
+
} catch {
|
|
76469
|
+
return void 0;
|
|
76470
|
+
}
|
|
76471
|
+
const cwd = typeof p.flags.cwd === "string" ? p.flags.cwd : void 0;
|
|
76472
|
+
const evidence = evidenceForCriteria(audit2, ids, { cwd, standard });
|
|
76473
|
+
const { citationDrift } = adjudicationLimits(cwd);
|
|
76474
|
+
return (it, cite) => evidenceFallback(evidence.get(it.criteriaId) ?? [], cite, citationDrift).fallback;
|
|
76475
|
+
}
|
|
76428
76476
|
function conformityClaimsFor(p, standard, lang) {
|
|
76429
76477
|
if (p.flags.conformities === false || p.flags["no-conformities"] === true) return [];
|
|
76430
76478
|
const named3 = typeof p.flags.conformities === "string" && p.flags.conformities ? p.flags.conformities : void 0;
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: ultra11y
|
|
|
3
3
|
description: "Use to AUDIT a repository, site, or page against WCAG 2.2 AA or a country standard such as RGAA; produce a dated conformance report, criterion grid, PRD backlog, or tickets; AUTHOR accessible markup; fix accessibility; or wire a repo gate and rendered-DOM capture pipeline. The bundled install-free engine runs 93 static checks tied to success criteria, while the agent adjudicates judgment criteria and routes rendering criteria to browser scans; check/verify reject invented non-conformities. Use `review-a11y` instead for a review scoped only to a diff, branch, or PR."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: 5.43.
|
|
6
|
+
version: 5.43.2
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -15,16 +15,21 @@ Pin the version. `npx` resolves `latest` otherwise, so what runs can change with
|
|
|
15
15
|
anywhere.
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
npm_config_update_notifier=false npx -y ultra11y@
|
|
18
|
+
npm_config_update_notifier=false npx -y ultra11y@5.43.0 --version # → 5.43.0, exit 0
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
- **`npm_config_update_notifier=false` is not optional.** Without it npm prints its
|
|
22
22
|
"New major version available" notice into the output a step is about to parse.
|
|
23
23
|
- **`ULTRA11Y_BIN`** points at an engine you already have (a vendored `ultra11y.mjs`, a
|
|
24
|
-
container layer).
|
|
25
|
-
vendoring is a supported path — that is exactly what the skills
|
|
24
|
+
container layer). For every command but `scan` the engine is one self-contained file with
|
|
25
|
+
no runtime dependencies, so vendoring is a supported path — that is exactly what the skills
|
|
26
|
+
and the Action do. `scan` resolves `@playwright/test` and `@axe-core/playwright` from
|
|
27
|
+
`--cwd` first, then from the engine's own install, so a vendored file alone cannot scan.
|
|
26
28
|
- **Node ≥ 22.18.** No other system dependency, no API key. (`judge` takes a key; nothing
|
|
27
29
|
else does. `scan` needs a browser tier — see `references/dynamic.md`.)
|
|
30
|
+
- **Pin the file, not only the version.** An orchestrator that resolves the engine itself
|
|
31
|
+
should check the entry file's sha256 before running it: `--version` is a banner any file
|
|
32
|
+
can print.
|
|
28
33
|
- **Verify what you resolved, and fail if it is not what you asked for.** A step that lets a
|
|
29
34
|
missing engine fall through reports zero findings, which reads exactly like a clean audit.
|
|
30
35
|
This is the one failure mode worth being loud about.
|
|
@@ -34,7 +39,7 @@ npm_config_update_notifier=false npx -y ultra11y@2.31.2 --version # → 2.31.2
|
|
|
34
39
|
Six steps. Four are deterministic; only step 3 involves a model.
|
|
35
40
|
|
|
36
41
|
```sh
|
|
37
|
-
ENGINE="npx -y ultra11y@
|
|
42
|
+
ENGINE="npx -y ultra11y@5.43.0" # or "node $ULTRA11Y_BIN"
|
|
38
43
|
RUN=/tmp/a11y-run # out-of-tree: see "Where output lands"
|
|
39
44
|
|
|
40
45
|
# 1. FIND — the static engine. No model. This is the finding set.
|
|
@@ -42,15 +47,21 @@ $ENGINE audit "<globs>" --graph --out "$RUN" --json --lang en > /dev/null
|
|
|
42
47
|
# …or, on a pull request, exactly what the branch introduced:
|
|
43
48
|
$ENGINE audit --since "$BASE_REF" --graph --out "$RUN" --json --lang en > /dev/null
|
|
44
49
|
|
|
45
|
-
# 2. WORKLIST —
|
|
46
|
-
$ENGINE
|
|
47
|
-
|
|
50
|
+
# 2. WORKLIST — the criteria a static pass cannot decide, one brief per criterion.
|
|
51
|
+
$ENGINE verify --manual --in "$RUN/audit-latest.json" --out "$RUN" --json
|
|
52
|
+
# → { count, todoPath, verdictsPath, itemsDir, mdPath, items }. ADJUDICATE.todo.json carries
|
|
53
|
+
# the evidence (it can reach hundreds of KB); ADJUDICATE.verdicts.json is the same worklist
|
|
54
|
+
# without it — hand the agent that slim file plus the briefs in <itemsDir>/<criterion>.md.
|
|
48
55
|
|
|
49
|
-
# 3. ADJUDICATE —
|
|
50
|
-
# The file carries its own `contract` (see below) — the agent needs nothing else.
|
|
56
|
+
# 3. ADJUDICATE — the agent fills every `verdict` (C/NC/NA/manual) in the verdicts file.
|
|
57
|
+
# The file carries its own `contract` (see below) — the agent needs nothing else. The fold
|
|
58
|
+
# re-hydrates the evidence from the audit, so a verdicts-only file is accepted.
|
|
51
59
|
|
|
52
|
-
# 4. FOLD —
|
|
53
|
-
|
|
60
|
+
# 4. FOLD — partial by default: every verdict the gate accepts lands, every refusal is named
|
|
61
|
+
# in `rejectedCriteria[]` / `issues[]`, and a criterion absent from the file is refused as a
|
|
62
|
+
# coverage gap. Exit 1 only when nothing landed. `--strict` makes it all-or-nothing (exit 1
|
|
63
|
+
# on any refusal, audit left untouched).
|
|
64
|
+
$ENGINE verify --apply "$RUN/ADJUDICATE.verdicts.json" --in "$RUN/audit-latest.json" --out "$RUN" --json
|
|
54
65
|
|
|
55
66
|
# 5. REPORT — deterministic rendering of the adjudicated audit.
|
|
56
67
|
$ENGINE report --in "$RUN/audit-latest.json" --out audits --standard rgaa --lang fr
|
|
@@ -153,15 +164,16 @@ repo it does not own — write the run's intermediates out-of-tree and let only
|
|
|
153
164
|
dated report land in the project, if at all.
|
|
154
165
|
|
|
155
166
|
`.ultra11y/pages/` is written only by the page-snapshot commands (`scan`, `snapshot`,
|
|
156
|
-
`dev`)
|
|
157
|
-
|
|
167
|
+
`dev`). `snapshot`, `dev` and `pages discover` take `--root` to relocate it; `scan` does not —
|
|
168
|
+
it writes under the current working directory, so run it from the directory that should
|
|
169
|
+
hold the snapshots. `audit`, `report`, `prd`, `tickets`, `verify` and `check` never touch it.
|
|
158
170
|
|
|
159
171
|
## Exit codes
|
|
160
172
|
|
|
161
173
|
| | 0 | 1 | 2 |
|
|
162
174
|
|---|---|---|---|
|
|
163
175
|
| `audit` | reported (no `--fail-on`), or clean | findings ≥ `--fail-on` | usage / bad input |
|
|
164
|
-
| `verify --apply` |
|
|
176
|
+
| `verify --apply` | at least one verdict landed (refusals, if any, in `rejectedCriteria[]` / `issues[]`) | nothing landed, or `--strict` and a refusal | usage / bad input |
|
|
165
177
|
| `check` | report is sound | integrity failure | report not found / bad input |
|
|
166
178
|
| `prd` | written | — (it writes markdown and files nothing) | usage / bad input |
|
|
167
179
|
| `tickets` | filed, skipped, or nothing to file | a creation failed, the provider is unusable, or no page in scope | usage / bad input / past `--max-tickets` |
|
|
@@ -174,8 +186,24 @@ Read exit codes from the process, **not** from a pipeline — `cmd --json | head
|
|
|
174
186
|
- `audit --json` → the `AuditResult` (also at `<out>/audit-latest.json`): `findings[]` with
|
|
175
187
|
stable `findingId`, `criteriaId`, `severity`, `file`, `line`; `residualRisks[]`;
|
|
176
188
|
`conformancePct` — the **automatic static pass rate**, not a conformance rate.
|
|
177
|
-
- `verify --apply --json` → `{ ok, auditPath, applied, stillManual,
|
|
178
|
-
|
|
189
|
+
- `verify --apply --json` → `{ ok, auditPath, applied, stillManual, rejected, rejectedCriteria[], issues[],
|
|
190
|
+
conformancePct, findings, grounding }`; `ok: false` when nothing landed. Count on
|
|
191
|
+
`applied + stillManual + rejected`, never on the exit code alone.
|
|
192
|
+
- With `--prune`, **`ok: false` does not mean "nothing was applied"**. The CLI computes the
|
|
193
|
+
coverage and grounding verdicts first, but `applyPrune` writes the pruned
|
|
194
|
+
`audit-latest.json` and repairs the `--ledger` file BEFORE `ok` is derived — so a trial that
|
|
195
|
+
fails on grounding still applied every refutation it decided. Read `pruned` in the JSON
|
|
196
|
+
(or the audit document itself) to know what the audit now says; never report `ok: false` as
|
|
197
|
+
"trial not applied".
|
|
198
|
+
- `grounding` re-opens every cited file of a retained verdict. A `file` that is a served URL
|
|
199
|
+
(`^[a-z][a-z0-9+.-]*://` — a rendered finding whose page has no `.ultra11y/pages` capture
|
|
200
|
+
mapped back to source) has no on-disk source to re-open and is skipped like `stdin`, not
|
|
201
|
+
counted as `failed`.
|
|
202
|
+
- Under a pack standard (`--standard rgaa`), `audit-latest.json` is a `pack-audit` document:
|
|
203
|
+
its root `conformancePct` is the automatic pass rate for that standard, `core` is the WCAG
|
|
204
|
+
audit, and `report --json` and `verify --apply --json` echo the **core** rate. The
|
|
205
|
+
standard's own conformance rate (validated ÷ validated + NC + à évaluer, NA excluded) is
|
|
206
|
+
printed by the report; derive it from `criteria[]` if a number is needed in a pipeline.
|
|
179
207
|
- `check --json` → `{ ok, issues[] }`.
|
|
180
208
|
|
|
181
209
|
## Honesty rules an orchestrator has to carry
|
|
@@ -23,6 +23,9 @@ node scripts/ultra11y.mjs check --report audits/wcag-YYYY-MM-DD.md --semantic [-
|
|
|
23
23
|
Fails **closed** if there is no adjudicated verdicts artifact (never green-but-inactive),
|
|
24
24
|
re-runs the **uncapped** coverage check, and re-grounds every passing verdict against the cited
|
|
25
25
|
source. `--verdicts` points at the artifact (default `VERIFY.todo.json` next to the report).
|
|
26
|
+
A `file` that is a served URL (a rendered finding whose page has no `.ultra11y/pages` capture
|
|
27
|
+
mapped back to source) is not a path to re-open: it is skipped like `stdin`, never reported as
|
|
28
|
+
« cited file not found ».
|
|
26
29
|
|
|
27
30
|
### Applicability gate — `check --report … --standard rgaa --in audit.json`
|
|
28
31
|
|
|
@@ -52,10 +55,16 @@ so an agent can fill verdicts in-memory). **Prioritise `preliminary` findings**
|
|
|
52
55
|
source, see `references/false-positives.md`): refute the ones the rendered DOM disproves.
|
|
53
56
|
Then apply the gate:
|
|
54
57
|
```
|
|
55
|
-
node scripts/ultra11y.mjs verify --apply VERIFY.todo.json
|
|
58
|
+
node scripts/ultra11y.mjs verify --apply VERIFY.todo.json --report audits/wcag-YYYY-MM-DD.md --in audit.json
|
|
56
59
|
```
|
|
57
|
-
The gate fails (non-zero) if any entry is `refuted`, `unsupported`, or unadjudicated.
|
|
58
|
-
|
|
60
|
+
The gate fails (non-zero) if any entry is `refuted`, `unsupported`, or unadjudicated. Every
|
|
61
|
+
retained claim is also re-grounded against the cited source. A conformity is grounded the way
|
|
62
|
+
the fold grounded it: a citation that is one of its criterion's own harvested anchors falls
|
|
63
|
+
back to that anchor (the engine read its snippet out of the file), so a retyping the fold
|
|
64
|
+
accepted at `verify --apply <adjudication>` is not refused again at trial — pass `--in
|
|
65
|
+
<audit.json>` so the harvest can be re-derived; a citation that is NOT one of the criterion's
|
|
66
|
+
anchors must still match the source literally. Goal: no fabricated non-conformity survives
|
|
67
|
+
into the final report, and no accepted conformity is re-refused one gate later.
|
|
59
68
|
|
|
60
69
|
## `verify --manual` — adjudicate the residual (judgment / needs-rendering) criteria
|
|
61
70
|
|
|
@@ -6,7 +6,7 @@ import { join as join52, relative as relative6, resolve as resolve17, 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.43.
|
|
9
|
+
var VERSION = "5.43.2";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -2298,7 +2298,7 @@ var wcag_universe_default = {
|
|
|
2298
2298
|
wcagVersion: "2.2",
|
|
2299
2299
|
source: "https://www.w3.org/TR/WCAG22/",
|
|
2300
2300
|
criteriaSource: "https://github.com/w3c/wcag",
|
|
2301
|
-
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-10-
|
|
2301
|
+
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-10-19 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
2302
2302
|
criteria: [
|
|
2303
2303
|
{
|
|
2304
2304
|
id: "1.1.1",
|
|
@@ -62643,9 +62643,12 @@ var resumableItemKey = (it) => {
|
|
|
62643
62643
|
return `${it.kind ?? "nc"}|${it.criteriaId}|${canonicalFile(it.file)}|${it.line}|${it.selector}|${it.claim}|${citationSetKey(it)}`;
|
|
62644
62644
|
};
|
|
62645
62645
|
var legacyConformityKey = (criteriaId, file, line, selector, claim) => `${criteriaId}|${canonicalFile(file)}|${line}|${selector}|${claim}`;
|
|
62646
|
-
function verifyGroundingInputs(items) {
|
|
62646
|
+
function verifyGroundingInputs(items, fallbackFor) {
|
|
62647
62647
|
return items.flatMap(
|
|
62648
|
-
(it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) =>
|
|
62648
|
+
(it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) => {
|
|
62649
|
+
const fallback = fallbackFor?.(it, cite);
|
|
62650
|
+
return { file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet, ...fallback ? { fallback } : {} };
|
|
62651
|
+
}) : [{ file: it.file, line: it.line, selector: it.selector, snippet: it.snippet }]
|
|
62649
62652
|
);
|
|
62650
62653
|
}
|
|
62651
62654
|
var PASSING = /* @__PURE__ */ new Set(["supported", "partial"]);
|
|
@@ -62733,7 +62736,7 @@ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
|
|
|
62733
62736
|
import { resolve as resolve10 } from "path";
|
|
62734
62737
|
var WINDOW = 10;
|
|
62735
62738
|
var norm2 = (s) => s.normalize("NFKC").replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, " ").replace(/>\s+/g, ">").replace(/\s+</g, "<").trim();
|
|
62736
|
-
var isUnresolvable = (file) => !file || file === "-" || file === "<stdin>" || file === "stdin";
|
|
62739
|
+
var isUnresolvable = (file) => !file || file === "-" || file === "<stdin>" || file === "stdin" || /^[a-z][a-z0-9+.-]*:\/\//.test(file);
|
|
62737
62740
|
var SEMANTIC_SELECTORS = /* @__PURE__ */ new Set(["handler"]);
|
|
62738
62741
|
function selectorProbes(selector) {
|
|
62739
62742
|
if (SEMANTIC_SELECTORS.has(selector.trim().toLowerCase())) return [];
|
|
@@ -62783,7 +62786,11 @@ function groundFinding(g, opts = {}) {
|
|
|
62783
62786
|
function groundItems(items, opts = {}) {
|
|
62784
62787
|
const s = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
62785
62788
|
for (const it of items) {
|
|
62786
|
-
|
|
62789
|
+
let r = groundFinding(it, opts);
|
|
62790
|
+
if (!r.ok && it.fallback) {
|
|
62791
|
+
const viaAnchor = groundFinding(it.fallback, opts);
|
|
62792
|
+
if (viaAnchor.ok) r = { ok: true, moved: true };
|
|
62793
|
+
}
|
|
62787
62794
|
if (r.ok) {
|
|
62788
62795
|
s.grounded++;
|
|
62789
62796
|
if (r.moved) s.moved++;
|
|
@@ -64210,6 +64217,23 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
64210
64217
|
(r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc2(r.criteriaId), docs), limits)
|
|
64211
64218
|
);
|
|
64212
64219
|
}
|
|
64220
|
+
function evidenceForCriteria(audit2, criteriaIds, opts = {}) {
|
|
64221
|
+
const limits = adjudicationLimits(opts.cwd);
|
|
64222
|
+
const docs = docsForAudit(audit2, opts.cwd);
|
|
64223
|
+
const standard = opts.standard;
|
|
64224
|
+
const pack = standard !== void 0 && !isCore(standard) ? loadPack(standard) : void 0;
|
|
64225
|
+
const candidates2 = new Map(pack && standard ? derivePackResults(audit2, standard).map((pc) => [pc.id, pc.candidateFindings ?? []]) : []);
|
|
64226
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
64227
|
+
for (const id of criteriaIds) {
|
|
64228
|
+
if (out2.has(id)) continue;
|
|
64229
|
+
const crit = pack ? getCriterion(pack, id) : void 0;
|
|
64230
|
+
const subjects = pack && standard ? subjectsForPackCriterion2(standard, id, crit?.wcag ?? []) : subjectsForSc2(id);
|
|
64231
|
+
let evidence = collapse2(harvestSubjects(subjects, docs), limits).evidence;
|
|
64232
|
+
if (!evidence.length) evidence = candidateFindingEvidence(candidates2.get(id) ?? [], limits.maxClasses);
|
|
64233
|
+
out2.set(id, evidence);
|
|
64234
|
+
}
|
|
64235
|
+
return out2;
|
|
64236
|
+
}
|
|
64213
64237
|
function unrenderedResidual(audit2, items) {
|
|
64214
64238
|
const audited = audit2.scope.pagesAudited;
|
|
64215
64239
|
const readSomePage = audited === void 0 ? (audit2.scope.pages ?? []).length > 0 : audited.length > 0;
|
|
@@ -64336,6 +64360,14 @@ function cites0(anchors, c2, drift) {
|
|
|
64336
64360
|
}
|
|
64337
64361
|
return false;
|
|
64338
64362
|
}
|
|
64363
|
+
function evidenceFallback(evidence, c2, drift) {
|
|
64364
|
+
const anchor = anchorFor(evidence, c2, drift);
|
|
64365
|
+
if (!anchor || !recognisablySame(c2, anchor.at)) return { onAnchor: anchor !== void 0 };
|
|
64366
|
+
return {
|
|
64367
|
+
onAnchor: true,
|
|
64368
|
+
fallback: anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
|
|
64369
|
+
};
|
|
64370
|
+
}
|
|
64339
64371
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
64340
64372
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
64341
64373
|
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
@@ -64485,16 +64517,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
64485
64517
|
if (!cites0(anchors, c2, citationDrift) && !inAuditScope(c2.file)) {
|
|
64486
64518
|
blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
|
|
64487
64519
|
}
|
|
64488
|
-
const anchor = anchorFor(it.evidence, c2, citationDrift);
|
|
64489
64520
|
const cite = { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet };
|
|
64490
|
-
|
|
64491
|
-
|
|
64492
|
-
|
|
64493
|
-
cite,
|
|
64494
|
-
anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
|
|
64495
|
-
);
|
|
64521
|
+
const { fallback, onAnchor } = evidenceFallback(it.evidence, c2, citationDrift);
|
|
64522
|
+
if (fallback) {
|
|
64523
|
+
toGround(it.criteriaId, cite, fallback);
|
|
64496
64524
|
} else {
|
|
64497
|
-
toGround(it.criteriaId, cite, void 0,
|
|
64525
|
+
toGround(it.criteriaId, cite, void 0, onAnchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
|
|
64498
64526
|
}
|
|
64499
64527
|
}
|
|
64500
64528
|
}
|
|
@@ -74398,6 +74426,11 @@ Options:
|
|
|
74398
74426
|
(requires --report \u2014 coverage is re-derived from the report, uncapped)
|
|
74399
74427
|
Also accepts an ADJUDICATION (--in <audit.json>), or a VERDICT LEDGER,
|
|
74400
74428
|
which it REPLAYS onto the audit with no model in the loop
|
|
74429
|
+
Retained conformities are grounded the way the fold grounded them:
|
|
74430
|
+
a citation that is one of its criterion's own harvested anchors
|
|
74431
|
+
(--in <audit.json>) falls back to that anchor, so a retyping the
|
|
74432
|
+
fold accepted is not refused again at trial; without the audit the
|
|
74433
|
+
cited snippet must match the source literally
|
|
74401
74434
|
--ledger <file> verify --apply / judge --apply: record the verdicts the fold ACCEPTED,
|
|
74402
74435
|
so a later run replays them for free (default:
|
|
74403
74436
|
.ultra11y/verdicts/<standard>.json). Replay re-derives the evidence
|
|
@@ -76267,7 +76300,7 @@ function cmdVerify(p) {
|
|
|
76267
76300
|
const expected = [...expectedNc, ...buildConformityWorklist(conformityClaimsFor(p, standard, lang), expectedNc.length, Number.POSITIVE_INFINITY)];
|
|
76268
76301
|
const r = applyVerdicts(items, expected);
|
|
76269
76302
|
const passing = items.filter((it) => typeof it.verdict === "string" && ["supported", "partial"].includes(it.verdict.trim().toLowerCase()));
|
|
76270
|
-
const grounding = groundItems(verifyGroundingInputs(passing));
|
|
76303
|
+
const grounding = groundItems(verifyGroundingInputs(passing, trialAnchorFallbacks(p, standard, passing)));
|
|
76271
76304
|
const pruned = p.flags.prune === true ? applyPrune(p, standard, items, lang) : void 0;
|
|
76272
76305
|
const ok = (pruned ? r.unadjudicated + r.invalid + r.missing === 0 : r.ok) && grounding.failed === 0;
|
|
76273
76306
|
if (p.flags.json) console.log(JSON.stringify({ ...r, ok, grounding, ...pruned ? { pruned } : {} }, null, 2));
|
|
@@ -76425,6 +76458,21 @@ function cmdVerifyWorklist(p, langIn) {
|
|
|
76425
76458
|
);
|
|
76426
76459
|
return 0;
|
|
76427
76460
|
}
|
|
76461
|
+
function trialAnchorFallbacks(p, standard, passing) {
|
|
76462
|
+
const ids = [...new Set(passing.filter((it) => it.kind === "c").map((it) => it.criteriaId))];
|
|
76463
|
+
const inFlag = p.flags.in;
|
|
76464
|
+
if (!ids.length || typeof inFlag !== "string" || !inFlag || inFlag === "-") return void 0;
|
|
76465
|
+
let audit2;
|
|
76466
|
+
try {
|
|
76467
|
+
audit2 = unwrapAudit(JSON.parse(readText(inFlag)));
|
|
76468
|
+
} catch {
|
|
76469
|
+
return void 0;
|
|
76470
|
+
}
|
|
76471
|
+
const cwd = typeof p.flags.cwd === "string" ? p.flags.cwd : void 0;
|
|
76472
|
+
const evidence = evidenceForCriteria(audit2, ids, { cwd, standard });
|
|
76473
|
+
const { citationDrift } = adjudicationLimits(cwd);
|
|
76474
|
+
return (it, cite) => evidenceFallback(evidence.get(it.criteriaId) ?? [], cite, citationDrift).fallback;
|
|
76475
|
+
}
|
|
76428
76476
|
function conformityClaimsFor(p, standard, lang) {
|
|
76429
76477
|
if (p.flags.conformities === false || p.flags["no-conformities"] === true) return [];
|
|
76430
76478
|
const named3 = typeof p.flags.conformities === "string" && p.flags.conformities ? p.flags.conformities : void 0;
|
|
@@ -27,7 +27,7 @@ variables:
|
|
|
27
27
|
ULTRA11Y_LANG: en
|
|
28
28
|
# Exact by default: a CI report must say which engine produced it. The release script keeps
|
|
29
29
|
# this pin aligned with the package version; override only for a deliberate upgrade test.
|
|
30
|
-
ULTRA11Y_VERSION: '5.43.
|
|
30
|
+
ULTRA11Y_VERSION: '5.43.2'
|
|
31
31
|
# Batches of 8 amortise the prompt; each accepted verdict is checkpointed before the next
|
|
32
32
|
# batch, and later passes receive only the residue.
|
|
33
33
|
ULTRA11Y_GRAIN: batch
|
package/src/adjudicate.ts
CHANGED
|
@@ -524,6 +524,40 @@ export function buildAdjudicationWorklist(audit: AuditResult, opts: { cwd?: stri
|
|
|
524
524
|
);
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
/** The harvested evidence behind ARBITRARY criteria — including ones a prior fold already
|
|
528
|
+
* decided, which `buildAdjudicationWorklist` filters out on purpose (a prior agent decision
|
|
529
|
+
* is never reopened). The trial (`verify --apply … --report`) needs them anyway: it grounds
|
|
530
|
+
* a retained claim the way the fold grounded it, and the fold's evidence-anchor fallback
|
|
531
|
+
* vouches only for a citation that is one of the criterion's OWN harvested anchors.
|
|
532
|
+
*
|
|
533
|
+
* Same derivation, item for item, as the worklist the verdict was gated against — the worklist
|
|
534
|
+
* is a pure function of the audit, so re-reading the audit's inputs rebuilds the very anchor
|
|
535
|
+
* set the citation was checked against. When a criterion's harvest is empty, the candidate
|
|
536
|
+
* browser-rule findings stand in, exactly as the worklist's items carry them. */
|
|
537
|
+
export function evidenceForCriteria(
|
|
538
|
+
audit: AuditResult,
|
|
539
|
+
criteriaIds: readonly string[],
|
|
540
|
+
opts: { cwd?: string; standard?: StandardId } = {},
|
|
541
|
+
): Map<string, Evidence[]> {
|
|
542
|
+
const limits = adjudicationLimits(opts.cwd);
|
|
543
|
+
const docs = docsForAudit(audit, opts.cwd);
|
|
544
|
+
const standard = opts.standard;
|
|
545
|
+
const pack = standard !== undefined && !isCore(standard) ? loadPack(standard) : undefined;
|
|
546
|
+
// Candidate findings only exist in pack mode, and a criterion's own `wcag` mapping decides
|
|
547
|
+
// its subjects the same way `buildAdjudicationWorklist` lets it (its OWN subjects first).
|
|
548
|
+
const candidates = new Map(pack && standard ? derivePackResults(audit, standard).map((pc) => [pc.id, pc.candidateFindings ?? []] as const) : []);
|
|
549
|
+
const out = new Map<string, Evidence[]>();
|
|
550
|
+
for (const id of criteriaIds) {
|
|
551
|
+
if (out.has(id)) continue;
|
|
552
|
+
const crit = pack ? getCriterion(pack, id) : undefined;
|
|
553
|
+
const subjects = pack && standard ? subjectsForPackCriterion(standard, id, crit?.wcag ?? []) : subjectsForSc(id);
|
|
554
|
+
let evidence = collapse(harvestSubjects(subjects, docs), limits).evidence;
|
|
555
|
+
if (!evidence.length) evidence = candidateFindingEvidence(candidates.get(id) ?? [], limits.maxClasses);
|
|
556
|
+
out.set(id, evidence);
|
|
557
|
+
}
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
|
|
527
561
|
/** THE CRITERIA THIS RUN COULD NOT POSSIBLY HAVE DECIDED — because nothing was rendered.
|
|
528
562
|
*
|
|
529
563
|
* `applyAdjudication` already refuses a `needs-rendered-dom` verdict whose own evidence sits
|
|
@@ -768,6 +802,32 @@ function cites0(anchors: string[], c: { file: string; line: number }, drift: num
|
|
|
768
802
|
return false;
|
|
769
803
|
}
|
|
770
804
|
|
|
805
|
+
/** The evidence-anchor stand-in the fold grants a citation: the harvested anchor of THIS
|
|
806
|
+
* criterion the citation resolves to, grounding the anchor's own snippet — the engine read
|
|
807
|
+
* it out of the file, so it is there by construction. A sibling from `alsoAt` carries no
|
|
808
|
+
* snippet (it is a bare `file:line`), so its stand-in is the cited place itself. Undefined
|
|
809
|
+
* fallback when the citation is off-harvest or claims a different KIND of element than the
|
|
810
|
+
* anchor holds — those keep the strict literal check (`onAnchor` says which, because only a
|
|
811
|
+
* genuinely off-harvest citation earns the off-harvest explanation).
|
|
812
|
+
*
|
|
813
|
+
* SHARED WITH THE TRIAL on purpose: `verify --apply … --report` grounds a retained claim
|
|
814
|
+
* through the very same function, so a conformity the fold accepted is not refused one gate
|
|
815
|
+
* later for the retyping the fold already forgave. */
|
|
816
|
+
export function evidenceFallback(
|
|
817
|
+
evidence: Evidence[],
|
|
818
|
+
c: { file: string; line: number; selector?: string; snippet?: string },
|
|
819
|
+
drift: number,
|
|
820
|
+
): { fallback?: { file: string; line: number; selector?: string; snippet?: string }; onAnchor: boolean } {
|
|
821
|
+
const anchor = anchorFor(evidence, c, drift);
|
|
822
|
+
if (!anchor || !recognisablySame(c, anchor.at)) return { onAnchor: anchor !== undefined };
|
|
823
|
+
return {
|
|
824
|
+
onAnchor: true,
|
|
825
|
+
fallback: anchor.representative
|
|
826
|
+
? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c.selector, snippet: anchor.at.snippet }
|
|
827
|
+
: { file: c.file, line: c.line, selector: anchor.at.selector ?? c.selector },
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
771
831
|
export interface ApplyAdjudicationResult {
|
|
772
832
|
ok: boolean;
|
|
773
833
|
audit: AuditResult;
|
|
@@ -1115,16 +1175,10 @@ export function applyAdjudication(
|
|
|
1115
1175
|
// It is authoritative there: the engine read it out of the file itself. And only
|
|
1116
1176
|
// then does recognisability have anything to say, because only then is the anchor
|
|
1117
1177
|
// being used to vouch for something the file did not confirm on its own.
|
|
1118
|
-
const anchor = anchorFor(it.evidence, c, citationDrift);
|
|
1119
1178
|
const cite = { file: c.file, line: c.line, selector: c.selector, snippet: c.snippet };
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
cite,
|
|
1124
|
-
anchor.representative
|
|
1125
|
-
? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c.selector, snippet: anchor.at.snippet }
|
|
1126
|
-
: { file: c.file, line: c.line, selector: anchor.at.selector ?? c.selector },
|
|
1127
|
-
);
|
|
1179
|
+
const { fallback, onAnchor } = evidenceFallback(it.evidence, c, citationDrift);
|
|
1180
|
+
if (fallback) {
|
|
1181
|
+
toGround(it.criteriaId, cite, fallback);
|
|
1128
1182
|
} else {
|
|
1129
1183
|
// WHY THE STRICT PATH APPLIED, said at the point it applies.
|
|
1130
1184
|
//
|
|
@@ -1149,7 +1203,7 @@ export function applyAdjudication(
|
|
|
1149
1203
|
// be false: the anchor is right there, it is the element that is wrong. That case
|
|
1150
1204
|
// keeps the bare refusal, which already says the honest thing — this markup is not
|
|
1151
1205
|
// in the file.
|
|
1152
|
-
toGround(it.criteriaId, cite, undefined,
|
|
1206
|
+
toGround(it.criteriaId, cite, undefined, onAnchor ? undefined : offHarvestHint(it.criteriaId, it.evidence));
|
|
1153
1207
|
}
|
|
1154
1208
|
}
|
|
1155
1209
|
}
|
package/src/cli.ts
CHANGED
|
@@ -62,6 +62,9 @@ import {
|
|
|
62
62
|
applyAdjudication,
|
|
63
63
|
hydrateAdjudication,
|
|
64
64
|
unrenderedResidual,
|
|
65
|
+
adjudicationLimits,
|
|
66
|
+
evidenceFallback,
|
|
67
|
+
evidenceForCriteria,
|
|
65
68
|
type AdjudicationFile,
|
|
66
69
|
} from "./adjudicate.js";
|
|
67
70
|
import {
|
|
@@ -431,6 +434,11 @@ Options:
|
|
|
431
434
|
(requires --report — coverage is re-derived from the report, uncapped)
|
|
432
435
|
Also accepts an ADJUDICATION (--in <audit.json>), or a VERDICT LEDGER,
|
|
433
436
|
which it REPLAYS onto the audit with no model in the loop
|
|
437
|
+
Retained conformities are grounded the way the fold grounded them:
|
|
438
|
+
a citation that is one of its criterion's own harvested anchors
|
|
439
|
+
(--in <audit.json>) falls back to that anchor, so a retyping the
|
|
440
|
+
fold accepted is not refused again at trial; without the audit the
|
|
441
|
+
cited snippet must match the source literally
|
|
434
442
|
--ledger <file> verify --apply / judge --apply: record the verdicts the fold ACCEPTED,
|
|
435
443
|
so a later run replays them for free (default:
|
|
436
444
|
.ultra11y/verdicts/<standard>.json). Replay re-derives the evidence
|
|
@@ -2929,9 +2937,13 @@ function cmdVerify(p: ParsedArgs): number {
|
|
|
2929
2937
|
const expected = [...expectedNc, ...buildConformityWorklist(conformityClaimsFor(p, standard, lang), expectedNc.length, Number.POSITIVE_INFINITY)];
|
|
2930
2938
|
const r = applyVerdicts(items, expected);
|
|
2931
2939
|
// Content-level grounding of every verdict that passed adjudication: the cited
|
|
2932
|
-
// file/line/snippet must still exist and match the source (see src/grounding.ts).
|
|
2940
|
+
// file/line/snippet must still exist and match the source (see src/grounding.ts). A
|
|
2941
|
+
// RETAINED CONFORMITY IS GROUNDED THE WAY THE FOLD GROUNDED IT: a citation that is one of
|
|
2942
|
+
// its criterion's own harvested anchors falls back to the anchor, so a claim `verify
|
|
2943
|
+
// --apply <adjudication>` accepted is not refused again here — on every later trial —
|
|
2944
|
+
// for the retyping the fold already forgave.
|
|
2933
2945
|
const passing = items.filter((it) => typeof it.verdict === "string" && ["supported", "partial"].includes(it.verdict.trim().toLowerCase()));
|
|
2934
|
-
const grounding = groundItems(verifyGroundingInputs(passing));
|
|
2946
|
+
const grounding = groundItems(verifyGroundingInputs(passing, trialAnchorFallbacks(p, standard, passing)));
|
|
2935
2947
|
// --prune: APPLY the outcome instead of only reporting it. See src/refute.ts for why a
|
|
2936
2948
|
// gate that can only go red is the wrong shape for a cheap adjudicator's run, and for the
|
|
2937
2949
|
// asymmetry between withdrawing a non-conformity and withdrawing a conformity.
|
|
@@ -3168,6 +3180,45 @@ function cmdVerifyWorklist(p: ParsedArgs, langIn: Lang): number {
|
|
|
3168
3180
|
return 0;
|
|
3169
3181
|
}
|
|
3170
3182
|
|
|
3183
|
+
/** The fold's evidence-anchor fallback, rebuilt for the trial (`verify --apply … --report`).
|
|
3184
|
+
*
|
|
3185
|
+
* The fold grounds a citation that is one of its criterion's own harvested anchors against
|
|
3186
|
+
* THAT ANCHOR when the literal check fails — an adjudicator's retyping of an element it did
|
|
3187
|
+
* read. The trial of the same report must not refuse the claim one gate later for the
|
|
3188
|
+
* retyping the fold already forgave, so it re-derives the harvest from the audit under `--in`
|
|
3189
|
+
* (the worklist evidence is a pure function of the audit) and grants the same fallback,
|
|
3190
|
+
* through the very same `evidenceFallback` the fold runs. A citation that is NOT one of the
|
|
3191
|
+
* criterion's anchors still fails strictly: without the audit there is nothing to re-derive
|
|
3192
|
+
* and nothing is granted.
|
|
3193
|
+
*
|
|
3194
|
+
* Returns the per-citation resolver `verifyGroundingInputs` applies; undefined when there is
|
|
3195
|
+
* nothing to resolve (no conformity on trial, no `--in`, unreadable audit). */
|
|
3196
|
+
function trialAnchorFallbacks(
|
|
3197
|
+
p: ParsedArgs,
|
|
3198
|
+
standard: StandardId,
|
|
3199
|
+
passing: VerifyItem[],
|
|
3200
|
+
):
|
|
3201
|
+
| ((
|
|
3202
|
+
it: VerifyItem,
|
|
3203
|
+
cite: { file: string; line: number; selector?: string; snippet?: string },
|
|
3204
|
+
) => { file: string; line: number; selector?: string; snippet?: string } | undefined)
|
|
3205
|
+
| undefined {
|
|
3206
|
+
const ids = [...new Set(passing.filter((it) => it.kind === "c").map((it) => it.criteriaId))];
|
|
3207
|
+
const inFlag = p.flags.in;
|
|
3208
|
+
if (!ids.length || typeof inFlag !== "string" || !inFlag || inFlag === "-") return undefined;
|
|
3209
|
+
let audit: AuditResult;
|
|
3210
|
+
try {
|
|
3211
|
+
audit = unwrapAudit(JSON.parse(readText(inFlag))) as AuditResult;
|
|
3212
|
+
} catch {
|
|
3213
|
+
// The command that owns --in reports malformed input; the trial stays strict.
|
|
3214
|
+
return undefined;
|
|
3215
|
+
}
|
|
3216
|
+
const cwd = typeof p.flags.cwd === "string" ? (p.flags.cwd as string) : undefined;
|
|
3217
|
+
const evidence = evidenceForCriteria(audit, ids, { cwd, standard });
|
|
3218
|
+
const { citationDrift } = adjudicationLimits(cwd);
|
|
3219
|
+
return (it, cite) => evidenceFallback(evidence.get(it.criteriaId) ?? [], cite, citationDrift).fallback;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3171
3222
|
/**
|
|
3172
3223
|
* The claimed conformities to put on trial, from the verdict ledger (or an adjudication file).
|
|
3173
3224
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"wcagVersion": "2.2",
|
|
3
3
|
"source": "https://www.w3.org/TR/WCAG22/",
|
|
4
4
|
"criteriaSource": "https://github.com/w3c/wcag",
|
|
5
|
-
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-10-
|
|
5
|
+
"provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-10-19 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
6
6
|
"criteria": [
|
|
7
7
|
{
|
|
8
8
|
"id": "1.1.1",
|
package/src/grounding.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface GroundingInput {
|
|
|
13
13
|
line: number;
|
|
14
14
|
snippet?: string;
|
|
15
15
|
selector?: string;
|
|
16
|
+
/** The criterion's harvested anchor standing in for this citation when the literal check
|
|
17
|
+
* fails — the evidence-anchor fallback the fold grants a citation that is one of the
|
|
18
|
+
* criterion's own harvested anchors (see src/adjudicate.ts). Optional; absent on the
|
|
19
|
+
* strict inputs, which must ground literally. */
|
|
20
|
+
fallback?: GroundingInput;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
export interface GroundingVerdict {
|
|
@@ -39,8 +44,11 @@ const WINDOW = 10;
|
|
|
39
44
|
const norm = (s: string) =>
|
|
40
45
|
s.normalize("NFKC").replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, " ").replace(/>\s+/g, ">").replace(/\s+</g, "<").trim();
|
|
41
46
|
|
|
42
|
-
/** Streams/aggregates have no on-disk source to re-open.
|
|
43
|
-
|
|
47
|
+
/** Streams/aggregates have no on-disk source to re-open. Neither has a served URL: a rendered
|
|
48
|
+
* finding whose page has no `.ultra11y/pages` capture keeps the page URL as its `file`, and
|
|
49
|
+
* resolving that against the cwd can only report it missing. The `://` keeps a Windows drive
|
|
50
|
+
* letter (`C:\…`) out — only a scheme + authority is a URL. */
|
|
51
|
+
const isUnresolvable = (file: string) => !file || file === "-" || file === "<stdin>" || file === "stdin" || /^[a-z][a-z0-9+.-]*:\/\//.test(file);
|
|
44
52
|
|
|
45
53
|
/** A markup token a selector implies. `ci` = case-INSENSITIVE (HTML tag & attribute
|
|
46
54
|
* names, which the HTML spec treats case-insensitively — `<IMG>` ≡ `<img>`); `cs` =
|
|
@@ -118,7 +126,14 @@ export function groundFinding(g: GroundingInput, opts: { cwd?: string } = {}): G
|
|
|
118
126
|
export function groundItems(items: GroundingInput[], opts: { cwd?: string } = {}): GroundingSummary {
|
|
119
127
|
const s: GroundingSummary = { grounded: 0, moved: 0, failed: 0, issues: [] };
|
|
120
128
|
for (const it of items) {
|
|
121
|
-
|
|
129
|
+
let r = groundFinding(it, opts);
|
|
130
|
+
// The harvested anchor vouches for a citation the file could not confirm on its own —
|
|
131
|
+
// an adjudicator's retyping of an element it did read. `moved` either way: what grounded
|
|
132
|
+
// is not verbatim what was cited (same contract as the fold, src/adjudicate.ts).
|
|
133
|
+
if (!r.ok && it.fallback) {
|
|
134
|
+
const viaAnchor = groundFinding(it.fallback, opts);
|
|
135
|
+
if (viaAnchor.ok) r = { ok: true, moved: true };
|
|
136
|
+
}
|
|
122
137
|
if (r.ok) {
|
|
123
138
|
s.grounded++;
|
|
124
139
|
if (r.moved) s.moved++;
|
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.43.
|
|
3
|
+
export const VERSION = "5.43.2";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|
package/src/verify.ts
CHANGED
|
@@ -465,11 +465,20 @@ const resumableItemKey = (it: VerifyItem): string => {
|
|
|
465
465
|
const legacyConformityKey = (criteriaId: string, file: string, line: number, selector: string, claim: string): string =>
|
|
466
466
|
`${criteriaId}|${canonicalFile(file)}|${line}|${selector}|${claim}`;
|
|
467
467
|
|
|
468
|
-
/** Every source anchor a passing claim asks the grounding gate to re-open.
|
|
469
|
-
|
|
468
|
+
/** Every source anchor a passing claim asks the grounding gate to re-open. `fallbackFor`
|
|
469
|
+
* optionally attaches, per cited conformity, the harvested anchor that may stand in when the
|
|
470
|
+
* literal check fails — the fold's evidence-anchor fallback (src/adjudicate.ts), so a claim
|
|
471
|
+
* the fold accepted is grounded the same way here. */
|
|
472
|
+
export function verifyGroundingInputs(
|
|
473
|
+
items: VerifyItem[],
|
|
474
|
+
fallbackFor?: (it: VerifyItem, cite: CriterionCitation) => { file: string; line: number; selector?: string; snippet?: string } | undefined,
|
|
475
|
+
): Array<{ file: string; line: number; selector?: string; snippet?: string; fallback?: { file: string; line: number; selector?: string; snippet?: string } }> {
|
|
470
476
|
return items.flatMap((it) =>
|
|
471
477
|
it.kind === "c" && it.citations?.length
|
|
472
|
-
? it.citations.map((cite) =>
|
|
478
|
+
? it.citations.map((cite) => {
|
|
479
|
+
const fallback = fallbackFor?.(it, cite);
|
|
480
|
+
return { file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet, ...(fallback ? { fallback } : {}) };
|
|
481
|
+
})
|
|
473
482
|
: [{ file: it.file, line: it.line, selector: it.selector, snippet: (it as { snippet?: string }).snippet }],
|
|
474
483
|
);
|
|
475
484
|
}
|