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[] {
|
): RuleFileCandidate[] {
|
||||||
const startDir = dirname(resolve(currentFile));
|
const startDir = dirname(resolve(currentFile));
|
||||||
const skipClaudeUserRules = options?.skipClaudeUserRules ?? false;
|
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);
|
const cached = cache?.get(cacheKey);
|
||||||
if (cached) return [...cached];
|
if (cached) return [...cached];
|
||||||
const candidates: RuleFileCandidate[] = [];
|
const candidates: RuleFileCandidate[] = [];
|
||||||
const seenRealPaths = new Set<string>();
|
const seenRealPaths = new Set<string>();
|
||||||
if (projectRoot) {
|
addProjectRuleCandidates(effectiveProjectRoot, startDir, candidates, seenRealPaths, cache);
|
||||||
addProjectRuleCandidates(projectRoot, startDir, candidates, seenRealPaths, cache);
|
addProjectSingleFileCandidates(effectiveProjectRoot, candidates, seenRealPaths);
|
||||||
addProjectSingleFileCandidates(projectRoot, candidates, seenRealPaths);
|
|
||||||
}
|
|
||||||
addUserRuleCandidates(homeDir || homedir(), skipClaudeUserRules, candidates, seenRealPaths, cache);
|
addUserRuleCandidates(homeDir || homedir(), skipClaudeUserRules, candidates, seenRealPaths, cache);
|
||||||
const sorted = sortCandidates(candidates);
|
const sorted = sortCandidates(candidates);
|
||||||
cache?.set(cacheKey, sorted);
|
cache?.set(cacheKey, sorted);
|
||||||
return 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(
|
function addProjectRuleCandidates(
|
||||||
projectRoot: string,
|
projectRoot: string,
|
||||||
startDir: string,
|
startDir: string,
|
||||||
|
|||||||
@@ -62,6 +62,29 @@ describe("rules-core", () => {
|
|||||||
expect(found.map((rule) => rule.relativePath)).not.toContain(".sisyphus/rules/sisyphus.md");
|
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", () => {
|
it("#given frontmatter aliases and negative glob #when matching #then honors applyTo paths and exclusions", () => {
|
||||||
// given
|
// given
|
||||||
const { metadata } = parseRuleFrontmatter(`---\npaths: ["src/**/*.ts"]\napplyTo:\n - "!src/**/*.test.ts"\n---\nRule\n`);
|
const { metadata } = parseRuleFrontmatter(`---\npaths: ["src/**/*.ts"]\napplyTo:\n - "!src/**/*.test.ts"\n---\nRule\n`);
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export interface RuleScanCache {
|
|||||||
|
|
||||||
export interface FindRuleFilesOptions {
|
export interface FindRuleFilesOptions {
|
||||||
readonly skipClaudeUserRules?: boolean;
|
readonly skipClaudeUserRules?: boolean;
|
||||||
|
readonly workspaceDirectory?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentsMdCache {
|
export interface AgentsMdCache {
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ describe("createRuleInjectionProcessor", () => {
|
|||||||
contentHashes: new Set<string>(),
|
contentHashes: new Set<string>(),
|
||||||
realPaths: new Set<string>([ruleRealPath]),
|
realPaths: new Set<string>([ruleRealPath]),
|
||||||
}),
|
}),
|
||||||
|
homedir: () => homeRoot,
|
||||||
});
|
});
|
||||||
|
|
||||||
// when
|
// when
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ export function createRuleInjectionProcessor(deps: {
|
|||||||
} = deps;
|
} = deps;
|
||||||
|
|
||||||
const matchDecisionCache: MatchDecisionCache = new Map();
|
const matchDecisionCache: MatchDecisionCache = new Map();
|
||||||
|
const finderOptions: FindRuleFilesOptions = ruleFinderOptions
|
||||||
|
? { ...ruleFinderOptions, workspaceDirectory }
|
||||||
|
: { workspaceDirectory };
|
||||||
|
|
||||||
function getParsedRule(filePath: string, realPath: string): ParsedRule {
|
function getParsedRule(filePath: string, realPath: string): ParsedRule {
|
||||||
try {
|
try {
|
||||||
@@ -189,7 +192,7 @@ export function createRuleInjectionProcessor(deps: {
|
|||||||
projectRoot,
|
projectRoot,
|
||||||
home,
|
home,
|
||||||
resolved,
|
resolved,
|
||||||
ruleFinderOptions,
|
finderOptions,
|
||||||
ruleScanCache,
|
ruleScanCache,
|
||||||
);
|
);
|
||||||
const toInject: RuleToInject[] = [];
|
const toInject: RuleToInject[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user