pumuki-ast-hooks 6.1.13 → 6.2.1

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.
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki-ast-hooks",
3
- "version": "6.1.13",
3
+ "version": "6.2.1",
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": {
@@ -136,4 +136,4 @@
136
136
  "./skills": "./skills/skill-rules.json",
137
137
  "./hooks": "./hooks/index.js"
138
138
  }
139
- }
139
+ }
@@ -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')) {
@@ -37,8 +37,16 @@ function detectMissingMakeSUT({ filePath, content }) {
37
37
  if (!content.includes('XCTest')) return null;
38
38
  const hasTests = /func\s+test\w*\s*\(/.test(content);
39
39
  if (!hasTests) return null;
40
- 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
+
41
45
  if (hasMakeSUT) return null;
46
+
47
+ const testCount = (content.match(/func\s+test/g) || []).length;
48
+ if (testCount < 3) return null;
49
+
42
50
  return {
43
51
  ruleId: 'ios.testing.missing_make_sut',
44
52
  severity: 'high',
@@ -129,10 +137,6 @@ async function runIOSIntelligence(project, findings, platform) {
129
137
 
130
138
  if (platformOf(filePath) !== "ios") return;
131
139
 
132
- sf.getDescendantsOfKind(SyntaxKind.NonNullExpression).forEach((expr) => {
133
- pushFinding("ios.force_unwrapping", "high", sf, expr, "Force unwrapping (!) detected - use if let or guard let instead", findings);
134
- });
135
-
136
140
  const completionHandlerFilePath = sf.getFilePath();
137
141
  const isAnalyzer = /infrastructure\/ast\/|analyzers\/|detectors\/|scanner|analyzer|detector/i.test(completionHandlerFilePath);
138
142
  const isCompletionTestFile = /\.(spec|test)\.(js|ts|swift)$/i.test(completionHandlerFilePath);
@@ -120,17 +120,33 @@ function analyzeImportsAST(analyzer, filePath) {
120
120
  const unusedImportAllowlist = new Set(['Foundation', 'SwiftUI', 'UIKit', 'Combine']);
121
121
 
122
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
+
123
127
  for (const imp of analyzer.imports) {
124
128
  if (!unusedImportAllowlist.has(imp.name)) continue;
125
129
 
126
130
  let isUsed = analyzer.allNodes.some((n) => {
127
131
  const typename = n['key.typename'] || '';
128
132
  const name = n['key.name'] || '';
129
- 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);
130
136
  });
137
+
131
138
  if (!isUsed && imp.name === 'Foundation') {
132
139
  isUsed = foundationTypeUsage.test(content);
133
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
+ }
134
150
 
135
151
  if (!isUsed) {
136
152
  analyzer.pushFinding('ios.imports.unused', 'low', filePath, imp.line, `Unused import: ${imp.name}`);
@@ -298,8 +314,12 @@ async function analyzeClassAST(analyzer, node, filePath) {
298
314
  }
299
315
 
300
316
  if (filePath.includes('Test') && name.includes('Test')) {
301
- const hasMakeSUT = methods.some((m) => (m['key.name'] || '').includes('makeSUT'));
302
- 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) {
303
323
  analyzer.pushFinding('ios.testing.missing_makesut', 'medium', filePath, line, `Test class '${name}' missing makeSUT() factory`);
304
324
  }
305
325
  }
@@ -431,8 +451,11 @@ function analyzeFunctionAST(analyzer, node, filePath) {
431
451
  const ifStatements = countStatementsOfType(substructure, 'stmt.if');
432
452
  const guardStatements = countStatementsOfType(substructure, 'stmt.guard');
433
453
 
434
- if (ifStatements > 3 && guardStatements === 0) {
435
- 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`);
436
459
  }
437
460
 
438
461
  const isAsync = attributes.includes('async');
@@ -465,12 +488,16 @@ function analyzePropertyAST(analyzer, node, filePath) {
465
488
  const isMutable = isComputed ? hasAccessorSet : true;
466
489
 
467
490
  const hasSetterAccessRestriction = attributes.some(a => String(a).startsWith('setter_access'));
468
- 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) {
469
494
  analyzer.pushFinding('ios.encapsulation.public_mutable', 'medium', filePath, line, `Public mutable property '${name}' - consider private(set)`);
470
495
  }
471
496
  }
472
497
 
473
498
  function analyzeClosuresAST(analyzer, filePath) {
499
+ const isStruct = analyzer.structs.length > 0;
500
+
474
501
  for (const closure of analyzer.closures) {
475
502
  const closureText = analyzer.safeStringify(closure);
476
503
  const hasSelfReference = closureText.includes('"self"') || closureText.includes('key.name":"self');
@@ -478,10 +505,16 @@ function analyzeClosuresAST(analyzer, filePath) {
478
505
  const parentFunc = closure._parent;
479
506
  const isEscaping = parentFunc && (parentFunc['key.typename'] || '').includes('@escaping');
480
507
 
481
- if (hasSelfReference && isEscaping) {
482
- 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);
483
512
 
484
- 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) {
485
518
  const line = closure['key.line'] || 1;
486
519
  analyzer.pushFinding('ios.memory.missing_weak_self', 'high', filePath, line, 'Escaping closure captures self without [weak self]');
487
520
  }
@@ -550,14 +583,27 @@ function analyzeAdditionalRules(analyzer, filePath) {
550
583
  analyzer.pushFinding('ios.concurrency.dispatch_queue', 'medium', filePath, line, 'DispatchQueue detected - use async/await in new code');
551
584
  }
552
585
 
553
- if (/Task\s*\{/.test(analyzer.fileContent || '') && !/Task\s*\{[^}]*do\s*\{/.test(analyzer.fileContent || '')) {
554
- const line = analyzer.findLineNumber('Task {');
555
- analyzer.pushFinding('ios.concurrency.task_no_error_handling', 'high', filePath, line, 'Task without do-catch - handle errors');
556
- }
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);
591
+
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
+ });
557
597
 
558
- if ((analyzer.fileContent || '').includes('UserDefaults') && /password|token|secret|key/i.test(analyzer.fileContent || '')) {
559
- const line = analyzer.findLineNumber('UserDefaults');
560
- analyzer.pushFinding('ios.security.sensitive_userdefaults', 'critical', filePath, line, 'Sensitive data in UserDefaults - use Keychain');
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
+ }
561
607
  }
562
608
 
563
609
  const hardcodedStrings = (analyzer.fileContent || '').match(/Text\s*\(\s*"[^"]{10,}"\s*\)/g) || [];
@@ -693,15 +739,35 @@ function countStatementsOfType(substructure, stmtType) {
693
739
  let count = 0;
694
740
  const traverse = (nodes) => {
695
741
  if (!Array.isArray(nodes)) return;
696
- for (const node of nodes) {
697
- if ((node['key.kind'] || '').includes(stmtType)) count++;
698
- traverse(node['key.substructure'] || []);
742
+ for (const n of nodes) {
743
+ if ((n['key.kind'] || '').includes(stmtType)) count++;
744
+ traverse(n['key.substructure']);
699
745
  }
700
746
  };
701
747
  traverse(substructure);
702
748
  return count;
703
749
  }
704
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
+
705
771
  async function checkDependencyInjectionAST(analyzer, properties, filePath, className, line) {
706
772
  await diValidationService.validateDependencyInjection(analyzer, properties, filePath, className, line);
707
773
  }
@@ -809,7 +875,10 @@ function finalizeGodClassDetection(analyzer) {
809
875
 
810
876
  const signalCount = [sizeOutlier, complexityOutlier].filter(Boolean).length;
811
877
 
812
- 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) {
813
882
  const severity = resolveSrpSeverity(c.filePath, {
814
883
  coreSeverity: 'critical',
815
884
  defaultSeverity: 'high',
@@ -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;
@@ -2504,12 +2504,18 @@ if (require.main === module) {
2504
2504
  * Called ONLY after MCP handshake is complete
2505
2505
  */
2506
2506
  function startPollingLoops() {
2507
+ const evidenceMonitor = getCompositionRoot().getEvidenceMonitor();
2508
+ evidenceMonitor.start();
2509
+
2510
+ if (process.env.DEBUG) {
2511
+ process.stderr.write('[MCP] EvidenceMonitorService started with 3-min auto-refresh\n');
2512
+ }
2513
+
2507
2514
  setInterval(async () => {
2508
2515
  try {
2509
2516
  const now = Date.now();
2510
2517
  const gitFlowService = getCompositionRoot().getGitFlowService();
2511
2518
  const gitQuery = getCompositionRoot().getGitQueryAdapter();
2512
- const evidenceMonitor = getCompositionRoot().getEvidenceMonitor();
2513
2519
  const orchestrator = getCompositionRoot().getOrchestrator();
2514
2520
 
2515
2521
  const currentBranch = gitFlowService.getCurrentBranch();
@@ -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"