pumuki-ast-hooks 5.5.53 → 5.5.54
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 +0 -9
- package/docs/MCP_SERVERS.md +16 -2
- package/docs/RELEASE_NOTES.md +34 -0
- package/package.json +1 -1
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +40 -0
- package/scripts/hooks-system/application/services/installation/McpConfigurator.js +9 -205
- package/scripts/hooks-system/application/services/installation/mcp/McpGlobalConfigCleaner.js +49 -0
- package/scripts/hooks-system/application/services/installation/mcp/McpProjectConfigWriter.js +59 -0
- package/scripts/hooks-system/application/services/installation/mcp/McpServerConfigBuilder.js +103 -0
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +13 -8
- package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +15 -8
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDChecks.js +385 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDRules.js +38 -408
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMChecks.js +408 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMRules.js +36 -442
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenExtractor.js +146 -0
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenParser.js +22 -190
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenRunner.js +62 -0
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +27 -9
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
const { exec } = require('child_process');
|
|
4
|
-
const util = require('util');
|
|
5
3
|
const fs = require('fs').promises;
|
|
6
4
|
const path = require('path');
|
|
7
5
|
const { DomainError } = require('../../../../domain/errors');
|
|
8
|
-
|
|
9
|
-
const
|
|
6
|
+
const SourceKittenRunner = require('./SourceKittenRunner');
|
|
7
|
+
const SourceKittenExtractor = require('./SourceKittenExtractor');
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
10
|
* SourceKittenParser
|
|
@@ -17,9 +15,11 @@ const execPromise = util.promisify(exec);
|
|
|
17
15
|
* @see https://github.com/jpsim/SourceKitten
|
|
18
16
|
*/
|
|
19
17
|
class SourceKittenParser {
|
|
20
|
-
constructor() {
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
18
|
+
constructor({ runner = null, timeout = 30000 } = {}) {
|
|
19
|
+
this.runner = runner || new SourceKittenRunner({ defaultTimeoutMs: timeout });
|
|
20
|
+
this.sourceKittenPath = this.runner.binaryPath;
|
|
21
|
+
this.timeout = timeout;
|
|
22
|
+
this.extractor = new SourceKittenExtractor();
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -28,10 +28,8 @@ class SourceKittenParser {
|
|
|
28
28
|
*/
|
|
29
29
|
async isInstalled() {
|
|
30
30
|
try {
|
|
31
|
-
const { stdout } = await
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
return stdout.includes('SourceKitten');
|
|
31
|
+
const { stdout } = await this.runner.version();
|
|
32
|
+
return Boolean(stdout) && stdout.includes('SourceKitten');
|
|
35
33
|
} catch (error) {
|
|
36
34
|
console.error('[SourceKitten] Not installed. Run: brew install sourcekitten');
|
|
37
35
|
return false;
|
|
@@ -49,16 +47,13 @@ class SourceKittenParser {
|
|
|
49
47
|
|
|
50
48
|
await fs.access(absolutePath);
|
|
51
49
|
|
|
52
|
-
const { stdout, stderr } = await
|
|
53
|
-
`${this.sourceKittenPath} structure --file "${absolutePath}"`,
|
|
54
|
-
{ timeout: this.timeout }
|
|
55
|
-
);
|
|
50
|
+
const { stdout, stderr } = await this.runner.structure(absolutePath);
|
|
56
51
|
|
|
57
52
|
if (stderr && stderr.includes('error')) {
|
|
58
53
|
throw new DomainError(`SourceKitten parse error: ${stderr}`, 'PARSE_ERROR');
|
|
59
54
|
}
|
|
60
55
|
|
|
61
|
-
const ast =
|
|
56
|
+
const ast = stdout;
|
|
62
57
|
|
|
63
58
|
return {
|
|
64
59
|
filePath: absolutePath,
|
|
@@ -89,18 +84,14 @@ class SourceKittenParser {
|
|
|
89
84
|
async parseProject(projectPath, moduleName, scheme) {
|
|
90
85
|
try {
|
|
91
86
|
const isWorkspace = projectPath.endsWith('.xcworkspace');
|
|
92
|
-
const projectFlag = isWorkspace ? '-workspace' : '-project';
|
|
93
87
|
|
|
94
|
-
const { stdout, stderr } = await
|
|
95
|
-
`${this.sourceKittenPath} doc ${projectFlag} "${projectPath}" -scheme "${scheme}"`,
|
|
96
|
-
{ timeout: 120000 }
|
|
97
|
-
);
|
|
88
|
+
const { stdout, stderr } = await this.runner.projectDoc(projectPath, scheme, isWorkspace);
|
|
98
89
|
|
|
99
90
|
if (stderr && stderr.includes('error')) {
|
|
100
91
|
throw new DomainError(`SourceKitten project parse error: ${stderr}`, 'PARSE_ERROR');
|
|
101
92
|
}
|
|
102
93
|
|
|
103
|
-
const projectAST =
|
|
94
|
+
const projectAST = stdout;
|
|
104
95
|
|
|
105
96
|
return {
|
|
106
97
|
projectPath,
|
|
@@ -132,12 +123,8 @@ class SourceKittenParser {
|
|
|
132
123
|
try {
|
|
133
124
|
const absolutePath = path.resolve(filePath);
|
|
134
125
|
|
|
135
|
-
const { stdout } = await
|
|
136
|
-
|
|
137
|
-
{ timeout: this.timeout }
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
const syntaxMap = JSON.parse(stdout);
|
|
126
|
+
const { stdout } = await this.runner.syntax(absolutePath);
|
|
127
|
+
const syntaxMap = stdout;
|
|
141
128
|
|
|
142
129
|
return {
|
|
143
130
|
filePath: absolutePath,
|
|
@@ -175,33 +162,7 @@ class SourceKittenParser {
|
|
|
175
162
|
* @returns {ClassNode[]}
|
|
176
163
|
*/
|
|
177
164
|
extractClasses(ast) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const traverse = (nodes) => {
|
|
181
|
-
if (!Array.isArray(nodes)) return;
|
|
182
|
-
|
|
183
|
-
nodes.forEach(node => {
|
|
184
|
-
const kind = node['key.kind'];
|
|
185
|
-
|
|
186
|
-
if (kind === 'source.lang.swift.decl.class') {
|
|
187
|
-
classes.push({
|
|
188
|
-
name: node['key.name'],
|
|
189
|
-
line: node['key.line'],
|
|
190
|
-
column: node['key.column'],
|
|
191
|
-
accessibility: node['key.accessibility'],
|
|
192
|
-
inheritedTypes: node['key.inheritedtypes'] || [],
|
|
193
|
-
substructure: node['key.substructure'] || [],
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (node['key.substructure']) {
|
|
198
|
-
traverse(node['key.substructure']);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
traverse(ast.substructure);
|
|
204
|
-
return classes;
|
|
165
|
+
return this.extractor.extractClasses(ast);
|
|
205
166
|
}
|
|
206
167
|
|
|
207
168
|
/**
|
|
@@ -210,39 +171,7 @@ class SourceKittenParser {
|
|
|
210
171
|
* @returns {FunctionNode[]}
|
|
211
172
|
*/
|
|
212
173
|
extractFunctions(ast) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const traverse = (nodes) => {
|
|
216
|
-
if (!Array.isArray(nodes)) return;
|
|
217
|
-
|
|
218
|
-
nodes.forEach(node => {
|
|
219
|
-
const kind = node['key.kind'];
|
|
220
|
-
|
|
221
|
-
if (kind === 'source.lang.swift.decl.function.method.instance' ||
|
|
222
|
-
kind === 'source.lang.swift.decl.function.method.class' ||
|
|
223
|
-
kind === 'source.lang.swift.decl.function.method.static' ||
|
|
224
|
-
kind === 'source.lang.swift.decl.function.free') {
|
|
225
|
-
|
|
226
|
-
functions.push({
|
|
227
|
-
name: node['key.name'],
|
|
228
|
-
line: node['key.line'],
|
|
229
|
-
column: node['key.column'],
|
|
230
|
-
kind,
|
|
231
|
-
accessibility: node['key.accessibility'],
|
|
232
|
-
typename: node['key.typename'],
|
|
233
|
-
length: node['key.length'],
|
|
234
|
-
bodyLength: node['key.bodylength'],
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (node['key.substructure']) {
|
|
239
|
-
traverse(node['key.substructure']);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
traverse(ast.substructure);
|
|
245
|
-
return functions;
|
|
174
|
+
return this.extractor.extractFunctions(ast);
|
|
246
175
|
}
|
|
247
176
|
|
|
248
177
|
/**
|
|
@@ -251,36 +180,7 @@ class SourceKittenParser {
|
|
|
251
180
|
* @returns {PropertyNode[]}
|
|
252
181
|
*/
|
|
253
182
|
extractProperties(ast) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const traverse = (nodes) => {
|
|
257
|
-
if (!Array.isArray(nodes)) return;
|
|
258
|
-
|
|
259
|
-
nodes.forEach(node => {
|
|
260
|
-
const kind = node['key.kind'];
|
|
261
|
-
|
|
262
|
-
if (kind === 'source.lang.swift.decl.var.instance' ||
|
|
263
|
-
kind === 'source.lang.swift.decl.var.class' ||
|
|
264
|
-
kind === 'source.lang.swift.decl.var.static') {
|
|
265
|
-
|
|
266
|
-
properties.push({
|
|
267
|
-
name: node['key.name'],
|
|
268
|
-
line: node['key.line'],
|
|
269
|
-
column: node['key.column'],
|
|
270
|
-
kind,
|
|
271
|
-
typename: node['key.typename'],
|
|
272
|
-
accessibility: node['key.accessibility'],
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (node['key.substructure']) {
|
|
277
|
-
traverse(node['key.substructure']);
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
traverse(ast.substructure);
|
|
283
|
-
return properties;
|
|
183
|
+
return this.extractor.extractProperties(ast);
|
|
284
184
|
}
|
|
285
185
|
|
|
286
186
|
/**
|
|
@@ -289,33 +189,7 @@ class SourceKittenParser {
|
|
|
289
189
|
* @returns {ProtocolNode[]}
|
|
290
190
|
*/
|
|
291
191
|
extractProtocols(ast) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const traverse = (nodes) => {
|
|
295
|
-
if (!Array.isArray(nodes)) return;
|
|
296
|
-
|
|
297
|
-
nodes.forEach(node => {
|
|
298
|
-
const kind = node['key.kind'];
|
|
299
|
-
|
|
300
|
-
if (kind === 'source.lang.swift.decl.protocol') {
|
|
301
|
-
protocols.push({
|
|
302
|
-
name: node['key.name'],
|
|
303
|
-
line: node['key.line'],
|
|
304
|
-
column: node['key.column'],
|
|
305
|
-
accessibility: node['key.accessibility'],
|
|
306
|
-
inheritedTypes: node['key.inheritedtypes'] || [],
|
|
307
|
-
substructure: node['key.substructure'] || [],
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (node['key.substructure']) {
|
|
312
|
-
traverse(node['key.substructure']);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
traverse(ast.substructure);
|
|
318
|
-
return protocols;
|
|
192
|
+
return this.extractor.extractProtocols(ast);
|
|
319
193
|
}
|
|
320
194
|
|
|
321
195
|
/**
|
|
@@ -324,19 +198,7 @@ class SourceKittenParser {
|
|
|
324
198
|
* @returns {boolean}
|
|
325
199
|
*/
|
|
326
200
|
usesSwiftUI(ast) {
|
|
327
|
-
|
|
328
|
-
if (!Array.isArray(nodes)) return false;
|
|
329
|
-
|
|
330
|
-
return nodes.some(node => {
|
|
331
|
-
const inheritedTypes = node['key.inheritedtypes'] || [];
|
|
332
|
-
if (inheritedTypes.some(t => t['key.name'] === 'View')) {
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
return hasViewProtocol(node['key.substructure'] || []);
|
|
336
|
-
});
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
return hasViewProtocol(ast.substructure);
|
|
201
|
+
return this.extractor.usesSwiftUI(ast);
|
|
340
202
|
}
|
|
341
203
|
|
|
342
204
|
/**
|
|
@@ -345,22 +207,7 @@ class SourceKittenParser {
|
|
|
345
207
|
* @returns {boolean}
|
|
346
208
|
*/
|
|
347
209
|
usesUIKit(ast) {
|
|
348
|
-
|
|
349
|
-
if (!Array.isArray(nodes)) return false;
|
|
350
|
-
|
|
351
|
-
return nodes.some(node => {
|
|
352
|
-
const inheritedTypes = node['key.inheritedtypes'] || [];
|
|
353
|
-
if (inheritedTypes.some(t =>
|
|
354
|
-
t['key.name'] === 'UIViewController' ||
|
|
355
|
-
t['key.name'] === 'UIView'
|
|
356
|
-
)) {
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
|
-
return hasUIKitBase(node['key.substructure'] || []);
|
|
360
|
-
});
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
return hasUIKitBase(ast.substructure);
|
|
210
|
+
return this.extractor.usesUIKit(ast);
|
|
364
211
|
}
|
|
365
212
|
|
|
366
213
|
/**
|
|
@@ -370,22 +217,7 @@ class SourceKittenParser {
|
|
|
370
217
|
* @returns {ForceUnwrap[]}
|
|
371
218
|
*/
|
|
372
219
|
detectForceUnwraps(syntaxMap, fileContent) {
|
|
373
|
-
|
|
374
|
-
const lines = fileContent.split('\n');
|
|
375
|
-
|
|
376
|
-
lines.forEach((line, index) => {
|
|
377
|
-
const matches = [...line.matchAll(/(\w+)\s*!/g)];
|
|
378
|
-
matches.forEach(match => {
|
|
379
|
-
forceUnwraps.push({
|
|
380
|
-
line: index + 1,
|
|
381
|
-
column: match.index + 1,
|
|
382
|
-
variable: match[1],
|
|
383
|
-
context: line.trim(),
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
return forceUnwraps;
|
|
220
|
+
return this.extractor.detectForceUnwraps(syntaxMap, fileContent);
|
|
389
221
|
}
|
|
390
222
|
}
|
|
391
223
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const util = require('util');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const execPromise = util.promisify(exec);
|
|
6
|
+
|
|
7
|
+
class SourceKittenRunner {
|
|
8
|
+
constructor({
|
|
9
|
+
binaryPath = '/opt/homebrew/bin/sourcekitten',
|
|
10
|
+
defaultTimeoutMs = 30000,
|
|
11
|
+
logger = console
|
|
12
|
+
} = {}) {
|
|
13
|
+
this.binaryPath = binaryPath;
|
|
14
|
+
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async version() {
|
|
19
|
+
return this.execRaw(`${this.binaryPath} version`, 5000);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async structure(filePath) {
|
|
23
|
+
const cmd = `${this.binaryPath} structure --file "${path.resolve(filePath)}"`;
|
|
24
|
+
return this.execJson(cmd, this.defaultTimeoutMs);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async syntax(filePath) {
|
|
28
|
+
const cmd = `${this.binaryPath} syntax --file "${path.resolve(filePath)}"`;
|
|
29
|
+
return this.execJson(cmd, this.defaultTimeoutMs);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async projectDoc(projectPath, scheme, isWorkspace = false) {
|
|
33
|
+
const projectFlag = isWorkspace ? '-workspace' : '-project';
|
|
34
|
+
const cmd = `${this.binaryPath} doc ${projectFlag} "${projectPath}" -scheme "${scheme}"`;
|
|
35
|
+
return this.execJson(cmd, 120000);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async execJson(command, timeoutMs) {
|
|
39
|
+
const { stdout, stderr } = await this.execRaw(command, timeoutMs);
|
|
40
|
+
return { stdout: this.safeJson(stdout), stderr };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async execRaw(command, timeoutMs) {
|
|
44
|
+
try {
|
|
45
|
+
return await execPromise(command, { timeout: timeoutMs });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const msg = error && error.message ? error.message : String(error);
|
|
48
|
+
this.logger?.debug?.('[SourceKittenRunner] exec error', { command, error: msg });
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
safeJson(text) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(text);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = SourceKittenRunner;
|
|
@@ -967,7 +967,6 @@ async function aiGateCheck() {
|
|
|
967
967
|
}
|
|
968
968
|
}
|
|
969
969
|
|
|
970
|
-
let mandatoryRules = null;
|
|
971
970
|
let detectedPlatforms = [];
|
|
972
971
|
try {
|
|
973
972
|
const orchestrator = getCompositionRoot().getOrchestrator();
|
|
@@ -975,10 +974,31 @@ async function aiGateCheck() {
|
|
|
975
974
|
if (contextDecision && contextDecision.platforms) {
|
|
976
975
|
detectedPlatforms = contextDecision.platforms.map(p => p.platform || p);
|
|
977
976
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
.
|
|
981
|
-
|
|
977
|
+
} catch (err) {
|
|
978
|
+
if (process.env.DEBUG) {
|
|
979
|
+
process.stderr.write(`[MCP] analyzeContext failed, using fallback: ${err.message}\n`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (detectedPlatforms.length === 0) {
|
|
984
|
+
try {
|
|
985
|
+
const PlatformDetectionService = require('../../application/services/PlatformDetectionService');
|
|
986
|
+
const detector = new PlatformDetectionService();
|
|
987
|
+
detectedPlatforms = await detector.detectPlatforms(REPO_ROOT);
|
|
988
|
+
} catch (err) {
|
|
989
|
+
if (process.env.DEBUG) {
|
|
990
|
+
process.stderr.write(`[MCP] PlatformDetectionService failed: ${err.message}\n`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const fallbackPlatforms = ['backend', 'frontend', 'ios', 'android'];
|
|
996
|
+
const platformsForRules = (detectedPlatforms.length > 0 ? detectedPlatforms : fallbackPlatforms)
|
|
997
|
+
.filter(Boolean);
|
|
998
|
+
const normalizedPlatforms = Array.from(new Set(platformsForRules));
|
|
999
|
+
|
|
1000
|
+
let mandatoryRules = null;
|
|
1001
|
+
try {
|
|
982
1002
|
const rulesData = await loadPlatformRules(normalizedPlatforms);
|
|
983
1003
|
const rulesSample = rulesData.criticalRules.slice(0, 5).map(r => r.rule || r);
|
|
984
1004
|
const rulesCount = rulesData.criticalRules.length;
|
|
@@ -987,16 +1007,14 @@ async function aiGateCheck() {
|
|
|
987
1007
|
criticalRules: rulesData.criticalRules,
|
|
988
1008
|
rulesLoaded: Object.keys(rulesData.rules),
|
|
989
1009
|
totalRulesCount: rulesCount,
|
|
990
|
-
rulesSample
|
|
1010
|
+
rulesSample,
|
|
991
1011
|
proofOfRead: `✅ VERIFIED: ${rulesCount} critical rules loaded from ${Object.keys(rulesData.rules).join(', ')}`
|
|
992
1012
|
};
|
|
993
1013
|
} catch (error) {
|
|
994
1014
|
if (process.env.DEBUG) {
|
|
995
|
-
process.stderr.write(`[MCP]
|
|
1015
|
+
process.stderr.write(`[MCP] loadPlatformRules failed: ${error.message}\n`);
|
|
996
1016
|
}
|
|
997
1017
|
|
|
998
|
-
const fallbackPlatforms = ['backend', 'frontend', 'ios', 'android'];
|
|
999
|
-
const normalizedPlatforms = Array.from(new Set((detectedPlatforms.length > 0 ? detectedPlatforms : fallbackPlatforms).filter(Boolean)));
|
|
1000
1018
|
mandatoryRules = {
|
|
1001
1019
|
platforms: normalizedPlatforms,
|
|
1002
1020
|
criticalRules: [],
|