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.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/docker/Dockerfile +7 -0
  4. package/docker/package.json +9 -0
  5. package/docker/runner.mjs +23 -0
  6. package/package.json +73 -0
  7. package/scripts/bench.mjs +151 -0
  8. package/scripts/build-act-corpus.mjs +143 -0
  9. package/scripts/build-adjudication.mjs +503 -0
  10. package/scripts/build-pack-rgaa.mjs +397 -0
  11. package/scripts/build-standards.mjs +548 -0
  12. package/scripts/copy-bundle.mjs +39 -0
  13. package/scripts/import-pack.mjs +107 -0
  14. package/scripts/sync-engine.mjs +84 -0
  15. package/scripts/sync-version.mjs +43 -0
  16. package/scripts/ultra11y.mjs +52739 -0
  17. package/scripts/vendor/act-testcases.json +12297 -0
  18. package/scripts/vendor/rgaa/SOURCE.json +8 -0
  19. package/scripts/vendor/rgaa/criteres.json +4144 -0
  20. package/scripts/vendor/rgaa/glossaire.json +480 -0
  21. package/scripts/vendor/wcag-2.2-fr.json +82 -0
  22. package/scripts/vendor/wcag-2.2-sc-universe.json +950 -0
  23. package/scripts/vendor/wcag-2.2-sc.json +574 -0
  24. package/scripts/verify-rgaa-source.mjs +126 -0
  25. package/scripts/verify-skill-bundle.mjs +91 -0
  26. package/skills/review-a11y/SKILL.md +123 -0
  27. package/skills/review-a11y/scripts/ultra11y.mjs +52739 -0
  28. package/skills/ultra11y/SKILL.md +292 -0
  29. package/skills/ultra11y/references/act.md +163 -0
  30. package/skills/ultra11y/references/adjudication.md +599 -0
  31. package/skills/ultra11y/references/audit.md +100 -0
  32. package/skills/ultra11y/references/authoring.md +37 -0
  33. package/skills/ultra11y/references/automation.md +113 -0
  34. package/skills/ultra11y/references/correction.md +41 -0
  35. package/skills/ultra11y/references/criteria.md +129 -0
  36. package/skills/ultra11y/references/cross-file.md +82 -0
  37. package/skills/ultra11y/references/dynamic.md +152 -0
  38. package/skills/ultra11y/references/false-positives.md +81 -0
  39. package/skills/ultra11y/references/fix.md +46 -0
  40. package/skills/ultra11y/references/focus-and-logic.md +64 -0
  41. package/skills/ultra11y/references/forbidden-patterns.md +61 -0
  42. package/skills/ultra11y/references/forms-and-errors.md +147 -0
  43. package/skills/ultra11y/references/guidance.md +55 -0
  44. package/skills/ultra11y/references/judgment.md +90 -0
  45. package/skills/ultra11y/references/media-and-motion.md +116 -0
  46. package/skills/ultra11y/references/methodology.md +72 -0
  47. package/skills/ultra11y/references/naming.md +117 -0
  48. package/skills/ultra11y/references/orchestration.md +44 -0
  49. package/skills/ultra11y/references/packs.md +193 -0
  50. package/skills/ultra11y/references/prd.md +104 -0
  51. package/skills/ultra11y/references/rendered.md +110 -0
  52. package/skills/ultra11y/references/scale.md +49 -0
  53. package/skills/ultra11y/references/standards.md +97 -0
  54. package/skills/ultra11y/references/structure.md +138 -0
  55. package/skills/ultra11y/references/verify.md +88 -0
  56. package/skills/ultra11y/references/widgets.md +106 -0
  57. package/skills/ultra11y/scripts/ultra11y.mjs +52739 -0
  58. package/src/act.ts +441 -0
  59. package/src/adjudicate.ts +630 -0
  60. package/src/aria.ts +230 -0
  61. package/src/audit.ts +396 -0
  62. package/src/auditor.ts +399 -0
  63. package/src/autofill.ts +102 -0
  64. package/src/axe-map.ts +294 -0
  65. package/src/baseline.ts +75 -0
  66. package/src/capture.ts +182 -0
  67. package/src/check.ts +243 -0
  68. package/src/cli.ts +1927 -0
  69. package/src/color.ts +102 -0
  70. package/src/config.ts +182 -0
  71. package/src/crawl.ts +75 -0
  72. package/src/criteria.ts +183 -0
  73. package/src/data/adjudication.json +770 -0
  74. package/src/data/guidance/rgaa.json +2395 -0
  75. package/src/data/standards/rgaa.glossary.json +478 -0
  76. package/src/data/standards/rgaa.json +3454 -0
  77. package/src/data/wcag-universe.json +530 -0
  78. package/src/data/wcag.json +1178 -0
  79. package/src/discover.ts +203 -0
  80. package/src/fix/codemods.ts +76 -0
  81. package/src/fix/diff.ts +64 -0
  82. package/src/fix/edits.ts +126 -0
  83. package/src/fix.ts +215 -0
  84. package/src/gh.ts +214 -0
  85. package/src/glob.ts +206 -0
  86. package/src/graph/build.ts +114 -0
  87. package/src/graph/engine-resolve.ts +0 -0
  88. package/src/graph/graph.ts +126 -0
  89. package/src/graph/imports.ts +359 -0
  90. package/src/graph/resolve.ts +73 -0
  91. package/src/graph/tsconfig.ts +113 -0
  92. package/src/grounding.ts +119 -0
  93. package/src/guidance/index.ts +45 -0
  94. package/src/guidance/types.ts +36 -0
  95. package/src/init.ts +113 -0
  96. package/src/mcp/handlers.ts +388 -0
  97. package/src/mcp/http.ts +260 -0
  98. package/src/mcp/prompts.ts +183 -0
  99. package/src/mcp/protocol.ts +187 -0
  100. package/src/mcp/resources.ts +136 -0
  101. package/src/mcp/server.ts +228 -0
  102. package/src/mcp/stdio.ts +119 -0
  103. package/src/mcp/tools.ts +347 -0
  104. package/src/messages.ts +1011 -0
  105. package/src/name.ts +174 -0
  106. package/src/orchestrate-templates.ts +231 -0
  107. package/src/orchestrate.ts +186 -0
  108. package/src/output.ts +108 -0
  109. package/src/pack.ts +187 -0
  110. package/src/parse/html.ts +258 -0
  111. package/src/parse/jsx-ast.ts +67 -0
  112. package/src/parse/jsx-bridge.ts +247 -0
  113. package/src/parse/jsx.ts +19 -0
  114. package/src/parse/source.ts +75 -0
  115. package/src/prd.ts +406 -0
  116. package/src/project-lock.ts +42 -0
  117. package/src/render.ts +307 -0
  118. package/src/report.ts +399 -0
  119. package/src/rules/colors.ts +125 -0
  120. package/src/rules/cross-registry.ts +264 -0
  121. package/src/rules/cross-rule.ts +83 -0
  122. package/src/rules/forms.ts +590 -0
  123. package/src/rules/frames.ts +33 -0
  124. package/src/rules/headings.ts +227 -0
  125. package/src/rules/images.ts +200 -0
  126. package/src/rules/links.ts +193 -0
  127. package/src/rules/mandatory.ts +173 -0
  128. package/src/rules/multimedia.ts +77 -0
  129. package/src/rules/navigation.ts +190 -0
  130. package/src/rules/presentation.ts +75 -0
  131. package/src/rules/registry.ts +57 -0
  132. package/src/rules/rule.ts +122 -0
  133. package/src/rules/scripts-aria.ts +636 -0
  134. package/src/rules/tables.ts +264 -0
  135. package/src/rules/timing.ts +55 -0
  136. package/src/sample.ts +176 -0
  137. package/src/scan-local.ts +866 -0
  138. package/src/scan.ts +538 -0
  139. package/src/standards/derive.ts +195 -0
  140. package/src/standards/index.ts +31 -0
  141. package/src/standards/pack-rules.ts +111 -0
  142. package/src/standards/pack.ts +81 -0
  143. package/src/standards/registry.ts +100 -0
  144. package/src/standards/types.ts +202 -0
  145. package/src/standards/validate.ts +544 -0
  146. package/src/standards/vocabulary.ts +75 -0
  147. package/src/types.ts +355 -0
  148. package/src/util.ts +32 -0
  149. package/src/vendor/codeindex-engine.d.mts +1009 -0
  150. package/src/vendor/codeindex-engine.mjs +13701 -0
  151. package/src/vendor/engine.meta.json +9 -0
  152. package/src/verify.ts +275 -0
  153. package/src/wcag.ts +128 -0
package/src/check.ts ADDED
@@ -0,0 +1,243 @@
1
+ // `check` — structural integrity gate on a produced report. Catches the ways a
2
+ // report can lie: a section dropped, a cited criterion that doesn't exist in the
3
+ // active standard, an NA without a justification, a missing pass rate. Exit non-zero
4
+ // on any issue. This is the anti-hallucination guard around the audit deliverable.
5
+ // The canonical WCAG report is keyed by 3-segment success criteria (1.4.3); a pack
6
+ // report by the pack's own 2-segment ids (RGAA 8.3) — the id grammar is per-standard
7
+ // so the version token "WCAG 2.2 —" can never be mistaken for a criterion.
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import type { AuditResult, Lang } from "./types.js";
11
+ import { hasSC } from "./wcag.js";
12
+ import { type StandardId, isCore, loadPack, hasId, idCaptureSource, derivePackResults } from "./standards/index.js";
13
+ import { buildWorklist, applyVerdicts, type VerifyItem } from "./verify.js";
14
+ import { groundItems } from "./grounding.js";
15
+
16
+ export interface CheckResult {
17
+ ok: boolean;
18
+ issues: string[];
19
+ }
20
+
21
+ const M = {
22
+ fr: {
23
+ section: (n: number) => `Section ${n} manquante dans le rapport.`,
24
+ crit: (id: string) => `Critère inexistant cité dans le rapport : ${id}.`,
25
+ na: (id: string) => `Critère NA sans justification : ${id}.`,
26
+ rateMissing: "Taux de réussite absent de l'en-tête du rapport.",
27
+ rateRange: (v: string) => `Taux de réussite hors bornes (0–100) : ${v}%.`,
28
+ rateInconsistent: (v: string, expected: number, c: number, nc: number) =>
29
+ `Taux de réussite incohérent avec la synthèse : l'en-tête indique ${v}% alors que C ÷ (C+NC) = ${c} ÷ ${c + nc} = ${expected}%.`,
30
+ overProject: (id: string) =>
31
+ `Critère sur-projeté : ${id} est marqué non conforme dans le rapport mais l'audit ne le dérive pas comme NC (élément hors périmètre du critère).`,
32
+ underProject: (id: string) => `Critère absent : l'audit dérive ${id} comme non conforme mais le rapport ne le présente pas.`,
33
+ semanticMissing: (p: string) =>
34
+ `Gate sémantique : aucun artefact de verdicts trouvé (${p}). Générez la worklist (\`verify --report <md>\`), statuez, puis relancez — ou passez \`--verdicts <fichier>\`.`,
35
+ semanticUnreadable: (p: string) => `Gate sémantique : artefact de verdicts illisible ou JSON invalide : ${p}.`,
36
+ semanticGate: (failed: number, total: number) => `Gate sémantique : ${failed}/${total} verdict(s) en échec (non statué, réfuté, non étayé ou non couvert).`,
37
+ semanticGround: (issue: string) => `Gate sémantique : ${issue}`,
38
+ },
39
+ en: {
40
+ section: (n: number) => `Section ${n} missing from the report.`,
41
+ crit: (id: string) => `Non-existent criterion cited in the report: ${id}.`,
42
+ na: (id: string) => `NA criterion without a justification: ${id}.`,
43
+ rateMissing: "Pass rate missing from the report header.",
44
+ rateRange: (v: string) => `Pass rate out of range (0–100): ${v}%.`,
45
+ rateInconsistent: (v: string, expected: number, c: number, nc: number) =>
46
+ `Pass rate inconsistent with the synthesis table: header says ${v}% but C ÷ (C+NC) = ${c} ÷ ${c + nc} = ${expected}%.`,
47
+ overProject: (id: string) =>
48
+ `Over-projected criterion: ${id} is marked non-conformant in the report but the audit does not derive it as NC (element outside the criterion's scope).`,
49
+ underProject: (id: string) => `Missing criterion: the audit derives ${id} as non-conformant but the report does not present it.`,
50
+ semanticMissing: (p: string) =>
51
+ `Semantic gate: no verdicts artifact found (${p}). Generate the worklist (\`verify --report <md>\`), adjudicate it, then re-run — or pass \`--verdicts <file>\`.`,
52
+ semanticUnreadable: (p: string) => `Semantic gate: verdicts artifact unreadable or invalid JSON: ${p}.`,
53
+ semanticGate: (failed: number, total: number) => `Semantic gate: ${failed}/${total} verdict(s) failing (unadjudicated, refuted, unsupported or uncovered).`,
54
+ semanticGround: (issue: string) => `Semantic gate: ${issue}`,
55
+ },
56
+ } as const;
57
+
58
+ export interface CheckOpts {
59
+ /** When given (with a pack standard), the applicability gate (R1) re-derives the pack
60
+ * view from this audit and fails on any NC criterion the report over- or under-projects. */
61
+ audit?: AuditResult;
62
+ }
63
+
64
+ export function checkReport(md: string, standard: StandardId = "wcag", lang: Lang = "en", opts: CheckOpts = {}): CheckResult {
65
+ const issues: string[] = [];
66
+ const s = M[lang];
67
+ const core = isCore(standard);
68
+ const pack = core ? null : loadPack(standard);
69
+ const exists = (id: string) => (core ? hasSC(id) : hasId(pack!, id));
70
+ // Core = the fixed 3-segment WCAG grammar (1.4.3). A pack's grammar is whatever its own
71
+ // `idPattern` declares (RGAA's 2-segment "8.3", a hypothetical Section 508 "E205.4"…) —
72
+ // built from the pack itself so the version token "WCAG 2.2 —" can never be mistaken
73
+ // for a criterion, without the engine hardcoding a single fixed pack shape.
74
+ const idGrammar = core ? "\\d{1,2}(?:\\.\\d{1,2}){2}" : idCaptureSource(pack!);
75
+ // A REAL criterion always renders "<id> — <title>", so "<id> —" (below) recognizes it
76
+ // wherever it sits. But a FABRICATED id has no title (the lookup fails), so the auditor
77
+ // block renders it BARE — on its "### 🔴 <id>" heading and its "**<criterion>** :" /
78
+ // "**WCAG** :" lines with no em-dash. Anchoring ONLY on "<id> —" is therefore blind to
79
+ // fabrications by construction (the exact P0 the gate exists to stop), so we ALSO scan
80
+ // those bare structural positions. The trailing `(?=\s|—|$)` lookahead keeps a pack's
81
+ // 2-segment grammar from mis-matching a 3-segment WCAG mapping ref (e.g. "1.3.1" on a
82
+ // pack's **WCAG** line stops at "1.3" then a ".", never a boundary).
83
+ const bound = "(?=\\s|—|$)";
84
+ const critRefs = [
85
+ new RegExp(`(${idGrammar})\\s*—`, "g"), // "<id> — <title>" (real criteria, anywhere)
86
+ new RegExp(`^#{2,4}\\s+\\S+\\s+(?:.*?\\s)?(${idGrammar})${bound}`, "gm"), // auditor-block heading "### 🔴 <id>" (pack: "### 🔴 RGAA <id>")
87
+ new RegExp(`^\\*\\*[^*\\n]+\\*\\*\\s*:\\s*(${idGrammar})${bound}`, "gm"), // "**Success criterion** : <id>" / "**WCAG** : <id>"
88
+ ];
89
+ const naItem = core ? /^-\s+(?:[A-Za-z]+\s+)?(\d{1,2}(?:\.\d{1,2}){2})\s*—/ : new RegExp(`^-\\s+(?:[A-Za-z]+\\s+)?(${idCaptureSource(pack!)})\\s*—`);
90
+
91
+ // 1. required sections (language-agnostic: "## 1." … "## 5.")
92
+ for (let n = 1; n <= 5; n++) {
93
+ if (!new RegExp(`^##\\s+${n}\\.`, "m").test(md)) issues.push(s.section(n));
94
+ }
95
+
96
+ // 2. every cited criterion id must resolve to a real criterion in the active standard
97
+ const seen = new Set<string>();
98
+ for (const critRef of critRefs) {
99
+ let m: RegExpExecArray | null;
100
+ critRef.lastIndex = 0;
101
+ while ((m = critRef.exec(md))) {
102
+ const id = m[1]!;
103
+ if (seen.has(id)) continue;
104
+ seen.add(id);
105
+ if (!exists(id)) issues.push(s.crit(id));
106
+ }
107
+ }
108
+
109
+ // 3. every NA entry must carry a justification (section 4 list items)
110
+ const naSection = sectionBody(md, 4);
111
+ for (const line of naSection.split("\n")) {
112
+ const item = naItem.exec(line);
113
+ if (item && !line.includes("_")) issues.push(s.na(item[1]!));
114
+ }
115
+
116
+ // 4. a pass rate must be present in a header bullet, be a sane 0–100 value, AND be
117
+ // arithmetically consistent with the report's own C/NC synthesis totals (a report that
118
+ // claims 99% while its table implies 17% is lying). The rate is defined as C ÷ (C + NC),
119
+ // rounded — mirroring src/audit.ts's conformancePct — so we recompute it from the Total
120
+ // row and fail on a material disagreement (±1 for rounding). Core (WCAG) only: a pack
121
+ // report's header rate is the WCAG-derived pct, not its own theme table's C/NC.
122
+ const rateM = /^-\s+\*\*[^*\n]*\*\*\s*:\s*(\d+(?:[.,]\d+)?)\s*%/m.exec(md);
123
+ if (!rateM) {
124
+ issues.push(s.rateMissing);
125
+ } else {
126
+ const pct = parseFloat(rateM[1]!.replace(",", "."));
127
+ if (pct < 0 || pct > 100) issues.push(s.rateRange(rateM[1]!));
128
+ else if (core) {
129
+ const totals = synthesisTotals(md);
130
+ if (totals) {
131
+ const { c, nc } = totals;
132
+ const expected = c + nc === 0 ? 100 : Math.round((c / (c + nc)) * 100);
133
+ if (Math.abs(pct - expected) > 1) issues.push(s.rateInconsistent(rateM[1]!, expected, c, nc));
134
+ }
135
+ }
136
+ }
137
+
138
+ // 5. applicability gate (pack + --in audit): the report's non-conformant criteria set
139
+ // must EQUAL what the audit derives with applicability (src/standards/derive.ts). Catches
140
+ // a hand-edited report that over-projects an NC onto an inapplicable criterion (RGAA R1),
141
+ // or drops a real one. Only runs for a pack standard with an audit in hand.
142
+ if (!core && pack && opts.audit) {
143
+ const derivedNc = new Set(
144
+ derivePackResults(opts.audit, standard)
145
+ .filter((r) => r.status === "NC")
146
+ .map((r) => r.id),
147
+ );
148
+ const reportNc = packReportNcIds(md, idCaptureSource(pack));
149
+ for (const id of reportNc) if (!derivedNc.has(id)) issues.push(s.overProject(id));
150
+ for (const id of derivedNc) if (!reportNc.has(id)) issues.push(s.underProject(id));
151
+ }
152
+
153
+ return { ok: issues.length === 0, issues };
154
+ }
155
+
156
+ /** The set of criterion ids the report presents as non-conformant — parsed from the
157
+ * section-2 auditor blocks' "**<criterion>** : <id> — …" lines (the theme line "8." can't
158
+ * match the 2-segment id grammar, so it's never mistaken for a criterion). */
159
+ function packReportNcIds(md: string, idGrammar: string): Set<string> {
160
+ const body = sectionBody(md, 2);
161
+ const re = new RegExp(`^\\*\\*[^*\\n]+\\*\\*\\s*:\\s*(${idGrammar})\\s*—`, "gm");
162
+ const ids = new Set<string>();
163
+ let m: RegExpExecArray | null;
164
+ while ((m = re.exec(body))) ids.add(m[1]!);
165
+ return ids;
166
+ }
167
+
168
+ export interface SemanticOptions {
169
+ /** Path of the report file — anchors the artifact auto-discovery. */
170
+ reportPath: string;
171
+ /** Explicit verdicts artifact; default: `VERIFY.todo.json` next to the report. */
172
+ verdictsPath?: string;
173
+ standard?: StandardId;
174
+ lang?: Lang;
175
+ /** Base dir for resolving the citations' relative file paths (default: cwd). */
176
+ cwd?: string;
177
+ }
178
+
179
+ export interface SemanticResult {
180
+ ok: boolean;
181
+ issues: string[];
182
+ total: number;
183
+ grounded: number;
184
+ moved: number;
185
+ failed: number;
186
+ }
187
+
188
+ /** `check --semantic` — the support-level gate above the structural `checkReport`.
189
+ * Fails CLOSED: no adjudicated verdicts artifact → fail (a green semantic exit must
190
+ * always mean the gate actually engaged). Coverage is re-derived from the report
191
+ * UNCAPPED, so a truncated worklist can't hide non-conformities; every passing
192
+ * verdict is then re-grounded content-level against the cited source (grounding.ts). */
193
+ export function checkSemantic(md: string, opts: SemanticOptions): SemanticResult {
194
+ const lang = opts.lang ?? "en";
195
+ const standard = opts.standard ?? "wcag";
196
+ const s = M[lang];
197
+ const empty = { total: 0, grounded: 0, moved: 0, failed: 0 };
198
+
199
+ const artifact = opts.verdictsPath ?? join(dirname(opts.reportPath), "VERIFY.todo.json");
200
+ if (!existsSync(artifact)) return { ok: false, issues: [s.semanticMissing(artifact)], ...empty };
201
+ let items: VerifyItem[];
202
+ try {
203
+ const parsed: unknown = JSON.parse(readFileSync(artifact, "utf8"));
204
+ if (!Array.isArray(parsed)) throw new Error("not an array");
205
+ items = parsed as VerifyItem[];
206
+ } catch {
207
+ return { ok: false, issues: [s.semanticUnreadable(artifact)], ...empty };
208
+ }
209
+
210
+ const issues: string[] = [];
211
+ const expected = buildWorklist(md, standard, Number.POSITIVE_INFINITY);
212
+ const gate = applyVerdicts(items, expected);
213
+ if (!gate.ok) issues.push(s.semanticGate(gate.failures.length, gate.total));
214
+
215
+ // Content-level re-validation of every verdict that passed the adjudication gate.
216
+ const passing = items.filter((it) => typeof it.verdict === "string" && ["supported", "partial"].includes(it.verdict.trim().toLowerCase()));
217
+ const grounding = groundItems(
218
+ passing.map((it) => ({ file: it.file, line: it.line, selector: it.selector, snippet: (it as { snippet?: string }).snippet })),
219
+ { cwd: opts.cwd },
220
+ );
221
+ for (const issue of grounding.issues) issues.push(s.semanticGround(issue));
222
+
223
+ return { ok: issues.length === 0, issues, total: gate.total, grounded: grounding.grounded, moved: grounding.moved, failed: grounding.failed };
224
+ }
225
+
226
+ /** The Conforming/Non-conforming counts from the synthesis table's bold Total row —
227
+ * `| **Total** | **C** | **NC** | **NA** | **To assess** |` (label is localized but
228
+ * always bold; the data rows' numeric cells are NOT bold, so only the Total row matches).
229
+ * null when no such row is present (nothing to cross-check against). */
230
+ function synthesisTotals(md: string): { c: number; nc: number } | null {
231
+ const m = /^\|\s*\*\*[^|*]+\*\*\s*\|\s*\*\*(\d+)\*\*\s*\|\s*\*\*(\d+)\*\*\s*\|\s*\*\*(\d+)\*\*\s*\|\s*\*\*(\d+)\*\*\s*\|/m.exec(md);
232
+ if (!m) return null;
233
+ return { c: Number.parseInt(m[1]!, 10), nc: Number.parseInt(m[2]!, 10) };
234
+ }
235
+
236
+ /** The body of section N (between "## N." and the next "## "). */
237
+ function sectionBody(md: string, n: number): string {
238
+ const start = new RegExp(`^##\\s+${n}\\.`, "m").exec(md);
239
+ if (!start) return "";
240
+ const from = start.index + start[0].length;
241
+ const next = /^##\s+/m.exec(md.slice(from));
242
+ return next ? md.slice(from, from + next.index) : md.slice(from);
243
+ }