Repository navigation
feat(discovery)!: match exclude patterns with picomatch under an owned glob grammar - #462
Merged
Merged
Conversation
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The implementation, validation boundary, public exports, documentation, and test coverage are consistent and complete.
Review effort: Balanced
Findings: None
What changed in this PR
Adds validated picomatch-based glob filtering to repository discovery while preserving existing documented behavior.
Changes:
- Compiles and validates exclude patterns at input parsing.
- Adds
?, character classes, and negation support. - Updates tests, e2e scenarios, documentation, and dependencies.
| File | Description |
|---|---|
src/discovery/discover.ts |
Implements validated compiled glob matching. |
src/flows/inputs.ts |
Compiles patterns during input parsing. |
src/problem.ts |
Adds detailed invalid-pattern errors. |
src/index.ts |
Exports the new public API. |
test/discovery/discover.test.ts |
Tests grammar and matching semantics. |
test/action/run.test.ts |
Tests action-level validation errors. |
test/cli/inputs.test.ts |
Adapts CLI input assertions. |
test/problem.test.ts |
Updates problem-message specimens. |
test/private.test.ts |
Records the reviewed export surface. |
test/e2e/oracle.ts |
Supports ? and negation independently. |
test/e2e/scenarios/multi-discovery-exclude.yml |
Covers scoped and single-character globs. |
test/e2e/scenarios/multi-discovery-exclude-negation.yml |
Covers negated exclusion. |
docs/operate/multi-repo.md |
Documents the owned glob grammar. |
docs/reference/library.md |
Documents new library exports. |
docs/reference/inputs.md |
Updates generated input reference. |
action.yml |
Updates generated action input help. |
package.json |
Adds picomatch and its types. |
bun.lock |
Locks the added dependencies. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Vivswan
force-pushed
the
refactor/discovery-picomatch
branch
from
October 5, 2026 06:09
179d0a4 to
2320431
Compare
Contributor
File size check0 over a hard cap (fails), 11 warning(s).
Split the file, wrap the line, shorten or exempt the comment, or list the path in 5 managed file(s) skipped; repo-platform owns them. |
Vivswan
force-pushed
the
refactor/discovery-picomatch
branch
from
October 5, 2026 06:56
2320431 to
d8dad6e
Compare
Vivswan
force-pushed
the
refactor/discovery-picomatch
branch
from
October 5, 2026 07:18
d8dad6e to
07b31db
Compare
Vivswan
force-pushed
the
refactor/discovery-picomatch
branch
from
October 5, 2026 07:58
07b31db to
8cdf561
Compare
Vivswan
force-pushed
the
refactor/discovery-picomatch
branch
from
October 5, 2026 08:26
8cdf561 to
4017d15
Compare
Vivswan
force-pushed
the
refactor/discovery-picomatch
branch
from
October 5, 2026 08:52
4017d15 to
58a4d99
Compare
Vivswan
force-pushed
the
refactor/discovery-picomatch
branch
from
October 5, 2026 09:06
58a4d99 to
d7709af
Compare
Vivswan
force-pushed
the
refactor/discovery-picomatch
branch
from
October 5, 2026 09:17
d7709af to
fbdb52e
Compare
…d glob grammar
The exclude discovery filter compiled its own RegExp from each pattern, with "*" the only operator and every other character literal. Each pattern is now compiled once at the input boundary with picomatch (the posix entry, nocase, dot, posix classes, no fast paths, debug), and the boundary owns the grammar: name characters, at most one "/", "*", "?", "[abc]", "[!abc]" or "[^abc]", and one leading "!". Anything else is refused when the inputs are parsed, naming the written pattern, the reason, and the fix, because the pattern is the user's input and because picomatch reads braces, parentheses, quotes, backslashes, a leading "./", and a run of stars as regex syntax through paths no option closes, compiling a wrong or never-matching filter silently.
DiscoveryFilters.exclude carries the compiled ExcludePattern (pattern, scope, regex) instead of strings, so a config built from argv still equals one built from the environment, and compileExcludePattern with ExcludePattern join the library surface. The e2e oracle's independent matcher learns "?" and a leading "!", and two scenarios pin the new forms end to end. The upgrading guide gains section 61.
BREAKING CHANGE: In an exclude pattern, "?" now matches one character, "[abc]" and "[!abc]" are classes, and a leading "!" negates the pattern; each matched nothing before, so a pattern that relied on one of them matching literally is deleted and one that meant a glob is rewritten in that grammar. A leading "./" (drop it), a run of stars (write one "*"), surrounding whitespace from a library caller (trim it), a "." or ".." side, a second "/", a doubled "!", and any regex token ("(a)+", "\d", "{a..b}", quotes) fail the run naming the fix. Library callers supply exclude as ExcludePattern values from compileExcludePattern instead of strings.
Vivswan
force-pushed
the
refactor/discovery-picomatch
branch
from
October 5, 2026 10:58
fbdb52e to
474dc3b
Compare
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.



What this changes
The
excludediscovery filter now matches through picomatch inside a grammar the input boundary owns. Documented forms keep their meaning. Anything else is refused by sentences that each name one fix, and one round yields acceptance.How
compileExcludePatternkeeps the text, the scope, and the regex, so an argv config equals an env config./at most,*,?,[abc],[!abc], one leading!. Picomatch reads braces, quotes, and backslashes as regex, so anything else is refused.Proof
tsc, biome,lint:arch, knip,build:check,merge-tree, smoke green.Technical details
Line accounting (
git diff --numstat origin/main...HEAD)Dependency:
picomatch4.0.7 (runtime, exact),@types/picomatch4.0.3 (dev, exact). The main entry withwindows: false, since the extensionlesspicomatch/posixsubpath fails Node ESM resolution for the externalized library (package smoke) and the main entry otherwise follows the host OS.literalBrackets: false, since picomatch otherwise emits(?:\[a\]|[a])per class, an alternation no name can match that made 20,000 classes quadratic and reached the engine's size limit.Deliberate readings
EXCLUDE_STEPS, a pipeline of(view) => Passfunctions folded byjudgeExcludePattern, the only scope holding the raw pattern. Order: trim, remove every stray character (listed), remove unpaired brackets (by position), remove a!or^that neither negates nor opens a class (by position), per class in one rebuild (remove*and?, delete an empty class, move a literal-last, turn a backwards range), drop every leading./, collapse a star run. Then what the user has to write, named with no fix applied: the length cap, an empty body (ends the pass), the segment count, a dot-only side, and a regex the engine itself refuses.regex.test("")so V8's lazy compile surfaces there, and hands the regex out through the fold;compileExcludePatternkeeps it. A test spies onpicomatch.makeReand sees one call forsvc-[ab]; a clean result with no regex is aBUG:throw, since the engine step is last and runs whenever no earlier step ended the pass.-only as a range between two name characters or a literal first or last, so turning a range around can never pair a new one.indexOfwalk collects classes, a flag array marks unpaired brackets, removal skips through aSeton code units. Worst inputs at 65,536 characters:[runs 8 ms,!runs 7 ms,]runs 11 ms, 20,000 classes 48 ms under node including the engine probe../, collapse stars, replace or delete a quoted class), applies them in order over 300 seeded patterns (seed 20261005, 36 pieces), and asserts acceptance or only remainder sentences the first round already named; a sentence with no applier throws. Named one-round cases include!!./tmp-**,x/.!,*[]*,[]./x,./+ 60,000?, and the over-cap inputs.applyFixandoneRound: 4000 patterns with a 65,540-character piece (seed 20261005), 50,000 patterns over 96 pieces up to 12 per pattern (seed 0x9e3779b9), 50,000 over 104 pieces (seed 0xa57a1c21), 30,000 over 114 pieces with emoji and lone surrogates (seed 0x6a09e667). All clean.quote(moved tosrc/text.ts), so a"or\in the input stays distinguishable from the message's punctuation.matchBasewas rejected: under ito/*stops matchingo/x. The scope split stays our code.ExcludePatternstores picomatch'sRegExp, not a closure, sotoEqualholds across the argv and env paths.input-exclude-pattern-invalid { pattern, reason }.Recorded, not built
[.]or[!A-Za-z0-9_.-]compile correctly and match nothing.?or!patterns: the oracle supports them, the generator still rolls literal names.Files:
src/discovery/discover.ts,src/flows/input-readers.ts,src/flows/inputs.ts,src/problem.ts,src/text.ts,src/index.ts,test/discovery/discover.test.ts,test/e2e/oracle.ts, two e2e scenarios,docs/upgrading/v2-to-v3.md,docs/operate/multi-repo.md,docs/reference/library.md, regeneratedaction.ymlanddocs/reference/inputs.md.BEGIN_COMMIT_OVERRIDE
feat(discovery)!: match exclude patterns with picomatch under an owned glob grammar
BREAKING CHANGE: In an exclude pattern, "?" now matches one character, "[abc]" and "[!abc]" are classes, and a leading "!" negates the pattern; each matched nothing before, so a pattern that relied on one of them matching literally is deleted and one that meant a glob is rewritten in that grammar. A leading "./" (drop it), a run of stars (write one "*"), surrounding whitespace from a library caller (trim it), a "." or ".." side, a second "/", a doubled "!", and any regex token ("(a)+", "\d", "{a..b}", quotes) fail the run naming the fix. Library callers supply exclude as ExcludePattern values from compileExcludePattern instead of strings.
END_COMMIT_OVERRIDE