The linter¶
The linter is the contract. The documents describe it; it decides. The standing rule on this project:
Quote
If something is off and the linter did not catch it, that is a gap in the rules — fix the rule, do not work around it.
Several rules below exist only because something shipped wrong and the fix was a new rule.
The rules¶
Vocabulary¶
unknown-class- A class in a template that the loaded stylesheet does not define. The vocabulary is read from
the stylesheet itself, following
@import, so it is always what the browser actually loads. unused-class- The reverse: a class the stylesheet defines that no template names. Either it is dead, or it
is a second component doing a job the vocabulary already had —
.ads-tagbeside.ads-chip,.ads-crumbsbeside.ads-breadcrumb. Four of those shipped before this rule existed, each added by someone who did not know the first was there. arbitrary-value,bare-value,dark-variant- Syntactic rules that give a precise message — "bypasses the token scale" — where
unknown-classwould only say "not in the design system". inline-stylestyle=is a design decision in the wrong place. A custom property carrying data is allowed:style="--bar: 63%"on a progress bar is fine, because the number exists only at render time.dynamic-class- A class name assembled from an expression, which no static check can verify.
Stylesheet hygiene¶
duplicate-rule- The same top-level selector given a property block twice.
.ads-spinnerwas written twice in one file, with two sizes and two durations; the second silently won and the first had never rendered. Neither vocabulary rule can see this — both spellings define the class. duplicate-token- A custom property declared twice inside one rule. Only custom properties: a repeated ordinary
property is usually a deliberate fallback (
display: -webkit-box; display: flex), and a repeated custom property never is.
Accessibility¶
no-accessible-name,no-alt,no-label,heading-skip,duplicate-id,positive-tabindex,no-lang- Static checks on rendered markup.
no-labelunderstands a control wrapped by its own<label>, which needs nofor. table-overflow- An
.ads-tablenot wrapped in.ads-table-scroll. Without it the page scrolls sideways instead of the table and the far columns become unreachable.
Vendoring¶
asset-drift,asset-missing- Every vendored file is hashed against the packaged original. This is what stops
syncfrom being an escape hatch.
Allowlist¶
Classes a library sets at runtime cannot be checked against the stylesheet:
htmx-indicator # htmx shows this while a request is in flight
htmx-request # htmx sets this on the element issuing a request
Projects add their own in .abilian-ds-allow, same format. Every entry needs a reason. A
growing list is a bug in the design system, not a normal cost of doing business.
Things that turned out to be hard¶
Two mistakes worth knowing about, because both made the gate quieter rather than noisier:
Comments are not declarations. A well-commented stylesheet is full of class names in prose. Reading them as definitions meant the vocabulary was whatever the sheet declared plus whatever it talked about, and any class mentioned in a comment passed while generating no CSS.
Strings are not selectors. Every drawn checkbox carries an inline SVG data-URI. Scanning it
for .-prefixed names yielded w3, org/2000/svg' and 5l2 from the path data — 48 fragments
in all, every one of which became a class name a template could use and pass. Strings and
comments are both stripped before the scan now.