pumuki-ast-hooks 6.1.11 → 6.2.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/README.md CHANGED
@@ -13,7 +13,7 @@ Portable, project‑agnostic, multi‑platform enterprise framework to govern AI
13
13
 
14
14
  ---
15
15
 
16
- ## Inicio rápido (30–60s)
16
+ ## Quick start (30–60s)
17
17
 
18
18
  ```bash
19
19
  git init
@@ -22,9 +22,9 @@ npx ast-install
22
22
  npx ast-hooks audit
23
23
  ```
24
24
 
25
- Modo de instalación por defecto: `npm-runtime`.
25
+ Default installation mode: `npm-runtime`.
26
26
 
27
- Para usar runtime embebido (modo `vendored`):
27
+ To use the embedded runtime (`vendored` mode):
28
28
 
29
29
  ```bash
30
30
  HOOK_INSTALL_MODE=vendored npx ast-install
@@ -360,55 +360,55 @@ The Git Flow cycle includes branch validation, commits, push, PR, merge (by poli
360
360
 
361
361
  ---
362
362
 
363
- ## 9. Sección de comandos (OBLIGATORIO – NO CAMBIAR)
363
+ ## 9. Commands (required)
364
364
 
365
- ### Instalación (dev)
365
+ ### Installation (dev)
366
366
 
367
367
  ```bash
368
368
  npm install --save-dev pumuki-ast-hooks
369
369
  npx ast-install
370
370
  ```
371
371
 
372
- ### Instalación (legacy)
372
+ ### Installation (legacy)
373
373
 
374
374
  ```bash
375
375
  npm install --save-dev pumuki-ast-hooks
376
376
  npx ast-install
377
377
  ```
378
378
 
379
- ### Actualización
379
+ ### Update
380
380
 
381
381
  ```bash
382
382
  npm install --save-dev pumuki-ast-hooks@latest
383
383
  npx ast-install
384
384
  ```
385
385
 
386
- ### Desinstalación
386
+ ### Uninstall
387
387
 
388
388
  ```bash
389
389
  npx ast-uninstall
390
390
  npm uninstall pumuki-ast-hooks
391
391
  ```
392
392
 
393
- ### Instalar hooks
393
+ ### Install hooks
394
394
 
395
395
  ```bash
396
396
  npm run install-hooks
397
397
  ```
398
398
 
399
- ### Menú interactivo
399
+ ### Interactive menu
400
400
 
401
401
  ```bash
402
402
  npx ast-hooks
403
403
  ```
404
404
 
405
- ### Verificación de versión
405
+ ### Version check
406
406
 
407
407
  ```bash
408
408
  npm run ast:check-version
409
409
  ```
410
410
 
411
- ### Auditoría
411
+ ### Audit
412
412
 
413
413
  ```bash
414
414
  npm run audit
@@ -435,7 +435,7 @@ npm run ast:guard:status
435
435
  npm run ast:guard:logs
436
436
  ```
437
437
 
438
- ### Refresco de evidencia
438
+ ### Evidence refresh
439
439
 
440
440
  ```bash
441
441
  npm run ast:refresh
@@ -0,0 +1,137 @@
1
+ # Auditoría de Severidades - Framework AST Intelligence
2
+
3
+ ## Criterios de Clasificación
4
+
5
+ ### CRITICAL
6
+ - **Vulnerabilidades de seguridad** que exponen datos sensibles
7
+ - **Data loss** potencial o corrupción de datos
8
+ - **Crashes** probables en producción
9
+ - **Violaciones de seguridad** críticas
10
+
11
+ ### HIGH
12
+ - **Bugs probables** que causan comportamiento incorrecto
13
+ - **Memory leaks** y problemas de gestión de memoria
14
+ - **Concurrency issues** que pueden causar race conditions
15
+ - **Violaciones SOLID** que impactan arquitectura
16
+
17
+ ### MEDIUM
18
+ - **Code smells** que afectan mantenibilidad
19
+ - **Performance** no crítico pero medible
20
+ - **Violaciones de patrones** establecidos
21
+ - **Deuda técnica** acumulable
22
+
23
+ ### LOW
24
+ - **Sugerencias de estilo** y mejores prácticas
25
+ - **Optimizaciones** opcionales
26
+ - **Mejoras** de legibilidad
27
+ - **Warnings** informativos
28
+
29
+ ---
30
+
31
+ ## Reglas por Severidad
32
+
33
+ ### CRITICAL (3 reglas)
34
+
35
+ | Regla | Justificación |
36
+ |-------|---------------|
37
+ | `ios.security.sensitive_userdefaults` | Credenciales en UserDefaults = vulnerabilidad de seguridad |
38
+ | `ios.security.hardcoded_secrets` | API keys/tokens hardcodeados = exposición de secretos |
39
+ | `backend.antipattern.god_classes` | Clases God extremas = arquitectura colapsada |
40
+
41
+ ### HIGH (12 reglas)
42
+
43
+ | Regla | Justificación |
44
+ |-------|---------------|
45
+ | `ios.force_unwrapping` | Force unwrap = crash probable en runtime |
46
+ | `ios.safety.force_unwrap_property` | Propiedades force unwrap = crash al acceder |
47
+ | `ios.memory.missing_weak_self` | Retain cycles = memory leaks |
48
+ | `ios.concurrency.task_no_error_handling` | Errores sin manejar = crashes silenciosos |
49
+ | `ios.concurrency.async_ui_update` | UI updates fuera MainActor = crashes/bugs visuales |
50
+ | `ios.quality.long_function` | Funciones >50 líneas = bugs difíciles de detectar |
51
+ | `ios.quality.high_complexity` | Complejidad >10 = bugs probables |
52
+ | `ios.swiftui.complex_body` | Body >100 líneas = performance/bugs |
53
+ | `ios.antipattern.singleton` | Singletons = testing imposible, acoplamiento |
54
+ | `ios.architecture.repository_no_protocol` | Repositorios sin protocolo = violación DIP |
55
+ | `ios.architecture.usecase_wrong_layer` | UseCase en capa incorrecta = arquitectura rota |
56
+ | `ios.solid.srp.god_class` | God class detectada = violación SRP crítica |
57
+
58
+ ### MEDIUM (15 reglas)
59
+
60
+ | Regla | Justificación |
61
+ |-------|---------------|
62
+ | `ios.quality.pyramid_of_doom` | Anidación profunda = legibilidad/mantenibilidad |
63
+ | `ios.quality.too_many_params` | >5 parámetros = difícil de usar/mantener |
64
+ | `ios.quality.nested_closures` | >3 closures = callback hell, usar async/await |
65
+ | `ios.encapsulation.public_mutable` | Propiedades públicas mutables = encapsulación rota |
66
+ | `ios.swiftui.too_many_state` | >5 @State = considerar ViewModel |
67
+ | `ios.swiftui.observed_without_state` | @ObservedObject sin ownership = bugs sutiles |
68
+ | `ios.solid.isp.fat_protocol` | Protocolo >5 requisitos = violación ISP |
69
+ | `ios.architecture.usecase_no_execute` | UseCase sin execute() = patrón inconsistente |
70
+ | `ios.architecture.usecase_void` | UseCase retorna Void = no testable |
71
+ | `ios.testing.missing_makesut` | Test sin makeSUT = código duplicado |
72
+ | `ios.naming.god_naming` | Nombres Manager/Helper = posible God class |
73
+ | `ios.i18n.hardcoded_strings` | >3 strings hardcodeados = i18n faltante |
74
+ | `ios.accessibility.missing_labels` | Imágenes sin labels = accesibilidad rota |
75
+ | `ios.solid.dip.concrete_dependency` | Dependencias concretas = violación DIP |
76
+ | `ios.solid.ocp.modification` | Modificar en lugar de extender = violación OCP |
77
+
78
+ ### LOW (8 reglas)
79
+
80
+ | Regla | Justificación |
81
+ |-------|---------------|
82
+ | `ios.imports.unused` | Imports sin usar = ruido, build time |
83
+ | `ios.performance.non_final_class` | Clases no final = optimización perdida |
84
+ | `ios.codable.missing_coding_keys` | CodingKeys opcionales = mejor control |
85
+ | `ios.performance.large_equatable` | Equatable >5 props = performance menor |
86
+ | `ios.concurrency.task_cancellation` | Task sin cancelación = recursos no liberados |
87
+ | `ios.performance.blocking_main_thread` | Operaciones síncronas = UI freeze potencial |
88
+ | `ios.testing.missing_leak_tracking` | Tests sin leak tracking = leaks no detectados |
89
+ | `ios.quality.magic_numbers` | Números mágicos = legibilidad |
90
+
91
+ ---
92
+
93
+ ## Cambios Recomendados
94
+
95
+ ### Ninguno - Clasificación Correcta
96
+
97
+ Tras la auditoría, **todas las severidades están correctamente asignadas** según los criterios establecidos:
98
+
99
+ - ✅ **CRITICAL**: Solo vulnerabilidades de seguridad y arquitectura colapsada
100
+ - ✅ **HIGH**: Bugs probables, memory leaks, crashes
101
+ - ✅ **MEDIUM**: Code smells, mantenibilidad, patrones
102
+ - ✅ **LOW**: Optimizaciones, estilo, mejoras opcionales
103
+
104
+ ---
105
+
106
+ ## Estadísticas
107
+
108
+ | Severidad | Cantidad | % |
109
+ |-----------|----------|---|
110
+ | CRITICAL | 3 | 8% |
111
+ | HIGH | 12 | 32% |
112
+ | MEDIUM | 15 | 39% |
113
+ | LOW | 8 | 21% |
114
+ | **TOTAL** | **38** | **100%** |
115
+
116
+ ---
117
+
118
+ ## Validación
119
+
120
+ ### Criterios Aplicados
121
+
122
+ 1. **CRITICAL**: ¿Expone datos sensibles o causa data loss?
123
+ 2. **HIGH**: ¿Causa crashes o memory leaks probables?
124
+ 3. **MEDIUM**: ¿Afecta mantenibilidad o patrones?
125
+ 4. **LOW**: ¿Es optimización o mejora opcional?
126
+
127
+ ### Resultado
128
+
129
+ ✅ **Todas las reglas cumplen con los criterios de su severidad asignada**
130
+
131
+ No se requieren reclasificaciones.
132
+
133
+ ---
134
+
135
+ **Auditoría completada:** 24 Enero 2026
136
+ **Framework:** pumuki-ast-hooks v6.1.13
137
+ **Total reglas auditadas:** 38
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "pumuki-ast-hooks",
3
- "version": "6.1.11",
3
+ "version": "6.2.0",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "pumuki-ast-hooks": "./bin/pumuki-mcp-server.js",
8
- "audit": "./bin/audit",
9
- "ast-hooks": "./bin/cli.js",
10
- "ast-install": "./bin/install.js",
11
- "ast-uninstall": "./bin/uninstall.js",
12
- "ast-violations": "./bin/violations-api.js",
13
- "ast-check-version": "./bin/check-version.js",
14
- "ast-gitflow": "./scripts/hooks-system/bin/gitflow-cycle.js",
15
- "ai-commit": "./bin/ai-commit.sh",
16
- "hook-run-orchestrator": "./bin/run-orchestrator.js",
17
- "hook-watch": "./bin/watch-hooks.js",
18
- "hook-status": "./bin/hook-status.js",
19
- "hook-predict": "./bin/predictive-hooks.js",
20
- "hook-playbook": "./bin/run-playbook.js",
21
- "hook-doc-drift": "./bin/check-doc-drift.js",
22
- "hook-plan-review": "./bin/plan-review.js"
7
+ "pumuki-ast-hooks": "bin/pumuki-mcp-server.js",
8
+ "audit": "bin/audit",
9
+ "ast-hooks": "bin/cli.js",
10
+ "ast-install": "bin/install.js",
11
+ "ast-uninstall": "bin/uninstall.js",
12
+ "ast-violations": "bin/violations-api.js",
13
+ "ast-check-version": "bin/check-version.js",
14
+ "ast-gitflow": "scripts/hooks-system/bin/gitflow-cycle.js",
15
+ "ai-commit": "bin/ai-commit.sh",
16
+ "hook-run-orchestrator": "bin/run-orchestrator.js",
17
+ "hook-watch": "bin/watch-hooks.js",
18
+ "hook-status": "bin/hook-status.js",
19
+ "hook-predict": "bin/predictive-hooks.js",
20
+ "hook-playbook": "bin/run-playbook.js",
21
+ "hook-doc-drift": "bin/check-doc-drift.js",
22
+ "hook-plan-review": "bin/plan-review.js"
23
23
  },
24
24
  "scripts": {
25
25
  "install-hooks": "node bin/install.js",
@@ -81,7 +81,7 @@
81
81
  "license": "MIT",
82
82
  "repository": {
83
83
  "type": "git",
84
- "url": "https://github.com/SwiftEnProfundidad/ast-intelligence-hooks.git"
84
+ "url": "git+https://github.com/SwiftEnProfundidad/ast-intelligence-hooks.git"
85
85
  },
86
86
  "bugs": {
87
87
  "url": "https://github.com/SwiftEnProfundidad/ast-intelligence-hooks/issues"
@@ -46,3 +46,5 @@
46
46
  {"timestamp":"2026-01-21T13:16:57.700Z","level":"info","component":"AutoRecovery","event":"NotificationCenterService shutdown","data":{"totalEnqueued":0,"totalSent":0,"totalDeduplicated":0,"totalCooldownSkipped":0,"totalFailed":0,"totalRetries":0,"queueSize":0,"deduplication":{"size":0},"cooldowns":{"activeCooldowns":0}},"context":{}}
47
47
  {"timestamp":"2026-01-21T14:08:08.597Z","level":"info","component":"AutoRecovery","event":"NotificationCenterService shutdown","data":{"totalEnqueued":0,"totalSent":0,"totalDeduplicated":0,"totalCooldownSkipped":0,"totalFailed":0,"totalRetries":0,"queueSize":0,"deduplication":{"size":0},"cooldowns":{"activeCooldowns":0}},"context":{}}
48
48
  {"timestamp":"2026-01-21T14:14:43.580Z","level":"info","component":"AutoRecovery","event":"NotificationCenterService shutdown","data":{"totalEnqueued":0,"totalSent":0,"totalDeduplicated":0,"totalCooldownSkipped":0,"totalFailed":0,"totalRetries":0,"queueSize":0,"deduplication":{"size":0},"cooldowns":{"activeCooldowns":0}},"context":{}}
49
+ {"timestamp":"2026-01-21T14:19:14.000Z","level":"info","component":"AutoRecovery","event":"NotificationCenterService shutdown","data":{"totalEnqueued":0,"totalSent":0,"totalDeduplicated":0,"totalCooldownSkipped":0,"totalFailed":0,"totalRetries":0,"queueSize":0,"deduplication":{"size":0},"cooldowns":{"activeCooldowns":0}},"context":{}}
50
+ {"timestamp":"2026-01-21T14:43:15.797Z","level":"info","component":"AutoRecovery","event":"NotificationCenterService shutdown","data":{"totalEnqueued":0,"totalSent":0,"totalDeduplicated":0,"totalCooldownSkipped":0,"totalFailed":0,"totalRetries":0,"queueSize":0,"deduplication":{"size":0},"cooldowns":{"activeCooldowns":0}},"context":{}}
@@ -194,3 +194,11 @@
194
194
  {"timestamp":"2026-01-21T14:14:44.277Z","level":"info","component":"InstallWizard","event":"INSTALL_WIZARD_CONFIG_EXISTS","data":{"configPath":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system/.hook-system/config.json"},"context":{}}
195
195
  {"timestamp":"2026-01-21T14:14:44.277Z","level":"error","component":"InstallWizard","event":"INSTALL_WIZARD_SYMLINK_FAILED","data":{"error":"EEXIST: file already exists, symlink '/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system/scripts/hooks-system/bin/guard-supervisor.js' -> '/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system/.git/hooks/guard-supervisor'"},"context":{}}
196
196
  {"timestamp":"2026-01-21T14:14:44.277Z","level":"info","component":"InstallWizard","event":"INSTALL_WIZARD_COMPLETED","data":{},"context":{}}
197
+ {"timestamp":"2026-01-21T14:19:14.062Z","level":"info","component":"InstallWizard","event":"INSTALL_WIZARD_START","data":{"repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"},"context":{}}
198
+ {"timestamp":"2026-01-21T14:19:14.069Z","level":"info","component":"InstallWizard","event":"INSTALL_WIZARD_CONFIG_EXISTS","data":{"configPath":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system/.hook-system/config.json"},"context":{}}
199
+ {"timestamp":"2026-01-21T14:19:14.070Z","level":"error","component":"InstallWizard","event":"INSTALL_WIZARD_SYMLINK_FAILED","data":{"error":"EEXIST: file already exists, symlink '/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system/scripts/hooks-system/bin/guard-supervisor.js' -> '/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system/.git/hooks/guard-supervisor'"},"context":{}}
200
+ {"timestamp":"2026-01-21T14:19:14.070Z","level":"info","component":"InstallWizard","event":"INSTALL_WIZARD_COMPLETED","data":{},"context":{}}
201
+ {"timestamp":"2026-01-21T14:43:15.954Z","level":"info","component":"InstallWizard","event":"INSTALL_WIZARD_START","data":{"repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"},"context":{}}
202
+ {"timestamp":"2026-01-21T14:43:15.961Z","level":"info","component":"InstallWizard","event":"INSTALL_WIZARD_CONFIG_EXISTS","data":{"configPath":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system/.hook-system/config.json"},"context":{}}
203
+ {"timestamp":"2026-01-21T14:43:15.961Z","level":"error","component":"InstallWizard","event":"INSTALL_WIZARD_SYMLINK_FAILED","data":{"error":"EEXIST: file already exists, symlink '/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system/scripts/hooks-system/bin/guard-supervisor.js' -> '/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system/.git/hooks/guard-supervisor'"},"context":{}}
204
+ {"timestamp":"2026-01-21T14:43:15.961Z","level":"info","component":"InstallWizard","event":"INSTALL_WIZARD_COMPLETED","data":{},"context":{}}
@@ -1450,3 +1450,59 @@
1450
1450
  {"timestamp":1769004905775,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1451
1451
  {"timestamp":1769004905775,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1452
1452
  {"timestamp":1769004905775,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1453
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1454
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1455
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1456
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1457
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1458
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1459
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1460
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1461
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1462
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1463
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1464
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1465
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1466
+ {"timestamp":1769005153999,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1467
+ {"timestamp":1769005154000,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1468
+ {"timestamp":1769005154000,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1469
+ {"timestamp":1769005154000,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1470
+ {"timestamp":1769005154000,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1471
+ {"timestamp":1769005154000,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1472
+ {"timestamp":1769005154000,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1473
+ {"timestamp":1769005154000,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1474
+ {"timestamp":1769005154000,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1475
+ {"timestamp":1769005154000,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1476
+ {"timestamp":1769005154000,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1477
+ {"timestamp":1769005445389,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1478
+ {"timestamp":1769005445389,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1479
+ {"timestamp":1769005445390,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1480
+ {"timestamp":1769005445390,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1481
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1482
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1483
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1484
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1485
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1486
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1487
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1488
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1489
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1490
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1491
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1492
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1493
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1494
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1495
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1496
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1497
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1498
+ {"timestamp":1769006595796,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1499
+ {"timestamp":1769006595797,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1500
+ {"timestamp":1769006595797,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1501
+ {"timestamp":1769006595797,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1502
+ {"timestamp":1769006595797,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1503
+ {"timestamp":1769006595797,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1504
+ {"timestamp":1769006595797,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1505
+ {"timestamp":1769006619003,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
1506
+ {"timestamp":1769006619004,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
1507
+ {"timestamp":1769006619004,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
1508
+ {"timestamp":1769006619004,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
@@ -0,0 +1,62 @@
1
+ const { summarizeSwiftTypes, evaluateMultipleTypeGroups, resolveSrpSeverity, isThinWrapperSummary } = require('../utils/ios-srp-helpers');
2
+
3
+ describe('ios srp heuristics', () => {
4
+ it('flags multiple unrelated types by group', () => {
5
+ const content = `
6
+ struct User { let id: String }
7
+ struct Order { let id: String }
8
+ struct Product { let id: String }
9
+ `;
10
+ const summaries = summarizeSwiftTypes(content);
11
+ const result = evaluateMultipleTypeGroups(summaries);
12
+ expect(result.shouldFlag).toBe(true);
13
+ expect(result.distinctGroups).toBeGreaterThan(1);
14
+ });
15
+
16
+ it('does not flag multiple types with shared prefix', () => {
17
+ const content = `
18
+ struct UserView { var body: some View { Text("x") } }
19
+ final class UserViewModel { func load() {} }
20
+ final class UserRouter { func route() {} }
21
+ `;
22
+ const summaries = summarizeSwiftTypes(content);
23
+ const result = evaluateMultipleTypeGroups(summaries);
24
+ expect(result.shouldFlag).toBe(false);
25
+ });
26
+
27
+ it('counts methods without nested type members', () => {
28
+ const content = `
29
+ struct Wrapper {
30
+ struct Nested { func inner() {} }
31
+ func outer() {}
32
+ }
33
+ `;
34
+ const summaries = summarizeSwiftTypes(content);
35
+ const wrapper = summaries.find((item) => item.name === 'Wrapper');
36
+ expect(wrapper.methodsCount).toBe(1);
37
+ });
38
+
39
+ it('detects thin wrappers', () => {
40
+ const content = `
41
+ struct TokenSpyStorageState {
42
+ var savedTokens: [String]
43
+ var deletedTokensCount: Int
44
+ }
45
+
46
+ final class AccessTokenSpyStorage {
47
+ var state: TokenSpyStorageState
48
+ }
49
+ `;
50
+ const summaries = summarizeSwiftTypes(content);
51
+ const wrapper = summaries.find((item) => item.name === 'AccessTokenSpyStorage');
52
+ expect(isThinWrapperSummary(wrapper)).toBe(true);
53
+ });
54
+
55
+ it('resolves severity by layer', () => {
56
+ const config = { coreSeverity: 'critical', defaultSeverity: 'high', testSeverity: 'low' };
57
+ expect(resolveSrpSeverity('Apps/Domain/User.swift', config)).toBe('critical');
58
+ expect(resolveSrpSeverity('Apps/Application/UserUseCase.swift', config)).toBe('critical');
59
+ expect(resolveSrpSeverity('Apps/Tests/UserTests.swift', config)).toBe('low');
60
+ expect(resolveSrpSeverity('Apps/Presentation/UserView.swift', config)).toBe('high');
61
+ });
62
+ });
@@ -211,6 +211,29 @@ class iOSASTIntelligentAnalyzer {
211
211
 
212
212
  collectAllNodes(this, substructure, null);
213
213
  await analyzeCollectedNodes(this, displayPath);
214
+
215
+ this.detectForceUnwrapping(displayPath);
216
+ }
217
+
218
+ detectForceUnwrapping(filePath) {
219
+ const lines = this.fileContent.split('\n');
220
+ lines.forEach((line, index) => {
221
+ const matches = [...line.matchAll(/(\w+)!/g)];
222
+ matches.forEach(match => {
223
+ const charBefore = match.index > 0 ? line[match.index - 1] : '';
224
+ const isLogicalNegation = charBefore === '!' || /[&|=<>]/.test(charBefore);
225
+
226
+ if (!isLogicalNegation) {
227
+ this.pushFinding(
228
+ 'ios.force_unwrapping',
229
+ 'high',
230
+ filePath,
231
+ index + 1,
232
+ `Force unwrapping (!) detected on '${match[1]}' - use if let or guard let instead`
233
+ );
234
+ }
235
+ });
236
+ });
214
237
  }
215
238
 
216
239
  safeStringify(obj) {
@@ -287,9 +287,16 @@ function analyzeConcurrency({ content, filePath, addFinding }) {
287
287
  'Manual main thread dispatch - use @MainActor annotation');
288
288
  }
289
289
 
290
- if (content.includes('Task {') && !content.includes('.cancel()') && !content.includes('Task.isCancelled')) {
291
- addFinding('ios.concurrency.task_cancellation', 'low', filePath, 1,
292
- 'Task without cancellation handling');
290
+ if (content.includes('Task {')) {
291
+ const hasCancelCheck = content.includes('Task.isCancelled') ||
292
+ content.includes('Task.checkCancellation') ||
293
+ content.includes('withTaskCancellationHandler') ||
294
+ content.includes('.cancel()');
295
+
296
+ if (!hasCancelCheck) {
297
+ addFinding('ios.concurrency.task_cancellation', 'low', filePath, 1,
298
+ 'Task without cancellation handling - consider guard !Task.isCancelled or try Task.checkCancellation()');
299
+ }
293
300
  }
294
301
 
295
302
  if (content.includes('var ') && content.includes('queue') && !content.includes('actor')) {
@@ -304,9 +311,16 @@ function analyzeTesting({ content, filePath, addFinding }) {
304
311
  'Test file without XCTest or Quick import');
305
312
  }
306
313
 
307
- if (filePath.includes('Test') && !content.includes('makeSUT') && content.includes('func test')) {
308
- addFinding('ios.testing.missing_makesut', 'medium', filePath, 1,
309
- 'Test without makeSUT pattern - centralize system under test creation');
314
+ if (filePath.includes('Test') && content.includes('func test')) {
315
+ const hasMakeSUT = /func\s+(private\s+)?make[Ss][Uu][Tt]\b/.test(content) ||
316
+ /func\s+(private\s+)?make_sut\b/.test(content) ||
317
+ /func\s+(private\s+)?sut\b/.test(content);
318
+ const testCount = (content.match(/func\s+test/g) || []).length;
319
+
320
+ if (!hasMakeSUT && testCount >= 3) {
321
+ addFinding('ios.testing.missing_makesut', 'medium', filePath, 1,
322
+ 'Test without makeSUT pattern - centralize system under test creation');
323
+ }
310
324
  }
311
325
 
312
326
  if (filePath.includes('Test') && !content.includes('trackForMemoryLeaks') && content.includes('class')) {
@@ -17,6 +17,7 @@ const { iOSCICDRules } = require(path.join(__dirname, 'analyzers/iOSCICDRules'))
17
17
  const { iOSForbiddenLiteralsAnalyzer } = require(path.join(__dirname, 'analyzers/iOSForbiddenLiteralsAnalyzer'));
18
18
  const { iOSASTIntelligentAnalyzer } = require(path.join(__dirname, 'analyzers/iOSASTIntelligentAnalyzer'));
19
19
  const { iOSModernPracticesRules } = require(path.join(__dirname, 'analyzers/iOSModernPracticesRules'));
20
+ const { summarizeSwiftTypes, evaluateMultipleTypeGroups, resolveSrpSeverity, isThinWrapperSummary } = require(path.join(__dirname, 'utils/ios-srp-helpers'));
20
21
 
21
22
  function detectForbiddenTestableImport({ filePath, content }) {
22
23
  if (!filePath || !content) return null;
@@ -36,8 +37,16 @@ function detectMissingMakeSUT({ filePath, content }) {
36
37
  if (!content.includes('XCTest')) return null;
37
38
  const hasTests = /func\s+test\w*\s*\(/.test(content);
38
39
  if (!hasTests) return null;
39
- const hasMakeSUT = /func\s+makeSUT\b/.test(content);
40
+
41
+ const hasMakeSUT = /func\s+(private\s+)?make[Ss][Uu][Tt]\b/.test(content) ||
42
+ /func\s+(private\s+)?make_sut\b/.test(content) ||
43
+ /func\s+(private\s+)?sut\b/.test(content);
44
+
40
45
  if (hasMakeSUT) return null;
46
+
47
+ const testCount = (content.match(/func\s+test/g) || []).length;
48
+ if (testCount < 3) return null;
49
+
41
50
  return {
42
51
  ruleId: 'ios.testing.missing_make_sut',
43
52
  severity: 'high',
@@ -128,10 +137,6 @@ async function runIOSIntelligence(project, findings, platform) {
128
137
 
129
138
  if (platformOf(filePath) !== "ios") return;
130
139
 
131
- sf.getDescendantsOfKind(SyntaxKind.NonNullExpression).forEach((expr) => {
132
- pushFinding("ios.force_unwrapping", "high", sf, expr, "Force unwrapping (!) detected - use if let or guard let instead", findings);
133
- });
134
-
135
140
  const completionHandlerFilePath = sf.getFilePath();
136
141
  const isAnalyzer = /infrastructure\/ast\/|analyzers\/|detectors\/|scanner|analyzer|detector/i.test(completionHandlerFilePath);
137
142
  const isCompletionTestFile = /\.(spec|test)\.(js|ts|swift)$/i.test(completionHandlerFilePath);
@@ -1134,29 +1139,42 @@ async function runIOSIntelligence(project, findings, platform) {
1134
1139
  // ==========================================
1135
1140
 
1136
1141
 
1137
- const typePattern = /\b(class|struct|enum|protocol)\s+\w+/g;
1138
- const classCount = (content.match(typePattern) || []).length;
1139
- if (classCount > 3 && !filePath.includes('Generated')) {
1142
+ const typeSummaries = summarizeSwiftTypes(content);
1143
+ const typeGroups = evaluateMultipleTypeGroups(typeSummaries);
1144
+ if (typeGroups.shouldFlag && !filePath.includes('Generated')) {
1145
+ const severity = resolveSrpSeverity(filePath, {
1146
+ coreSeverity: 'high',
1147
+ defaultSeverity: 'medium',
1148
+ testSeverity: 'low'
1149
+ });
1140
1150
  pushFinding(
1141
1151
  "ios.solid.srp_multiple_types",
1142
- "high",
1152
+ severity,
1143
1153
  sf,
1144
1154
  sf,
1145
- `File defines ${classCount} types - split into separate files (SRP: one responsibility per file)`,
1155
+ `File defines ${typeGroups.totalTypes} types across ${typeGroups.distinctGroups} domains - split by responsibility (SRP)`,
1146
1156
  findings
1147
1157
  );
1148
1158
  }
1149
1159
 
1150
- if (content.includes('class') || content.includes('struct')) {
1151
- const funcPattern = /func\s+\w+/g;
1152
- const funcCount = (content.match(funcPattern) || []).length;
1153
- if (funcCount > 20) {
1160
+ const godClassSeverityConfig = {
1161
+ coreSeverity: 'critical',
1162
+ defaultSeverity: 'high',
1163
+ testSeverity: 'low'
1164
+ };
1165
+ const godClassThreshold = 20;
1166
+ for (const summary of typeSummaries) {
1167
+ if (isThinWrapperSummary(summary)) {
1168
+ continue;
1169
+ }
1170
+ if (summary.methodsCount > godClassThreshold) {
1171
+ const severity = resolveSrpSeverity(filePath, godClassSeverityConfig);
1154
1172
  pushFinding(
1155
1173
  "ios.solid.srp_god_class",
1156
- "critical",
1174
+ severity,
1157
1175
  sf,
1158
1176
  sf,
1159
- `Type has ${funcCount} methods - split responsibilities (SRP: classes should have one reason to change)`,
1177
+ `Type '${summary.name}' has ${summary.methodsCount} methods - split responsibilities (SRP: one reason to change)`,
1160
1178
  findings
1161
1179
  );
1162
1180
  }
@@ -1,5 +1,6 @@
1
1
  const path = require('path');
2
2
  const DIValidationService = require('../../../../application/DIValidationService');
3
+ const { resolveSrpSeverity, isThinWrapperSummary } = require('../utils/ios-srp-helpers');
3
4
 
4
5
  const diValidationService = new DIValidationService();
5
6
 
@@ -119,17 +120,33 @@ function analyzeImportsAST(analyzer, filePath) {
119
120
  const unusedImportAllowlist = new Set(['Foundation', 'SwiftUI', 'UIKit', 'Combine']);
120
121
 
121
122
  const foundationTypeUsage = /\b(Data|Date|URL|UUID|Decimal|NSNumber|NSDecimalNumber|NSSet|NSDictionary|NSArray|IndexPath|Notification|FileManager|Bundle|Locale|TimeZone|Calendar|DateComponents|URLRequest|URLSession)\b/;
123
+ const swiftUIUsage = /\b(View|some View|Text|VStack|HStack|ZStack|Button|Image|List|NavigationView|NavigationStack|\.font|\.padding|\.frame|\.background|\.foregroundColor|@State|@Binding|@ObservedObject|@StateObject|@Environment)\b/;
124
+ const uiKitUsage = /\b(UIViewController|UIView|UITableView|UICollectionView|UIButton|UILabel|UIImageView|UINavigationController|viewDidLoad|viewWillAppear)\b/;
125
+ const combineUsage = /\b(Publisher|AnyPublisher|PassthroughSubject|CurrentValueSubject|@Published|sink|subscribe|eraseToAnyPublisher)\b/;
126
+
122
127
  for (const imp of analyzer.imports) {
123
128
  if (!unusedImportAllowlist.has(imp.name)) continue;
124
129
 
125
130
  let isUsed = analyzer.allNodes.some((n) => {
126
131
  const typename = n['key.typename'] || '';
127
132
  const name = n['key.name'] || '';
128
- return typename.includes(imp.name) || name.includes(imp.name);
133
+ const inheritedTypes = n['key.inheritedtypes'] || [];
134
+ const conformsToView = inheritedTypes.some(t => (t['key.name'] || '') === 'View');
135
+ return typename.includes(imp.name) || name.includes(imp.name) || (imp.name === 'SwiftUI' && conformsToView);
129
136
  });
137
+
130
138
  if (!isUsed && imp.name === 'Foundation') {
131
139
  isUsed = foundationTypeUsage.test(content);
132
140
  }
141
+ if (!isUsed && imp.name === 'SwiftUI') {
142
+ isUsed = swiftUIUsage.test(content);
143
+ }
144
+ if (!isUsed && imp.name === 'UIKit') {
145
+ isUsed = uiKitUsage.test(content);
146
+ }
147
+ if (!isUsed && imp.name === 'Combine') {
148
+ isUsed = combineUsage.test(content);
149
+ }
133
150
 
134
151
  if (!isUsed) {
135
152
  analyzer.pushFinding('ios.imports.unused', 'low', filePath, imp.line, `Unused import: ${imp.name}`);
@@ -168,16 +185,22 @@ async function analyzeClassAST(analyzer, node, filePath) {
168
185
 
169
186
  if (name && !/Spec$|Test$|Mock/.test(name) && !name.includes('Coordinator')) {
170
187
  const complexity = calculateComplexityAST(substructure);
171
- analyzer.godClassCandidates.push({
172
- name,
173
- filePath,
174
- line,
175
- methodsCount: methods.length,
176
- significantMethodsCount: significantMethods.length,
188
+ const isThinWrapper = isThinWrapperSummary({
189
+ methodsCount: significantMethods.length,
177
190
  propertiesCount: properties.length,
178
- bodyLength,
179
- complexity,
180
191
  });
192
+ if (!isThinWrapper) {
193
+ analyzer.godClassCandidates.push({
194
+ name,
195
+ filePath,
196
+ line,
197
+ methodsCount: methods.length,
198
+ significantMethodsCount: significantMethods.length,
199
+ propertiesCount: properties.length,
200
+ bodyLength,
201
+ complexity,
202
+ });
203
+ }
181
204
  }
182
205
 
183
206
  if (name.includes('ViewController')) {
@@ -291,8 +314,12 @@ async function analyzeClassAST(analyzer, node, filePath) {
291
314
  }
292
315
 
293
316
  if (filePath.includes('Test') && name.includes('Test')) {
294
- const hasMakeSUT = methods.some((m) => (m['key.name'] || '').includes('makeSUT'));
295
- if (!hasMakeSUT && methods.length > 2) {
317
+ const hasMakeSUT = methods.some((m) => {
318
+ const methodName = (m['key.name'] || '').toLowerCase();
319
+ return methodName.includes('makesut') || methodName.includes('make_sut') || methodName === 'sut';
320
+ });
321
+ const testMethods = methods.filter(m => (m['key.name'] || '').startsWith('test'));
322
+ if (!hasMakeSUT && testMethods.length >= 3) {
296
323
  analyzer.pushFinding('ios.testing.missing_makesut', 'medium', filePath, line, `Test class '${name}' missing makeSUT() factory`);
297
324
  }
298
325
  }
@@ -424,8 +451,11 @@ function analyzeFunctionAST(analyzer, node, filePath) {
424
451
  const ifStatements = countStatementsOfType(substructure, 'stmt.if');
425
452
  const guardStatements = countStatementsOfType(substructure, 'stmt.guard');
426
453
 
427
- if (ifStatements > 3 && guardStatements === 0) {
428
- analyzer.pushFinding('ios.quality.pyramid_of_doom', 'medium', filePath, line, `Function '${name}' has ${ifStatements} nested ifs - use guard clauses`);
454
+ const hasEarlyReturns = (analyzer.fileContent || '').includes('return') && guardStatements > 0;
455
+ const nestingLevel = calculateNestingDepth(substructure);
456
+
457
+ if (nestingLevel >= 3 && !hasEarlyReturns) {
458
+ analyzer.pushFinding('ios.quality.pyramid_of_doom', 'medium', filePath, line, `Function '${name}' has ${nestingLevel} levels of nesting - use guard clauses for early returns`);
429
459
  }
430
460
 
431
461
  const isAsync = attributes.includes('async');
@@ -458,12 +488,16 @@ function analyzePropertyAST(analyzer, node, filePath) {
458
488
  const isMutable = isComputed ? hasAccessorSet : true;
459
489
 
460
490
  const hasSetterAccessRestriction = attributes.some(a => String(a).startsWith('setter_access'));
461
- if (isPublic && isInstance && isMutable && !hasSetterAccessRestriction) {
491
+ const isProtocolRequirement = name === 'body' || attributes.some(a => String(a).includes('protocol'));
492
+
493
+ if (isPublic && isInstance && isMutable && !hasSetterAccessRestriction && !isProtocolRequirement) {
462
494
  analyzer.pushFinding('ios.encapsulation.public_mutable', 'medium', filePath, line, `Public mutable property '${name}' - consider private(set)`);
463
495
  }
464
496
  }
465
497
 
466
498
  function analyzeClosuresAST(analyzer, filePath) {
499
+ const isStruct = analyzer.structs.length > 0;
500
+
467
501
  for (const closure of analyzer.closures) {
468
502
  const closureText = analyzer.safeStringify(closure);
469
503
  const hasSelfReference = closureText.includes('"self"') || closureText.includes('key.name":"self');
@@ -471,10 +505,16 @@ function analyzeClosuresAST(analyzer, filePath) {
471
505
  const parentFunc = closure._parent;
472
506
  const isEscaping = parentFunc && (parentFunc['key.typename'] || '').includes('@escaping');
473
507
 
474
- if (hasSelfReference && isEscaping) {
475
- const hasWeakCapture = closureText.includes('weak') || closureText.includes('unowned');
508
+ if (hasSelfReference && isEscaping && !isStruct) {
509
+ const offset = closure['key.offset'] || 0;
510
+ const length = closure['key.length'] || 100;
511
+ const closureCode = (analyzer.fileContent || '').substring(offset, offset + length);
476
512
 
477
- if (!hasWeakCapture) {
513
+ const hasExplicitSelfInCode = /\bself\b/.test(closureCode);
514
+ const hasWeakCapture = /\[.*weak.*\]/.test(closureCode) || /\[.*unowned.*\]/.test(closureCode);
515
+ const hasCaptureListWithoutSelf = /\[[^\]]+\]/.test(closureCode) && !/\[.*self.*\]/.test(closureCode);
516
+
517
+ if (hasExplicitSelfInCode && !hasWeakCapture && !hasCaptureListWithoutSelf) {
478
518
  const line = closure['key.line'] || 1;
479
519
  analyzer.pushFinding('ios.memory.missing_weak_self', 'high', filePath, line, 'Escaping closure captures self without [weak self]');
480
520
  }
@@ -543,14 +583,27 @@ function analyzeAdditionalRules(analyzer, filePath) {
543
583
  analyzer.pushFinding('ios.concurrency.dispatch_queue', 'medium', filePath, line, 'DispatchQueue detected - use async/await in new code');
544
584
  }
545
585
 
546
- if (/Task\s*\{/.test(analyzer.fileContent || '') && !/Task\s*\{[^}]*do\s*\{/.test(analyzer.fileContent || '')) {
547
- const line = analyzer.findLineNumber('Task {');
548
- analyzer.pushFinding('ios.concurrency.task_no_error_handling', 'high', filePath, line, 'Task without do-catch - handle errors');
549
- }
586
+ const taskMatches = [...(analyzer.fileContent || '').matchAll(/Task\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs)];
587
+ taskMatches.forEach(match => {
588
+ const taskBody = match[1] || '';
589
+ const hasTryCall = /\btry\s+/.test(taskBody);
590
+ const hasDoBlock = /\bdo\s*\{/.test(taskBody);
550
591
 
551
- if ((analyzer.fileContent || '').includes('UserDefaults') && /password|token|secret|key/i.test(analyzer.fileContent || '')) {
552
- const line = analyzer.findLineNumber('UserDefaults');
553
- analyzer.pushFinding('ios.security.sensitive_userdefaults', 'critical', filePath, line, 'Sensitive data in UserDefaults - use Keychain');
592
+ if (hasTryCall && !hasDoBlock) {
593
+ const line = analyzer.findLineNumber(match[0].substring(0, 20));
594
+ analyzer.pushFinding('ios.concurrency.task_no_error_handling', 'high', filePath, line, 'Task with throwing calls (try) without do-catch - handle errors');
595
+ }
596
+ });
597
+
598
+ if ((analyzer.fileContent || '').includes('UserDefaults')) {
599
+ const content = analyzer.fileContent || '';
600
+ const hasSensitiveData = /UserDefaults.*\.set.*\b(password|token|secret|apiKey|authToken|accessToken|refreshToken)\b/i.test(content) ||
601
+ /\.set.*\b(password|token|secret|apiKey|authToken|accessToken|refreshToken)\b.*UserDefaults/i.test(content);
602
+
603
+ if (hasSensitiveData) {
604
+ const line = analyzer.findLineNumber('UserDefaults');
605
+ analyzer.pushFinding('ios.security.sensitive_userdefaults', 'critical', filePath, line, 'Sensitive credentials in UserDefaults - use Keychain for passwords/tokens');
606
+ }
554
607
  }
555
608
 
556
609
  const hardcodedStrings = (analyzer.fileContent || '').match(/Text\s*\(\s*"[^"]{10,}"\s*\)/g) || [];
@@ -686,15 +739,35 @@ function countStatementsOfType(substructure, stmtType) {
686
739
  let count = 0;
687
740
  const traverse = (nodes) => {
688
741
  if (!Array.isArray(nodes)) return;
689
- for (const node of nodes) {
690
- if ((node['key.kind'] || '').includes(stmtType)) count++;
691
- traverse(node['key.substructure'] || []);
742
+ for (const n of nodes) {
743
+ if ((n['key.kind'] || '').includes(stmtType)) count++;
744
+ traverse(n['key.substructure']);
692
745
  }
693
746
  };
694
747
  traverse(substructure);
695
748
  return count;
696
749
  }
697
750
 
751
+ function calculateNestingDepth(substructure) {
752
+ let maxDepth = 0;
753
+ const traverse = (nodes, currentDepth) => {
754
+ if (!Array.isArray(nodes)) return;
755
+ for (const n of nodes) {
756
+ const kind = n['key.kind'] || '';
757
+ let depth = currentDepth;
758
+
759
+ if (kind.includes('stmt.if') || kind.includes('stmt.guard') || kind.includes('stmt.for') || kind.includes('stmt.while')) {
760
+ depth++;
761
+ maxDepth = Math.max(maxDepth, depth);
762
+ }
763
+
764
+ traverse(n['key.substructure'], depth);
765
+ }
766
+ };
767
+ traverse(substructure, 0);
768
+ return maxDepth;
769
+ }
770
+
698
771
  async function checkDependencyInjectionAST(analyzer, properties, filePath, className, line) {
699
772
  await diValidationService.validateDependencyInjection(analyzer, properties, filePath, className, line);
700
773
  }
@@ -802,10 +875,18 @@ function finalizeGodClassDetection(analyzer) {
802
875
 
803
876
  const signalCount = [sizeOutlier, complexityOutlier].filter(Boolean).length;
804
877
 
805
- if (extremeOutlier || signalCount >= 2) {
878
+ const isTestDouble = /Spy$|Mock$|Stub$|Fake$|Dummy$/i.test(c.name) ||
879
+ c.filePath.includes('Test') && /Double|Spy|Mock|Stub/i.test(c.name);
880
+
881
+ if ((extremeOutlier || signalCount >= 2) && !isTestDouble) {
882
+ const severity = resolveSrpSeverity(c.filePath, {
883
+ coreSeverity: 'critical',
884
+ defaultSeverity: 'high',
885
+ testSeverity: 'low',
886
+ });
806
887
  analyzer.pushFinding(
807
888
  'ios.solid.srp.god_class',
808
- 'critical',
889
+ severity,
809
890
  c.filePath,
810
891
  c.line,
811
892
  `God class '${c.name}': ${c.methodsCount} methods (z=${methodsZ.toFixed(2)}), ${c.propertiesCount} properties (z=${propsZ.toFixed(2)}), body ${c.bodyLength} (z=${bodyZ.toFixed(2)}), complexity ${c.complexity} (z=${complexityZ.toFixed(2)}) - VIOLATES SRP`
@@ -119,14 +119,19 @@ class SourceKittenExtractor {
119
119
  const forceUnwraps = [];
120
120
  const lines = (fileContent || '').split('\n');
121
121
  lines.forEach((line, index) => {
122
- const matches = [...line.matchAll(/(\w+)\s*!/g)];
122
+ const matches = [...line.matchAll(/(\w+)!/g)];
123
123
  matches.forEach(match => {
124
- forceUnwraps.push({
125
- line: index + 1,
126
- column: match.index + 1,
127
- variable: match[1],
128
- context: line.trim(),
129
- });
124
+ const charBefore = match.index > 0 ? line[match.index - 1] : '';
125
+ const isLogicalNegation = charBefore === '!' || /[&|=<>]/.test(charBefore);
126
+
127
+ if (!isLogicalNegation) {
128
+ forceUnwraps.push({
129
+ line: index + 1,
130
+ column: match.index + 1,
131
+ variable: match[1],
132
+ context: line.trim(),
133
+ });
134
+ }
130
135
  });
131
136
  });
132
137
  return forceUnwraps;
@@ -0,0 +1,162 @@
1
+ function resolveSrpSeverity(filePath, { coreSeverity, defaultSeverity, testSeverity }) {
2
+ const normalized = String(filePath || '').replace(/\\/g, '/');
3
+ const isTest = /Tests|Test|Spec/.test(normalized);
4
+ if (isTest) return testSeverity || defaultSeverity;
5
+ const isCore = normalized.includes('/Domain/') || normalized.includes('/Application/');
6
+ if (isCore) return coreSeverity || defaultSeverity;
7
+ return defaultSeverity;
8
+ }
9
+
10
+ function findMatchingBraceIndex(content, startIndex) {
11
+ let depth = 0;
12
+ let inString = false;
13
+ let stringChar = '';
14
+ let inLineComment = false;
15
+ let inBlockComment = false;
16
+
17
+ for (let i = startIndex; i < content.length; i += 1) {
18
+ const current = content[i];
19
+ const next = content[i + 1];
20
+
21
+ if (inLineComment) {
22
+ if (current === '\n') inLineComment = false;
23
+ continue;
24
+ }
25
+
26
+ if (inBlockComment) {
27
+ if (current === '*' && next === '/') {
28
+ inBlockComment = false;
29
+ i += 1;
30
+ }
31
+ continue;
32
+ }
33
+
34
+ if (inString) {
35
+ if (current === '\\') {
36
+ i += 1;
37
+ continue;
38
+ }
39
+ if (current === stringChar) {
40
+ inString = false;
41
+ }
42
+ continue;
43
+ }
44
+
45
+ if (current === '/' && next === '/') {
46
+ inLineComment = true;
47
+ i += 1;
48
+ continue;
49
+ }
50
+
51
+ if (current === '/' && next === '*') {
52
+ inBlockComment = true;
53
+ i += 1;
54
+ continue;
55
+ }
56
+
57
+ if (current === '"' || current === "'") {
58
+ inString = true;
59
+ stringChar = current;
60
+ continue;
61
+ }
62
+
63
+ if (current === '{') depth += 1;
64
+ if (current === '}') {
65
+ depth -= 1;
66
+ if (depth === 0) return i;
67
+ }
68
+ }
69
+
70
+ return -1;
71
+ }
72
+
73
+ function extractSwiftTypeBlocks(content) {
74
+ const blocks = [];
75
+ const pattern = /(^|\n)\s*(?:public|internal|fileprivate|private|open)?\s*(?:final\s+)?(class|struct|enum|protocol)\s+([A-Za-z_][A-Za-z0-9_]*)[^\n{]*\{/g;
76
+ let match = pattern.exec(content);
77
+ while (match) {
78
+ const name = match[3];
79
+ const braceIndex = match.index + match[0].lastIndexOf('{');
80
+ const endIndex = findMatchingBraceIndex(content, braceIndex);
81
+ if (endIndex !== -1) {
82
+ blocks.push({
83
+ name,
84
+ openIndex: braceIndex,
85
+ closeIndex: endIndex,
86
+ body: content.slice(braceIndex + 1, endIndex)
87
+ });
88
+ pattern.lastIndex = endIndex + 1;
89
+ }
90
+ match = pattern.exec(content);
91
+ }
92
+ return blocks;
93
+ }
94
+
95
+ function stripNestedTypeBlocks(content) {
96
+ const nestedBlocks = extractSwiftTypeBlocks(content);
97
+ if (nestedBlocks.length === 0) return content;
98
+ const sorted = nestedBlocks.slice().sort((a, b) => b.openIndex - a.openIndex);
99
+ let output = content;
100
+ for (const block of sorted) {
101
+ const length = block.closeIndex - block.openIndex + 1;
102
+ output = `${output.slice(0, block.openIndex)}${' '.repeat(length)}${output.slice(block.closeIndex + 1)}`;
103
+ }
104
+ return output;
105
+ }
106
+
107
+ function countFunctions(content) {
108
+ const matches = content.match(/\bfunc\s+[A-Za-z_][A-Za-z0-9_]*/g) || [];
109
+ return matches.length;
110
+ }
111
+
112
+ function countProperties(content) {
113
+ const matches = content.match(/(^|\n)\s*(?:@[A-Za-z0-9_.]+\s*)*(var|let)\s+[A-Za-z_][A-Za-z0-9_]*/g) || [];
114
+ return matches.length;
115
+ }
116
+
117
+ function splitTypeName(name) {
118
+ return String(name).match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|[0-9]+/g) || [name];
119
+ }
120
+
121
+ function getTypeGroupKey(name) {
122
+ const parts = splitTypeName(name);
123
+ return parts[0] || name;
124
+ }
125
+
126
+ function summarizeSwiftTypes(content) {
127
+ const blocks = extractSwiftTypeBlocks(String(content || ''));
128
+ return blocks.map((block) => {
129
+ const sanitizedBody = stripNestedTypeBlocks(block.body);
130
+ return {
131
+ name: block.name,
132
+ methodsCount: countFunctions(sanitizedBody),
133
+ propertiesCount: countProperties(sanitizedBody),
134
+ groupKey: getTypeGroupKey(block.name)
135
+ };
136
+ });
137
+ }
138
+
139
+ function evaluateMultipleTypeGroups(summaries) {
140
+ const groups = new Set((summaries || []).map((item) => item.groupKey).filter(Boolean));
141
+ const totalTypes = (summaries || []).length;
142
+ const distinctGroups = groups.size;
143
+ const shouldFlag = totalTypes >= 3 && distinctGroups >= 2;
144
+ return {
145
+ totalTypes,
146
+ distinctGroups,
147
+ shouldFlag,
148
+ groups: Array.from(groups)
149
+ };
150
+ }
151
+
152
+ function isThinWrapperSummary(summary) {
153
+ if (!summary) return false;
154
+ return summary.methodsCount === 0 && summary.propertiesCount <= 1;
155
+ }
156
+
157
+ module.exports = {
158
+ summarizeSwiftTypes,
159
+ evaluateMultipleTypeGroups,
160
+ resolveSrpSeverity,
161
+ isThinWrapperSummary
162
+ };
@@ -27,8 +27,11 @@ class GatePolicies {
27
27
  LOW: this.checkLevel('LOW', grouped.LOW || [], effectivePolicies.LOW || effectivePolicies['LOW'])
28
28
  };
29
29
 
30
- const passed = !results.CRITICAL.failed && !results.HIGH.failed;
31
- const blockedBy = results.CRITICAL.failed ? 'CRITICAL' : (results.HIGH.failed ? 'HIGH' : null);
30
+ const passed = !results.CRITICAL.failed && !results.HIGH.failed && !results.MEDIUM.failed && !results.LOW.failed;
31
+ const blockedBy = results.CRITICAL.failed ? 'CRITICAL' :
32
+ (results.HIGH.failed ? 'HIGH' :
33
+ (results.MEDIUM.failed ? 'MEDIUM' :
34
+ (results.LOW.failed ? 'LOW' : null)));
32
35
 
33
36
  return {
34
37
  passed,
@@ -62,6 +62,15 @@ evidence_age() {
62
62
  ensure_evidence_fresh() {
63
63
  local age
64
64
  age=$(evidence_age)
65
+
66
+ # En modo check (pre-commit), SIEMPRE refrescar para analizar staged files
67
+ if [[ "${GITFLOW_STRICT_CHECK:-false}" == "true" ]]; then
68
+ printf "${CYAN}🔄 Refrescando evidencia para staged files...${NC}\n"
69
+ refresh_evidence "pre-commit"
70
+ return
71
+ fi
72
+
73
+ # En otros modos, usar umbral de tiempo
65
74
  if [[ "$age" == "-1" ]]; then
66
75
  printf "${YELLOW}⚠️ Evidencia ausente o inválida. Refrescando...${NC}\n"
67
76
  refresh_evidence "missing"
@@ -142,3 +142,9 @@
142
142
  {"timestamp":"2026-01-21T14:14:45.047Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
143
143
  {"timestamp":"2026-01-21T14:14:45.048Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
144
144
  {"timestamp":"2026-01-21T14:14:45.048Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}
145
+ {"timestamp":"2026-01-21T14:19:16.416Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
146
+ {"timestamp":"2026-01-21T14:19:16.417Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
147
+ {"timestamp":"2026-01-21T14:19:16.418Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}
148
+ {"timestamp":"2026-01-21T14:43:17.747Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"ok","percentUsed":10,"tokensUsed":100000,"maxTokens":1000000,"source":"realtime","stale":false},"context":{"message":"Result level=ok percent=10% used=100000/1000000 source=realtime"}}
149
+ {"timestamp":"2026-01-21T14:43:17.748Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"warning","percentUsed":91,"tokensUsed":910000,"maxTokens":1000000,"source":"fallback","stale":false},"context":{"message":"Result level=warning percent=91% used=910000/1000000 source=fallback"}}
150
+ {"timestamp":"2026-01-21T14:43:17.749Z","level":"info","component":"TokenMonitor","event":"TOKEN_MONITOR_RESULT","data":{"level":"critical","percentUsed":98,"tokensUsed":980000,"maxTokens":1000000,"source":"realtime","stale":true},"context":{"message":"Result level=critical percent=98% used=980000/1000000 source=realtime (stale)"}}