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,
contact, mentions légales, déclaration d'accessibilité, plan du site, aide, authentification,
pages représentatives + éléments transverses).
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.