pumuki-ast-hooks 5.5.50 → 5.5.51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki-ast-hooks",
3
- "version": "5.5.50",
3
+ "version": "5.5.51",
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": {
@@ -110,3 +110,51 @@
110
110
  {"timestamp":1767772971939,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
111
111
  {"timestamp":1767772971939,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
112
112
  {"timestamp":1767772971939,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
113
+ {"timestamp":1767775802167,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
114
+ {"timestamp":1767775802167,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
115
+ {"timestamp":1767775802167,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
116
+ {"timestamp":1767775802167,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
117
+ {"timestamp":1767775833943,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
118
+ {"timestamp":1767775833943,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
119
+ {"timestamp":1767775833943,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
120
+ {"timestamp":1767775833943,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
121
+ {"timestamp":1767776135844,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
122
+ {"timestamp":1767776135844,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
123
+ {"timestamp":1767776135844,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
124
+ {"timestamp":1767776135844,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
125
+ {"timestamp":1767776384649,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
126
+ {"timestamp":1767776384649,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
127
+ {"timestamp":1767776384649,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
128
+ {"timestamp":1767776384649,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
129
+ {"timestamp":1767777653343,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
130
+ {"timestamp":1767777653343,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
131
+ {"timestamp":1767777653343,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
132
+ {"timestamp":1767777653344,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
133
+ {"timestamp":1767778142566,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
134
+ {"timestamp":1767778142567,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
135
+ {"timestamp":1767778142567,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
136
+ {"timestamp":1767778142567,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
137
+ {"timestamp":1767778800954,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
138
+ {"timestamp":1767778800955,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
139
+ {"timestamp":1767778800955,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
140
+ {"timestamp":1767778800955,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
141
+ {"timestamp":1767779528552,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
142
+ {"timestamp":1767779528552,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
143
+ {"timestamp":1767779528552,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
144
+ {"timestamp":1767779528552,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
145
+ {"timestamp":1767779903813,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
146
+ {"timestamp":1767779903813,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
147
+ {"timestamp":1767779903813,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
148
+ {"timestamp":1767779903813,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
149
+ {"timestamp":1767780497468,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
150
+ {"timestamp":1767780497468,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
151
+ {"timestamp":1767780497468,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
152
+ {"timestamp":1767780497468,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
153
+ {"timestamp":1767780655724,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
154
+ {"timestamp":1767780655725,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
155
+ {"timestamp":1767780655725,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
156
+ {"timestamp":1767780655725,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
157
+ {"timestamp":1767782291627,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
158
+ {"timestamp":1767782291627,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
159
+ {"timestamp":1767782291627,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
160
+ {"timestamp":1767782291627,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
@@ -130,33 +130,99 @@ class McpConfigurator {
130
130
  }
131
131
  };
132
132
 
133
- const globalConfigPath = this.getGlobalWindsurfConfigPath();
134
- const globalConfigDir = path.dirname(globalConfigPath);
135
- if (!fs.existsSync(globalConfigDir)) {
136
- fs.mkdirSync(globalConfigDir, { recursive: true });
137
- }
133
+ this.configureProjectScoped(mcpConfig, serverId);
134
+ this.cleanupGlobalConfig(serverId);
135
+ }
136
+
137
+ configureProjectScoped(mcpConfig, serverId) {
138
+ const windsurfProjectDir = path.join(this.targetRoot, '.windsurf');
139
+ const windsurfProjectPath = path.join(windsurfProjectDir, 'mcp.json');
138
140
 
139
141
  try {
140
- if (!fs.existsSync(globalConfigPath)) {
141
- fs.writeFileSync(globalConfigPath, JSON.stringify(mcpConfig, null, 2));
142
- this.logSuccess(`Configured global Windsurf MCP at ${globalConfigPath}`);
143
- if (this.logger) this.logger.info('MCP_GLOBAL_CONFIGURED', { path: globalConfigPath });
144
- } else {
145
- const existing = JSON.parse(fs.readFileSync(globalConfigPath, 'utf8'));
142
+ if (!fs.existsSync(windsurfProjectDir)) {
143
+ fs.mkdirSync(windsurfProjectDir, { recursive: true });
144
+ }
145
+
146
+ let finalConfig = mcpConfig;
147
+ if (fs.existsSync(windsurfProjectPath)) {
148
+ const existing = JSON.parse(fs.readFileSync(windsurfProjectPath, 'utf8'));
146
149
  if (!existing.mcpServers) existing.mcpServers = {};
150
+ Object.keys(existing.mcpServers).forEach(id => {
151
+ if (id.startsWith('ast-intelligence-automation-') && id !== serverId) {
152
+ delete existing.mcpServers[id];
153
+ }
154
+ });
155
+ existing.mcpServers[serverId] = mcpConfig.mcpServers[serverId];
156
+ finalConfig = existing;
157
+ }
158
+
159
+ fs.writeFileSync(windsurfProjectPath, JSON.stringify(finalConfig, null, 2));
160
+ this.logSuccess(`Configured project-scoped Windsurf MCP at ${windsurfProjectPath}`);
161
+ if (this.logger) this.logger.info('MCP_PROJECT_CONFIGURED', { path: windsurfProjectPath, serverId });
162
+ } catch (error) {
163
+ this.logWarning(`Failed to configure project-scoped MCP: ${error.message}`);
164
+ if (this.logger) this.logger.warn('MCP_PROJECT_CONFIGURE_FAILED', { error: error.message });
165
+ }
147
166
 
148
- // Prevent duplicate MCP servers for the same repoRoot by disabling legacy entries.
149
- this.disableDuplicateServersForRepo(existing, serverId);
167
+ const cursorProjectDir = path.join(this.targetRoot, '.cursor');
168
+ const cursorProjectPath = path.join(cursorProjectDir, 'mcp.json');
150
169
 
170
+ try {
171
+ if (!fs.existsSync(cursorProjectDir)) {
172
+ fs.mkdirSync(cursorProjectDir, { recursive: true });
173
+ }
174
+
175
+ let finalConfig = mcpConfig;
176
+ if (fs.existsSync(cursorProjectPath)) {
177
+ const existing = JSON.parse(fs.readFileSync(cursorProjectPath, 'utf8'));
178
+ if (!existing.mcpServers) existing.mcpServers = {};
179
+ Object.keys(existing.mcpServers).forEach(id => {
180
+ if (id.startsWith('ast-intelligence-automation-') && id !== serverId) {
181
+ delete existing.mcpServers[id];
182
+ }
183
+ });
151
184
  existing.mcpServers[serverId] = mcpConfig.mcpServers[serverId];
185
+ finalConfig = existing;
186
+ }
187
+
188
+ fs.writeFileSync(cursorProjectPath, JSON.stringify(finalConfig, null, 2));
189
+ this.logSuccess(`Configured project-scoped Cursor MCP at ${cursorProjectPath}`);
190
+ if (this.logger) this.logger.info('MCP_CURSOR_CONFIGURED', { path: cursorProjectPath, serverId });
191
+ } catch (error) {
192
+ this.logWarning(`Failed to configure Cursor MCP: ${error.message}`);
193
+ if (this.logger) this.logger.warn('MCP_CURSOR_CONFIGURE_FAILED', { error: error.message });
194
+ }
195
+ }
196
+
197
+ cleanupGlobalConfig(currentServerId) {
198
+ const globalConfigPath = this.getGlobalWindsurfConfigPath();
152
199
 
200
+ try {
201
+ if (!fs.existsSync(globalConfigPath)) return;
202
+
203
+ const existing = JSON.parse(fs.readFileSync(globalConfigPath, 'utf8'));
204
+ if (!existing.mcpServers) return;
205
+
206
+ let modified = false;
207
+ Object.keys(existing.mcpServers).forEach(id => {
208
+ const server = existing.mcpServers[id];
209
+ if (!server || !server.env) return;
210
+
211
+ if (server.env.REPO_ROOT === this.targetRoot) {
212
+ delete existing.mcpServers[id];
213
+ modified = true;
214
+ this.logInfo(`Removed ${id} from global config (now project-scoped)`);
215
+ }
216
+ });
217
+
218
+ if (modified) {
153
219
  fs.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2));
154
- this.logSuccess(`Updated global Windsurf MCP at ${globalConfigPath}`);
155
- if (this.logger) this.logger.info('MCP_GLOBAL_UPDATED', { path: globalConfigPath });
220
+ if (this.logger) this.logger.info('MCP_GLOBAL_CLEANUP', { removedForRepo: this.targetRoot });
221
+ }
222
+ } catch (error) {
223
+ if (process.env.DEBUG) {
224
+ process.stderr.write(`[MCP] cleanupGlobalConfig failed: ${error.message}\n`);
156
225
  }
157
- } catch (mergeError) {
158
- this.logWarning(`${globalConfigPath} exists but couldn't be merged, skipping`);
159
- if (this.logger) this.logger.warn('MCP_GLOBAL_MERGE_FAILED', { error: mergeError.message });
160
226
  }
161
227
  }
162
228
 
@@ -1,7 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { execSync } = require('child_process');
4
- const { ConfigurationError, DomainError } = require('../../../domain/errors');
3
+ const { EvidenceRefreshRunner } = require('./EvidenceRefreshRunner');
5
4
  const AuditLogger = require('../logging/AuditLogger');
6
5
 
7
6
  class EvidenceMonitor {
@@ -14,98 +13,9 @@ class EvidenceMonitor {
14
13
  this.lastStaleNotification = 0;
15
14
  this.pollTimer = null;
16
15
  this.evidencePath = path.join(repoRoot, '.AI_EVIDENCE.json');
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;
16
+ this.refreshRunner = new EvidenceRefreshRunner(repoRoot, {
17
+ refreshTimeoutMs: options.refreshTimeoutMs
18
+ });
109
19
  }
110
20
 
111
21
  isStale() {
@@ -123,58 +33,7 @@ class EvidenceMonitor {
123
33
  }
124
34
 
125
35
  async 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
- });
36
+ return this.refreshRunner.refresh();
178
37
  }
179
38
 
180
39
  startPolling(onStale, onRefreshed) {
@@ -0,0 +1,161 @@
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 };
@@ -0,0 +1,59 @@
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 };
@@ -0,0 +1,51 @@
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 };