Product design skills

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 fill

pack 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: status manual with a dedicated out-of-scope justification (outOfScope: true from derivePackResults), 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:

  1. Read the source (its criteria list, and its concrete good/bad implementation rules). The fuzzy mapping rule → criterion → WCAG SC is yours to do — the model is far better at it than a regex.
  2. Draft three artifacts: the StandardPack JSON (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 in references/guidance.md).
  3. 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.
  4. 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:

FieldMeaning
tagintrinsic 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
scopepage (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.

On this page