pumuki-ast-hooks 5.5.47 → 5.5.49

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.
Files changed (31) hide show
  1. package/docs/CODE_STANDARDS.md +5 -0
  2. package/docs/VIOLATIONS_RESOLUTION_PLAN.md +5 -140
  3. package/package.json +1 -1
  4. package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +96 -0
  5. package/scripts/hooks-system/application/services/installation/VSCodeTaskConfigurator.js +3 -1
  6. package/scripts/hooks-system/bin/gitflow-cycle.js +0 -0
  7. package/scripts/hooks-system/config/project.config.json +1 -1
  8. package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidSOLIDAnalyzer.js +11 -255
  9. package/scripts/hooks-system/infrastructure/ast/android/detectors/android-solid-detectors.js +227 -0
  10. package/scripts/hooks-system/infrastructure/ast/ast-core.js +12 -3
  11. package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +36 -13
  12. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +10 -83
  13. package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +83 -0
  14. package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +17 -2
  15. package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/FrontendArchitectureDetector.js +12 -142
  16. package/scripts/hooks-system/infrastructure/ast/frontend/detectors/frontend-architecture-strategies.js +126 -0
  17. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +30 -783
  18. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureDetector.js +21 -224
  19. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureRules.js +18 -605
  20. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSModernPracticesRules.js +4 -1
  21. package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +4 -1
  22. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-architecture-rules-strategies.js +595 -0
  23. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-architecture-strategies.js +192 -0
  24. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +789 -0
  25. package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-god-class-detector.js +79 -0
  26. package/scripts/hooks-system/infrastructure/ast/ios/native-bridge.js +4 -1
  27. package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +24 -13
  28. package/skills/android-guidelines/SKILL.md +1 -0
  29. package/skills/backend-guidelines/SKILL.md +1 -0
  30. package/skills/frontend-guidelines/SKILL.md +1 -0
  31. package/skills/ios-guidelines/SKILL.md +1 -0
@@ -0,0 +1,227 @@
1
+ const path = require('path');
2
+ const { SyntaxKind } = require(path.join(__dirname, '../../ast-core'));
3
+
4
+ function analyzeOCP(sf, findings, pushFinding) {
5
+ const filePath = sf.getFilePath();
6
+ const fileName = filePath.split('/').pop() || 'unknown';
7
+
8
+ const functions = sf.getFunctions();
9
+ const arrowFunctions = sf.getVariableDeclarations().filter(vd => {
10
+ const init = vd.getInitializer();
11
+ return init && init.getKind() === SyntaxKind.ArrowFunction;
12
+ });
13
+
14
+ const classes = sf.getClasses();
15
+
16
+ const allNodes = [
17
+ ...functions.map(f => ({ type: 'function', node: f, name: f.getName() || 'anonymous' })),
18
+ ...arrowFunctions.map(af => ({ type: 'arrow', node: af, name: af.getName() || 'anonymous' })),
19
+ ...classes.map(c => ({ type: 'class', node: c, name: c.getName() || 'AnonymousClass' })),
20
+ ];
21
+
22
+ allNodes.forEach(({ node, name }) => {
23
+ analyzeNodeForOCP(node, name, fileName, sf, pushFinding);
24
+ });
25
+ }
26
+
27
+ function analyzeNodeForOCP(node, nodeName, fileName, sf, pushFinding) {
28
+ let body;
29
+
30
+ if (node.getKind() === SyntaxKind.FunctionDeclaration ||
31
+ node.getKind() === SyntaxKind.FunctionExpression) {
32
+ body = node.getBody();
33
+ } else if (node.getKind() === SyntaxKind.VariableDeclaration) {
34
+ const init = node.getInitializer();
35
+ if (init && init.getKind() === SyntaxKind.ArrowFunction) {
36
+ body = init.getBody();
37
+ }
38
+ } else if (node.getKind() === SyntaxKind.ClassDeclaration) {
39
+ const methods = node.getMethods();
40
+ methods.forEach(method => {
41
+ const methodBody = method.getBody();
42
+ if (methodBody) {
43
+ analyzeBodyForOCP(methodBody, `${nodeName}.${method.getName()}`, fileName, sf, pushFinding);
44
+ }
45
+ });
46
+ return;
47
+ } else {
48
+ return;
49
+ }
50
+
51
+ if (body) {
52
+ analyzeBodyForOCP(body, nodeName, fileName, sf, pushFinding);
53
+ }
54
+ }
55
+
56
+ function analyzeBodyForOCP(body, name, fileName, sf, pushFinding) {
57
+ const switchStatements = body.getDescendantsOfKind(SyntaxKind.SwitchStatement);
58
+ const stringLiterals = body.getDescendantsOfKind(SyntaxKind.StringLiteral);
59
+
60
+ let hasTypeCodes = false;
61
+ const suspectStrings = ['type', 'kind', 'state', 'status', 'mode', 'screen', 'variant'];
62
+ for (const s of stringLiterals) {
63
+ const value = (s.getLiteralText && s.getLiteralText()) || s.getText() || '';
64
+ if (suspectStrings.some(x => value.toLowerCase().includes(x))) {
65
+ hasTypeCodes = true;
66
+ break;
67
+ }
68
+ }
69
+
70
+ if (switchStatements.length >= 3 && hasTypeCodes) {
71
+ const message = `OCP VIOLATION in ${fileName}::${name}: ${switchStatements.length} switch statements with type codes - consider polymorphism or sealed classes`;
72
+ pushFinding('solid.ocp.switch_typecodes', 'critical', sf, body, message, []);
73
+ return;
74
+ }
75
+
76
+ const ifStatements = body.getDescendantsOfKind(SyntaxKind.IfStatement);
77
+ const hasManyIfs = ifStatements.length >= 6;
78
+ const hasElseIfChains = ifStatements.some(ifStmt => {
79
+ const elseStmt = ifStmt.getElseStatement();
80
+ return elseStmt && elseStmt.getKind() === SyntaxKind.IfStatement;
81
+ });
82
+
83
+ if (hasManyIfs && hasElseIfChains) {
84
+ const message = `OCP WARNING in ${fileName}::${name}: multiple if/else-if chains - consider strategy/visitor`;
85
+ pushFinding('solid.ocp.if_chains', 'high', sf, body, message, []);
86
+ }
87
+ }
88
+
89
+ function analyzeDIP(sf, findings, pushFinding) {
90
+ const filePath = sf.getFilePath();
91
+ const fileName = filePath.split('/').pop() || 'unknown';
92
+
93
+ const isDomain = /\/domain\//i.test(filePath);
94
+
95
+ if (isDomain) {
96
+ const imports = sf.getImportDeclarations();
97
+
98
+ imports.forEach(imp => {
99
+ const importPath = imp.getModuleSpecifierValue();
100
+
101
+ if (/\/infrastructure\//i.test(importPath) ||
102
+ /androidx|kotlinx|retrofit|room|hilt/i.test(importPath)) {
103
+ const message = `DIP VIOLATION in ${fileName}: Domain layer importing from Infrastructure/Framework: ${importPath} - Domain should depend only on abstractions`;
104
+ pushFinding('solid.dip.domain_depends_infrastructure', 'critical', sf, imp, message, findings);
105
+ }
106
+ });
107
+ }
108
+
109
+ const isPresentation = /\/presentation\//i.test(filePath);
110
+
111
+ if (isPresentation) {
112
+ const imports = sf.getImportDeclarations();
113
+
114
+ imports.forEach(imp => {
115
+ const importPath = imp.getModuleSpecifierValue();
116
+
117
+ if (/\/infrastructure\//i.test(importPath) &&
118
+ !/\/infrastructure\/repositories\/|\/infrastructure\/config\//i.test(importPath)) {
119
+ const message = `DIP VIOLATION in ${fileName}: Presentation layer importing from Infrastructure: ${importPath} - use repository interfaces or abstractions`;
120
+ pushFinding('solid.dip.presentation_infrastructure', 'critical', sf, imp, message, findings);
121
+ }
122
+ });
123
+ }
124
+
125
+ const classes = sf.getClasses();
126
+ classes.forEach(cls => {
127
+ const className = cls.getName() || 'AnonymousClass';
128
+
129
+ if (/ViewModel|UseCase/i.test(className)) {
130
+ const imports = sf.getImportDeclarations();
131
+
132
+ imports.forEach(imp => {
133
+ const importPath = imp.getModuleSpecifierValue();
134
+
135
+ if (/Repository|Service|Client/i.test(importPath) &&
136
+ !/interface|protocol|Repository.*Protocol/i.test(importPath)) {
137
+ const message = `DIP VIOLATION in ${fileName}::${className}: depends on concrete implementation '${importPath}' - inject interface/abstraction`;
138
+ pushFinding('solid.dip.concrete_dependency', 'critical', sf, imp, message, findings);
139
+ }
140
+ });
141
+ }
142
+ });
143
+ }
144
+
145
+ function analyzeSRP(sf, findings, pushFinding) {
146
+ const filePath = sf.getFilePath();
147
+ const fileName = filePath.split('/').pop() || 'unknown';
148
+
149
+ const functions = sf.getFunctions();
150
+ const arrowFunctions = sf.getVariableDeclarations().filter(vd => {
151
+ const init = vd.getInitializer();
152
+ return init && init.getKind() === SyntaxKind.ArrowFunction;
153
+ });
154
+
155
+ [...functions, ...arrowFunctions].forEach(func => {
156
+ const funcName = func.getName?.() || 'anonymous';
157
+ const body = func.getBody?.() || func.getInitializer()?.getBody();
158
+
159
+ if (!body) return;
160
+
161
+ const statements = body.getStatements();
162
+ const ifStatements = body.getDescendantsOfKind(SyntaxKind.IfStatement);
163
+ const switchStatements = body.getDescendantsOfKind(SyntaxKind.SwitchStatement);
164
+ const loops = body.getDescendantsOfKind(SyntaxKind.ForStatement)
165
+ .concat(body.getDescendantsOfKind(SyntaxKind.ForInStatement))
166
+ .concat(body.getDescendantsOfKind(SyntaxKind.WhileStatement));
167
+
168
+ if (statements.length > 30 || ifStatements.length > 10 || switchStatements.length > 2 || loops.length > 5) {
169
+ const message = `SRP VIOLATION in ${fileName}::${funcName}: high complexity (${statements.length} statements, ${ifStatements.length} ifs, ${switchStatements.length} switches) - extract responsibilities`;
170
+ pushFinding('solid.srp.high_complexity', 'critical', sf, func, message, findings);
171
+ }
172
+ });
173
+
174
+ const classes = sf.getClasses();
175
+ classes.forEach(cls => {
176
+ const className = cls.getName() || 'AnonymousClass';
177
+ const methods = cls.getMethods();
178
+
179
+ if (methods.length > 20) {
180
+ const message = `SRP VIOLATION in ${fileName}::${className}: God class with ${methods.length} methods - split into focused classes`;
181
+ pushFinding('solid.srp.god_class', 'critical', sf, cls, message, findings);
182
+ }
183
+ });
184
+ }
185
+
186
+ function detectMethodConcern(name) {
187
+ if (!name) return 'unknown';
188
+ const lower = name.toLowerCase();
189
+ if (/fetch|get|load|retrieve|query/.test(lower)) return 'data';
190
+ if (/update|set|save|persist|write/.test(lower)) return 'mutation';
191
+ if (/validate|check|verify|ensure/.test(lower)) return 'validation';
192
+ if (/render|draw|display|view/.test(lower)) return 'ui';
193
+ if (/handle|process|execute|run|perform/.test(lower)) return 'logic';
194
+ return 'unknown';
195
+ }
196
+
197
+ function analyzeISP(sf, findings, pushFinding) {
198
+ const interfaces = sf.getInterfaces();
199
+
200
+ interfaces.forEach(iface => {
201
+ const interfaceName = iface.getName();
202
+ const properties = iface.getProperties();
203
+ const methods = iface.getMethods();
204
+
205
+ if (properties.length > 10) {
206
+ const message = `ISP VIOLATION: ${interfaceName} has ${properties.length} properties - split into focused interfaces`;
207
+ pushFinding('solid.isp.fat_interface', 'critical', sf, iface, message, findings);
208
+ }
209
+
210
+ if (methods.length > 0) {
211
+ const methodConcerns = methods.map(m => detectMethodConcern(m.getName()));
212
+ const uniqueConcerns = new Set(methodConcerns.filter(c => c !== 'unknown'));
213
+
214
+ if (uniqueConcerns.size >= 3) {
215
+ const message = `ISP VIOLATION: ${interfaceName} mixes ${uniqueConcerns.size} concerns (${Array.from(uniqueConcerns).join(', ')}) - segregate into focused interfaces`;
216
+ pushFinding('solid.isp.multiple_concerns', 'critical', sf, iface, message, findings);
217
+ }
218
+ }
219
+ });
220
+ }
221
+
222
+ module.exports = {
223
+ analyzeOCP,
224
+ analyzeDIP,
225
+ analyzeSRP,
226
+ analyzeISP,
227
+ };
@@ -30,7 +30,10 @@ function getRepoRoot() {
30
30
  try {
31
31
  const { execSync } = require('child_process');
32
32
  return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
33
- } catch {
33
+ } catch (error) {
34
+ if (process.env.DEBUG) {
35
+ console.debug(`[ast-core] Failed to detect git repo root, using cwd: ${error.message}`);
36
+ }
34
37
  return process.cwd();
35
38
  }
36
39
  }
@@ -191,7 +194,10 @@ function pushFinding(ruleId, severity, sf, node, message, findings, metrics = {}
191
194
  let strictRegex;
192
195
  try {
193
196
  strictRegex = new RegExp(strictRegexSource, 'i');
194
- } catch {
197
+ } catch (error) {
198
+ if (process.env.DEBUG) {
199
+ console.debug(`[ast-core] Invalid STRICT_CRITICAL_RULES_REGEX, using default: ${error.message}`);
200
+ }
195
201
  strictRegex = new RegExp(env.getBool('AUDIT_LIBRARY', false) ? defaultStrictCriticalRegexLibrary : defaultStrictCriticalRegex, 'i');
196
202
  }
197
203
  isStrictCriticalRule = strictRegex.test(ruleId);
@@ -261,7 +267,10 @@ function pushFileFinding(ruleId, severity, filePath, line, column, message, find
261
267
  let strictRegex;
262
268
  try {
263
269
  strictRegex = new RegExp(strictRegexSource, 'i');
264
- } catch {
270
+ } catch (error) {
271
+ if (process.env.DEBUG) {
272
+ console.debug(`[ast-core] Invalid STRICT_CRITICAL_RULES_REGEX, using default: ${error.message}`);
273
+ }
265
274
  strictRegex = new RegExp(env.getBool('AUDIT_LIBRARY', false) ? defaultStrictCriticalRegexLibrary : defaultStrictCriticalRegex, 'i');
266
275
  }
267
276
  isStrictCriticalRule = strictRegex.test(ruleId);
@@ -115,7 +115,10 @@ function getStagedFilesRel(root) {
115
115
  .split('\n')
116
116
  .map(s => s.trim())
117
117
  .filter(Boolean);
118
- } catch {
118
+ } catch (error) {
119
+ if (process.env.DEBUG) {
120
+ console.debug(`[ast-intelligence] Failed to read staged files: ${error.message}`);
121
+ }
119
122
  return [];
120
123
  }
121
124
  }
@@ -172,7 +175,10 @@ function runProjectHardcodedThresholdAudit(root, allFiles, findings) {
172
175
  let content;
173
176
  try {
174
177
  content = fs.readFileSync(filePath, 'utf8');
175
- } catch {
178
+ } catch (error) {
179
+ if (process.env.DEBUG) {
180
+ console.debug(`[ast-intelligence] Failed to read file ${filePath}: ${error.message}`);
181
+ }
176
182
  continue;
177
183
  }
178
184
 
@@ -214,7 +220,10 @@ function runHardcodedThresholdAudit(root, findings) {
214
220
  ].filter((d) => {
215
221
  try {
216
222
  return fs.existsSync(d) && fs.statSync(d).isDirectory();
217
- } catch {
223
+ } catch (error) {
224
+ if (process.env.DEBUG) {
225
+ console.debug(`[ast-intelligence] Failed to stat dir ${d}: ${error.message}`);
226
+ }
218
227
  return false;
219
228
  }
220
229
  });
@@ -238,7 +247,10 @@ function runHardcodedThresholdAudit(root, findings) {
238
247
  let entries;
239
248
  try {
240
249
  entries = fs.readdirSync(current, { withFileTypes: true });
241
- } catch {
250
+ } catch (error) {
251
+ if (process.env.DEBUG) {
252
+ console.debug(`[ast-intelligence] Failed to read dir ${current}: ${error.message}`);
253
+ }
242
254
  continue;
243
255
  }
244
256
 
@@ -290,7 +302,10 @@ function runHardcodedThresholdAudit(root, findings) {
290
302
  let content;
291
303
  try {
292
304
  content = fs.readFileSync(filePath, 'utf8');
293
- } catch {
305
+ } catch (error) {
306
+ if (process.env.DEBUG) {
307
+ console.debug(`[ast-intelligence] Failed to read file ${filePath}: ${error.message}`);
308
+ }
294
309
  continue;
295
310
  }
296
311
 
@@ -394,15 +409,23 @@ function generateOutput(findings, context, project, root) {
394
409
  .filter(Boolean);
395
410
 
396
411
  if (stagedRel.length > 0) {
397
- const stagedAbs = new Set(stagedRel.map(r => path.resolve(root, r)));
398
- findings = (findings || []).filter(f => {
399
- if (!f || !f.filePath) return false;
400
- const fp = String(f.filePath);
401
- if (stagedAbs.has(fp)) return true;
402
- return stagedRel.some(rel => fp.endsWith(rel) || fp.includes(`/${rel}`));
403
- });
412
+ try {
413
+ findings = findings.filter((f) => {
414
+ if (!f || !f.filePath) return false;
415
+ const fp = String(f.filePath).replace(/\\/g, '/');
416
+ return stagedRel.some(rel => fp.endsWith(rel) || fp.includes(`/${rel}`));
417
+ });
418
+ } catch (error) {
419
+ if (process.env.DEBUG) {
420
+ console.debug(`[ast-intelligence] Failed to filter findings by staged files: ${error.message}`);
421
+ }
422
+ findings = [];
423
+ }
424
+ }
425
+ } catch (error) {
426
+ if (process.env.DEBUG) {
427
+ console.debug(`[ast-intelligence] Failed to get staged files: ${error.message}`);
404
428
  }
405
- } catch {
406
429
  findings = [];
407
430
  }
408
431
  }
@@ -21,6 +21,7 @@ const {
21
21
  getRepoRoot,
22
22
  } = require(path.join(__dirname, '../ast-core'));
23
23
  const { BackendArchitectureDetector } = require(path.join(__dirname, 'analyzers/BackendArchitectureDetector'));
24
+ const { analyzeGodClasses } = require(path.join(__dirname, 'detectors/god-class-detector'));
24
25
 
25
26
  /**
26
27
  * Run Backend-specific AST intelligence analysis
@@ -267,10 +268,13 @@ function runBackendIntelligence(project, findings, platform) {
267
268
  }
268
269
  }
269
270
 
270
- const hasEnvSpecific = sf.getFullText().includes("process.env.NODE_ENV") ||
271
- sf.getFullText().includes("app.get('env')") ||
272
- sf.getFullText().includes("ConfigService");
273
- const hasConfigUsage = sf.getFullText().includes("config") || sf.getFullText().includes("env");
271
+ const fullTextUpper = sf.getFullText();
272
+ const usesEnvHelper = /config\/env/.test(fullTextUpper) || /env\.get(Bool|Number)?\(/.test(fullTextUpper);
273
+ const hasEnvSpecific = fullTextUpper.includes("process.env.NODE_ENV") ||
274
+ fullTextUpper.includes("app.get('env')") ||
275
+ fullTextUpper.includes("ConfigService") ||
276
+ usesEnvHelper;
277
+ const hasConfigUsage = /process\.env\b|ConfigService|env\.get(Bool|Number)?\(|\bconfig\s*[\.\[]/i.test(sf.getFullText());
274
278
  if (!hasEnvSpecific && hasConfigUsage && !isTestFile(filePath)) {
275
279
  pushFinding("backend.config.missing_env_separation", "info", sf, sf, "Missing environment-specific configuration - consider NODE_ENV or ConfigService", findings);
276
280
  }
@@ -283,83 +287,7 @@ function runBackendIntelligence(project, findings, platform) {
283
287
  }
284
288
 
285
289
  if (godClassBaseline) {
286
- sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
287
- const className = cls.getName() || '';
288
- const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
289
- const isTestClass = /Spec$|Test$|Mock/.test(className);
290
- if (isValueObject || isTestClass) return;
291
-
292
- const methodsCount = cls.getMethods().length;
293
- const propertiesCount = cls.getProperties().length;
294
- const startLine = cls.getStartLineNumber();
295
- const endLine = cls.getEndLineNumber();
296
- const lineCount = Math.max(0, endLine - startLine);
297
-
298
- const decisionKinds = [
299
- SyntaxKind.IfStatement,
300
- SyntaxKind.ForStatement,
301
- SyntaxKind.ForInStatement,
302
- SyntaxKind.ForOfStatement,
303
- SyntaxKind.WhileStatement,
304
- SyntaxKind.DoStatement,
305
- SyntaxKind.SwitchStatement,
306
- SyntaxKind.ConditionalExpression,
307
- SyntaxKind.TryStatement,
308
- SyntaxKind.CatchClause
309
- ];
310
- const complexity = decisionKinds.reduce((acc, kind) => acc + cls.getDescendantsOfKind(kind).length, 0);
311
-
312
- const clsText = cls.getFullText();
313
- const concerns = new Set();
314
- if (/\bfs\.|\bfs\.promises\b|readFileSync|writeFileSync|mkdirSync|unlinkSync|readdirSync/.test(clsText)) concerns.add('io');
315
- if (/\bpath\.|join\(|resolve\(|dirname\(|basename\(/.test(clsText)) concerns.add('path');
316
- if (/execSync\(|spawnSync\(|spawn\(|child_process/.test(clsText)) concerns.add('process');
317
- if (/\bfetch\b|axios\b|http\.|https\.|request\(/.test(clsText)) concerns.add('network');
318
- if (/\bcrypto\b|encrypt|decrypt|hash|jwt|bearer|token/i.test(clsText)) concerns.add('security');
319
- if (/setTimeout\(|setInterval\(|clearInterval\(|cron|schedule/i.test(clsText)) concerns.add('scheduling');
320
- if (/\brepo\b|repository|prisma|typeorm|mongoose|sequelize|knex|\bdb\b|database|sql/i.test(clsText)) concerns.add('persistence');
321
- if (/notification|notifier|terminal-notifier|osascript/i.test(clsText)) concerns.add('notifications');
322
- if (/\bgit\b|rev-parse|git diff|git status|git log/i.test(clsText)) concerns.add('git');
323
- const concernCount = concerns.size;
324
-
325
- const methodsZ = godClassBaseline.robustZ(methodsCount, godClassBaseline.med.methodsCount, godClassBaseline.mad.methodsCount);
326
- const propsZ = godClassBaseline.robustZ(propertiesCount, godClassBaseline.med.propertiesCount, godClassBaseline.mad.propertiesCount);
327
- const linesZ = godClassBaseline.robustZ(lineCount, godClassBaseline.med.lineCount, godClassBaseline.mad.lineCount);
328
- const complexityZ = godClassBaseline.robustZ(complexity, godClassBaseline.med.complexity, godClassBaseline.mad.complexity);
329
-
330
- const sizeOutlier =
331
- methodsZ >= godClassBaseline.thresholds.outlier.methodsCountZ ||
332
- propsZ >= godClassBaseline.thresholds.outlier.propertiesCountZ ||
333
- linesZ >= godClassBaseline.thresholds.outlier.lineCountZ;
334
- const complexityOutlier = complexityZ >= godClassBaseline.thresholds.outlier.complexityZ;
335
- const concernOutlier = concernCount >= godClassBaseline.thresholds.outlier.concerns;
336
-
337
- // Detección híbrida: estadística + umbrales absolutos
338
- // Archivo masivo: cualquier archivo con >500 líneas es sospechoso
339
- const isMassiveFile = lineCount > 500;
340
- // God class absoluta: archivo muy grande O (grande + complejo) O (grande + muchos métodos)
341
- const isAbsoluteGod = lineCount > 1000 ||
342
- (lineCount > 500 && complexity > 50) ||
343
- (lineCount > 500 && methodsCount > 20) ||
344
- (lineCount > 600 && methodsCount > 30 && complexity > 80);
345
- const isUnderThreshold = lineCount < 300 && methodsCount < 15 && complexity < 30;
346
-
347
- let signalCount = 0;
348
- if (sizeOutlier) signalCount++;
349
- if (complexityOutlier) signalCount++;
350
- if (concernOutlier) signalCount++;
351
- if (isMassiveFile) signalCount++; // Añadir señal extra por tamaño masivo
352
-
353
- const isInternalAstToolingFile = /infrastructure\/ast\//i.test(filePath);
354
- const isInfrastructureService = /application\/services.*\/(RealtimeGuardService|EvidenceManager|HookInstaller|InstallService|EvidenceMonitor)/i.test(filePath);
355
- if (!isUnderThreshold && !isInternalAstToolingFile && !isInfrastructureService && (signalCount >= 2 || isAbsoluteGod)) {
356
- console.error(`[GOD CLASS DEBUG] ${className}: methods=${methodsCount}, props=${propertiesCount}, lines=${lineCount}, complexity=${complexity}, concerns=${concernCount}, isAbsoluteGod=${isAbsoluteGod}, signalCount=${signalCount}`);
357
- pushFinding("backend.antipattern.god_classes", "critical", sf, cls,
358
- `God class detected: ${methodsCount} methods, ${propertiesCount} properties, ${lineCount} lines, complexity ${complexity}, concerns ${concernCount} - VIOLATES SRP`,
359
- findings
360
- );
361
- }
362
- });
290
+ analyzeGodClasses(sf, findings, { SyntaxKind, pushFinding, godClassBaseline });
363
291
  }
364
292
 
365
293
  sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
@@ -760,8 +688,7 @@ function runBackendIntelligence(project, findings, platform) {
760
688
 
761
689
  sf.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach((str) => {
762
690
  const text = str.getLiteralValue();
763
- const sqlKeywords = ['SEL' + 'ECT ', 'INS' + 'ERT ', 'UPD' + 'ATE ', 'DEL' + 'ETE ', 'DR' + 'OP ', 'AL' + 'TER ', 'TRUN' + 'CATE '];
764
- const sqlPattern = new RegExp(sqlKeywords.join('|'), 'i');
691
+ const sqlPattern = /\b(select|insert|update|delete|drop|alter|truncate)\b\s+(from|into|table|database|schema|where)/i;
765
692
  if (sqlPattern.test(text)) {
766
693
  pushFinding("backend.database.raw_sql", "medium", sf, str, "Raw SQL detected - prefer ORM queries for type safety and security", findings);
767
694
  if (/[\"'`].*\+.*[\"'`]/.test(text) || /\$\{.*\}/.test(text)) {
@@ -0,0 +1,83 @@
1
+ /**
2
+ * God Class detector extracted from ast-backend.js to keep responsibilities separated.
3
+ */
4
+ function analyzeGodClasses(sourceFile, findings, { SyntaxKind, pushFinding, godClassBaseline }) {
5
+ if (!godClassBaseline) return;
6
+
7
+ sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
8
+ const className = cls.getName() || '';
9
+ const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
10
+ const isTestClass = /Spec$|Test$|Mock/.test(className);
11
+ if (isValueObject || isTestClass) return;
12
+
13
+ const methodsCount = cls.getMethods().length;
14
+ const propertiesCount = cls.getProperties().length;
15
+ const startLine = cls.getStartLineNumber();
16
+ const endLine = cls.getEndLineNumber();
17
+ const lineCount = Math.max(0, endLine - startLine);
18
+
19
+ const decisionKinds = [
20
+ SyntaxKind.IfStatement,
21
+ SyntaxKind.ForStatement,
22
+ SyntaxKind.ForInStatement,
23
+ SyntaxKind.ForOfStatement,
24
+ SyntaxKind.WhileStatement,
25
+ SyntaxKind.DoStatement,
26
+ SyntaxKind.SwitchStatement,
27
+ SyntaxKind.ConditionalExpression,
28
+ SyntaxKind.TryStatement,
29
+ SyntaxKind.CatchClause
30
+ ];
31
+ const complexity = decisionKinds.reduce((acc, kind) => acc + cls.getDescendantsOfKind(kind).length, 0);
32
+
33
+ const clsText = cls.getFullText();
34
+ const concerns = new Set();
35
+ if (/\bfs\.|\bfs\.promises\b|readFileSync|writeFileSync|mkdirSync|unlinkSync|readdirSync/.test(clsText)) concerns.add('io');
36
+ if (/\bpath\.|join\(|resolve\(|dirname\(|basename\(/.test(clsText)) concerns.add('path');
37
+ if (/execSync\(|spawnSync\(|spawn\(|child_process/.test(clsText)) concerns.add('process');
38
+ if (/\bfetch\b|axios\b|http\.|https\.|request\(/.test(clsText)) concerns.add('network');
39
+ if (/\bcrypto\b|encrypt|decrypt|hash|jwt|bearer|token/i.test(clsText)) concerns.add('security');
40
+ if (/setTimeout\(|setInterval\(|clearInterval\(|cron|schedule/i.test(clsText)) concerns.add('scheduling');
41
+ if (/\brepo\b|repository|prisma|typeorm|mongoose|sequelize|knex|\bdb\b|database|sql/i.test(clsText)) concerns.add('persistence');
42
+ if (/notification|notifier|terminal-notifier|osascript/i.test(clsText)) concerns.add('notifications');
43
+ if (/\bgit\b|rev-parse|git diff|git status|git log/i.test(clsText)) concerns.add('git');
44
+ const concernCount = concerns.size;
45
+
46
+ const methodsZ = godClassBaseline.robustZ(methodsCount, godClassBaseline.med.methodsCount, godClassBaseline.mad.methodsCount);
47
+ const propsZ = godClassBaseline.robustZ(propertiesCount, godClassBaseline.med.propertiesCount, godClassBaseline.mad.propertiesCount);
48
+ const linesZ = godClassBaseline.robustZ(lineCount, godClassBaseline.med.lineCount, godClassBaseline.mad.lineCount);
49
+ const complexityZ = godClassBaseline.robustZ(complexity, godClassBaseline.med.complexity, godClassBaseline.mad.complexity);
50
+
51
+ const sizeOutlier =
52
+ methodsZ >= godClassBaseline.thresholds.outlier.methodsCountZ ||
53
+ propsZ >= godClassBaseline.thresholds.outlier.propertiesCountZ ||
54
+ linesZ >= godClassBaseline.thresholds.outlier.lineCountZ;
55
+ const complexityOutlier = complexityZ >= godClassBaseline.thresholds.outlier.complexityZ;
56
+ const concernOutlier = concernCount >= godClassBaseline.thresholds.outlier.concerns;
57
+
58
+ const isMassiveFile = lineCount > 500;
59
+ const isAbsoluteGod = lineCount > 1000 ||
60
+ (lineCount > 500 && complexity > 50) ||
61
+ (lineCount > 500 && methodsCount > 20) ||
62
+ (lineCount > 600 && methodsCount > 30 && complexity > 80);
63
+ const isUnderThreshold = lineCount < 300 && methodsCount < 15 && complexity < 30;
64
+
65
+ let signalCount = 0;
66
+ if (sizeOutlier) signalCount++;
67
+ if (complexityOutlier) signalCount++;
68
+ if (concernOutlier) signalCount++;
69
+ if (isMassiveFile) signalCount++;
70
+
71
+ if (!isUnderThreshold && (signalCount >= 2 || isAbsoluteGod)) {
72
+ console.error(`[GOD CLASS DEBUG] ${className}: methods=${methodsCount}, props=${propertiesCount}, lines=${lineCount}, complexity=${complexity}, concerns=${concernCount}, isAbsoluteGod=${isAbsoluteGod}, signalCount=${signalCount}`);
73
+ pushFinding("backend.antipattern.god_classes", "critical", sourceFile, cls,
74
+ `God class detected: ${methodsCount} methods, ${propertiesCount} properties, ${lineCount} lines, complexity ${complexity}, concerns ${concernCount} - VIOLATES SRP`,
75
+ findings
76
+ );
77
+ }
78
+ });
79
+ }
80
+
81
+ module.exports = {
82
+ analyzeGodClasses,
83
+ };
@@ -145,6 +145,21 @@ function runCommonIntelligence(project, findings) {
145
145
  }
146
146
  });
147
147
 
148
+ sf.getDescendantsOfKind(SyntaxKind.CatchClause).forEach((clause) => {
149
+ const block = typeof clause.getBlock === 'function' ? clause.getBlock() : null;
150
+ const statements = block && typeof block.getStatements === 'function' ? block.getStatements() : [];
151
+ if ((statements || []).length === 0) {
152
+ pushFinding(
153
+ 'common.error.empty_catch',
154
+ 'critical',
155
+ sf,
156
+ clause,
157
+ 'Empty catch block detected - always handle errors (log, rethrow, wrap, or return Result)',
158
+ findings
159
+ );
160
+ }
161
+ });
162
+
148
163
  sf.getDescendantsOfKind(SyntaxKind.AnyKeyword).forEach((node) => {
149
164
 
150
165
  const isUtilityScript = /\/(scripts?|migrations?|seeders?|fixtures?)\//i.test(filePath);
@@ -318,7 +333,7 @@ function runCommonIntelligence(project, findings) {
318
333
  withoutStrings = withoutStrings.replace(/`[^`]*`/g, '');
319
334
  withoutStrings = withoutStrings.replace(/"[^"]*"/g, '');
320
335
  withoutStrings = withoutStrings.replace(/'[^']*'/g, '');
321
-
336
+
322
337
  if (!/\/\/|\/\*/.test(withoutStrings)) return;
323
338
 
324
339
  const withoutUrls = withoutStrings.replace(/https?:\/\//g, '');
@@ -327,7 +342,7 @@ function runCommonIntelligence(project, findings) {
327
342
  const withoutJSDoc = withoutUrls.replace(/\/\*\*[\s\S]*?\*\//g, '');
328
343
  const hasOnlyJSDoc = !/\/\/|\/\*/.test(withoutJSDoc);
329
344
  if (hasOnlyJSDoc) return;
330
-
345
+
331
346
  const withoutAllComments = withoutJSDoc.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
332
347
  if (!/\/\/|\/\*/.test(withoutAllComments)) return;
333
348