ultra11y 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/docker/Dockerfile +7 -0
- package/docker/package.json +9 -0
- package/docker/runner.mjs +23 -0
- package/package.json +73 -0
- package/scripts/bench.mjs +151 -0
- package/scripts/build-act-corpus.mjs +143 -0
- package/scripts/build-adjudication.mjs +503 -0
- package/scripts/build-pack-rgaa.mjs +397 -0
- package/scripts/build-standards.mjs +548 -0
- package/scripts/copy-bundle.mjs +39 -0
- package/scripts/import-pack.mjs +107 -0
- package/scripts/sync-engine.mjs +84 -0
- package/scripts/sync-version.mjs +43 -0
- package/scripts/ultra11y.mjs +52739 -0
- package/scripts/vendor/act-testcases.json +12297 -0
- package/scripts/vendor/rgaa/SOURCE.json +8 -0
- package/scripts/vendor/rgaa/criteres.json +4144 -0
- package/scripts/vendor/rgaa/glossaire.json +480 -0
- package/scripts/vendor/wcag-2.2-fr.json +82 -0
- package/scripts/vendor/wcag-2.2-sc-universe.json +950 -0
- package/scripts/vendor/wcag-2.2-sc.json +574 -0
- package/scripts/verify-rgaa-source.mjs +126 -0
- package/scripts/verify-skill-bundle.mjs +91 -0
- package/skills/review-a11y/SKILL.md +123 -0
- package/skills/review-a11y/scripts/ultra11y.mjs +52739 -0
- package/skills/ultra11y/SKILL.md +292 -0
- package/skills/ultra11y/references/act.md +163 -0
- package/skills/ultra11y/references/adjudication.md +599 -0
- package/skills/ultra11y/references/audit.md +100 -0
- package/skills/ultra11y/references/authoring.md +37 -0
- package/skills/ultra11y/references/automation.md +113 -0
- package/skills/ultra11y/references/correction.md +41 -0
- package/skills/ultra11y/references/criteria.md +129 -0
- package/skills/ultra11y/references/cross-file.md +82 -0
- package/skills/ultra11y/references/dynamic.md +152 -0
- package/skills/ultra11y/references/false-positives.md +81 -0
- package/skills/ultra11y/references/fix.md +46 -0
- package/skills/ultra11y/references/focus-and-logic.md +64 -0
- package/skills/ultra11y/references/forbidden-patterns.md +61 -0
- package/skills/ultra11y/references/forms-and-errors.md +147 -0
- package/skills/ultra11y/references/guidance.md +55 -0
- package/skills/ultra11y/references/judgment.md +90 -0
- package/skills/ultra11y/references/media-and-motion.md +116 -0
- package/skills/ultra11y/references/methodology.md +72 -0
- package/skills/ultra11y/references/naming.md +117 -0
- package/skills/ultra11y/references/orchestration.md +44 -0
- package/skills/ultra11y/references/packs.md +193 -0
- package/skills/ultra11y/references/prd.md +104 -0
- package/skills/ultra11y/references/rendered.md +110 -0
- package/skills/ultra11y/references/scale.md +49 -0
- package/skills/ultra11y/references/standards.md +97 -0
- package/skills/ultra11y/references/structure.md +138 -0
- package/skills/ultra11y/references/verify.md +88 -0
- package/skills/ultra11y/references/widgets.md +106 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +52739 -0
- package/src/act.ts +441 -0
- package/src/adjudicate.ts +630 -0
- package/src/aria.ts +230 -0
- package/src/audit.ts +396 -0
- package/src/auditor.ts +399 -0
- package/src/autofill.ts +102 -0
- package/src/axe-map.ts +294 -0
- package/src/baseline.ts +75 -0
- package/src/capture.ts +182 -0
- package/src/check.ts +243 -0
- package/src/cli.ts +1927 -0
- package/src/color.ts +102 -0
- package/src/config.ts +182 -0
- package/src/crawl.ts +75 -0
- package/src/criteria.ts +183 -0
- package/src/data/adjudication.json +770 -0
- package/src/data/guidance/rgaa.json +2395 -0
- package/src/data/standards/rgaa.glossary.json +478 -0
- package/src/data/standards/rgaa.json +3454 -0
- package/src/data/wcag-universe.json +530 -0
- package/src/data/wcag.json +1178 -0
- package/src/discover.ts +203 -0
- package/src/fix/codemods.ts +76 -0
- package/src/fix/diff.ts +64 -0
- package/src/fix/edits.ts +126 -0
- package/src/fix.ts +215 -0
- package/src/gh.ts +214 -0
- package/src/glob.ts +206 -0
- package/src/graph/build.ts +114 -0
- package/src/graph/engine-resolve.ts +0 -0
- package/src/graph/graph.ts +126 -0
- package/src/graph/imports.ts +359 -0
- package/src/graph/resolve.ts +73 -0
- package/src/graph/tsconfig.ts +113 -0
- package/src/grounding.ts +119 -0
- package/src/guidance/index.ts +45 -0
- package/src/guidance/types.ts +36 -0
- package/src/init.ts +113 -0
- package/src/mcp/handlers.ts +388 -0
- package/src/mcp/http.ts +260 -0
- package/src/mcp/prompts.ts +183 -0
- package/src/mcp/protocol.ts +187 -0
- package/src/mcp/resources.ts +136 -0
- package/src/mcp/server.ts +228 -0
- package/src/mcp/stdio.ts +119 -0
- package/src/mcp/tools.ts +347 -0
- package/src/messages.ts +1011 -0
- package/src/name.ts +174 -0
- package/src/orchestrate-templates.ts +231 -0
- package/src/orchestrate.ts +186 -0
- package/src/output.ts +108 -0
- package/src/pack.ts +187 -0
- package/src/parse/html.ts +258 -0
- package/src/parse/jsx-ast.ts +67 -0
- package/src/parse/jsx-bridge.ts +247 -0
- package/src/parse/jsx.ts +19 -0
- package/src/parse/source.ts +75 -0
- package/src/prd.ts +406 -0
- package/src/project-lock.ts +42 -0
- package/src/render.ts +307 -0
- package/src/report.ts +399 -0
- package/src/rules/colors.ts +125 -0
- package/src/rules/cross-registry.ts +264 -0
- package/src/rules/cross-rule.ts +83 -0
- package/src/rules/forms.ts +590 -0
- package/src/rules/frames.ts +33 -0
- package/src/rules/headings.ts +227 -0
- package/src/rules/images.ts +200 -0
- package/src/rules/links.ts +193 -0
- package/src/rules/mandatory.ts +173 -0
- package/src/rules/multimedia.ts +77 -0
- package/src/rules/navigation.ts +190 -0
- package/src/rules/presentation.ts +75 -0
- package/src/rules/registry.ts +57 -0
- package/src/rules/rule.ts +122 -0
- package/src/rules/scripts-aria.ts +636 -0
- package/src/rules/tables.ts +264 -0
- package/src/rules/timing.ts +55 -0
- package/src/sample.ts +176 -0
- package/src/scan-local.ts +866 -0
- package/src/scan.ts +538 -0
- package/src/standards/derive.ts +195 -0
- package/src/standards/index.ts +31 -0
- package/src/standards/pack-rules.ts +111 -0
- package/src/standards/pack.ts +81 -0
- package/src/standards/registry.ts +100 -0
- package/src/standards/types.ts +202 -0
- package/src/standards/validate.ts +544 -0
- package/src/standards/vocabulary.ts +75 -0
- package/src/types.ts +355 -0
- package/src/util.ts +32 -0
- package/src/vendor/codeindex-engine.d.mts +1009 -0
- package/src/vendor/codeindex-engine.mjs +13701 -0
- package/src/vendor/engine.meta.json +9 -0
- package/src/verify.ts +275 -0
- package/src/wcag.ts +128 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tag": "v2.20.1",
|
|
3
|
+
"engineVersion": "2.20.1",
|
|
4
|
+
"sha256": {
|
|
5
|
+
"codeindex-engine.mjs": "a10b176230006be9d597aa04831d8fa4ad698ae5ef1e6e631eaeca8203833c1a",
|
|
6
|
+
"codeindex-engine.d.mts": "d841735589227b71c950a4f85c8758246a16d5ecb13482ac6b547c7e1c473642"
|
|
7
|
+
},
|
|
8
|
+
"syncedAt": "2026-07-26T09:00:10.480Z"
|
|
9
|
+
}
|
package/src/verify.ts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// `verify` — the high-assurance gate above `check`. Turns a report's
|
|
2
|
+
// non-conformities into a claim↔criterion↔element worklist for adversarial
|
|
3
|
+
// support-checking, then (--apply) reduces a filled worklist to pass/fail:
|
|
4
|
+
// any refuted/unsupported (or unadjudicated) claim fails the gate. Guards against
|
|
5
|
+
// fabricated non-conformities surviving into the final report.
|
|
6
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import type { Lang } from "./types.js";
|
|
9
|
+
import { getSC, scTitle } from "./wcag.js";
|
|
10
|
+
import { type StandardId, isCore, loadPack, getCriterion as getPackCriterion, idCaptureSource } from "./standards/index.js";
|
|
11
|
+
|
|
12
|
+
export const VERIFY_MAX = 40;
|
|
13
|
+
|
|
14
|
+
export type Verdict = "supported" | "partial" | "refuted" | "unsupported" | null;
|
|
15
|
+
|
|
16
|
+
export interface VerifyItem {
|
|
17
|
+
n: number;
|
|
18
|
+
criteriaId: string;
|
|
19
|
+
file: string;
|
|
20
|
+
line: number;
|
|
21
|
+
selector: string;
|
|
22
|
+
claim: string;
|
|
23
|
+
verdict: Verdict;
|
|
24
|
+
note: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const plain = (s: string) => s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
|
|
28
|
+
|
|
29
|
+
// ---- CURRENT shape (Phase 4): report.ts §2 renders one auditor block per NC
|
|
30
|
+
// criterion (src/auditor.ts `renderAuditorUnit`) — a "#### <icon> <label>" heading,
|
|
31
|
+
// a "**<criterion term>** : <id>[ — <title>]" line, then a checklist of
|
|
32
|
+
// "- [ ] `file:line` (`sel`) — message" occurrences (one per finding). The id is
|
|
33
|
+
// stated ONCE per criterion (not per occurrence, unlike the legacy shape below), so
|
|
34
|
+
// parsing tracks the "current criterion" as it walks the lines. ----
|
|
35
|
+
|
|
36
|
+
/** The auditor block's criterion line. Deliberately does NOT anchor on the label
|
|
37
|
+
* TEXT ("Critère"/"Success criterion"/"Critère de succès"/…) — that's the active
|
|
38
|
+
* standard's vocabulary (src/standards/vocabulary.ts) and varies per pack/lang.
|
|
39
|
+
* Matches ANY bold label, keying only on the id + the em-dash grammar, end-of-line
|
|
40
|
+
* anchored so a pack's (possibly shorter) id pattern can never partial-match inside
|
|
41
|
+
* a longer line, e.g. "**WCAG** : 2.4.7 (A)" (no trailing em-dash there). */
|
|
42
|
+
function auditorCriterionLine(standard: StandardId): RegExp {
|
|
43
|
+
const id = isCore(standard) ? "\\d{1,2}(?:\\.\\d{1,2}){2}" : idCaptureSource(loadPack(standard));
|
|
44
|
+
return new RegExp(`^\\*\\*[^*:]+\\*\\*\\s*:\\s*(${id})(?:\\s*—.*)?\\s*$`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// One checklist occurrence line under a criterion block. Exported so tests can pin the
|
|
48
|
+
// shared renderer (src/auditor.ts `occurrenceLine`) to this parser directly, rather than
|
|
49
|
+
// re-deriving the grammar — the two must never drift apart.
|
|
50
|
+
export const AUDITOR_OCCURRENCE = /^-\s\[ \]\s+`([^`]+):(\d+)`\s+\(`([^`]*)`\)\s+—\s+(.*)$/;
|
|
51
|
+
// Any markdown heading (##/###/####) — leaving one resets the "current criterion" so
|
|
52
|
+
// an occurrence-shaped line elsewhere in the document can never be mis-attributed.
|
|
53
|
+
const HEADING_LINE = /^#{2,4}\s/;
|
|
54
|
+
|
|
55
|
+
function buildWorklistFromAuditorBlocks(reportMd: string, standard: StandardId, max: number): VerifyItem[] {
|
|
56
|
+
const items: VerifyItem[] = [];
|
|
57
|
+
const critLine = auditorCriterionLine(standard);
|
|
58
|
+
const lines = reportMd.split("\n");
|
|
59
|
+
let currentId: string | null = null;
|
|
60
|
+
for (let i = 0; i < lines.length && items.length < max; i++) {
|
|
61
|
+
const line = lines[i]!;
|
|
62
|
+
const c = critLine.exec(line);
|
|
63
|
+
if (c) {
|
|
64
|
+
currentId = c[1]!;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (HEADING_LINE.test(line)) {
|
|
68
|
+
currentId = null;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!currentId) continue;
|
|
72
|
+
const occ = AUDITOR_OCCURRENCE.exec(line);
|
|
73
|
+
if (!occ) continue;
|
|
74
|
+
items.push({ n: items.length + 1, criteriaId: currentId, file: occ[1]!, line: Number(occ[2]), selector: occ[3]!, claim: occ[4]!, verdict: null, note: "" });
|
|
75
|
+
}
|
|
76
|
+
return items;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- LEGACY shape (pre-Phase-4 reports): report.ts §2 used to render one FLAT
|
|
80
|
+
// bullet per finding, "- **<id> — <title>** — `file:line` (`sel`)" followed by a
|
|
81
|
+
// plain-message sub-bullet. Kept as a fallback ONLY — a report produced by an older
|
|
82
|
+
// ultra11y version (or re-rendered from an old on-disk report.md) must still verify,
|
|
83
|
+
// never silently produce a 0-item (un-gated) worklist. ----
|
|
84
|
+
function legacyNcHeader(standard: StandardId): RegExp {
|
|
85
|
+
const id = isCore(standard) ? "\\d{1,2}(?:\\.\\d{1,2}){2}" : idCaptureSource(loadPack(standard));
|
|
86
|
+
return new RegExp(`^- \\*\\*(?:[A-Za-z]+ )?(${id}) — (.*?)\\*\\* — \`([^\`]+):(\\d+)\` \\(\`([^\`]*)\`\\)`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildWorklistLegacy(reportMd: string, standard: StandardId, max: number): VerifyItem[] {
|
|
90
|
+
const items: VerifyItem[] = [];
|
|
91
|
+
const header = legacyNcHeader(standard);
|
|
92
|
+
const lines = reportMd.split("\n");
|
|
93
|
+
for (let i = 0; i < lines.length && items.length < max; i++) {
|
|
94
|
+
const h = header.exec(lines[i]!);
|
|
95
|
+
if (!h) continue;
|
|
96
|
+
let claim = h[2] ?? "";
|
|
97
|
+
for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
|
|
98
|
+
const sub = /^\s+-\s+(.*)$/.exec(lines[j]!);
|
|
99
|
+
if (sub && !sub[1]!.startsWith("_")) {
|
|
100
|
+
claim = sub[1]!;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
items.push({ n: items.length + 1, criteriaId: h[1]!, file: h[3]!, line: Number(h[4]), selector: h[5]!, claim, verdict: null, note: "" });
|
|
105
|
+
}
|
|
106
|
+
return items;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Turn a report's non-conformities into a claim↔criterion↔element worklist for
|
|
110
|
+
* adversarial support-checking. Parses the CURRENT auditor-block NC shape first
|
|
111
|
+
* (see `buildWorklistFromAuditorBlocks`); if that finds nothing, falls back to the
|
|
112
|
+
* LEGACY flat-bullet shape (`buildWorklistLegacy`) so an older report still verifies.
|
|
113
|
+
* The two shapes are structurally disjoint (legacy bullets start with "- **", the
|
|
114
|
+
* new checklist items with "- [ ] "), so there is no ambiguity about which parsed. */
|
|
115
|
+
export function buildWorklist(reportMd: string, standard: StandardId = "wcag", max = VERIFY_MAX): VerifyItem[] {
|
|
116
|
+
const items = buildWorklistFromAuditorBlocks(reportMd, standard, max);
|
|
117
|
+
if (items.length) return items;
|
|
118
|
+
return buildWorklistLegacy(reportMd, standard, max);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const T = {
|
|
122
|
+
fr: {
|
|
123
|
+
title: "# Vérification des non-conformités (ultra11y)",
|
|
124
|
+
intro: "Pour CHAQUE entrée, ouvrez le fichier à la ligne citée et attribuez un verdict dans\n`VERIFY.todo.json` (champ `verdict`), avec une `note` :",
|
|
125
|
+
supported: "- `supported` — la non-conformité est réelle et correctement rattachée au critère ;",
|
|
126
|
+
partial: "- `partial` — réelle mais le critère/la formulation est imprécis ;",
|
|
127
|
+
refuted: "- `refuted` — fausse (l'élément cité est en réalité conforme) ;",
|
|
128
|
+
unsupported: "- `unsupported` — l'élément cité ne permet pas de trancher.",
|
|
129
|
+
semantic: "> Mode --semantic : vérifiez que l'extrait cité **étaye** réellement la non-conformité.",
|
|
130
|
+
then: "Puis : `ultra11y verify --apply VERIFY.todo.json` (échoue si un verdict est refuted/unsupported).",
|
|
131
|
+
understand: "Comprendre",
|
|
132
|
+
moreTests: (n: number, id: string) => `… +${n} autre(s) test(s) — voir \`criteria --standard <pack> ${id}\``,
|
|
133
|
+
checklistTitle: "## Liste de contrôle avant clôture",
|
|
134
|
+
checklist: [
|
|
135
|
+
"- [ ] Chaque entrée porte un verdict (aucun `null`).",
|
|
136
|
+
"- [ ] Aucune non-conformité inventée : chaque verdict `supported` cite un élément réel à la ligne indiquée.",
|
|
137
|
+
"- [ ] Non-conformité UNIQUEMENT si un test précis du référentiel actif échoue — citez-le. Une bonne pratique sans test normatif est une recommandation ; une simple préoccupation UX n'est ni l'un ni l'autre.",
|
|
138
|
+
"- [ ] Les critères « à évaluer » (rendu / jugement) ont été adjugés par l'agent (`verify --manual` → `--apply`), ou laissés en risque résiduel explicite (rendu → `scan`).",
|
|
139
|
+
"- [ ] Pour un code rendu par une bibliothèque (DSFR…), le verdict s'appuie sur le HTML **produit** (build / `scan`), pas sur la source JSX.",
|
|
140
|
+
"- [ ] `ultra11y verify --apply VERIFY.todo.json` repasse au vert.",
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
en: {
|
|
144
|
+
title: "# Non-conformity verification (ultra11y)",
|
|
145
|
+
intro: "For EACH entry, open the file at the cited line and assign a verdict in\n`VERIFY.todo.json` (field `verdict`), with a `note`:",
|
|
146
|
+
supported: "- `supported` — the non-conformity is real and correctly tied to the criterion;",
|
|
147
|
+
partial: "- `partial` — real but the criterion/wording is imprecise;",
|
|
148
|
+
refuted: "- `refuted` — false (the cited element is actually conforming);",
|
|
149
|
+
unsupported: "- `unsupported` — the cited element is not enough to decide.",
|
|
150
|
+
semantic: "> --semantic mode: confirm the cited snippet actually **supports** the non-conformity.",
|
|
151
|
+
then: "Then: `ultra11y verify --apply VERIFY.todo.json` (fails if any verdict is refuted/unsupported).",
|
|
152
|
+
understand: "Understanding",
|
|
153
|
+
moreTests: (n: number, id: string) => `… +${n} more test(s) — see \`criteria --standard <pack> ${id}\``,
|
|
154
|
+
checklistTitle: "## Pre-completion checklist",
|
|
155
|
+
checklist: [
|
|
156
|
+
"- [ ] Every entry has a verdict (no `null`).",
|
|
157
|
+
"- [ ] No invented non-conformity: every `supported` verdict cites a real element at the given line.",
|
|
158
|
+
"- [ ] Report NC ONLY if a precise test of the active standard fails — cite it. A good practice without a normative test is a recommendation; a purely UX concern is neither.",
|
|
159
|
+
"- [ ] The “to assess” criteria (rendering / judgment) have been adjudicated by the agent (`verify --manual` → `--apply`), or left as an explicit residual risk (rendering → `scan`).",
|
|
160
|
+
"- [ ] For component-library-rendered code (DSFR…), the verdict relies on the **produced** HTML (build / `scan`), not the JSX source.",
|
|
161
|
+
"- [ ] `ultra11y verify --apply VERIFY.todo.json` is green again.",
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
} as const;
|
|
165
|
+
|
|
166
|
+
export function formatWorklist(items: VerifyItem[], semantic: boolean, standard: StandardId = "wcag", lang: Lang = "en"): string {
|
|
167
|
+
const s = T[lang];
|
|
168
|
+
const core = isCore(standard);
|
|
169
|
+
const pack = core ? null : loadPack(standard);
|
|
170
|
+
const out: string[] = [];
|
|
171
|
+
out.push(s.title, "");
|
|
172
|
+
out.push(s.intro, "");
|
|
173
|
+
out.push(s.supported, s.partial, s.refuted, s.unsupported, "");
|
|
174
|
+
if (semantic) out.push(s.semantic, "");
|
|
175
|
+
out.push(s.then, "");
|
|
176
|
+
for (const it of items) {
|
|
177
|
+
out.push(`- [ ] #${it.n} **${it.criteriaId}** @ \`${it.file}:${it.line}\` (\`${it.selector}\`) — ${it.claim}`);
|
|
178
|
+
// Ground the judgment in the active standard's reference so the verdict is checked
|
|
179
|
+
// against real conditions, not a guess.
|
|
180
|
+
if (core) {
|
|
181
|
+
const sc = getSC(it.criteriaId);
|
|
182
|
+
if (sc) {
|
|
183
|
+
out.push(` WCAG ${sc.sc} — ${scTitle(sc.sc, lang)} [${sc.level}] · ${s.understand}: ${sc.understanding}`);
|
|
184
|
+
if (sc.techniques?.length) out.push(` Techniques: ${sc.techniques.join(", ")}`);
|
|
185
|
+
}
|
|
186
|
+
} else if (pack) {
|
|
187
|
+
const c = getPackCriterion(pack, it.criteriaId);
|
|
188
|
+
const tests = Object.values(c?.tests ?? {}).flat();
|
|
189
|
+
if (tests.length) {
|
|
190
|
+
out.push(` ${pack.name} ${it.criteriaId} :`);
|
|
191
|
+
for (const test of tests.slice(0, 6)) out.push(` - ${plain(test)}`);
|
|
192
|
+
// Honest overflow count instead of a silent drop — point at the full list.
|
|
193
|
+
if (tests.length > 6) out.push(` - ${s.moreTests(tests.length - 6, it.criteriaId)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
out.push("");
|
|
198
|
+
out.push(s.checklistTitle, "");
|
|
199
|
+
for (const line of s.checklist) out.push(line);
|
|
200
|
+
out.push("");
|
|
201
|
+
return out.join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface ApplyResult {
|
|
205
|
+
ok: boolean;
|
|
206
|
+
total: number;
|
|
207
|
+
refuted: number;
|
|
208
|
+
unsupported: number;
|
|
209
|
+
unadjudicated: number;
|
|
210
|
+
invalid: number;
|
|
211
|
+
missing: number; // report NCs with no verdict at all (coverage gap) — only when `expected` is given
|
|
212
|
+
failures: VerifyItem[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const itemKey = (it: VerifyItem): string => `${it.criteriaId}|${it.file}|${it.line}|${it.selector}`;
|
|
216
|
+
|
|
217
|
+
// Only these two verdicts clear the gate. Everything else — refuted, unsupported,
|
|
218
|
+
// null/unadjudicated, AND any unknown/typo/mis-cased token — must FAIL, so a
|
|
219
|
+
// fat-fingered verdict can never produce a false-green gate.
|
|
220
|
+
const PASSING: ReadonlySet<string> = new Set(["supported", "partial"]);
|
|
221
|
+
|
|
222
|
+
/** Canonicalise a stored verdict for gating: trim + lowercase, null if not a
|
|
223
|
+
* non-empty string. Does not coerce unknown tokens to a valid verdict. */
|
|
224
|
+
function normalizeVerdict(v: unknown): string | null {
|
|
225
|
+
if (typeof v !== "string") return null;
|
|
226
|
+
const s = v.trim().toLowerCase();
|
|
227
|
+
return s ? s : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Adjudicate a verdicts file. When `expected` (the worklist derived from the report via
|
|
231
|
+
* buildWorklist) is provided, ALSO fail on any report NC the verdicts file does not cover
|
|
232
|
+
* — so a truncated/empty verdicts set can't slip a to-be-refuted finding past the gate. */
|
|
233
|
+
export function applyVerdicts(items: VerifyItem[], expected?: VerifyItem[]): ApplyResult {
|
|
234
|
+
let refuted = 0;
|
|
235
|
+
let unsupported = 0;
|
|
236
|
+
let unadjudicated = 0;
|
|
237
|
+
let invalid = 0;
|
|
238
|
+
let missing = 0;
|
|
239
|
+
const failures: VerifyItem[] = [];
|
|
240
|
+
for (const it of items) {
|
|
241
|
+
const v = normalizeVerdict(it.verdict);
|
|
242
|
+
if (v !== null && PASSING.has(v)) continue;
|
|
243
|
+
failures.push(it);
|
|
244
|
+
if (v === "refuted") refuted++;
|
|
245
|
+
else if (v === "unsupported") unsupported++;
|
|
246
|
+
else if (v === null) unadjudicated++;
|
|
247
|
+
else invalid++; // unknown/typo token — counted as a failure, not silently passed
|
|
248
|
+
}
|
|
249
|
+
if (expected) {
|
|
250
|
+
const covered = new Set(items.map(itemKey));
|
|
251
|
+
for (const e of expected) {
|
|
252
|
+
if (!covered.has(itemKey(e))) {
|
|
253
|
+
failures.push(e);
|
|
254
|
+
missing++;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const total = expected ? expected.length : items.length;
|
|
259
|
+
return { ok: failures.length === 0, total, refuted, unsupported, unadjudicated, invalid, missing, failures };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface WriteWorklistResult {
|
|
263
|
+
todoPath: string;
|
|
264
|
+
mdPath: string;
|
|
265
|
+
count: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function writeWorklist(items: VerifyItem[], outDir: string, semantic: boolean, standard: StandardId = "wcag", lang: Lang = "en"): WriteWorklistResult {
|
|
269
|
+
mkdirSync(outDir, { recursive: true });
|
|
270
|
+
const todoPath = join(outDir, "VERIFY.todo.json");
|
|
271
|
+
const mdPath = join(outDir, "VERIFY.md");
|
|
272
|
+
writeFileSync(todoPath, JSON.stringify(items, null, 2) + "\n");
|
|
273
|
+
writeFileSync(mdPath, formatWorklist(items, semantic, standard, lang));
|
|
274
|
+
return { todoPath, mdPath, count: items.length };
|
|
275
|
+
}
|
package/src/wcag.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Loads the bundled WCAG 2.2 Level AA reference (offline, embedded by tsup) and exposes
|
|
2
|
+
// success-criterion / principle / guideline / technique lookups. This is the engine's
|
|
3
|
+
// CANONICAL key: everything downstream (audit, report, criteria, check, verify) resolves
|
|
4
|
+
// success criteria through here. Country standards (RGAA, …) are derived packs that map
|
|
5
|
+
// their criteria onto these SCs — see src/standards/.
|
|
6
|
+
import wcagJson from "./data/wcag.json";
|
|
7
|
+
import wcagUniverseJson from "./data/wcag-universe.json";
|
|
8
|
+
import type { WcagData, Sc, WcagPrinciple, WcagGuideline, Automatability, WcagUniverseData, ScStatus, Lang } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const data = wcagJson as unknown as WcagData;
|
|
11
|
+
const universe = wcagUniverseJson as unknown as WcagUniverseData;
|
|
12
|
+
|
|
13
|
+
const byId = new Map<string, Sc>(data.criteria.map((c) => [c.sc, c]));
|
|
14
|
+
const universeById = new Map(universe.criteria.map((c) => [c.id, c]));
|
|
15
|
+
|
|
16
|
+
export function allSC(): Sc[] {
|
|
17
|
+
return data.criteria;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getSC(id: string): Sc | undefined {
|
|
21
|
+
return byId.get(id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function hasSC(id: string): boolean {
|
|
25
|
+
return byId.has(id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Classify ANY real WCAG 2.x success criterion against the shipped AA core — "core-AA"
|
|
29
|
+
* (in `allSC()`), "out-of-core" (a real WCAG AAA criterion), "removed" (obsolete, e.g.
|
|
30
|
+
* 4.1.1 Parsing), or `undefined` if `id` is not a real WCAG success criterion at all
|
|
31
|
+
* (fabricated). Backs a pack's out-of-core SC mappings (src/standards/validate.ts
|
|
32
|
+
* `classifySc`, src/standards/derive.ts) without hardcoding a single tolerated id. */
|
|
33
|
+
export function knownScStatus(id: string): ScStatus | undefined {
|
|
34
|
+
return universeById.get(id)?.status;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function allPrinciples(): WcagPrinciple[] {
|
|
38
|
+
return data.principles;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function allGuidelines(): WcagGuideline[] {
|
|
42
|
+
return data.guidelines;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Default `lang` stays "en" so every pre-existing call site (no lang argument) keeps
|
|
46
|
+
// rendering the authoritative W3C English title — back-compat, no behaviour change.
|
|
47
|
+
// `lang: "fr"` resolves the W3C AUTHORIZED French translation (titleFr); falls back to
|
|
48
|
+
// the English title if a French title is ever absent (should not happen for core-AA —
|
|
49
|
+
// the build guarantees completeness, see scripts/build-standards.mjs).
|
|
50
|
+
export function principleTitle(n: number, lang: Lang = "en"): string | undefined {
|
|
51
|
+
const p = data.principles.find((p) => p.number === n);
|
|
52
|
+
if (!p) return undefined;
|
|
53
|
+
return lang === "fr" ? (p.titleFr ?? p.title) : p.title;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function guidelineTitle(n: string, lang: Lang = "en"): string | undefined {
|
|
57
|
+
const g = data.guidelines.find((g) => g.number === n);
|
|
58
|
+
if (!g) return undefined;
|
|
59
|
+
return lang === "fr" ? (g.titleFr ?? g.title) : g.title;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Localized success-criterion title. `getSC(id)?.title` stays the raw English record;
|
|
63
|
+
* use this wherever a title is RENDERED so `--lang fr` resolves the authorized French
|
|
64
|
+
* translation. */
|
|
65
|
+
export function scTitle(id: string, lang: Lang = "en"): string | undefined {
|
|
66
|
+
const sc = byId.get(id);
|
|
67
|
+
if (!sc) return undefined;
|
|
68
|
+
return lang === "fr" ? (sc.titleFr ?? sc.title) : sc.title;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** SCs grouped by guideline ("1.4" → [...]), in dataset (numbering) order. */
|
|
72
|
+
export function scsByGuideline(): Map<string, Sc[]> {
|
|
73
|
+
const m = new Map<string, Sc[]>();
|
|
74
|
+
for (const c of data.criteria) (m.get(c.guideline) ?? m.set(c.guideline, []).get(c.guideline)!).push(c);
|
|
75
|
+
return m;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** SCs grouped by principle (1..4 → [...]), in dataset (numbering) order. */
|
|
79
|
+
export function scsByPrinciple(): Map<number, Sc[]> {
|
|
80
|
+
const m = new Map<number, Sc[]>();
|
|
81
|
+
for (const c of data.criteria) (m.get(c.principle) ?? m.set(c.principle, []).get(c.principle)!).push(c);
|
|
82
|
+
return m;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function automatability(id: string): Automatability | undefined {
|
|
86
|
+
return byId.get(id)?.automatability;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function techniques(id: string): string[] {
|
|
90
|
+
return byId.get(id)?.techniques ?? [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function ruleIds(id: string): string[] {
|
|
94
|
+
return byId.get(id)?.ruleIds ?? [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function understanding(id: string): string | undefined {
|
|
98
|
+
return byId.get(id)?.understanding;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function meta(): Pick<WcagData, "wcagVersion" | "level" | "source" | "license" | "criteriaSource"> {
|
|
102
|
+
return {
|
|
103
|
+
wcagVersion: data.wcagVersion,
|
|
104
|
+
level: data.level,
|
|
105
|
+
source: data.source,
|
|
106
|
+
license: data.license,
|
|
107
|
+
criteriaSource: data.criteriaSource,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Numeric SC compare: sorts "1.4.10" after "1.4.3" (version tuples). Moved from standard.ts. */
|
|
112
|
+
export function compareSC(a: string, b: string): number {
|
|
113
|
+
const pa = a.split(".").map(Number);
|
|
114
|
+
const pb = b.split(".").map(Number);
|
|
115
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
116
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
117
|
+
if (d) return d;
|
|
118
|
+
}
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { data as wcag };
|
|
123
|
+
|
|
124
|
+
/** The W3C technique/failure ids recorded for a success criterion — the set a non-conformity
|
|
125
|
+
* on that criterion may legitimately cite. Empty when the dataset records none. */
|
|
126
|
+
export function techniquesFor(id: string): string[] {
|
|
127
|
+
return getSC(id)?.techniques ?? [];
|
|
128
|
+
}
|