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.
@@ -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 execPromise = util.promisify(exec);
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.sourceKittenPath = '/opt/homebrew/bin/sourcekitten';
22
- this.timeout = 30000;
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 execPromise(`${this.sourceKittenPath} version`, {
32
- timeout: 5000,
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 execPromise(
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 = JSON.parse(stdout);
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 execPromise(
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 = JSON.parse(stdout);
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 execPromise(
136
- `${this.sourceKittenPath} syntax --file "${absolutePath}"`,
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
- const classes = [];
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
- const functions = [];
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
- const properties = [];
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
- const protocols = [];
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
- const hasViewProtocol = (nodes) => {
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
- const hasUIKitBase = (nodes) => {
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
- const forceUnwraps = [];
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
- const fallbackPlatforms = ['backend', 'frontend', 'ios', 'android'];
979
- const platformsForRules = (detectedPlatforms.length > 0 ? detectedPlatforms : fallbackPlatforms)
980
- .filter(Boolean);
981
- const normalizedPlatforms = Array.from(new Set(platformsForRules));
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: 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] Failed to load mandatory rules: ${error.message}\n`);
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: [],