diff --git a/packages/docs/src/content/docs/config/scan.mdx b/packages/docs/src/content/docs/config/scan.mdx
index 07f72a6c..2f2300c9 100644
--- a/packages/docs/src/content/docs/config/scan.mdx
+++ b/packages/docs/src/content/docs/config/scan.mdx
@@ -41,7 +41,7 @@ maxFileLines = 3000
maxChangedLines
number
-
Max changed lines to analyze after ignores are applied. Default: 10000.
+ Max changed lines to analyze for PR and diff-based scans after ignores are applied. Explicit local file-target runs do not enforce this budget. Default: 10000.
@@ -59,6 +59,7 @@ maxFileLines = 3000
-When a run exceeds `maxFiles` or `maxChangedLines`, Warden scans files in the
-existing diff order until the budget is exhausted and records the remaining
-files as skipped.
+When a PR or diff-based run exceeds `maxFiles` or `maxChangedLines`, Warden
+scans files in the existing diff order until the budget is exhausted and records
+the remaining files as skipped. Explicit local file-target runs still enforce
+`maxFiles`, but not `maxChangedLines`.
diff --git a/packages/warden/src/cli/context.ts b/packages/warden/src/cli/context.ts
index c331b070..fc0af12a 100644
--- a/packages/warden/src/cli/context.ts
+++ b/packages/warden/src/cli/context.ts
@@ -150,6 +150,7 @@ export async function buildFileEventContext(options: FileContextOptions): Promis
files,
},
diffContextSource: { type: 'working-tree' },
+ explicitFileTargets: true,
repoPath: cwd,
};
}
diff --git a/packages/warden/src/sdk/prepare.test.ts b/packages/warden/src/sdk/prepare.test.ts
index 3bb58799..7459c84d 100644
--- a/packages/warden/src/sdk/prepare.test.ts
+++ b/packages/warden/src/sdk/prepare.test.ts
@@ -5,7 +5,7 @@ import { join } from 'node:path';
import { afterEach, describe, it, expect } from 'vitest';
import { prepareFiles } from './prepare.js';
import type { EventContext, FileChange } from '../types/index.js';
-import { buildLocalEventContext } from '../cli/context.js';
+import { buildFileEventContext, buildLocalEventContext } from '../cli/context.js';
function makeContext(
files: {
@@ -283,6 +283,43 @@ describe('prepareFiles', () => {
});
});
+ it('does not apply changed-line budget to explicit file targets', async () => {
+ const repoPath = mkdtempSync(join(tmpdir(), 'warden-file-targets-'));
+ tempDirs.push(repoPath);
+ mkdirSync(join(repoPath, 'src'), { recursive: true });
+ writeFileSync(join(repoPath, 'src/one.ts'), 'one\ntwo\nthree');
+ writeFileSync(join(repoPath, 'src/two.ts'), 'one\ntwo\nthree');
+
+ const context = await buildFileEventContext({
+ patterns: ['src/*.ts'],
+ cwd: repoPath,
+ });
+
+ const result = prepareFiles(context, {
+ scan: { maxChangedLines: 3 },
+ });
+
+ expect(result.files.map((file) => file.filename)).toEqual(['src/one.ts', 'src/two.ts']);
+ expect(result.skippedFiles).toEqual([]);
+ });
+
+ it('keeps file-count budget for explicit file targets', () => {
+ const context = makeContext([
+ { filename: 'src/one.ts', patch: '@@ -0,0 +1,1 @@\n+one', additions: 1 },
+ { filename: 'src/two.ts', patch: '@@ -0,0 +1,1 @@\n+two', additions: 1 },
+ ]);
+ context.explicitFileTargets = true;
+
+ const result = prepareFiles(context, {
+ scan: { maxFiles: 1, maxChangedLines: 1 },
+ });
+
+ expect(result.files.map((file) => file.filename)).toEqual(['src/one.ts']);
+ expect(result.skippedFiles).toEqual([
+ { filename: 'src/two.ts', reason: 'limit:file_count' },
+ ]);
+ });
+
it('skips patchless files before applying changed-line budget', () => {
const context = makeContext([
{ filename: 'src/large-diff.ts', additions: 10 },
diff --git a/packages/warden/src/sdk/prepare.ts b/packages/warden/src/sdk/prepare.ts
index 84808b91..d79e9ca0 100644
--- a/packages/warden/src/sdk/prepare.ts
+++ b/packages/warden/src/sdk/prepare.ts
@@ -58,6 +58,7 @@ export function prepareFiles(
ignore: options.ignore,
scan: options.scan,
diffContextSource: context.diffContextSource,
+ enforceChangedLineBudget: context.explicitFileTargets !== true,
});
skippedFiles.push(...scanPolicy.skippedFiles);
diff --git a/packages/warden/src/sdk/scan-policy.ts b/packages/warden/src/sdk/scan-policy.ts
index 1150de3a..7e097966 100644
--- a/packages/warden/src/sdk/scan-policy.ts
+++ b/packages/warden/src/sdk/scan-policy.ts
@@ -83,6 +83,8 @@ export interface ScanPolicyOptions {
ignore?: IgnoreConfig;
scan?: ScanConfig;
diffContextSource?: DiffContextSource;
+ /** Whether to enforce PR/diff changed-line scan budgets. Defaults to true. */
+ enforceChangedLineBudget?: boolean;
}
export type ScannableFileChange = FileChange & { patch: string };
@@ -387,6 +389,7 @@ export function applyScanPolicy(
): ApplyScanPolicyResult {
const scan = effectiveScanConfig(options.scan);
const diffContextSource = options.diffContextSource ?? { type: 'working-tree' };
+ const enforceChangedLineBudget = options.enforceChangedLineBudget ?? true;
const skippedFiles: SkippedFile[] = [];
const eligible: ScannableFileChange[] = [];
@@ -418,7 +421,7 @@ export function applyScanPolicy(
}
const fileChangedLines = changedLines(file);
- if (consumedChangedLines + fileChangedLines > scan.maxChangedLines) {
+ if (enforceChangedLineBudget && consumedChangedLines + fileChangedLines > scan.maxChangedLines) {
skippedFiles.push({ filename: file.filename, reason: 'limit:changed_lines' });
continue;
}
diff --git a/packages/warden/src/triggers/matcher.test.ts b/packages/warden/src/triggers/matcher.test.ts
index f7976c9b..360588f3 100644
--- a/packages/warden/src/triggers/matcher.test.ts
+++ b/packages/warden/src/triggers/matcher.test.ts
@@ -460,10 +460,18 @@ describe('filterContextByPaths', () => {
});
it('preserves other context fields', () => {
- const result = filterContextByPaths(baseContext, { paths: ['src/**/*.ts'] });
- expect(result.eventType).toBe(baseContext.eventType);
- expect(result.repository).toBe(baseContext.repository);
- expect(result.repoPath).toBe(baseContext.repoPath);
+ const context: EventContext = {
+ ...baseContext,
+ diffContextSource: { type: 'working-tree' },
+ explicitFileTargets: true,
+ };
+
+ const result = filterContextByPaths(context, { paths: ['src/**/*.ts'] });
+ expect(result.eventType).toBe(context.eventType);
+ expect(result.repository).toBe(context.repository);
+ expect(result.repoPath).toBe(context.repoPath);
+ expect(result.diffContextSource).toBe(context.diffContextSource);
+ expect(result.explicitFileTargets).toBe(true);
expect(result.pullRequest!.title).toBe('Test PR');
});
});
diff --git a/packages/warden/src/types/index.ts b/packages/warden/src/types/index.ts
index 6aed9bb2..828cad55 100644
--- a/packages/warden/src/types/index.ts
+++ b/packages/warden/src/types/index.ts
@@ -446,6 +446,7 @@ export const EventContextSchema = z.object({
pullRequest: PullRequestContextSchema.optional(),
repoPath: z.string(),
diffContextSource: DiffContextSourceSchema.optional(),
+ explicitFileTargets: z.boolean().optional(),
});
export type EventContext = z.infer;