From 76a135f450c318c2eddd77ac1e6113556818e17e Mon Sep 17 00:00:00 2001 From: oscaruiz Date: Mon, 6 Apr 2026 23:05:00 +0200 Subject: [PATCH 1/3] fix: prevent Python/Java backend projects from being detected as web frontend (#48) --- packages/autoskills/lib.mjs | 30 ++++++++----- packages/autoskills/skills-map.mjs | 2 + packages/autoskills/tests/cli.test.mjs | 20 +++++++++ packages/autoskills/tests/detect.test.mjs | 53 +++++++++++++++++++++++ 4 files changed, 93 insertions(+), 12 deletions(-) diff --git a/packages/autoskills/lib.mjs b/packages/autoskills/lib.mjs index 7c13c0fe..81332d1e 100644 --- a/packages/autoskills/lib.mjs +++ b/packages/autoskills/lib.mjs @@ -7,6 +7,7 @@ export { COMBO_SKILLS_MAP, FRONTEND_PACKAGES, FRONTEND_BONUS_SKILLS, + BACKEND_ONLY_IDS, WEB_FRONTEND_EXTENSIONS, AGENT_FOLDER_MAP, } from "./skills-map.mjs"; @@ -16,6 +17,7 @@ import { COMBO_SKILLS_MAP, FRONTEND_PACKAGES, FRONTEND_BONUS_SKILLS, + BACKEND_ONLY_IDS, WEB_FRONTEND_EXTENSIONS, AGENT_FOLDER_MAP, } from "./skills-map.mjs"; @@ -404,14 +406,12 @@ export function getAllPackageNames(pkg) { /** * Scans a single directory for known technologies by checking packages, package patterns, * config files, and config file content against the SKILLS_MAP. - * Also determines whether the directory looks like a frontend project. + * Also determines whether the directory has frontend packages. * @param {string} dir - Directory to scan. - * @returns {{ detected: object[], isFrontendByPackages: boolean, isFrontendByFiles: boolean }} + * @param {{ pkg?: object|null, denoJson?: object|null }} [opts] - Pre-read manifests to avoid duplicate I/O. + * @returns {{ detected: object[], isFrontendByPackages: boolean }} */ -function detectTechnologiesInDir( - dir, - { skipFrontendFiles = false, pkg: preloadedPkg, denoJson: preloadedDeno } = {}, -) { +function detectTechnologiesInDir(dir, { pkg: preloadedPkg, denoJson: preloadedDeno } = {}) { const pkg = preloadedPkg !== undefined ? preloadedPkg : readPackageJson(dir); const allPackages = getAllPackageNames(pkg); const deno = preloadedDeno !== undefined ? preloadedDeno : readDenoJson(dir); @@ -489,10 +489,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 }; } /** @@ -506,11 +504,11 @@ export function detectTechnologies(projectDir) { 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)) { @@ -518,13 +516,21 @@ export function detectTechnologies(projectDir) { } } - 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.mjs b/packages/autoskills/skills-map.mjs index fd5fdfab..cb284296 100644 --- a/packages/autoskills/skills-map.mjs +++ b/packages/autoskills/skills-map.mjs @@ -1262,6 +1262,8 @@ export const AGENT_FOLDER_MAP = { ".kiro": "kiro-cli", }; +export const BACKEND_ONLY_IDS = new Set(["python", "java", "springboot", "django", "flask", "fastapi"]); + export const WEB_FRONTEND_EXTENSIONS = new Set([ ".html", ".htm", diff --git a/packages/autoskills/tests/cli.test.mjs b/packages/autoskills/tests/cli.test.mjs index 66691b99..3d9f3a40 100644 --- a/packages/autoskills/tests/cli.test.mjs +++ b/packages/autoskills/tests/cli.test.mjs @@ -481,6 +481,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" } }); diff --git a/packages/autoskills/tests/detect.test.mjs b/packages/autoskills/tests/detect.test.mjs index 8723492d..3f63fe83 100644 --- a/packages/autoskills/tests/detect.test.mjs +++ b/packages/autoskills/tests/detect.test.mjs @@ -1403,6 +1403,59 @@ 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", () => { From 3e0536b54757b1c4ebc472669c9e6172b4f77436 Mon Sep 17 00:00:00 2001 From: oscaruiz Date: Thu, 9 Apr 2026 12:59:45 +0200 Subject: [PATCH 2/3] test: add base HTML/CSS case and document BACKEND_ONLY_IDS Address review feedback on #56: - Add regression test verifying that pure HTML/CSS projects without any backend stack are still detected as frontend, ensuring the refactor of hasWebFrontendFiles() out of detectTechnologiesInDir() did not break the happy path. - Document BACKEND_ONLY_IDS with a JSDoc block covering the known false-negative trade-off (backend + vanilla frontend) and noting that the set is extensible to other backend template engines. --- packages/autoskills/skills-map.mjs | 11 +++++++++++ packages/autoskills/tests/detect.test.mjs | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/packages/autoskills/skills-map.mjs b/packages/autoskills/skills-map.mjs index cb284296..ca493921 100644 --- a/packages/autoskills/skills-map.mjs +++ b/packages/autoskills/skills-map.mjs @@ -1262,6 +1262,17 @@ export const AGENT_FOLDER_MAP = { ".kiro": "kiro-cli", }; +/** + * 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 = new Set(["python", "java", "springboot", "django", "flask", "fastapi"]); export const WEB_FRONTEND_EXTENSIONS = new Set([ diff --git a/packages/autoskills/tests/detect.test.mjs b/packages/autoskills/tests/detect.test.mjs index 3f63fe83..98b94af3 100644 --- a/packages/autoskills/tests/detect.test.mjs +++ b/packages/autoskills/tests/detect.test.mjs @@ -268,6 +268,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); From 1e053a0d6d12f0a95ecc641d9db4fe8532389719 Mon Sep 17 00:00:00 2001 From: oscaruiz Date: Sun, 19 Apr 2026 21:48:45 +0200 Subject: [PATCH 3/3] style: format files with oxfmt --- packages/autoskills/lib.ts | 3 +-- packages/autoskills/skills-map.ts | 13 ++++--------- packages/autoskills/tests/detect.test.ts | 1 - 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/autoskills/lib.ts b/packages/autoskills/lib.ts index 7abe58d7..ef71dfe0 100644 --- a/packages/autoskills/lib.ts +++ b/packages/autoskills/lib.ts @@ -535,8 +535,7 @@ export function detectTechnologies(projectDir: string): DetectResult { // 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)); + hasWebFrontendFiles(projectDir) || workspaceDirs.some((dir) => hasWebFrontendFiles(dir)); } const combos = detectCombos(detectedIds); diff --git a/packages/autoskills/skills-map.ts b/packages/autoskills/skills-map.ts index 54081488..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", ], }, { diff --git a/packages/autoskills/tests/detect.test.ts b/packages/autoskills/tests/detect.test.ts index 9bf145c9..4651e76c 100644 --- a/packages/autoskills/tests/detect.test.ts +++ b/packages/autoskills/tests/detect.test.ts @@ -1539,7 +1539,6 @@ describe("detectTechnologies (Python)", () => { const { isFrontend } = detectTechnologies(tmp.path); strictEqual(isFrontend, true); }); - }); // ── detectCombos ──────────────────────────────────────────────