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
package/src/auditor.ts
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// The AUDITOR conformance block — the DEFAULT rendering of `prd` (backlog + per-criterion)
|
|
2
|
+
// and of the GitHub issues it files. Where `renderBacklog`/`issueBody` (prd.ts/gh.ts) speak
|
|
3
|
+
// to a developer (Fix / Effort / Occurrences), this speaks to an accessibility AUDITOR: per
|
|
4
|
+
// criterion it states the theme, the criterion + its official wording, the test(s), the WCAG
|
|
5
|
+
// mapping + level, the finding (non-conformity), the expected conformant state, and a
|
|
6
|
+
// verification method. Crucially it renders with the ACTIVE STANDARD's vocabulary
|
|
7
|
+
// (src/standards/vocabulary.ts): RGAA reads "Thématique / Critère / Test / C-NC-NA", the WCAG
|
|
8
|
+
// core reads "Principle · Guideline / Success criterion / Technique / Pass-Fail", and any
|
|
9
|
+
// future country pack reads its own — no term is hardcoded to one standard.
|
|
10
|
+
import type { AuditResult, Finding, Lang, Severity } from "./types.js";
|
|
11
|
+
import { prdUnits, partitionUnits, effortOf, guidanceFor, guidanceExampleBlock, acceptanceCriteria, type PrdUnit, type PrdFile } from "./prd.js";
|
|
12
|
+
import { getSC, guidelineTitle, principleTitle, techniques as scTechniques } from "./wcag.js";
|
|
13
|
+
import { resolveMessage, resolveRemediation, resolveNote } from "./messages.js";
|
|
14
|
+
import { type StandardId, isCore, loadPack, standardLabel, themeName, vocabularyFor } from "./standards/index.js";
|
|
15
|
+
|
|
16
|
+
const SEV_ORDER: Severity[] = ["bloquant", "majeur", "mineur"];
|
|
17
|
+
const ICON: Record<Severity, string> = { bloquant: "🔴", majeur: "🟠", mineur: "🟡" };
|
|
18
|
+
const SEV_LABEL: Record<Lang, Record<Severity, string>> = {
|
|
19
|
+
fr: { bloquant: "Bloquant", majeur: "Majeur", mineur: "Mineur" },
|
|
20
|
+
en: { bloquant: "Blocking", majeur: "Major", mineur: "Minor" },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Generic (standard-agnostic) auditor labels; the standard-specific NOUNS come from the
|
|
24
|
+
// resolved vocabulary. Kept separate so a pack overrides only its terminology, not the
|
|
25
|
+
// auditor frame.
|
|
26
|
+
const L = {
|
|
27
|
+
fr: {
|
|
28
|
+
lead: "Lecture auditeur",
|
|
29
|
+
tail: "Correspondance normative.",
|
|
30
|
+
finding: "Constat",
|
|
31
|
+
expected: "Attendu",
|
|
32
|
+
verification: "Vérification",
|
|
33
|
+
occ: "occurrence(s)",
|
|
34
|
+
verify: "contrôler chaque occurrence ci-dessous (inspecteur / lecteur d'écran), puis rejouer l'audit (`ultra11y` / axe).",
|
|
35
|
+
intro:
|
|
36
|
+
"Lecture auditeur : une entrée par critère non conforme (constat, attendu, méthode de vérification). Les critères « à évaluer » (rendu / jugement) sont adjugés par l'agent IA (`verify --manual`, de façon gatée), le rendu via `scan`.",
|
|
37
|
+
date: "Date",
|
|
38
|
+
scope: "Périmètre",
|
|
39
|
+
files: "fichier(s)",
|
|
40
|
+
none: "Aucune non-conformité relevée automatiquement par le moteur statique.",
|
|
41
|
+
captureOf: (comp: string, src: string) => `capture rendue de \`${comp}\` — source \`${src}\``,
|
|
42
|
+
recommendationsTitle: "Recommandations (non normatives)",
|
|
43
|
+
// Advisory (non-normative recommendation) vocabulary — deliberately NOT the NC wording.
|
|
44
|
+
advisoryTag: "Recommandation (non normative)",
|
|
45
|
+
advisoryNote: "Bonne pratique — aucun test normatif du référentiel actif ne l'exige. Ce n'est PAS une non-conformité.",
|
|
46
|
+
observation: "Observation",
|
|
47
|
+
suggestion: "Suggestion",
|
|
48
|
+
relatedRef: "Critère lié",
|
|
49
|
+
// Unified ticket template (Task 2).
|
|
50
|
+
priority: "Priorité",
|
|
51
|
+
technical: "Partie technique",
|
|
52
|
+
impactedFiles: "Fichiers impactés",
|
|
53
|
+
pages: "Pages / URLs impactées",
|
|
54
|
+
change: "Changement attendu",
|
|
55
|
+
ac: "Critères d'acceptation",
|
|
56
|
+
complexity: "Complexité",
|
|
57
|
+
pts: "pts",
|
|
58
|
+
reproduction: "Contexte de reproduction",
|
|
59
|
+
authRequired: "authentification requise",
|
|
60
|
+
yes: "oui",
|
|
61
|
+
no: "non",
|
|
62
|
+
unknown: "inconnu",
|
|
63
|
+
reproSteps: "état requis / étapes pour reproduire",
|
|
64
|
+
associatedRec: "Recommandations associées (non normatives)",
|
|
65
|
+
},
|
|
66
|
+
en: {
|
|
67
|
+
lead: "Auditor view",
|
|
68
|
+
tail: "Normative mapping.",
|
|
69
|
+
finding: "Finding",
|
|
70
|
+
expected: "Expected",
|
|
71
|
+
verification: "Verification",
|
|
72
|
+
occ: "occurrence(s)",
|
|
73
|
+
verify: "check each occurrence below (inspector / screen reader), then re-run the audit (`ultra11y` / axe).",
|
|
74
|
+
intro:
|
|
75
|
+
"Auditor view: one entry per non-conforming criterion (finding, expected state, verification method). The “to assess” criteria (rendering / judgment) are adjudicated by the AI agent (`verify --manual`, gated), rendering via `scan`.",
|
|
76
|
+
date: "Date",
|
|
77
|
+
scope: "Scope",
|
|
78
|
+
files: "file(s)",
|
|
79
|
+
none: "No non-conformity found automatically by the static engine.",
|
|
80
|
+
captureOf: (comp: string, src: string) => `rendered capture of \`${comp}\` — source \`${src}\``,
|
|
81
|
+
recommendationsTitle: "Recommendations (non-normative)",
|
|
82
|
+
// Advisory (non-normative recommendation) vocabulary — deliberately NOT the NC wording.
|
|
83
|
+
advisoryTag: "Recommendation (non-normative)",
|
|
84
|
+
advisoryNote: "Good practice — no normative test of the active standard requires it. This is NOT a non-conformity.",
|
|
85
|
+
observation: "Observation",
|
|
86
|
+
suggestion: "Suggestion",
|
|
87
|
+
relatedRef: "Related criterion",
|
|
88
|
+
// Unified ticket template (Task 2).
|
|
89
|
+
priority: "Priority",
|
|
90
|
+
technical: "Technical details",
|
|
91
|
+
impactedFiles: "Impacted files",
|
|
92
|
+
pages: "Impacted pages / URLs",
|
|
93
|
+
change: "Expected change",
|
|
94
|
+
ac: "Acceptance criteria",
|
|
95
|
+
complexity: "Complexity",
|
|
96
|
+
pts: "pts",
|
|
97
|
+
reproduction: "Reproduction context",
|
|
98
|
+
authRequired: "authentication required",
|
|
99
|
+
yes: "yes",
|
|
100
|
+
no: "no",
|
|
101
|
+
unknown: "unknown",
|
|
102
|
+
reproSteps: "required state / steps to reproduce",
|
|
103
|
+
associatedRec: "Related recommendations (non-normative)",
|
|
104
|
+
},
|
|
105
|
+
} as const;
|
|
106
|
+
|
|
107
|
+
const uniq = (xs: string[]): string[] => [...new Set(xs.filter(Boolean))];
|
|
108
|
+
|
|
109
|
+
export interface AuditorUnitOpts {
|
|
110
|
+
heading?: string; // "###"/"##" → emit a criterion heading; omit for a bare issue body
|
|
111
|
+
// Emit the technical ticket sections (Partie technique + Contexte de reproduction).
|
|
112
|
+
// Default true; `prd --no-technical` turns it off for a pure-auditor consumption.
|
|
113
|
+
technical?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// A URL-shaped finding location (a served page a `scan` crawled) vs a source file path.
|
|
117
|
+
const isUrlLocation = (file: string): boolean => /^https?:\/\//i.test(file);
|
|
118
|
+
|
|
119
|
+
// Per-sample provenance attached to a finding by `mergeDynamic` (`scan --sample`) — the
|
|
120
|
+
// crawled page name, whether it sits behind authentication, and reproduction notes. The
|
|
121
|
+
// technical + reproduction sections render it when present, and gracefully (URL only) when
|
|
122
|
+
// absent (a plain single-page/served-URL scan).
|
|
123
|
+
interface SampleMeta {
|
|
124
|
+
page?: string;
|
|
125
|
+
authRequired?: boolean;
|
|
126
|
+
notes?: string;
|
|
127
|
+
}
|
|
128
|
+
function sampleMetaOf(f: Finding): SampleMeta | undefined {
|
|
129
|
+
const meta = f.sample;
|
|
130
|
+
return meta && typeof meta === "object" ? meta : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Nest one level below the unit heading, CLAMPED to `####` so the level always stays
|
|
134
|
+
* within verify.ts's `HEADING_LINE` (/^#{2,4}\s/): a `#####` would NOT reset the current
|
|
135
|
+
* criterion, letting a technical-section line leak into the worklist. */
|
|
136
|
+
function subHeading(heading?: string): string {
|
|
137
|
+
return "#".repeat(Math.min((heading?.length ?? 2) + 1, 4));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ADVISORY_ICON = "💡";
|
|
141
|
+
|
|
142
|
+
/** The single renderer for a finding's occurrence line, shared by auditor.ts and prd.ts.
|
|
143
|
+
* `checkbox` renders the EXACT parseable shape `verify.ts`'s `AUDITOR_OCCURRENCE` regex
|
|
144
|
+
* (src/verify.ts:48) keys on; `advisory` renders the deliberately NON-parseable `💡` shape
|
|
145
|
+
* (an advisory/recommendation must never enter the verify worklist as a claimed NC). Byte
|
|
146
|
+
* shape is otherwise identical — only the leading marker differs. */
|
|
147
|
+
export function occurrenceLine(f: Finding, lang: Lang, opts: { marker: "checkbox" | "advisory" }): string {
|
|
148
|
+
const marker = opts.marker === "checkbox" ? "[ ]" : ADVISORY_ICON;
|
|
149
|
+
return `- ${marker} \`${f.file}:${f.line}\` (\`${f.selectorHint}\`) — ${resolveMessage(f, lang)}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The `↳` related-occurrence sub-bullet, shared by auditor.ts and prd.ts. `selector: false`
|
|
153
|
+
* omits the `` (`selectorHint`) `` segment (prd.ts's `renderPrdDoc` task list does this). */
|
|
154
|
+
export function relatedLine(related: NonNullable<Finding["related"]>, lang: Lang, opts: { selector: boolean }): string {
|
|
155
|
+
const sel = opts.selector ? ` (\`${related.selectorHint}\`)` : "";
|
|
156
|
+
return ` - ↳ ${resolveNote(related, lang)} : \`${related.file}:${related.line}\`${sel}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The auditor block for ONE criterion (a PrdUnit), localized and rendered with the active
|
|
160
|
+
* standard's vocabulary. Emits the full owner-validated ticket structure: the criterion
|
|
161
|
+
* block + Priorité, the parseable occurrence checklist, then (unless `opts.technical` is
|
|
162
|
+
* false) Partie technique + Contexte de reproduction. Returns lines (caller joins). */
|
|
163
|
+
export function renderAuditorUnit(unit: PrdUnit, standard: StandardId, lang: Lang, opts: AuditorUnitOpts = {}): string[] {
|
|
164
|
+
const s = L[lang];
|
|
165
|
+
if (unit.advisory) return renderAdvisoryUnit(unit, standard, lang, opts);
|
|
166
|
+
const v = vocabularyFor(standard, lang);
|
|
167
|
+
const technical = opts.technical ?? true;
|
|
168
|
+
const out: string[] = [];
|
|
169
|
+
if (opts.heading) out.push(`${opts.heading} ${ICON[unit.severity]} ${unit.label}`, "");
|
|
170
|
+
out.push(`> ${v.normativeNote ?? `${s.lead} — ${standardLabel(standard)}. ${s.tail}`}`, "");
|
|
171
|
+
|
|
172
|
+
// ---- 1. criterion block (unchanged) + the explicit Priorité line ----
|
|
173
|
+
if (isCore(standard)) {
|
|
174
|
+
const sc = getSC(unit.criteriaId);
|
|
175
|
+
if (sc) {
|
|
176
|
+
const pr = `${sc.principle} ${principleTitle(sc.principle, lang) ?? ""}`.trim();
|
|
177
|
+
const gl = `${sc.guideline} ${guidelineTitle(sc.guideline, lang) ?? ""}`.trim();
|
|
178
|
+
out.push(`**${v.theme}** : ${[pr, gl].filter(Boolean).join(" · ")}`);
|
|
179
|
+
}
|
|
180
|
+
out.push(`**${v.criterion}** : ${unit.criteriaId}${sc ? ` — ${unit.title}` : ""}`);
|
|
181
|
+
const techs = scTechniques(unit.criteriaId);
|
|
182
|
+
if (techs.length) out.push(`**${v.test}** : ${techs.join(", ")}`);
|
|
183
|
+
out.push(`**WCAG** : ${unit.criteriaId}${sc ? ` (${sc.level})` : ""}`);
|
|
184
|
+
} else {
|
|
185
|
+
const pack = loadPack(standard);
|
|
186
|
+
const pc = pack.criteria.find((c) => c.id === unit.criteriaId);
|
|
187
|
+
if (pc) out.push(`**${v.theme}** : ${pc.theme}. ${themeName(pack, pc.theme, lang) ?? ""}`.trimEnd());
|
|
188
|
+
out.push(`**${v.criterion}** : ${unit.criteriaId} — ${unit.title}`);
|
|
189
|
+
const testNums = pc?.tests ? Object.keys(pc.tests).map((k) => `${unit.criteriaId}.${k}`) : [];
|
|
190
|
+
if (testNums.length) out.push(`**${v.test}(s)** : ${testNums.join(" · ")}`);
|
|
191
|
+
if (unit.refs.length) out.push(`**WCAG** : ${unit.refs.map((sc) => `${sc}${scLevel(sc)}`).join(" · ")}`);
|
|
192
|
+
}
|
|
193
|
+
out.push(`**${s.priority}** : ${ICON[unit.severity]} ${SEV_LABEL[lang][unit.severity]}`);
|
|
194
|
+
|
|
195
|
+
// ---- 2. finding / expected / verification + the PARSEABLE occurrence checklist ----
|
|
196
|
+
// Advisory findings riding along in a MIXED unit are excluded from the checklist (they are
|
|
197
|
+
// recommendations, never non-conformities) and rendered below in a distinct, NON-parseable
|
|
198
|
+
// sub-list — so verify.ts's worklist never captures a recommendation as an NC claim. Its
|
|
199
|
+
// POSITION (before any newly-introduced heading) is load-bearing for the parser.
|
|
200
|
+
const normative = unit.findings.filter((f) => !f.advisory);
|
|
201
|
+
const advisories = unit.findings.filter((f) => f.advisory);
|
|
202
|
+
const ncView: PrdUnit = { ...unit, findings: normative };
|
|
203
|
+
const messages = uniq(normative.map((f) => resolveMessage(f, lang)));
|
|
204
|
+
const fixes = uniq(normative.map((f) => resolveRemediation(f, lang)));
|
|
205
|
+
out.push("");
|
|
206
|
+
out.push(`**${s.finding} (${v.nonConformant})** : ${normative.length} ${s.occ} — ${messages.join(" ; ")}`);
|
|
207
|
+
if (fixes.length) out.push(`**${s.expected} (${v.conformant})** : ${fixes.join(" ; ")}`);
|
|
208
|
+
out.push(`**${s.verification}** : ${s.verify}`, "");
|
|
209
|
+
for (const f of normative) {
|
|
210
|
+
out.push(occurrenceLine(f, lang, { marker: "checkbox" }));
|
|
211
|
+
// A finding projected via an opt-in secondary crosswalk mapping carries a note
|
|
212
|
+
// explaining the deviation from the SC-faithful projection — appended as an inert
|
|
213
|
+
// (worklist-ignored) sub-bullet, mirroring the related-occurrence line shape.
|
|
214
|
+
if (f.secondary?.note) out.push(` - ↳ ${f.secondary.note}`);
|
|
215
|
+
if (f.related) out.push(relatedLine(f.related, lang, { selector: true }));
|
|
216
|
+
if (f.origin) {
|
|
217
|
+
const comp = f.origin.component ?? f.origin.sourceFile ?? f.file;
|
|
218
|
+
const srcFile = f.origin.sourceFile ?? f.origin.capture;
|
|
219
|
+
const src = f.origin.sourceFile && f.origin.sourceLine !== undefined ? `${f.origin.sourceFile}:${f.origin.sourceLine}` : srcFile;
|
|
220
|
+
out.push(` - _${s.captureOf(comp, src)}_`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
out.push("");
|
|
224
|
+
|
|
225
|
+
// Associated recommendations (mixed unit only): a visually distinct, NON-parseable list —
|
|
226
|
+
// the `- 💡` bullet deliberately avoids the `- [ ] \`file:line\`` checklist grammar.
|
|
227
|
+
if (advisories.length) {
|
|
228
|
+
out.push(`_${s.associatedRec}_`, "");
|
|
229
|
+
for (const f of advisories) out.push(occurrenceLine(f, lang, { marker: "advisory" }));
|
|
230
|
+
out.push("");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---- 3 + 4. technical ticket sections (opt-out via prd --no-technical) ----
|
|
234
|
+
if (technical) {
|
|
235
|
+
out.push(...renderTechnicalSection(ncView, unit, standard, lang, opts));
|
|
236
|
+
out.push(...renderReproductionContext(normative, lang));
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Section 3 — Partie technique: impacted files, impacted pages/URLs (with Task-5 sample
|
|
242
|
+
* provenance when present), the expected change + guidance example, Given/When/Then
|
|
243
|
+
* acceptance criteria, and the deterministic complexity. All lines here are worklist-inert:
|
|
244
|
+
* they sit AFTER the section's heading, which resets verify.ts's current criterion. */
|
|
245
|
+
function renderTechnicalSection(ncView: PrdUnit, unit: PrdUnit, standard: StandardId, lang: Lang, opts: AuditorUnitOpts): string[] {
|
|
246
|
+
const s = L[lang];
|
|
247
|
+
const out: string[] = [`${subHeading(opts.heading)} ${s.technical}`, ""];
|
|
248
|
+
|
|
249
|
+
// Impacted files — unique source paths (URL-only locations are listed under Pages/URLs).
|
|
250
|
+
const files = uniq(ncView.findings.filter((f) => !isUrlLocation(f.file)).map((f) => f.file));
|
|
251
|
+
if (files.length) {
|
|
252
|
+
out.push(`**${s.impactedFiles}**`, "");
|
|
253
|
+
for (const f of files) out.push(`- \`${f}\``);
|
|
254
|
+
out.push("");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Impacted pages / URLs — served locations, deduped, with the sample page + auth flag
|
|
258
|
+
// once Task 5 attaches that provenance (rendered gracefully as URL-only until then).
|
|
259
|
+
const seenUrl = new Set<string>();
|
|
260
|
+
const pageLines: string[] = [];
|
|
261
|
+
for (const f of ncView.findings) {
|
|
262
|
+
if (!isUrlLocation(f.file) || seenUrl.has(f.file)) continue;
|
|
263
|
+
seenUrl.add(f.file);
|
|
264
|
+
const meta = sampleMetaOf(f);
|
|
265
|
+
const suffix = meta ? ` — ${meta.page ? `${meta.page} · ` : ""}${s.authRequired} : ${meta.authRequired ? s.yes : s.no}` : "";
|
|
266
|
+
pageLines.push(`- \`${f.file}\`${suffix}`);
|
|
267
|
+
}
|
|
268
|
+
if (pageLines.length) out.push(`**${s.pages}**`, "", ...pageLines, "");
|
|
269
|
+
|
|
270
|
+
// Expected change — deduped remediation texts + the shared before/after guidance example.
|
|
271
|
+
out.push(`**${s.change}**`, "");
|
|
272
|
+
for (const fx of uniq(ncView.findings.map((f) => resolveRemediation(f, lang)))) out.push(`- ${fx}`);
|
|
273
|
+
out.push("");
|
|
274
|
+
out.push(...guidanceExampleBlock(guidanceFor(unit, standard), lang));
|
|
275
|
+
|
|
276
|
+
// Acceptance criteria — the shared Given/When/Then generator, as a checkbox list.
|
|
277
|
+
out.push(`**${s.ac}**`, "");
|
|
278
|
+
out.push(...acceptanceCriteria(ncView, standard, lang, { checkbox: true }));
|
|
279
|
+
out.push("");
|
|
280
|
+
|
|
281
|
+
// Complexity — the shared deterministic effort heuristic (t-shirt size + points).
|
|
282
|
+
const { bucket, points } = effortOf(ncView);
|
|
283
|
+
out.push(`**${s.complexity}** : ${bucket} (${points} ${s.pts})`, "");
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Section 4 — Contexte de reproduction: emitted only when ≥1 occurrence cites a served URL
|
|
288
|
+
* that static grounding could not resolve (the `mergeDynamic` unresolved-anchor case: a URL
|
|
289
|
+
* location at line 0) or a sample page behind authentication (Task-5 provenance). Gives the
|
|
290
|
+
* URL, the auth requirement, and a placeholder for the required state / reproduction steps. */
|
|
291
|
+
function renderReproductionContext(normative: Finding[], lang: Lang): string[] {
|
|
292
|
+
const s = L[lang];
|
|
293
|
+
const seen = new Set<string>();
|
|
294
|
+
const qualifying: Finding[] = [];
|
|
295
|
+
for (const f of normative) {
|
|
296
|
+
const unresolvedUrl = isUrlLocation(f.file) && f.line === 0;
|
|
297
|
+
const authGated = sampleMetaOf(f)?.authRequired === true;
|
|
298
|
+
if ((!unresolvedUrl && !authGated) || seen.has(f.file)) continue;
|
|
299
|
+
seen.add(f.file);
|
|
300
|
+
qualifying.push(f);
|
|
301
|
+
}
|
|
302
|
+
if (!qualifying.length) return [];
|
|
303
|
+
const out: string[] = [`**${s.reproduction}**`, ""];
|
|
304
|
+
for (const f of qualifying) {
|
|
305
|
+
const meta = sampleMetaOf(f);
|
|
306
|
+
const auth = meta ? (meta.authRequired ? s.yes : s.no) : s.unknown;
|
|
307
|
+
const name = meta?.page ? `${meta.page} · ` : "";
|
|
308
|
+
out.push(`- **URL** : \`${f.file}\` — ${name}${s.authRequired} : ${auth}`);
|
|
309
|
+
// Task 5: the sample page's notes ARE the required state / reproduction steps.
|
|
310
|
+
if (meta?.notes) out.push(` - ↳ ${meta.notes}`);
|
|
311
|
+
}
|
|
312
|
+
out.push(`- _${s.reproSteps}_`, "");
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The auditor block for ONE advisory (non-normative recommendation) unit. Rendered with
|
|
317
|
+
* the « Recommandation (non normative) » vocabulary, NEVER the non-conformity wording.
|
|
318
|
+
* Crucially, the criterion reference deliberately AVOIDS the "**label** : <id>" colon
|
|
319
|
+
* grammar the verify worklist parser keys on (src/verify.ts `auditorCriterionLine`), so
|
|
320
|
+
* an advisory block can never enter the non-conformity worklist — the related criterion
|
|
321
|
+
* is cited with an em-dash + middot instead of a colon. */
|
|
322
|
+
function renderAdvisoryUnit(unit: PrdUnit, standard: StandardId, lang: Lang, opts: AuditorUnitOpts): string[] {
|
|
323
|
+
const s = L[lang];
|
|
324
|
+
const out: string[] = [];
|
|
325
|
+
if (opts.heading) out.push(`${opts.heading} ${ADVISORY_ICON} ${unit.label}`, "");
|
|
326
|
+
out.push(`> ${s.advisoryTag} — ${s.advisoryNote}`, "");
|
|
327
|
+
out.push(`**${s.advisoryTag}** — ${unit.criteriaId} · ${unit.title}`);
|
|
328
|
+
if (!isCore(standard) && unit.refs.length) out.push(`_${s.relatedRef}: WCAG ${unit.refs.join(", ")}_`);
|
|
329
|
+
const messages = uniq(unit.findings.map((f) => resolveMessage(f, lang)));
|
|
330
|
+
const fixes = uniq(unit.findings.map((f) => resolveRemediation(f, lang)));
|
|
331
|
+
out.push("");
|
|
332
|
+
out.push(`**${s.observation}** : ${unit.findings.length} ${s.occ} — ${messages.join(" ; ")}`);
|
|
333
|
+
if (fixes.length) out.push(`**${s.suggestion}** : ${fixes.join(" ; ")}`);
|
|
334
|
+
out.push("");
|
|
335
|
+
for (const f of unit.findings) {
|
|
336
|
+
out.push(occurrenceLine(f, lang, { marker: "checkbox" }));
|
|
337
|
+
if (f.related) out.push(relatedLine(f.related, lang, { selector: true }));
|
|
338
|
+
}
|
|
339
|
+
out.push("");
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function scLevel(sc: string): string {
|
|
344
|
+
const c = getSC(sc);
|
|
345
|
+
return c ? ` (${c.level})` : "";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function auditorHeader(r: AuditResult, lang: Lang, standard: StandardId): string[] {
|
|
349
|
+
const s = L[lang];
|
|
350
|
+
const v = vocabularyFor(standard, lang);
|
|
351
|
+
return [
|
|
352
|
+
`# ${v.auditorHeading} — ${standardLabel(standard)}`,
|
|
353
|
+
"",
|
|
354
|
+
`- **${s.date}** : ${r.date}`,
|
|
355
|
+
`- **${s.scope}** : ${r.scope.files} ${s.files} — ${r.scope.inputs.join(", ")}`,
|
|
356
|
+
"",
|
|
357
|
+
`> ${s.intro}`,
|
|
358
|
+
"",
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface AuditorBacklogOpts {
|
|
363
|
+
// Emit the technical ticket sections per unit. Default true; `prd --no-technical` off.
|
|
364
|
+
technical?: boolean;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** A single auditor backlog, sectioned by priority (bloquant → majeur → mineur), with
|
|
368
|
+
* advisory recommendations kept in their own trailing section (never among NC). Default `prd`. */
|
|
369
|
+
export function renderAuditorBacklog(r: AuditResult, lang: Lang = "en", standard: StandardId = "wcag", opts: AuditorBacklogOpts = {}): string {
|
|
370
|
+
const s = L[lang];
|
|
371
|
+
const technical = opts.technical ?? true;
|
|
372
|
+
const { nc, advisory } = partitionUnits(prdUnits(r, standard, lang));
|
|
373
|
+
const out = auditorHeader(r, lang, standard);
|
|
374
|
+
if (!nc.length && !advisory.length) {
|
|
375
|
+
out.push(s.none, "");
|
|
376
|
+
return out.join("\n");
|
|
377
|
+
}
|
|
378
|
+
for (const sev of SEV_ORDER) {
|
|
379
|
+
const group = nc.filter((u) => u.severity === sev);
|
|
380
|
+
if (!group.length) continue;
|
|
381
|
+
out.push(`## ${ICON[sev]} ${SEV_LABEL[lang][sev]} (${group.length})`, "");
|
|
382
|
+
for (const u of group) out.push(...renderAuditorUnit(u, standard, lang, { heading: "###", technical }));
|
|
383
|
+
}
|
|
384
|
+
if (advisory.length) {
|
|
385
|
+
out.push(`## ${ADVISORY_ICON} ${s.recommendationsTitle} (${advisory.length})`, "");
|
|
386
|
+
for (const u of advisory) out.push(...renderAuditorUnit(u, standard, lang, { heading: "###", technical }));
|
|
387
|
+
}
|
|
388
|
+
return out.join("\n");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** One standalone auditor document per criterion (`prd --split criterion`). */
|
|
392
|
+
export function renderAuditorPerCriterion(r: AuditResult, lang: Lang = "en", standard: StandardId = "wcag", opts: AuditorBacklogOpts = {}): PrdFile[] {
|
|
393
|
+
const technical = opts.technical ?? true;
|
|
394
|
+
return prdUnits(r, standard, lang).map((u) => {
|
|
395
|
+
const out = auditorHeader(r, lang, standard);
|
|
396
|
+
out.push(...renderAuditorUnit(u, standard, lang, { heading: "##", technical }));
|
|
397
|
+
return { name: `prd-${u.criteriaId}-${r.date}.md`, content: out.join("\n") };
|
|
398
|
+
});
|
|
399
|
+
}
|
package/src/autofill.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// The HTML autofill vocabulary — the basis for WCAG 1.3.5 "Identify Input Purpose".
|
|
2
|
+
//
|
|
3
|
+
// 1.3.5 asks that a field collecting information ABOUT THE USER declare its purpose with a
|
|
4
|
+
// token from the WCAG "Input Purposes for User Interface Components" list, which is a
|
|
5
|
+
// subset of HTML's autofill field names. A misspelled or invented token is worse than no
|
|
6
|
+
// token at all: it looks compliant, silently autofills nothing and helps nobody.
|
|
7
|
+
//
|
|
8
|
+
// Source: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill
|
|
9
|
+
// https://www.w3.org/TR/WCAG22/#input-purposes
|
|
10
|
+
|
|
11
|
+
/** Autofill field names that identify information about the user (WCAG §7). */
|
|
12
|
+
export const AUTOFILL_FIELD_NAMES = new Set([
|
|
13
|
+
"name",
|
|
14
|
+
"honorific-prefix",
|
|
15
|
+
"given-name",
|
|
16
|
+
"additional-name",
|
|
17
|
+
"family-name",
|
|
18
|
+
"honorific-suffix",
|
|
19
|
+
"nickname",
|
|
20
|
+
"organization-title",
|
|
21
|
+
"username",
|
|
22
|
+
"new-password",
|
|
23
|
+
"current-password",
|
|
24
|
+
"one-time-code",
|
|
25
|
+
"organization",
|
|
26
|
+
"street-address",
|
|
27
|
+
"address-line1",
|
|
28
|
+
"address-line2",
|
|
29
|
+
"address-line3",
|
|
30
|
+
"address-level4",
|
|
31
|
+
"address-level3",
|
|
32
|
+
"address-level2",
|
|
33
|
+
"address-level1",
|
|
34
|
+
"country",
|
|
35
|
+
"country-name",
|
|
36
|
+
"postal-code",
|
|
37
|
+
"cc-name",
|
|
38
|
+
"cc-given-name",
|
|
39
|
+
"cc-additional-name",
|
|
40
|
+
"cc-family-name",
|
|
41
|
+
"cc-number",
|
|
42
|
+
"cc-exp",
|
|
43
|
+
"cc-exp-month",
|
|
44
|
+
"cc-exp-year",
|
|
45
|
+
"cc-csc",
|
|
46
|
+
"cc-type",
|
|
47
|
+
"transaction-currency",
|
|
48
|
+
"transaction-amount",
|
|
49
|
+
"language",
|
|
50
|
+
"bday",
|
|
51
|
+
"bday-day",
|
|
52
|
+
"bday-month",
|
|
53
|
+
"bday-year",
|
|
54
|
+
"sex",
|
|
55
|
+
"url",
|
|
56
|
+
"photo",
|
|
57
|
+
"tel",
|
|
58
|
+
"tel-country-code",
|
|
59
|
+
"tel-national",
|
|
60
|
+
"tel-area-code",
|
|
61
|
+
"tel-local",
|
|
62
|
+
"tel-local-prefix",
|
|
63
|
+
"tel-local-suffix",
|
|
64
|
+
"tel-extension",
|
|
65
|
+
"email",
|
|
66
|
+
"impp",
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
// Contact-type qualifiers, legal only in front of tel/email/impp-family field names.
|
|
70
|
+
const CONTACT_TOKENS = new Set(["home", "work", "mobile", "fax", "pager"]);
|
|
71
|
+
// Address-purpose qualifiers, legal in front of any field name.
|
|
72
|
+
const PURPOSE_TOKENS = new Set(["shipping", "billing"]);
|
|
73
|
+
// Field names that accept a contact qualifier.
|
|
74
|
+
const CONTACTABLE = /^(tel|email|impp)(-|$)/;
|
|
75
|
+
|
|
76
|
+
/** Is `value` a well-formed autocomplete attribute? Follows the HTML grammar:
|
|
77
|
+
* `[section-*] [shipping|billing] [home|work|mobile|fax|pager] <field-name> [webauthn]`,
|
|
78
|
+
* plus the standalone `on` / `off` switches. */
|
|
79
|
+
export function isValidAutocomplete(value: string): boolean {
|
|
80
|
+
const tokens = value.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
81
|
+
if (!tokens.length) return false;
|
|
82
|
+
if (tokens.length === 1 && (tokens[0] === "on" || tokens[0] === "off")) return true;
|
|
83
|
+
|
|
84
|
+
let i = 0;
|
|
85
|
+
if (tokens[i]?.startsWith("section-") && tokens[i]!.length > "section-".length) i++;
|
|
86
|
+
if (tokens[i] && PURPOSE_TOKENS.has(tokens[i]!)) i++;
|
|
87
|
+
const contactAt = i;
|
|
88
|
+
if (tokens[i] && CONTACT_TOKENS.has(tokens[i]!)) i++;
|
|
89
|
+
|
|
90
|
+
const field = tokens[i];
|
|
91
|
+
if (!field || !AUTOFILL_FIELD_NAMES.has(field)) return false;
|
|
92
|
+
// A contact qualifier only makes sense on the tel/email/impp family.
|
|
93
|
+
if (contactAt !== i && !CONTACTABLE.test(field)) return false;
|
|
94
|
+
i++;
|
|
95
|
+
|
|
96
|
+
if (tokens[i] === "webauthn") i++;
|
|
97
|
+
return i === tokens.length;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Credential fields whose autofill must NOT be suppressed: blocking the password manager
|
|
101
|
+
* is what WCAG 2.2's 3.3.8 Accessible Authentication is about. */
|
|
102
|
+
export const CREDENTIAL_FIELDS = new Set(["username", "current-password", "new-password", "one-time-code", "email"]);
|