chore: guard the contrast and theming contract in CI - #206
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
SishirP17
force-pushed
the
fix/contrast-offenders
branch
from
August 18, 2026 23:03
75ef74a to
4ad1c20
Compare
Nothing in lint, typecheck, or the test suite could see the button gradient
bug. The stylesheet rule looked correct in isolation; only the interaction
with a lower-specificity rule broke it. These are the checks that would
have caught it.
scripts/css-contract-scan.ts, wired into check.sh and the security-scan CI
job. Eight rules, each one a bug that actually shipped:
1. background-color longhand in a rule that declares no image of its own,
which is the original defect stated as a decidable rule.
2. brand rgba outside :root, the values that cannot follow an agency.
3. raw hex outside :root. The old test banned hex in .tsx but exempted
the stylesheet, which is how ~27 stray values accumulated there.
4. var() references with no :root definition.
5. AGENCY_THEME_VARIABLES against :root, both directions. This is the one
that permanently closes the original hole: a token derived from the
brand can no longer be added without being made overridable, and a
resolver-managed token cannot lack a static default.
6. --color-brand-raw used as a text color. It deliberately holds the
UNADJUSTED agency color and may fail AA.
7. the hex-alpha-append idiom.
8. dead var() fallbacks.
Verified by reintroducing the original defect: the scan fails with the
exact line and reason, and passes again on restore. Against the stylesheet
as it stood before this work it reports 130 issues.
design-system.test.ts gains a table of real (background, foreground)
pairings. It previously only checked white-against-token, so it had no way
to know which foreground met which background and .btn-ghost passed it.
Translucent tokens are composited before scoring, since the contrast of an
8% tint is not the contrast of the color it is mixed from.
lib/contrast-audit.ts is a dev-only DOM walker for the cases static rules
cannot reach. It resolves each text node's effective background by climbing
through transparent ancestors, and when it meets a background-image it
parses the gradient stops and scores the worst one , exactly the case that
made this bug invisible. Call window.__contrastAudit(), or append
?contrast=1 to outline offenders. It is tree-shaken out of production.
SishirP17
force-pushed
the
chore/css-contract-guards
branch
from
August 18, 2026 23:06
f4603ad to
82934e1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nothing in lint, typecheck, or the test suite could see the bug in #202. The stylesheet rule looked correct in isolation; only its interaction with a lower-specificity rule broke it. These are the checks that would have caught it.
scripts/css-contract-scan.tsWired into
check.shand thesecurity-scanCI job, following thesecurity-surface-scan.tspattern. Eight rules, each one a bug that actually shipped:background-colorlonghand in a rule that declares no image of its own — the original defect, stated as a decidable rule.rgbaoutside:root— the values that cannot follow an agency.:root— the old test banned hex in.tsxbut exempted the stylesheet, which is how ~27 stray values accumulated there.var()references with no:rootdefinition.AGENCY_THEME_VARIABLESagainst:root, both directions. This is the one that permanently closes the original hole: a token derived from the brand can no longer be added without being made overridable, and a resolver-managed token cannot lack a static default.--color-brand-rawused as a text color — it deliberately holds the unadjusted agency color and may fail AA.var()fallbacks.Verified by reintroducing the original defect:
Against the stylesheet as it stood before this work it reports 130 issues.
design-system.test.tsGains a table of real (background, foreground) pairings. It previously only checked white-against-token, so it had no way to know which foreground met which background — which is exactly why
.btn-ghostpassed it. Translucent tokens are composited before scoring, since the contrast of an 8% tint is not the contrast of the color it is mixed from.Borders are deliberately not asserted at 3:1, with the reasoning in a comment: WCAG 1.4.11 covers non-text content required to identify a control, and these are decorative separators.
lib/contrast-audit.tsA dev-only DOM walker for what static rules cannot reach. It resolves each text node's effective background by climbing through transparent ancestors, and when it meets a
background-imageit parses the gradient's color stops and scores the worst one — precisely the case that made this bug invisible to static analysis.window.__contrastAudit(), or?contrast=1to outline offenders in the page. Confirmed tree-shaken out of the production bundle.Last of five, stacked on #205.