pumuki-ast-hooks 5.5.49 → 5.5.50

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.
@@ -42,6 +42,7 @@ function getGitStatus(projectDir: string): GitStatus | null {
42
42
  hasUncommittedChanges: lines.length > 0
43
43
  };
44
44
  } catch (err) {
45
+ console.error(`[git-status-monitor] Failed to read git status: ${(err as Error).message}`);
45
46
  return null;
46
47
  }
47
48
  }
@@ -79,6 +80,7 @@ function detectPlatformFromFiles(projectDir: string): string[] {
79
80
  }
80
81
  }
81
82
  } catch (err) {
83
+ console.error(`[git-status-monitor] Failed to detect platforms from files: ${(err as Error).message}`);
82
84
  }
83
85
 
84
86
  return platforms.length > 0 ? platforms : ['frontend', 'backend', 'ios', 'android'];
@@ -134,6 +136,7 @@ async function main() {
134
136
  sound: 'Ping'
135
137
  });
136
138
  } catch (err) {
139
+ console.error(`[git-status-monitor] Notification failed (staged): ${(err as Error).message}`);
137
140
  }
138
141
  } else if (totalChanges > 10) {
139
142
  try {
@@ -144,11 +147,13 @@ async function main() {
144
147
  sound: 'Glass'
145
148
  });
146
149
  } catch (err) {
150
+ console.error(`[git-status-monitor] Notification failed (unstaged): ${(err as Error).message}`);
147
151
  }
148
152
  }
149
153
 
150
154
  process.exit(0);
151
155
  } catch (err) {
156
+ console.error(`[git-status-monitor] Unexpected error: ${(err as Error).message}`);
152
157
  process.exit(0);
153
158
  }
154
159
  }
@@ -24,6 +24,7 @@ export function sendMacOSNotification(options: NotificationOptions): void {
24
24
  try {
25
25
  execSync(`osascript -e '${script}'`, { stdio: 'ignore' });
26
26
  } catch (err) {
27
+ console.error(`[notify-macos] Failed to send notification: ${(err as Error).message}`);
27
28
  }
28
29
  }
29
30
 
@@ -235,6 +235,7 @@ async function main() {
235
235
  sound: 'Basso'
236
236
  });
237
237
  } catch (err) {
238
+ process.stderr.write(`Notification failed: ${err instanceof Error ? err.message : String(err)}\n`);
238
239
  }
239
240
  process.stderr.write(`${validation.error || ''}\n`);
240
241
  process.exit(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki-ast-hooks",
3
- "version": "5.5.49",
3
+ "version": "5.5.50",
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": {
@@ -102,3 +102,11 @@
102
102
  {"timestamp":1767739777882,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
103
103
  {"timestamp":1767739777882,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
104
104
  {"timestamp":1767739777882,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
105
+ {"timestamp":1767772854432,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
106
+ {"timestamp":1767772854432,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
107
+ {"timestamp":1767772854432,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
108
+ {"timestamp":1767772854432,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
109
+ {"timestamp":1767772971939,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
110
+ {"timestamp":1767772971939,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
111
+ {"timestamp":1767772971939,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
112
+ {"timestamp":1767772971939,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
@@ -3,9 +3,11 @@ const AuditLogger = require('../logging/AuditLogger');
3
3
 
4
4
  class GuardConfig {
5
5
  constructor(env = envHelper) {
6
-
7
- this.auditLogger = new AuditLogger({ repoRoot: process.cwd() });const getNumber = (name, def) =>
6
+ this.auditLogger = new AuditLogger({ repoRoot: process.cwd() });
7
+
8
+ const getNumber = (name, def) =>
8
9
  typeof env.getNumber === 'function' ? env.getNumber(name, def) : Number(env[name] || def);
10
+
9
11
  const getBool = (name, def) =>
10
12
  typeof env.getBool === 'function' ? env.getBool(name, def) : (env[name] !== 'false');
11
13
 
@@ -128,6 +128,20 @@ if [[ "$CURRENT_BRANCH" == "main" ]] || [[ "$CURRENT_BRANCH" == "master" ]] || [
128
128
  exit 1
129
129
  fi
130
130
 
131
+ # Enforce Git Flow checks (strict) before allowing commit
132
+ ENFORCER_SCRIPT="scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh"
133
+ if [[ -f "$ENFORCER_SCRIPT" ]]; then
134
+ echo ""
135
+ echo "🔍 Running Git Flow checks (strict)..."
136
+ echo ""
137
+ if ! GITFLOW_STRICT_CHECK=true bash "$ENFORCER_SCRIPT" check; then
138
+ echo ""
139
+ echo "🚨 COMMIT BLOCKED: Git Flow checks failed"
140
+ echo ""
141
+ exit 1
142
+ fi
143
+ fi
144
+
131
145
  # Check if there are staged files
132
146
  STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\\.(ts|tsx|js|jsx|swift|kt)$' || true)
133
147
  if [ -z "$STAGED_FILES" ]; then
@@ -263,10 +277,14 @@ fi
263
277
  # Run gitflow-enforcer if available (optional validation)
264
278
  ENFORCER_SCRIPT="scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh"
265
279
  if [[ -f "$ENFORCER_SCRIPT" ]]; then
266
- if ! bash "$ENFORCER_SCRIPT" check 2>/dev/null; then
280
+ echo ""
281
+ echo "🔍 Running Git Flow checks (strict)..."
282
+ echo ""
283
+ if ! GITFLOW_STRICT_CHECK=true bash "$ENFORCER_SCRIPT" check; then
267
284
  echo ""
268
- echo "⚠️ Git Flow check completed with warnings (non-blocking)"
285
+ echo "🚨 PUSH BLOCKED: Git Flow checks failed"
269
286
  echo ""
287
+ exit 1
270
288
  fi
271
289
  fi
272
290
 
@@ -45,6 +45,18 @@ function getRepoRoot() {
45
45
  */
46
46
  function shouldIgnore(file) {
47
47
  const p = file.replace(/\\/g, "/");
48
+ try {
49
+ const configPaths = loadExclusions()?.exclusions?.paths;
50
+ if (configPaths && typeof configPaths === 'object') {
51
+ for (const [key, enabled] of Object.entries(configPaths)) {
52
+ if (enabled && p.includes(key)) return true;
53
+ }
54
+ }
55
+ } catch (error) {
56
+ if (process.env.DEBUG) {
57
+ console.debug(`[ast-core] Failed to load exclusions for shouldIgnore: ${error.message || String(error)}`);
58
+ }
59
+ }
48
60
  if (p.includes("node_modules/")) return true;
49
61
  if (p.includes("/.next/")) return true;
50
62
  if (p.includes("/dist/")) return true;
@@ -125,7 +137,7 @@ let exclusionsConfig = null;
125
137
  function loadExclusions() {
126
138
  if (exclusionsConfig) return exclusionsConfig;
127
139
  try {
128
- const configPath = path.join(__dirname, '../../config/ast-exclusions.json');
140
+ const configPath = path.join(getRepoRoot(), 'config', 'ast-exclusions.json');
129
141
  if (fs.existsSync(configPath)) {
130
142
  exclusionsConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
131
143
  }
@@ -22,7 +22,7 @@ function formatLocalTimestamp(date = new Date()) {
22
22
  }
23
23
 
24
24
  const astModulesPath = __dirname;
25
- const { createProject, platformOf, mapToLevel } = require(path.join(astModulesPath, "ast-core"));
25
+ const { createProject, platformOf, mapToLevel, shouldIgnore: coreShouldIgnore } = require(path.join(astModulesPath, "ast-core"));
26
26
  const MacOSNotificationAdapter = require(path.join(__dirname, '../adapters/MacOSNotificationAdapter'));
27
27
  const { runBackendIntelligence } = require(path.join(astModulesPath, "backend/ast-backend"));
28
28
  const { runFrontendIntelligence } = require(path.join(astModulesPath, "frontend/ast-frontend"));
@@ -138,8 +138,6 @@ function runProjectHardcodedThresholdAudit(root, allFiles, findings) {
138
138
  if (p.includes('/build/')) return true;
139
139
  if (p.includes('/coverage/')) return true;
140
140
  if (p.includes('/.audit_tmp/')) return true;
141
- if (p.includes('/infrastructure/ast/')) return true;
142
- if (p.includes('/scripts/hooks-system/')) return true;
143
141
  return false;
144
142
  };
145
143
 
@@ -706,6 +704,7 @@ function listSourceFiles(root) {
706
704
  */
707
705
  function shouldIgnore(file) {
708
706
  const p = file.replace(/\\/g, "/");
707
+ if (typeof coreShouldIgnore === 'function' && coreShouldIgnore(p)) return true;
709
708
  if (p.includes("node_modules/")) return true;
710
709
  if (p.includes("/.cursor/")) return true;
711
710
  if (/\.bak/i.test(p)) return true;
@@ -126,13 +126,10 @@ function runBackendIntelligence(project, findings, platform) {
126
126
  return;
127
127
  }
128
128
  // NO excluir archivos AST - la librería debe auto-auditarse
129
- if (isTestFile(filePath)) return;
130
-
131
129
  sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
132
130
  const className = cls.getName() || '';
133
131
  const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
134
- const isTestClass = /Spec$|Test$|Mock/.test(className);
135
- if (isValueObject || isTestClass) return;
132
+ if (isValueObject) return;
136
133
 
137
134
  const methodsCount = cls.getMethods().length;
138
135
  const propertiesCount = cls.getProperties().length;
@@ -7,8 +7,7 @@ function analyzeGodClasses(sourceFile, findings, { SyntaxKind, pushFinding, godC
7
7
  sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
8
8
  const className = cls.getName() || '';
9
9
  const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
10
- const isTestClass = /Spec$|Test$|Mock/.test(className);
11
- if (isValueObject || isTestClass) return;
10
+ if (isValueObject) return;
12
11
 
13
12
  const methodsCount = cls.getMethods().length;
14
13
  const propertiesCount = cls.getProperties().length;
@@ -1,7 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs').promises;
3
3
  const { SourceKittenParser } = require('../parsers/SourceKittenParser');
4
- const { pushFinding, mapToLevel } = require(path.join(__dirname, '../../ast-core'));
4
+ const checks = require('./iOSEnterpriseChecks');
5
5
 
6
6
  class iOSEnterpriseAnalyzer {
7
7
  constructor() {
@@ -9,230 +9,6 @@ class iOSEnterpriseAnalyzer {
9
9
  this.findings = [];
10
10
  }
11
11
 
12
- async analyzeFile(filePath, findings) {
13
- this.findings = findings;
14
-
15
- try {
16
- const ast = await this.parser.parseFile(filePath);
17
-
18
- if (!ast.parsed) {
19
- console.warn(`[iOS Enterprise] Could not parse ${filePath}: ${ast.error}`);
20
- return;
21
- }
22
-
23
- const content = await fs.readFile(filePath, 'utf-8');
24
-
25
- const classes = this.parser.extractClasses(ast);
26
- const functions = this.parser.extractFunctions(ast);
27
- const properties = this.parser.extractProperties(ast);
28
- const protocols = this.parser.extractProtocols(ast);
29
-
30
- await this.analyzeSwiftModerno(ast, content, filePath);
31
- await this.analyzeSwiftUI(ast, classes, filePath);
32
- await this.analyzeUIKit(ast, classes, filePath);
33
- await this.analyzeProtocolOriented(protocols, filePath);
34
- await this.analyzeValueTypes(classes, filePath);
35
- await this.analyzeMemoryManagement(content, filePath);
36
- await this.analyzeOptionals(content, filePath);
37
- await this.analyzeDependencyInjection(classes, filePath);
38
- await this.analyzeNetworking(content, filePath);
39
- await this.analyzePersistence(content, filePath);
40
- await this.analyzeCombine(content, filePath);
41
- await this.analyzeConcurrency(content, filePath);
42
- await this.analyzeTesting(content, filePath);
43
- await this.analyzeUITesting(content, filePath);
44
- await this.analyzeSecurity(content, filePath);
45
- await this.analyzeAccessibility(content, filePath);
46
- await this.analyzeLocalization(content, filePath);
47
- await this.analyzeArchitecturePatterns(classes, functions, filePath);
48
- await this.analyzePerformance(functions, content, filePath);
49
- await this.analyzeCodeOrganization(filePath, content);
50
-
51
- } catch (error) {
52
- console.error(`[iOS Enterprise] Error analyzing ${filePath}:`, error.message);
53
- }
54
- }
55
-
56
- async analyzeSwiftModerno(ast, content, filePath) {
57
- if (content.includes('completion:') && !content.includes('async ')) {
58
- this.addFinding('ios.async_await_missing', 'medium', filePath, 1,
59
- 'Using completion handlers instead of async/await (Swift 5.9+ required)');
60
- }
61
-
62
- const taskCount = (content.match(/\bTask\s*\{/g) || []).length;
63
- if (taskCount > 3 && !content.includes('TaskGroup')) {
64
- this.addFinding('ios.structured_concurrency_missing', 'medium', filePath, 1,
65
- `Multiple Task blocks (${taskCount}) without TaskGroup - use structured concurrency`);
66
- }
67
-
68
- if (content.includes('actor ') && !content.includes(': Sendable')) {
69
- this.addFinding('ios.sendable_missing', 'low', filePath, 1,
70
- 'Actor should conform to Sendable protocol for thread-safe types');
71
- }
72
-
73
- if (content.includes('func ') && content.includes('-> View') && !content.includes('some View')) {
74
- this.addFinding('ios.opaque_types_missing', 'low', filePath, 1,
75
- 'Use "some View" instead of explicit View protocol return');
76
- }
77
-
78
- if (content.includes('UIViewController') && !content.includes('@State') && !content.includes('@Binding')) {
79
- this.addFinding('ios.property_wrappers_missing', 'info', filePath, 1,
80
- 'Consider using SwiftUI property wrappers (@State, @Binding) for state management');
81
- }
82
-
83
- const functions = this.parser.extractFunctions(ast);
84
- functions.forEach(fn => {
85
- if (fn.name.includes('Array') || fn.name.includes('Collection')) {
86
- if (!content.includes('<T>') && !content.includes('<Element>')) {
87
- this.addFinding('ios.generics_missing', 'low', filePath, fn.line,
88
- `Function ${fn.name} should use generics for type safety`);
89
- }
90
- }
91
- });
92
- }
93
-
94
- async analyzeSwiftUI(ast, classes, filePath) {
95
- const content = await fs.readFile(filePath, 'utf-8');
96
- const usesSwiftUI = this.parser.usesSwiftUI(ast);
97
- const usesUIKit = this.parser.usesUIKit(ast);
98
-
99
- if (usesUIKit && !usesSwiftUI) {
100
- this.addFinding('ios.swiftui_first', 'medium', filePath, 1,
101
- 'Consider migrating to SwiftUI for new views (UIKit only when strictly necessary)');
102
- }
103
-
104
- if (usesSwiftUI) {
105
- if (!content.includes('@State')) {
106
- this.addFinding('ios.state_local_missing', 'info', filePath, 1,
107
- 'SwiftUI view without @State - consider if local state is needed');
108
- }
109
-
110
- if (content.includes('ObservableObject') && !content.includes('@StateObject')) {
111
- this.addFinding('ios.stateobject_missing', 'high', filePath, 1,
112
- 'ObservableObject should be owned with @StateObject, not @ObservedObject');
113
- }
114
-
115
- if (content.includes('class') && content.includes('ObservableObject') && !content.includes('@EnvironmentObject')) {
116
- this.addFinding('ios.environmentobject_missing', 'info', filePath, 1,
117
- 'Consider using @EnvironmentObject for dependency injection in SwiftUI');
118
- }
119
-
120
- if (content.includes('.frame(') && content.includes('CGRect(')) {
121
- this.addFinding('ios.declarativo_missing', 'medium', filePath, 1,
122
- 'Using imperative CGRect in SwiftUI - use declarative .frame() modifiers');
123
- }
124
-
125
- const geometryReaderCount = (content.match(/GeometryReader/g) || []).length;
126
- if (geometryReaderCount > 2) {
127
- this.addFinding('ios.geometryreader_moderation', 'medium', filePath, 1,
128
- `Excessive GeometryReader usage (${geometryReaderCount}x) - use only when necessary`);
129
- }
130
- }
131
- }
132
-
133
- async analyzeUIKit(ast, classes, filePath) {
134
- const content = await fs.readFile(filePath, 'utf-8');
135
-
136
- classes.forEach(cls => {
137
- if (cls.name.includes('ViewController')) {
138
- const linesCount = cls.substructure.length * 10;
139
- if (linesCount > 300) {
140
- this.addFinding('ios.massive_viewcontrollers', 'high', filePath, cls.line,
141
- `Massive ViewController ${cls.name} (~${linesCount} lines) - break down into smaller components`);
142
- }
143
-
144
- if (!content.includes('ViewModel')) {
145
- this.addFinding('ios.uikit.viewmodel_delegation', 'medium', filePath, cls.line,
146
- `ViewController ${cls.name} should delegate logic to ViewModel (MVVM pattern)`);
147
- }
148
- }
149
- });
150
-
151
- if (filePath.endsWith('.swift') && !filePath.includes('analyzer') && !filePath.includes('detector')) {
152
- if (content.includes('storyboard') || content.includes('.xib') || content.includes('.nib')) {
153
- this.addFinding('ios.storyboards', 'high', filePath, 1,
154
- 'Storyboard/XIB detected - use programmatic UI for better version control');
155
- }
156
- }
157
- }
158
-
159
- async analyzeProtocolOriented(protocols, filePath) {
160
- const content = await fs.readFile(filePath, 'utf-8');
161
-
162
- if (protocols.length > 0 && !content.includes('extension ')) {
163
- this.addFinding('ios.pop.missing_extensions', 'low', filePath, 1,
164
- 'Protocols detected but no extensions - consider protocol extensions for default implementations');
165
- }
166
-
167
- if (content.includes('class ') && content.includes(': ')) {
168
- const inheritanceCount = (content.match(/class\s+\w+\s*:\s*\w+/g) || []).length;
169
- if (inheritanceCount > 2) {
170
- this.addFinding('ios.pop.missing_composition_over_inheritance', 'medium', filePath, 1,
171
- `Excessive class inheritance (${inheritanceCount}x) - prefer protocol composition`);
172
- }
173
- }
174
- }
175
-
176
- async analyzeValueTypes(classes, filePath) {
177
- const content = await fs.readFile(filePath, 'utf-8');
178
-
179
- classes.forEach(cls => {
180
- if (!cls.inheritedTypes.length && !content.includes('ObservableObject')) {
181
- this.addFinding('ios.values.classes_instead_structs', 'medium', filePath, cls.line,
182
- `Class ${cls.name} without inheritance - consider struct for value semantics`);
183
- }
184
- });
185
-
186
- const varCount = (content.match(/\bvar\s+/g) || []).length;
187
- const letCount = (content.match(/\blet\s+/g) || []).length;
188
- if (varCount > letCount) {
189
- this.addFinding('ios.values.mutability', 'low', filePath, 1,
190
- `More var (${varCount}) than let (${letCount}) - prefer immutability`);
191
- }
192
- }
193
-
194
- async analyzeMemoryManagement(content, filePath) {
195
- const closureMatches = content.match(/\{\s*\[/g);
196
- const weakSelfMatches = content.match(/\[weak self\]/g);
197
- if (closureMatches && closureMatches.length > (weakSelfMatches?.length || 0)) {
198
- this.addFinding('ios.memory.missing_weak_self', 'high', filePath, 1,
199
- 'Closures without [weak self] - potential retain cycles');
200
- }
201
-
202
- if (content.includes('self.') && content.includes('{') && !content.includes('[weak self]')) {
203
- this.addFinding('ios.memory.retain_cycles', 'high', filePath, 1,
204
- 'Potential retain cycle - closure captures self without [weak self]');
205
- }
206
-
207
- if (content.includes('class ') && !content.includes('deinit')) {
208
- this.addFinding('ios.memory.missing_deinit', 'low', filePath, 1,
209
- 'Class without deinit - consider adding for cleanup verification');
210
- }
211
- }
212
-
213
- async analyzeOptionals(content, filePath) {
214
- const forceUnwraps = content.match(/(\w+)\s*!/g);
215
- if (forceUnwraps && forceUnwraps.length > 0) {
216
- const nonIBOutlets = forceUnwraps.filter(match => !content.includes(`@IBOutlet`));
217
- if (nonIBOutlets.length > 0) {
218
- this.addFinding('ios.force_unwrapping', 'high', filePath, 1,
219
- `Force unwrapping (!) detected ${nonIBOutlets.length}x - use if let or guard let`);
220
- }
221
- }
222
-
223
- const ifLetCount = (content.match(/if\s+let\s+/g) || []).length;
224
- const guardLetCount = (content.match(/guard\s+let\s+/g) || []).length;
225
- if (ifLetCount === 0 && guardLetCount === 0 && content.includes('?')) {
226
- this.addFinding('ios.optionals.optional_binding', 'medium', filePath, 1,
227
- 'Optionals present but no optional binding - use if let or guard let');
228
- }
229
-
230
- if (content.includes('?') && !content.includes('??')) {
231
- this.addFinding('ios.optionals.missing_nil_coalescing', 'info', filePath, 1,
232
- 'Consider using nil coalescing operator (??) for default values');
233
- }
234
- }
235
-
236
12
  addFinding(ruleId, severity, filePath, line, message) {
237
13
  this.findings.push({
238
14
  ruleId,
@@ -244,180 +20,41 @@ class iOSEnterpriseAnalyzer {
244
20
  });
245
21
  }
246
22
 
247
- async analyzeDependencyInjection(classes, filePath) {
248
- const content = await fs.readFile(filePath, 'utf-8');
249
-
250
- if (content.includes('.shared') || content.includes('static let shared')) {
251
- this.addFinding('ios.di.singleton_usage', 'high', filePath, 1,
252
- 'Singleton detected - use dependency injection instead');
253
- }
254
-
255
- classes.forEach(cls => {
256
- if (cls.name.includes('ViewModel') || cls.name.includes('Service')) {
257
- const hasInit = content.includes(`init(`);
258
- if (!hasInit) {
259
- this.addFinding('ios.di.missing_protocol_injection', 'medium', filePath, cls.line,
260
- `${cls.name} should inject dependencies via initializer`);
261
- }
262
- }
263
- });
264
-
265
- if (content.includes('init(') && content.match(/init\([^)]{50,}\)/)) {
266
- this.addFinding('ios.di.missing_factory', 'low', filePath, 1,
267
- 'Complex initialization - consider factory pattern');
268
- }
269
- }
270
-
271
- async analyzeNetworking(content, filePath) {
272
- if (String(filePath || '').endsWith('/Package.swift') || String(filePath || '').endsWith('Package.swift')) {
273
- return;
274
- }
275
- if (!content.includes('URLSession') && !content.includes('Alamofire')) {
276
- if (content.includes('http://') || content.includes('https://')) {
277
- this.addFinding('ios.networking.missing_urlsession', 'high', filePath, 1,
278
- 'Network URLs detected but no URLSession/Alamofire usage');
23
+ async analyzeFile(filePath, findings) {
24
+ this.findings = findings;
25
+ try {
26
+ const ast = await this.parser.parseFile(filePath);
27
+ if (!ast.parsed) {
28
+ console.warn(`[iOS Enterprise] Could not parse ${filePath}: ${ast.error}`);
29
+ return;
279
30
  }
280
- }
281
-
282
- if (content.includes('URLSession') && content.includes('completionHandler:') && !content.includes('async')) {
283
- this.addFinding('ios.networking.completion_handlers_instead_async', 'medium', filePath, 1,
284
- 'Using completion handlers with URLSession - migrate to async/await');
285
- }
286
-
287
- if (content.includes('JSONSerialization') && !content.includes('Codable')) {
288
- this.addFinding('ios.networking.missing_codable', 'medium', filePath, 1,
289
- 'Manual JSON parsing - use Codable for type safety');
290
- }
291
-
292
- if (content.includes('URLSession') && !content.includes('NetworkError')) {
293
- this.addFinding('ios.networking.missing_error_handling', 'high', filePath, 1,
294
- 'Network code without custom NetworkError enum');
295
- }
296
-
297
- // Check for SSL pinning implementation
298
- const hasSSLPinningImplementation =
299
- content.includes('serverTrustPolicy') ||
300
- content.includes('pinning') ||
301
- (content.includes('URLSessionDelegate') && content.includes('URLAuthenticationChallenge'));
302
-
303
- if (content.includes('URLSession') && !hasSSLPinningImplementation) {
304
- this.addFinding('ios.networking.missing_ssl_pinning', 'medium', filePath, 1,
305
- 'Consider SSL pinning for high-security apps');
306
- }
307
-
308
- if (content.includes('URLSession') && !content.includes('retry')) {
309
- this.addFinding('ios.networking.missing_retry', 'low', filePath, 1,
310
- 'Network requests without retry logic');
311
- }
312
- }
313
-
314
- async analyzePersistence(content, filePath) {
315
- if (content.includes('UserDefaults') && (content.includes('password') || content.includes('token') || content.includes('auth'))) {
316
- this.addFinding('ios.persistence.userdefaults_sensitive', 'critical', filePath, 1,
317
- 'Sensitive data in UserDefaults - use Keychain instead');
318
- }
319
-
320
- if ((content.includes('password') || content.includes('token')) && !content.includes('Keychain') && !content.includes('Security')) {
321
- this.addFinding('ios.persistence.missing_keychain', 'critical', filePath, 1,
322
- 'Sensitive data detected but no Keychain usage');
323
- }
324
31
 
325
- if (content.includes('NSManagedObjectContext') && content.includes('.main')) {
326
- this.addFinding('ios.persistence.core_data_on_main', 'high', filePath, 1,
327
- 'Core Data operations on main thread - use background context');
328
- }
329
-
330
- if (content.includes('NSPersistentContainer') && !content.includes('NSMigrationManager')) {
331
- this.addFinding('ios.persistence.missing_migration', 'medium', filePath, 1,
332
- 'Core Data without migration strategy');
333
- }
334
- }
335
-
336
- async analyzeCombine(content, filePath) {
337
- if (content.includes('.sink(') && !content.includes('AnyCancellable')) {
338
- this.addFinding('ios.combine.missing_cancellables', 'high', filePath, 1,
339
- 'Combine sink without storing AnyCancellable - memory leak');
340
- }
341
-
342
- if (content.includes('@Published') && !content.includes('import Combine')) {
343
- this.addFinding('ios.combine.published_without_combine', 'high', filePath, 1,
344
- '@Published used but Combine not imported');
345
- }
346
-
347
- if (content.includes('.sink(') && !content.includes('receiveCompletion')) {
348
- this.addFinding('ios.combine.error_handling', 'medium', filePath, 1,
349
- 'Combine subscriber without error handling (receiveCompletion)');
350
- }
351
-
352
- if (content.includes('Future<') && !content.includes('async')) {
353
- this.addFinding('ios.combine.prefer_async_await', 'low', filePath, 1,
354
- 'Combine Future for single value - consider async/await instead');
355
- }
356
- }
357
-
358
- async analyzeConcurrency(content, filePath) {
359
- if (content.includes('DispatchQueue') && !content.includes('async func')) {
360
- this.addFinding('ios.concurrency.dispatchqueue_old', 'medium', filePath, 1,
361
- 'Using DispatchQueue - prefer async/await for new code');
362
- }
363
-
364
- if (content.includes('DispatchQueue.main') && content.includes('UI')) {
365
- this.addFinding('ios.concurrency.missing_mainactor', 'medium', filePath, 1,
366
- 'Manual main thread dispatch - use @MainActor annotation');
367
- }
368
-
369
- if (content.includes('Task {') && !content.includes('.cancel()') && !content.includes('Task.isCancelled')) {
370
- this.addFinding('ios.concurrency.task_cancellation', 'low', filePath, 1,
371
- 'Task without cancellation handling');
372
- }
373
-
374
- if (content.includes('var ') && content.includes('queue') && !content.includes('actor')) {
375
- this.addFinding('ios.concurrency.actor_missing', 'medium', filePath, 1,
376
- 'Manual synchronization with queue - consider actor for thread safety');
377
- }
378
- }
379
-
380
- async analyzeTesting(content, filePath) {
381
- if (filePath.includes('Test') && !content.includes('XCTest') && !content.includes('Quick')) {
382
- this.addFinding('ios.testing.missing_xctest', 'high', filePath, 1,
383
- 'Test file without XCTest or Quick import');
384
- }
385
-
386
- if (filePath.includes('Test') && !content.includes('makeSUT') && content.includes('func test')) {
387
- this.addFinding('ios.testing.missing_makesut', 'medium', filePath, 1,
388
- 'Test without makeSUT pattern - centralize system under test creation');
389
- }
390
-
391
- if (filePath.includes('Test') && !content.includes('trackForMemoryLeaks') && content.includes('class')) {
392
- this.addFinding('ios.testing.missing_memory_leak_tracking', 'medium', filePath, 1,
393
- 'Test without trackForMemoryLeaks helper');
394
- }
395
-
396
- if (filePath.includes('Test') && content.includes('init(') && !content.includes('Protocol')) {
397
- this.addFinding('ios.testing.concrete_dependencies', 'medium', filePath, 1,
398
- 'Test using concrete dependencies - inject protocols for testability');
399
- }
400
- }
401
-
402
- async analyzeUITesting(content, filePath) {
403
- if (filePath.includes('UITest') && !content.includes('XCUIApplication')) {
404
- this.addFinding('ios.uitesting.missing_xcuitest', 'high', filePath, 1,
405
- 'UI test file without XCUIApplication');
406
- }
407
-
408
- if (filePath.includes('UITest') && !content.includes('accessibilityIdentifier')) {
409
- this.addFinding('ios.uitesting.missing_accessibility', 'medium', filePath, 1,
410
- 'UI test without accessibility identifiers for element location');
411
- }
412
-
413
- if (filePath.includes('UITest') && content.includes('XCUIElement') && !content.includes('Page')) {
414
- this.addFinding('ios.uitesting.missing_page_object', 'low', filePath, 1,
415
- 'UI test without Page Object pattern for encapsulation');
416
- }
417
-
418
- if (filePath.includes('UITest') && content.includes('.tap()') && !content.includes('waitForExistence')) {
419
- this.addFinding('ios.uitesting.missing_wait', 'high', filePath, 1,
420
- 'UI test tapping without waitForExistence - flaky test');
32
+ const content = await fs.readFile(filePath, 'utf-8');
33
+ const classes = this.parser.extractClasses(ast) || [];
34
+ const functions = this.parser.extractFunctions(ast) || [];
35
+ const protocols = this.parser.extractProtocols(ast) || [];
36
+ const usesSwiftUI = typeof this.parser.usesSwiftUI === 'function' ? this.parser.usesSwiftUI(ast) : false;
37
+ const usesUIKit = typeof this.parser.usesUIKit === 'function' ? this.parser.usesUIKit(ast) : false;
38
+
39
+ const add = (ruleId, severity, line, message) =>
40
+ this.addFinding(ruleId, severity, filePath, line, message);
41
+
42
+ checks.analyzeSwiftModerno({ content, functions, filePath, addFinding: add });
43
+ checks.analyzeSwiftUI({ usesSwiftUI, usesUIKit, content, classes, filePath, addFinding: add });
44
+ checks.analyzeUIKit({ classes, content, filePath, addFinding: add });
45
+ checks.analyzeProtocolOriented({ protocols, content, filePath, addFinding: add });
46
+ checks.analyzeValueTypes({ classes, content, filePath, addFinding: add });
47
+ checks.analyzeMemoryManagement({ content, filePath, addFinding: add });
48
+ checks.analyzeOptionals({ content, filePath, addFinding: add });
49
+ checks.analyzeDependencyInjection({ classes, content, filePath, addFinding: add });
50
+ checks.analyzeNetworking({ content, filePath, addFinding: add });
51
+ checks.analyzePersistence({ content, filePath, addFinding: add });
52
+ checks.analyzeCombine({ content, filePath, addFinding: add });
53
+ checks.analyzeConcurrency({ content, filePath, addFinding: add });
54
+ checks.analyzeTesting({ content, filePath, addFinding: add });
55
+ checks.analyzeUITesting({ content, filePath, addFinding: add });
56
+ } catch (error) {
57
+ console.error(`[iOS Enterprise] Error analyzing ${filePath}:`, error.message);
421
58
  }
422
59
  }
423
60
 
@@ -0,0 +1,350 @@
1
+ function analyzeSwiftModerno({ content, functions = [], filePath, addFinding }) {
2
+ if (content.includes('completion:') && !content.includes('async ')) {
3
+ addFinding('ios.async_await_missing', 'medium', filePath, 1,
4
+ 'Using completion handlers instead of async/await (Swift 5.9+ required)');
5
+ }
6
+
7
+ const taskCount = (content.match(/\bTask\s*\{/g) || []).length;
8
+ if (taskCount > 3 && !content.includes('TaskGroup')) {
9
+ addFinding('ios.structured_concurrency_missing', 'medium', filePath, 1,
10
+ `Multiple Task blocks (${taskCount}) without TaskGroup - use structured concurrency`);
11
+ }
12
+
13
+ if (content.includes('actor ') && !content.includes(': Sendable')) {
14
+ addFinding('ios.sendable_missing', 'low', filePath, 1,
15
+ 'Actor should conform to Sendable protocol for thread-safe types');
16
+ }
17
+
18
+ if (content.includes('func ') && content.includes('-> View') && !content.includes('some View')) {
19
+ addFinding('ios.opaque_types_missing', 'low', filePath, 1,
20
+ 'Use "some View" instead of explicit View protocol return');
21
+ }
22
+
23
+ if (content.includes('UIViewController') && !content.includes('@State') && !content.includes('@Binding') && !content.includes('@ObservedObject')) {
24
+ addFinding('ios.property_wrappers_missing', 'info', filePath, 1,
25
+ 'Consider using SwiftUI property wrappers (@State, @Binding, @ObservedObject) for state management');
26
+ }
27
+
28
+ const extractedFunctions = parser.extractFunctions ? parser.extractFunctions({ parsed: true }) || [] : [];
29
+ extractedFunctions.forEach(fn => {
30
+ if (fn.name.includes('Array') || fn.name.includes('Collection')) {
31
+ if (!content.includes('<T>') && !content.includes('<Element>')) {
32
+ addFinding('ios.generics_missing', 'low', filePath, fn.line,
33
+ `Function ${fn.name} should use generics for type safety`);
34
+ }
35
+ }
36
+ });
37
+ }
38
+
39
+ async function analyzeSwiftUI({ usesSwiftUI, usesUIKit, content, classes, filePath, addFinding }) {
40
+ if (usesUIKit && !usesSwiftUI) {
41
+ addFinding('ios.swiftui_first', 'medium', filePath, 1,
42
+ 'Consider migrating to SwiftUI for new views (UIKit only when strictly necessary)');
43
+ }
44
+
45
+ if (usesSwiftUI) {
46
+ if (!content.includes('@State')) {
47
+ addFinding('ios.state_local_missing', 'info', filePath, 1,
48
+ 'SwiftUI view without @State - consider if local state is needed');
49
+ }
50
+
51
+ if (content.includes('ObservableObject') && !content.includes('@StateObject')) {
52
+ addFinding('ios.stateobject_missing', 'high', filePath, 1,
53
+ 'ObservableObject should be owned with @StateObject, not @ObservedObject');
54
+ }
55
+
56
+ if (content.includes('class') && content.includes('ObservableObject') && !content.includes('@EnvironmentObject')) {
57
+ addFinding('ios.environmentobject_missing', 'info', filePath, 1,
58
+ 'Consider using @EnvironmentObject for dependency injection in SwiftUI');
59
+ }
60
+
61
+ if (content.includes('.frame(') && content.includes('CGRect(')) {
62
+ addFinding('ios.declarativo_missing', 'medium', filePath, 1,
63
+ 'Using imperative CGRect in SwiftUI - use declarative .frame() modifiers');
64
+ }
65
+
66
+ const geometryReaderCount = (content.match(/GeometryReader/g) || []).length;
67
+ if (geometryReaderCount > 2) {
68
+ addFinding('ios.geometryreader_moderation', 'medium', filePath, 1,
69
+ `Excessive GeometryReader usage (${geometryReaderCount}x) - use only when necessary`);
70
+ }
71
+ }
72
+ }
73
+
74
+ async function analyzeUIKit({ classes, content, filePath, addFinding }) {
75
+ classes.forEach(cls => {
76
+ if (cls.name.includes('ViewController')) {
77
+ const linesCount = cls.substructure.length * 10;
78
+ if (linesCount > 300) {
79
+ addFinding('ios.massive_viewcontrollers', 'high', filePath, cls.line,
80
+ `Massive ViewController ${cls.name} (~${linesCount} lines) - break down into smaller components`);
81
+ }
82
+
83
+ if (!content.includes('ViewModel')) {
84
+ addFinding('ios.uikit.viewmodel_delegation', 'medium', filePath, cls.line,
85
+ `ViewController ${cls.name} should delegate logic to ViewModel (MVVM pattern)`);
86
+ }
87
+ }
88
+ });
89
+
90
+ if (filePath.endsWith('.swift') && !filePath.includes('analyzer') && !filePath.includes('detector')) {
91
+ if (content.includes('storyboard') || content.includes('.xib') || content.includes('.nib')) {
92
+ addFinding('ios.storyboards', 'high', filePath, 1,
93
+ 'Storyboard/XIB detected - use programmatic UI for better version control');
94
+ }
95
+ }
96
+ }
97
+
98
+ async function analyzeProtocolOriented({ protocols, content, filePath, addFinding }) {
99
+ if (protocols.length > 0 && !content.includes('extension ')) {
100
+ addFinding('ios.pop.missing_extensions', 'low', filePath, 1,
101
+ 'Protocols detected but no extensions - consider protocol extensions for default implementations');
102
+ }
103
+
104
+ if (content.includes('class ') && content.includes(': ')) {
105
+ const inheritanceCount = (content.match(/class\s+\w+\s*:\s*\w+/g) || []).length;
106
+ if (inheritanceCount > 2) {
107
+ addFinding('ios.pop.missing_composition_over_inheritance', 'medium', filePath, 1,
108
+ `Excessive class inheritance (${inheritanceCount}x) - prefer protocol composition`);
109
+ }
110
+ }
111
+ }
112
+
113
+ async function analyzeValueTypes({ classes, content, filePath, addFinding }) {
114
+ classes.forEach(cls => {
115
+ if (!cls.inheritedTypes.length && !content.includes('ObservableObject')) {
116
+ addFinding('ios.values.classes_instead_structs', 'medium', filePath, cls.line,
117
+ `Class ${cls.name} without inheritance - consider struct for value semantics`);
118
+ }
119
+ });
120
+
121
+ const varCount = (content.match(/\bvar\s+/g) || []).length;
122
+ const letCount = (content.match(/\blet\s+/g) || []).length;
123
+ if (varCount > letCount) {
124
+ addFinding('ios.values.mutability', 'low', filePath, 1,
125
+ `More var (${varCount}) than let (${letCount}) - prefer immutability`);
126
+ }
127
+ }
128
+
129
+ function analyzeMemoryManagement({ content, filePath, addFinding }) {
130
+ const closureMatches = content.match(/\{\s*\[/g);
131
+ const weakSelfMatches = content.match(/\[weak self\]/g);
132
+ if (closureMatches && closureMatches.length > (weakSelfMatches?.length || 0)) {
133
+ addFinding('ios.memory.missing_weak_self', 'high', filePath, 1,
134
+ 'Closures without [weak self] - potential retain cycles');
135
+ }
136
+
137
+ if (content.includes('self.') && content.includes('{') && !content.includes('[weak self]')) {
138
+ addFinding('ios.memory.retain_cycles', 'high', filePath, 1,
139
+ 'Potential retain cycle - closure captures self without [weak self]');
140
+ }
141
+
142
+ if (content.includes('class ') && !content.includes('deinit')) {
143
+ addFinding('ios.memory.missing_deinit', 'low', filePath, 1,
144
+ 'Class without deinit - consider adding for cleanup verification');
145
+ }
146
+ }
147
+
148
+ function analyzeOptionals({ content, filePath, addFinding }) {
149
+ const forceUnwraps = content.match(/(\w+)\s*!/g);
150
+ if (forceUnwraps && forceUnwraps.length > 0) {
151
+ const nonIBOutlets = forceUnwraps.filter(match => !content.includes(`@IBOutlet`));
152
+ if (nonIBOutlets.length > 0) {
153
+ addFinding('ios.force_unwrapping', 'high', filePath, 1,
154
+ `Force unwrapping (!) detected ${nonIBOutlets.length}x - use if let or guard let`);
155
+ }
156
+ }
157
+
158
+ const ifLetCount = (content.match(/if\s+let\s+/g) || []).length;
159
+ const guardLetCount = (content.match(/guard\s+let\s+/g) || []).length;
160
+ if (ifLetCount === 0 && guardLetCount === 0 && content.includes('?')) {
161
+ addFinding('ios.optionals.optional_binding', 'medium', filePath, 1,
162
+ 'Optionals present but no optional binding - use if let or guard let');
163
+ }
164
+
165
+ if (content.includes('?') && !content.includes('??')) {
166
+ addFinding('ios.optionals.missing_nil_coalescing', 'info', filePath, 1,
167
+ 'Consider using nil coalescing operator (??) for default values');
168
+ }
169
+ }
170
+
171
+ async function analyzeDependencyInjection({ classes, content, filePath, addFinding }) {
172
+ if (content.includes('.shared') || content.includes('static let shared')) {
173
+ addFinding('ios.di.singleton_usage', 'high', filePath, 1,
174
+ 'Singleton detected - use dependency injection instead');
175
+ }
176
+
177
+ classes.forEach(cls => {
178
+ if (cls.name.includes('ViewModel') || cls.name.includes('Service')) {
179
+ const hasInit = content.includes('init(');
180
+ if (!hasInit) {
181
+ addFinding('ios.di.missing_protocol_injection', 'medium', filePath, cls.line,
182
+ `${cls.name} should inject dependencies via initializer`);
183
+ }
184
+ }
185
+ });
186
+
187
+ if (content.includes('init(') && content.match(/init\([^)]{50,}\)/)) {
188
+ addFinding('ios.di.missing_factory', 'low', filePath, 1,
189
+ 'Complex initialization - consider factory pattern');
190
+ }
191
+ }
192
+
193
+ async function analyzeNetworking({ content, filePath, addFinding }) {
194
+ if (String(filePath || '').endsWith('/Package.swift') || String(filePath || '').endsWith('Package.swift')) {
195
+ return;
196
+ }
197
+ if (!content.includes('URLSession') && !content.includes('Alamofire')) {
198
+ if (content.includes('http://') || content.includes('https://')) {
199
+ addFinding('ios.networking.missing_urlsession', 'high', filePath, 1,
200
+ 'Network URLs detected but no URLSession/Alamofire usage');
201
+ }
202
+ }
203
+
204
+ if (content.includes('URLSession') && content.includes('completionHandler:') && !content.includes('async')) {
205
+ addFinding('ios.networking.completion_handlers_instead_async', 'medium', filePath, 1,
206
+ 'Using completion handlers with URLSession - migrate to async/await');
207
+ }
208
+
209
+ if (content.includes('JSONSerialization') && !content.includes('Codable')) {
210
+ addFinding('ios.networking.missing_codable', 'medium', filePath, 1,
211
+ 'Manual JSON parsing - use Codable for type safety');
212
+ }
213
+
214
+ if (content.includes('URLSession') && !content.includes('NetworkError')) {
215
+ addFinding('ios.networking.missing_error_handling', 'high', filePath, 1,
216
+ 'Network code without custom NetworkError enum');
217
+ }
218
+
219
+ const hasSSLPinningImplementation =
220
+ content.includes('serverTrustPolicy') ||
221
+ content.includes('pinning') ||
222
+ (content.includes('URLSessionDelegate') && content.includes('URLAuthenticationChallenge'));
223
+
224
+ if (content.includes('URLSession') && !hasSSLPinningImplementation) {
225
+ addFinding('ios.networking.missing_ssl_pinning', 'medium', filePath, 1,
226
+ 'Consider SSL pinning for high-security apps');
227
+ }
228
+
229
+ if (content.includes('URLSession') && !content.includes('retry')) {
230
+ addFinding('ios.networking.missing_retry', 'low', filePath, 1,
231
+ 'Network requests without retry logic');
232
+ }
233
+ }
234
+
235
+ async function analyzePersistence({ content, filePath, addFinding }) {
236
+ if (content.includes('UserDefaults') && (content.includes('password') || content.includes('token') || content.includes('auth'))) {
237
+ addFinding('ios.persistence.userdefaults_sensitive', 'critical', filePath, 1,
238
+ 'Sensitive data in UserDefaults - use Keychain instead');
239
+ }
240
+
241
+ if ((content.includes('password') || content.includes('token')) && !content.includes('Keychain') && !content.includes('Security')) {
242
+ addFinding('ios.persistence.missing_keychain', 'critical', filePath, 1,
243
+ 'Sensitive data detected but no Keychain usage');
244
+ }
245
+
246
+ if (content.includes('NSManagedObjectContext') && content.includes('.main')) {
247
+ addFinding('ios.persistence.core_data_on_main', 'high', filePath, 1,
248
+ 'Core Data operations on main thread - use background context');
249
+ }
250
+
251
+ if (content.includes('NSPersistentContainer') && !content.includes('NSMigrationManager')) {
252
+ addFinding('ios.persistence.missing_migration', 'medium', filePath, 1,
253
+ 'Core Data without migration strategy');
254
+ }
255
+ }
256
+
257
+ function analyzeCombine({ content, filePath, addFinding }) {
258
+ if (content.includes('.sink(') && !content.includes('AnyCancellable')) {
259
+ addFinding('ios.combine.missing_cancellables', 'high', filePath, 1,
260
+ 'Combine sink without storing AnyCancellable - memory leak');
261
+ }
262
+
263
+ if (content.includes('@Published') && !content.includes('import Combine')) {
264
+ addFinding('ios.combine.published_without_combine', 'high', filePath, 1,
265
+ '@Published used but Combine not imported');
266
+ }
267
+
268
+ if (content.includes('.sink(') && !content.includes('receiveCompletion')) {
269
+ addFinding('ios.combine.error_handling', 'medium', filePath, 1,
270
+ 'Combine subscriber without error handling (receiveCompletion)');
271
+ }
272
+
273
+ if (content.includes('Future<') && !content.includes('async')) {
274
+ addFinding('ios.combine.prefer_async_await', 'low', filePath, 1,
275
+ 'Combine Future for single value - consider async/await instead');
276
+ }
277
+ }
278
+
279
+ function analyzeConcurrency({ content, filePath, addFinding }) {
280
+ if (content.includes('DispatchQueue') && !content.includes('async func')) {
281
+ addFinding('ios.concurrency.dispatchqueue_old', 'medium', filePath, 1,
282
+ 'Using DispatchQueue - prefer async/await for new code');
283
+ }
284
+
285
+ if (content.includes('DispatchQueue.main') && content.includes('UI')) {
286
+ addFinding('ios.concurrency.missing_mainactor', 'medium', filePath, 1,
287
+ 'Manual main thread dispatch - use @MainActor annotation');
288
+ }
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');
293
+ }
294
+
295
+ if (content.includes('var ') && content.includes('queue') && !content.includes('actor')) {
296
+ addFinding('ios.concurrency.actor_missing', 'medium', filePath, 1,
297
+ 'Manual synchronization with queue - consider actor for thread safety');
298
+ }
299
+ }
300
+
301
+ function analyzeTesting({ content, filePath, addFinding }) {
302
+ if (filePath.includes('Test') && !content.includes('XCTest') && !content.includes('Quick')) {
303
+ addFinding('ios.testing.missing_xctest', 'high', filePath, 1,
304
+ 'Test file without XCTest or Quick import');
305
+ }
306
+
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');
310
+ }
311
+
312
+ if (filePath.includes('Test') && !content.includes('trackForMemoryLeaks') && content.includes('class')) {
313
+ addFinding('ios.testing.missing_memory_leak_tracking', 'medium', filePath, 1,
314
+ 'Test without trackForMemoryLeaks helper');
315
+ }
316
+
317
+ if (filePath.includes('Test') && content.includes('init(') && !content.includes('Protocol')) {
318
+ addFinding('ios.testing.concrete_dependencies', 'medium', filePath, 1,
319
+ 'Test using concrete dependencies - inject protocols for testability');
320
+ }
321
+ }
322
+
323
+ function analyzeUITesting({ content, filePath, addFinding }) {
324
+ if (filePath.includes('UITest') && !content.includes('XCTest')) {
325
+ addFinding('ios.uitesting.missing_xctest', 'medium', filePath, 1,
326
+ 'UI Test without XCTest import');
327
+ }
328
+
329
+ if (filePath.includes('UITest') && !content.includes('XCUIApplication')) {
330
+ addFinding('ios.uitesting.missing_application_launch', 'medium', filePath, 1,
331
+ 'UI Test missing XCUIApplication launch');
332
+ }
333
+ }
334
+
335
+ module.exports = {
336
+ analyzeSwiftModerno,
337
+ analyzeSwiftUI,
338
+ analyzeUIKit,
339
+ analyzeProtocolOriented,
340
+ analyzeValueTypes,
341
+ analyzeMemoryManagement,
342
+ analyzeOptionals,
343
+ analyzeDependencyInjection,
344
+ analyzeNetworking,
345
+ analyzePersistence,
346
+ analyzeCombine,
347
+ analyzeConcurrency,
348
+ analyzeTesting,
349
+ analyzeUITesting,
350
+ };
@@ -11,6 +11,22 @@ describe('intelligent-audit', () => {
11
11
  const mod = require('../intelligent-audit');
12
12
  expect(typeof mod.runIntelligentAudit).toBe('function');
13
13
  });
14
+
15
+ it('should filter staged violations strictly (no substring matches, no .audit_tmp)', () => {
16
+ const mod = require('../intelligent-audit');
17
+
18
+ expect(typeof mod.isViolationInStagedFiles).toBe('function');
19
+
20
+ const stagedSet = new Set([
21
+ 'apps/ios/Application/AppCoordinator.swift'
22
+ ]);
23
+
24
+ expect(mod.isViolationInStagedFiles('apps/ios/Application/AppCoordinator.swift', stagedSet)).toBe(true);
25
+ expect(mod.isViolationInStagedFiles('apps/ios/Application/AppCoordinator.swift.backup', stagedSet)).toBe(false);
26
+ expect(mod.isViolationInStagedFiles('.audit_tmp/AppCoordinator.123.staged.swift', stagedSet)).toBe(false);
27
+ expect(mod.isViolationInStagedFiles('some/dir/.audit_tmp/AppCoordinator.123.staged.swift', stagedSet)).toBe(false);
28
+ expect(mod.isViolationInStagedFiles('apps/ios/Application/AppCoordinator', stagedSet)).toBe(false);
29
+ });
14
30
  });
15
31
 
16
32
  describe('AI_EVIDENCE.json structure validation', () => {
@@ -561,7 +561,7 @@ async function updateAIEvidence(violations, gateResult, tokenUsage) {
561
561
  file: v.filePath || v.file || 'unknown',
562
562
  line: v.line || null,
563
563
  severity: v.severity,
564
- rule: ruleId,
564
+ rule_id: ruleId,
565
565
  message: v.message || v.description || '',
566
566
  category: v.category || deriveCategoryFromRuleId(ruleId),
567
567
  intelligent_evaluation: v.intelligentEvaluation || false,
@@ -25,6 +25,7 @@ AUTO_MERGE_PR=${GITFLOW_AUTO_MERGE:-false}
25
25
  PR_BASE_BRANCH=${GITFLOW_PR_BASE:-develop}
26
26
  STRICT_ATOMIC=${GITFLOW_STRICT_ATOMIC:-true}
27
27
  REQUIRE_TEST_RELATIONS=${GITFLOW_REQUIRE_TESTS:-true}
28
+ STRICT_CHECK=${GITFLOW_STRICT_CHECK:-false}
28
29
 
29
30
  print_section() {
30
31
  printf "${BLUE}═══════════════════════════════════════════════════════════════${NC}\n"
@@ -167,12 +168,17 @@ verify_atomic_commit() {
167
168
  return 0
168
169
  fi
169
170
 
170
- mapfile -t files < <($GIT_BIN diff --name-only "${commit}^..${commit}")
171
+ local files=()
172
+ while IFS= read -r file; do
173
+ [[ -z "$file" ]] && continue
174
+ files+=("$file")
175
+ done < <($GIT_BIN diff --name-only "${commit}^..${commit}")
171
176
  if [[ "${#files[@]}" -eq 0 ]]; then
172
177
  return 0
173
178
  fi
174
179
 
175
- declare -A roots=()
180
+ local roots_list
181
+ roots_list=()
176
182
  for file in "${files[@]}"; do
177
183
  local root="${file%%/*}"
178
184
  if [[ "$root" == "$file" ]]; then
@@ -186,12 +192,37 @@ verify_atomic_commit() {
186
192
  root="(root)"
187
193
  ;;
188
194
  esac
189
- roots["$root"]=1
195
+
196
+ local seen=0
197
+ local existing
198
+ if (( ${#roots_list[@]:-0} > 0 )); then
199
+ for existing in "${roots_list[@]}"; do
200
+ if [[ "$existing" == "$root" ]]; then
201
+ seen=1
202
+ break
203
+ fi
204
+ done
205
+ fi
206
+ if [[ "$seen" -eq 0 ]]; then
207
+ roots_list+=("$root")
208
+ fi
190
209
  done
191
210
 
192
- local root_count=${#roots[@]}
211
+ local root_count=${#roots_list[@]}
193
212
  if (( root_count > 1 )); then
194
- printf "${RED}❌ Commit %s toca múltiples raíces (%s). Divide los cambios en commits atómicos.${NC}\n" "$commit" "$(printf "%s " "${!roots[@]}")"
213
+ local has_scripts=0
214
+ local has_tests=0
215
+ for root in "${roots_list[@]}"; do
216
+ [[ "$root" == "scripts" ]] && has_scripts=1
217
+ [[ "$root" == "tests" ]] && has_tests=1
218
+ done
219
+
220
+ if [[ $has_scripts -eq 1 && $has_tests -eq 1 && $root_count -eq 2 ]]; then
221
+ printf "${GREEN}✅ Commit %s toca scripts + tests (permitido para bugfixes/features con tests).${NC}\n" "$commit"
222
+ return 0
223
+ fi
224
+
225
+ printf "${RED}❌ Commit %s toca múltiples raíces (%s). Divide los cambios en commits atómicos.${NC}\n" "$commit" "$(printf "%s " "${roots_list[@]}")"
195
226
  return 1
196
227
  fi
197
228
  if (( root_count == 0 )); then
@@ -199,7 +230,7 @@ verify_atomic_commit() {
199
230
  return 0
200
231
  fi
201
232
  local root_name
202
- for root_name in "${!roots[@]}"; do
233
+ for root_name in "${roots_list[@]}"; do
203
234
  printf "${GREEN}✅ Commit %s cumple atomicidad (raíz %s).${NC}\n" "$commit" "$root_name"
204
235
  done
205
236
  return 0
@@ -219,7 +250,11 @@ verify_pending_commits_atomic() {
219
250
  return $?
220
251
  fi
221
252
 
222
- mapfile -t commits < <($GIT_BIN rev-list "${base_ref}..${branch}")
253
+ local commits=()
254
+ while IFS= read -r commit; do
255
+ [[ -z "$commit" ]] && continue
256
+ commits+=("$commit")
257
+ done < <($GIT_BIN rev-list "${base_ref}..${branch}")
223
258
  local failed=0
224
259
  for commit in "${commits[@]}"; do
225
260
  if ! verify_atomic_commit "$commit"; then
@@ -349,22 +384,50 @@ cmd_check() {
349
384
  local branch
350
385
  branch=$(current_branch)
351
386
  printf "${CYAN}📍 Rama actual: %s${NC}\n" "$branch"
352
- ensure_evidence_fresh || true
353
- lint_hooks_system || true
354
- run_mobile_checks || true
387
+ local failed=0
388
+
389
+ if [[ "${STRICT_CHECK}" == "true" ]]; then
390
+ ensure_evidence_fresh || failed=1
391
+ lint_hooks_system || failed=1
392
+ run_mobile_checks || failed=1
393
+ else
394
+ ensure_evidence_fresh || true
395
+ lint_hooks_system || true
396
+ run_mobile_checks || true
397
+ fi
355
398
  print_sync_table
356
399
  print_cleanup_candidates
357
- verify_atomic_commit "HEAD" || true
358
- if [[ "$REQUIRE_TEST_RELATIONS" == "true" ]]; then
359
- verify_related_files_commit "HEAD" || true
400
+ if [[ "${STRICT_CHECK}" == "true" ]]; then
401
+ verify_atomic_commit "HEAD" || failed=1
402
+ if [[ "$REQUIRE_TEST_RELATIONS" == "true" ]]; then
403
+ verify_related_files_commit "HEAD" || failed=1
404
+ fi
405
+ if ! verify_pending_commits_atomic "$branch"; then
406
+ failed=1
407
+ fi
408
+ if [[ "$REQUIRE_TEST_RELATIONS" == "true" ]]; then
409
+ if ! verify_pending_commits_related "$branch"; then
410
+ failed=1
411
+ fi
412
+ fi
413
+ else
414
+ verify_atomic_commit "HEAD" || true
415
+ if [[ "$REQUIRE_TEST_RELATIONS" == "true" ]]; then
416
+ verify_related_files_commit "HEAD" || true
417
+ fi
360
418
  fi
361
419
  local pending
362
420
  pending=$(unpushed_commits "$branch")
363
421
  if [[ "$pending" != "0" ]]; then
364
422
  printf "${YELLOW}⚠️ Commits sin subir (${pending}). Ejecuta git push.${NC}\n"
423
+ if [[ "${STRICT_CHECK}" == "true" ]]; then
424
+ failed=1
425
+ fi
365
426
  else
366
427
  printf "${GREEN}✅ No hay commits pendientes de push.${NC}\n"
367
428
  fi
429
+
430
+ return $failed
368
431
  }
369
432
 
370
433
  cmd_cycle() {
@@ -518,8 +581,6 @@ main() {
518
581
  esac
519
582
  }
520
583
 
521
- main "$@"
522
-
523
584
  is_test_file() {
524
585
  local file="$1"
525
586
  case "$file" in
@@ -620,10 +681,15 @@ verify_pending_commits_related() {
620
681
  local base_ref="origin/${branch}"
621
682
 
622
683
  if ! $GIT_BIN show-ref --verify --quiet "refs/remotes/origin/${branch}"; then
623
- return verify_related_files_commit "HEAD"
684
+ verify_related_files_commit "HEAD"
685
+ return $?
624
686
  fi
625
687
 
626
- mapfile -t commits < <($GIT_BIN rev-list "${base_ref}..${branch}")
688
+ local commits=()
689
+ while IFS= read -r commit; do
690
+ [[ -z "$commit" ]] && continue
691
+ commits+=("$commit")
692
+ done < <($GIT_BIN rev-list "${base_ref}..${branch}")
627
693
  local failed=0
628
694
  local commit
629
695
  for commit in "${commits[@]}"; do
@@ -633,3 +699,5 @@ verify_pending_commits_related() {
633
699
  done
634
700
  return $failed
635
701
  }
702
+
703
+ main "$@"