Playwright --shard distributes by count (per file with fullyParallel:false),
blind to duration — measured skew on the 9-shard matrix: 24m47s worst vs 1m47s
best (14x), putting E2E on the CI critical path (~25min of the 33min gate).
- scripts/quality/balance-e2e-shards.mjs: LPT greedy (heaviest first into the
lightest shard) over config/quality/e2e-timings.json; deterministic
(weight desc, filename tiebreak); new specs get the median weight; the CLI
self-verifies the shard union equals the discovered spec list and exits
non-zero on ANY inconsistency (missing timings, lost spec) so the CI step
falls back to plain --shard — never fewer specs than before.
- config/quality/e2e-timings.json: relative weights seeded from spec LOC
(proxy); replace with real per-file durations from a full run when convenient
(documented in _meta). LOC-seeded packing already lands at 742-761 per shard
(1.03x skew) vs the alphabetical round-robin that produced 14x.
- ci.yml test-e2e: balanced list per shard with logged assignment + fallback.
TDD: 5 unit tests (LPT invariants, determinism, completeness, median fallback,
seed-vs-specs drift guard).