mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
feat(lint): --exclude flag for mixed-content repos (takeover of #2649)
Adds --exclude=a,b (and LintOpts.exclude) so mixed-content repos can skip software trees and repo metafiles by basename when collecting pages. The only built-in default is node_modules — vendored dependency trees are never knowledge pages; dot/underscore entries were already skipped by the walk. Diverges from #2649 deliberately: the original hardcoded an opinionated default list (README.md, CHANGELOG.md, CLAUDE.md, test/ dirs at any depth, plus fork-specific filenames), which silently changed lint counts for every existing repo. Those are repo policy — pass --exclude. Co-authored-by: ryangu00 <ryangu00@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
ryangu00
Claude Fable 5
parent
216116912a
commit
6da0664c7c
+43
-8
@@ -378,15 +378,30 @@ async function resolveLintContentSanity(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Directories never containing knowledge pages, skipped by default.
|
||||
* Deliberately tiny: only vendored dependency trees qualify. Anything
|
||||
* more opinionated (README.md, CHANGELOG.md, test/) is repo policy —
|
||||
* callers opt in via `--exclude` / `LintOpts.exclude`. Dot- and
|
||||
* underscore-prefixed entries are already skipped by the walk.
|
||||
*/
|
||||
const DEFAULT_LINT_EXCLUDE_DIRS = new Set(['node_modules']);
|
||||
|
||||
/** Collect markdown files from a directory */
|
||||
function collectPages(dir: string): string[] {
|
||||
function collectPages(dir: string, extraExcludes: string[] = []): string[] {
|
||||
const extra = new Set(extraExcludes);
|
||||
const pages: string[] = [];
|
||||
function walk(d: string) {
|
||||
for (const entry of readdirSync(d)) {
|
||||
if (entry.startsWith('.') || entry.startsWith('_')) continue;
|
||||
const full = join(d, entry);
|
||||
if (lstatSync(full).isDirectory()) walk(full);
|
||||
else if (entry.endsWith('.md')) pages.push(full);
|
||||
if (lstatSync(full).isDirectory()) {
|
||||
if (DEFAULT_LINT_EXCLUDE_DIRS.has(entry) || extra.has(entry)) continue;
|
||||
walk(full);
|
||||
} else if (entry.endsWith('.md')) {
|
||||
if (extra.has(entry)) continue;
|
||||
pages.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(dir);
|
||||
@@ -414,6 +429,13 @@ export interface LintOpts {
|
||||
* yields + checks this every 200 pages.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
/**
|
||||
* #2649: extra dir/file basenames to skip while collecting pages, in
|
||||
* addition to node_modules and dot/underscore entries. For mixed-content
|
||||
* repos (knowledge pages alongside software trees). Ignored for
|
||||
* single-file targets.
|
||||
*/
|
||||
exclude?: string[];
|
||||
}
|
||||
|
||||
export interface LintResult {
|
||||
@@ -440,7 +462,7 @@ export async function runLintCore(opts: LintOpts): Promise<LintResult> {
|
||||
}
|
||||
|
||||
const isSingleFile = statSync(opts.target).isFile();
|
||||
const pages = isSingleFile ? [opts.target] : collectPages(opts.target);
|
||||
const pages = isSingleFile ? [opts.target] : collectPages(opts.target, opts.exclude ?? []);
|
||||
|
||||
// Resolve content-sanity config once for this lint run (D1: lift DB
|
||||
// config when reachable). Caller can pre-pass via opts.contentSanity
|
||||
@@ -491,14 +513,27 @@ export async function runLintCore(opts: LintOpts): Promise<LintResult> {
|
||||
}
|
||||
|
||||
export async function runLint(args: string[]) {
|
||||
const target = args.find(a => !a.startsWith('--'));
|
||||
// #2649: --exclude=a,b or --exclude a,b — extra basenames to skip.
|
||||
const extraExcludes: string[] = [];
|
||||
const skipIdx = new Set<number>();
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a.startsWith('--exclude=')) {
|
||||
extraExcludes.push(...a.slice('--exclude='.length).split(',').map(s => s.trim()).filter(Boolean));
|
||||
} else if (a === '--exclude' && i + 1 < args.length) {
|
||||
extraExcludes.push(...args[i + 1].split(',').map(s => s.trim()).filter(Boolean));
|
||||
skipIdx.add(i + 1);
|
||||
}
|
||||
}
|
||||
const target = args.find((a, i) => !a.startsWith('--') && !skipIdx.has(i));
|
||||
const doFix = args.includes('--fix');
|
||||
const dryRun = args.includes('--dry-run');
|
||||
|
||||
if (!target) {
|
||||
console.error('Usage: gbrain lint <dir|file.md> [--fix] [--dry-run]');
|
||||
console.error('Usage: gbrain lint <dir|file.md> [--fix] [--dry-run] [--exclude a,b]');
|
||||
console.error(' --fix Auto-fix fixable issues (LLM preambles, code fences)');
|
||||
console.error(' --dry-run Preview fixes without writing');
|
||||
console.error(' --exclude Comma-separated dir/file basenames to skip (in addition to node_modules)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -510,7 +545,7 @@ export async function runLint(args: string[]) {
|
||||
// Single file or directory — print human detail as we go, then rely on
|
||||
// Core for the aggregate numbers at the end.
|
||||
const isSingleFile = statSync(target).isFile();
|
||||
const pages = isSingleFile ? [target] : collectPages(target);
|
||||
const pages = isSingleFile ? [target] : collectPages(target, extraExcludes);
|
||||
|
||||
// Progress on stderr. Stdout keeps the per-issue human output it always had.
|
||||
const { createProgress } = await import('../core/progress.ts');
|
||||
@@ -557,7 +592,7 @@ export async function runLint(args: string[]) {
|
||||
// produces canonical numbers for the summary line).
|
||||
// Pass contentSanity through so runLintCore skips its own resolve
|
||||
// (we already resolved once for the human-detail loop above).
|
||||
const result = await runLintCore({ target, fix: doFix, dryRun, contentSanity });
|
||||
const result = await runLintCore({ target, fix: doFix, dryRun, contentSanity, exclude: extraExcludes });
|
||||
console.log(`\n${result.pages_scanned} pages scanned. ${result.total_issues} issue(s) in ${result.pages_with_issues} page(s).`);
|
||||
if (doFix) {
|
||||
console.log(`${dryRun ? '(dry run) ' : ''}${result.total_fixed} auto-fixed.`);
|
||||
|
||||
@@ -116,3 +116,48 @@ describe('fixContent', () => {
|
||||
expect(fixed).toContain('# Title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('runLintCore exclude (takeover of #2649)', () => {
|
||||
const { mkdtempSync, rmSync, mkdirSync, writeFileSync } = require('node:fs') as typeof import('node:fs');
|
||||
const { tmpdir } = require('node:os') as typeof import('node:os');
|
||||
const { join } = require('node:path') as typeof import('node:path');
|
||||
const { runLintCore } = require('../src/commands/lint.ts') as typeof import('../src/commands/lint.ts');
|
||||
|
||||
const PAGE = '---\ntitle: T\ntype: note\ncreated: 2026-04-11\n---\n\n# T\n\nBody.\n';
|
||||
|
||||
function makeRepo(): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'gbrain-lint-excl-'));
|
||||
writeFileSync(join(dir, 'page.md'), PAGE);
|
||||
writeFileSync(join(dir, 'README.md'), PAGE);
|
||||
mkdirSync(join(dir, 'node_modules', 'dep'), { recursive: true });
|
||||
writeFileSync(join(dir, 'node_modules', 'dep', 'vendor.md'), PAGE);
|
||||
mkdirSync(join(dir, 'software'));
|
||||
writeFileSync(join(dir, 'software', 'notes.md'), PAGE);
|
||||
return dir;
|
||||
}
|
||||
|
||||
test('node_modules is excluded by default; nothing else is', async () => {
|
||||
const dir = makeRepo();
|
||||
try {
|
||||
const result = await runLintCore({ target: dir, contentSanity: { disabled: true } });
|
||||
// page.md + README.md + software/notes.md — vendor.md skipped.
|
||||
expect(result.pages_scanned).toBe(3);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('--exclude basenames skip dirs and files', async () => {
|
||||
const dir = makeRepo();
|
||||
try {
|
||||
const result = await runLintCore({
|
||||
target: dir,
|
||||
contentSanity: { disabled: true },
|
||||
exclude: ['software', 'README.md'],
|
||||
});
|
||||
expect(result.pages_scanned).toBe(1); // only page.md
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user