fix(skills): address QA round 11
QA11-1: Expand recursive-scan ignore set to skip common heavyweight folders (.venv, venv, Pods, bin, obj, .gradle, DerivedData, out) so the bounded scan is far less likely to exhaust its budget before reaching relevant nested project files. QA11-2: Remove the arbitrary 10-file cap from FastAPI dependency reads. All discovered requirements.txt / pyproject.toml files within the bounded scan are now checked, eliminating traversal-order dependence in multi-service repos. QA11-3: Normalize safe nested project markers from the recursive scan back into PROJECT_FILES markers (e.g. nested next.config.ts, manage.py, requirements.txt, prisma/schema.prisma, app/build.gradle) while keeping noisy root-only markers like package.json and generic build.gradle root-only. Add regression tests for these nested layouts and Android root-only exclusion behavior.
This commit is contained in:
parent
bc63161593
commit
18508c1129
2 changed files with 94 additions and 1 deletions
|
|
@ -226,6 +226,8 @@ const TEST_MARKERS = [
|
|||
const RECURSIVE_SCAN_IGNORED_DIRS = new Set([
|
||||
".git",
|
||||
"node_modules",
|
||||
".venv",
|
||||
"venv",
|
||||
"dist",
|
||||
"build",
|
||||
"coverage",
|
||||
|
|
@ -234,8 +236,27 @@ const RECURSIVE_SCAN_IGNORED_DIRS = new Set([
|
|||
"target",
|
||||
"vendor",
|
||||
".turbo",
|
||||
"Pods",
|
||||
"bin",
|
||||
"obj",
|
||||
".gradle",
|
||||
"DerivedData",
|
||||
"out",
|
||||
]) as ReadonlySet<string>;
|
||||
|
||||
/** Project file markers safe to detect recursively via suffix matching. */
|
||||
const ROOT_ONLY_PROJECT_FILES = new Set<string>([
|
||||
".github/workflows",
|
||||
"package.json",
|
||||
"Gemfile",
|
||||
"Makefile",
|
||||
"CMakeLists.txt",
|
||||
"build.gradle",
|
||||
"build.gradle.kts",
|
||||
"deno.json",
|
||||
"deno.jsonc",
|
||||
]);
|
||||
|
||||
const MAX_RECURSIVE_SCAN_FILES = 2000;
|
||||
const MAX_RECURSIVE_SCAN_DEPTH = 6;
|
||||
|
||||
|
|
@ -366,6 +387,16 @@ export function detectProjectSignals(basePath: string): ProjectSignals {
|
|||
// without walking the entire repo or diving into heavyweight folders.
|
||||
const scannedFiles = scanProjectFiles(basePath);
|
||||
|
||||
for (const file of PROJECT_FILES) {
|
||||
if (detectedFiles.includes(file) || ROOT_ONLY_PROJECT_FILES.has(file)) continue;
|
||||
if (scannedFiles.some((scannedFile) => matchesProjectFileMarker(scannedFile, file))) {
|
||||
pushUnique(detectedFiles, file);
|
||||
if (!primaryLanguage && LANGUAGE_MAP[file]) {
|
||||
primaryLanguage = LANGUAGE_MAP[file];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scannedFiles.some((file) => SQLITE_EXTENSIONS.some((ext) => file.endsWith(ext)))) {
|
||||
pushUnique(detectedFiles, "*.sqlite");
|
||||
}
|
||||
|
|
@ -402,7 +433,7 @@ export function detectProjectSignals(basePath: string): ProjectSignals {
|
|||
if (dependencyFiles.length > 0) {
|
||||
try {
|
||||
const depContent: string[] = [];
|
||||
for (const relativePath of dependencyFiles.slice(0, 10)) {
|
||||
for (const relativePath of dependencyFiles) {
|
||||
depContent.push(readBounded(join(basePath, relativePath), 64 * 1024));
|
||||
}
|
||||
const combined = depContent.join("\n").toLowerCase();
|
||||
|
|
@ -730,6 +761,14 @@ function pushUnique(arr: string[], value: string): void {
|
|||
if (!arr.includes(value)) arr.push(value);
|
||||
}
|
||||
|
||||
function matchesProjectFileMarker(scannedFile: string, marker: string): boolean {
|
||||
return (
|
||||
scannedFile === marker ||
|
||||
scannedFile.endsWith(`/${marker}`) ||
|
||||
scannedFile.endsWith(`\\${marker}`)
|
||||
);
|
||||
}
|
||||
|
||||
function scanProjectFiles(basePath: string): string[] {
|
||||
const files: string[] = [];
|
||||
const queue: Array<{ path: string; depth: number }> = [{ path: basePath, depth: 0 }];
|
||||
|
|
|
|||
|
|
@ -528,6 +528,18 @@ test("detectProjectSignals: Next.js project via next.config.ts", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: nested Next.js config via packages/web/next.config.ts", () => {
|
||||
const dir = makeTempDir("signals-nextjs-nested");
|
||||
try {
|
||||
mkdirSync(join(dir, "packages", "web"), { recursive: true });
|
||||
writeFileSync(join(dir, "packages", "web", "next.config.ts"), "export default {}", "utf-8");
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(signals.detectedFiles.includes("next.config.ts"), "should detect nested Next.js config");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: Flutter project via pubspec.yaml", () => {
|
||||
const dir = makeTempDir("signals-flutter");
|
||||
try {
|
||||
|
|
@ -552,6 +564,19 @@ test("detectProjectSignals: Django project via manage.py", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: nested Django manage.py", () => {
|
||||
const dir = makeTempDir("signals-django-nested");
|
||||
try {
|
||||
mkdirSync(join(dir, "services", "api"), { recursive: true });
|
||||
writeFileSync(join(dir, "services", "api", "manage.py"), "#!/usr/bin/env python", "utf-8");
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(signals.detectedFiles.includes("manage.py"), "should detect nested manage.py");
|
||||
assert.equal(signals.primaryLanguage, "python");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: Docker project via Dockerfile", () => {
|
||||
const dir = makeTempDir("signals-docker");
|
||||
try {
|
||||
|
|
@ -635,6 +660,21 @@ test("detectProjectSignals: Android project via app/build.gradle", () => {
|
|||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(signals.detectedFiles.includes("app/build.gradle"));
|
||||
assert.equal(signals.primaryLanguage, "java/kotlin");
|
||||
assert.ok(!signals.detectedFiles.includes("build.gradle"), "should not collapse Android app/build.gradle into generic build.gradle");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: nested app/build.gradle normalizes to Android marker", () => {
|
||||
const dir = makeTempDir("signals-android-nested");
|
||||
try {
|
||||
mkdirSync(join(dir, "apps", "mobile", "app"), { recursive: true });
|
||||
writeFileSync(join(dir, "apps", "mobile", "app", "build.gradle"), "apply plugin: 'com.android.application'", "utf-8");
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(signals.detectedFiles.includes("app/build.gradle"), "should detect nested Android app/build.gradle");
|
||||
assert.ok(!signals.detectedFiles.includes("build.gradle"), "should not emit generic build.gradle marker for nested Android modules");
|
||||
assert.equal(signals.primaryLanguage, "java/kotlin");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
|
|
@ -772,6 +812,20 @@ test("detectProjectSignals: FastAPI detected via nested service requirements.txt
|
|||
writeFileSync(join(dir, "services", "api", "requirements.txt"), "fastapi==0.115.0\n", "utf-8");
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(signals.detectedFiles.includes("dep:fastapi"), "should detect FastAPI in nested service requirements.txt");
|
||||
assert.ok(signals.detectedFiles.includes("requirements.txt"), "should normalize nested requirements.txt marker");
|
||||
assert.equal(signals.primaryLanguage, "python");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: nested Prisma schema normalizes to prisma/schema.prisma", () => {
|
||||
const dir = makeTempDir("signals-prisma-nested");
|
||||
try {
|
||||
mkdirSync(join(dir, "services", "api", "prisma"), { recursive: true });
|
||||
writeFileSync(join(dir, "services", "api", "prisma", "schema.prisma"), "datasource db { provider = \"sqlite\" }", "utf-8");
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(signals.detectedFiles.includes("prisma/schema.prisma"), "should detect nested Prisma schema");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue