diff --git a/packages/autoskills/lib.ts b/packages/autoskills/lib.ts index 535c74eb..ef71dfe0 100644 --- a/packages/autoskills/lib.ts +++ b/packages/autoskills/lib.ts @@ -9,6 +9,7 @@ export { COMBO_SKILLS_MAP, FRONTEND_PACKAGES, FRONTEND_BONUS_SKILLS, + BACKEND_ONLY_IDS, WEB_FRONTEND_EXTENSIONS, AGENT_FOLDER_MAP, } from "./skills-map.ts"; @@ -20,6 +21,7 @@ import { COMBO_SKILLS_MAP, FRONTEND_PACKAGES, FRONTEND_BONUS_SKILLS, + BACKEND_ONLY_IDS, WEB_FRONTEND_EXTENSIONS, AGENT_FOLDER_MAP, } from "./skills-map.ts"; @@ -404,7 +406,6 @@ export function getAllPackageNames(pkg: Record | null): string[ } interface DetectInDirOptions { - skipFrontendFiles?: boolean; pkg?: Record | null; denoJson?: Record | null; } @@ -412,16 +413,11 @@ interface DetectInDirOptions { interface DetectInDirResult { detected: Technology[]; isFrontendByPackages: boolean; - isFrontendByFiles: boolean; } function detectTechnologiesInDir( dir: string, - { - skipFrontendFiles = false, - pkg: preloadedPkg, - denoJson: preloadedDeno, - }: DetectInDirOptions = {}, + { pkg: preloadedPkg, denoJson: preloadedDeno }: DetectInDirOptions = {}, ): DetectInDirResult { const pkg = preloadedPkg !== undefined ? preloadedPkg : readPackageJson(dir); const allPackages = getAllPackageNames(pkg); @@ -500,10 +496,8 @@ function detectTechnologiesInDir( } const isFrontendByPackages = allDepsArray.some((p) => FRONTEND_PACKAGES.has(p)); - const isFrontendByFiles = - isFrontendByPackages || skipFrontendFiles ? false : hasWebFrontendFiles(dir); - return { detected, isFrontendByPackages, isFrontendByFiles }; + return { detected, isFrontendByPackages }; } export interface DetectResult { @@ -517,11 +511,11 @@ export function detectTechnologies(projectDir: string): DetectResult { const denoJson = readDenoJson(projectDir); const root = detectTechnologiesInDir(projectDir, { pkg, denoJson }); const seenIds = new Map(root.detected.map((t) => [t.id, t])); - let isFrontend = root.isFrontendByPackages || root.isFrontendByFiles; + let isFrontend = root.isFrontendByPackages; const workspaceDirs = resolveWorkspaces(projectDir, { pkg, denoJson }); for (const wsDir of workspaceDirs) { - const ws = detectTechnologiesInDir(wsDir, { skipFrontendFiles: isFrontend }); + const ws = detectTechnologiesInDir(wsDir); for (const tech of ws.detected) { if (!seenIds.has(tech.id)) { @@ -529,13 +523,21 @@ export function detectTechnologies(projectDir: string): DetectResult { } } - if (ws.isFrontendByPackages || ws.isFrontendByFiles) { + if (ws.isFrontendByPackages) { isFrontend = true; } } const detected = [...seenIds.values()]; const detectedIds = detected.map((t) => t.id); + + // Backend-only stacks (e.g. Python, Java) often contain .html templates + // or static .css files that should not trigger frontend classification. + if (!isFrontend && !detectedIds.some((id) => BACKEND_ONLY_IDS.has(id))) { + isFrontend = + hasWebFrontendFiles(projectDir) || workspaceDirs.some((dir) => hasWebFrontendFiles(dir)); + } + const combos = detectCombos(detectedIds); return { detected, isFrontend, combos }; diff --git a/packages/autoskills/skills-map.ts b/packages/autoskills/skills-map.ts index 55348b77..79447152 100644 --- a/packages/autoskills/skills-map.ts +++ b/packages/autoskills/skills-map.ts @@ -788,7 +788,7 @@ export const SKILLS_MAP: Technology[] = [ skills: [ "github/awesome-copilot/dotnet-best-practices", "github/awesome-copilot/dotnet-design-pattern-review", - "github/awesome-copilot/dotnet-upgrade" + "github/awesome-copilot/dotnet-upgrade", ], }, { @@ -819,10 +819,7 @@ export const SKILLS_MAP: Technology[] = [ patterns: ["Microsoft.NET.Sdk.Web"], }, }, - skills: [ - "github/awesome-copilot/containerize-aspnetcore", - "openai/skills/aspnet-core", - ], + skills: ["github/awesome-copilot/containerize-aspnetcore", "openai/skills/aspnet-core"], }, { id: "aspnet-blazor", @@ -833,9 +830,7 @@ export const SKILLS_MAP: Technology[] = [ patterns: ["Microsoft.NET.Sdk.BlazorWebAssembly", "Microsoft.AspNetCore.Components"], }, }, - skills: [ - "github/awesome-copilot/fluentui-blazor" - ], + skills: ["github/awesome-copilot/fluentui-blazor"], }, { id: "aspnet-minimal-api", @@ -849,7 +844,7 @@ export const SKILLS_MAP: Technology[] = [ }, skills: [ "github/awesome-copilot/aspnet-minimal-api-openapi", - "dotnet/skills/minimal-api-file-upload" + "dotnet/skills/minimal-api-file-upload", ], }, { @@ -1408,3 +1403,23 @@ export const WEB_FRONTEND_EXTENSIONS: Set = new Set([ ".pug", ".njk", ]); + +/** + * Backend-only stacks whose .html/.css files are server-side templates or + * static assets, not a frontend application. Skipping frontend detection for + * these avoids misclassifying e.g. Flask or Django projects as frontend. + * + * Trade-off: causes a false negative when one of these backends is paired + * with a vanilla frontend directory (no package.json). Accepted because the + * inverse case is significantly more common. Extend this set when adding + * backend languages whose templates match WEB_FRONTEND_EXTENSIONS (e.g. Go, + * PHP, server-side Node). + */ +export const BACKEND_ONLY_IDS: Set = new Set([ + "python", + "java", + "springboot", + "django", + "flask", + "fastapi", +]); diff --git a/packages/autoskills/tests/cli.test.ts b/packages/autoskills/tests/cli.test.ts index 247f6ba7..0ba50c95 100644 --- a/packages/autoskills/tests/cli.test.ts +++ b/packages/autoskills/tests/cli.test.ts @@ -409,6 +409,26 @@ describe("CLI", () => { ok(output.includes("Spring Boot")); }); + it("does NOT detect web frontend for Python-only project with --dry-run", () => { + writeFile(tmp.path, "requirements.txt", "flask==3.0.0"); + writeFile(tmp.path, "app/main.py", "from flask import Flask"); + writeFile(tmp.path, "templates/index.html", "Hello"); + + const output = run(["--dry-run"], tmp.path); + + ok(output.includes("Python")); + ok(!output.includes("Web frontend detected")); + ok(!output.includes("frontend-design")); + }); + + it("detects Python from requirements.txt with --dry-run", () => { + writeFile(tmp.path, "requirements.txt", "flask==3.0.0"); + + const output = run(["--dry-run"], tmp.path); + + ok(output.includes("Python")); + }); + it("adds web fundamentals when npm frontend is detected too", () => { writePackageJson(tmp.path, { dependencies: { react: "^19", next: "^15" } }); const output = run(["--dry-run"], tmp.path); diff --git a/packages/autoskills/tests/detect.test.ts b/packages/autoskills/tests/detect.test.ts index b67be9b3..4651e76c 100644 --- a/packages/autoskills/tests/detect.test.ts +++ b/packages/autoskills/tests/detect.test.ts @@ -342,6 +342,13 @@ describe("detectTechnologies", () => { strictEqual(isFrontend, false); }); + it("detects frontend from .html files when no backend stack is present", () => { + writeFile(tmp.path, "index.html", ""); + writeFile(tmp.path, "style.css", "body { margin: 0 }"); + const { isFrontend } = detectTechnologies(tmp.path); + strictEqual(isFrontend, true); + }); + it("detects combos when multiple technologies match", () => { writePackageJson(tmp.path, { dependencies: { expo: "^52.0.0", tailwindcss: "^4.0.0" } }); const { combos } = detectTechnologies(tmp.path); @@ -1482,6 +1489,58 @@ describe("detectTechnologies (monorepo)", () => { }); }); +// ── Python detection ───────────────────────────────────────── + +describe("detectTechnologies (Python)", () => { + const tmp = useTmpDir(); + + it("detects Python from requirements.txt", () => { + writeFile(tmp.path, "requirements.txt", "flask==3.0.0"); + const { detected } = detectTechnologies(tmp.path); + ok(detected.some((t) => t.id === "python")); + }); + + it("detects Python from pyproject.toml", () => { + writeFile(tmp.path, "pyproject.toml", "[project]\nname = 'myapp'"); + const { detected } = detectTechnologies(tmp.path); + ok(detected.some((t) => t.id === "python")); + }); + + it("detects Python from setup.py", () => { + writeFile(tmp.path, "setup.py", "from setuptools import setup"); + const { detected } = detectTechnologies(tmp.path); + ok(detected.some((t) => t.id === "python")); + }); + + it("detects Python from Pipfile", () => { + writeFile(tmp.path, "Pipfile", "[packages]\nflask = '*'"); + const { detected } = detectTechnologies(tmp.path); + ok(detected.some((t) => t.id === "python")); + }); + + it("does NOT detect web frontend for Python project with .html templates", () => { + writeFile(tmp.path, "requirements.txt", "flask==3.0.0"); + writeFile(tmp.path, "templates/index.html", "Hello"); + const { isFrontend } = detectTechnologies(tmp.path); + strictEqual(isFrontend, false); + }); + + it("does NOT detect web frontend for Django project with templates", () => { + writeFile(tmp.path, "manage.py", "#!/usr/bin/env python"); + writeFile(tmp.path, "templates/base.html", "{% block content %}{% endblock %}"); + writeFile(tmp.path, "static/style.css", "body { margin: 0 }"); + const { isFrontend } = detectTechnologies(tmp.path); + strictEqual(isFrontend, false); + }); + + it("detects frontend when both Python and frontend framework are present", () => { + writeFile(tmp.path, "requirements.txt", "flask==3.0.0"); + writePackageJson(tmp.path, { dependencies: { react: "^19" } }); + const { isFrontend } = detectTechnologies(tmp.path); + strictEqual(isFrontend, true); + }); +}); + // ── detectCombos ────────────────────────────────────────────── describe("detectCombos", () => {