Parent: #9434
Summary
matchesAny (packages/loopover-engine/src/signals/change-guardrail.ts) is positive-only: no ! negation, and {a,b} braces are regex-escaped to literals. There is no way to express "this directory, except these generated artifacts".
That gap is what let apps/loopover-ui/public/** sit in a screenshotTableGate.whenPaths list. The directory is overwhelmingly non-visual — a generated 536KB openapi.json, robots.txt, sitemap.xml, sw.js, manifest.webmanifest, favicons — and CLAUDE.md requires contributors to run npm run ui:openapi on any API/schema change, which regenerates exactly that file. Following the documented contribution steps therefore tripped a gate demanding a Desktop/Tablet/Mobile × Dark before/after screenshot matrix of a JSON spec file. With action: close and the gate being one-shot, 5 contributor PRs were auto-closed with no recovery path (#9414, #9411, #9410, #9409, #9405).
The only available workaround was to drop the whole directory from scope — losing genuine coverage of the visual assets that do live there.
Deliverables
The fail-direction requirement (the subtle part)
The include and exclude halves need opposite fail directions for an over-complex (ReDoS-capped) glob:
- Include: failing toward "matches" is safe — worst case, more paths are considered in scope. This is
matchesAny's existing behaviour and must be preserved.
- Exclude: failing toward "matches" is wrong — an over-complex exclude glob resolving to "matches everything" would silently widen what gets excluded, shrinking a safety gate's coverage. That is the opposite of what the gate exists for.
So the exclude half must fail toward excluding nothing, meaning gate coverage can only ever end up too wide from a malformed exclude glob, never too narrow.
Tests
Also in scope: the sibling repos (#9434 follow-up 1)
The parent issue flagged that JSONbored/metagraphed and JSONbored/awesome-claude private configs were not yet verified for the same over-broad glob.
Parent: #9434
Summary
matchesAny(packages/loopover-engine/src/signals/change-guardrail.ts) is positive-only: no!negation, and{a,b}braces are regex-escaped to literals. There is no way to express "this directory, except these generated artifacts".That gap is what let
apps/loopover-ui/public/**sit in ascreenshotTableGate.whenPathslist. The directory is overwhelmingly non-visual — a generated 536KBopenapi.json,robots.txt,sitemap.xml,sw.js,manifest.webmanifest, favicons — andCLAUDE.mdrequires contributors to runnpm run ui:openapion any API/schema change, which regenerates exactly that file. Following the documented contribution steps therefore tripped a gate demanding a Desktop/Tablet/Mobile × Dark before/after screenshot matrix of a JSON spec file. Withaction: closeand the gate being one-shot, 5 contributor PRs were auto-closed with no recovery path (#9414, #9411, #9410, #9409, #9405).The only available workaround was to drop the whole directory from scope — losing genuine coverage of the visual assets that do live there.
Deliverables
matchesAnyis also the hard-guardrail matcher, where an unrecognized glob failing toward matching is the safety-correct default. ChangingmatchesAnyitself would mean a maintainer's literal path beginning with!is silently reinterpreted as an exclusion rather than guarded — so exclusion semantics must be opt-in for callers that want them.whenPathsmatching, in both places that read that list (isScreenshotTableGateInScopeandhasCommittedImageFile). They must share one matcher — disagreeing on what "scoped" means would let a path excluded from gate scope still count as a stray committed image.The fail-direction requirement (the subtle part)
The include and exclude halves need opposite fail directions for an over-complex (ReDoS-capped) glob:
matchesAny's existing behaviour and must be preserved.So the exclude half must fail toward excluding nothing, meaning gate coverage can only ever end up too wide from a malformed exclude glob, never too narrow.
Tests
matchesAny.!stays a literal include, not a malformed exclusion.Also in scope: the sibling repos (#9434 follow-up 1)
The parent issue flagged that
JSONbored/metagraphedandJSONbored/awesome-claudeprivate configs were not yet verified for the same over-broad glob.