From 46b965b9daa2ca3e9c4bcc4d74defbbe4deb10c2 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 18 May 2026 12:36:33 +0900 Subject: [PATCH] fix(rules-injector): bound matcher cache Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/rules-injector/matcher.test.ts | 13 +++++++++++++ src/hooks/rules-injector/matcher.ts | 13 ++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/hooks/rules-injector/matcher.test.ts b/src/hooks/rules-injector/matcher.test.ts index 009c19645..fce36bc4e 100644 --- a/src/hooks/rules-injector/matcher.test.ts +++ b/src/hooks/rules-injector/matcher.test.ts @@ -30,6 +30,19 @@ describe("shouldApplyRule", () => { expect(getMatcherCacheStats()).toEqual({ entries: 2 }) }) + it("#given many unique globs #when matching repeatedly #then matcher cache stays bounded", () => { + // given + const projectRoot = "/workspace/project" + + // when + for (let index = 0; index < 300; index += 1) { + shouldApplyRule({ globs: `src/file-${index}.ts` }, `${projectRoot}/src/file-${index}.ts`, projectRoot) + } + + // then + expect(getMatcherCacheStats().entries <= 256).toBe(true) + }) + it("#given matching glob #when path is under project root #then returns matching reason", () => { // given / when const result = shouldApplyRule({ globs: "src/**/*.ts" }, "/workspace/project/src/index.ts", "/workspace/project") diff --git a/src/hooks/rules-injector/matcher.ts b/src/hooks/rules-injector/matcher.ts index d68dae109..cd6995f6e 100644 --- a/src/hooks/rules-injector/matcher.ts +++ b/src/hooks/rules-injector/matcher.ts @@ -15,13 +15,24 @@ export interface MatcherCacheStats { } const PICOMATCH_OPTIONS = { dot: true, bash: true } as const +const MAX_MATCHER_CACHE_ENTRIES = 256 const matcherCache = new Map() function matcherFor(pattern: string): PathMatcher { const cached = matcherCache.get(pattern) - if (cached) return cached + if (cached) { + matcherCache.delete(pattern) + matcherCache.set(pattern, cached) + return cached + } const matcher = picomatch(pattern, PICOMATCH_OPTIONS) + if (matcherCache.size >= MAX_MATCHER_CACHE_ENTRIES) { + const oldestPattern = matcherCache.keys().next().value + if (oldestPattern !== undefined) { + matcherCache.delete(oldestPattern) + } + } matcherCache.set(pattern, matcher) return matcher }