pumuki-ast-hooks 5.5.47 → 5.5.49

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 (31) hide show
  1. package/docs/CODE_STANDARDS.md +5 -0
  2. package/docs/VIOLATIONS_RESOLUTION_PLAN.md +5 -140
  3. package/package.json +1 -1
  4. package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +96 -0
  5. package/scripts/hooks-system/application/services/installation/VSCodeTaskConfigurator.js +3 -1
  6. package/scripts/hooks-system/bin/gitflow-cycle.js +0 -0
  7. package/scripts/hooks-system/config/project.config.json +1 -1
  8. package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidSOLIDAnalyzer.js +11 -255
  9. package/scripts/hooks-system/infrastructure/ast/android/detectors/android-solid-detectors.js +227 -0
  10. package/scripts/hooks-system/infrastructure/ast/ast-core.js +12 -3
  11. package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +36 -13
  12. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +10 -83
  13. package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +83 -0
  14. package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +17 -2
  15. package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/FrontendArchitectureDetector.js +12 -142
  16. package/scripts/hooks-system/infrastructure/ast/frontend/detectors/frontend-architecture-strategies.js +126 -0
  17. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +30 -783
  18. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureDetector.js +21 -224
  19. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureRules.js +18 -605
  20. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSModernPracticesRules.js +4 -1
  21. package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +4 -1
  22. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-architecture-rules-strategies.js +595 -0
  23. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-architecture-strategies.js +192 -0
  24. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +789 -0
  25. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-god-class-detector.js +79 -0
  26. package/scripts/hooks-system/infrastructure/ast/ios/native-bridge.js +4 -1
  27. package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +24 -13
  28. package/skills/android-guidelines/SKILL.md +1 -0
  29. package/skills/backend-guidelines/SKILL.md +1 -0
  30. package/skills/frontend-guidelines/SKILL.md +1 -0
  31. package/skills/ios-guidelines/SKILL.md +1 -0
@@ -0,0 +1,595 @@
1
+ const { pushFinding } = require('../../ast-core');
2
+
3
+ function readFileSafe(file) {
4
+ const fs = require('fs');
5
+ try {
6
+ return fs.readFileSync(file, 'utf-8');
7
+ } catch (error) {
8
+ if (process.env.DEBUG) {
9
+ console.debug(`[iOSArchitectureRules] Failed to read file ${file}: ${error.message}`);
10
+ }
11
+ return '';
12
+ }
13
+ }
14
+
15
+ function checkFeatureFirstCleanDDDRules(findings, files) {
16
+ files.forEach(file => {
17
+ const content = readFileSafe(file);
18
+
19
+ if (file.includes('/domain/')) {
20
+ const forbiddenImports = [
21
+ 'import UIKit',
22
+ 'import SwiftUI',
23
+ 'import Alamofire',
24
+ 'import CoreData'
25
+ ];
26
+
27
+ forbiddenImports.forEach(forbiddenImport => {
28
+ if (content.includes(forbiddenImport)) {
29
+ pushFinding(findings, {
30
+ ruleId: 'ios.clean.domain_dependency_violation',
31
+ severity: 'critical',
32
+ message: `Domain layer tiene dependencia de ${forbiddenImport}. Domain debe ser independiente de frameworks.`,
33
+ filePath: file,
34
+ line: content.split('\n').findIndex(line => line.includes(forbiddenImport)) + 1,
35
+ suggestion: 'Remover dependencia. Domain solo debe contener lógica de negocio pura.'
36
+ });
37
+ }
38
+ });
39
+
40
+ if (file.includes('/domain/') && !file.includes('/entities/') &&
41
+ !file.includes('/value-objects/') && !file.includes('/interfaces/')) {
42
+ pushFinding(findings, {
43
+ ruleId: 'ios.clean.domain_structure',
44
+ severity: 'critical',
45
+ message: 'Archivo en domain/ sin estructura correcta. Usar entities/, value-objects/, interfaces/',
46
+ filePath: file,
47
+ line: 1
48
+ });
49
+ }
50
+ }
51
+
52
+ if (file.includes('Entity.swift')) {
53
+ const hasMethods = (content.match(/func\s+\w+/g) || []).length;
54
+ const hasProperties = (content.match(/(let|var)\s+\w+:/g) || []).length;
55
+
56
+ if (hasProperties > 3 && hasMethods === 0) {
57
+ pushFinding(findings, {
58
+ ruleId: 'ios.ddd.anemic_entity',
59
+ severity: 'critical',
60
+ message: 'Entity anémica (solo properties, sin comportamiento). Añadir métodos de negocio.',
61
+ filePath: file,
62
+ line: 1,
63
+ suggestion: 'Entities deben encapsular lógica de negocio, no ser solo contenedores de datos.'
64
+ });
65
+ }
66
+ }
67
+
68
+ if (file.includes('VO.swift') || file.includes('ValueObject.swift')) {
69
+ if (content.includes('var ') && !content.includes('private(set)')) {
70
+ pushFinding(findings, {
71
+ ruleId: 'ios.ddd.mutable_value_object',
72
+ severity: 'critical',
73
+ message: 'Value Object con properties mutables. VOs deben ser inmutables (usar let).',
74
+ filePath: file,
75
+ line: 1,
76
+ suggestion: 'Cambiar var por let, o usar private(set) var si necesitas computed properties'
77
+ });
78
+ }
79
+
80
+ if (!content.includes('init(') || !content.includes('throw')) {
81
+ pushFinding(findings, {
82
+ ruleId: 'ios.ddd.value_object_no_validation',
83
+ severity: 'critical',
84
+ message: 'Value Object sin validación en init(). VOs deben garantizar invariantes.',
85
+ filePath: file,
86
+ line: 1,
87
+ suggestion: `init(_ value: String) throws {
88
+ guard isValid(value) else {
89
+ throw ValidationError.invalid
90
+ }
91
+ self.value = value
92
+ }`
93
+ });
94
+ }
95
+ }
96
+
97
+ if (file.includes('UseCase.swift')) {
98
+ if (!content.includes('func execute(')) {
99
+ pushFinding(findings, {
100
+ ruleId: 'ios.ddd.usecase_missing_execute',
101
+ severity: 'critical',
102
+ message: 'Use Case sin método execute(). Convención: func execute(input: Input) async throws -> Output',
103
+ filePath: file,
104
+ line: 1
105
+ });
106
+ }
107
+
108
+ if (content.includes('UIKit') || content.includes('SwiftUI')) {
109
+ pushFinding(findings, {
110
+ ruleId: 'ios.clean.usecase_ui_dependency',
111
+ severity: 'critical',
112
+ message: 'Use Case depende de UI framework. Application layer debe ser UI-agnostic.',
113
+ filePath: file,
114
+ line: 1
115
+ });
116
+ }
117
+ }
118
+
119
+ if (content.includes('Repository') && content.includes('class ') && !content.includes('protocol ')) {
120
+ if (!file.includes('/infrastructure/')) {
121
+ pushFinding(findings, {
122
+ ruleId: 'ios.clean.repository_wrong_layer',
123
+ severity: 'critical',
124
+ message: 'Repository implementation fuera de infrastructure/. Mover a infrastructure/repositories/',
125
+ filePath: file,
126
+ line: 1
127
+ });
128
+ }
129
+ }
130
+
131
+ if (content.includes('protocol ') && content.includes('Repository')) {
132
+ if (!file.includes('/domain/')) {
133
+ pushFinding(findings, {
134
+ ruleId: 'ios.clean.repository_interface_wrong_layer',
135
+ severity: 'critical',
136
+ message: 'Repository protocol fuera de domain/. Mover a domain/interfaces/',
137
+ filePath: file,
138
+ line: 1
139
+ });
140
+ }
141
+ }
142
+
143
+ if (file.includes('DTO.swift') || file.includes('Dto.swift')) {
144
+ if (!file.includes('/application/')) {
145
+ pushFinding(findings, {
146
+ ruleId: 'ios.clean.dto_wrong_layer',
147
+ severity: 'critical',
148
+ message: 'DTO fuera de application/. Mover a application/dto/',
149
+ filePath: file,
150
+ line: 1
151
+ });
152
+ }
153
+ }
154
+
155
+ const featureMatch = file.match(/\/Features?\/(\w+)\//);
156
+ if (featureMatch) {
157
+ const currentFeature = featureMatch[1];
158
+ const importMatches = content.matchAll(/import\s+(\w+)/g);
159
+
160
+ for (const match of importMatches) {
161
+ const importedModule = match[1];
162
+
163
+ if (files.some(f => f.includes(`/Features/${importedModule}/`)) && importedModule !== currentFeature) {
164
+ pushFinding(findings, {
165
+ ruleId: 'ios.ddd.feature_coupling',
166
+ severity: 'critical',
167
+ message: `Feature '${currentFeature}' importa feature '${importedModule}'. Bounded Contexts NO deben acoplarse.`,
168
+ filePath: file,
169
+ line: content.split('\n').findIndex(line => line.includes(`import ${importedModule}`)) + 1,
170
+ suggestion: 'Comunicar vía eventos de dominio o crear Shared Kernel en Core/'
171
+ });
172
+ }
173
+ }
174
+ }
175
+
176
+ if (file.includes('/infrastructure/')) {
177
+ const methods = content.match(/func\s+\w+[\s\S]*?\{([\s\S]*?)(?=\n\s*func|\n})/g) || [];
178
+ const complexMethods = methods.filter(m => {
179
+ const lines = m.split('\n').length;
180
+ return lines > 30 || (m.includes('if ') && m.includes('guard ') && m.includes('throw '));
181
+ });
182
+
183
+ if (complexMethods.length > 0) {
184
+ pushFinding(findings, {
185
+ ruleId: 'ios.clean.infrastructure_business_logic',
186
+ severity: 'critical',
187
+ message: 'Infrastructure con lógica de negocio compleja. Mover a domain/ o application/',
188
+ filePath: file,
189
+ line: 1,
190
+ suggestion: 'Infrastructure solo debe adaptar tecnologías, no contener lógica de negocio'
191
+ });
192
+ }
193
+ }
194
+
195
+ if (file.includes('/presentation/')) {
196
+ if (content.includes('Entity') && !content.includes('DTO') && !content.includes('Dto')) {
197
+ pushFinding(findings, {
198
+ ruleId: 'ios.clean.presentation_uses_entity',
199
+ severity: 'critical',
200
+ message: 'Presentation usando Entities de domain directamente. Usar DTOs para desacoplar.',
201
+ filePath: file,
202
+ line: 1,
203
+ suggestion: 'Mapear Entities → DTOs antes de exponer a presentation layer'
204
+ });
205
+ }
206
+ }
207
+ });
208
+ }
209
+
210
+ function checkMVVMRules(findings, files) {
211
+ files.forEach(file => {
212
+ const content = readFileSafe(file);
213
+
214
+ if (file.includes('ViewModel.swift')) {
215
+ if (!content.includes('ObservableObject') && !content.includes('@Observable')) {
216
+ pushFinding(findings, {
217
+ ruleId: 'ios.mvvm.viewmodel_not_observable',
218
+ severity: 'critical',
219
+ message: 'ViewModel debe conformar ObservableObject o usar @Observable macro (iOS 17+)',
220
+ filePath: file,
221
+ line: 1
222
+ });
223
+ }
224
+
225
+ if (content.match(/import\s+UIKit/) && !content.includes('#if canImport(UIKit)')) {
226
+ pushFinding(findings, {
227
+ ruleId: 'ios.mvvm.viewmodel_uikit_dependency',
228
+ severity: 'critical',
229
+ message: 'ViewModel NO debe depender de UIKit. Usar tipos agnósticos de plataforma.',
230
+ filePath: file,
231
+ line: content.split('\n').findIndex(line => line.includes('import UIKit')) + 1
232
+ });
233
+ }
234
+
235
+ const classMatch = content.match(/class\s+\w+ViewModel/);
236
+ if (classMatch && content.includes('var ') && !content.includes('@Published')) {
237
+ pushFinding(findings, {
238
+ ruleId: 'ios.mvvm.missing_published',
239
+ severity: 'critical',
240
+ message: 'ViewModel properties que cambian deben usar @Published para notificar a la View',
241
+ filePath: file,
242
+ line: 1
243
+ });
244
+ }
245
+ }
246
+
247
+ if (file.includes('View.swift') || (content.includes('struct ') && content.includes(': View'))) {
248
+ const hasBusinessLogic =
249
+ /func\s+\w+\([^)]*\)\s*->\s*\w+\s*{[\s\S]{100,}/.test(content) ||
250
+ content.includes('URLSession') ||
251
+ content.includes('CoreData') ||
252
+ /\.save\(|\.fetch\(|\.delete\(/.test(content);
253
+
254
+ if (hasBusinessLogic) {
255
+ pushFinding(findings, {
256
+ ruleId: 'ios.mvvm.view_business_logic',
257
+ severity: 'critical',
258
+ message: 'View contiene lógica de negocio. Mover al ViewModel.',
259
+ filePath: file,
260
+ line: 1
261
+ });
262
+ }
263
+ }
264
+ });
265
+ }
266
+
267
+ function checkMVPRules(findings, files) {
268
+ files.forEach(file => {
269
+ const content = readFileSafe(file);
270
+
271
+ if (file.includes('View.swift') && !file.includes('ViewController')) {
272
+ if (!content.includes('protocol ') && content.includes('View')) {
273
+ pushFinding(findings, {
274
+ ruleId: 'ios.mvp.view_not_protocol',
275
+ severity: 'critical',
276
+ message: 'En MVP, View debe ser un protocol implementado por ViewController',
277
+ filePath: file,
278
+ line: 1
279
+ });
280
+ }
281
+ }
282
+
283
+ if (file.includes('Presenter.swift')) {
284
+ if (content.includes('var view:') && !content.includes('weak var view')) {
285
+ pushFinding(findings, {
286
+ ruleId: 'ios.mvp.presenter_strong_view',
287
+ severity: 'critical',
288
+ message: 'Presenter debe tener referencia weak a View para evitar retain cycles',
289
+ filePath: file,
290
+ line: content.split('\n').findIndex(line => line.includes('var view:')) + 1
291
+ });
292
+ }
293
+
294
+ const hasLogic = content.split('\n').filter(line =>
295
+ line.includes('func ') && !line.includes('viewDidLoad')
296
+ ).length;
297
+
298
+ if (hasLogic < 3) {
299
+ pushFinding(findings, {
300
+ ruleId: 'ios.mvp.presenter_thin',
301
+ severity: 'critical',
302
+ message: 'Presenter parece tener poca lógica. En MVP, Presenter debe contener toda la lógica de presentación.',
303
+ filePath: file,
304
+ line: 1
305
+ });
306
+ }
307
+ }
308
+
309
+ if (file.includes('ViewController.swift')) {
310
+ const hasBusinessLogic =
311
+ content.includes('URLSession') ||
312
+ content.includes('CoreData') ||
313
+ /func\s+\w+\([^)]*\)\s*->\s*\w+\s*{[\s\S]{50,}/.test(content);
314
+
315
+ if (hasBusinessLogic) {
316
+ pushFinding(findings, {
317
+ ruleId: 'ios.mvp.viewcontroller_business_logic',
318
+ severity: 'critical',
319
+ message: 'ViewController NO debe contener lógica de negocio. Delegar al Presenter.',
320
+ filePath: file,
321
+ line: 1
322
+ });
323
+ }
324
+ }
325
+ });
326
+ }
327
+
328
+ function checkVIPERRules(findings, files) {
329
+ files.forEach(file => {
330
+ const content = readFileSafe(file);
331
+
332
+ if (file.includes('View.swift') || (file.includes('ViewController.swift') && content.includes('ViewProtocol'))) {
333
+ if (!content.includes('protocol ') || !content.includes('ViewProtocol')) {
334
+ pushFinding(findings, {
335
+ ruleId: 'ios.viper.view_protocol',
336
+ severity: 'high',
337
+ message: 'En VIPER, View debe ser un protocol (ViewProtocol)',
338
+ filePath: file,
339
+ line: 1
340
+ });
341
+ }
342
+ }
343
+
344
+ if (file.includes('Interactor.swift')) {
345
+ if (content.includes('UIKit') || content.includes('import SwiftUI')) {
346
+ pushFinding(findings, {
347
+ ruleId: 'ios.viper.interactor_ui_dependency',
348
+ severity: 'high',
349
+ message: 'Interactor NO debe depender de frameworks de UI (UIKit, SwiftUI)',
350
+ filePath: file,
351
+ line: 1
352
+ });
353
+ }
354
+
355
+ if (!content.includes('var presenter:') && !content.includes('var output:')) {
356
+ pushFinding(findings, {
357
+ ruleId: 'ios.viper.interactor_no_output',
358
+ severity: 'high',
359
+ message: 'Interactor debe tener referencia a Presenter (output) para enviar resultados',
360
+ filePath: file,
361
+ line: 1
362
+ });
363
+ }
364
+ }
365
+
366
+ if (file.includes('Presenter.swift')) {
367
+ const hasViewReference = content.includes('var view') || content.includes('weak var view');
368
+ const hasInteractorReference = content.includes('var interactor');
369
+
370
+ if (!hasViewReference || !hasInteractorReference) {
371
+ pushFinding(findings, {
372
+ ruleId: 'ios.viper.presenter_missing_references',
373
+ severity: 'high',
374
+ message: 'Presenter debe tener referencias a View e Interactor',
375
+ filePath: file,
376
+ line: 1
377
+ });
378
+ }
379
+
380
+ if (content.includes('URLSession') || content.includes('CoreData')) {
381
+ pushFinding(findings, {
382
+ ruleId: 'ios.viper.presenter_business_logic',
383
+ severity: 'high',
384
+ message: 'Presenter NO debe contener lógica de negocio. Delegar al Interactor.',
385
+ filePath: file,
386
+ line: 1
387
+ });
388
+ }
389
+ }
390
+
391
+ if (file.includes('Router.swift') || file.includes('Wireframe.swift')) {
392
+ const hasBusinessLogic =
393
+ content.includes('URLSession') ||
394
+ content.includes('CoreData') ||
395
+ content.includes('UserDefaults');
396
+
397
+ if (hasBusinessLogic) {
398
+ pushFinding(findings, {
399
+ ruleId: 'ios.viper.router_business_logic',
400
+ severity: 'high',
401
+ message: 'Router NO debe contener lógica de negocio ni persistencia. Solo navegación.',
402
+ filePath: file,
403
+ line: 1
404
+ });
405
+ }
406
+ }
407
+
408
+ if (file.includes('Entity.swift')) {
409
+ const hasMethods = (content.match(/func\s+/g) || []).length;
410
+ if (hasMethods > 2) {
411
+ pushFinding(findings, {
412
+ ruleId: 'ios.viper.entity_with_logic',
413
+ severity: 'medium',
414
+ message: 'Entity debe contener solo datos. Lógica debe estar en Interactor.',
415
+ filePath: file,
416
+ line: 1
417
+ });
418
+ }
419
+ }
420
+ });
421
+ }
422
+
423
+ function checkTCARules(findings, files) {
424
+ files.forEach(file => {
425
+ const content = readFileSafe(file);
426
+
427
+ if (content.includes('struct ') && content.includes('State')) {
428
+ if (content.includes('var ') && !content.includes('mutating func')) {
429
+ pushFinding(findings, {
430
+ ruleId: 'ios.tca.mutable_state',
431
+ severity: 'medium',
432
+ message: 'State debe ser inmutable. Usar mutating func en Reducer para cambios.',
433
+ filePath: file,
434
+ line: 1
435
+ });
436
+ }
437
+ }
438
+
439
+ if (content.includes('Action')) {
440
+ if (!content.includes('enum ') && content.includes('Action')) {
441
+ pushFinding(findings, {
442
+ ruleId: 'ios.tca.action_not_enum',
443
+ severity: 'high',
444
+ message: 'Action debe ser enum para type-safety',
445
+ filePath: file,
446
+ line: 1
447
+ });
448
+ }
449
+ }
450
+
451
+ if (content.includes(': Reducer')) {
452
+ if ((content.includes('URLSession') || content.includes('async ')) &&
453
+ !content.includes('Effect')) {
454
+ pushFinding(findings, {
455
+ ruleId: 'ios.tca.missing_effect',
456
+ severity: 'high',
457
+ message: 'Side effects en TCA deben usar Effect para manejo de async',
458
+ filePath: file,
459
+ line: 1
460
+ });
461
+ }
462
+ }
463
+
464
+ if (content.includes('Store<')) {
465
+ const storeCount = (content.match(/Store</g) || []).length;
466
+ if (storeCount > 2) {
467
+ pushFinding(findings, {
468
+ ruleId: 'ios.tca.multiple_stores',
469
+ severity: 'medium',
470
+ message: 'Considerar unificar stores. TCA recomienda un Store por feature.',
471
+ filePath: file,
472
+ line: 1
473
+ });
474
+ }
475
+ }
476
+ });
477
+ }
478
+
479
+ function checkCleanSwiftRules(findings, files) {
480
+ files.forEach(file => {
481
+ const content = readFileSafe(file);
482
+
483
+ if (file.includes('Models.swift')) {
484
+ const hasRequest = content.includes('Request');
485
+ const hasResponse = content.includes('Response');
486
+ const hasViewModel = content.includes('ViewModel');
487
+
488
+ if (!hasRequest || !hasResponse || !hasViewModel) {
489
+ pushFinding(findings, {
490
+ ruleId: 'ios.clean_swift.incomplete_cycle',
491
+ severity: 'high',
492
+ message: 'Clean Swift requiere ciclo completo: Request → Response → ViewModel',
493
+ filePath: file,
494
+ line: 1
495
+ });
496
+ }
497
+ }
498
+
499
+ if (file.includes('Presenter.swift')) {
500
+ if (content.includes('interactor?.')) {
501
+ pushFinding(findings, {
502
+ ruleId: 'ios.clean_swift.bidirectional_flow',
503
+ severity: 'high',
504
+ message: 'Presenter NO debe llamar a Interactor directamente. Flujo debe ser unidireccional.',
505
+ filePath: file,
506
+ line: 1
507
+ });
508
+ }
509
+ }
510
+ });
511
+ }
512
+
513
+ function checkMVCLegacyRules(findings, files) {
514
+ files.forEach(file => {
515
+ const content = readFileSafe(file);
516
+
517
+ if (file.includes('ViewController.swift')) {
518
+ const lines = content.split('\n').length;
519
+
520
+ if (lines > 500) {
521
+ pushFinding(findings, {
522
+ ruleId: 'ios.mvc.massive_view_controller_critical',
523
+ severity: 'critical',
524
+ message: `Massive View Controller detectado (${lines} líneas). Refactorizar urgentemente a MVVM, MVP, VIPER o TCA.`,
525
+ filePath: file,
526
+ line: 1
527
+ });
528
+ } else if (lines > 300) {
529
+ pushFinding(findings, {
530
+ ruleId: 'ios.mvc.massive_view_controller',
531
+ severity: 'high',
532
+ message: `View Controller grande (${lines} líneas). Considerar refactorizar a arquitectura moderna.`,
533
+ filePath: file,
534
+ line: 1
535
+ });
536
+ }
537
+
538
+ const hasBusinessLogic =
539
+ content.includes('URLSession') ||
540
+ content.includes('CoreData') ||
541
+ content.includes('UserDefaults') ||
542
+ (content.match(/func\s+\w+\([^)]*\)\s*{[\s\S]{100,}}/g) || []).length > 5;
543
+
544
+ if (hasBusinessLogic) {
545
+ pushFinding(findings, {
546
+ ruleId: 'ios.mvc.business_logic_in_controller',
547
+ severity: 'high',
548
+ message: 'ViewController contiene lógica de negocio. Extraer a capa separada (ViewModel, Presenter, Interactor).',
549
+ filePath: file,
550
+ line: 1
551
+ });
552
+ }
553
+ }
554
+ });
555
+ }
556
+
557
+ function checkMixedArchitectureRules(findings, files) {
558
+ pushFinding(findings, {
559
+ ruleId: 'ios.architecture.mixed_patterns',
560
+ severity: 'critical',
561
+ message: 'Múltiples patrones arquitectónicos detectados en el proyecto. Esto indica inconsistencia arquitectónica grave.',
562
+ filePath: 'PROJECT_ROOT',
563
+ line: 1,
564
+ suggestion: 'Refactorizar para usar un único patrón arquitectónico consistente en todo el proyecto.'
565
+ });
566
+
567
+ const patterns = {
568
+ mvvm: files.filter(f => f.includes('ViewModel.swift')).length,
569
+ mvp: files.filter(f => f.includes('Presenter.swift')).length,
570
+ viper: files.filter(f => f.includes('Interactor.swift') || f.includes('Router.swift')).length
571
+ };
572
+
573
+ const activePatternsCount = Object.values(patterns).filter(count => count > 0).length;
574
+
575
+ if (activePatternsCount >= 2) {
576
+ pushFinding(findings, {
577
+ ruleId: 'ios.architecture.inconsistent_structure',
578
+ severity: 'high',
579
+ message: `Se detectaron ${activePatternsCount} patrones diferentes. Estandarizar en un único patrón.`,
580
+ filePath: 'PROJECT_ROOT',
581
+ line: 1
582
+ });
583
+ }
584
+ }
585
+
586
+ module.exports = {
587
+ checkFeatureFirstCleanDDDRules,
588
+ checkMVVMRules,
589
+ checkMVPRules,
590
+ checkVIPERRules,
591
+ checkTCARules,
592
+ checkCleanSwiftRules,
593
+ checkMVCLegacyRules,
594
+ checkMixedArchitectureRules,
595
+ };