Merge pull request #4196 from code-yeongyu/fix/rules-core-fallback-to-workspace-when-no-project-root
fix(rules-core): fall back to workspace directory when no project root marker is found
This commit is contained in:
@@ -15,21 +15,37 @@ export function findRuleFiles(
|
||||
): RuleFileCandidate[] {
|
||||
const startDir = dirname(resolve(currentFile));
|
||||
const skipClaudeUserRules = options?.skipClaudeUserRules ?? false;
|
||||
const cacheKey = [projectRoot ?? "", startDir, skipClaudeUserRules ? "1" : "0"].join("\0");
|
||||
const effectiveProjectRoot = resolveEffectiveProjectRoot(
|
||||
projectRoot,
|
||||
options?.workspaceDirectory,
|
||||
startDir,
|
||||
);
|
||||
const cacheKey = [projectRoot ?? "", effectiveProjectRoot, startDir, skipClaudeUserRules ? "1" : "0"].join(
|
||||
"\0",
|
||||
);
|
||||
const cached = cache?.get(cacheKey);
|
||||
if (cached) return [...cached];
|
||||
const candidates: RuleFileCandidate[] = [];
|
||||
const seenRealPaths = new Set<string>();
|
||||
if (projectRoot) {
|
||||
addProjectRuleCandidates(projectRoot, startDir, candidates, seenRealPaths, cache);
|
||||
addProjectSingleFileCandidates(projectRoot, candidates, seenRealPaths);
|
||||
}
|
||||
addProjectRuleCandidates(effectiveProjectRoot, startDir, candidates, seenRealPaths, cache);
|
||||
addProjectSingleFileCandidates(effectiveProjectRoot, candidates, seenRealPaths);
|
||||
addUserRuleCandidates(homeDir || homedir(), skipClaudeUserRules, candidates, seenRealPaths, cache);
|
||||
const sorted = sortCandidates(candidates);
|
||||
cache?.set(cacheKey, sorted);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function resolveEffectiveProjectRoot(
|
||||
projectRoot: string | null,
|
||||
workspaceDirectory: string | undefined,
|
||||
startDir: string,
|
||||
): string {
|
||||
if (projectRoot) return projectRoot;
|
||||
if (!workspaceDirectory) return startDir;
|
||||
const workspaceRoot = resolve(workspaceDirectory);
|
||||
return isSameOrChildPath(startDir, workspaceRoot) ? workspaceRoot : startDir;
|
||||
}
|
||||
|
||||
function addProjectRuleCandidates(
|
||||
projectRoot: string,
|
||||
startDir: string,
|
||||
|
||||
@@ -62,6 +62,29 @@ describe("rules-core", () => {
|
||||
expect(found.map((rule) => rule.relativePath)).not.toContain(".sisyphus/rules/sisyphus.md");
|
||||
});
|
||||
|
||||
it("#given a workspace directory has no project marker (no .git, no package.json, etc.) AND contains .omo/rules/ #when findRuleFiles is called #then the .omo/rules/ files are still discovered", () => {
|
||||
// given
|
||||
const root = createTestRoot("rules-core-markerless-workspace");
|
||||
const homeDir = join(root, "home");
|
||||
const sourceDir = join(root, "src");
|
||||
const ruleFile = join(root, ".omo", "rules", "test-rule.md");
|
||||
const currentFile = join(sourceDir, "index.ts");
|
||||
mkdirSync(join(root, ".omo", "rules"), { recursive: true });
|
||||
mkdirSync(homeDir, { recursive: true });
|
||||
mkdirSync(sourceDir, { recursive: true });
|
||||
writeFileSync(ruleFile, "markerless workspace rule");
|
||||
writeFileSync(currentFile, "export {};");
|
||||
const projectRoot = findProjectRoot(currentFile);
|
||||
const options = { skipClaudeUserRules: false, workspaceDirectory: root };
|
||||
|
||||
// when
|
||||
const found = findRuleFiles(projectRoot, homeDir, currentFile, options);
|
||||
|
||||
// then
|
||||
expect(projectRoot).toBeNull();
|
||||
expect(found.map((rule) => rule.path)).toContain(ruleFile);
|
||||
});
|
||||
|
||||
it("#given frontmatter aliases and negative glob #when matching #then honors applyTo paths and exclusions", () => {
|
||||
// given
|
||||
const { metadata } = parseRuleFrontmatter(`---\npaths: ["src/**/*.ts"]\napplyTo:\n - "!src/**/*.test.ts"\n---\nRule\n`);
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface RuleScanCache {
|
||||
|
||||
export interface FindRuleFilesOptions {
|
||||
readonly skipClaudeUserRules?: boolean;
|
||||
readonly workspaceDirectory?: string;
|
||||
}
|
||||
|
||||
export interface AgentsMdCache {
|
||||
|
||||
@@ -339,6 +339,7 @@ describe("createRuleInjectionProcessor", () => {
|
||||
contentHashes: new Set<string>(),
|
||||
realPaths: new Set<string>([ruleRealPath]),
|
||||
}),
|
||||
homedir: () => homeRoot,
|
||||
});
|
||||
|
||||
// when
|
||||
|
||||
@@ -137,6 +137,9 @@ export function createRuleInjectionProcessor(deps: {
|
||||
} = deps;
|
||||
|
||||
const matchDecisionCache: MatchDecisionCache = new Map();
|
||||
const finderOptions: FindRuleFilesOptions = ruleFinderOptions
|
||||
? { ...ruleFinderOptions, workspaceDirectory }
|
||||
: { workspaceDirectory };
|
||||
|
||||
function getParsedRule(filePath: string, realPath: string): ParsedRule {
|
||||
try {
|
||||
@@ -189,7 +192,7 @@ export function createRuleInjectionProcessor(deps: {
|
||||
projectRoot,
|
||||
home,
|
||||
resolved,
|
||||
ruleFinderOptions,
|
||||
finderOptions,
|
||||
ruleScanCache,
|
||||
);
|
||||
const toInject: RuleToInject[] = [];
|
||||
|
||||
Reference in New Issue
Block a user