From 183b54d75e0e652a5c4d40917b8133e4cf96abb3 Mon Sep 17 00:00:00 2001 From: Derek Pearson Date: Sun, 22 Mar 2026 07:39:16 -0400 Subject: [PATCH] fix(skills): detect FastAPI via dependency scanning Replace the lazy 'no brownfield detection' approach with proper dependency-based detection. Scan requirements.txt and pyproject.toml for the 'fastapi' package name (case-insensitive word-boundary match) using the existing readBounded() utility (64KB cap). Adds 'dep:fastapi' synthetic marker to detectedFiles when found, which the FastAPI skill pack matches via matchFiles: ['dep:fastapi']. This ensures only actual FastAPI projects get the pack recommended, not all Python projects. Tests: 3 new detection tests (requirements.txt, pyproject.toml, negative Django case) + 1 new catalog test (dep:fastapi matching). Total: 50 detection + 17 catalog + 5 activation + 12 smoke = 84. --- src/resources/extensions/gsd/detection.ts | 18 ++++++++++ src/resources/extensions/gsd/skill-catalog.ts | 6 ++-- .../extensions/gsd/tests/detection.test.ts | 35 +++++++++++++++++++ .../gsd/tests/skill-catalog.test.ts | 5 +++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/resources/extensions/gsd/detection.ts b/src/resources/extensions/gsd/detection.ts index 0afbcdf68..1141fefac 100644 --- a/src/resources/extensions/gsd/detection.ts +++ b/src/resources/extensions/gsd/detection.ts @@ -371,6 +371,24 @@ export function detectProjectSignals(basePath: string): ProjectSignals { // unreadable root — skip extension scan } + // Python framework detection — scan dependency files for framework-specific packages. + // Adds synthetic markers (e.g. "dep:fastapi") so skill catalog matchFiles can reference them. + if (detectedFiles.includes("requirements.txt") || detectedFiles.includes("pyproject.toml")) { + try { + const depContent: string[] = []; + const reqPath = join(basePath, "requirements.txt"); + if (existsSync(reqPath)) depContent.push(readBounded(reqPath, 64 * 1024)); + const pyprojectPath = join(basePath, "pyproject.toml"); + if (existsSync(pyprojectPath)) depContent.push(readBounded(pyprojectPath, 64 * 1024)); + const combined = depContent.join("\n").toLowerCase(); + if (/\bfastapi\b/.test(combined)) { + detectedFiles.push("dep:fastapi"); + } + } catch { + // unreadable dependency files — skip framework scan + } + } + // Git repo detection const isGitRepo = existsSync(join(basePath, ".git")); diff --git a/src/resources/extensions/gsd/skill-catalog.ts b/src/resources/extensions/gsd/skill-catalog.ts index 231b15b29..3771e217d 100644 --- a/src/resources/extensions/gsd/skill-catalog.ts +++ b/src/resources/extensions/gsd/skill-catalog.ts @@ -394,14 +394,14 @@ export const SKILL_CATALOG: SkillPack[] = [ matchLanguages: ["python"], matchFiles: ["pyproject.toml", "setup.py", "requirements.txt"], }, - // FastAPI — no brownfield auto-detection (generic Python markers can't - // distinguish FastAPI from other frameworks). Available via greenfield - // stack selection or manual install: npx skills add wshobson/agents --skill fastapi-templates + // FastAPI — detected by scanning requirements.txt / pyproject.toml for the + // "fastapi" dependency. Uses the "dep:fastapi" synthetic marker from detection.ts. { label: "FastAPI", description: "Production-ready FastAPI projects with async patterns and error handling", repo: "wshobson/agents", skills: ["fastapi-templates"], + matchFiles: ["dep:fastapi"], }, // ── Go ──────────────────────────────────────────────────────────────────── { diff --git a/src/resources/extensions/gsd/tests/detection.test.ts b/src/resources/extensions/gsd/tests/detection.test.ts index ccb41a48d..f76740cc0 100644 --- a/src/resources/extensions/gsd/tests/detection.test.ts +++ b/src/resources/extensions/gsd/tests/detection.test.ts @@ -667,3 +667,38 @@ test("detectProjectSignals: Tailwind via tailwind.config.ts", () => { cleanup(dir); } }); + +test("detectProjectSignals: FastAPI detected via requirements.txt dependency", () => { + const dir = makeTempDir("signals-fastapi-req"); + try { + writeFileSync(join(dir, "requirements.txt"), "fastapi==0.115.0\nuvicorn[standard]\n", "utf-8"); + const signals = detectProjectSignals(dir); + assert.ok(signals.detectedFiles.includes("dep:fastapi"), "should add dep:fastapi marker"); + assert.equal(signals.primaryLanguage, "python"); + } finally { + cleanup(dir); + } +}); + +test("detectProjectSignals: FastAPI detected via pyproject.toml dependency", () => { + const dir = makeTempDir("signals-fastapi-pyproject"); + try { + writeFileSync(join(dir, "pyproject.toml"), '[project]\ndependencies = ["fastapi>=0.100"]\n', "utf-8"); + const signals = detectProjectSignals(dir); + assert.ok(signals.detectedFiles.includes("dep:fastapi"), "should add dep:fastapi marker"); + } finally { + cleanup(dir); + } +}); + +test("detectProjectSignals: Django project does NOT get dep:fastapi marker", () => { + const dir = makeTempDir("signals-django-no-fastapi"); + try { + writeFileSync(join(dir, "requirements.txt"), "django==5.0\ncelery\n", "utf-8"); + writeFileSync(join(dir, "manage.py"), "#!/usr/bin/env python", "utf-8"); + const signals = detectProjectSignals(dir); + assert.ok(!signals.detectedFiles.includes("dep:fastapi"), "should NOT add dep:fastapi for Django"); + } finally { + cleanup(dir); + } +}); diff --git a/src/resources/extensions/gsd/tests/skill-catalog.test.ts b/src/resources/extensions/gsd/tests/skill-catalog.test.ts index 6f51e495d..8c6d194ef 100644 --- a/src/resources/extensions/gsd/tests/skill-catalog.test.ts +++ b/src/resources/extensions/gsd/tests/skill-catalog.test.ts @@ -121,6 +121,11 @@ test("matchPacksForProject: FastAPI does not match generic Python", () => { assert.ok(!labels.includes("FastAPI"), "FastAPI should NOT match generic Python projects"); }); +test("matchPacksForProject: FastAPI matches when dep:fastapi detected", () => { + const labels = packLabels(makeSignals({ primaryLanguage: "python", detectedFiles: ["pyproject.toml", "dep:fastapi"] })); + assert.ok(labels.includes("FastAPI"), "FastAPI should match when dep:fastapi is in detectedFiles"); +}); + test("matchPacksForProject: Spring Boot does not match via language alone", () => { // Simulate Android project: has java/kotlin language but no root pom.xml/build.gradle const labels = packLabels(makeSignals({ primaryLanguage: "java/kotlin", detectedFiles: ["app/build.gradle"] }));