fix(engine): count fnmatch label-pattern wildcard groups per raw star, not by path-glob rules - #10128
Conversation
…, not by path-glob rules labelPatternToRegExp reused change-guardrail's path-glob wildcard-group counter to guard against catastrophic backtracking, but that counter treats a ** pair as ONE group (the path compiler collapses ** into a single .*). The fnmatch compiler here has no ** concept and emits one .* per *, so the count and the compiled regex disagreed for any ** pattern: *a**b counted 2 but compiled 3 .* groups and was wrongly accepted, admitting a pattern this compiler builds into a catastrophic- backtracking RegExp on an adversarial near-miss label. Count one group per raw * (no ** pairing; ? and [..] are not counted) and compare against the shared MAX_GLOB_WILDCARD_GROUPS, now exported from change-guardrail rather than redeclared. An over-complex registry key degrades to the existing LABEL_PATTERN_NEVER_MATCHES and is still cached. The path-glob counter and every path consumer keep their **-is-one-group semantics unchanged. Closes JSONbored#9994
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-31 08:41:06 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #10128 +/- ##
==========================================
+ Coverage 91.95% 91.98% +0.02%
==========================================
Files 931 931
Lines 113937 113955 +18
Branches 27505 27512 +7
==========================================
+ Hits 104774 104823 +49
+ Misses 7863 7828 -35
- Partials 1300 1304 +4
Flags with carried forward coverage won't be shown. Click here to find out more.
|
What & why
labelPatternToRegExp(packages/loopover-engine/src/scoring/label-match.ts) guards against catastrophic backtracking by counting wildcard groups before compiling a label pattern to a RegExp — reusingchange-guardrail.ts'shasUnsafeWildcardCount. But that predicate is the path-glob counter, which deliberately treats a**pair as one group because the path compiler collapses**into a single.*.The fnmatch compiler here has no
**concept — it emits one.*per*. So the count and the compiled regex disagreed for any**pattern:.*groups compiled*a**b**/**a**b**cThe cap's own benchmark puts 3 groups at "over 2 seconds at ~4,000 chars" and 4 at "35 seconds at 1,614 chars", so these accepted patterns are exactly the ones that risk a catastrophic-backtracking
RegExp.test()on an adversarial near-miss label.labelMatchesPattern's left-hand input is caller-supplied (ScorePreviewInput.labels) and the right-hand patterns are registrylabelMultiplierskeys the module itself documents as untrusted.The fix
Count the groups the fnmatch compiler actually emits — one per raw
*, with no**pairing (?compiles to a single.and[…]classes are not counted, neither can backtrack ambiguously) — and compare againstMAX_GLOB_WILDCARD_GROUPS, now exported fromchange-guardrail.tsso the two surfaces share one empirically-safe threshold rather than redeclaring it (the exact drift the existinghasUnsafeWildcardCountexport comment warns about).An over-complex pattern degrades to the existing
LABEL_PATTERN_NEVER_MATCHESand is still cached, exactly as today — the fail-safe direction here is "no multiplier applies".Unchanged:
hasUnsafeWildcardCount,countWildcardGroups,globToRegExp,matchesAnyand every path-glob consumer keep their**-is-one-group semantics byte-identically (correct for the path compiler — rejectingpublic/**/*.jsonthere would break the content lane). Every ≤2-group label pattern (type:*,kind/*,priority:?,a*b*c,[bc]ug), the[seq]/[!seq]/invalid-range handling, and the LRU cache behaviour are all preserved.Tests
packages/loopover-engine/test/label-match.test.ts, new — node:test per thecontent-lane-flag.test.tsconvention):*a**band**/**are rejected (never match); the ≤2-group and non-*cases still match; a rejected pattern is still cached (repeated read served from the cache).test/unit/scoring.test.ts): the same rejection + preserved cases throughlabelMatchesPattern/labelMultiplierFor. One existing assertion is updated —public/**/*.json(3 compiled groups) is now correctly rejected as a label pattern (it was the path-glob-count's false accept); the comment now states the fnmatch counting rule.mainand pass with the fix.Validation
packages/loopover-engine/src/scoring/label-match.tsand.../signals/change-guardrail.tsis 100% line and branch (engine lines credited via the root-vitest upload; the added test is also inpackages/loopover-engine/test/**for the dual-upload union).npm run typecheckclean for these files;npm run engine-parity:drift-checkpasses; the engine's ownnode --testsuite is green;npm run dead-exports:checkclean.git diff --checkclean; no schema/migration/generated-artifact change.Closes #9994