perf(rules-injector): cache match decisions

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-05-18 13:30:23 +09:00
parent 61890f0857
commit 50ae2e110e
2 changed files with 623 additions and 367 deletions
+340 -204
View File
@@ -5,13 +5,18 @@ import * as os from "node:os";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { RULES_INJECTOR_STORAGE } from "./constants"; import { RULES_INJECTOR_STORAGE } from "./constants";
import { clearParsedRuleCache, createRuleInjectionProcessor, getParsedRuleCacheStats } from "./injector"; import {
clearParsedRuleCache,
createRuleInjectionProcessor,
getParsedRuleCacheStats,
} from "./injector";
type StatSnapshot = { mtimeMs: number; size: number }; type StatSnapshot = { mtimeMs: number; size: number };
let trackedRulePath = ""; let trackedRulePath = "";
let statSnapshots: Array<StatSnapshot | Error> = []; let statSnapshots: Array<StatSnapshot | Error> = [];
let trackedReadFileCount = 0; let trackedReadFileCount = 0;
let trackedShouldApplyRuleCount = 0;
let mockedHomeDir = ""; let mockedHomeDir = "";
const originalReadFileSync = fs.readFileSync.bind(fs); const originalReadFileSync = fs.readFileSync.bind(fs);
@@ -19,246 +24,377 @@ const originalStatSync = fs.statSync.bind(fs);
const originalHomedir = os.homedir.bind(os); const originalHomedir = os.homedir.bind(os);
function createOutput(): { title: string; output: string; metadata: unknown } { function createOutput(): { title: string; output: string; metadata: unknown } {
return { title: "tool", output: "", metadata: {} }; return { title: "tool", output: "", metadata: {} };
} }
async function createProcessor(projectRoot: string): Promise<{ async function createProcessor(projectRoot: string): Promise<{
processFilePathForInjection: ( processFilePathForInjection: (
filePath: string, filePath: string,
sessionID: string, sessionID: string,
output: { title: string; output: string; metadata: unknown } output: { title: string; output: string; metadata: unknown },
) => Promise<void>; ) => Promise<void>;
}> { }> {
const sessionCaches = new Map< const sessionCaches = new Map<
string, string,
{ contentHashes: Set<string>; realPaths: Set<string> } { contentHashes: Set<string>; realPaths: Set<string> }
>(); >();
return createRuleInjectionProcessor({ return createRuleInjectionProcessor({
workspaceDirectory: projectRoot, workspaceDirectory: projectRoot,
truncator: { truncator: {
truncate: async (_sessionID: string, content: string) => ({ truncate: async (_sessionID: string, content: string) => ({
result: content, result: content,
truncated: false, truncated: false,
}), }),
}, },
getSessionCache: (sessionID: string) => { getSessionCache: (sessionID: string) => {
if (!sessionCaches.has(sessionID)) { if (!sessionCaches.has(sessionID)) {
sessionCaches.set(sessionID, { sessionCaches.set(sessionID, {
contentHashes: new Set<string>(), contentHashes: new Set<string>(),
realPaths: new Set<string>(), realPaths: new Set<string>(),
}); });
} }
const cache = sessionCaches.get(sessionID); const cache = sessionCaches.get(sessionID);
if (!cache) { if (!cache) {
throw new Error("Session cache should exist"); throw new Error("Session cache should exist");
} }
return cache; return cache;
}, },
readFileSync: (filePath: string, encoding: "utf-8") => { readFileSync: (filePath: string, encoding: "utf-8") => {
if (filePath === trackedRulePath) { if (filePath === trackedRulePath) {
trackedReadFileCount += 1; trackedReadFileCount += 1;
} }
return originalReadFileSync(filePath, encoding); return originalReadFileSync(filePath, encoding);
}, },
statSync: (filePath: fs.PathLike) => { statSync: (filePath: fs.PathLike) => {
if (filePath === trackedRulePath) { if (filePath === trackedRulePath) {
const next = statSnapshots.shift(); const next = statSnapshots.shift();
if (next instanceof Error) { if (next instanceof Error) {
throw next; throw next;
} }
if (next) { if (next) {
return { return {
mtimeMs: next.mtimeMs, mtimeMs: next.mtimeMs,
size: next.size, size: next.size,
isFile: () => true, isFile: () => true,
} as ReturnType<typeof originalStatSync>; } as ReturnType<typeof originalStatSync>;
} }
} }
return originalStatSync(filePath); return originalStatSync(filePath);
}, },
homedir: () => mockedHomeDir || originalHomedir(), homedir: () => mockedHomeDir || originalHomedir(),
shouldApplyRule: () => ({ applies: true, reason: "matched" }), shouldApplyRule: () => {
isDuplicateByRealPath: (realPath: string, cache: Set<string>) => cache.has(realPath), trackedShouldApplyRuleCount += 1;
createContentHash: (content: string) => `hash:${content}`, return { applies: true, reason: "matched" };
isDuplicateByContentHash: (hash: string, cache: Set<string>) => cache.has(hash), },
}); isDuplicateByRealPath: (realPath: string, cache: Set<string>) =>
cache.has(realPath),
createContentHash: (content: string) => `hash:${content}`,
isDuplicateByContentHash: (hash: string, cache: Set<string>) =>
cache.has(hash),
});
} }
function getInjectedRulesPath(sessionID: string): string { function getInjectedRulesPath(sessionID: string): string {
return join(RULES_INJECTOR_STORAGE, `${sessionID}.json`); return join(RULES_INJECTOR_STORAGE, `${sessionID}.json`);
} }
describe("createRuleInjectionProcessor", () => { describe("createRuleInjectionProcessor", () => {
let testRoot: string; let testRoot: string;
let projectRoot: string; let projectRoot: string;
let homeRoot: string; let homeRoot: string;
let targetFile: string; let targetFile: string;
let ruleFile: string; let ruleFile: string;
let ruleRealPath: string; let ruleRealPath: string;
beforeEach(() => { beforeEach(() => {
clearParsedRuleCache(); clearParsedRuleCache();
testRoot = join(tmpdir(), `rules-injector-injector-${Date.now()}`); testRoot = join(tmpdir(), `rules-injector-injector-${Date.now()}`);
projectRoot = join(testRoot, "project"); projectRoot = join(testRoot, "project");
homeRoot = join(testRoot, "home"); homeRoot = join(testRoot, "home");
targetFile = join(projectRoot, "src", "index.ts"); targetFile = join(projectRoot, "src", "index.ts");
ruleFile = join( ruleFile = join(
projectRoot, projectRoot,
".github", ".github",
"instructions", "instructions",
"typescript.instructions.md" "typescript.instructions.md",
); );
mkdirSync(join(projectRoot, ".git"), { recursive: true }); mkdirSync(join(projectRoot, ".git"), { recursive: true });
mkdirSync(join(projectRoot, "src"), { recursive: true }); mkdirSync(join(projectRoot, "src"), { recursive: true });
mkdirSync(join(projectRoot, ".github", "instructions"), { recursive: true }); mkdirSync(join(projectRoot, ".github", "instructions"), {
mkdirSync(homeRoot, { recursive: true }); recursive: true,
});
mkdirSync(homeRoot, { recursive: true });
writeFileSync(targetFile, "export const value = 1;\n"); writeFileSync(targetFile, "export const value = 1;\n");
writeFileSync(ruleFile, "rule-content\n"); writeFileSync(ruleFile, "rule-content\n");
ruleRealPath = fs.realpathSync(ruleFile); ruleRealPath = fs.realpathSync(ruleFile);
trackedRulePath = ruleFile; trackedRulePath = ruleFile;
statSnapshots = []; statSnapshots = [];
trackedReadFileCount = 0; trackedReadFileCount = 0;
mockedHomeDir = homeRoot; trackedShouldApplyRuleCount = 0;
}); mockedHomeDir = homeRoot;
});
afterEach(() => { afterEach(() => {
clearParsedRuleCache(); clearParsedRuleCache();
if (fs.existsSync(testRoot)) { if (fs.existsSync(testRoot)) {
rmSync(testRoot, { recursive: true, force: true }); rmSync(testRoot, { recursive: true, force: true });
} }
}); });
it("reads and parses same file once when stat is unchanged", async () => { it("reads and parses same file once when stat is unchanged", async () => {
// given // given
statSnapshots = [ statSnapshots = [
{ mtimeMs: 1000, size: 13 }, { mtimeMs: 1000, size: 13 },
{ mtimeMs: 1000, size: 13 }, { mtimeMs: 1000, size: 13 },
]; ];
const processor = await createProcessor(projectRoot); const processor = await createProcessor(projectRoot);
// when // when
await processor.processFilePathForInjection(targetFile, "session-1", createOutput()); await processor.processFilePathForInjection(
await processor.processFilePathForInjection(targetFile, "session-2", createOutput()); targetFile,
"session-1",
createOutput(),
);
await processor.processFilePathForInjection(
targetFile,
"session-2",
createOutput(),
);
// then // then
expect(trackedReadFileCount).toBe(1); expect(trackedReadFileCount).toBe(1);
}); });
it("re-reads file when mtime changes", async () => { it("re-reads file when mtime changes", async () => {
// given // given
statSnapshots = [ statSnapshots = [
{ mtimeMs: 1000, size: 13 }, { mtimeMs: 1000, size: 13 },
{ mtimeMs: 2000, size: 13 }, { mtimeMs: 2000, size: 13 },
]; ];
const processor = await createProcessor(projectRoot); const processor = await createProcessor(projectRoot);
// when // when
await processor.processFilePathForInjection(targetFile, "session-1", createOutput()); await processor.processFilePathForInjection(
await processor.processFilePathForInjection(targetFile, "session-2", createOutput()); targetFile,
"session-1",
createOutput(),
);
await processor.processFilePathForInjection(
targetFile,
"session-2",
createOutput(),
);
// then // then
expect(trackedReadFileCount).toBe(2); expect(trackedReadFileCount).toBe(2);
}); });
it("re-reads file when size changes", async () => { it("re-reads file when size changes", async () => {
// given // given
statSnapshots = [ statSnapshots = [
{ mtimeMs: 1000, size: 13 }, { mtimeMs: 1000, size: 13 },
{ mtimeMs: 1000, size: 21 }, { mtimeMs: 1000, size: 21 },
]; ];
const processor = await createProcessor(projectRoot); const processor = await createProcessor(projectRoot);
// when // when
await processor.processFilePathForInjection(targetFile, "session-1", createOutput()); await processor.processFilePathForInjection(
await processor.processFilePathForInjection(targetFile, "session-2", createOutput()); targetFile,
"session-1",
createOutput(),
);
await processor.processFilePathForInjection(
targetFile,
"session-2",
createOutput(),
);
// then // then
expect(trackedReadFileCount).toBe(2); expect(trackedReadFileCount).toBe(2);
}); });
it("does not cache oversized parsed rule bodies", async () => { it("reuses match decision when stat fingerprint and target are unchanged", async () => {
// given // given
const largeBody = "x".repeat(70 * 1024); statSnapshots = [
writeFileSync(ruleFile, largeBody); { mtimeMs: 1000, size: 13 },
statSnapshots = [ { mtimeMs: 1000, size: 13 },
{ mtimeMs: 1000, size: largeBody.length }, ];
{ mtimeMs: 1000, size: largeBody.length }, const processor = await createProcessor(projectRoot);
];
const processor = await createProcessor(projectRoot);
// when // when
await processor.processFilePathForInjection(targetFile, "session-1", createOutput()); await processor.processFilePathForInjection(
await processor.processFilePathForInjection(targetFile, "session-2", createOutput()); targetFile,
"session-1",
createOutput(),
);
await processor.processFilePathForInjection(
targetFile,
"session-2",
createOutput(),
);
// then // then
expect(trackedReadFileCount).toBe(2); expect(trackedShouldApplyRuleCount).toBe(1);
expect(getParsedRuleCacheStats()).toEqual({ entries: 0, bodyBytes: 0 }); });
});
it("does not save injected rules when all candidates are already cached", async () => { it("re-evaluates match decision when stat fingerprint changes", async () => {
// given // given
const sessionID = `dirty-no-new-${Date.now()}`; statSnapshots = [
const injectedPath = getInjectedRulesPath(sessionID); { mtimeMs: 1000, size: 13 },
if (fs.existsSync(injectedPath)) { { mtimeMs: 2000, size: 13 },
fs.unlinkSync(injectedPath); ];
} const processor = await createProcessor(projectRoot);
const { createRuleInjectionProcessor } = await import("./injector"); // when
const processor = createRuleInjectionProcessor({ await processor.processFilePathForInjection(
workspaceDirectory: projectRoot, targetFile,
truncator: { "session-1",
truncate: async (_sessionID: string, content: string) => ({ createOutput(),
result: content, );
truncated: false, await processor.processFilePathForInjection(
}), targetFile,
}, "session-2",
getSessionCache: () => ({ createOutput(),
contentHashes: new Set<string>(), );
realPaths: new Set<string>([ruleRealPath]),
}),
});
// when // then
await processor.processFilePathForInjection(targetFile, sessionID, createOutput()); expect(trackedShouldApplyRuleCount).toBe(2);
});
// then it("keeps match decisions separate for different target files", async () => {
expect(fs.existsSync(injectedPath)).toBe(false); // given
}); const secondTargetFile = join(projectRoot, "src", "other.ts");
writeFileSync(secondTargetFile, "export const other = 2;\n");
statSnapshots = [
{ mtimeMs: 1000, size: 13 },
{ mtimeMs: 1000, size: 13 },
];
const processor = await createProcessor(projectRoot);
it("saves injected rules when a new rule is added", async () => { // when
// given await processor.processFilePathForInjection(
const sessionID = `dirty-new-${Date.now()}`; targetFile,
const injectedPath = getInjectedRulesPath(sessionID); "session-1",
if (fs.existsSync(injectedPath)) { createOutput(),
fs.unlinkSync(injectedPath); );
} await processor.processFilePathForInjection(
const processor = await createProcessor(projectRoot); secondTargetFile,
"session-2",
createOutput(),
);
// when // then
await processor.processFilePathForInjection(targetFile, sessionID, createOutput()); expect(trackedShouldApplyRuleCount).toBe(2);
});
// then it("does not cache oversized parsed rule bodies", async () => {
expect(fs.existsSync(injectedPath)).toBe(true); // given
const largeBody = "x".repeat(70 * 1024);
writeFileSync(ruleFile, largeBody);
statSnapshots = [
{ mtimeMs: 1000, size: largeBody.length },
{ mtimeMs: 1000, size: largeBody.length },
];
const processor = await createProcessor(projectRoot);
if (fs.existsSync(injectedPath)) { // when
fs.unlinkSync(injectedPath); await processor.processFilePathForInjection(
} targetFile,
}); "session-1",
createOutput(),
);
await processor.processFilePathForInjection(
targetFile,
"session-2",
createOutput(),
);
it("falls back to direct read and parse when statSync throws", async () => { // then
// given expect(trackedReadFileCount).toBe(2);
statSnapshots = [new Error("stat failed"), new Error("stat failed")]; expect(getParsedRuleCacheStats()).toEqual({ entries: 0, bodyBytes: 0 });
const processor = await createProcessor(projectRoot); });
// when it("does not save injected rules when all candidates are already cached", async () => {
await processor.processFilePathForInjection(targetFile, "session-1", createOutput()); // given
await processor.processFilePathForInjection(targetFile, "session-2", createOutput()); const sessionID = `dirty-no-new-${Date.now()}`;
const injectedPath = getInjectedRulesPath(sessionID);
if (fs.existsSync(injectedPath)) {
fs.unlinkSync(injectedPath);
}
// then const { createRuleInjectionProcessor } = await import("./injector");
expect(trackedReadFileCount).toBe(2); const processor = createRuleInjectionProcessor({
}); workspaceDirectory: projectRoot,
truncator: {
truncate: async (_sessionID: string, content: string) => ({
result: content,
truncated: false,
}),
},
getSessionCache: () => ({
contentHashes: new Set<string>(),
realPaths: new Set<string>([ruleRealPath]),
}),
});
// when
await processor.processFilePathForInjection(
targetFile,
sessionID,
createOutput(),
);
// then
expect(fs.existsSync(injectedPath)).toBe(false);
});
it("saves injected rules when a new rule is added", async () => {
// given
const sessionID = `dirty-new-${Date.now()}`;
const injectedPath = getInjectedRulesPath(sessionID);
if (fs.existsSync(injectedPath)) {
fs.unlinkSync(injectedPath);
}
const processor = await createProcessor(projectRoot);
// when
await processor.processFilePathForInjection(
targetFile,
sessionID,
createOutput(),
);
// then
expect(fs.existsSync(injectedPath)).toBe(true);
if (fs.existsSync(injectedPath)) {
fs.unlinkSync(injectedPath);
}
});
it("falls back to direct read and parse when statSync throws", async () => {
// given
statSnapshots = [new Error("stat failed"), new Error("stat failed")];
const processor = await createProcessor(projectRoot);
// when
await processor.processFilePathForInjection(
targetFile,
"session-1",
createOutput(),
);
await processor.processFilePathForInjection(
targetFile,
"session-2",
createOutput(),
);
// then
expect(trackedReadFileCount).toBe(2);
expect(trackedShouldApplyRuleCount).toBe(2);
});
}); });
+283 -163
View File
@@ -4,10 +4,10 @@ import { relative, resolve } from "node:path";
import { findProjectRoot, findRuleFiles } from "./finder"; import { findProjectRoot, findRuleFiles } from "./finder";
import type { FindRuleFilesOptions } from "./rule-file-finder"; import type { FindRuleFilesOptions } from "./rule-file-finder";
import { import {
createContentHash, createContentHash,
isDuplicateByContentHash, isDuplicateByContentHash,
isDuplicateByRealPath, isDuplicateByRealPath,
shouldApplyRule, shouldApplyRule,
} from "./matcher"; } from "./matcher";
import { parseRuleFrontmatter } from "./parser"; import { parseRuleFrontmatter } from "./parser";
import { saveInjectedRules } from "./storage"; import { saveInjectedRules } from "./storage";
@@ -16,216 +16,336 @@ import type { RuleScanCache } from "./rule-scan-cache";
import type { RuleMetadata } from "./types"; import type { RuleMetadata } from "./types";
type ToolExecuteOutput = { type ToolExecuteOutput = {
title: string; title: string;
output: string; output: string;
metadata: unknown; metadata: unknown;
}; };
type RuleToInject = { type RuleToInject = {
relativePath: string; relativePath: string;
matchReason: string; matchReason: string;
content: string; content: string;
distance: number; distance: number;
}; };
type DynamicTruncator = { type DynamicTruncator = {
truncate: ( truncate: (
sessionID: string, sessionID: string,
content: string content: string,
) => Promise<{ result: string; truncated: boolean }>; ) => Promise<{ result: string; truncated: boolean }>;
}; };
type RuleFileReader = (path: string, encoding: "utf-8") => string; type RuleFileReader = (path: string, encoding: "utf-8") => string;
interface ParsedRuleEntry { interface ParsedRuleEntry {
mtimeMs: number; mtimeMs: number;
size: number; size: number;
metadata: RuleMetadata; metadata: RuleMetadata;
body: string; body: string;
} }
type ParsedRule = {
metadata: RuleMetadata;
body: string;
statFingerprint: string | null;
};
type MatchDecisionCache = Map<string, string | null>;
export interface ParsedRuleCacheStats { export interface ParsedRuleCacheStats {
entries: number; entries: number;
bodyBytes: number; bodyBytes: number;
} }
const MAX_PARSED_RULE_CACHE_ENTRIES = 256; const MAX_PARSED_RULE_CACHE_ENTRIES = 256;
const MAX_PARSED_RULE_CACHE_BODY_BYTES = 64 * 1024; const MAX_PARSED_RULE_CACHE_BODY_BYTES = 64 * 1024;
const MAX_MATCH_DECISION_CACHE_ENTRIES = 4096;
const parsedRuleCache = new Map<string, ParsedRuleEntry>(); const parsedRuleCache = new Map<string, ParsedRuleEntry>();
export function clearParsedRuleCache(): void { export function clearParsedRuleCache(): void {
parsedRuleCache.clear(); parsedRuleCache.clear();
} }
export function getParsedRuleCacheStats(): ParsedRuleCacheStats { export function getParsedRuleCacheStats(): ParsedRuleCacheStats {
let bodyBytes = 0; let bodyBytes = 0;
for (const entry of parsedRuleCache.values()) { for (const entry of parsedRuleCache.values()) {
bodyBytes += Buffer.byteLength(entry.body, "utf8"); bodyBytes += Buffer.byteLength(entry.body, "utf8");
} }
return { entries: parsedRuleCache.size, bodyBytes }; return { entries: parsedRuleCache.size, bodyBytes };
} }
function setParsedRuleCacheEntry(realPath: string, entry: ParsedRuleEntry): void { function setParsedRuleCacheEntry(
if (Buffer.byteLength(entry.body, "utf8") > MAX_PARSED_RULE_CACHE_BODY_BYTES) return; realPath: string,
if (parsedRuleCache.size >= MAX_PARSED_RULE_CACHE_ENTRIES) { entry: ParsedRuleEntry,
const oldestRealPath = parsedRuleCache.keys().next().value; ): void {
if (oldestRealPath !== undefined) { if (Buffer.byteLength(entry.body, "utf8") > MAX_PARSED_RULE_CACHE_BODY_BYTES)
parsedRuleCache.delete(oldestRealPath); return;
} if (parsedRuleCache.size >= MAX_PARSED_RULE_CACHE_ENTRIES) {
} const oldestRealPath = parsedRuleCache.keys().next().value;
parsedRuleCache.set(realPath, entry); if (oldestRealPath !== undefined) {
parsedRuleCache.delete(oldestRealPath);
}
}
parsedRuleCache.set(realPath, entry);
} }
function resolveFilePath( function resolveFilePath(
workspaceDirectory: string, workspaceDirectory: string,
path: string path: string,
): string | null { ): string | null {
if (!path) return null; if (!path) return null;
if (path.startsWith("/")) return path; if (path.startsWith("/")) return path;
return resolve(workspaceDirectory, path); return resolve(workspaceDirectory, path);
} }
export function createRuleInjectionProcessor(deps: { export function createRuleInjectionProcessor(deps: {
workspaceDirectory: string; workspaceDirectory: string;
truncator: DynamicTruncator; truncator: DynamicTruncator;
getSessionCache: (sessionID: string) => SessionInjectedRulesCache; getSessionCache: (sessionID: string) => SessionInjectedRulesCache;
getSessionRuleScanCache?: (sessionID: string) => RuleScanCache; getSessionRuleScanCache?: (sessionID: string) => RuleScanCache;
ruleFinderOptions?: FindRuleFilesOptions; ruleFinderOptions?: FindRuleFilesOptions;
readFileSync?: RuleFileReader; readFileSync?: RuleFileReader;
statSync?: typeof statSync; statSync?: typeof statSync;
homedir?: typeof homedir; homedir?: typeof homedir;
shouldApplyRule?: typeof shouldApplyRule; shouldApplyRule?: typeof shouldApplyRule;
isDuplicateByRealPath?: typeof isDuplicateByRealPath; isDuplicateByRealPath?: typeof isDuplicateByRealPath;
createContentHash?: typeof createContentHash; createContentHash?: typeof createContentHash;
isDuplicateByContentHash?: typeof isDuplicateByContentHash; isDuplicateByContentHash?: typeof isDuplicateByContentHash;
saveInjectedRules?: typeof saveInjectedRules; saveInjectedRules?: typeof saveInjectedRules;
}): { }): {
processFilePathForInjection: ( processFilePathForInjection: (
filePath: string, filePath: string,
sessionID: string, sessionID: string,
output: ToolExecuteOutput output: ToolExecuteOutput,
) => Promise<void>; ) => Promise<void>;
} { } {
const { const {
workspaceDirectory, workspaceDirectory,
truncator, truncator,
getSessionCache, getSessionCache,
getSessionRuleScanCache, getSessionRuleScanCache,
ruleFinderOptions, ruleFinderOptions,
readFileSync: readRuleFileSync = readFileSync, readFileSync: readRuleFileSync = readFileSync,
statSync: statRuleSync = statSync, statSync: statRuleSync = statSync,
homedir: getHomeDir = homedir, homedir: getHomeDir = homedir,
shouldApplyRule: shouldApplyRuleImpl = shouldApplyRule, shouldApplyRule: shouldApplyRuleImpl = shouldApplyRule,
isDuplicateByRealPath: isDuplicateByRealPathImpl = isDuplicateByRealPath, isDuplicateByRealPath: isDuplicateByRealPathImpl = isDuplicateByRealPath,
createContentHash: createContentHashImpl = createContentHash, createContentHash: createContentHashImpl = createContentHash,
isDuplicateByContentHash: isDuplicateByContentHashImpl = isDuplicateByContentHash, isDuplicateByContentHash:
saveInjectedRules: saveInjectedRulesImpl = saveInjectedRules, isDuplicateByContentHashImpl = isDuplicateByContentHash,
} = deps; saveInjectedRules: saveInjectedRulesImpl = saveInjectedRules,
} = deps;
function getParsedRule(filePath: string, realPath: string): { metadata: RuleMetadata; body: string } { const matchDecisionCache: MatchDecisionCache = new Map();
try {
const stat = statRuleSync(filePath);
const cached = parsedRuleCache.get(realPath);
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) { function getParsedRule(filePath: string, realPath: string): ParsedRule {
return { metadata: cached.metadata, body: cached.body }; try {
} const stat = statRuleSync(filePath);
const statFingerprint = `${stat.mtimeMs}:${stat.size}`;
const cached = parsedRuleCache.get(realPath);
const rawContent = readRuleFileSync(filePath, "utf-8"); if (
const { metadata, body } = parseRuleFrontmatter(rawContent); cached &&
setParsedRuleCacheEntry(realPath, { cached.mtimeMs === stat.mtimeMs &&
mtimeMs: stat.mtimeMs, cached.size === stat.size
size: stat.size, ) {
metadata, return {
body, metadata: cached.metadata,
}); body: cached.body,
return { metadata, body }; statFingerprint,
} catch { };
const rawContent = readRuleFileSync(filePath, "utf-8"); }
return parseRuleFrontmatter(rawContent);
}
}
async function processFilePathForInjection( const rawContent = readRuleFileSync(filePath, "utf-8");
filePath: string, const { metadata, body } = parseRuleFrontmatter(rawContent);
sessionID: string, setParsedRuleCacheEntry(realPath, {
output: ToolExecuteOutput mtimeMs: stat.mtimeMs,
): Promise<void> { size: stat.size,
const resolved = resolveFilePath(workspaceDirectory, filePath); metadata,
if (!resolved) return; body,
});
return { metadata, body, statFingerprint };
} catch {
const rawContent = readRuleFileSync(filePath, "utf-8");
const { metadata, body } = parseRuleFrontmatter(rawContent);
return { metadata, body, statFingerprint: null };
}
}
const projectRoot = findProjectRoot(resolved); async function processFilePathForInjection(
const cache = getSessionCache(sessionID); filePath: string,
const ruleScanCache = getSessionRuleScanCache?.(sessionID); sessionID: string,
const home = getHomeDir(); output: ToolExecuteOutput,
): Promise<void> {
const resolved = resolveFilePath(workspaceDirectory, filePath);
if (!resolved) return;
const ruleFileCandidates = findRuleFiles( const projectRoot = findProjectRoot(resolved);
projectRoot, const cache = getSessionCache(sessionID);
home, const ruleScanCache = getSessionRuleScanCache?.(sessionID);
resolved, const home = getHomeDir();
ruleFinderOptions,
ruleScanCache,
);
const toInject: RuleToInject[] = [];
let dirty = false;
for (const candidate of ruleFileCandidates) { const ruleFileCandidates = findRuleFiles(
if (isDuplicateByRealPathImpl(candidate.realPath, cache.realPaths)) continue; projectRoot,
home,
resolved,
ruleFinderOptions,
ruleScanCache,
);
const toInject: RuleToInject[] = [];
let dirty = false;
try { for (const candidate of ruleFileCandidates) {
const { metadata, body } = getParsedRule( if (isDuplicateByRealPathImpl(candidate.realPath, cache.realPaths))
candidate.path, continue;
candidate.realPath
);
let matchReason: string; try {
if (candidate.isSingleFile) { const { metadata, body, statFingerprint } = getParsedRule(
matchReason = "copilot-instructions (always apply)"; candidate.path,
} else { candidate.realPath,
const matchResult = shouldApplyRuleImpl(metadata, resolved, projectRoot); );
if (!matchResult.applies) continue;
matchReason = matchResult.reason ?? "matched";
}
const contentHash = createContentHashImpl(body); let matchReason: string;
if (isDuplicateByContentHashImpl(contentHash, cache.contentHashes)) continue; if (candidate.isSingleFile) {
matchReason = "copilot-instructions (always apply)";
} else {
const cachedMatchReason = getCachedMatchReason(
matchDecisionCache,
projectRoot,
resolved,
candidate.realPath,
statFingerprint,
);
if (cachedMatchReason !== undefined) {
if (cachedMatchReason === null) continue;
matchReason = cachedMatchReason;
} else {
const matchResult = shouldApplyRuleImpl(
metadata,
resolved,
projectRoot,
);
if (!matchResult.applies) {
setCachedMatchReason(
matchDecisionCache,
projectRoot,
resolved,
candidate.realPath,
statFingerprint,
null,
);
continue;
}
matchReason = matchResult.reason ?? "matched";
setCachedMatchReason(
matchDecisionCache,
projectRoot,
resolved,
candidate.realPath,
statFingerprint,
matchReason,
);
}
}
const relativePath = projectRoot const contentHash = createContentHashImpl(body);
? relative(projectRoot, candidate.path) if (isDuplicateByContentHashImpl(contentHash, cache.contentHashes))
: candidate.path; continue;
toInject.push({ const relativePath = projectRoot
relativePath, ? relative(projectRoot, candidate.path)
matchReason, : candidate.path;
content: body,
distance: candidate.distance,
});
cache.realPaths.add(candidate.realPath); toInject.push({
cache.contentHashes.add(contentHash); relativePath,
dirty = true; matchReason,
} catch {} content: body,
} distance: candidate.distance,
});
if (toInject.length === 0) return; cache.realPaths.add(candidate.realPath);
cache.contentHashes.add(contentHash);
dirty = true;
} catch {}
}
toInject.sort((a, b) => a.distance - b.distance); if (toInject.length === 0) return;
for (const rule of toInject) { toInject.sort((a, b) => a.distance - b.distance);
const { result, truncated } = await truncator.truncate(
sessionID,
rule.content
);
const truncationNotice = truncated
? `\n\n[Note: Content was truncated to save context window space. For full context, please read the file directly: ${rule.relativePath}]`
: "";
output.output += `\n\n[Rule: ${rule.relativePath}]\n[Match: ${rule.matchReason}]\n${result}${truncationNotice}`;
}
if (dirty) { for (const rule of toInject) {
saveInjectedRulesImpl(sessionID, cache); const { result, truncated } = await truncator.truncate(
} sessionID,
} rule.content,
);
const truncationNotice = truncated
? `\n\n[Note: Content was truncated to save context window space. For full context, please read the file directly: ${rule.relativePath}]`
: "";
output.output += `\n\n[Rule: ${rule.relativePath}]\n[Match: ${rule.matchReason}]\n${result}${truncationNotice}`;
}
return { processFilePathForInjection }; if (dirty) {
saveInjectedRulesImpl(sessionID, cache);
}
}
return { processFilePathForInjection };
}
function getCachedMatchReason(
cache: MatchDecisionCache,
projectRoot: string | null,
resolvedFilePath: string,
realPath: string,
statFingerprint: string | null,
): string | null | undefined {
const cacheKey = matchDecisionCacheKey(
projectRoot,
resolvedFilePath,
realPath,
statFingerprint,
);
if (cacheKey === null || !cache.has(cacheKey)) return undefined;
const cached = cache.get(cacheKey) ?? null;
cache.delete(cacheKey);
cache.set(cacheKey, cached);
return cached;
}
function setCachedMatchReason(
cache: MatchDecisionCache,
projectRoot: string | null,
resolvedFilePath: string,
realPath: string,
statFingerprint: string | null,
matchReason: string | null,
): void {
const cacheKey = matchDecisionCacheKey(
projectRoot,
resolvedFilePath,
realPath,
statFingerprint,
);
if (cacheKey === null) return;
if (cache.size >= MAX_MATCH_DECISION_CACHE_ENTRIES) {
const oldestCacheKey = cache.keys().next().value;
if (oldestCacheKey !== undefined) {
cache.delete(oldestCacheKey);
}
}
cache.set(cacheKey, matchReason);
}
function matchDecisionCacheKey(
projectRoot: string | null,
resolvedFilePath: string,
realPath: string,
statFingerprint: string | null,
): string | null {
if (statFingerprint === null) return null;
return [projectRoot ?? "", resolvedFilePath, realPath, statFingerprint].join(
"\0",
);
} }