fix(skills): address QA round 12
QA12-1: Replace generic Gradle/POM matching for Java & Spring Boot with a real framework marker () detected by scanning pom.xml and Gradle files for Spring Boot plugins/dependencies. This restores nested Gradle service detection without reintroducing Android false positives. QA12-2: Prevent standard Android projects (root build.gradle + app/build.gradle) from matching the Spring Boot pack. Spring Boot now requires the synthetic dependency marker, not generic build files. QA12-3: Harden FastAPI detection: - strip comments before matching - scan each dependency file independently - continue on per-file read errors instead of failing the whole scan Also add regression tests for comment-only FastAPI mentions, nested Spring Boot Gradle services, Android non-Spring Gradle projects, and Spring Boot pack matching via dep:spring-boot.
This commit is contained in:
parent
18508c1129
commit
c297fe2e34
4 changed files with 96 additions and 14 deletions
|
|
@ -430,19 +430,15 @@ export function detectProjectSignals(basePath: string): ProjectSignals {
|
|||
const dependencyFiles = scannedFiles.filter((file) =>
|
||||
file.endsWith("requirements.txt") || file.endsWith("pyproject.toml"),
|
||||
);
|
||||
if (dependencyFiles.length > 0) {
|
||||
try {
|
||||
const depContent: string[] = [];
|
||||
for (const relativePath of dependencyFiles) {
|
||||
depContent.push(readBounded(join(basePath, relativePath), 64 * 1024));
|
||||
}
|
||||
const combined = depContent.join("\n").toLowerCase();
|
||||
if (/\bfastapi\b/.test(combined)) {
|
||||
pushUnique(detectedFiles, "dep:fastapi");
|
||||
}
|
||||
} catch {
|
||||
// unreadable dependency files — skip framework scan
|
||||
}
|
||||
if (containsDependencyMarker(basePath, dependencyFiles, "fastapi")) {
|
||||
pushUnique(detectedFiles, "dep:fastapi");
|
||||
}
|
||||
|
||||
const springBootFiles = scannedFiles.filter((file) =>
|
||||
file.endsWith("pom.xml") || file.endsWith("build.gradle") || file.endsWith("build.gradle.kts"),
|
||||
);
|
||||
if (containsDependencyMarker(basePath, springBootFiles, "spring-boot")) {
|
||||
pushUnique(detectedFiles, "dep:spring-boot");
|
||||
}
|
||||
|
||||
// Git repo detection
|
||||
|
|
@ -769,6 +765,43 @@ function matchesProjectFileMarker(scannedFile: string, marker: string): boolean
|
|||
);
|
||||
}
|
||||
|
||||
function containsDependencyMarker(basePath: string, relativePaths: string[], marker: "fastapi" | "spring-boot"): boolean {
|
||||
for (const relativePath of relativePaths) {
|
||||
try {
|
||||
const raw = readBounded(join(basePath, relativePath), 64 * 1024);
|
||||
const content = stripDependencyComments(relativePath, raw).toLowerCase();
|
||||
if (marker === "fastapi" && /\bfastapi(?:[-_][a-z0-9]+)?\b/.test(content)) {
|
||||
return true;
|
||||
}
|
||||
if (marker === "spring-boot" && /(org\.springframework\.boot|spring-boot(?:-starter)?)/.test(content)) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// unreadable file — continue scanning other candidate files
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function stripDependencyComments(relativePath: string, content: string): string {
|
||||
if (relativePath.endsWith("requirements.txt")) {
|
||||
return content.replace(/^\s*#.*$/gm, "");
|
||||
}
|
||||
if (relativePath.endsWith("pyproject.toml")) {
|
||||
return content.replace(/^\s*#.*$/gm, "");
|
||||
}
|
||||
if (relativePath.endsWith("pom.xml")) {
|
||||
return content.replace(/<!--[\s\S]*?-->/g, "");
|
||||
}
|
||||
if (relativePath.endsWith("build.gradle") || relativePath.endsWith("build.gradle.kts")) {
|
||||
return content
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/^\s*\/\/.*$/gm, "");
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function scanProjectFiles(basePath: string): string[] {
|
||||
const files: string[] = [];
|
||||
const queue: Array<{ path: string; depth: number }> = [{ path: basePath, depth: 0 }];
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ export const SKILL_CATALOG: SkillPack[] = [
|
|||
description: "Spring Boot best practices, DI, RESTful APIs, JPA, testing, and security",
|
||||
repo: "github/awesome-copilot",
|
||||
skills: ["java-springboot"],
|
||||
matchFiles: ["pom.xml", "build.gradle", "build.gradle.kts"],
|
||||
matchFiles: ["dep:spring-boot"],
|
||||
},
|
||||
// ── .NET / C# ────────────────────────────────────────────────────────────
|
||||
{
|
||||
|
|
|
|||
|
|
@ -782,6 +782,17 @@ test("detectProjectSignals: FastAPI detected via pyproject.toml dependency", ()
|
|||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: FastAPI comments do not trigger dep:fastapi", () => {
|
||||
const dir = makeTempDir("signals-fastapi-comment");
|
||||
try {
|
||||
writeFileSync(join(dir, "requirements.txt"), "# maybe evaluate fastapi later\nflask==3.0\n", "utf-8");
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "comments should not trigger FastAPI detection");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: Django project does NOT get dep:fastapi marker", () => {
|
||||
const dir = makeTempDir("signals-django-no-fastapi");
|
||||
try {
|
||||
|
|
@ -830,3 +841,32 @@ test("detectProjectSignals: nested Prisma schema normalizes to prisma/schema.pri
|
|||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: nested Spring Boot Gradle service emits dep:spring-boot", () => {
|
||||
const dir = makeTempDir("signals-spring-gradle-nested");
|
||||
try {
|
||||
mkdirSync(join(dir, "services", "api"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, "services", "api", "build.gradle"),
|
||||
"plugins { id 'org.springframework.boot' version '3.2.0' }",
|
||||
"utf-8",
|
||||
);
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(signals.detectedFiles.includes("dep:spring-boot"), "should detect nested Spring Boot Gradle service");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectProjectSignals: Android Gradle project does not emit dep:spring-boot", () => {
|
||||
const dir = makeTempDir("signals-android-no-spring");
|
||||
try {
|
||||
writeFileSync(join(dir, "build.gradle"), "plugins { id 'com.android.application' }", "utf-8");
|
||||
mkdirSync(join(dir, "app"), { recursive: true });
|
||||
writeFileSync(join(dir, "app", "build.gradle"), "plugins { id 'com.android.application' }", "utf-8");
|
||||
const signals = detectProjectSignals(dir);
|
||||
assert.ok(!signals.detectedFiles.includes("dep:spring-boot"), "Android Gradle files should not trigger Spring Boot detection");
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -133,6 +133,14 @@ test("matchPacksForProject: Spring Boot does not match via language alone", () =
|
|||
assert.ok(!labels.includes("Java & Spring Boot"), "Spring Boot should NOT match via language alone");
|
||||
});
|
||||
|
||||
test("matchPacksForProject: Spring Boot matches only dep:spring-boot", () => {
|
||||
const positive = packLabels(makeSignals({ detectedFiles: ["dep:spring-boot"] }));
|
||||
assert.ok(positive.includes("Java & Spring Boot"), "should include Spring Boot pack when dependency marker exists");
|
||||
|
||||
const androidLike = packLabels(makeSignals({ detectedFiles: ["build.gradle", "app/build.gradle"], primaryLanguage: "java/kotlin" }));
|
||||
assert.ok(!androidLike.includes("Java & Spring Boot"), "generic Gradle + Android markers should not imply Spring Boot");
|
||||
});
|
||||
|
||||
test("matchPacksForProject: Unity does not include Godot", () => {
|
||||
const labels = packLabels(makeSignals({ detectedFiles: ["ProjectSettings/ProjectVersion.txt"] }));
|
||||
assert.ok(labels.includes("Unity"), "should include Unity");
|
||||
|
|
@ -164,6 +172,7 @@ test("SKILL_CATALOG: every matchFiles entry is backed by detection", () => {
|
|||
"*.sln",
|
||||
"*.vue",
|
||||
"dep:fastapi",
|
||||
"dep:spring-boot",
|
||||
]);
|
||||
|
||||
for (const pack of SKILL_CATALOG) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue