diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9e5e1a..31819b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ Formato basado en [Keep a Changelog](https://keepachangelog.com/) y [SemVer](htt
## [No publicado]
### Added
+- **Estándares de C# en el agente `coder`.** "Estándares por lenguaje" cubría TypeScript, Python, Go, Rust, Solidity, PHP, Java y Dart, pero **no C#**, pese a existir un stack `backend/dotnet` completo y a que la regla de nombres ya lo mencionaba. Se añade su sección (nullable reference types, `record`/`sealed`, `async` sin `.Result`/`async void`, `CancellationToken`, excepciones específicas, analyzers como error) y se incluye C# en la lista de lenguajes del agente.
+- **Overlays de Spring, .NET y Angular más profundos.** Spring: virtual threads y `@Async` con executor explícito, Resilience4j, `@Cacheable` con clave y TTL, Actuator + Micrometer, `readOnly` en lecturas, la trampa del proxy de `@Transactional` en llamadas internas, y slices de test frente a `@SpringBootTest`. .NET: `CancellationToken` propagado, `IHttpClientFactory` con typed clients y resiliencia, `HybridCache`, OpenTelemetry y health checks, `MapGroup` con filtros, la captura de `Scoped` desde `Singleton`, `Nullable`/`TreatWarningsAsErrors`, y sustitución de dependencias vía `ConfigureTestServices`. Angular: `@defer`, `computed` frente a `effect`, `linkedSignal`, rutas lazy con guards funcionales y `withComponentInputBinding`, interceptores funcionales, formularios reactivos tipados, `NgOptimizedImage`, accesibilidad y presupuestos de bundle.
+
+### Fixed
+- **Detección de Spring incompleta**: se exigía la cadena literal `spring-boot` en el `pom.xml`, así que un proyecto que solo declara `org.springframework` (BOM o Spring Framework sin Boot) no se detectaba; y los `settings.gradle`/`settings.gradle.kts` no se miraban en absoluto. Ahora se buscan varias señales en los cinco archivos de build.
+- **Un proyecto Java sin Spring quedaba en un callejón sin salida**: la detección devolvía `null` sin explicar nada y, con `--yes`, el instalador salía con código 1 a secas. Ahora imprime qué vio (`pom.xml`/`build.gradle`), aclara que el overlay de Java del kit es Spring Boot y avisa de que para Quarkus, Micronaut o Jakarta EE puro ese overlay no encaja. Misma pista para ecosistemas sin overlay (Go, Rust) y para un `package.json` sin framework reconocido.
- **`validate.cjs` verifica que el README no mienta.** El README se había desviado en silencio: listaba helpers borrados dos releases antes, 9 de los 13 stacks, "los 8 stacks" del CI y ejemplos pineados a `#v1.2.0`. Ahora se comprueban contra el repo los conteos de agentes, skills y stacks (los del árbol y los del CI), la lista de helpers, los stacks por categoría, el número de componentes descartados, y que todo ejemplo `#vX.Y.Z` apunte a la versión de `package.json`. Si una de esas frases desaparece, falla explícitamente en vez de dejar de comprobar: 38 → 49 comprobaciones.
## [2.1.0] - 2026-08-08
diff --git a/README.md b/README.md
index 4f95fd1..91046f8 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,9 @@ Flags: `--stack backend/nestjs` (fuerza stack) · `--yes` (no interactivo, conse
`--no-flow` · `--no-external` · `--update`/`--force` (trae la última capa base sin pisar lo que hayas editado) · `--dry-run` (simula) · `--help`.
El instalador:
-1. **Detecta el stack** (pubspec→flutter, react-native, next, angular, nest, foundry, .csproj→dotnet, spring, django, composer→php, fastapi, react, express).
+1. **Detecta el stack** (pubspec→flutter, react-native, next, angular, nest, foundry, `.csproj`/`.sln`→dotnet, Spring, django, composer→php, fastapi, react, express).
+ - **Java/Kotlin**: se reconoce Spring por sus señales (`spring-boot`, `org.springframework`, el plugin de Gradle o `io.spring.dependency-management`) en `pom.xml`, `build.gradle(.kts)` y `settings.gradle(.kts)`. Un proyecto Java **sin** Spring no se fuerza al overlay de Spring: el instalador lo dice y te deja elegir.
+ - Si la detección falla, imprime **qué vio** en el directorio en vez de rendirse sin explicación.
- En un proyecto **fullstack con un solo `package.json`** (p. ej. express + react juntos) gana el **frontend**; si el repo es principalmente API, fuerza con `--stack backend/express`.
2. Hace **backup** de tu `.claude/` si existe.
3. Aplica la **capa base** (`shared/.claude` → `.claude/`): agentes, skills, helpers, settings, comandos.
diff --git a/install.js b/install.js
index af75a50..db0cb81 100644
--- a/install.js
+++ b/install.js
@@ -170,6 +170,29 @@ function dirHasExt(re) {
return false;
}
}
+// Archivos de build de Java/Kotlin. Un proyecto Java NO es necesariamente Spring.
+const JAVA_BUILD_FILES = ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"];
+const javaBuildFiles = () => JAVA_BUILD_FILES.filter((f) => fs.existsSync(path.join(CWD, f)));
+
+// Spring se declara de muchas formas: starters, BOM, plugin de Gradle, imports del framework.
+// Antes solo se miraba /spring-boot/ en pom.xml y /spring/ en build.gradle, así que un Gradle
+// con Kotlin DSL o un proyecto Spring sin la palabra exacta se quedaba sin detectar.
+const SPRING_SIGNALS = /spring-boot|spring-framework|org\.springframework|io\.spring\.dependency-management/i;
+const isSpringProject = () => javaBuildFiles().some((f) => fileHas(path.join(CWD, f), SPRING_SIGNALS));
+
+// Qué se vio en el directorio cuando la detección falla. Sin esto el usuario solo recibe
+// "no se pudo detectar" y tiene que adivinar por qué.
+function detectionHint() {
+ const java = javaBuildFiles();
+ if (java.length) {
+ return `Vi ${java.join(", ")}: es un proyecto Java/Kotlin, pero no encontré señales de Spring.\n El overlay de Java del kit es Spring Boot; úsalo con --stack backend/spring si aplica.\n Si es Quarkus, Micronaut o Jakarta EE puro, ese overlay no encaja: dinos cuál necesitas.`;
+ }
+ if (fs.existsSync(path.join(CWD, "go.mod"))) return "Vi go.mod: aún no hay overlay de Go en el kit.";
+ if (fs.existsSync(path.join(CWD, "Cargo.toml"))) return "Vi Cargo.toml: aún no hay overlay de Rust en el kit.";
+ if (fs.existsSync(path.join(CWD, "package.json"))) return "Vi package.json pero ningún framework reconocido en sus dependencias.";
+ return "No vi ningún archivo de proyecto reconocible en este directorio.";
+}
+
function detectStack() {
const pkg = readJSON(path.join(CWD, "package.json")) || {};
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -185,7 +208,7 @@ function detectStack() {
if (fs.existsSync(path.join(CWD, "nest-cli.json")) || deps["@nestjs/core"]) return "backend/nestjs";
if (fs.existsSync(path.join(CWD, "foundry.toml"))) return "blockchain/solidity";
if (dirHasExt(/\.(csproj|sln)$/)) return "backend/dotnet";
- if (fileHas(path.join(CWD, "pom.xml"), /spring-boot/i) || fileHas(path.join(CWD, "build.gradle"), /spring/i) || fileHas(path.join(CWD, "build.gradle.kts"), /spring/i)) return "backend/spring";
+ if (isSpringProject()) return "backend/spring";
if (fs.existsSync(path.join(CWD, "manage.py")) || fileHas(req, /(^|\n)\s*django\b/i) || fileHas(pyproj, /\bdjango\b/i)) return "backend/django";
if (fs.existsSync(path.join(CWD, "composer.json"))) return "backend/php";
if (fileHas(req, /fastapi/i) || fileHas(pyproj, /fastapi/i)) return "backend/fastapi";
@@ -748,6 +771,7 @@ async function main() {
if (!(await confirm("¿Es correcto?", true))) stackId = null;
}
if (!stackId) {
+ if (!FLAGS.stack) console.log(`\n🔍 ${detectionHint()}`);
console.log("\nStacks disponibles:\n " + Object.keys(STACKS).join("\n "));
const answer = await ask("\nElige stack (category/stack): ");
stackId = (answer || "").trim();
diff --git a/shared/.claude/agents/coder.md b/shared/.claude/agents/coder.md
index 1f99fb9..602ce80 100644
--- a/shared/.claude/agents/coder.md
+++ b/shared/.claude/agents/coder.md
@@ -4,7 +4,7 @@ description: "Úsalo para implementar features o refactorizar con criterio senio
tools: Read, Write, Edit, Bash, Glob, Grep
---
-Eres un ingeniero de software senior con más de 20 años de experiencia construyendo y operando sistemas de producción en muchos lenguajes (TypeScript/JavaScript, Python, Go, Rust, Java, PHP, Solidity, Dart) y paradigmas (orientado a objetos, funcional, reactivo). Tu enfoque abarca correctitud, legibilidad, rendimiento y mantenibilidad a largo plazo, con un fuerte sesgo hacia el cambio mínimo y coherente y hacia código que se lee como prosa. Escribes código que tu yo del futuro —y cualquier colega— entenderá sin preguntar.
+Eres un ingeniero de software senior con más de 20 años de experiencia construyendo y operando sistemas de producción en muchos lenguajes (TypeScript/JavaScript, Python, Go, Rust, Java, C#, PHP, Solidity, Dart) y paradigmas (orientado a objetos, funcional, reactivo). Tu enfoque abarca correctitud, legibilidad, rendimiento y mantenibilidad a largo plazo, con un fuerte sesgo hacia el cambio mínimo y coherente y hacia código que se lee como prosa. Escribes código que tu yo del futuro —y cualquier colega— entenderá sin preguntar.
## Preparación
@@ -51,6 +51,10 @@ Al ser invocado, establece contexto primero: lee el archivo objetivo y sus vecin
### Java
- Inyección por constructor (no `@Autowired` en campos). DTOs en la API, no entidades JPA. `Optional` en vez de null donde aplique; `final` por defecto. Maneja `checked exceptions` con intención. Evita N+1 (fetch joins/`@EntityGraph`).
+### C#
+- **Nullable reference types** habilitados (`enable`); el operador `!` no es una solución, es callar al compilador. `record`/`readonly struct` para datos inmutables; `sealed` por defecto.
+- `async`/`await` en todo I/O: **nunca** `.Result`/`.Wait()` (deadlock), nada de `async void` salvo event handlers, propaga `CancellationToken` hasta el fondo y usa `ConfigureAwait(false)` en librerías. Excepciones específicas, jamás un `catch (Exception)` que se traga el error. Analyzers activos con `TreatWarningsAsErrors`.
+
### Dart
- Null-safety estricto; modelos inmutables (`freezed`) e igualdad por valor. `const` en todo lo posible. Un solo paradigma de estado (Riverpod o BLoC), no mezclar. `analysis_options.yaml` con lints fuertes.
diff --git a/stacks/backend/dotnet/.claude/CLAUDE.md b/stacks/backend/dotnet/.claude/CLAUDE.md
index aabec4c..4b70a2b 100644
--- a/stacks/backend/dotnet/.claude/CLAUDE.md
+++ b/stacks/backend/dotnet/.claude/CLAUDE.md
@@ -3,24 +3,42 @@
## Estructura y DI
- Minimal APIs o Controllers; capas con el **contenedor DI integrado** (constructor injection). Lifetimes correctos (`Scoped` para EF DbContext, no `Singleton`).
- DTOs/`record` en la API, **nunca** entidades EF expuestas. Mapea con Mapster/AutoMapper o a mano.
+- Con Minimal APIs, agrupa por dominio (`MapGroup`) y aplica filtros por grupo en vez de repetir validación endpoint por endpoint.
+- **Nunca** captures un servicio `Scoped` desde uno `Singleton`: es el bug de lifetime clásico y explota tarde, en producción.
## Persistencia (EF Core)
- Lecturas con `AsNoTracking()`. Evita **N+1**: `Include`/`ThenInclude` o proyecciones a DTO con `Select`. `await` en todo I/O.
- Migraciones con `dotnet ef migrations`; transacciones explícitas para operaciones multi-tabla. Concurrencia optimista con `[Timestamp]`/rowversion.
+- Proyecta a DTO **dentro** de la query (`Select` antes de materializar): traer la entidad entera para usar dos columnas es el desperdicio más común.
+
+## Async y cancelación
+- `CancellationToken` en toda firma async, propagado hasta EF y `HttpClient`: sin él, el trabajo sigue corriendo después de que el cliente se fue.
+- Nada de `.Result`/`.Wait()` (deadlock) ni `async void` fuera de event handlers. `IAsyncEnumerable` para streaming en vez de materializar listas grandes.
## API, validación, errores
- Validación con **FluentValidation** o DataAnnotations en el boundary. Manejo global con middleware de excepciones → `ProblemDetails` (RFC 7807) consistente.
-- Versionado de API y paginación.
+- Versionado de API y paginación, con tope de tamaño de página en el servidor.
+
+## Resiliencia y caché
+- `IHttpClientFactory` con **typed clients**: nunca `new HttpClient()` (agota sockets y no refresca DNS). Añade timeout + retry con backoff + circuit breaker (`Microsoft.Extensions.Http.Resilience` o Polly).
+- `HybridCache`/`IMemoryCache` con TTL explícito; incluye en la clave todo lo que varíe la respuesta (usuario, tenant, cultura).
## Config y seguridad
-- Config por `appsettings.{Environment}.json` + env vars + **user-secrets** en dev; secretos nunca versionados (usa Key Vault/secret manager en prod). **Options pattern** validado al boot.
+- Config por `appsettings.{Environment}.json` + env vars + **user-secrets** en dev; secretos nunca versionados (usa Key Vault/secret manager en prod). **Options pattern** validado al boot (`ValidateOnStart`).
- Auth con ASP.NET Core Identity/JWT; **authorization policies deny-by-default**. Logging estructurado con **Serilog** (sin secretos/PII).
+## Observabilidad
+- **OpenTelemetry** para trazas y métricas; health checks (`/health/live`, `/health/ready`) diferenciados y conectados al orquestador.
+
+## Calidad del proyecto
+- `enable` y `true` en el `.csproj`; `.editorconfig` con analyzers. `dotnet format` en el hook de pre-commit.
+
## Comandos
- `dotnet run` · `dotnet build` · `dotnet test` · `dotnet format`
- `dotnet ef migrations add ` · `dotnet ef database update`
## Tests
- **xUnit** + Moq (unit); `WebApplicationFactory` (integración) + Testcontainers para DB real.
+- Reemplaza dependencias externas en el `WebApplicationFactory` vía `ConfigureTestServices`, no con banderas de entorno dentro del código de producción.
> Aplica también el playbook común de backend.
diff --git a/stacks/backend/spring/.claude/CLAUDE.md b/stacks/backend/spring/.claude/CLAUDE.md
index bb8e4dc..6aea12c 100644
--- a/stacks/backend/spring/.claude/CLAUDE.md
+++ b/stacks/backend/spring/.claude/CLAUDE.md
@@ -3,23 +3,38 @@
## Estructura y DI
- Capas: `Controller` → `Service` → `Repository` (Spring Data JPA). **Inyección por constructor** (nunca `@Autowired` en campos: rompe testabilidad e inmutabilidad).
- DTOs en la API, **nunca** entidades JPA expuestas (evita fugas y problemas de serialización lazy). Mapea con MapStruct o a mano.
+- DTOs como `record` (Java 17+): inmutables y con equals/hashCode correctos sin boilerplate. En Kotlin, `data class` con `val`.
## Persistencia (JPA)
- Evita **N+1**: `JOIN FETCH`, `@EntityGraph` o proyecciones; cuidado con `FetchType.EAGER`. `@Transactional` con límites claros (en el service, no en el controller).
+- `@Transactional(readOnly = true)` en lecturas: habilita optimizaciones del proveedor y documenta la intención.
- Migraciones con **Flyway**/Liquibase (nunca `ddl-auto=update` en prod). Locking optimista con `@Version`.
+- Cuidado con el proxy de `@Transactional`: una llamada interna al mismo bean **no** pasa por el proxy y se queda sin transacción.
## API, validación, errores
- Bean Validation (`@Valid`, `@NotNull`...) en los DTOs de entrada. Manejo global con `@RestControllerAdvice` → envelope de error consistente.
-- Versionado y paginación (`Pageable`).
+- Versionado y paginación (`Pageable`). Limita el tamaño de página en el servidor: un `size` sin tope es un DoS gratis.
+
+## Concurrencia
+- Java 21+: **virtual threads** (`spring.threads.virtual.enabled=true`) para I/O bloqueante; no los uses con código `synchronized` de bloqueo largo (pinning).
+- `@Async` con un executor **explícito** (nunca el default sin acotar) y `CompletableFuture` para composición. Propaga el contexto de seguridad si lo necesitas.
+
+## Resiliencia y caché
+- **Resilience4j** para llamadas a terceros: timeout + retry con backoff + circuit breaker. Un cliente HTTP sin timeout acaba tumbando el pool.
+- `@Cacheable` con clave explícita y TTL; invalida en la escritura (`@CacheEvict`). Nunca cachees respuestas que dependan del usuario sin incluirlo en la clave.
## Config y seguridad
- `application-{env}.yml` por perfil; secretos vía env vars / config server, **nunca** en el yml versionado. Fail-fast con `@ConfigurationProperties` validadas.
- **Spring Security** deny-by-default; passwords con BCrypt; authz por método/endpoint. Actuator con endpoints sensibles protegidos.
+## Observabilidad
+- Actuator + **Micrometer** para métricas; `/health` y `/health/readiness` diferenciados. Logging estructurado (JSON) con `traceId` correlacionado, sin secretos ni PII.
+
## Comandos
- Maven: `mvn spring-boot:run` · `mvn verify` — Gradle: `./gradlew bootRun` · `./gradlew test`
## Tests
- JUnit 5 + Mockito (unit), `@SpringBootTest` + **Testcontainers** (integración con DB real). `@DataJpaTest` para repos.
+- Prefiere **slices** (`@WebMvcTest`, `@DataJpaTest`) al `@SpringBootTest` completo: arrancar todo el contexto en cada test es lo que vuelve lenta la suite.
> Aplica también el playbook común de backend.
diff --git a/stacks/frontend/angular/.claude/CLAUDE.md b/stacks/frontend/angular/.claude/CLAUDE.md
index 20307c8..5af56b9 100644
--- a/stacks/frontend/angular/.claude/CLAUDE.md
+++ b/stacks/frontend/angular/.claude/CLAUDE.md
@@ -9,6 +9,22 @@
- **Standalone components** (sin NgModules nuevos). `ChangeDetectionStrategy.OnPush` por defecto.
- `@if`/`@for`/`@switch` (control flow nuevo) con `track` en los `@for` (clave estable, no índice).
- `inject()` sobre constructor cuando aporte; `DestroyRef`/`takeUntilDestroyed` para limpiar suscripciones.
+- `@defer` para diferir lo que no es visible de entrada (bloques pesados, below the fold), con `@placeholder`/`@loading`.
+- `effect()` es para sincronizar con el mundo exterior, **no** para derivar estado: si calculas un valor a partir de otros, eso es `computed`.
+
+## Estado con signals
+- `computed` para todo lo derivado; nada de duplicar estado que puedas calcular.
+- `linkedSignal` cuando un estado local deba reinicializarse al cambiar su origen (p. ej. la selección al cambiar de lista).
+- Las APIs `resource`/`rxResource`/`httpResource` cubren carga asíncrona declarativa con estado y recarga; confirma su nivel de estabilidad contra la versión exacta de tu proyecto antes de apoyarte en ellas.
+
+## Rutas
+- Lazy loading con `loadComponent`/`loadChildren` por ruta: el bundle inicial solo debe traer la primera pantalla.
+- Guards y resolvers **funcionales** (`CanActivateFn`, `ResolveFn`) con `inject()`; los basados en clase son legado.
+- `withComponentInputBinding()` para recibir parámetros de ruta como inputs en vez de leer `ActivatedRoute` a mano.
+
+## HTTP y formularios
+- Interceptores **funcionales** (`HttpInterceptorFn`) para auth, correlación y reintentos; el manejo de errores va en el stream, no en cada componente.
+- Formularios **reactivos y tipados**; template-driven solo para casos triviales. Valida en el `FormGroup`, no en el template.
## RxJS con criterio
- Usa signals para estado; RxJS para flujos de eventos/async complejos. No subscribes manuales sin limpieza.
@@ -18,9 +34,14 @@
- Todo texto visible vía `| translate` (ngx-translate); nunca hardcodeado.
- Nada de acceso directo a `localStorage`: usa un `StorageService` inyectable.
- Tailwind para estilos; evita CSS suelto duplicado. Sanitiza con `DomSanitizer` si inyectas HTML.
+- Accesibilidad como requisito: HTML semántico, foco visible y navegable por teclado, `aria-*` solo cuando el elemento nativo no alcanza.
+- `NgOptimizedImage` (`ngSrc`) para imágenes: dimensiona, prioriza el LCP y evita layout shift.
## Comandos
- `ng serve` · `ng build` · `ng test` · `ng lint`
## Tests
- Testing Library Angular o TestBed con `OnPush`. Mockea servicios HTTP (`HttpTestingController`). Prueba comportamiento visible, no internals.
+
+## Presupuesto de bundle
+- Mantén los `budgets` del `angular.json` y trátalos como error en CI: sin tope, el bundle solo crece.
diff --git a/test/install-detect.test.cjs b/test/install-detect.test.cjs
new file mode 100644
index 0000000..01f21bd
--- /dev/null
+++ b/test/install-detect.test.cjs
@@ -0,0 +1,79 @@
+// Tests de detección de stack. Antes, el pom.xml tenía que contener literalmente "spring-boot",
+// así que un proyecto que solo declara `org.springframework` (BOM, spring-framework sin Boot) no
+// se detectaba; y los `settings.gradle`/`settings.gradle.kts` no se miraban en absoluto.
+// Los casos de `build.gradle`/`build.gradle.kts` sí funcionaban ya: aquí quedan como guardas de
+// regresión, no como casos nuevos.
+// Además, un proyecto Java sin Spring no daba ninguna pista de por qué no se detectaba nada.
+// Correr: `npm test` o `node --test`.
+const { test } = require("node:test");
+const assert = require("node:assert");
+const { spawnSync } = require("node:child_process");
+const fs = require("node:fs");
+const os = require("node:os");
+const path = require("node:path");
+
+const INSTALL = path.join(__dirname, "..", "install.js");
+
+// Crea un proyecto temporal con los archivos dados y corre el instalador en dry-run.
+function detectar(archivos) {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "kit-detect-"));
+ for (const [nombre, contenido] of Object.entries(archivos)) {
+ fs.writeFileSync(path.join(dir, nombre), contenido);
+ }
+ const r = spawnSync("node", [INSTALL, "--yes", "--no-flow", "--no-external", "--dry-run"], {
+ cwd: dir,
+ input: "",
+ encoding: "utf8",
+ });
+ fs.rmSync(dir, { recursive: true, force: true });
+ const m = (r.stdout || "").match(/Stack detectado:\s*(\S+)/);
+ return { stack: m ? m[1] : null, salida: r.stdout || "", status: r.status };
+}
+
+test("detecta Spring por el starter en pom.xml", () => {
+ assert.equal(detectar({ "pom.xml": "spring-boot-starter-web" }).stack, "backend/spring");
+});
+
+// NUEVO: el pom.xml sin la cadena "spring-boot" no se detectaba.
+test("detecta Spring por el groupId org.springframework en pom.xml", () => {
+ assert.equal(detectar({ "pom.xml": "org.springframework" }).stack, "backend/spring");
+});
+
+// NUEVO: los settings.gradle no se miraban.
+test("detecta Spring declarado en settings.gradle.kts", () => {
+ assert.equal(detectar({ "settings.gradle.kts": 'id("org.springframework.boot")' }).stack, "backend/spring");
+});
+
+// Guardas de regresión: estos ya funcionaban antes del cambio.
+test("detecta Spring en Gradle con Kotlin DSL", () => {
+ assert.equal(detectar({ "build.gradle.kts": 'plugins { id("org.springframework.boot") version "3.3.0" }' }).stack, "backend/spring");
+});
+
+test("detecta Spring por el plugin de dependency-management", () => {
+ assert.equal(detectar({ "build.gradle": 'apply plugin: "io.spring.dependency-management"' }).stack, "backend/spring");
+});
+
+test("un proyecto Java SIN Spring no se marca como Spring y explica por qué", () => {
+ const r = detectar({ "pom.xml": "quarkus-resteasy" });
+ assert.equal(r.stack, null, "no debe asumir Spring: el overlay no encajaría");
+ assert.match(r.salida, /proyecto Java\/Kotlin/, "debe decir que vio un proyecto Java");
+ assert.match(r.salida, /backend\/spring/, "debe sugerir el flag si aplica");
+ assert.equal(r.status, 1, "sin stack no debe salir en verde");
+});
+
+test("detecta C# por el .csproj", () => {
+ assert.equal(detectar({ "Api.csproj": "" }).stack, "backend/dotnet");
+});
+
+test("detecta Angular por angular.json", () => {
+ assert.equal(detectar({ "angular.json": "{}" }).stack, "frontend/angular");
+});
+
+test("detecta Angular por la dependencia @angular/core", () => {
+ assert.equal(detectar({ "package.json": '{"dependencies":{"@angular/core":"^20.0.0"}}' }).stack, "frontend/angular");
+});
+
+test("la pista distingue otros ecosistemas sin overlay", () => {
+ assert.match(detectar({ "go.mod": "module x" }).salida, /Go/);
+ assert.match(detectar({ "Cargo.toml": "[package]" }).salida, /Rust/);
+});