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? }oppresent | absent | equals | matches (regex, ReDoS-guarded, case-insensitive)
text{ op, value }opmatches | 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, 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.

On this page