references/packs.md
A supporting file of the ultra11y skill.
Pluggable standards packs — runtime loading + AI ingestion, gated
WCAG 2.2 AA is the engine's canonical key. A standards pack is a localized criterion set (RGAA, Section 508, EN 301 549…) that maps each of its criteria onto WCAG success criteria. RGAA ships built-in; you can add more without rebuilding the engine.
Load an external pack at runtime
A pack is plain JSON conforming to StandardPack (see references/standards.md and
CONTRIBUTING.md). Point the CLI at one — no install, no rebuild:
node scripts/ultra11y.mjs audit "src/**/*.tsx" --graph --pack ./packs/section508.json
node scripts/ultra11y.mjs report --in audit.json --standard section508 --pack ./packs/section508.json--pack accepts a JSON file, a directory holding pack.json (+ optional glossary.json
/ guidance.json), or a comma-separated list. Or declare it once in a .ultra11yrc.json
at the project root:
A pack loaded this way is a first-class standard everywhere, not just on the CLI: the
MCP server resolves each project's packs from its own .ultra11yrc.json, so
ultra11y_criteria { standard: "<yourpack>" }, ultra11y_method, ultra11y_glossary and
the std://<yourpack>/… resources all work the moment it validates. Two projects may define
different packs under the same key without either shadowing the other, and a project's
secondaryMappings are applied to a copy of a built-in pack, never to the shared one. See
references/mcp.md.
{ "packs": ["./packs/section508.json"], "guidance": ["./packs/section508.guidance.json"], "standard": "section508" }Every pack is validated before it is registered. An invalid pack is a hard error
(never a silent skip): the runtime loader, the pack check command, and the AI-ingestion
gate all share the one validator, so the rules below hold everywhere. A runtime pack whose
key collides with a built-in or already-loaded standard (e.g. rgaa) is rejected —
pass --override to deliberately replace it.
pack check — the anti-hallucination gate
node scripts/ultra11y.mjs pack check ./packs/rgaa.json
node scripts/ultra11y.mjs pack check ./packs/rgaa.json --guidance ./packs/rgaa.guidance.json
node scripts/ultra11y.mjs pack scaffold > ./packs/mypack.json # a blank, valid skeleton to fillpack check fails (exit 1) on any error: a missing required field, a key that collides
with the reserved core wcag, an idPattern that won't compile, a theme count that
disagrees with its criteria, a criterion whose wcag SC is not a recognized WCAG
success criterion, and — with --guidance — a guidance entry whose criterionId
doesn't resolve to a real pack criterion, or whose code example won't parse. This is what
makes an AI-authored pack trustworthy: the model proposes, the deterministic gate refuses
fabrication.
SC classification runs against the full WCAG 2.x universe — every real 2.x success criterion at every level (A/AA/AAA) plus the removed ones, vendored from the same W3C source as the AA core (never invented, no id ever special-cased):
- core (in the WCAG 2.2 AA core) — the normal case; the engine can audit it.
- out-of-core (a real SC outside the AA core, e.g. an AAA criterion) or removed
(the obsolete
4.1.1 Parsing) — accepted with a warning. At report/derive time a pack criterion whose mapping is ENTIRELY out-of-core/removed is classed out of engine scope: statusmanualwith a dedicated out-of-scope justification (outOfScope: truefromderivePackResults), never a silent NA and never a fabricated verdict. RGAA 8.1 (doctype → the removed 4.1.1) is the shipped example. - unknown (a well-formed id that never existed —
9.9.9) — rejected (error).
Pack locales are free-form. LocaleString keys are BCP-47-ish tags (fr, en,
pt-BR, nl-BE…) validated by shape only — a pack may be authored in any language,
independent of the CLI's own --lang fr|en UI frame. The pack's defaultLocale also
feeds the CLI's --lang auto fallback chain (used when it is fr/en and neither an
explicit --lang nor a repo <html lang> signal decided the output language).
AI-assisted ingestion of any rule source (the primary path)
To turn an external rule source — the RGAA SocialGouv/etalab rule packs, or any other national standard — into something ultra11y can use, drive this loop:
- Read the source (its criteria list, and its concrete good/bad implementation
rules). The fuzzy mapping
rule → criterion → WCAG SCis yours to do — the model is far better at it than a regex. - Draft three artifacts: the
StandardPackJSON (if the standard isn't already built-in), a guidance dataset (references/guidance.md), and a list of proposed new detectors (only the machine-checkable ones — see the honesty rule inreferences/guidance.md). - Gate: run
pack check <pack.json> --guidance <guidance.json>. Fix every error. A bogus criterion id misses; a bogus SC is rejected; an unparseable example fails. - Iterate until green, then load it with
--pack/.ultra11yrc.json.
An optional deterministic scaffolder, scripts/import-pack.mjs, pre-fills a draft
guidance dataset from a SocialGouv-style rules directory (frontmatter + before/after code
blocks + reference link), leaving any unresolved criterionId as null for you to
complete; it never finalizes — pack check is always the gate. See the script header for
usage.
Pack-only detection: declarative rules + normativity overrides
A pack can go beyond re-keying WCAG findings: it can ship its own detection and
re-normativize a finding within its own projection — without forking the engine. Both
mechanisms are pure data (no JavaScript plugins — a --pack-loaded pack stays fully
validatable), and both are enforced by the same validatePack / pack check gate.
rules — a bounded declarative matcher
Each rule matches source elements and emits a namespaced pack:<key>:<id> finding that
projects onto the pack criterion it reports under (via the same appliesTo/ruleMatches
machinery as engine findings). Rules run after the core engine rules and never
affect the WCAG core verdict — they surface only in the pack's own report.
{
"rules": [{
"id": "download-link-format",
"criterion": "6.1",
"wcag": ["2.4.4"],
"severity": "mineur",
"advisory": true,
"match": {
"tag": "a",
"attrs": [{ "name": "href", "op": "matches", "value": "\\.(pdf|docx?|zip)(\\?|#|$)" }],
"text": { "op": "lacks", "value": "(pdf|docx?|zip|\\d+\\s*(ko|mo|go))" }
},
"message": { "en": "…", "fr": "…" },
"remediation": { "en": "…", "fr": "…" }
}]
}The shipped RGAA pack uses exactly this rule (built by scripts/build-pack-rgaa.mjs, never
hand-edited): an advisory flag on download links whose visible text omits the file
format/weight — the DSFR auditor recommendation under criterion 6.1. Advisory ⇒ it renders
as a recommendation and never makes 6.1 non-conformant. Its message/remediation pair
is localized: the finding carries both en and fr, so a --lang fr RGAA report renders
the French recommendation text (not the English bake).
The matcher vocabulary is deliberately CAPPED at what RGAA needs today:
| Field | Meaning |
|---|---|
tag | intrinsic tag (case-insensitive) |
attrs[] | { name, op, value? } — op ∈ present | absent | equals | matches (regex, ReDoS-guarded, case-insensitive) |
text | { op, value } — op ∈ matches | lacks over the element's visible text (regex, case-insensitive) |
has[] / lacks[] | descendant conditions (each a match node); nesting ≤ 3 levels |
scope | page (full document only) | fragment (default) |
Anything more expressive — sibling/positional combinators, arbitrary CSS selectors,
cross-file resolution — is out of scope on purpose: it belongs in a core WCAG-keyed
engine rule instead, where every standard benefits. pack check rejects a rule whose
criterion doesn't exist, whose SC isn't a real WCAG criterion, whose regex is unsafe/won't
compile, whose has/lacks nest too deep, or whose message/remediation is missing en
or fr. It also rejects a match node with no condition ({} — it would fire on every
element) and a match node carrying an unknown key (a typo like { tgo: "a" } is a hard
error, never silently ignored): every node must carry ≥1 of tag/attrs/text/has/lacks.
The ReDoS guard on every regex rejects the classic single-quantified-atom shape
((a+)+), an overlapping alternation under a quantifier ((a|a)*), and a nested quantifier
over a group ((ab+)+) — while still accepting an anchored criterion grammar like (\.\d+)*
and a non-quantified extension alternation like (pdf|docx?|zip).
overrides — re-normativize within a projection
{ "overrides": { "pack:rgaa:some-rule": { "advisory": false, "severity": "majeur" } } }overrides is a map keyed by a finding's ruleId (a core engine rule, or a pack rule).
Applied only in derivePackResults, it flips a finding's normativity
(advisory↔normative) and/or re-grades its severity inside that pack's projection — the
core WCAG result is never mutated. This is the precise "WCAG can differ from RGAA"
mechanism: a standard can treat as normative something WCAG leaves advisory (or vice
versa) without changing the canonical engine verdict.
sampleMethodology — the standard's required page kinds
A country standard defines its audit over a normative page sample (see
references/audit.md). A pack declares the page KINDS a real audit of it must cover; the
mechanics (a project's actual sample.pages) live in .ultra11yrc.json, never in the pack.
{ "sampleMethodology": {
"requiredKinds": [
{ "id": "accueil", "label": { "fr": "Page d'accueil" }, "keywords": ["accueil", "home"] },
{ "id": "mentions-legales", "label": { "fr": "Mentions légales" }, "keywords": ["mentions", "legal"] },
{ "id": "declaration-accessibilite", "label": { "fr": "Déclaration d'accessibilité" }, "keywords": ["declaration", "accessibilite"] }
] } }sample check fuzzy-matches (accent-insensitive) each required kind's keywords against the
configured pages' name/notes/url and reports the missing kinds. It is purely advisory
(the sample is opt-in, the missing kinds are guidance not a gate), so a malformed
sampleMethodology is a validator warning and the field is ignored — never a hard
failure that blocks the pack from deriving reports. RGAA ships the reference list (accueil
(home), contact, mentions légales (legal notice), déclaration d'accessibilité (accessibility
statement), plan du site (sitemap), aide (help), authentification,
pages représentatives (representative pages) + éléments transverses (cross-cutting elements)).
Gate-compatibility note (id grammar)
check and verify recognize a pack's criterion ids in a rendered report by building
their citation regex FROM the pack's own idPattern (already validated compilable by
pack check) — not a single fixed shape. RGAA's 2-segment <n>.<n> (8.3) and a
hypothetical Section 508 E<n>.<n> (E205.4) are both fully gate-compatible out of the
box; any idPattern a pack declares works, with zero engine changes.