feat: reduce the friction of adopting cssert (0.2.0) - #2
Merged
Merged
Conversation
Acts on a field report from a Django + Tailwind v4 + daisyUI repository that
ran @cssert/cli 0.1.0 in CI. The detection itself held up; every item below is
about the cost of getting it wired in.
Four defaults change, each with an opt-out:
- Relative paths in a config file resolve against the config file's directory
rather than cwd, the way eslint/vitest/tsc resolve theirs. A repository whose
npm root is a subdirectory could not put cssert in an npm script, because
`cssert check --config ../cssert.config.mjs` matched nothing from there.
Globs passed as flags stay cwd-relative. `root`/`--root` override the base and
`resolveFrom: "cwd"` restores the old behaviour.
- `--format github` emits one annotation per class, anchored at the first
occurrence with the rest counted in the message. GitHub caps the annotations
it shows per check run, so one class used in 85 places hid every other
finding, worst on the first run. `--annotate-occurrences` restores the old
behaviour; human and SARIF are unchanged.
- A baseline file that does not exist is a note on stderr, not exit 2. Writing
the config before freezing the findings is the normal order of work.
`--require-baseline` makes it fatal, `--no-baseline` (or an empty path, which
used to fail with EISDIR) ignores an existing one.
- `baseline create` freezes only the kinds that fail the check. Entries keyed by
a whole `{% if %}` expression rot on any edit to that expression and could not
fail the build anyway. `--kind all` restores the old behaviour.
Added, so that a green run says what it did not verify:
- `--min-documents`/`--min-stylesheets` fail the run when fewer inputs arrived
than expected, reported through the new `Report.errors`. A CI artefact that
failed to download must not pass as a green check.
- The summary counts documents that still contain unresolved class expressions.
- JSON findings carry `sources`, the input globs they were seen under, so a
finding listed only under a template glob is a page nobody rendered.
- `hooks`/`--hook` for classes that are supposed to have no styles, `baseline
create|prune --dry-run`, and `--version` naming the package.
README gains "What a green run does and does not prove" (existence, only over
the files it was given) so the adopting side can quote it instead of writing
it themselves, a baseline operations guide covering what to freeze and what
not to, a field report of what the tool found in practice, and an upgrade
table for the four changed defaults.
The Django recipe gains a pytest-based rendering recipe that reuses the
existing test fixtures and asserts each page's status code, a note that
rendering a page is not the same as covering it (both branches of every
`{% if %}` need data), and the three-job GitHub Actions workflow that the CSS
and HTML coming from different toolchains actually requires.
D28-D33 record the reasoning; D19 and D22 are marked superseded and amended.
tokibito
force-pushed
the
feat/0.2.0-adoption-friction
branch
from
September 13, 2026 14:52
6ee71b1 to
eb65f75
Compare
Merged
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.
Acts on a field report from a repository running
@cssert/cli0.1.0 in CI:Django 6.1 + Tailwind v4 + daisyUI 5 + Vite, with the npm root in a
frontend/subdirectory. The report had no complaints about the detection itself — every
item is about the cost of wiring the tool in.
Implements all of P1 (4), P2 (2) and P3 (2), plus documentation items A-F.
Breaking changes (4, each with an opt-out)
resolveFrom: "cwd"--format githubemits one annotation per class--annotate-occurrences--require-baselinebaseline createfreezes only the kinds that fail the check--kind all1. Where globs resolve from
The new
src/cli/roots.tskeeps two bases: paths written in a config fileresolve against that file's directory, paths passed as flags stay relative
to cwd. Reported paths are relative to whichever base found them, so the same
config produces identical output from any directory — the acceptance criterion
in the report.
test/cli/roots.test.tsasserts byte-identical stdout from therepository root, from
frontend/, and fromfrontend/sub/.Before this, a repository whose npm root is a subdirectory could not put cssert
in an npm script at all:
cssert check --config ../cssert.config.mjssilentlymatched nothing from there.
root(config, relative to the config file) and--root(CLI, relative tocwd) override the base. Note that
rootin a config file rebases only thatfile's own paths and leaves command-line globs on cwd — "the file I pointed at
from here" is the obvious reading of a flag.
--rootis given at invocationtime and so rebases flags too.
2-4
message (
… (and 65 other places).), so N classes produce N annotations.The human format still lists every occurrence; SARIF still emits one result
per occurrence, since code scanning deduplicates by fingerprint.
no baseline at ...; run "cssert baseline create" ...to stderr and continues. This fails louder, not quieter — nothing issuppressed.
--no-baselineis new, and--baseline ""is now read as"disabled" rather than failing with EISDIR.
baseline createdefaults to--kind failing(missing, plusdynamic-suspectonly under--fail-on-dynamic). Entries keyed by a whole{% if %}expression rot on any edit to that expression and could not failthe build anyway.
--kind all|missing|dynamic-suspectselects explicitly, andboth
createandprunetake--dry-run.Additions
documents still contain unresolved class expressions.
--min-documents/--min-stylesheetsturn a truncated input set into exit 1,surfaced in every format through the new
Report.errors. This closes the holewhere a failed CI artefact download passes as a green run over zero documents.
sources, the input globs theywere seen under, so
select(.sources == ["templates/**/*.html"])extracts exactly the pages thatwere never rendered.
hooks/--hookfor classes that are supposed to have no styles, and--version/--helpnaming the package.Two deviations from the report
--expect-html <glob>is implemented as--min-documents <n>. Therequest wavered between a glob and a count, so this takes the "declare how
many should have arrived" reading.
and rendered output as two distinct input kinds and reporting coverage
between them.
sourcesalready makes the distinction mechanically available;worth seeing how that is used in practice first.
Documentation
guide (D), a field report of what the tool found in practice (F), and an
"Upgrading from 0.1" table for the four changed defaults.
docs/recipes/django.md: a pytest-based rendering recipe that reuses existingfixtures and asserts each page's status code (A), a note that rendering a page
is not the same as covering it — both branches of every
{% if %}need data(B), and the three-job GitHub Actions workflow (C).
docs/decisions.md: D28-D33 record the reasoning; D19 and D22 are markedsuperseded and amended.
Verification
test/cli/roots.test.ts,test/cli/coverage.test.ts);typecheck, biome and build are clean.
from the repository root and from
frontend/,--baseline "", and a missingbaseline file.
The release goes through changesets, so
package.jsonis untouched — theversion bump to 0.2.0 happens in the Version Packages PR as
docs/releasing.mddescribes.