Skip to content

feat(atomic-a11y): add merge-shards for parallel a11y report aggregation#7137

Open
y-lakhdar wants to merge 11 commits intofeat/a11y-reporterfrom
feat/a11y-merge-shards
Open

feat(atomic-a11y): add merge-shards for parallel a11y report aggregation#7137
y-lakhdar wants to merge 11 commits intofeat/a11y-reporterfrom
feat/a11y-merge-shards

Conversation

@y-lakhdar
Copy link
Contributor

@y-lakhdar y-lakhdar commented Feb 17, 2026

TL;DR

Merge shard JSON reports from parallel CI runs into a single consolidated a11y report.

Context

When Storybook tests run with --shard=N/M, each shard writes its own a11y-report.shard-N.json. This PR adds the merge step that combines them.

CI (parallel shards)
  ├─ shard 1/3 → a11y-report.shard-1.json   ─┐
  ├─ shard 2/3 → a11y-report.shard-2.json   ─┤──→  mergeA11yShardReports()  →  a11y-report.json
  └─ shard 3/3 → a11y-report.shard-3.json   ─┘
                                                     • Deduplicate components across shards
                                                     • Sum violation/pass/incomplete counts
                                                     • Union criteria coverage + affected components
                                                     • Recompute summary from merged data

How the shard merge works

When Storybook tests run with --shard, each shard writes its own a11y-report.shard-N.json. The merge step recombines them into a single a11y-report.json.

mergeA11yShardReports() — entry point

  1. Scan the output directory for files matching a11y-report.shard-{N}.json
  2. Read & validate each shard file in parallel (Promise.allSettled — invalid shards are skipped with a warning)
  3. Call mergeComponents()mergeCriteria()createSummary()
  4. Take report metadata (product, versions, etc.) from the first shard, stamp a fresh reportDate
  5. Write the merged a11y-report.json

mergeComponents() — deduplicate by component name

  • First shard wins as the base record for each component
  • Subsequent shards sum counts (storyCount, violations, passes, incomplete, inapplicable)
  • Union criteriaCovered sets and incompleteDetails arrays
  • Output sorted alphabetically by name

example
Shard 1 tested atomic-search-box with commerce stories, shard 2 tested it with search stories:

Shard 1:                                  Shard 2:
├─ name: "atomic-search-box"              ├─ name: "atomic-search-box"
├─ storyCount: 3                          ├─ storyCount: 2
└─ automated:                             └─ automated:
   ├─ violations: 1                          ├─ violations: 0
   ├─ passes: 12                             ├─ passes: 8
   ├─ incomplete: 0                          ├─ incomplete: 1
   ├─ inapplicable: 4                        ├─ inapplicable: 3
   ├─ criteriaCovered: ["1.4.3"]             ├─ criteriaCovered: ["1.4.3", "2.1.1"]
   └─ incompleteDetails: []                  └─ incompleteDetails: [{ruleId: "color-contrast", ...}]

Merged:
├─ name: "atomic-search-box"
├─ category: "search"                                       ← backfilled from shard 2 ("unknown" → "search")
├─ storyCount: 5                                            ← 3 + 2
└─ automated:
   ├─ violations: 1                                         ← 1 + 0
   ├─ passes: 20                                            ← 12 + 8
   ├─ incomplete: 1                                         ← 0 + 1
   ├─ inapplicable: 7                                       ← 4 + 3
   ├─ criteriaCovered: ["1.4.3", "2.1.1"]                   ← union of both sets
   └─ incompleteDetails: [{ruleId: "color-contrast", ...}]  ← concatenated

mergeCriteria() — deduplicate by criterion ID, two passes

  1. Pass 1 — merge from shard criteria lists: union affectedComponents across shards for each criterion ID
  2. Pass 2 — infer from merged components: if a component covers a criterion ID that no shard's criteria list included, synthesize a new criterion entry from WCAG metadata (logs a warning with the count of inferred criteria)
  • Output sorted by numeric criterion ID (1.1.1 < 1.4.3 < 4.1.2)

example
Pass 1 — merge from shard criteria lists:

Shard 1 criteria:                         Shard 2 criteria:
├─ id: "1.4.3"                            ├─ id: "1.4.3"
│  └─ affectedComponents:                 │  └─ affectedComponents:
│     ["atomic-search-box"]               │     ["atomic-search-box", "atomic-result-list"]
└─ id: "2.1.1"                            └─ (no 2.1.1 entry)
   └─ affectedComponents:
      ["atomic-search-box"]
After pass 1:
├─ id: "1.4.3"
│  └─ affectedComponents: ["atomic-result-list", "atomic-search-box"]  ← union, sorted
└─ id: "2.1.1"
   └─ affectedComponents: ["atomic-search-box"]

Pass 2 — infer from merged components. Suppose atomic-result-list covers criterion "4.1.2" (from its criteriaCovered array), but no shard had a "4.1.2" criteria entry:

After pass 2:
├─ id: "1.4.3"  (from shards)
├─ id: "2.1.1"  (from shards)
└─ id: "4.1.2"  ← synthesized from WCAG metadata
   ├─ ...
   └─ affectedComponents: ["atomic-result-list"]
   [merge-shards] 1 criteria were inferred from component coverage.

PR Chain (4 of 7)

# PR Description
1 #7111 Package scaffolding
2 #7122 Shared types, constants, utilities
3 #7123 VitestA11yReporter + wiring
4 #7137 Shard merging ← this PR
5 #7124 OpenACR report generator
6 #7125 CLI scripts
7 #7117 Weekly a11y scan workflow

KIT-5469

@y-lakhdar y-lakhdar force-pushed the feat/a11y-merge-shards branch from 5cb5fe5 to 61496ce Compare February 17, 2026 14:37
@y-lakhdar y-lakhdar added the a11y Accessibility issues label Feb 17, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds shard-report aggregation support to @coveo/atomic-a11y so parallel Storybook/Vitest shard runs can be merged into a single consolidated a11y-report.json artifact.

Changes:

  • Added mergeA11yShardReports() (plus mergeComponents / mergeCriteria) to load shard JSON files and produce a merged report + recomputed summary.
  • Updated VitestA11yReporter sharded behavior to write only a11y-report.shard-N.json (skip base file to avoid partial data).
  • Added a package-level Vitest config + unit tests for merge behavior and a simple merge CLI script.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/atomic-a11y/vitest.config.js Adds Vitest config for running atomic-a11y unit tests.
packages/atomic-a11y/src/shared/constants.ts Changes default report output directory constant.
packages/atomic-a11y/src/reporter/vitest-a11y-reporter.ts Adjusts sharded output behavior to only write shard report files.
packages/atomic-a11y/src/reporter/merge-shards.ts Implements shard discovery, validation, merge logic, and merged report writing.
packages/atomic-a11y/src/index.ts Exposes mergeA11yShardReports on the package public API.
packages/atomic-a11y/src/data/wcag-criteria.ts Modifies a generated WCAG data file (interface export change).
packages/atomic-a11y/src/tests/merge-shards.test.ts Adds unit coverage for component/criteria merge behavior and summary integration.
packages/atomic-a11y/scripts/merge-shards-cli.mjs Adds a minimal CLI entrypoint invoking the merge function from dist.
packages/atomic-a11y/package.json Adds a11y:merge-shards script.

Comment on lines +98 to +109
const existing = componentsByName.get(component.name);
if (!existing) {
componentsByName.set(component.name, toMutableComponent(component));
continue;
}

existing.storyCount += component.storyCount;
existing.automated.violations += component.automated.violations;
existing.automated.passes += component.automated.passes;
existing.automated.incomplete += component.automated.incomplete;
existing.automated.inapplicable += component.automated.inapplicable;

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mergeComponents currently sums counts and unions criteria, but it never backfills category/framework when the first shard has unknown and a later shard has a known value. This contradicts the intended merge behavior and will cause the included tests to fail. Update the merge to mirror VitestA11yReporter.getOrCreateComponent() behavior (prefer non-unknown values when merging).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants