pumuki-ast-hooks 5.5.51 → 5.5.52

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 (38) hide show
  1. package/docs/MCP_SERVERS.md +16 -2
  2. package/docs/RELEASE_NOTES.md +34 -0
  3. package/hooks/git-status-monitor.ts +0 -5
  4. package/hooks/notify-macos.ts +0 -1
  5. package/hooks/pre-tool-use-evidence-validator.ts +0 -1
  6. package/package.json +2 -2
  7. package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +48 -0
  8. package/scripts/hooks-system/application/services/guard/GuardConfig.js +2 -4
  9. package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +2 -20
  10. package/scripts/hooks-system/application/services/installation/McpConfigurator.js +9 -205
  11. package/scripts/hooks-system/application/services/installation/mcp/McpGlobalConfigCleaner.js +49 -0
  12. package/scripts/hooks-system/application/services/installation/mcp/McpProjectConfigWriter.js +59 -0
  13. package/scripts/hooks-system/application/services/installation/mcp/McpServerConfigBuilder.js +103 -0
  14. package/scripts/hooks-system/application/services/monitoring/EvidenceMonitor.js +146 -5
  15. package/scripts/hooks-system/infrastructure/ast/ast-core.js +1 -13
  16. package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +3 -2
  17. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +17 -9
  18. package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +2 -1
  19. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +137 -27
  20. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDChecks.js +385 -0
  21. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDRules.js +38 -408
  22. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +397 -34
  23. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMChecks.js +408 -0
  24. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMRules.js +36 -442
  25. package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenExtractor.js +146 -0
  26. package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenParser.js +22 -190
  27. package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenRunner.js +62 -0
  28. package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +42 -47
  29. package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +0 -16
  30. package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +14 -25
  31. package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +0 -10
  32. package/scripts/hooks-system/application/services/installation/HookAssetsInstaller.js +0 -0
  33. package/scripts/hooks-system/application/services/monitoring/EvidenceRefreshRunner.js +0 -161
  34. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/StagedSwiftFilePreparer.js +0 -59
  35. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftAstRunner.js +0 -51
  36. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftToolchainResolver.js +0 -57
  37. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSAstAnalysisOrchestrator.js +0 -32
  38. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +0 -350
@@ -0,0 +1,103 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const crypto = require('crypto');
5
+ const env = require('../../../../config/env.js');
6
+
7
+ function slugifyId(input) {
8
+ return String(input || '')
9
+ .trim()
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, '-')
12
+ .replace(/^-+|-+$/g, '')
13
+ .slice(0, 48);
14
+ }
15
+
16
+ class McpServerConfigBuilder {
17
+ constructor(targetRoot, hookSystemRoot, logger = null) {
18
+ this.targetRoot = targetRoot;
19
+ this.hookSystemRoot = hookSystemRoot;
20
+ this.logger = logger;
21
+ }
22
+
23
+ build() {
24
+ const serverId = this.computeServerIdForRepo(this.targetRoot);
25
+ const entrypoint = this.resolveAutomationEntrypoint();
26
+ const nodePath = this.resolveNodeBinary();
27
+
28
+ const mcpConfig = {
29
+ mcpServers: {
30
+ [serverId]: {
31
+ command: nodePath,
32
+ args: [entrypoint],
33
+ env: {
34
+ REPO_ROOT: this.targetRoot,
35
+ AUTO_COMMIT_ENABLED: 'false',
36
+ AUTO_PUSH_ENABLED: 'false',
37
+ AUTO_PR_ENABLED: 'false'
38
+ }
39
+ }
40
+ }
41
+ };
42
+
43
+ return { serverId, mcpConfig };
44
+ }
45
+
46
+ resolveAutomationEntrypoint() {
47
+ const candidates = [
48
+ this.hookSystemRoot
49
+ ? path.join(this.hookSystemRoot, 'infrastructure', 'mcp', 'ast-intelligence-automation.js')
50
+ : null,
51
+ path.join(this.targetRoot, 'scripts', 'hooks-system', 'infrastructure', 'mcp', 'ast-intelligence-automation.js')
52
+ ].filter(Boolean);
53
+
54
+ for (const candidate of candidates) {
55
+ try {
56
+ if (fs.existsSync(candidate)) return candidate;
57
+ } catch (error) {
58
+ this.logger?.warn?.('MCP_ENTRYPOINT_CHECK_FAILED', { candidate, error: error.message });
59
+ }
60
+ }
61
+
62
+ return path.join(this.targetRoot, 'scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js');
63
+ }
64
+
65
+ resolveNodeBinary() {
66
+ let nodePath = process.execPath;
67
+ if (nodePath && fs.existsSync(nodePath)) return nodePath;
68
+
69
+ try {
70
+ return execSync('which node', { encoding: 'utf-8' }).trim();
71
+ } catch (error) {
72
+ if (process.env.DEBUG) {
73
+ process.stderr.write(`[MCP] which node failed: ${error && error.message ? error.message : String(error)}\n`);
74
+ }
75
+ return 'node';
76
+ }
77
+ }
78
+
79
+ computeServerIdForRepo(repoRoot) {
80
+ const legacyServerId = 'ast-intelligence-automation';
81
+ const forced = (env.get('MCP_SERVER_ID', '') || '').trim();
82
+ if (forced.length > 0) return forced;
83
+
84
+ const repoName = path.basename(repoRoot || process.cwd());
85
+ const slug = slugifyId(repoName) || 'repo';
86
+ const fp = this.computeRepoFingerprint(repoRoot);
87
+ return `${legacyServerId}-${slug}-${fp}`;
88
+ }
89
+
90
+ computeRepoFingerprint(repoRoot) {
91
+ try {
92
+ const real = fs.realpathSync(repoRoot);
93
+ return crypto.createHash('sha1').update(real).digest('hex').slice(0, 8);
94
+ } catch (error) {
95
+ if (process.env.DEBUG) {
96
+ process.stderr.write(`[MCP] computeRepoFingerprint fallback: ${error && error.message ? error.message : String(error)}\n`);
97
+ }
98
+ return crypto.createHash('sha1').update(String(repoRoot || '')).digest('hex').slice(0, 8);
99
+ }
100
+ }
101
+ }
102
+
103
+ module.exports = McpServerConfigBuilder;
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { EvidenceRefreshRunner } = require('./EvidenceRefreshRunner');
3
+ const { execSync } = require('child_process');
4
+ const { ConfigurationError, DomainError } = require('../../../domain/errors');
4
5
  const AuditLogger = require('../logging/AuditLogger');
5
6
 
6
7
  class EvidenceMonitor {
@@ -13,9 +14,98 @@ class EvidenceMonitor {
13
14
  this.lastStaleNotification = 0;
14
15
  this.pollTimer = null;
15
16
  this.evidencePath = path.join(repoRoot, '.AI_EVIDENCE.json');
16
- this.refreshRunner = new EvidenceRefreshRunner(repoRoot, {
17
- refreshTimeoutMs: options.refreshTimeoutMs
18
- });
17
+ this.tempDir = path.join(repoRoot, '.audit_tmp');
18
+ this.updateScript = this.resolveUpdateEvidenceScript();
19
+ this.refreshInFlight = false;
20
+ this.refreshTimeoutMs = options.refreshTimeoutMs || 120000;
21
+ this.refreshLockFile = path.join(this.tempDir, 'evidence-refresh.lock');
22
+ }
23
+
24
+ isPidRunning(pid) {
25
+ if (!pid || !Number.isFinite(pid) || pid <= 0) return false;
26
+ try {
27
+ process.kill(pid, 0);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ acquireRefreshLock() {
35
+ try {
36
+ fs.mkdirSync(this.tempDir, { recursive: true });
37
+ } catch (error) {
38
+ console.warn('[EvidenceMonitor] Failed to ensure temp dir:', error.message);
39
+ }
40
+
41
+ try {
42
+ const fd = fs.openSync(this.refreshLockFile, 'wx');
43
+ const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
44
+ fs.writeFileSync(fd, payload, { encoding: 'utf8' });
45
+ fs.closeSync(fd);
46
+ return { acquired: true };
47
+ } catch (error) {
48
+ if (error && error.code !== 'EEXIST') {
49
+ return { acquired: false, reason: 'error', error };
50
+ }
51
+
52
+ try {
53
+ const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
54
+ const data = raw ? JSON.parse(raw) : null;
55
+ const lockPid = data && Number(data.pid);
56
+ if (lockPid && this.isPidRunning(lockPid)) {
57
+ return { acquired: false, reason: 'locked', pid: lockPid };
58
+ }
59
+ } catch (error) {
60
+ console.warn('[EvidenceMonitor] Failed to read refresh lock file:', error.message);
61
+ }
62
+
63
+ try {
64
+ fs.unlinkSync(this.refreshLockFile);
65
+ } catch (error) {
66
+ console.warn('[EvidenceMonitor] Failed to remove stale refresh lock:', error.message);
67
+ }
68
+
69
+ try {
70
+ const fd = fs.openSync(this.refreshLockFile, 'wx');
71
+ const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
72
+ fs.writeFileSync(fd, payload, { encoding: 'utf8' });
73
+ fs.closeSync(fd);
74
+ return { acquired: true };
75
+ } catch (retryError) {
76
+ return { acquired: false, reason: 'locked', error: retryError };
77
+ }
78
+ }
79
+ }
80
+
81
+ releaseRefreshLock() {
82
+ try {
83
+ if (!fs.existsSync(this.refreshLockFile)) return;
84
+ const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
85
+ const data = raw ? JSON.parse(raw) : null;
86
+ const lockPid = data && Number(data.pid);
87
+ if (lockPid === process.pid) {
88
+ fs.unlinkSync(this.refreshLockFile);
89
+ }
90
+ } catch (error) {
91
+ console.warn('[EvidenceMonitor] Failed to release refresh lock:', error.message);
92
+ }
93
+ }
94
+
95
+ resolveUpdateEvidenceScript() {
96
+ const candidates = [
97
+ path.join(this.repoRoot, 'node_modules/@pumuki/ast-intelligence-hooks/bin/update-evidence.sh'),
98
+ path.join(this.repoRoot, 'scripts/hooks-system/bin/update-evidence.sh'),
99
+ path.join(this.repoRoot, 'bin/update-evidence.sh')
100
+ ];
101
+
102
+ for (const candidate of candidates) {
103
+ if (fs.existsSync(candidate)) {
104
+ return candidate;
105
+ }
106
+ }
107
+
108
+ return null;
19
109
  }
20
110
 
21
111
  isStale() {
@@ -33,7 +123,58 @@ class EvidenceMonitor {
33
123
  }
34
124
 
35
125
  async refresh() {
36
- return this.refreshRunner.refresh();
126
+ if (!this.updateScript) {
127
+ throw new ConfigurationError('Update evidence script not found', 'updateScript');
128
+ }
129
+
130
+ if (this.refreshInFlight) {
131
+ return '';
132
+ }
133
+
134
+ const lock = this.acquireRefreshLock();
135
+ if (!lock.acquired) {
136
+ return '';
137
+ }
138
+
139
+ this.refreshInFlight = true;
140
+
141
+ return new Promise((resolve, reject) => {
142
+ const child = require('child_process').spawn('bash', [this.updateScript, '--auto', '--refresh-only'], {
143
+ cwd: this.repoRoot,
144
+ stdio: ['pipe', 'pipe', 'pipe']
145
+ });
146
+
147
+ let output = '';
148
+ child.stdout.on('data', (data) => {
149
+ output += data.toString();
150
+ });
151
+
152
+ const timeoutId = setTimeout(() => {
153
+ try {
154
+ child.kill('SIGKILL');
155
+ } catch (error) {
156
+ console.warn('[EvidenceMonitor] Failed to kill timed-out refresh process:', error.message);
157
+ }
158
+ }, this.refreshTimeoutMs);
159
+
160
+ child.on('close', (code) => {
161
+ clearTimeout(timeoutId);
162
+ this.refreshInFlight = false;
163
+ this.releaseRefreshLock();
164
+ if (code === 0) {
165
+ resolve(output);
166
+ } else {
167
+ reject(new DomainError(`Evidence refresh failed with code ${code}`, 'EVIDENCE_REFRESH_FAILED'));
168
+ }
169
+ });
170
+
171
+ child.on('error', (err) => {
172
+ clearTimeout(timeoutId);
173
+ this.refreshInFlight = false;
174
+ this.releaseRefreshLock();
175
+ reject(err);
176
+ });
177
+ });
37
178
  }
38
179
 
39
180
  startPolling(onStale, onRefreshed) {
@@ -45,18 +45,6 @@ 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
- }
60
48
  if (p.includes("node_modules/")) return true;
61
49
  if (p.includes("/.next/")) return true;
62
50
  if (p.includes("/dist/")) return true;
@@ -137,7 +125,7 @@ let exclusionsConfig = null;
137
125
  function loadExclusions() {
138
126
  if (exclusionsConfig) return exclusionsConfig;
139
127
  try {
140
- const configPath = path.join(getRepoRoot(), 'config', 'ast-exclusions.json');
128
+ const configPath = path.join(__dirname, '../../config/ast-exclusions.json');
141
129
  if (fs.existsSync(configPath)) {
142
130
  exclusionsConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
143
131
  }
@@ -22,7 +22,7 @@ function formatLocalTimestamp(date = new Date()) {
22
22
  }
23
23
 
24
24
  const astModulesPath = __dirname;
25
- const { createProject, platformOf, mapToLevel, shouldIgnore: coreShouldIgnore } = require(path.join(astModulesPath, "ast-core"));
25
+ const { createProject, platformOf, mapToLevel } = 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,6 +138,8 @@ 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;
141
143
  return false;
142
144
  };
143
145
 
@@ -704,7 +706,6 @@ function listSourceFiles(root) {
704
706
  */
705
707
  function shouldIgnore(file) {
706
708
  const p = file.replace(/\\/g, "/");
707
- if (typeof coreShouldIgnore === 'function' && coreShouldIgnore(p)) return true;
708
709
  if (p.includes("node_modules/")) return true;
709
710
  if (p.includes("/.cursor/")) return true;
710
711
  if (/\.bak/i.test(p)) return true;
@@ -126,10 +126,13 @@ 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
+
129
131
  sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
130
132
  const className = cls.getName() || '';
131
133
  const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
132
- if (isValueObject) return;
134
+ const isTestClass = /Spec$|Test$|Mock/.test(className);
135
+ if (isValueObject || isTestClass) return;
133
136
 
134
137
  const methodsCount = cls.getMethods().length;
135
138
  const propertiesCount = cls.getProperties().length;
@@ -207,6 +210,9 @@ function runBackendIntelligence(project, findings, platform) {
207
210
 
208
211
  if (platformOf(filePath) !== "backend") return;
209
212
 
213
+ const normalizedBackendPath = filePath.replace(/\\/g, '/');
214
+ const isRealBackendAppFile = normalizedBackendPath.includes('/apps/backend/') || normalizedBackendPath.includes('apps/backend/');
215
+
210
216
  // NO excluir archivos AST - la librería debe auto-auditarse para detectar God classes masivas
211
217
 
212
218
  const fullText = sf.getFullText();
@@ -272,7 +278,7 @@ function runBackendIntelligence(project, findings, platform) {
272
278
  fullTextUpper.includes("ConfigService") ||
273
279
  usesEnvHelper;
274
280
  const hasConfigUsage = /process\.env\b|ConfigService|env\.get(Bool|Number)?\(|\bconfig\s*[\.\[]/i.test(sf.getFullText());
275
- if (!hasEnvSpecific && hasConfigUsage && !isTestFile(filePath)) {
281
+ if (isRealBackendAppFile && !hasEnvSpecific && hasConfigUsage && !isTestFile(filePath)) {
276
282
  pushFinding("backend.config.missing_env_separation", "info", sf, sf, "Missing environment-specific configuration - consider NODE_ENV or ConfigService", findings);
277
283
  }
278
284
 
@@ -287,6 +293,10 @@ function runBackendIntelligence(project, findings, platform) {
287
293
  analyzeGodClasses(sf, findings, { SyntaxKind, pushFinding, godClassBaseline });
288
294
  }
289
295
 
296
+ if (!isRealBackendAppFile) {
297
+ return;
298
+ }
299
+
290
300
  sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
291
301
  const name = cls.getName();
292
302
  if (name && /Entity|Model|Domain/.test(name)) {
@@ -349,16 +359,12 @@ function runBackendIntelligence(project, findings, platform) {
349
359
  }
350
360
  });
351
361
 
352
- const filePathNormalizedForMetrics = filePath.replace(/\\/g, "/");
353
- const filePathNormalizedForMetricsLower = filePathNormalizedForMetrics.toLowerCase();
354
- const isInternalAstToolingFileForMetrics = filePathNormalizedForMetricsLower.includes("/infrastructure/ast/") || filePathNormalizedForMetricsLower.includes("infrastructure/ast/") || filePathNormalizedForMetricsLower.includes("/scripts/hooks-system/infrastructure/ast/");
355
-
356
362
  const fullTextLower = fullText.toLowerCase();
357
363
  const hasMetrics = fullTextLower.includes("micrometer") || fullTextLower.includes("prometheus") ||
358
364
  fullTextLower.includes("actuator") || fullTextLower.includes("metrics");
359
365
  const looksLikeServiceOrController = fullTextLower.includes("controller") || fullTextLower.includes("service");
360
366
 
361
- if (!isInternalAstToolingFileForMetrics && !hasMetrics && looksLikeServiceOrController) {
367
+ if (isRealBackendAppFile && !hasMetrics && looksLikeServiceOrController) {
362
368
  pushFinding("backend.metrics.missing_prometheus", "info", sf, sf, "Missing application metrics - consider Spring Boot Actuator or Micrometer for monitoring", findings);
363
369
  }
364
370
 
@@ -478,7 +484,7 @@ function runBackendIntelligence(project, findings, platform) {
478
484
  sf.getDescendantsOfKind(SyntaxKind.ThrowStatement).forEach((throwStmt) => {
479
485
  const expr = throwStmt.getExpression();
480
486
  if (!expr) return;
481
- const exprText = expr.getText();
487
+ const exprText = throwStmt.getExpression().getText();
482
488
  if (exprText.includes("Error(") || exprText.includes("Exception(")) {
483
489
  const isCustom = exprText.includes("Exception") &&
484
490
  (exprText.includes("Validation") ||
@@ -486,7 +492,9 @@ function runBackendIntelligence(project, findings, platform) {
486
492
  exprText.includes("Unauthorized") ||
487
493
  exprText.includes("Forbidden"));
488
494
  if (!isCustom) {
489
- pushFinding("backend.error.custom_exceptions", "info", sf, throwStmt, "Generic Error/Exception thrown - create custom exception classes for better error handling", findings);
495
+ if (isRealBackendAppFile) {
496
+ pushFinding("backend.error.custom_exceptions", "info", sf, throwStmt, "Generic Error/Exception thrown - create custom exception classes for better error handling", findings);
497
+ }
490
498
  }
491
499
  }
492
500
  });
@@ -7,7 +7,8 @@ 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
- if (isValueObject) return;
10
+ const isTestClass = /Spec$|Test$|Mock/.test(className);
11
+ if (isValueObject || isTestClass) return;
11
12
 
12
13
  const methodsCount = cls.getMethods().length;
13
14
  const propertiesCount = cls.getProperties().length;
@@ -1,6 +1,7 @@
1
1
  const { execSync } = require('child_process');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
+ const crypto = require('crypto');
4
5
  const env = require(path.join(__dirname, '../../../../config/env'));
5
6
  const {
6
7
  resetCollections,
@@ -8,21 +9,13 @@ const {
8
9
  analyzeCollectedNodes,
9
10
  finalizeGodClassDetection,
10
11
  } = require('../detectors/ios-ast-intelligent-strategies');
11
- const { SwiftToolchainResolver } = require('./SwiftToolchainResolver');
12
- const { StagedSwiftFilePreparer } = require('./StagedSwiftFilePreparer');
13
- const { SwiftAstRunner } = require('./SwiftAstRunner');
14
- const { iOSAstAnalysisOrchestrator } = require('./iOSAstAnalysisOrchestrator');
15
12
 
16
13
  class iOSASTIntelligentAnalyzer {
17
14
  constructor(findings) {
18
15
  this.findings = findings;
19
- this.toolchain = new SwiftToolchainResolver();
20
- this.sourceKittenPath = this.toolchain.sourceKittenPath;
21
- this.isAvailable = this.toolchain.checkSourceKitten();
22
- this.swiftSyntaxPath = this.toolchain.swiftSyntaxPath;
23
- this.hasSwiftSyntax = Boolean(this.swiftSyntaxPath);
24
- this.swiftRunner = new SwiftAstRunner({ toolchain: this.toolchain });
25
- this.analysisOrchestrator = new iOSAstAnalysisOrchestrator();
16
+ this.sourceKittenPath = '/opt/homebrew/bin/sourcekitten';
17
+ this.isAvailable = this.checkSourceKitten();
18
+ this.hasSwiftSyntax = this.checkSwiftSyntax();
26
19
  this.fileContent = '';
27
20
  this.currentFilePath = '';
28
21
  this.godClassCandidates = [];
@@ -36,6 +29,99 @@ class iOSASTIntelligentAnalyzer {
36
29
  this.closures = [];
37
30
  }
38
31
 
32
+ resolveAuditTmpDir(repoRoot) {
33
+ const configured = String(env.get('AUDIT_TMP', '') || '').trim();
34
+ if (configured.length > 0) {
35
+ return path.isAbsolute(configured) ? configured : path.join(repoRoot, configured);
36
+ }
37
+ return path.join(repoRoot, '.audit_tmp');
38
+ }
39
+
40
+ safeTempFilePath(repoRoot, displayPath) {
41
+ const tmpDir = this.resolveAuditTmpDir(repoRoot);
42
+ const hash = crypto.createHash('sha1').update(String(displayPath)).digest('hex').slice(0, 10);
43
+ const base = path.basename(displayPath, '.swift');
44
+ const filename = `${base}.${hash}.staged.swift`;
45
+ return path.join(tmpDir, filename);
46
+ }
47
+
48
+ readStagedFileContent(repoRoot, relPath) {
49
+ try {
50
+ return execSync(`git show :"${relPath}"`, { encoding: 'utf8', cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });
51
+ } catch (error) {
52
+ if (process.env.DEBUG) {
53
+ console.debug(`[iOSASTIntelligentAnalyzer] Failed to read staged file ${relPath}: ${error.message}`);
54
+ }
55
+ return null;
56
+ }
57
+ }
58
+
59
+ checkSourceKitten() {
60
+ try {
61
+ execSync(`${this.sourceKittenPath} version`, { stdio: 'pipe' });
62
+ return true;
63
+ } catch (error) {
64
+ if (process.env.DEBUG) {
65
+ console.debug(`[iOSASTIntelligentAnalyzer] SourceKitten not available at ${this.sourceKittenPath}: ${error.message}`);
66
+ }
67
+ return false;
68
+ }
69
+ }
70
+
71
+ checkSwiftSyntax() {
72
+ const projectRoot = require('child_process')
73
+ .execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' })
74
+ .trim();
75
+
76
+ const possiblePaths = [
77
+ path.join(__dirname, '../../../../../CustomLintRules/.build/debug/swift-ast-analyzer'),
78
+ path.join(projectRoot, 'CustomLintRules/.build/debug/swift-ast-analyzer'),
79
+ path.join(projectRoot, '.build/debug/swift-ast-analyzer')
80
+ ];
81
+ for (const p of possiblePaths) {
82
+ if (fs.existsSync(p)) {
83
+ this.swiftSyntaxPath = p;
84
+ return true;
85
+ }
86
+ }
87
+ return false;
88
+ }
89
+
90
+ analyzeWithSwiftSyntax(filePath, displayPath = null) {
91
+ if (!this.swiftSyntaxPath) return;
92
+ try {
93
+ const result = execSync(`"${this.swiftSyntaxPath}" "${filePath}"`, {
94
+ encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe']
95
+ });
96
+ const violations = JSON.parse(result);
97
+ for (const v of violations) {
98
+ const reportedPath = displayPath || filePath;
99
+ this.findings.push({
100
+ ruleId: v.ruleId, severity: v.severity, filePath: reportedPath,
101
+ line: v.line, column: v.column, message: v.message
102
+ });
103
+ }
104
+ } catch (error) {
105
+ console.error('[iOSASTIntelligentAnalyzer] Error parsing file:', error.message);
106
+ }
107
+ }
108
+
109
+ parseFile(filePath) {
110
+ if (!this.isAvailable) return null;
111
+ try {
112
+ const result = execSync(
113
+ `${this.sourceKittenPath} structure --file "${filePath}"`,
114
+ { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
115
+ );
116
+ return JSON.parse(result);
117
+ } catch (error) {
118
+ if (process.env.DEBUG) {
119
+ console.debug(`[iOSASTIntelligentAnalyzer] SourceKitten parse failed for ${filePath}: ${error.message}`);
120
+ }
121
+ return null;
122
+ }
123
+ }
124
+
39
125
  analyzeFile(filePath, options = {}) {
40
126
  if (!filePath || !String(filePath).endsWith('.swift')) return;
41
127
 
@@ -43,29 +129,53 @@ class iOSASTIntelligentAnalyzer {
43
129
  const displayPath = options.displayPath || filePath;
44
130
  const stagedRelPath = options.stagedRelPath || null;
45
131
  const stagingOnly = env.get('STAGING_ONLY_MODE', '0') === '1';
46
- const auditTmpDir = this.toolchain.resolveAuditTmpDir(repoRoot);
47
- const stagedPreparer = new StagedSwiftFilePreparer({ repoRoot, auditTmpDir });
48
132
 
49
- const { parsePath, contentOverride } = stagedPreparer.prepare({
50
- filePath,
51
- stagedRelPath,
52
- stagingOnly
53
- });
133
+ let parsePath = filePath;
134
+ let contentOverride = null;
135
+
136
+ if (stagingOnly && stagedRelPath) {
137
+ const stagedContent = this.readStagedFileContent(repoRoot, stagedRelPath);
138
+ if (typeof stagedContent === 'string') {
139
+ const tmpPath = this.safeTempFilePath(repoRoot, stagedRelPath);
140
+ try {
141
+ fs.mkdirSync(path.dirname(tmpPath), { recursive: true });
142
+ fs.writeFileSync(tmpPath, stagedContent, 'utf8');
143
+ parsePath = tmpPath;
144
+ contentOverride = stagedContent;
145
+ } catch (error) {
146
+ if (process.env.DEBUG) {
147
+ console.debug(`[iOSASTIntelligentAnalyzer] Failed to write temp staged file ${tmpPath}: ${error.message}`);
148
+ }
149
+ // Fall back to working tree file
150
+ }
151
+ }
152
+ }
54
153
 
55
154
  if (this.hasSwiftSyntax) {
56
- this.swiftRunner.runSwiftSyntax(parsePath, displayPath, this.findings);
155
+ this.analyzeWithSwiftSyntax(parsePath, displayPath);
57
156
  }
58
157
 
59
- const ast = this.swiftRunner.parseSourceKitten(parsePath);
158
+ const ast = this.parseFile(parsePath);
60
159
  if (!ast) return;
61
160
 
62
- this.analysisOrchestrator.run({
63
- analyzer: this,
64
- ast,
65
- parsePath,
66
- displayPath,
67
- contentOverride
68
- });
161
+ this.currentFilePath = displayPath;
162
+ resetCollections(this);
163
+
164
+ try {
165
+ this.fileContent = typeof contentOverride === 'string'
166
+ ? contentOverride
167
+ : fs.readFileSync(parsePath, 'utf8');
168
+ } catch (error) {
169
+ if (process.env.DEBUG) {
170
+ console.debug(`[iOSASTIntelligentAnalyzer] Failed to read file content ${parsePath}: ${error.message}`);
171
+ }
172
+ this.fileContent = '';
173
+ }
174
+
175
+ const substructure = ast['key.substructure'] || [];
176
+
177
+ collectAllNodes(this, substructure, null);
178
+ analyzeCollectedNodes(this, displayPath);
69
179
  }
70
180
 
71
181
  safeStringify(obj) {