specleap-framework 2.1.0 → 2.1.5

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.
Files changed (75) hide show
  1. package/.agents/backend.md +3 -3
  2. package/.agents/frontend.md +2 -2
  3. package/.agents/producto.md +9 -11
  4. package/.claude/hooks/spec-guard.sh +132 -0
  5. package/.claude/settings.json.template +15 -0
  6. package/.clinerules +3 -3
  7. package/.coderabbit.yaml +2 -4
  8. package/.commands/compliance.md +89 -0
  9. package/.commands/inicio.md +15 -15
  10. package/.commands/nuevo/README.md +2 -2
  11. package/.commands/planificar.md +1 -1
  12. package/.continue/rules/04-git-workflow.md +5 -5
  13. package/.continuerules +3 -4
  14. package/.cursorrules +1 -1
  15. package/.github/copilot-instructions.md +1 -1
  16. package/.specleap/i18n/en.json +177 -0
  17. package/.specleap/i18n/es.json +177 -0
  18. package/.specleap/i18n.sh +63 -0
  19. package/CHANGELOG.md +276 -0
  20. package/CLAUDE.md +54 -13
  21. package/README.md +169 -528
  22. package/SETUP.md +16 -13
  23. package/openspec/INDEX.md +53 -0
  24. package/openspec/README.md +104 -0
  25. package/openspec/SPEC-FORMAT.md +168 -0
  26. package/openspec/changes/.gitkeep +0 -0
  27. package/openspec/cli/COMMAND_REFERENCE.md +817 -0
  28. package/openspec/cli/README.md +189 -0
  29. package/openspec/cli/apply.sh +229 -0
  30. package/openspec/cli/archive.sh +240 -0
  31. package/openspec/cli/code-review.sh +207 -0
  32. package/openspec/cli/common.sh +171 -0
  33. package/openspec/cli/enrich.sh +188 -0
  34. package/openspec/cli/ff.sh +329 -0
  35. package/openspec/cli/new.sh +260 -0
  36. package/openspec/cli/openspec +82 -0
  37. package/openspec/cli/report.sh +244 -0
  38. package/openspec/cli/status.sh +178 -0
  39. package/openspec/cli/verify.sh +246 -0
  40. package/openspec/config.yaml +76 -0
  41. package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/00-original-user-story.md +5 -0
  42. package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/01-refined-user-story.md +106 -0
  43. package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/README.md +333 -0
  44. package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/design.md +461 -0
  45. package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/proposal.md +124 -0
  46. package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/specs/functional/F001-authentication.spec.md +399 -0
  47. package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/specs/technical/T001-jwt-implementation.spec.md +606 -0
  48. package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/tasks.md +433 -0
  49. package/openspec/examples/MERMAID_DIAGRAMS.md +481 -0
  50. package/openspec/examples/README.md +334 -0
  51. package/openspec/specs/functional/.gitkeep +0 -0
  52. package/openspec/specs/integration/.gitkeep +0 -0
  53. package/openspec/specs/security/.gitkeep +0 -0
  54. package/openspec/specs/technical/.gitkeep +0 -0
  55. package/openspec/templates/.coderabbit.yaml +259 -0
  56. package/openspec/templates/design.md +181 -0
  57. package/openspec/templates/proposal.md +79 -0
  58. package/openspec/templates/tasks.md +193 -0
  59. package/package.json +10 -5
  60. package/rules/git-workflow.md +3 -3
  61. package/rules/session-protocol.md +3 -3
  62. package/scripts/README.md +13 -25
  63. package/scripts/compliance-audit.sh +325 -0
  64. package/scripts/generate-contract.sh +4 -4
  65. package/scripts/install-skills.sh +12 -11
  66. package/scripts/lib/render-contrato.py +1 -1
  67. package/scripts/quality-baseline.sh +210 -0
  68. package/scripts/quality-healing.sh +241 -0
  69. package/setup.sh +3 -3
  70. package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc +0 -0
  71. package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc +0 -0
  72. package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/search.cpython-314.pyc +0 -0
  73. package/scripts/lib/jira-project-utils.sh +0 -222
  74. package/scripts/setup-mcp.sh +0 -654
  75. package/scripts/test-cuestionario.sh +0 -428
@@ -0,0 +1,606 @@
1
+ # T001 — Implementación JWT
2
+
3
+ | Campo | Valor |
4
+ |-------|-------|
5
+ | ID | T001 |
6
+ | Título | Implementación de Autenticación JWT |
7
+ | Dominio | technical |
8
+ | Estado | implemented |
9
+ | Autor | SpecLeap Contributor |
10
+ | Fecha Creación | 2026-02-12 |
11
+ | Última Actualización | 2026-02-12 |
12
+ | Versión | 1.0 |
13
+ | Relacionada | F001-authentication |
14
+
15
+ ## Descripción
16
+
17
+ Especificación técnica de la implementación de autenticación basada en JSON Web Tokens (JWT) con algoritmo RS256, gestión de claves RSA, y middleware de autorización.
18
+
19
+ ## Arquitectura de Componentes
20
+
21
+ ### Componentes Implementados
22
+
23
+ ```
24
+ AuthController (API Layer)
25
+ ├── AuthService (Business Logic)
26
+ │ ├── UserRepository (Data Access)
27
+ │ └── JWTService (Token Management)
28
+ │ └── RSA Keys (public/private)
29
+
30
+ ├── RateLimitMiddleware
31
+ │ └── LoginAttemptsRepository
32
+
33
+ └── AuthMiddleware (Request Guard)
34
+ └── JWTService (validation)
35
+ ```
36
+
37
+ ## Especificación de Clases
38
+
39
+ ### JWTService
40
+
41
+ **Responsabilidad:** Generación y validación de tokens JWT.
42
+
43
+ #### Métodos
44
+
45
+ ```php
46
+ class JWTService
47
+ {
48
+ /**
49
+ * Genera un token JWT para un usuario.
50
+ *
51
+ * @param User $user Usuario autenticado
52
+ * @param bool $remember Extender expiración a 30 días
53
+ * @return string Token JWT firmado
54
+ */
55
+ public function generateToken(User $user, bool $remember = false): string;
56
+
57
+ /**
58
+ * Valida un token JWT.
59
+ *
60
+ * @param string $token Token a validar
61
+ * @return array Payload decodificado
62
+ * @throws TokenExpiredException Si el token expiró
63
+ * @throws TokenInvalidException Si la firma no es válida
64
+ */
65
+ public function validateToken(string $token): array;
66
+
67
+ /**
68
+ * Refresca un token JWT.
69
+ *
70
+ * @param string $token Token actual
71
+ * @return string Nuevo token
72
+ */
73
+ public function refreshToken(string $token): string;
74
+
75
+ /**
76
+ * Revoca un token (añade a blacklist).
77
+ *
78
+ * @param string $token Token a revocar
79
+ * @return void
80
+ */
81
+ public function revokeToken(string $token): void;
82
+ }
83
+ ```
84
+
85
+ #### Estructura del Token JWT
86
+
87
+ **Header:**
88
+ ```json
89
+ {
90
+ "alg": "RS256",
91
+ "typ": "JWT"
92
+ }
93
+ ```
94
+
95
+ **Payload:**
96
+ ```json
97
+ {
98
+ "sub": 123, // user_id
99
+ "email": "user@example.com",
100
+ "iat": 1707753600, // issued at (Unix timestamp)
101
+ "exp": 1707840000, // expiration (Unix timestamp)
102
+ "remember": false // opcional
103
+ }
104
+ ```
105
+
106
+ **Signature:**
107
+ ```
108
+ RSASHA256(
109
+ base64UrlEncode(header) + "." +
110
+ base64UrlEncode(payload),
111
+ privateKey
112
+ )
113
+ ```
114
+
115
+ #### Configuración de Claves RSA
116
+
117
+ **Generación:**
118
+ ```bash
119
+ # Private key (4096 bits)
120
+ openssl genrsa -out private.pem 4096
121
+
122
+ # Public key
123
+ openssl rsa -in private.pem -pubout -out public.pem
124
+ ```
125
+
126
+ **Ubicación:**
127
+ ```
128
+ storage/keys/
129
+ ├── private.pem (chmod 600, no commitear)
130
+ └── public.pem (chmod 644, OK commitear)
131
+ ```
132
+
133
+ **Variables de entorno:**
134
+ ```env
135
+ JWT_PRIVATE_KEY_PATH=/path/to/private.pem
136
+ JWT_PUBLIC_KEY_PATH=/path/to/public.pem
137
+ JWT_TTL=86400 # 24 horas en segundos
138
+ JWT_TTL_REMEMBER=2592000 # 30 días en segundos
139
+ ```
140
+
141
+ ---
142
+
143
+ ### AuthService
144
+
145
+ **Responsabilidad:** Lógica de negocio de autenticación.
146
+
147
+ #### Métodos
148
+
149
+ ```php
150
+ class AuthService
151
+ {
152
+ /**
153
+ * Autentica un usuario con email y contraseña.
154
+ *
155
+ * @param string $email
156
+ * @param string $password
157
+ * @param bool $remember
158
+ * @return array ['token' => string, 'user' => User]
159
+ * @throws AuthenticationException Si credenciales inválidas
160
+ */
161
+ public function authenticate(
162
+ string $email,
163
+ string $password,
164
+ bool $remember = false
165
+ ): array;
166
+
167
+ /**
168
+ * Cierra la sesión de un usuario.
169
+ *
170
+ * @param string $token Token a invalidar
171
+ * @return void
172
+ */
173
+ public function logout(string $token): void;
174
+ }
175
+ ```
176
+
177
+ #### Flujo de Autenticación
178
+
179
+ 1. **Validar inputs:**
180
+ - Email: formato válido, max 255 chars
181
+ - Password: min 8 chars, max 255 chars
182
+
183
+ 2. **Buscar usuario:**
184
+ ```php
185
+ $user = UserRepository::findByEmail($email);
186
+ if (!$user) {
187
+ throw new AuthenticationException('Credenciales inválidas');
188
+ }
189
+ ```
190
+
191
+ 3. **Verificar contraseña:**
192
+ ```php
193
+ if (!Hash::check($password, $user->password_hash)) {
194
+ // Registrar intento fallido
195
+ LoginAttempt::create([
196
+ 'email' => $email,
197
+ 'ip_address' => request()->ip(),
198
+ 'success' => false
199
+ ]);
200
+ throw new AuthenticationException('Credenciales inválidas');
201
+ }
202
+ ```
203
+
204
+ 4. **Generar token:**
205
+ ```php
206
+ $token = JWTService::generateToken($user, $remember);
207
+ ```
208
+
209
+ 5. **Registrar intento exitoso:**
210
+ ```php
211
+ LoginAttempt::create([
212
+ 'email' => $email,
213
+ 'ip_address' => request()->ip(),
214
+ 'success' => true
215
+ ]);
216
+ ```
217
+
218
+ 6. **Retornar respuesta:**
219
+ ```php
220
+ return [
221
+ 'token' => $token,
222
+ 'token_type' => 'Bearer',
223
+ 'expires_in' => $remember ? 2592000 : 86400,
224
+ 'user' => [
225
+ 'id' => $user->id,
226
+ 'email' => $user->email
227
+ ]
228
+ ];
229
+ ```
230
+
231
+ ---
232
+
233
+ ### AuthMiddleware
234
+
235
+ **Responsabilidad:** Proteger rutas que requieren autenticación.
236
+
237
+ #### Implementación
238
+
239
+ ```php
240
+ class AuthMiddleware
241
+ {
242
+ public function handle(Request $request, Closure $next)
243
+ {
244
+ // 1. Extraer token del header
245
+ $token = $request->bearerToken();
246
+
247
+ if (!$token) {
248
+ return response()->json([
249
+ 'error' => 'No autenticado. Token requerido.'
250
+ ], 401);
251
+ }
252
+
253
+ try {
254
+ // 2. Validar token
255
+ $payload = JWTService::validateToken($token);
256
+
257
+ // 3. Obtener usuario
258
+ $user = User::find($payload['sub']);
259
+
260
+ if (!$user) {
261
+ return response()->json([
262
+ 'error' => 'Usuario no encontrado'
263
+ ], 401);
264
+ }
265
+
266
+ // 4. Adjuntar usuario al request
267
+ $request->setUserResolver(function () use ($user) {
268
+ return $user;
269
+ });
270
+
271
+ return $next($request);
272
+
273
+ } catch (TokenExpiredException $e) {
274
+ return response()->json([
275
+ 'error' => 'Token expirado. Inicie sesión nuevamente.'
276
+ ], 401);
277
+
278
+ } catch (TokenInvalidException $e) {
279
+ return response()->json([
280
+ 'error' => 'Token inválido'
281
+ ], 401);
282
+ }
283
+ }
284
+ }
285
+ ```
286
+
287
+ ---
288
+
289
+ ### RateLimitMiddleware
290
+
291
+ **Responsabilidad:** Protección contra brute force.
292
+
293
+ #### Configuración
294
+
295
+ ```php
296
+ const MAX_ATTEMPTS = 5;
297
+ const LOCKOUT_TIME = 1800; // 30 minutos en segundos
298
+ const DECAY_MINUTES = 15;
299
+ ```
300
+
301
+ #### Implementación
302
+
303
+ ```php
304
+ class RateLimitMiddleware
305
+ {
306
+ public function handle(Request $request, Closure $next)
307
+ {
308
+ $email = $request->input('email');
309
+ $ip = $request->ip();
310
+ $key = "login_attempts:{$email}:{$ip}";
311
+
312
+ // 1. Contar intentos fallidos recientes
313
+ $attempts = LoginAttempt::where('email', $email)
314
+ ->where('ip_address', $ip)
315
+ ->where('success', false)
316
+ ->where('attempted_at', '>=', now()->subMinutes(self::DECAY_MINUTES))
317
+ ->count();
318
+
319
+ // 2. Verificar si está bloqueado
320
+ if ($attempts >= self::MAX_ATTEMPTS) {
321
+ $lastAttempt = LoginAttempt::where('email', $email)
322
+ ->where('ip_address', $ip)
323
+ ->orderBy('attempted_at', 'desc')
324
+ ->first();
325
+
326
+ $lockoutUntil = $lastAttempt->attempted_at
327
+ ->addSeconds(self::LOCKOUT_TIME);
328
+
329
+ if (now()->lt($lockoutUntil)) {
330
+ $remainingMinutes = now()->diffInMinutes($lockoutUntil);
331
+
332
+ return response()->json([
333
+ 'error' => "Cuenta temporalmente bloqueada por intentos fallidos. Intente nuevamente en {$remainingMinutes} minutos."
334
+ ], 429);
335
+ }
336
+ }
337
+
338
+ return $next($request);
339
+ }
340
+ }
341
+ ```
342
+
343
+ ---
344
+
345
+ ## Modelo de Datos
346
+
347
+ ### Tabla: `login_attempts`
348
+
349
+ ```sql
350
+ CREATE TABLE login_attempts (
351
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
352
+ email VARCHAR(255) NOT NULL,
353
+ ip_address VARCHAR(45) NOT NULL,
354
+ user_agent TEXT,
355
+ success BOOLEAN DEFAULT FALSE,
356
+ attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
357
+
358
+ INDEX idx_email_ip (email, ip_address),
359
+ INDEX idx_attempted_at (attempted_at)
360
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
361
+ ```
362
+
363
+ **Campos:**
364
+ - `email`: Email del intento (puede no existir en users)
365
+ - `ip_address`: IPv4 o IPv6
366
+ - `user_agent`: User agent del navegador
367
+ - `success`: true = login exitoso, false = fallido
368
+ - `attempted_at`: Timestamp del intento
369
+
370
+ **Índices:**
371
+ - `idx_email_ip`: Para rate limiting (WHERE email AND ip_address)
372
+ - `idx_attempted_at`: Para cleanup de registros antiguos
373
+
374
+ ---
375
+
376
+ ## Endpoints API
377
+
378
+ ### POST /api/v1/auth/login
379
+
380
+ **Middleware:** RateLimitMiddleware
381
+
382
+ **Request:**
383
+ ```http
384
+ POST /api/v1/auth/login HTTP/1.1
385
+ Content-Type: application/json
386
+
387
+ {
388
+ "email": "user@example.com",
389
+ "password": "SecureP@ssw0rd",
390
+ "remember": false
391
+ }
392
+ ```
393
+
394
+ **Response (200 OK):**
395
+ ```json
396
+ {
397
+ "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
398
+ "token_type": "Bearer",
399
+ "expires_in": 86400,
400
+ "user": {
401
+ "id": 123,
402
+ "email": "user@example.com"
403
+ }
404
+ }
405
+ ```
406
+
407
+ ---
408
+
409
+ ### POST /api/v1/auth/logout
410
+
411
+ **Middleware:** AuthMiddleware
412
+
413
+ **Request:**
414
+ ```http
415
+ POST /api/v1/auth/logout HTTP/1.1
416
+ Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
417
+ ```
418
+
419
+ **Response (200 OK):**
420
+ ```json
421
+ {
422
+ "message": "Sesión cerrada exitosamente"
423
+ }
424
+ ```
425
+
426
+ ---
427
+
428
+ ### GET /api/v1/auth/me
429
+
430
+ **Middleware:** AuthMiddleware
431
+
432
+ **Request:**
433
+ ```http
434
+ GET /api/v1/auth/me HTTP/1.1
435
+ Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
436
+ ```
437
+
438
+ **Response (200 OK):**
439
+ ```json
440
+ {
441
+ "id": 123,
442
+ "email": "user@example.com",
443
+ "created_at": "2026-01-15T10:30:00Z"
444
+ }
445
+ ```
446
+
447
+ ---
448
+
449
+ ## Seguridad
450
+
451
+ ### Protecciones Implementadas
452
+
453
+ 1. **Hashing de contraseñas:**
454
+ - Bcrypt con cost factor 12
455
+ - Nunca almacenar contraseñas en texto plano
456
+ - Verificación con timing-safe comparison
457
+
458
+ 2. **Rate limiting:**
459
+ - 5 intentos/15min por IP+email
460
+ - Bloqueo temporal de 30 minutos
461
+ - No aplicar a logins exitosos
462
+
463
+ 3. **JWT seguro:**
464
+ - Firma asimétrica (RS256)
465
+ - Claves RSA 4096 bits
466
+ - Payload mínimo (sin datos sensibles)
467
+ - Expiración obligatoria
468
+
469
+ 4. **Headers de seguridad:**
470
+ ```php
471
+ 'X-Content-Type-Options' => 'nosniff',
472
+ 'X-Frame-Options' => 'DENY',
473
+ 'Content-Security-Policy' => "default-src 'self'",
474
+ 'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains'
475
+ ```
476
+
477
+ 5. **Logging y auditoría:**
478
+ - Todos los intentos registrados
479
+ - IP, user agent, timestamp
480
+ - Retención 90 días mínimo
481
+
482
+ ### Vulnerabilidades Mitigadas
483
+
484
+ | Vulnerabilidad | Mitigación |
485
+ |----------------|-----------|
486
+ | Brute force | Rate limiting + bloqueo temporal |
487
+ | Timing attacks | Hash comparison constante |
488
+ | SQL injection | ORM + prepared statements |
489
+ | XSS | Sanitización de outputs |
490
+ | CSRF | Tokens CSRF en formularios web |
491
+ | Session hijacking | Tokens HttpOnly, HTTPS obligatorio |
492
+ | Enumeración de usuarios | Mensajes genéricos |
493
+
494
+ ---
495
+
496
+ ## Performance
497
+
498
+ ### Benchmarks
499
+
500
+ | Operación | Tiempo (P95) |
501
+ |-----------|--------------|
502
+ | generateToken() | 15ms |
503
+ | validateToken() | 8ms |
504
+ | Hash::check() | 250ms |
505
+ | DB query (findByEmail) | 5ms |
506
+ | Total login | ~280ms |
507
+
508
+ ### Optimizaciones
509
+
510
+ 1. **Cachear public key:**
511
+ ```php
512
+ Cache::remember('jwt_public_key', 3600, function () {
513
+ return file_get_contents(config('jwt.public_key_path'));
514
+ });
515
+ ```
516
+
517
+ 2. **Índices de BD:**
518
+ - `users.email` (UNIQUE)
519
+ - `login_attempts.email_ip` (compuesto)
520
+ - `login_attempts.attempted_at` (para queries con ventana de tiempo)
521
+
522
+ 3. **Rate limiting en Redis (futuro):**
523
+ ```php
524
+ // Mover de DB a Redis para mejor performance
525
+ Redis::incr("rate_limit:{$email}:{$ip}");
526
+ Redis::expire("rate_limit:{$email}:{$ip}", 900);
527
+ ```
528
+
529
+ ---
530
+
531
+ ## Testing
532
+
533
+ ### Unit Tests Requeridos
534
+
535
+ ```php
536
+ // JWTServiceTest.php
537
+ test_generate_token_includes_correct_claims()
538
+ test_generate_token_with_remember_extends_expiry()
539
+ test_validate_token_with_valid_token()
540
+ test_validate_token_throws_on_expired_token()
541
+ test_validate_token_throws_on_tampered_signature()
542
+
543
+ // AuthServiceTest.php
544
+ test_authenticate_with_valid_credentials()
545
+ test_authenticate_with_invalid_password()
546
+ test_authenticate_with_nonexistent_email()
547
+ test_logout_revokes_token()
548
+
549
+ // RateLimitMiddlewareTest.php
550
+ test_allows_requests_under_limit()
551
+ test_blocks_after_max_attempts()
552
+ test_resets_after_lockout_period()
553
+ ```
554
+
555
+ ### Integration Tests Requeridos
556
+
557
+ ```php
558
+ test_login_endpoint_returns_token()
559
+ test_login_endpoint_fails_with_invalid_credentials()
560
+ test_logout_endpoint_invalidates_token()
561
+ test_me_endpoint_returns_user_data()
562
+ test_protected_route_requires_valid_token()
563
+ ```
564
+
565
+ ---
566
+
567
+ ## Configuración
568
+
569
+ ### Variables de Entorno
570
+
571
+ ```env
572
+ # JWT Configuration
573
+ JWT_PRIVATE_KEY_PATH=/var/www/storage/keys/private.pem
574
+ JWT_PUBLIC_KEY_PATH=/var/www/storage/keys/public.pem
575
+ JWT_TTL=86400
576
+ JWT_TTL_REMEMBER=2592000
577
+ JWT_ALGORITHM=RS256
578
+
579
+ # Rate Limiting
580
+ RATE_LIMIT_MAX_ATTEMPTS=5
581
+ RATE_LIMIT_DECAY_MINUTES=15
582
+ RATE_LIMIT_LOCKOUT_SECONDS=1800
583
+
584
+ # Security
585
+ BCRYPT_COST=12
586
+ HTTPS_ONLY=true
587
+ ```
588
+
589
+ ---
590
+
591
+ ## Referencias
592
+
593
+ - JWT RFC 7519: https://tools.ietf.org/html/rfc7519
594
+ - RS256 Algorithm: https://tools.ietf.org/html/rfc7518#section-3.3
595
+ - OWASP Auth Cheatsheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
596
+
597
+ ## Historial de Cambios
598
+
599
+ | Versión | Fecha | Autor | Cambios |
600
+ |---------|-------|-------|---------|
601
+ | 1.0 | 2026-02-12 | SpecLeap Contributor | Versión inicial implementada |
602
+
603
+ ---
604
+
605
+ **Estado:** implemented
606
+ **Última revisión:** 2026-02-12