diff --git a/src/commands/check.ts b/src/commands/check.ts index 4fc716ce..5706056e 100644 --- a/src/commands/check.ts +++ b/src/commands/check.ts @@ -21,7 +21,8 @@ export function registerCheckCommand(program: Command) { .option("--staged", "Only check git-staged files") .option("--adr ", "Only check rules from a specific ADR") .option("--verbose", "Show passing rules and timing info") - .action(async (opts) => { + .argument("[files...]", "Only check rules relevant to these files") + .action(async (files, opts) => { const projectRoot = findProjectRoot(); if (!projectRoot) { logError( @@ -67,8 +68,19 @@ export function registerCheckCommand(program: Command) { process.exit(0); } + // Collect file paths from arguments and/or stdin pipe + let filterFiles: string[] = files ?? []; + if (!process.stdin.isTTY) { + const stdin = await new Response( + process.stdin as unknown as ReadableStream + ).text(); + const piped = stdin.trim().split(/\r?\n/).filter(Boolean); + filterFiles = [...filterFiles, ...piped]; + } + const result = await runChecks(projectRoot, loadedAdrs, { staged: opts.staged, + files: filterFiles.length > 0 ? filterFiles : undefined, }); if (opts.ci) { diff --git a/src/engine/runner.ts b/src/engine/runner.ts index 64f919a2..11540add 100644 --- a/src/engine/runner.ts +++ b/src/engine/runner.ts @@ -180,20 +180,41 @@ function createRuleContext( export async function runChecks( projectRoot: string, loadedAdrs: LoadedAdr[], - options: { staged?: boolean } = {} + options: { staged?: boolean; files?: string[] } = {} ): Promise { const startTime = performance.now(); const changedFiles = options.staged ? await getStagedFiles(projectRoot) : []; const results: RuleResult[] = []; + // Resolve user-specified files to relative paths for intersection + let filterFiles: Set | undefined; + if (options.files && options.files.length > 0) { + filterFiles = new Set( + options.files.map((f) => { + const absPath = safePath(projectRoot, f); + return relative(projectRoot, absPath).replaceAll("\\", "/"); + }) + ); + } + // Run ADRs in parallel const adrResults = await Promise.allSettled( loadedAdrs.map(async ({ adr, ruleSet }) => { - const scopedFiles = await resolveScopedFiles( + let scopedFiles = await resolveScopedFiles( projectRoot, adr.frontmatter.files ); + // When files are specified, narrow scopedFiles to the intersection + if (filterFiles) { + scopedFiles = scopedFiles.filter((f) => filterFiles.has(f)); + } + + // Skip this ADR entirely if no specified files are in scope + if (filterFiles && scopedFiles.length === 0) { + return []; + } + const adrRuleResults: RuleResult[] = []; // Run rules within an ADR sequentially diff --git a/tests/integration/check.test.ts b/tests/integration/check.test.ts index b9264c64..841f0f31 100644 --- a/tests/integration/check.test.ts +++ b/tests/integration/check.test.ts @@ -238,6 +238,42 @@ describe("check integration", () => { expect(stdout).toContain("ms"); }); + test("file args → scopes checks to specified files", async () => { + scaffoldProject(dir); + mkdirSync(join(dir, "src"), { recursive: true }); + mkdirSync(join(dir, "docs"), { recursive: true }); + writeFileSync(join(dir, "src", "good.ts"), "const x = 1;\n"); + writeFileSync(join(dir, "src", "bad.ts"), 'console.log("bad");\n'); + writeFileSync(join(dir, "docs", "readme.md"), "# Hello\n"); + const noConsoleRule = `export default { rules: { "no-console": { description: "No console.log", async check(ctx) { + for (const f of ctx.scopedFiles) { for (const m of await ctx.grep(f, /console\\.log/)) ctx.report.violation({ message: "found", file: m.file, line: m.line }); } + } } } };`; + writeAdr( + dir, + "FILE-001.md", + makeAdr({ + id: "FILE-001", + title: "X", + rules: true, + files: ["src/**/*.ts"], + }) + ); + writeRules(dir, "FILE-001.rules.ts", noConsoleRule); + + const good = await runCli(["check", "--json", "src/good.ts"], dir); + expect(good.exitCode).toBe(0); + expect(JSON.parse(good.stdout).pass).toBe(true); + + const bad = await runCli(["check", "--json", "src/bad.ts"], dir); + expect(bad.exitCode).toBe(1); + expect(JSON.parse(bad.stdout).pass).toBe(false); + + // Out-of-scope file → ADR skipped + const oos = await runCli(["check", "--json", "docs/readme.md"], dir); + expect(oos.exitCode).toBe(0); + expect(JSON.parse(oos.stdout).pass).toBe(true); + }); + test("exit non-zero when no .archgate project found", async () => { // dir has no .archgate scaffold const { exitCode, stderr } = await runCli(["check"], dir);