claude-code-arcane 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/agents/engineering/dotnet-engineer.md +78 -0
- package/dist/cli.js +284 -72
- package/docs/RELEASE-SETUP.md +99 -0
- package/docs/SKILLS-CATALOG.md +26 -3
- package/docs/presentations/arcane-overview-12.pptx +0 -0
- package/docs/presentations/arcane-overview.pptx +0 -0
- package/docs/presentations/build_arcane_deck.py +310 -0
- package/docs/presentations/build_arcane_deck_12.py +399 -0
- package/package.json +1 -1
- package/profiles/backend-dotnet.yaml +54 -0
- package/profiles/job-hunt.yaml +35 -0
- package/profiles/unity-design.yaml +1 -0
- package/profiles/unity-dev.yaml +1 -0
- package/rules/dotnet-code.md +64 -0
- package/skills/cold-outreach/SKILL.md +65 -0
- package/skills/cold-outreach/references/recruiter-playbook.md +65 -0
- package/skills/cover-letter/SKILL.md +66 -0
- package/skills/cv-ats-export/SKILL.md +64 -0
- package/skills/cv-ats-export/scripts/cv_export.py +306 -0
- package/skills/cv-tailor/SKILL.md +70 -0
- package/skills/cv-tailor/references/ats-keywords.md +46 -0
- package/skills/dotnet-architecture/SKILL.md +66 -0
- package/skills/dotnet-architecture/references/anti-patterns.md +12 -0
- package/skills/dotnet-architecture/references/checklist.md +19 -0
- package/skills/dotnet-architecture/references/patterns.md +118 -0
- package/skills/dotnet-architecture/references/project-structure.md +78 -0
- package/skills/dotnet-best-practices/SKILL.md +76 -0
- package/skills/dotnet-best-practices/references/api-design.md +75 -0
- package/skills/dotnet-best-practices/references/architecture.md +62 -0
- package/skills/dotnet-best-practices/references/async.md +62 -0
- package/skills/dotnet-best-practices/references/database.md +69 -0
- package/skills/dotnet-best-practices/references/dependency-injection.md +73 -0
- package/skills/dotnet-best-practices/references/devops.md +76 -0
- package/skills/dotnet-best-practices/references/error-handling.md +72 -0
- package/skills/dotnet-best-practices/references/performance.md +63 -0
- package/skills/dotnet-best-practices/references/security.md +73 -0
- package/skills/dotnet-best-practices/references/testing.md +76 -0
- package/skills/dotnet-scaffold/SKILL.md +99 -0
- package/skills/install-mcp/SKILL.md +107 -0
- package/skills/install-mcp/references/manual-setup.md +92 -0
- package/skills/interview-prep/SKILL.md +69 -0
- package/skills/interview-prep/references/star-framework.md +42 -0
- package/skills/job-hunt/SKILL.md +92 -0
- package/skills/job-hunt/references/templates/Aplicacion.md +48 -0
- package/skills/job-hunt/references/templates/CV Custom.md +53 -0
- package/skills/job-hunt/references/templates/Contacto.md +30 -0
- package/skills/job-hunt/references/templates/Dashboard.md +45 -0
- package/skills/job-hunt/references/templates/Empresa.md +36 -0
- package/skills/job-hunt/references/templates/Entrevista.md +44 -0
- package/skills/job-hunt/references/templates/Perfil.md +38 -0
- package/skills/job-search/SKILL.md +83 -0
- package/skills/job-search/references/scoring-rubric.md +43 -0
- package/skills/linkedin-optimize/SKILL.md +79 -0
- package/skills/master-profile/SKILL.md +69 -0
- package/skills/network-map/SKILL.md +61 -0
- package/skills/network-map/scripts/network_map.py +109 -0
- package/skills/personal-brand/SKILL.md +54 -0
- package/skills/personal-brand/references/post-pillars.md +66 -0
- package/skills/portfolio-site/SKILL.md +59 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 2. Dependency Injection (CRITICAL)
|
|
2
|
+
|
|
3
|
+
## 2.1 Prefer Constructor Injection (Primary Constructors) — CRITICAL
|
|
4
|
+
|
|
5
|
+
Dependencias explícitas, type-safe y testeables. Con primary constructors (C# 12+) el boilerplate desaparece.
|
|
6
|
+
|
|
7
|
+
**Correcto:**
|
|
8
|
+
```csharp
|
|
9
|
+
public sealed class CreateOrderHandler(IOrderRepository repo, ILogger<CreateOrderHandler> logger)
|
|
10
|
+
{
|
|
11
|
+
public async Task HandleAsync(CreateOrderRequest req, CancellationToken ct)
|
|
12
|
+
{
|
|
13
|
+
logger.LogInformation("Creating order for {Customer}", req.CustomerId);
|
|
14
|
+
await repo.AddAsync(req.ToOrder(), ct);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 2.2 Use Correct Lifetimes — CRITICAL
|
|
20
|
+
|
|
21
|
+
`Scoped` para `DbContext` y handlers (una instancia por request). `Singleton` solo para servicios stateless y thread-safe. `Transient` para servicios livianos sin estado.
|
|
22
|
+
|
|
23
|
+
```csharp
|
|
24
|
+
builder.Services.AddDbContext<AppDbContext>(o => o.UseNpgsql(cs)); // Scoped por defecto
|
|
25
|
+
builder.Services.AddScoped<CreateOrderHandler>();
|
|
26
|
+
builder.Services.AddSingleton<IClock, SystemClock>(); // stateless
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 2.3 Never Inject Scoped into Singleton — CRITICAL
|
|
30
|
+
|
|
31
|
+
Es la "captured dependency": el Singleton retiene una instancia Scoped muerta tras el primer request → datos corruptos o `ObjectDisposedException`. Resolverlo bajo demanda con un scope.
|
|
32
|
+
|
|
33
|
+
**Incorrecto:**
|
|
34
|
+
```csharp
|
|
35
|
+
public sealed class CacheWarmer(AppDbContext db) : IHostedService { } // Singleton captura DbContext Scoped
|
|
36
|
+
```
|
|
37
|
+
**Correcto:**
|
|
38
|
+
```csharp
|
|
39
|
+
public sealed class CacheWarmer(IServiceScopeFactory scopeFactory) : IHostedService
|
|
40
|
+
{
|
|
41
|
+
public async Task StartAsync(CancellationToken ct)
|
|
42
|
+
{
|
|
43
|
+
await using var scope = scopeFactory.CreateAsyncScope();
|
|
44
|
+
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 2.4 No Service Locator / No Manual BuildServiceProvider() — HIGH
|
|
50
|
+
|
|
51
|
+
`provider.GetService<T>()` dentro de la lógica oculta dependencias y rompe los tests. `BuildServiceProvider()` en `Program.cs` crea un container paralelo (singletons duplicados, fugas). Inyectar por constructor (2.1).
|
|
52
|
+
|
|
53
|
+
## 2.5 Register by Interface — HIGH
|
|
54
|
+
|
|
55
|
+
Registrar contra la abstracción permite sustituir implementaciones (tests, features) sin tocar consumidores.
|
|
56
|
+
|
|
57
|
+
```csharp
|
|
58
|
+
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 2.6 IOptions<T> Pattern for Config — MEDIUM-HIGH
|
|
62
|
+
|
|
63
|
+
Bind tipado y validado de configuración, en vez de leer `IConfiguration` con strings por todo el código.
|
|
64
|
+
|
|
65
|
+
```csharp
|
|
66
|
+
builder.Services.AddOptions<JwtOptions>()
|
|
67
|
+
.Bind(builder.Configuration.GetSection("Jwt"))
|
|
68
|
+
.ValidateDataAnnotations()
|
|
69
|
+
.ValidateOnStart();
|
|
70
|
+
// constructor(IOptions<JwtOptions> options) => _opts = options.Value;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
_Ref: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection · https://learn.microsoft.com/en-us/dotnet/core/extensions/options_
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 10. DevOps & Deployment (LOW-MEDIUM)
|
|
2
|
+
|
|
3
|
+
## 10.1 Multi-Stage Dockerfile, Non-Root User — MEDIUM
|
|
4
|
+
|
|
5
|
+
Compilar con la imagen SDK y correr sobre la imagen `aspnet` runtime reduce el tamaño y la superficie de ataque. Usar un usuario no-root limita el blast radius.
|
|
6
|
+
|
|
7
|
+
**Correcto:**
|
|
8
|
+
```dockerfile
|
|
9
|
+
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|
10
|
+
WORKDIR /src
|
|
11
|
+
COPY . .
|
|
12
|
+
RUN dotnet publish -c Release -o /app
|
|
13
|
+
|
|
14
|
+
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
|
15
|
+
WORKDIR /app
|
|
16
|
+
COPY --from=build /app .
|
|
17
|
+
USER $APP_UID
|
|
18
|
+
ENTRYPOINT ["dotnet", "Api.dll"]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 10.2 Health Checks (Liveness/Readiness) — MEDIUM
|
|
22
|
+
|
|
23
|
+
Los orquestadores (Kubernetes) necesitan endpoints separados: liveness (¿está vivo?) y readiness (¿puede recibir tráfico?, p. ej. DB conectada).
|
|
24
|
+
|
|
25
|
+
**Correcto:**
|
|
26
|
+
```csharp
|
|
27
|
+
builder.Services.AddHealthChecks().AddNpgSql(conn, tags: ["ready"]);
|
|
28
|
+
app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
|
|
29
|
+
app.MapHealthChecks("/health/ready", new() { Predicate = c => c.Tags.Contains("ready") });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 10.3 Structured Logging (Serilog JSON) — MEDIUM
|
|
33
|
+
|
|
34
|
+
Logs en JSON son parseables por agregadores (Loki, ELK). Serilog con sink de consola en formato compacto facilita la observabilidad.
|
|
35
|
+
|
|
36
|
+
**Correcto:**
|
|
37
|
+
```csharp
|
|
38
|
+
builder.Host.UseSerilog((ctx, cfg) =>
|
|
39
|
+
cfg.WriteTo.Console(new Serilog.Formatting.Compact.CompactJsonFormatter()));
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 10.4 OpenTelemetry for Traces & Metrics — MEDIUM
|
|
43
|
+
|
|
44
|
+
OpenTelemetry instrumenta traces y métricas de forma estándar y exportable a cualquier backend OTLP.
|
|
45
|
+
|
|
46
|
+
**Correcto:**
|
|
47
|
+
```csharp
|
|
48
|
+
builder.Services.AddOpenTelemetry()
|
|
49
|
+
.WithTracing(t => t.AddAspNetCoreInstrumentation().AddOtlpExporter())
|
|
50
|
+
.WithMetrics(m => m.AddAspNetCoreInstrumentation().AddOtlpExporter());
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 10.5 Config per Environment & Graceful Shutdown — LOW-MEDIUM
|
|
54
|
+
|
|
55
|
+
Nunca hardcodear secrets ni connection strings: usar `appsettings.{Environment}.json` y variables de entorno. Registrar limpieza ante el shutdown con `IHostApplicationLifetime`.
|
|
56
|
+
|
|
57
|
+
**Correcto:**
|
|
58
|
+
```csharp
|
|
59
|
+
var conn = builder.Configuration.GetConnectionString("Default"); // env / appsettings, no hardcoded
|
|
60
|
+
app.Lifetime.ApplicationStopping.Register(() => Log.Information("Draining connections..."));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 10.6 CI: build, test, format — LOW
|
|
64
|
+
|
|
65
|
+
El pipeline debe fallar ante código que no compila, tests rojos o formato inconsistente.
|
|
66
|
+
|
|
67
|
+
**Correcto:**
|
|
68
|
+
```yaml
|
|
69
|
+
steps:
|
|
70
|
+
- run: dotnet restore
|
|
71
|
+
- run: dotnet build --no-restore -c Release
|
|
72
|
+
- run: dotnet test --no-build -c Release
|
|
73
|
+
- run: dotnet format --verify-no-changes
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
_Ref: https://learn.microsoft.com/en-us/dotnet/core/docker/build-container · https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks · https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel · https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host_
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 4. Error Handling (HIGH)
|
|
2
|
+
|
|
3
|
+
## 4.1 Global IExceptionHandler + ProblemDetails — CRITICAL
|
|
4
|
+
|
|
5
|
+
Respuestas de error consistentes y estandarizadas (RFC 7807) sin try/catch repetido en cada endpoint. Registrar `AddProblemDetails()` y un `IExceptionHandler`.
|
|
6
|
+
|
|
7
|
+
**Correcto:**
|
|
8
|
+
```csharp
|
|
9
|
+
public sealed class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
|
|
10
|
+
{
|
|
11
|
+
public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
|
|
12
|
+
{
|
|
13
|
+
logger.LogError(ex, "Unhandled exception on {Path}", ctx.Request.Path);
|
|
14
|
+
await Results.Problem(statusCode: StatusCodes.Status500InternalServerError, title: "An error occurred")
|
|
15
|
+
.ExecuteAsync(ctx);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Program.cs
|
|
20
|
+
builder.Services.AddProblemDetails();
|
|
21
|
+
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
|
|
22
|
+
app.UseExceptionHandler();
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 4.2 Don't Leak Stack Traces — CRITICAL
|
|
26
|
+
|
|
27
|
+
En producción no exponer `ex.Message`, stack traces ni detalles internos: filtran rutas, versiones y vectores de ataque. Loggear el detalle, devolver un mensaje genérico (ver 4.1).
|
|
28
|
+
|
|
29
|
+
## 4.3 Result Pattern for Expected Failures — HIGH
|
|
30
|
+
|
|
31
|
+
Fallos esperados (validación, "not found", reglas de negocio) no son excepciones: las excepciones son caras y para lo excepcional. Devolver un `Result`.
|
|
32
|
+
|
|
33
|
+
**Incorrecto:**
|
|
34
|
+
```csharp
|
|
35
|
+
if (order is null) throw new NotFoundException(); // control de flujo con excepciones
|
|
36
|
+
```
|
|
37
|
+
**Correcto:**
|
|
38
|
+
```csharp
|
|
39
|
+
public readonly record struct Result<T>(bool IsSuccess, T? Value, string? Error)
|
|
40
|
+
{
|
|
41
|
+
public static Result<T> Ok(T value) => new(true, value, null);
|
|
42
|
+
public static Result<T> Fail(string error) => new(false, default, error);
|
|
43
|
+
}
|
|
44
|
+
return order is null ? Result<Order>.Fail("Order not found") : Result<Order>.Ok(order);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 4.4 Validation → ValidationProblemDetails — HIGH
|
|
48
|
+
|
|
49
|
+
Los errores de validación se devuelven como `ValidationProblemDetails` (400) con el mapa campo→errores, no como 500.
|
|
50
|
+
|
|
51
|
+
```csharp
|
|
52
|
+
return Results.ValidationProblem(new Dictionary<string, string[]>
|
|
53
|
+
{
|
|
54
|
+
["email"] = ["Email is required"]
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 4.5 No Empty Catch; Log With Context — MEDIUM-HIGH
|
|
59
|
+
|
|
60
|
+
`catch {}` oculta fallos y vuelve la app indebuggeable. Capturar tipos específicos, loggear con structured logging y re-lanzar o devolver un `Result`.
|
|
61
|
+
|
|
62
|
+
**Incorrecto:**
|
|
63
|
+
```csharp
|
|
64
|
+
try { await Pay(ct); } catch { } // se traga el error
|
|
65
|
+
```
|
|
66
|
+
**Correcto:**
|
|
67
|
+
```csharp
|
|
68
|
+
try { await Pay(ct); }
|
|
69
|
+
catch (PaymentException ex) { logger.LogError(ex, "Payment failed for {OrderId}", orderId); throw; }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
_Ref: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling · https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors_
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# 6. Performance (HIGH)
|
|
2
|
+
|
|
3
|
+
## 6.1 Output Caching & HybridCache — HIGH
|
|
4
|
+
|
|
5
|
+
Cachear respuestas caras evita recomputar en cada request. `HybridCache` combina cache en memoria (L1) y distribuida (L2) con stampede protection.
|
|
6
|
+
|
|
7
|
+
**Incorrecto:**
|
|
8
|
+
```csharp
|
|
9
|
+
app.MapGet("/stats", async (StatsService s) => await s.ComputeAsync()); // recomputa siempre
|
|
10
|
+
```
|
|
11
|
+
**Correcto:**
|
|
12
|
+
```csharp
|
|
13
|
+
builder.Services.AddHybridCache();
|
|
14
|
+
|
|
15
|
+
app.MapGet("/stats", async (HybridCache cache, StatsService s, CancellationToken ct) =>
|
|
16
|
+
await cache.GetOrCreateAsync("stats", async token => await s.ComputeAsync(token), cancellationToken: ct));
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 6.2 Response Compression — HIGH
|
|
20
|
+
|
|
21
|
+
Comprimir respuestas reduce el ancho de banda y la latencia percibida en payloads JSON grandes.
|
|
22
|
+
|
|
23
|
+
**Correcto:**
|
|
24
|
+
```csharp
|
|
25
|
+
builder.Services.AddResponseCompression(o => o.EnableForHttps = true);
|
|
26
|
+
var app = builder.Build();
|
|
27
|
+
app.UseResponseCompression();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 6.3 Never Block on Async (sync-over-async) — HIGH
|
|
31
|
+
|
|
32
|
+
`.Result` o `.Wait()` bloquean un hilo del thread pool y pueden causar deadlocks y thread starvation bajo carga.
|
|
33
|
+
|
|
34
|
+
**Incorrecto:**
|
|
35
|
+
```csharp
|
|
36
|
+
var user = _repo.GetUserAsync(id).Result; // bloquea el hilo
|
|
37
|
+
```
|
|
38
|
+
**Correcto:**
|
|
39
|
+
```csharp
|
|
40
|
+
var user = await _repo.GetUserAsync(id);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 6.4 DbContext Pooling — MEDIUM-HIGH
|
|
44
|
+
|
|
45
|
+
Reusar instancias de `DbContext` desde un pool reduce las allocations y el costo de setup por request.
|
|
46
|
+
|
|
47
|
+
**Correcto:**
|
|
48
|
+
```csharp
|
|
49
|
+
builder.Services.AddDbContextPool<AppDbContext>(o =>
|
|
50
|
+
o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 6.5 Stream Large Results with IAsyncEnumerable — MEDIUM
|
|
54
|
+
|
|
55
|
+
Devolver `IAsyncEnumerable<T>` transmite filas a medida que llegan en vez de materializar toda la colección en memoria.
|
|
56
|
+
|
|
57
|
+
**Correcto:**
|
|
58
|
+
```csharp
|
|
59
|
+
app.MapGet("/orders", (AppDbContext db) =>
|
|
60
|
+
db.Orders.AsNoTracking().AsAsyncEnumerable()); // streaming, server GC ayuda en cargas altas
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
_Ref: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid · https://learn.microsoft.com/en-us/aspnet/core/performance/response-compression · https://learn.microsoft.com/en-us/ef/core/performance/advanced-performance-topics_
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 5. Security (HIGH)
|
|
2
|
+
|
|
3
|
+
## 5.1 Short-Lived JWT + Refresh Tokens — CRITICAL
|
|
4
|
+
|
|
5
|
+
Access tokens cortos (5-15 min) limitan la ventana si se filtra uno; el refresh token (revocable, server-side) renueva sin re-login. Tokens de larga vida no se pueden invalidar.
|
|
6
|
+
|
|
7
|
+
```csharp
|
|
8
|
+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
9
|
+
.AddJwtBearer(o => o.TokenValidationParameters = new()
|
|
10
|
+
{
|
|
11
|
+
ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true,
|
|
12
|
+
ValidateIssuerSigningKey = true, ClockSkew = TimeSpan.FromSeconds(30)
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 5.2 Secrets in User-Secrets / Key Vault — CRITICAL
|
|
17
|
+
|
|
18
|
+
Nunca claves ni connection strings en `appsettings.json` (van al repo). En dev `dotnet user-secrets`; en prod Azure Key Vault o variables de entorno.
|
|
19
|
+
|
|
20
|
+
**Incorrecto:**
|
|
21
|
+
```json
|
|
22
|
+
{ "Jwt": { "Key": "super-secret-signing-key" } } // en el repo
|
|
23
|
+
```
|
|
24
|
+
**Correcto:**
|
|
25
|
+
```csharp
|
|
26
|
+
// dotnet user-secrets set "Jwt:Key" "..." (dev)
|
|
27
|
+
builder.Configuration.AddAzureKeyVault(vaultUri, new DefaultAzureCredential()); // prod
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 5.3 Authorize by Policies, Not Manual Role Checks — HIGH
|
|
31
|
+
|
|
32
|
+
Las policies centralizan la regla de autorización; los `if (user.Role == "Admin")` dispersos se desincronizan y se olvidan.
|
|
33
|
+
|
|
34
|
+
**Incorrecto:**
|
|
35
|
+
```csharp
|
|
36
|
+
if (!ctx.User.IsInRole("Admin")) return Results.Forbid(); // lógica esparcida
|
|
37
|
+
```
|
|
38
|
+
**Correcto:**
|
|
39
|
+
```csharp
|
|
40
|
+
builder.Services.AddAuthorizationBuilder()
|
|
41
|
+
.AddPolicy("CanManageOrders", p => p.RequireRole("Admin").RequireClaim("scope", "orders:write"));
|
|
42
|
+
app.MapPost("/orders", Handler).RequireAuthorization("CanManageOrders");
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 5.4 HTTPS, HSTS and Rate Limiting — HIGH
|
|
46
|
+
|
|
47
|
+
Forzar HTTPS + HSTS evita downgrade/MITM. El middleware `RateLimiter` integrado protege de abuso y brute-force.
|
|
48
|
+
|
|
49
|
+
```csharp
|
|
50
|
+
builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter("api", w =>
|
|
51
|
+
{ w.PermitLimit = 100; w.Window = TimeSpan.FromMinutes(1); }));
|
|
52
|
+
app.UseHsts();
|
|
53
|
+
app.UseHttpsRedirection();
|
|
54
|
+
app.UseRateLimiter();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 5.5 Don't Log Sensitive Data; Validate Input — HIGH
|
|
58
|
+
|
|
59
|
+
Nunca loggear passwords, tokens ni PII. Validar todo input con FluentValidation. Las queries de EF Core son parametrizadas por defecto (no concatenar SQL crudo) y `[ValidateAntiForgeryToken]` donde haya cookies/formularios.
|
|
60
|
+
|
|
61
|
+
```csharp
|
|
62
|
+
public sealed class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
|
|
63
|
+
{
|
|
64
|
+
public CreateOrderValidator()
|
|
65
|
+
{
|
|
66
|
+
RuleFor(x => x.CustomerId).NotEmpty();
|
|
67
|
+
RuleFor(x => x.Items).NotEmpty();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// EF Core parametriza: db.Users.Where(u => u.Email == input) — nunca string-interpolated FromSqlRaw
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
_Ref: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication · https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit_
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 8. Testing (MEDIUM-HIGH)
|
|
2
|
+
|
|
3
|
+
## 8.1 Integration Tests with WebApplicationFactory — HIGH
|
|
4
|
+
|
|
5
|
+
`WebApplicationFactory<T>` levanta la app en memoria con el pipeline real (DI, middleware, routing), probando el comportamiento de verdad y no mocks.
|
|
6
|
+
|
|
7
|
+
**Correcto:**
|
|
8
|
+
```csharp
|
|
9
|
+
public class OrdersApiTests(WebApplicationFactory<Program> factory)
|
|
10
|
+
: IClassFixture<WebApplicationFactory<Program>>
|
|
11
|
+
{
|
|
12
|
+
[Fact]
|
|
13
|
+
public async Task GetOrders_ReturnsOk()
|
|
14
|
+
{
|
|
15
|
+
var client = factory.CreateClient();
|
|
16
|
+
var response = await client.GetAsync("/api/v1/orders");
|
|
17
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 8.2 Real Postgres via Testcontainers — HIGH
|
|
23
|
+
|
|
24
|
+
El provider in-memory miente sobre el comportamiento relacional (transacciones, constraints, SQL). Testcontainers levanta un Postgres real y desechable.
|
|
25
|
+
|
|
26
|
+
**Incorrecto:**
|
|
27
|
+
```csharp
|
|
28
|
+
options.UseInMemoryDatabase("test"); // no valida FKs, constraints ni SQL real
|
|
29
|
+
```
|
|
30
|
+
**Correcto:**
|
|
31
|
+
```csharp
|
|
32
|
+
var pg = new PostgreSqlBuilder().WithImage("postgres:17").Build();
|
|
33
|
+
await pg.StartAsync();
|
|
34
|
+
options.UseNpgsql(pg.GetConnectionString());
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 8.3 Don't Mock DbContext — MEDIUM-HIGH
|
|
38
|
+
|
|
39
|
+
Mockear `DbContext` o `IQueryable` reimplementa LINQ-to-SQL en LINQ-to-Objects y da falsos verdes. Probar handlers/slices contra una base real.
|
|
40
|
+
|
|
41
|
+
**Incorrecto:**
|
|
42
|
+
```csharp
|
|
43
|
+
var mock = new Mock<AppDbContext>(); // no traduce queries como el provider real
|
|
44
|
+
```
|
|
45
|
+
**Correcto:**
|
|
46
|
+
```csharp
|
|
47
|
+
var handler = new CreateOrderHandler(realDbContext);
|
|
48
|
+
var result = await handler.Handle(command, CancellationToken.None);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 8.4 Arrange-Act-Assert with FluentAssertions — MEDIUM
|
|
52
|
+
|
|
53
|
+
Estructurar cada test en AAA y usar `FluentAssertions` para aserciones legibles y mensajes de error claros.
|
|
54
|
+
|
|
55
|
+
**Correcto:**
|
|
56
|
+
```csharp
|
|
57
|
+
[Fact]
|
|
58
|
+
public void Discount_AppliesPercentage()
|
|
59
|
+
{
|
|
60
|
+
var order = new Order(100m); // Arrange
|
|
61
|
+
order.ApplyDiscount(0.10m); // Act
|
|
62
|
+
order.Total.Should().Be(90m); // Assert
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 8.5 Deterministic Tests — MEDIUM
|
|
67
|
+
|
|
68
|
+
Evitar `DateTime.Now`, GUIDs aleatorios o dependencias de orden: inyectar `TimeProvider` para que los tests sean reproducibles.
|
|
69
|
+
|
|
70
|
+
**Correcto:**
|
|
71
|
+
```csharp
|
|
72
|
+
var time = new FakeTimeProvider(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
|
73
|
+
var service = new SubscriptionService(time); // sin reloj real, resultado estable
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
_Ref: https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests · https://learn.microsoft.com/en-us/dotnet/core/testing/ · https://learn.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/test-aspnet-core-services-web-apps_
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dotnet-scaffold
|
|
3
|
+
description: "Scaffold de API ASP.NET Core (.NET 10) production-ready: Vertical Slice o Clean Architecture, Minimal APIs, EF Core + PostgreSQL, JWT + Identity, ProblemDetails, health checks y testing. Usar para iniciar un backend .NET nuevo."
|
|
4
|
+
category: "backend"
|
|
5
|
+
argument-hint: "[project-name]"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Bash, Write, Edit, Task
|
|
8
|
+
---
|
|
9
|
+
# dotnet-scaffold — ASP.NET Core API Scaffolder
|
|
10
|
+
|
|
11
|
+
## MANDATORY WORKFLOW
|
|
12
|
+
|
|
13
|
+
**Antes de generar cualquier código, completar estos pasos en orden.** Confirmar las decisiones del Step 0 con el usuario antes de escribir archivos (Question → Decision → Approval).
|
|
14
|
+
|
|
15
|
+
### Step 0: Gather Requirements
|
|
16
|
+
1. **Nombre del servicio** (PascalCase para la solución, ej. `OrdersApi`)
|
|
17
|
+
2. **Arquitectura:** Vertical Slice (default) / Clean Architecture — ver skill `dotnet-architecture` para elegir
|
|
18
|
+
3. **Transport:** Web API REST con Minimal APIs (default) / Controllers / gRPC
|
|
19
|
+
4. **ORM:** EF Core (default) / Dapper
|
|
20
|
+
5. **DB:** PostgreSQL (default) / SQL Server
|
|
21
|
+
6. **Auth:** JWT + ASP.NET Identity (default) / OAuth / ninguna
|
|
22
|
+
7. **Deploy:** Docker (default) / Azure App Service
|
|
23
|
+
|
|
24
|
+
> Si el dominio es CRUD/simple → **Vertical Slice**. Si es complejo y long-lived con varios devs → **Clean Architecture**.
|
|
25
|
+
|
|
26
|
+
### Step 1: Crear base
|
|
27
|
+
```bash
|
|
28
|
+
dotnet new sln -n <Name>
|
|
29
|
+
dotnet new webapi -n <Name>.Api # Minimal APIs (default en .NET 8+); para Controllers: --use-controllers
|
|
30
|
+
dotnet sln add <Name>.Api
|
|
31
|
+
dotnet new xunit -n <Name>.Tests && dotnet sln add <Name>.Tests
|
|
32
|
+
cd <Name>.Api
|
|
33
|
+
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
|
|
34
|
+
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
|
|
35
|
+
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
|
36
|
+
dotnet add package FluentValidation.DependencyInjectionExtensions # core + DI; FluentValidation.AspNetCore (auto-validation) está deprecado
|
|
37
|
+
dotnet add package Serilog.AspNetCore
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Step 2: Estructura
|
|
41
|
+
|
|
42
|
+
**Vertical Slice (default)** — un folder por feature/use-case:
|
|
43
|
+
```
|
|
44
|
+
src/
|
|
45
|
+
Features/
|
|
46
|
+
Orders/ { CreateOrder.cs, GetOrder.cs, OrderEndpoints.cs } # request+handler+validator+endpoint por slice
|
|
47
|
+
Users/ { ... }
|
|
48
|
+
Infrastructure/ { AppDbContext.cs, Migrations/ }
|
|
49
|
+
Common/ { Behaviors/, Results/, ProblemDetails/ }
|
|
50
|
+
Program.cs
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Clean Architecture** — proyectos por capa (deps apuntan hacia adentro):
|
|
54
|
+
```
|
|
55
|
+
<Name>.Domain/ # entities, value objects — cero deps externas
|
|
56
|
+
<Name>.Application/ # use cases, interfaces (IAppDbContext), DTOs
|
|
57
|
+
<Name>.Infrastructure/ # EF Core, Identity, implementaciones
|
|
58
|
+
<Name>.Api/ # endpoints/controllers, DI, Program.cs
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Step 3: Defaults no negociables (`Program.cs` / `.csproj`)
|
|
62
|
+
- `<Nullable>enable</Nullable>` + `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` en el `.csproj`
|
|
63
|
+
- **Async end-to-end**: handlers `async Task<>`, `CancellationToken` propagado a EF Core y HTTP; nunca `.Result`/`.Wait()`
|
|
64
|
+
- **DI por constructor** (primary constructors); registrar servicios con scope correcto (`AddScoped` para DbContext/handlers)
|
|
65
|
+
- **Validación**: FluentValidation por slice, invocada manualmente (pipeline behavior MediatR o endpoint filter — no auto-validation) → responder `ValidationProblemDetails` (RFC 7807)
|
|
66
|
+
- **Errores**: `AddProblemDetails()` + exception handler global → `ProblemDetails` consistente, sin filtrar stack traces
|
|
67
|
+
- **EF Core**: migraciones versionadas (`dotnet ef migrations add`), **nunca** `EnsureCreated()` en prod; `DbContext` como Unit of Work (sin repos genéricos salvo necesidad)
|
|
68
|
+
- **Auth**: JWT de vida corta + refresh, secretos en `user-secrets`/secret manager, authorization por policies declarativas
|
|
69
|
+
- **Observabilidad**: Serilog structured logging (JSON) + health checks (`AddHealthChecks`) + OpenTelemetry opcional
|
|
70
|
+
- **Graceful shutdown**: respetar `IHostApplicationLifetime` / `CancellationToken` del host
|
|
71
|
+
|
|
72
|
+
### Step 4: Agregar piezas (invocar skills relacionadas)
|
|
73
|
+
- `dotnet-architecture` — estructura de slices/capas en detalle
|
|
74
|
+
- `database` / `data-migrations` — modelado EF Core + migraciones
|
|
75
|
+
- `jwt-strategy` / `auth-strategy` / `rbac-abac` — auth y authorization
|
|
76
|
+
- `api-design` / `api-versioning` / `api-docs` — contrato REST + OpenAPI
|
|
77
|
+
- `testing` / `contract-testing` — xUnit + Testcontainers (Postgres real en tests)
|
|
78
|
+
|
|
79
|
+
### Step 5: Verificar
|
|
80
|
+
`dotnet build` + `dotnet test`. Revisar contra la rule `dotnet-code` y el skill `dotnet-best-practices`. Build + tests verdes → scaffold **READY**.
|
|
81
|
+
|
|
82
|
+
## Stack Defaults
|
|
83
|
+
|
|
84
|
+
| Componente | Default |
|
|
85
|
+
|------------|---------|
|
|
86
|
+
| Runtime | .NET 10 LTS (C# 14) |
|
|
87
|
+
| Framework | ASP.NET Core — Minimal APIs |
|
|
88
|
+
| Arquitectura | Vertical Slice (Clean opcional) |
|
|
89
|
+
| ORM | EF Core 10 |
|
|
90
|
+
| DB | PostgreSQL (Npgsql) |
|
|
91
|
+
| Auth | JWT + ASP.NET Identity |
|
|
92
|
+
| Validación | FluentValidation → ProblemDetails |
|
|
93
|
+
| Logging | Serilog (JSON) |
|
|
94
|
+
| Testing | xUnit + Testcontainers |
|
|
95
|
+
| Deploy | Docker |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
_Inspirado en [dotnet/skills](https://github.com/dotnet/skills) (oficial Microsoft), [github/awesome-copilot](https://github.com/github/awesome-copilot) (dotnet-best-practices) y los templates [ardalis/CleanArchitecture](https://github.com/ardalis/CleanArchitecture) y [nadirbad/VerticalSliceArchitecture](https://github.com/nadirbad/VerticalSliceArchitecture). Adaptado al formato Arcane._
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: install-mcp
|
|
3
|
+
description: "Instala y registra el MCP de Unity (CoplayDev MCP for Unity) en un proyecto Unity + Claude Code: agrega el package UPM, registra el server MCP y verifica la conexion. Usar para: instalar mcp unity, conectar Claude con Unity, setup MCP Unity."
|
|
4
|
+
category: "gamedev"
|
|
5
|
+
argument-hint: "[--secondary] [project-path]"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Bash, Write, Edit
|
|
8
|
+
---
|
|
9
|
+
# install-mcp — Unity MCP Setup
|
|
10
|
+
|
|
11
|
+
Instala el package MCP del lado Unity y registra el server MCP en Claude Code, replicando el setup usado en los proyectos de tesis (TesisUade).
|
|
12
|
+
|
|
13
|
+
- **Package primario:** `com.coplaydev.unity-mcp` (CoplayDev "MCP for Unity"). Server Python vía `uv`/`uvx`, transport HTTP en `127.0.0.1:8080`. El server se levanta y auto-configura desde Unity en `Window → MCP for Unity`.
|
|
14
|
+
- **Package secundario (opcional, flag `--secondary`):** `com.gamelovers.mcp-unity` (CoderGamester). Server Node.
|
|
15
|
+
|
|
16
|
+
> El flujo de Unity (`Window → MCP for Unity`) requiere la GUI del Editor — Claude no puede manejarla. La skill automatiza lo que sí puede (editar `manifest.json`, registrar el cliente MCP, verificar) y **guía** el paso del Editor.
|
|
17
|
+
|
|
18
|
+
## Input
|
|
19
|
+
|
|
20
|
+
- `project-path` (opcional): raíz del proyecto Unity. Default: directorio actual.
|
|
21
|
+
- `--secondary` (opcional): además del primario, instala el package CoderGamester.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Workflow
|
|
26
|
+
|
|
27
|
+
### 1. Detectar proyecto Unity
|
|
28
|
+
|
|
29
|
+
Desde `project-path` (o cwd), confirmar que existen:
|
|
30
|
+
- `Packages/manifest.json`
|
|
31
|
+
- `ProjectSettings/ProjectVersion.txt`
|
|
32
|
+
|
|
33
|
+
Si falta alguno, **abortar** con: "No parece un proyecto Unity (falta Packages/manifest.json o ProjectSettings/ProjectVersion.txt). Pasá la ruta del proyecto con `/install-mcp <project-path>`."
|
|
34
|
+
|
|
35
|
+
### 2. Verificar prerrequisitos
|
|
36
|
+
|
|
37
|
+
- `uv --version` y `uvx --version` (server del primario). Si falta `uv`, **no abortar**: avisar y dar el comando de install y seguir editando el manifest:
|
|
38
|
+
- Windows: `winget install astral-sh.uv`
|
|
39
|
+
- alternativa: `pipx install uv`
|
|
40
|
+
- Solo con `--secondary`: `node --version`. Si falta, avisar (el server CoderGamester no podrá correr).
|
|
41
|
+
|
|
42
|
+
### 3. Agregar el package UPM (lado Unity)
|
|
43
|
+
|
|
44
|
+
Leer `Packages/manifest.json`. Dentro de `"dependencies"`, agregar **solo si la key no existe** (idempotente — nunca duplicar):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
"com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#main"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Con `--secondary`, agregar también:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
"com.gamelovers.mcp-unity": "https://github.com/CoderGamester/mcp-unity.git"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Usar `Edit` puntual sobre el bloque `dependencies` preservando el JSON válido (indentación, comas). Si la key ya estaba, reportar "ya presente, sin cambios".
|
|
57
|
+
|
|
58
|
+
> **Antes de escribir**, mostrar el diff propuesto sobre `manifest.json` (y el `.mcp.json` del paso 4B si aplica) y pedir aprobación al usuario. No editar sin confirmación.
|
|
59
|
+
|
|
60
|
+
### 4. Registrar el server MCP en Claude Code
|
|
61
|
+
|
|
62
|
+
Dos caminos, en orden de preferencia:
|
|
63
|
+
|
|
64
|
+
**A. Recomendado (auto-config desde Unity).** Instruir al usuario:
|
|
65
|
+
1. Abrir el proyecto en Unity (Unity importará el package nuevo).
|
|
66
|
+
2. `Window → MCP for Unity`.
|
|
67
|
+
3. En esa ventana, usar el botón de auto-configuración para **Claude Code** — instala el server `uv` y registra el cliente MCP automáticamente.
|
|
68
|
+
|
|
69
|
+
**B. Fallback manual.** Si el usuario no puede usar el Editor ahora, registrar el transport HTTP que usa CoplayDev:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
claude mcp add --transport http unity-mcp http://127.0.0.1:8080
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
(equivalente: escribir un `.mcp.json` de proyecto con
|
|
76
|
+
`{"mcpServers":{"unity-mcp":{"type":"http","url":"http://127.0.0.1:8080"}}}`).
|
|
77
|
+
|
|
78
|
+
> El server solo responde cuando Unity está abierto con `MCP for Unity` activo. Registrar el cliente no levanta el server.
|
|
79
|
+
|
|
80
|
+
### 5. Verificar
|
|
81
|
+
|
|
82
|
+
Correr:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
claude mcp list
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Reportar si `unity-mcp` aparece **conectado**. Seguir la guía de TesisUade: **si NO está conectado, avisar explícitamente** y aclarar que las operaciones de engine (scenes, SOs, prefabs, UI wiring) NO se aplicaron — no asumir éxito.
|
|
89
|
+
|
|
90
|
+
### 6. Resumen
|
|
91
|
+
|
|
92
|
+
Reportar:
|
|
93
|
+
- Qué keys se agregaron a `manifest.json` (o si ya estaban).
|
|
94
|
+
- Estado del registro MCP (auto-config pendiente en Unity vs. fallback aplicado).
|
|
95
|
+
- Resultado de `claude mcp list`.
|
|
96
|
+
- Próximo paso manual: abrir Unity → `Window → MCP for Unity`.
|
|
97
|
+
|
|
98
|
+
**Verdict:**
|
|
99
|
+
- **READY** — package en `manifest.json` + cliente registrado + `unity-mcp` conectado en `claude mcp list`.
|
|
100
|
+
- **PENDING** — package y cliente listos, pero falta abrir Unity (`Window → MCP for Unity`) para que el server conecte. Avisar que las operaciones de engine aún NO están disponibles.
|
|
101
|
+
- **BLOCKED** — falta un prerrequisito (`uv` ausente) o no es un proyecto Unity válido. Indicar el fix.
|
|
102
|
+
|
|
103
|
+
Tras conectar, seguir con `/unity-game-architecture` o `/scaffold-unity` para trabajar el proyecto.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
> → Read references/manual-setup.md for [pasos detallados del flujo Window → MCP for Unity, troubleshooting (uv no encontrado, puerto 8080 ocupado, MCP no conecta) y la variante secundaria CoderGamester]
|