Skip to content

gate(visual): the path matcher is positive-only, so a gate cannot exclude a generated artifact from an otherwise-visual directory #9554

Description

@JSONbored

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

  • Add exclusion support so a gate can say "this directory except these generated artifacts" without enumerating every visual extension.
  • It must be additive: matchesAny is also the hard-guardrail matcher, where an unrecognized glob failing toward matching is the safety-correct default. Changing matchesAny itself 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.
  • Wire it into the screenshot-table gate's whenPaths matching, in both places that read that list (isScreenshotTableGateInScope and hasCommittedImageFile). 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:

  • 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

  • The live shape: a directory glob plus one generated-file exclusion.
  • With no exclusions, behaviour is identical to matchesAny.
  • A path matching no include is false regardless of excludes; an all-exclude list is not an implicit wildcard include.
  • Multiple excludes all apply.
  • A bare ! stays a literal include, not a malformed exclusion.
  • SECURITY: an exclude glob is bound by the same wildcard cap as an include, resolves instantly against an adversarial multi-KB path, and fails toward excluding nothing.

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.

  • Verify and fix them.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.orbGittensory Orb related - maintainer self-hosting analytics.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions