From 18508c11299f8337f99a30fccbf1531bc4ec5c30 Mon Sep 17 00:00:00 2001 From: Derek Pearson Date: Sun, 22 Mar 2026 08:08:41 -0400 Subject: [PATCH] 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. --- src/resources/extensions/gsd/detection.ts | 41 +++++++++++++- .../extensions/gsd/tests/detection.test.ts | 54 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/detection.ts b/src/resources/extensions/gsd/detection.ts index 98cfe8689..ad363f5b6 100644 --- a/src/resources/extensions/gsd/detection.ts +++ b/src/resources/extensions/gsd/detection.ts @@ -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; +/** Project file markers safe to detect recursively via suffix matching. */ +const ROOT_ONLY_PROJECT_FILES = new Set([ + ".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 }]; diff --git a/src/resources/extensions/gsd/tests/detection.test.ts b/src/resources/extensions/gsd/tests/detection.test.ts index 9b5cd382c..3b989ed8a 100644 --- a/src/resources/extensions/gsd/tests/detection.test.ts +++ b/src/resources/extensions/gsd/tests/detection.test.ts @@ -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); }