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 +9 -0
- package/package.json +1 -1
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +8 -0
- package/scripts/hooks-system/application/services/AutonomousOrchestrator.js +37 -0
- package/scripts/hooks-system/application/services/monitoring/EvidenceMonitor.js +146 -5
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +137 -27
- package/scripts/hooks-system/application/services/installation/HookAssetsInstaller.js +0 -0
- package/scripts/hooks-system/application/services/monitoring/EvidenceRefreshRunner.js +0 -161
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/StagedSwiftFilePreparer.js +0 -59
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftAstRunner.js +0 -51
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftToolchainResolver.js +0 -57
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSAstAnalysisOrchestrator.js +0 -32
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.
|
|
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 {
|
|
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.
|
|
17
|
-
|
|
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
|
-
|
|
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.
|
|
20
|
-
this.
|
|
21
|
-
this.
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
155
|
+
this.analyzeWithSwiftSyntax(parsePath, displayPath);
|
|
57
156
|
}
|
|
58
157
|
|
|
59
|
-
const ast = this.
|
|
158
|
+
const ast = this.parseFile(parsePath);
|
|
60
159
|
if (!ast) return;
|
|
61
160
|
|
|
62
|
-
this.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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) {
|
|
File without changes
|
|
@@ -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 };
|