pumuki-ast-hooks 5.5.51 → 5.5.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -866,6 +866,15 @@ Automates the complete Git Flow cycle: commit → push → PR → merge, plus co
866
866
 
867
867
  For more details, see [MCP_SERVERS.md](./docs/MCP_SERVERS.md).
868
868
 
869
+ #### Troubleshooting
870
+
871
+ If `ai_gate_check` behaves inconsistently (stale branch name, missing rules, or intermittent transport errors), verify you are not running multiple `ast-intelligence-automation` servers across different repositories.
872
+
873
+ - Prefer enabling a single MCP server for the repository you are working on.
874
+ - Verify the active process points to this repository path:
875
+ - `.../ast-intelligence-hooks/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js`
876
+ - If you detect multiple processes, stop the duplicates and restart your IDE/MCP servers.
877
+
869
878
  ---
870
879
 
871
880
  ## API Reference
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki-ast-hooks",
3
- "version": "5.5.51",
3
+ "version": "5.5.53",
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": {
@@ -158,3 +158,11 @@
158
158
  {"timestamp":1767782291627,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
159
159
  {"timestamp":1767782291627,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
160
160
  {"timestamp":1767782291627,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
161
+ {"timestamp":1767784424360,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
162
+ {"timestamp":1767784424360,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
163
+ {"timestamp":1767784424360,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
164
+ {"timestamp":1767784424360,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
165
+ {"timestamp":1767784524038,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
166
+ {"timestamp":1767784524038,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
167
+ {"timestamp":1767784524038,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
168
+ {"timestamp":1767784524038,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
@@ -31,10 +31,47 @@ class AutonomousOrchestrator {
31
31
  }
32
32
  }
33
33
 
34
+ detectFromBranchKeywords(branchName) {
35
+ try {
36
+ const PlatformHeuristics = require('./platform/PlatformHeuristics');
37
+ const heuristics = new PlatformHeuristics(this.platformDetector);
38
+ return heuristics.detectFromBranchKeywords(branchName);
39
+ } catch (error) {
40
+ const msg = error && error.message ? error.message : String(error);
41
+ this.logger?.debug?.('ORCHESTRATOR_BRANCH_KEYWORDS_DETECTION_ERROR', { error: msg });
42
+ return [];
43
+ }
44
+ }
45
+
46
+ detectFromEvidenceFile() {
47
+ try {
48
+ const PlatformHeuristics = require('./platform/PlatformHeuristics');
49
+ const heuristics = new PlatformHeuristics(this.platformDetector);
50
+ return heuristics.detectFromEvidenceFile();
51
+ } catch (error) {
52
+ const msg = error && error.message ? error.message : String(error);
53
+ this.logger?.debug?.('ORCHESTRATOR_EVIDENCE_FILE_DETECTION_ERROR', { error: msg });
54
+ return [];
55
+ }
56
+ }
57
+
34
58
  detectFromASTSystemFilesLegacy(files) {
35
59
  return this.detectFromASTSystemFiles(files);
36
60
  }
37
61
 
62
+ async scoreConfidence(platforms) {
63
+ try {
64
+ const PlatformAnalysisService = require('./PlatformAnalysisService');
65
+ const analysisService = new PlatformAnalysisService(this.platformDetector);
66
+ const context = await this.contextEngine.detectContext();
67
+ return analysisService.analyzeConfidence(platforms || [], context || {});
68
+ } catch (error) {
69
+ const msg = error && error.message ? error.message : String(error);
70
+ this.logger?.debug?.('ORCHESTRATOR_SCORE_CONFIDENCE_ERROR', { error: msg });
71
+ return [];
72
+ }
73
+ }
74
+
38
75
  async analyzeContext() {
39
76
  const platforms = await this.detectActivePlatforms();
40
77
  const scores = await this.scoreConfidence(platforms);
@@ -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) {
@@ -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) {
@@ -1,161 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { spawn } = require('child_process');
4
- const { ConfigurationError, DomainError } = require('../../../domain/errors');
5
-
6
- class EvidenceRefreshRunner {
7
- constructor(repoRoot, options = {}) {
8
- this.repoRoot = repoRoot;
9
- this.tempDir = path.join(repoRoot, '.audit_tmp');
10
- this.refreshLockFile = path.join(this.tempDir, 'evidence-refresh.lock');
11
- this.refreshTimeoutMs = options.refreshTimeoutMs || 120000;
12
- this.refreshInFlight = false;
13
- this.updateScript = this.resolveUpdateEvidenceScript();
14
- }
15
-
16
- resolveUpdateEvidenceScript() {
17
- const candidates = [
18
- path.join(this.repoRoot, 'node_modules/@pumuki/ast-intelligence-hooks/bin/update-evidence.sh'),
19
- path.join(this.repoRoot, 'scripts/hooks-system/bin/update-evidence.sh'),
20
- path.join(this.repoRoot, 'bin/update-evidence.sh')
21
- ];
22
-
23
- for (const candidate of candidates) {
24
- if (fs.existsSync(candidate)) {
25
- return candidate;
26
- }
27
- }
28
-
29
- return null;
30
- }
31
-
32
- isPidRunning(pid) {
33
- if (!pid || !Number.isFinite(pid) || pid <= 0) return false;
34
- try {
35
- process.kill(pid, 0);
36
- return true;
37
- } catch {
38
- return false;
39
- }
40
- }
41
-
42
- acquireRefreshLock() {
43
- try {
44
- fs.mkdirSync(this.tempDir, { recursive: true });
45
- } catch (error) {
46
- console.warn('[EvidenceRefreshRunner] Failed to ensure temp dir:', error.message);
47
- }
48
-
49
- try {
50
- const fd = fs.openSync(this.refreshLockFile, 'wx');
51
- const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
52
- fs.writeFileSync(fd, payload, { encoding: 'utf8' });
53
- fs.closeSync(fd);
54
- return { acquired: true };
55
- } catch (error) {
56
- if (error && error.code !== 'EEXIST') {
57
- return { acquired: false, reason: 'error', error };
58
- }
59
-
60
- try {
61
- const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
62
- const data = raw ? JSON.parse(raw) : null;
63
- const lockPid = data && Number(data.pid);
64
- if (lockPid && this.isPidRunning(lockPid)) {
65
- return { acquired: false, reason: 'locked', pid: lockPid };
66
- }
67
- } catch (readError) {
68
- console.warn('[EvidenceRefreshRunner] Failed to read refresh lock file:', readError.message);
69
- }
70
-
71
- try {
72
- fs.unlinkSync(this.refreshLockFile);
73
- } catch (removeError) {
74
- console.warn('[EvidenceRefreshRunner] Failed to remove stale refresh lock:', removeError.message);
75
- }
76
-
77
- try {
78
- const fd = fs.openSync(this.refreshLockFile, 'wx');
79
- const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
80
- fs.writeFileSync(fd, payload, { encoding: 'utf8' });
81
- fs.closeSync(fd);
82
- return { acquired: true };
83
- } catch (retryError) {
84
- return { acquired: false, reason: 'locked', error: retryError };
85
- }
86
- }
87
- }
88
-
89
- releaseRefreshLock() {
90
- try {
91
- if (!fs.existsSync(this.refreshLockFile)) return;
92
- const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
93
- const data = raw ? JSON.parse(raw) : null;
94
- const lockPid = data && Number(data.pid);
95
- if (lockPid === process.pid) {
96
- fs.unlinkSync(this.refreshLockFile);
97
- }
98
- } catch (error) {
99
- console.warn('[EvidenceRefreshRunner] Failed to release refresh lock:', error.message);
100
- }
101
- }
102
-
103
- async refresh() {
104
- if (!this.updateScript) {
105
- throw new ConfigurationError('Update evidence script not found', 'updateScript');
106
- }
107
-
108
- if (this.refreshInFlight) {
109
- return '';
110
- }
111
-
112
- const lock = this.acquireRefreshLock();
113
- if (!lock.acquired) {
114
- return '';
115
- }
116
-
117
- this.refreshInFlight = true;
118
-
119
- return new Promise((resolve, reject) => {
120
- const child = spawn('bash', [this.updateScript, '--auto', '--refresh-only'], {
121
- cwd: this.repoRoot,
122
- stdio: ['pipe', 'pipe', 'pipe']
123
- });
124
-
125
- let output = '';
126
- child.stdout.on('data', (data) => {
127
- output += data.toString();
128
- });
129
-
130
- const timeoutId = setTimeout(() => {
131
- try {
132
- child.kill('SIGKILL');
133
- } catch (error) {
134
- console.warn('[EvidenceRefreshRunner] Failed to kill timed-out refresh process:', error.message);
135
- }
136
- }, this.refreshTimeoutMs);
137
-
138
- const finalize = () => {
139
- clearTimeout(timeoutId);
140
- this.refreshInFlight = false;
141
- this.releaseRefreshLock();
142
- };
143
-
144
- child.on('close', (code) => {
145
- finalize();
146
- if (code === 0) {
147
- resolve(output);
148
- } else {
149
- reject(new DomainError(`Evidence refresh failed with code ${code}`, 'EVIDENCE_REFRESH_FAILED'));
150
- }
151
- });
152
-
153
- child.on('error', (err) => {
154
- finalize();
155
- reject(err);
156
- });
157
- });
158
- }
159
- }
160
-
161
- module.exports = { EvidenceRefreshRunner };
@@ -1,59 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const crypto = require('crypto');
4
- const { execSync } = require('child_process');
5
-
6
- class StagedSwiftFilePreparer {
7
- constructor({ repoRoot, auditTmpDir }) {
8
- this.repoRoot = repoRoot;
9
- this.auditTmpDir = auditTmpDir || path.join(repoRoot, '.audit_tmp');
10
- }
11
-
12
- safeTempFilePath(displayPath) {
13
- const hash = crypto.createHash('sha1').update(String(displayPath)).digest('hex').slice(0, 10);
14
- const base = path.basename(displayPath, '.swift');
15
- const filename = `${base}.${hash}.staged.swift`;
16
- return path.join(this.auditTmpDir, filename);
17
- }
18
-
19
- readStagedFileContent(relPath) {
20
- try {
21
- return execSync(`git show :"${relPath}"`, {
22
- encoding: 'utf8',
23
- cwd: this.repoRoot,
24
- stdio: ['ignore', 'pipe', 'pipe']
25
- });
26
- } catch (error) {
27
- if (process.env.DEBUG) {
28
- console.debug(`[StagedSwiftFilePreparer] Failed to read staged file ${relPath}: ${error.message}`);
29
- }
30
- return null;
31
- }
32
- }
33
-
34
- prepare({ filePath, stagedRelPath, stagingOnly }) {
35
- let parsePath = filePath;
36
- let contentOverride = null;
37
-
38
- if (stagingOnly && stagedRelPath) {
39
- const stagedContent = this.readStagedFileContent(stagedRelPath);
40
- if (typeof stagedContent === 'string') {
41
- const tmpPath = this.safeTempFilePath(stagedRelPath);
42
- try {
43
- fs.mkdirSync(path.dirname(tmpPath), { recursive: true });
44
- fs.writeFileSync(tmpPath, stagedContent, 'utf8');
45
- parsePath = tmpPath;
46
- contentOverride = stagedContent;
47
- } catch (error) {
48
- if (process.env.DEBUG) {
49
- console.debug(`[StagedSwiftFilePreparer] Failed to write temp staged file ${tmpPath}: ${error.message}`);
50
- }
51
- }
52
- }
53
- }
54
-
55
- return { parsePath, contentOverride };
56
- }
57
- }
58
-
59
- module.exports = { StagedSwiftFilePreparer };
@@ -1,51 +0,0 @@
1
- const { execSync } = require('child_process');
2
-
3
- class SwiftAstRunner {
4
- constructor({ toolchain }) {
5
- this.toolchain = toolchain;
6
- }
7
-
8
- runSwiftSyntax(filePath, displayPath, findings) {
9
- if (!this.toolchain.swiftSyntaxPath) return;
10
-
11
- try {
12
- const result = execSync(`"${this.toolchain.swiftSyntaxPath}" "${filePath}"`, {
13
- encoding: 'utf8',
14
- timeout: 30000,
15
- stdio: ['pipe', 'pipe', 'pipe']
16
- });
17
- const violations = JSON.parse(result);
18
- violations.forEach(v => {
19
- findings.push({
20
- ruleId: v.ruleId,
21
- severity: String(v.severity || '').toUpperCase(),
22
- filePath: displayPath || filePath,
23
- line: v.line,
24
- column: v.column,
25
- message: v.message,
26
- });
27
- });
28
- } catch (error) {
29
- console.error('[SwiftAstRunner] Error parsing file with SwiftSyntax:', error.message);
30
- }
31
- }
32
-
33
- parseSourceKitten(filePath) {
34
- if (!this.toolchain.checkSourceKitten()) return null;
35
-
36
- try {
37
- const result = execSync(
38
- `${this.toolchain.sourceKittenPath} structure --file "${filePath}"`,
39
- { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
40
- );
41
- return JSON.parse(result);
42
- } catch (error) {
43
- if (process.env.DEBUG) {
44
- console.debug(`[SwiftAstRunner] SourceKitten parse failed for ${filePath}: ${error.message}`);
45
- }
46
- return null;
47
- }
48
- }
49
- }
50
-
51
- module.exports = { SwiftAstRunner };
@@ -1,57 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { execSync } = require('child_process');
4
- const env = require(path.join(__dirname, '../../../../config/env'));
5
-
6
- class SwiftToolchainResolver {
7
- constructor(options = {}) {
8
- this.sourceKittenPath = options.sourceKittenPath || '/opt/homebrew/bin/sourcekitten';
9
- this.projectRoot = options.projectRoot || this.detectRepoRoot();
10
- this.swiftSyntaxPath = this.findSwiftSyntaxBinary(this.projectRoot);
11
- }
12
-
13
- detectRepoRoot() {
14
- try {
15
- return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
16
- } catch {
17
- return process.cwd();
18
- }
19
- }
20
-
21
- resolveAuditTmpDir(repoRoot) {
22
- const configured = String(env.get('AUDIT_TMP', '') || '').trim();
23
- if (configured.length > 0) {
24
- return path.isAbsolute(configured) ? configured : path.join(repoRoot, configured);
25
- }
26
- return path.join(repoRoot, '.audit_tmp');
27
- }
28
-
29
- checkSourceKitten() {
30
- try {
31
- execSync(`${this.sourceKittenPath} version`, { stdio: 'pipe' });
32
- return true;
33
- } catch (error) {
34
- if (process.env.DEBUG) {
35
- console.debug(`[SwiftToolchainResolver] SourceKitten not available at ${this.sourceKittenPath}: ${error.message}`);
36
- }
37
- return false;
38
- }
39
- }
40
-
41
- findSwiftSyntaxBinary(projectRoot) {
42
- const possiblePaths = [
43
- path.join(__dirname, '../../../../../CustomLintRules/.build/debug/swift-ast-analyzer'),
44
- path.join(projectRoot, 'CustomLintRules/.build/debug/swift-ast-analyzer'),
45
- path.join(projectRoot, '.build/debug/swift-ast-analyzer'),
46
- ];
47
-
48
- for (const p of possiblePaths) {
49
- if (fs.existsSync(p)) {
50
- return p;
51
- }
52
- }
53
- return null;
54
- }
55
- }
56
-
57
- module.exports = { SwiftToolchainResolver };
@@ -1,32 +0,0 @@
1
- const fs = require('fs');
2
- const {
3
- resetCollections,
4
- collectAllNodes,
5
- analyzeCollectedNodes,
6
- } = require('../detectors/ios-ast-intelligent-strategies');
7
-
8
- class iOSAstAnalysisOrchestrator {
9
- run({ analyzer, ast, parsePath, displayPath, contentOverride }) {
10
- if (!ast || !analyzer) return;
11
-
12
- analyzer.currentFilePath = displayPath;
13
- resetCollections(analyzer);
14
-
15
- try {
16
- analyzer.fileContent = typeof contentOverride === 'string'
17
- ? contentOverride
18
- : fs.readFileSync(parsePath, 'utf8');
19
- } catch (error) {
20
- if (process.env.DEBUG) {
21
- console.debug(`[iOSAstAnalysisOrchestrator] Failed to read file content ${parsePath}: ${error.message}`);
22
- }
23
- analyzer.fileContent = '';
24
- }
25
-
26
- const substructure = ast['key.substructure'] || [];
27
- collectAllNodes(analyzer, substructure, null);
28
- analyzeCollectedNodes(analyzer, displayPath);
29
- }
30
- }
31
-
32
- module.exports = { iOSAstAnalysisOrchestrator };