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/types.ts ADDED
@@ -0,0 +1,355 @@
1
+ // Single source of truth for shared types + the embedded version. sync-version.mjs
2
+ // rewrites VERSION at release time (kept in lockstep with package.json + SKILL.md).
3
+ export const VERSION = "2.22.0";
4
+ export const SCHEMA_VERSION = 2;
5
+
6
+ // Lang = the UI FRAME's language — the `L` localization tables each module (report,
7
+ // check, verify, criteria…) renders in. NOT a country pack's own locale set, which may
8
+ // carry any BCP-47-ish tag ("de", "pt-BR"…) — see `LocaleString` in
9
+ // src/standards/types.ts for that decoupled, per-pack concept.
10
+ export type Lang = "fr" | "en";
11
+ export type Severity = "bloquant" | "majeur" | "mineur";
12
+ // C = conforme, NC = non conforme, NA = non applicable, manual = à évaluer
13
+ // (needs-rendering / judgment criteria the engine cannot decide on its own).
14
+ export type Status = "C" | "NC" | "NA" | "manual";
15
+ export type Automatability = "static" | "needs-rendering" | "judgment";
16
+ export type ParserKind = "html" | "css" | "jsx" | "cross";
17
+
18
+ export interface GlossaryEntry {
19
+ title: string;
20
+ body: string;
21
+ }
22
+ export type Glossary = Record<string, GlossaryEntry>;
23
+
24
+ // ---- normative page sample (échantillon) — Task 5. STANDARD-AGNOSTIC mechanics: a real
25
+ // audit runs over a representative set of served pages (+ transverse elements audited on
26
+ // every page), NOT the whole file tree. The REQUIRED page KINDS a given standard mandates
27
+ // live in the pack (`sampleMethodology`, src/standards/types.ts), never here.
28
+ // SECURITY: `storageState` is a FILE PATH (a Playwright session file) — its CONTENT is
29
+ // never read into any output/report/finding; only the path is ever cited.
30
+ export interface SamplePage {
31
+ id: string; // stable slug used for per-page provenance, e.g. "accueil"
32
+ name: string; // human page name shown in the report, e.g. "Page d'accueil"
33
+ url: string; // served URL (http(s)://) or a local HTML file path
34
+ auth?: boolean; // the page sits behind authentication (renders an auth badge)
35
+ storageState?: string; // Playwright storageState FILE PATH (content never read) — per-page auth
36
+ notes?: string; // reproduction steps / required state, surfaced in the ticket repro block
37
+ }
38
+ export interface SampleConfig {
39
+ pages: SamplePage[];
40
+ // Element descriptions audited on EVERY page (header, nav, footer, modals…).
41
+ transverse?: string[];
42
+ }
43
+ // The sample as recorded in an AuditResult / DynamicResult. The `storageState` PATH is
44
+ // deliberately dropped (never persisted): the report needs name/url/auth/notes only.
45
+ export interface SampleScope {
46
+ pages: Array<{ id: string; name: string; url: string; auth?: boolean; notes?: string }>;
47
+ transverse?: string[];
48
+ }
49
+
50
+ // ---- WCAG 2.2 canonical core (src/data/wcag.json, produced by scripts/build-standards.mjs)
51
+ // The engine's canonical key. Success-criterion ids/titles/levels are derived from the
52
+ // official W3C source (https://github.com/w3c/wcag); rule coverage + automatability are
53
+ // engine-specific. RGAA and other country standards are derived packs (see src/standards/).
54
+ export type WcagLevel = "A" | "AA";
55
+
56
+ export interface Sc {
57
+ sc: string; // dotted SC id, "<principle>.<guideline>.<n>", e.g. "1.4.3"
58
+ principle: number; // 1..4
59
+ guideline: string; // "1.4"
60
+ title: string; // authoritative W3C English title
61
+ // Authoritative French title from the W3C AUTHORIZED translation
62
+ // (https://www.w3.org/Translations/WCAG22-fr/), vendored at scripts/vendor/wcag-2.2-fr.json.
63
+ // Present on every core-AA SC (the build fails otherwise) — optional only so older
64
+ // wcag.json snapshots still parse. See src/wcag.ts `scTitle`.
65
+ titleFr?: string;
66
+ level: WcagLevel;
67
+ addedIn: string; // "2.0" | "2.1" | "2.2"
68
+ automatability: Automatability;
69
+ ruleIds: string[]; // engine rules contributing to this SC
70
+ understanding: string; // W3C Understanding doc URL (manual-check grounding)
71
+ techniques?: string[]; // language-neutral W3C technique codes
72
+ tests?: string[]; // optional manual-check lines
73
+ notes?: string;
74
+ }
75
+
76
+ export interface WcagPrinciple {
77
+ number: number;
78
+ title: string;
79
+ titleFr?: string; // W3C authorized French translation — see Sc.titleFr
80
+ }
81
+ export interface WcagGuideline {
82
+ number: string; // "1.4"
83
+ title: string;
84
+ titleFr?: string; // W3C authorized French translation — see Sc.titleFr
85
+ }
86
+
87
+ export interface WcagData {
88
+ wcagVersion: string; // "2.2"
89
+ level: string; // "AA"
90
+ source: string;
91
+ license: string;
92
+ criteriaSource: string;
93
+ principles: WcagPrinciple[];
94
+ guidelines: WcagGuideline[];
95
+ criteria: Sc[];
96
+ }
97
+
98
+ // ---- WCAG SC universe (src/data/wcag-universe.json, produced by
99
+ // scripts/build-standards.mjs --refresh-universe + build()): EVERY real WCAG 2.x
100
+ // success criterion — every level (A/AA/AAA), plus the obsolete/removed 4.1.1 Parsing —
101
+ // derived from the same vendored W3C source as the AA core, never invented. This is the
102
+ // guardrail a pack's out-of-core SC mapping (e.g. an AAA criterion, or the removed 4.1.1)
103
+ // is checked against, so validation never has to hardcode a single tolerated exception —
104
+ // see src/wcag.ts `knownScStatus` and src/standards/validate.ts `classifySc`.
105
+ export type ScStatus = "core-AA" | "out-of-core" | "removed";
106
+
107
+ export interface WcagUniverseEntry {
108
+ id: string; // dotted SC id, e.g. "1.4.6"
109
+ title: string;
110
+ level: string; // "A" | "AA" | "AAA" | "" (no level ⇒ removed/obsolete)
111
+ status: ScStatus;
112
+ }
113
+
114
+ export interface WcagUniverseData {
115
+ wcagVersion: string;
116
+ source: string;
117
+ criteriaSource: string;
118
+ provenance: string;
119
+ criteria: WcagUniverseEntry[];
120
+ }
121
+
122
+ // ---- engine findings + audit result
123
+ export interface Finding {
124
+ ruleId: string;
125
+ criteriaId: string;
126
+ file: string;
127
+ line: number;
128
+ col: number;
129
+ selectorHint: string;
130
+ severity: Severity;
131
+ message: string;
132
+ remediation: string;
133
+ // Language-neutral resolution key (src/messages.ts MSG_CATALOG), additive/optional so
134
+ // older AuditResult JSON (no `msg`) still renders via the baked message/remediation
135
+ // above. `message`/`remediation` are the canonical ENGLISH bake (AI-facing); a renderer
136
+ // resolves `msg` through resolveMessage/resolveRemediation for `--lang fr` (or any other
137
+ // supported lang), falling back to the baked strings when absent or the id is unknown.
138
+ msg?: { id: string; params?: Record<string, string | number> };
139
+ snippet: string;
140
+ // Source byte range of the anchoring element (htmlparser2 [start, end), open+close
141
+ // tag). Optional so older AuditResult JSON still parses. Used by `fix` (codemods
142
+ // edit by range) and by `init` baseline diffing (stable finding identity that
143
+ // survives line drift). Absent for stdin/JSX findings where it would be unusable.
144
+ sourceStart?: number;
145
+ sourceEnd?: number;
146
+ // Cross-file context: the OTHER site that explains this finding — e.g. the
147
+ // component definition for a finding raised at a usage site. Optional/additive
148
+ // (cross-file rules only), so existing AuditResult JSON still parses.
149
+ // `note` is the canonical baked ENGLISH prose (mirrors message/remediation above);
150
+ // `noteId` is an optional key into src/messages.ts's note catalog, resolved at
151
+ // render time by `resolveNote` for `--lang fr` (or any other supported lang),
152
+ // falling back to the baked `note` when absent or the id is unknown.
153
+ related?: { file: string; line: number; col: number; selectorHint: string; note: string; noteId?: string };
154
+ // AI signal: this finding was raised on a .vue/.svelte/.astro SOURCE template, whose
155
+ // slot/dynamic-injected content is invisible to static analysis — so it is a
156
+ // PRELIMINARY verdict to confirm against the rendered output, not a certainty.
157
+ // Optional/additive (no schemaVersion bump); absent = full-confidence static finding.
158
+ preliminary?: boolean;
159
+ // Set when this finding was raised on a RENDERED capture file (real serialized DOM)
160
+ // and re-attributed to the source component that produced it. `file`/`sourceStart`
161
+ // still index the capture bytes (so `fix` and baseline diffing stay stable); origin
162
+ // points back to the source. Optional/additive (no schemaVersion bump).
163
+ origin?: { capture: string; sourceFile?: string; component?: string; sourceLine?: number };
164
+ // NON-NORMATIVE recommendation: a good-practice signal NOT backed by a testable
165
+ // criterion of the active standard (e.g. "one h1 per page", a best-practice-only axe
166
+ // violation, an agent-noted UX improvement). An advisory finding can NEVER flip a
167
+ // criterion to NC nor enter `conformancePct`; it is rendered as « Recommandation (non
168
+ // normative) », never as a non-conformity, but stays attached to its criterion and in
169
+ // the flat findings list so grounding + `check` still resolve it. Optional/additive
170
+ // (no SCHEMA_VERSION bump) — absent ⇒ normative, following the `preliminary`/`origin`/
171
+ // `decidedBy` pattern.
172
+ advisory?: boolean;
173
+ // Per-sample provenance (`scan --sample`): the crawled page NAME, whether it sits behind
174
+ // authentication, and optional reproduction notes. Attached by `mergeDynamic` from the
175
+ // dynamic finding's originating sample page, so the auditor ticket (src/auditor.ts) can
176
+ // render the human page name + auth flag under « Pages / URLs impactées » and the notes
177
+ // under « Contexte de reproduction ». Optional/additive (no SCHEMA_VERSION bump).
178
+ sample?: { page?: string; authRequired?: boolean; notes?: string };
179
+ // Localized message/remediation PAIR carried on the finding itself — the runtime channel
180
+ // `resolveMessage`/`resolveRemediation` read BEFORE the static MSG_CATALOG. Declarative
181
+ // pack rules (src/standards/pack-rules.ts) are loaded at runtime and cannot register into
182
+ // the compiled MSG_CATALOG, so a `pack:<key>:<id>` finding attaches its rule's `{en,fr}`
183
+ // strings here; a renderer at `--lang fr` then picks the French text instead of falling
184
+ // back to the English `message` bake. Keyed by the UI frame's `Lang` (fr|en). Optional/
185
+ // additive (no SCHEMA_VERSION bump) — absent ⇒ resolve via `msg`/baked strings as before.
186
+ i18n?: { message: Partial<Record<Lang, string>>; remediation: Partial<Record<Lang, string>> };
187
+ // Set when this finding was projected onto a pack criterion via an opt-in SECONDARY
188
+ // crosswalk mapping (src/standards/types.ts SecondaryMapping) — an additional criterion
189
+ // whose official WCAG mapping does NOT contain the finding's SC, an explicit,
190
+ // config-enabled deviation. Carried ONLY on the copy attached to the secondary criterion
191
+ // in a pack projection (`derivePackResults`); the core finding is never mutated. `note`
192
+ // is the resolved localized explanation of the deviation, appended by the auditor block
193
+ // (src/auditor.ts). Optional/additive (no SCHEMA_VERSION bump) — absent ⇒ ordinary
194
+ // SC-faithful projection, following the `advisory`/`preliminary`/`origin` pattern.
195
+ secondary?: { note?: string };
196
+ }
197
+
198
+ export interface CriterionResult {
199
+ id: string; // WCAG success-criterion id, e.g. "1.4.3"
200
+ guideline: string; // WCAG guideline this SC belongs to, e.g. "1.4"
201
+ status: Status;
202
+ findings: Finding[];
203
+ justification?: string;
204
+ // Who decided this criterion's status. Absent = the deterministic engine (the default
205
+ // for every audit). "agent" = an AI adjudication of a formerly-`manual` judgment/
206
+ // rendering criterion, recorded via `verify --manual` → `verify --apply` (each carries a
207
+ // justification; an NC carries groundable findings). "scan" = the dynamic tier upgraded
208
+ // a needs-rendering residual to C/NC (src/scan.ts mergeDynamic). Optional/additive — no
209
+ // SCHEMA_VERSION bump; older AuditResult JSON (no `decidedBy`) reads as engine-decided.
210
+ decidedBy?: "engine" | "agent" | "scan";
211
+ }
212
+
213
+ export interface GuidelineTally {
214
+ key: string; // WCAG guideline number, e.g. "1.4"
215
+ title: string;
216
+ c: number;
217
+ nc: number;
218
+ na: number;
219
+ manual: number;
220
+ }
221
+
222
+ export interface ResidualRisk {
223
+ criteriaId: string;
224
+ reason: string;
225
+ automatability: Automatability;
226
+ }
227
+
228
+ export interface AuditResult {
229
+ tool: "ultra11y";
230
+ standard: "wcag"; // self-describing: the engine keys on WCAG 2.2 SCs (packs are derived views)
231
+ version: string;
232
+ schemaVersion: number;
233
+ date: string; // YYYY-MM-DD
234
+ scope: {
235
+ inputs: string[];
236
+ files: number;
237
+ // Set when `--max-files` capped discovery (highest-priority files audited first).
238
+ truncated?: { limit: number; total: number; skipped: number };
239
+ // Set when content de-duplication collapsed identical files to one canonical audit.
240
+ dedup?: { canonicalFiles: number; duplicateFiles: number };
241
+ // Set when source files render component-LIBRARY components whose real HTML
242
+ // output is invisible to static source analysis (e.g. DSFR). The source verdict
243
+ // is preliminary for them — audit the build output (`render`) or `scan`.
244
+ rendered?: { opaqueLibraries: string[]; files: number };
245
+ // Set when .vue/.svelte/.astro SOURCE templates were audited. Their slots,
246
+ // snippets and dynamic bindings are invisible to static analysis, so findings on
247
+ // them are PRELIMINARY — audit the rendered output (`render`/`scan`) to confirm.
248
+ sourceTemplate?: { files: number; extensions: string[] };
249
+ // Set when RENDERED capture files (real serialized DOM) were audited at full
250
+ // fidelity — the true markup a component library / SFC emits, not its source call.
251
+ captures?: { files: number; components: string[] };
252
+ // Set by the coverage pass (`--require-captures` / `render --coverage`): which
253
+ // components have a rendered capture vs which are still opaque-source-only blind
254
+ // spots. Keys are "posix/path#Component". `unattributed` = capture files with no
255
+ // resolvable source (no provenance) — audited, but not credited to a component.
256
+ captureCoverage?: { total: number; covered: string[]; blindSpots: string[]; unattributed: number };
257
+ // Set when at least one `<html lang>`/`xml:lang` was seen: the repo's declared
258
+ // language(s) (primary BCP-47 subtag, e.g. "fr-FR" → "fr"), sorted by descending
259
+ // frequency. Used as the CLI's `--lang auto` repo-detection signal (see
260
+ // `resolveLang` in src/cli.ts) — never invented when no document declares one.
261
+ langs?: string[];
262
+ // Set when a `scan --sample` run was merged in: the normative page sample the dynamic
263
+ // tier was run over (name/url/auth/notes per page, storageState paths dropped). Drives
264
+ // the report's « Constats par page » section. Optional/additive.
265
+ sample?: SampleScope;
266
+ // Set when dynamic scan results were merged in: which needs-rendering SCs the scan's
267
+ // engines/probes actually MEASURED (verdict coverage — independent of whether anything
268
+ // was found). Docker runner: 320px reflow only; the local runtime adds zoom / text
269
+ // spacing / focus / hover (+ live regions when interactions are on). Union across
270
+ // merges. Drives the partial-audit advisory (src/report.ts untestedNeedsRendering) so
271
+ // the banner never claims a probe ran when it did not. Optional/additive.
272
+ scan?: { testedScs: string[] };
273
+ };
274
+ guidelines: GuidelineTally[];
275
+ criteria: CriterionResult[];
276
+ findings: Finding[];
277
+ // Findings raised by DECLARATIVE PACK RULES (src/standards/pack-rules.ts), namespaced
278
+ // `pack:<key>:<id>`. Kept SEPARATE from the core `findings`/`criteria` so the WCAG core
279
+ // verdict is never touched by pack-only detection: they surface only when a pack
280
+ // projection is derived (`derivePackResults` routes them through the SAME
281
+ // appliesTo/ruleMatches machinery as engine findings). Optional/additive (no
282
+ // SCHEMA_VERSION bump) — absent/empty ⇒ no pack ran a rule.
283
+ packFindings?: Finding[];
284
+ residualRisks: ResidualRisk[];
285
+ // Automatic static-check pass rate over the machine-DECIDABLE SCs only (the small
286
+ // static set + any judgment SC that fired a definite NC). NOT a full conformance
287
+ // rate — most SCs are needs-rendering/judgment and stay manual (residual risk).
288
+ conformancePct: number;
289
+ // Set once `verify --apply <adjudication>` has folded an AI adjudication of the manual
290
+ // criteria back into the audit (src/adjudicate.ts). `stillManual` = criteria the agent
291
+ // left as an explicit residual (needs a rendered DOM → `scan`, or genuinely undecidable).
292
+ // Optional/additive — absent on a plain engine audit.
293
+ adjudicated?: { date: string; applied: number; stillManual: number };
294
+ }
295
+
296
+ // ---- optional dynamic tier (axe-core in a headless browser): Docker image, or a
297
+ // host/target Playwright resolved at runtime (`scan --local`). `engine` widens from
298
+ // the axe + 320px-reflow pair to the bespoke residual-criteria probes the local
299
+ // runtime adds (focus visibility, 200% zoom reflow, text spacing, content-on-hover)
300
+ // — the criteria axe alone cannot decide. (Target size 2.5.8 is left to axe's own rule.)
301
+ //
302
+ // The `input-overflow-*` and `live-region` engines are STATEFUL: they only ever
303
+ // populate when the local runtime runs with interactions ON (the default; `scan
304
+ // --no-interact` disables them). Each measures the page AFTER a real user action —
305
+ // long values typed into inputs, or a safe interaction (fill / toggle / click a
306
+ // type=button) — the class of non-conformity a pristine page never reveals. The
307
+ // three input-overflow engines share the "filled input clipped/unreadable" check but
308
+ // map to a different SC per stress (320px reflow 1.4.10, 200% zoom 1.4.4, text
309
+ // spacing 1.4.12); live-region maps to 4.1.3 Status Messages.
310
+ export type DynamicEngine =
311
+ | "axe"
312
+ | "reflow"
313
+ | "focus-visible"
314
+ | "reflow-zoom"
315
+ | "text-spacing"
316
+ | "hover"
317
+ | "input-overflow-reflow"
318
+ | "input-overflow-zoom"
319
+ | "input-overflow-spacing"
320
+ | "live-region";
321
+
322
+ export interface DynamicFinding {
323
+ criteriaId: string;
324
+ axeRule: string;
325
+ impact: string;
326
+ severity: Severity;
327
+ message: string;
328
+ selector: string;
329
+ snippet: string;
330
+ engine: DynamicEngine;
331
+ page?: string; // the scanned URL/page this finding came from (multi-page crawl)
332
+ // Best-practice-only dynamic finding (no `wcag*` tag on the axe violation): folds into
333
+ // the audit as an advisory recommendation, never a criterion NC. See Finding.advisory
334
+ // and src/axe-map.ts `axeAdvisory`. Optional/additive — absent ⇒ normative.
335
+ advisory?: boolean;
336
+ // Per-sample provenance (`scan --sample`): the originating sample page. Carried through
337
+ // `mergeDynamic` onto the merged Finding's `sample` for ticket rendering. Optional.
338
+ sample?: { id: string; name: string; auth?: boolean; notes?: string };
339
+ }
340
+
341
+ export interface DynamicResult {
342
+ tool: "ultra11y";
343
+ engine: string; // e.g. "axe-core@playwright (docker)"
344
+ target: string;
345
+ date: string;
346
+ findings: DynamicFinding[];
347
+ // Set by a `scan --sample` run: the normative page sample scanned (recorded onto the
348
+ // merged AuditResult's scope.sample). Optional/additive — absent for a plain scan.
349
+ sample?: SampleScope;
350
+ // Which needs-rendering SCs this run's engines/probes actually MEASURED (verdict
351
+ // coverage — independent of whether anything was found). Docker: 320px reflow only;
352
+ // local: + zoom / text spacing / focus / hover (+ live regions when interactions are
353
+ // on). Merged into AuditResult.scope.scan by mergeDynamic. Optional/additive.
354
+ testedScs?: string[];
355
+ }
package/src/util.ts ADDED
@@ -0,0 +1,32 @@
1
+ // Small shared utilities. Runtime-only Node APIs (Date, fs) are fine here — the
2
+ // "no Date" rule applies to workflow scripts, not the CLI bundle.
3
+ import { readFileSync } from "node:fs";
4
+ import { extname } from "node:path";
5
+
6
+ export function escapeRegExp(s: string): string {
7
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8
+ }
9
+
10
+ /** Today as YYYY-MM-DD (local-agnostic, ISO date). */
11
+ export function today(): string {
12
+ return new Date().toISOString().slice(0, 10);
13
+ }
14
+
15
+ export function readText(path: string): string {
16
+ return readFileSync(path, "utf8");
17
+ }
18
+
19
+ export function ext(path: string): string {
20
+ return extname(path).toLowerCase();
21
+ }
22
+
23
+ export function uniq<T>(items: T[]): T[] {
24
+ return [...new Set(items)];
25
+ }
26
+
27
+ /** Read all of stdin as text (for `audit -`). */
28
+ export async function readStdin(): Promise<string> {
29
+ const chunks: Buffer[] = [];
30
+ for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
31
+ return Buffer.concat(chunks).toString("utf8");
32
+ }