Files
Claude 0b79d4825a ci: replace the hand-rolled path classifier with dorny/paths-filter
The `changes` job was a shell script: one `gh api` call for the changed-file
list, two `grep -qvE`, and a fallback for every way that list can come back
unusable — empty, failed call, a push whose `before` is all zeros. Every one of
those branches is something `dorny/paths-filter` already does, and the edge
cases were exactly the half we were maintaining by hand.

This is `prefer-existing-solutions` applied to the place it had not been:
BUILD was never justified here, it was just what got written first.

THE COST, stated because it is real and it is the only one: on a `push` the
action diffs with git and therefore needs a checkout, while the old classifier
needed none and finished in ~3s. On a `pull_request` it reads the REST API and
still needs nothing. `push` in this workflow only ever means a merge into main,
so the checkout is paid once per merge and the job still rounds to the same
single billable minute.

🔴 `predicate-quantifier: some-with-excludes` IS LOAD-BEARING. Read out of the
action's own filter.ts before writing the filters, not assumed: the default is
`some`, i.e. `patterns.some(...)`, under which a file beneath site/ matches the
`'**'` pattern and sets root=true for a site-only diff — the exact hole the
filter exists to prevent, silently. `some-with-excludes` makes a negated pattern
a final exclusion ("included by >=1 pattern AND excluded by 0").

Also verified against the action's source rather than guessed: it calls
picomatch with `{ dot: true }`. Without that, `**` would not match `.claude/…`
and an agent-config-only diff would skip the root jobs.

WHAT THE TEST CAN AND CANNOT DO NOW. It could previously re-run the rule by
shelling out to `grep` with the workflow's own pattern. With an action it does
the nearest honest thing: reads the filters with a YAML parser and evaluates
them with the SAME matcher library the action uses, through a transcription of
the action's predicate. That transcription is the one copied thing, and its risk
is bounded two ways — the action is PINNED to `@v4.0.3` (a test fails on a
floating major), and the setting the predicate depends on is asserted
separately, because a transcription cannot catch its own premise being absent.

The conservative fallback is no longer ours to test, and that is most of the
point: the action documents that "all files are considered as added if there is
no common ancestor with base branch or no previous commit" — a new branch or a
shallow history runs everything.

Mutation-proven, each failing exactly its own assertion:
  - delete `predicate-quantifier`        -> `declares predicate-quantifier: …`
  - unpin `@v4.0.3` to `@v4`             -> `pins the action to an exact version`
  - break `!site/**` to `!nosuch/**`     -> `skips the root jobs for a site-only change`

`picomatch` + `@types/picomatch` become explicit devDependencies. picomatch was
already in the tree transitively; a test resting on an undeclared transitive
dependency is a landmine, so it is declared.

Gates: npm run check 18/18 · coverage 3975 passed, 0 failed. The 99.94%
statement gate fails locally on src/mock-model.ts (untouched; 25 tests skip
locally), green on CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 23:54:53 +00:00
..