Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export function registerCheckCommand(program: Command) {
.option("--staged", "Only check git-staged files")
.option("--adr <id>", "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(
Expand Down Expand Up @@ -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) {
Expand Down
25 changes: 23 additions & 2 deletions src/engine/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,20 +180,41 @@ function createRuleContext(
export async function runChecks(
projectRoot: string,
loadedAdrs: LoadedAdr[],
options: { staged?: boolean } = {}
options: { staged?: boolean; files?: string[] } = {}
): Promise<CheckResult> {
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<string> | 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
Expand Down
36 changes: 36 additions & 0 deletions tests/integration/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading