@girardelli/architect-core 8.1.0

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 (217) hide show
  1. package/dist/src/core/analyzer.d.ts +42 -0
  2. package/dist/src/core/analyzer.js +431 -0
  3. package/dist/src/core/analyzer.js.map +1 -0
  4. package/dist/src/core/analyzers/forecast.d.ts +84 -0
  5. package/dist/src/core/analyzers/forecast.js +338 -0
  6. package/dist/src/core/analyzers/forecast.js.map +1 -0
  7. package/dist/src/core/analyzers/index.d.ts +9 -0
  8. package/dist/src/core/analyzers/index.js +7 -0
  9. package/dist/src/core/analyzers/index.js.map +1 -0
  10. package/dist/src/core/analyzers/temporal-scorer.d.ts +71 -0
  11. package/dist/src/core/analyzers/temporal-scorer.js +141 -0
  12. package/dist/src/core/analyzers/temporal-scorer.js.map +1 -0
  13. package/dist/src/core/anti-patterns.d.ts +28 -0
  14. package/dist/src/core/anti-patterns.js +264 -0
  15. package/dist/src/core/anti-patterns.js.map +1 -0
  16. package/dist/src/core/ast/ast-parser.interface.d.ts +20 -0
  17. package/dist/src/core/ast/ast-parser.interface.js +2 -0
  18. package/dist/src/core/ast/ast-parser.interface.js.map +1 -0
  19. package/dist/src/core/ast/path-resolver.d.ts +13 -0
  20. package/dist/src/core/ast/path-resolver.js +54 -0
  21. package/dist/src/core/ast/path-resolver.js.map +1 -0
  22. package/dist/src/core/ast/tree-sitter-parser.d.ts +10 -0
  23. package/dist/src/core/ast/tree-sitter-parser.js +142 -0
  24. package/dist/src/core/ast/tree-sitter-parser.js.map +1 -0
  25. package/dist/src/core/config.d.ts +11 -0
  26. package/dist/src/core/config.js +112 -0
  27. package/dist/src/core/config.js.map +1 -0
  28. package/dist/src/core/diagram.d.ts +9 -0
  29. package/dist/src/core/diagram.js +101 -0
  30. package/dist/src/core/diagram.js.map +1 -0
  31. package/dist/src/core/i18n.d.ts +14 -0
  32. package/dist/src/core/i18n.js +54 -0
  33. package/dist/src/core/i18n.js.map +1 -0
  34. package/dist/src/core/locales/en.d.ts +2 -0
  35. package/dist/src/core/locales/en.js +337 -0
  36. package/dist/src/core/locales/en.js.map +1 -0
  37. package/dist/src/core/locales/pt-BR.d.ts +172 -0
  38. package/dist/src/core/locales/pt-BR.js +337 -0
  39. package/dist/src/core/locales/pt-BR.js.map +1 -0
  40. package/dist/src/core/locales/types.d.ts +86 -0
  41. package/dist/src/core/locales/types.js +2 -0
  42. package/dist/src/core/locales/types.js.map +1 -0
  43. package/dist/src/core/plugin-loader.d.ts +11 -0
  44. package/dist/src/core/plugin-loader.js +67 -0
  45. package/dist/src/core/plugin-loader.js.map +1 -0
  46. package/dist/src/core/project-summarizer.d.ts +16 -0
  47. package/dist/src/core/project-summarizer.js +37 -0
  48. package/dist/src/core/project-summarizer.js.map +1 -0
  49. package/dist/src/core/refactor-engine.d.ts +18 -0
  50. package/dist/src/core/refactor-engine.js +87 -0
  51. package/dist/src/core/refactor-engine.js.map +1 -0
  52. package/dist/src/core/rules/barrel-optimizer.d.ts +13 -0
  53. package/dist/src/core/rules/barrel-optimizer.js +76 -0
  54. package/dist/src/core/rules/barrel-optimizer.js.map +1 -0
  55. package/dist/src/core/rules/dead-code-detector.d.ts +21 -0
  56. package/dist/src/core/rules/dead-code-detector.js +116 -0
  57. package/dist/src/core/rules/dead-code-detector.js.map +1 -0
  58. package/dist/src/core/rules/hub-splitter.d.ts +13 -0
  59. package/dist/src/core/rules/hub-splitter.js +117 -0
  60. package/dist/src/core/rules/hub-splitter.js.map +1 -0
  61. package/dist/src/core/rules/import-organizer.d.ts +13 -0
  62. package/dist/src/core/rules/import-organizer.js +84 -0
  63. package/dist/src/core/rules/import-organizer.js.map +1 -0
  64. package/dist/src/core/rules/module-grouper.d.ts +13 -0
  65. package/dist/src/core/rules/module-grouper.js +116 -0
  66. package/dist/src/core/rules/module-grouper.js.map +1 -0
  67. package/dist/src/core/rules-engine.d.ts +7 -0
  68. package/dist/src/core/rules-engine.js +89 -0
  69. package/dist/src/core/rules-engine.js.map +1 -0
  70. package/dist/src/core/scorer.d.ts +15 -0
  71. package/dist/src/core/scorer.js +165 -0
  72. package/dist/src/core/scorer.js.map +1 -0
  73. package/dist/src/core/summarizer/keyword-extractor.d.ts +6 -0
  74. package/dist/src/core/summarizer/keyword-extractor.js +38 -0
  75. package/dist/src/core/summarizer/keyword-extractor.js.map +1 -0
  76. package/dist/src/core/summarizer/module-inferrer.d.ts +11 -0
  77. package/dist/src/core/summarizer/module-inferrer.js +171 -0
  78. package/dist/src/core/summarizer/module-inferrer.js.map +1 -0
  79. package/dist/src/core/summarizer/package-reader.d.ts +3 -0
  80. package/dist/src/core/summarizer/package-reader.js +33 -0
  81. package/dist/src/core/summarizer/package-reader.js.map +1 -0
  82. package/dist/src/core/summarizer/purpose-inferrer.d.ts +8 -0
  83. package/dist/src/core/summarizer/purpose-inferrer.js +179 -0
  84. package/dist/src/core/summarizer/purpose-inferrer.js.map +1 -0
  85. package/dist/src/core/summarizer/readme-reader.d.ts +3 -0
  86. package/dist/src/core/summarizer/readme-reader.js +24 -0
  87. package/dist/src/core/summarizer/readme-reader.js.map +1 -0
  88. package/dist/src/core/types/architect-rules.d.ts +27 -0
  89. package/dist/src/core/types/architect-rules.js +2 -0
  90. package/dist/src/core/types/architect-rules.js.map +1 -0
  91. package/dist/src/core/types/core.d.ts +87 -0
  92. package/dist/src/core/types/core.js +2 -0
  93. package/dist/src/core/types/core.js.map +1 -0
  94. package/dist/src/core/types/infrastructure.d.ts +38 -0
  95. package/dist/src/core/types/infrastructure.js +2 -0
  96. package/dist/src/core/types/infrastructure.js.map +1 -0
  97. package/dist/src/core/types/plugin.d.ts +12 -0
  98. package/dist/src/core/types/plugin.js +2 -0
  99. package/dist/src/core/types/plugin.js.map +1 -0
  100. package/dist/src/core/types/rules.d.ts +53 -0
  101. package/dist/src/core/types/rules.js +2 -0
  102. package/dist/src/core/types/rules.js.map +1 -0
  103. package/dist/src/core/types/summarizer.d.ts +12 -0
  104. package/dist/src/core/types/summarizer.js +2 -0
  105. package/dist/src/core/types/summarizer.js.map +1 -0
  106. package/dist/src/infrastructure/git-cache.d.ts +6 -0
  107. package/dist/src/infrastructure/git-cache.js +41 -0
  108. package/dist/src/infrastructure/git-cache.js.map +1 -0
  109. package/dist/src/infrastructure/git-history.d.ts +112 -0
  110. package/dist/src/infrastructure/git-history.js +340 -0
  111. package/dist/src/infrastructure/git-history.js.map +1 -0
  112. package/dist/src/infrastructure/logger.d.ts +20 -0
  113. package/dist/src/infrastructure/logger.js +57 -0
  114. package/dist/src/infrastructure/logger.js.map +1 -0
  115. package/dist/src/infrastructure/scanner.d.ts +31 -0
  116. package/dist/src/infrastructure/scanner.js +334 -0
  117. package/dist/src/infrastructure/scanner.js.map +1 -0
  118. package/dist/tests/analyzers-integration.test.d.ts +7 -0
  119. package/dist/tests/analyzers-integration.test.js +140 -0
  120. package/dist/tests/analyzers-integration.test.js.map +1 -0
  121. package/dist/tests/anti-patterns.test.d.ts +1 -0
  122. package/dist/tests/anti-patterns.test.js +81 -0
  123. package/dist/tests/anti-patterns.test.js.map +1 -0
  124. package/dist/tests/ast-parser.test.d.ts +1 -0
  125. package/dist/tests/ast-parser.test.js +94 -0
  126. package/dist/tests/ast-parser.test.js.map +1 -0
  127. package/dist/tests/fixtures/monorepo/packages/app/src/index.d.ts +1 -0
  128. package/dist/tests/fixtures/monorepo/packages/app/src/index.js +9 -0
  129. package/dist/tests/fixtures/monorepo/packages/app/src/index.js.map +1 -0
  130. package/dist/tests/fixtures/monorepo/packages/core/src/index.d.ts +2 -0
  131. package/dist/tests/fixtures/monorepo/packages/core/src/index.js +11 -0
  132. package/dist/tests/fixtures/monorepo/packages/core/src/index.js.map +1 -0
  133. package/dist/tests/forecast.test.d.ts +7 -0
  134. package/dist/tests/forecast.test.js +380 -0
  135. package/dist/tests/forecast.test.js.map +1 -0
  136. package/dist/tests/git-history.test.d.ts +7 -0
  137. package/dist/tests/git-history.test.js +193 -0
  138. package/dist/tests/git-history.test.js.map +1 -0
  139. package/dist/tests/i18n.test.d.ts +1 -0
  140. package/dist/tests/i18n.test.js +39 -0
  141. package/dist/tests/i18n.test.js.map +1 -0
  142. package/dist/tests/monorepo-scan.test.d.ts +11 -0
  143. package/dist/tests/monorepo-scan.test.js +143 -0
  144. package/dist/tests/monorepo-scan.test.js.map +1 -0
  145. package/dist/tests/plugin-loader.test.d.ts +1 -0
  146. package/dist/tests/plugin-loader.test.js +31 -0
  147. package/dist/tests/plugin-loader.test.js.map +1 -0
  148. package/dist/tests/rules-engine.test.d.ts +1 -0
  149. package/dist/tests/rules-engine.test.js +112 -0
  150. package/dist/tests/rules-engine.test.js.map +1 -0
  151. package/dist/tests/scanner.test.d.ts +1 -0
  152. package/dist/tests/scanner.test.js +44 -0
  153. package/dist/tests/scanner.test.js.map +1 -0
  154. package/dist/tests/scorer.test.d.ts +1 -0
  155. package/dist/tests/scorer.test.js +610 -0
  156. package/dist/tests/scorer.test.js.map +1 -0
  157. package/dist/tests/temporal-scorer.test.d.ts +7 -0
  158. package/dist/tests/temporal-scorer.test.js +239 -0
  159. package/dist/tests/temporal-scorer.test.js.map +1 -0
  160. package/package.json +29 -0
  161. package/src/core/analyzer.ts +499 -0
  162. package/src/core/analyzers/forecast.ts +497 -0
  163. package/src/core/analyzers/index.ts +33 -0
  164. package/src/core/analyzers/temporal-scorer.ts +227 -0
  165. package/src/core/anti-patterns.ts +324 -0
  166. package/src/core/ast/ast-parser.interface.ts +21 -0
  167. package/src/core/ast/path-resolver.ts +61 -0
  168. package/src/core/ast/tree-sitter-parser.ts +158 -0
  169. package/src/core/config.ts +125 -0
  170. package/src/core/diagram.ts +129 -0
  171. package/src/core/i18n.ts +64 -0
  172. package/src/core/locales/en.ts +340 -0
  173. package/src/core/locales/pt-BR.ts +341 -0
  174. package/src/core/locales/types.ts +95 -0
  175. package/src/core/plugin-loader.ts +80 -0
  176. package/src/core/project-summarizer.ts +42 -0
  177. package/src/core/refactor-engine.ts +112 -0
  178. package/src/core/rules/barrel-optimizer.ts +99 -0
  179. package/src/core/rules/dead-code-detector.ts +134 -0
  180. package/src/core/rules/hub-splitter.ts +135 -0
  181. package/src/core/rules/import-organizer.ts +100 -0
  182. package/src/core/rules/module-grouper.ts +133 -0
  183. package/src/core/rules-engine.ts +100 -0
  184. package/src/core/scorer.ts +181 -0
  185. package/src/core/summarizer/keyword-extractor.ts +53 -0
  186. package/src/core/summarizer/module-inferrer.ts +194 -0
  187. package/src/core/summarizer/package-reader.ts +34 -0
  188. package/src/core/summarizer/purpose-inferrer.ts +197 -0
  189. package/src/core/summarizer/readme-reader.ts +24 -0
  190. package/src/core/types/architect-rules.ts +29 -0
  191. package/src/core/types/core.ts +94 -0
  192. package/src/core/types/infrastructure.ts +41 -0
  193. package/src/core/types/plugin.ts +19 -0
  194. package/src/core/types/rules.ts +51 -0
  195. package/src/core/types/summarizer.ts +8 -0
  196. package/src/infrastructure/git-cache.ts +52 -0
  197. package/src/infrastructure/git-history.ts +496 -0
  198. package/src/infrastructure/logger.ts +68 -0
  199. package/src/infrastructure/scanner.ts +349 -0
  200. package/tests/analyzers-integration.test.ts +174 -0
  201. package/tests/anti-patterns.test.ts +95 -0
  202. package/tests/ast-parser.test.ts +102 -0
  203. package/tests/fixtures/monorepo/package.json +6 -0
  204. package/tests/fixtures/monorepo/packages/app/package.json +12 -0
  205. package/tests/fixtures/monorepo/packages/app/src/index.ts +6 -0
  206. package/tests/fixtures/monorepo/packages/core/package.json +7 -0
  207. package/tests/fixtures/monorepo/packages/core/src/index.ts +7 -0
  208. package/tests/forecast.test.ts +504 -0
  209. package/tests/git-history.test.ts +254 -0
  210. package/tests/i18n.test.ts +47 -0
  211. package/tests/monorepo-scan.test.ts +170 -0
  212. package/tests/plugin-loader.test.ts +40 -0
  213. package/tests/rules-engine.test.ts +131 -0
  214. package/tests/scanner.test.ts +54 -0
  215. package/tests/scorer.test.ts +675 -0
  216. package/tests/temporal-scorer.test.ts +306 -0
  217. package/tsconfig.json +9 -0
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Temporal Score Dimension — Adds time-series awareness to architecture scoring
3
+ *
4
+ * Combines current static score with historical velocity to produce:
5
+ * - Trend per module (improving / stable / degrading)
6
+ * - Temporal risk score (static score penalized by negative velocity)
7
+ * - Projected score in N weeks
8
+ *
9
+ * @author Camilo Girardelli — Girardelli Tecnologia
10
+ * @license MIT
11
+ */
12
+
13
+ import type {
14
+ ModuleHistory,
15
+ VelocityVector,
16
+ GitHistoryReport,
17
+ } from '../../infrastructure/git-history.js';
18
+
19
+ // ═══════════════════════════════════════════════════════════════
20
+ // TYPES
21
+ // ═══════════════════════════════════════════════════════════════
22
+
23
+ export type Trend = 'improving' | 'stable' | 'degrading';
24
+
25
+ export interface TemporalScore {
26
+ /** Module or file path */
27
+ module: string;
28
+ /** Current static score (from ArchitectureScorer) */
29
+ staticScore: number;
30
+ /** Temporal-adjusted score (penalizes degrading trends) */
31
+ temporalScore: number;
32
+ /** Direction of change */
33
+ trend: Trend;
34
+ /** Projected score in projectionWeeks */
35
+ projectedScore: number;
36
+ /** Confidence in the projection (0-1) */
37
+ projectionConfidence: number;
38
+ /** Weeks used for projection */
39
+ projectionWeeks: number;
40
+ /** Risk level derived from temporal analysis */
41
+ riskLevel: 'low' | 'medium' | 'high' | 'critical';
42
+ /** Velocity data */
43
+ velocity: VelocityVector;
44
+ }
45
+
46
+ export interface TemporalReport {
47
+ projectPath: string;
48
+ analyzedAt: string;
49
+ overallTrend: Trend;
50
+ overallTemporalScore: number;
51
+ modules: TemporalScore[];
52
+ degradingModules: TemporalScore[];
53
+ improvingModules: TemporalScore[];
54
+ }
55
+
56
+ export interface TemporalScorerConfig {
57
+ /** Weeks ahead to project (default: 12) */
58
+ projectionWeeks?: number;
59
+ /** Weight of churn trend in temporal penalty (0-1, default: 0.6) */
60
+ churnWeight?: number;
61
+ /** Weight of commit acceleration in temporal penalty (0-1, default: 0.4) */
62
+ commitWeight?: number;
63
+ /** Threshold for trend classification: accelerating if > threshold % */
64
+ acceleratingThreshold?: number;
65
+ /** Threshold for trend classification: decelerating if < -threshold % */
66
+ deceleratingThreshold?: number;
67
+ }
68
+
69
+ const DEFAULT_CONFIG: Required<TemporalScorerConfig> = {
70
+ projectionWeeks: 12,
71
+ churnWeight: 0.6,
72
+ commitWeight: 0.4,
73
+ acceleratingThreshold: 15,
74
+ deceleratingThreshold: -15,
75
+ };
76
+
77
+ // ═══════════════════════════════════════════════════════════════
78
+ // TEMPORAL SCORER
79
+ // ═══════════════════════════════════════════════════════════════
80
+
81
+ export class TemporalScorer {
82
+ private config: Required<TemporalScorerConfig>;
83
+
84
+ constructor(config?: TemporalScorerConfig) {
85
+ this.config = { ...DEFAULT_CONFIG, ...config };
86
+ }
87
+
88
+ /**
89
+ * Score modules temporally using git history + static scores.
90
+ *
91
+ * @param gitReport - Output from GitHistoryAnalyzer
92
+ * @param staticScores - Map of modulePath → static score (0-100)
93
+ */
94
+ score(
95
+ gitReport: GitHistoryReport,
96
+ staticScores: Map<string, number>,
97
+ ): TemporalReport {
98
+ const modules: TemporalScore[] = [];
99
+
100
+ for (const moduleHistory of gitReport.modules) {
101
+ const staticScore = staticScores.get(moduleHistory.modulePath)
102
+ ?? this.inferStaticScore(moduleHistory);
103
+
104
+ const ts = this.scoreModule(moduleHistory, staticScore);
105
+ modules.push(ts);
106
+ }
107
+
108
+ // Sort by risk (worst first)
109
+ modules.sort((a, b) => a.temporalScore - b.temporalScore);
110
+
111
+ const degrading = modules.filter(m => m.trend === 'degrading');
112
+ const improving = modules.filter(m => m.trend === 'improving');
113
+
114
+ const overallTrend = this.classifyOverallTrend(modules);
115
+ const overallScore = modules.length > 0
116
+ ? Math.round(modules.reduce((s, m) => s + m.temporalScore, 0) / modules.length)
117
+ : 0;
118
+
119
+ return {
120
+ projectPath: gitReport.projectPath,
121
+ analyzedAt: new Date().toISOString(),
122
+ overallTrend: overallTrend,
123
+ overallTemporalScore: overallScore,
124
+ modules,
125
+ degradingModules: degrading,
126
+ improvingModules: improving,
127
+ };
128
+ }
129
+
130
+ private scoreModule(module: ModuleHistory, staticScore: number): TemporalScore {
131
+ const velocity = module.velocityVector;
132
+
133
+ // Calculate temporal penalty based on velocity
134
+ const churnPenalty = velocity.churnTrend > 0
135
+ ? velocity.churnTrend * this.config.churnWeight * 0.3 // 30% impact per 100% churn increase
136
+ : velocity.churnTrend * this.config.churnWeight * 0.1; // 10% bonus per 100% churn decrease
137
+
138
+ const commitPenalty = velocity.commitAcceleration > 20
139
+ ? (velocity.commitAcceleration - 20) * this.config.commitWeight * 0.2 // penalty for excessive churn
140
+ : 0;
141
+
142
+ const totalPenalty = Math.max(-20, Math.min(30, churnPenalty + commitPenalty));
143
+ const temporalScore = Math.max(0, Math.min(100, Math.round(staticScore - totalPenalty)));
144
+
145
+ // Trend classification
146
+ const trend = this.classifyTrend(velocity);
147
+
148
+ // Linear projection
149
+ const weeklyDelta = totalPenalty / Math.max(this.config.projectionWeeks, 1);
150
+ const projectedScore = Math.max(0, Math.min(100,
151
+ Math.round(temporalScore - (weeklyDelta * this.config.projectionWeeks))
152
+ ));
153
+
154
+ // Confidence decreases with projection distance and instability
155
+ const instability = Math.abs(velocity.churnTrend) + Math.abs(velocity.commitAcceleration);
156
+ const projectionConfidence = Math.max(0.1, Math.min(1,
157
+ 1 - (instability / 200) - (this.config.projectionWeeks / 52)
158
+ ));
159
+
160
+ const riskLevel = this.classifyRisk(temporalScore, trend, module.busFactor);
161
+
162
+ return {
163
+ module: module.modulePath,
164
+ staticScore,
165
+ temporalScore,
166
+ trend,
167
+ projectedScore,
168
+ projectionConfidence: Math.round(projectionConfidence * 100) / 100,
169
+ projectionWeeks: this.config.projectionWeeks,
170
+ riskLevel,
171
+ velocity,
172
+ };
173
+ }
174
+
175
+ private classifyTrend(velocity: VelocityVector): Trend {
176
+ // Degrading: churn increasing significantly or commit acceleration very high
177
+ if (velocity.churnTrend > 30 || velocity.commitAcceleration > 50) {
178
+ return 'degrading';
179
+ }
180
+ // Improving: churn decreasing and stable or decelerating
181
+ if (velocity.churnTrend < -10 && velocity.direction !== 'accelerating') {
182
+ return 'improving';
183
+ }
184
+ return 'stable';
185
+ }
186
+
187
+ private classifyRisk(
188
+ temporalScore: number,
189
+ trend: Trend,
190
+ busFactor: number,
191
+ ): TemporalScore['riskLevel'] {
192
+ if (temporalScore < 30 || (temporalScore < 50 && trend === 'degrading')) {
193
+ return 'critical';
194
+ }
195
+ if (temporalScore < 50 || (trend === 'degrading' && busFactor <= 1)) {
196
+ return 'high';
197
+ }
198
+ if (temporalScore < 70 || trend === 'degrading') {
199
+ return 'medium';
200
+ }
201
+ return 'low';
202
+ }
203
+
204
+ private classifyOverallTrend(modules: TemporalScore[]): Trend {
205
+ if (modules.length === 0) return 'stable';
206
+
207
+ const degrading = modules.filter(m => m.trend === 'degrading').length;
208
+ const improving = modules.filter(m => m.trend === 'improving').length;
209
+
210
+ const degradingRatio = degrading / modules.length;
211
+ const improvingRatio = improving / modules.length;
212
+
213
+ if (degradingRatio > 0.3) return 'degrading';
214
+ if (improvingRatio > 0.3) return 'improving';
215
+ return 'stable';
216
+ }
217
+
218
+ /** Infer a static score when none is provided (based on churn metrics) */
219
+ private inferStaticScore(module: ModuleHistory): number {
220
+ const avgChurn = module.aggregateChurn / Math.max(module.aggregateCommits, 1);
221
+ if (avgChurn < 20) return 85;
222
+ if (avgChurn < 50) return 75;
223
+ if (avgChurn < 100) return 65;
224
+ if (avgChurn < 200) return 50;
225
+ return 35;
226
+ }
227
+ }
@@ -0,0 +1,324 @@
1
+ import { readFileSync } from 'fs';
2
+ import { AntiPattern, ArchitectConfig } from './types/core.js';
3
+ import { FileNode } from './types/infrastructure.js';
4
+ import type { CustomAntiPatternDetector, PluginContext } from './types/plugin.js';
5
+ import { logger } from '../infrastructure/logger.js';
6
+
7
+ export class AntiPatternDetector {
8
+ private config: ArchitectConfig;
9
+ private dependencyGraph: Map<string, Set<string>>;
10
+
11
+ /** Paths that indicate third-party or build artifacts — never report anti-patterns here */
12
+ private static readonly EXCLUDED_PATH_SEGMENTS = [
13
+ 'node_modules', '/dist/', '/build/', '/coverage/',
14
+ '/.next/', '/venv/', '/__pycache__/', '/target/',
15
+ ];
16
+
17
+ private customDetectors: CustomAntiPatternDetector[] = [];
18
+ private pluginContext?: PluginContext;
19
+
20
+ constructor(config: ArchitectConfig) {
21
+ this.config = config;
22
+ this.dependencyGraph = new Map();
23
+ }
24
+
25
+ public setCustomDetectors(detectors: CustomAntiPatternDetector[]) {
26
+ this.customDetectors = detectors;
27
+ }
28
+
29
+ /**
30
+ * Check if a file path belongs to the project's own source code.
31
+ * Returns false for node_modules, dist, build artifacts, etc.
32
+ */
33
+ private isProjectFile(filePath: string): boolean {
34
+ const normalized = filePath.replace(/\\/g, '/');
35
+ return !AntiPatternDetector.EXCLUDED_PATH_SEGMENTS.some(seg =>
36
+ normalized.includes(seg)
37
+ );
38
+ }
39
+
40
+ async detect(
41
+ fileTree: FileNode,
42
+ dependencies: Map<string, Set<string>>
43
+ ): Promise<AntiPattern[]> {
44
+ this.dependencyGraph = dependencies;
45
+ const patterns: AntiPattern[] = [];
46
+
47
+ patterns.push(...this.detectGodClasses(fileTree));
48
+ patterns.push(...this.detectCircularDependencies());
49
+ patterns.push(...this.detectLeakyAbstractions(fileTree));
50
+ patterns.push(...this.detectFeatureEnvy(fileTree, dependencies));
51
+ patterns.push(...this.detectShotgunSurgery(dependencies));
52
+
53
+ // Execute Enterprise Custom Plugin Detectors
54
+ const context: PluginContext = this.pluginContext || {
55
+ config: this.config,
56
+ projectPath: process.cwd() // Fallback if not injected explicitly
57
+ };
58
+
59
+ for (const detector of this.customDetectors) {
60
+ try {
61
+ const customPatterns = await detector(fileTree, dependencies, context);
62
+ if (Array.isArray(customPatterns)) {
63
+ patterns.push(...customPatterns);
64
+ }
65
+ } catch (err) {
66
+ logger.warn(`[Architect Plugin] A custom rule engine failed during detection: ${(err as Error).message}`);
67
+ }
68
+ }
69
+
70
+ return patterns.sort((a, b) => {
71
+ const severityOrder: Record<string, number> = {
72
+ CRITICAL: 0,
73
+ HIGH: 1,
74
+ MEDIUM: 2,
75
+ LOW: 3,
76
+ };
77
+ return severityOrder[a.severity] - severityOrder[b.severity];
78
+ });
79
+ }
80
+
81
+ private detectGodClasses(node: FileNode): AntiPattern[] {
82
+ const patterns: AntiPattern[] = [];
83
+ const threshold =
84
+ this.config.antiPatterns?.godClass?.linesThreshold || 800; // Increased to 800 for OSS realities
85
+ const methodThreshold =
86
+ this.config.antiPatterns?.godClass?.methodsThreshold || 20; // Increased to 20
87
+
88
+ this.walkFileTree(node, (file) => {
89
+ if (file.type === 'file' && (file.lines || 0) > threshold && this.isProjectFile(file.path)) {
90
+ const methods = this.countMethods(file.path);
91
+ if (methods > methodThreshold) {
92
+ patterns.push({
93
+ name: 'God Class',
94
+ severity: 'CRITICAL',
95
+ location: file.path,
96
+ description: `Class with ${file.lines} lines and ${methods} methods violates single responsibility principle`,
97
+ suggestion:
98
+ 'Consider splitting into smaller, focused classes with specific responsibilities',
99
+ metrics: {
100
+ lines: file.lines || 0,
101
+ methods,
102
+ },
103
+ });
104
+ }
105
+ }
106
+ });
107
+
108
+ return patterns;
109
+ }
110
+
111
+ private detectCircularDependencies(): AntiPattern[] {
112
+ const patterns: AntiPattern[] = [];
113
+ const visited = new Set<string>();
114
+ const recursionStack = new Set<string>();
115
+
116
+ for (const file of this.dependencyGraph.keys()) {
117
+ // Only check cycles starting from project files
118
+ if (!this.isProjectFile(file)) continue;
119
+
120
+ if (!visited.has(file)) {
121
+ const cycle = this.findCycle(file, visited, recursionStack);
122
+ if (cycle && cycle.every(f => this.isProjectFile(f))) {
123
+ patterns.push({
124
+ name: 'Circular Dependency',
125
+ severity: 'HIGH',
126
+ location: cycle.join(' -> '),
127
+ description: `Circular dependency detected: ${cycle.join(' -> ')}`,
128
+ suggestion:
129
+ 'Refactor code to break the circular dependency using dependency injection or intermediate abstractions',
130
+ affectedFiles: cycle,
131
+ });
132
+ }
133
+ }
134
+ }
135
+
136
+ return patterns;
137
+ }
138
+
139
+ private findCycle(
140
+ node: string,
141
+ visited: Set<string>,
142
+ recursionStack: Set<string>
143
+ ): string[] | null {
144
+ visited.add(node);
145
+ recursionStack.add(node);
146
+
147
+ const neighbors = this.dependencyGraph.get(node) || new Set();
148
+ for (const neighbor of neighbors) {
149
+ if (!visited.has(neighbor)) {
150
+ const cycle = this.findCycle(neighbor, visited, recursionStack);
151
+ if (cycle) {
152
+ cycle.unshift(node);
153
+ return cycle;
154
+ }
155
+ } else if (recursionStack.has(neighbor)) {
156
+ return [node, neighbor];
157
+ }
158
+ }
159
+
160
+ recursionStack.delete(node);
161
+ return null;
162
+ }
163
+
164
+ private detectLeakyAbstractions(node: FileNode): AntiPattern[] {
165
+ const patterns: AntiPattern[] = [];
166
+
167
+ this.walkFileTree(node, (file) => {
168
+ if (file.type === 'file' && this.isProjectFile(file.path)) {
169
+ const internalExports = this.countInternalExports(file.path);
170
+ if (internalExports > 5) {
171
+ patterns.push({
172
+ name: 'Leaky Abstraction',
173
+ severity: 'MEDIUM',
174
+ location: file.path,
175
+ description: `Exports ${internalExports} internal types that should be private`,
176
+ suggestion:
177
+ 'Use private/internal access modifiers and facade patterns to hide implementation details',
178
+ metrics: {
179
+ exportedInternalTypes: internalExports,
180
+ },
181
+ });
182
+ }
183
+ }
184
+ });
185
+
186
+ return patterns;
187
+ }
188
+
189
+ private detectFeatureEnvy(
190
+ node: FileNode,
191
+ dependencies: Map<string, Set<string>>
192
+ ): AntiPattern[] {
193
+ const patterns: AntiPattern[] = [];
194
+
195
+ this.walkFileTree(node, (file) => {
196
+ if (file.type === 'file' && this.isProjectFile(file.path)) {
197
+ const externalMethodCalls = (dependencies.get(file.path) || new Set())
198
+ .size;
199
+ const internalMethods = this.countMethods(file.path);
200
+ const name = file.name.toLowerCase();
201
+
202
+ // Skip infrastructure files where external deps are by design
203
+ const isInfraFile =
204
+ name.endsWith('.module.ts') ||
205
+ name.endsWith('.dto.ts') ||
206
+ name.endsWith('.entity.ts') ||
207
+ name.endsWith('.guard.ts') ||
208
+ name.endsWith('.pipe.ts') ||
209
+ name.endsWith('.interceptor.ts') ||
210
+ name.endsWith('.filter.ts') ||
211
+ name.endsWith('.decorator.ts') ||
212
+ name.endsWith('.spec.ts') ||
213
+ name.endsWith('.test.ts') ||
214
+ name.endsWith('-engine.ts') ||
215
+ name.endsWith('-enricher.ts') ||
216
+ name.endsWith('-detector.ts') ||
217
+ file.path.includes('/scripts/');
218
+
219
+ if (!isInfraFile && internalMethods > 0 && externalMethodCalls > internalMethods * 3) {
220
+ patterns.push({
221
+ name: 'Feature Envy',
222
+ severity: 'MEDIUM',
223
+ location: file.path,
224
+ description: `Uses more external methods (${externalMethodCalls}) than internal methods (${internalMethods})`,
225
+ suggestion:
226
+ 'Move functionality closer to where it is used or extract to shared utility',
227
+ metrics: {
228
+ externalMethodCalls,
229
+ internalMethods,
230
+ },
231
+ });
232
+ }
233
+ }
234
+ });
235
+
236
+ return patterns;
237
+ }
238
+
239
+ private detectShotgunSurgery(
240
+ dependencies: Map<string, Set<string>>
241
+ ): AntiPattern[] {
242
+ const patterns: AntiPattern[] = [];
243
+ const threshold =
244
+ this.config.antiPatterns?.shotgunSurgery?.changePropagationThreshold ||
245
+ Math.max(15, Math.ceil(this.dependencyGraph.size * 0.02));
246
+
247
+ for (const [file, dependents] of dependencies) {
248
+ // Only report for project files
249
+ if (!this.isProjectFile(file)) continue;
250
+
251
+ const fileName = file.split('/').pop() || '';
252
+ const isBaseFile = ['index.ts', 'index.js', 'types.ts', 'logger.ts', 'config.ts', 'architect.ts', 'constants.ts', 'interfaces.ts', 'globals.ts'].includes(fileName) ||
253
+ fileName.endsWith('.interface.ts') || fileName.endsWith('.constants.ts') || fileName.endsWith('.type.ts') || fileName.endsWith('.model.ts') || fileName.endsWith('.enum.ts');
254
+ const isExcludedDir = file.includes('tests/') || file.includes('scripts/') || file.includes('adapters/') || file.includes('agent-generator/');
255
+ if (isBaseFile || isExcludedDir) continue;
256
+
257
+ if (dependents.size >= threshold) {
258
+ patterns.push({
259
+ name: 'Shotgun Surgery',
260
+ severity: 'HIGH',
261
+ location: file,
262
+ description: `Changes to this file likely require modifications in ${dependents.size} other files`,
263
+ suggestion:
264
+ 'Refactor to reduce coupling and consolidate related functionality into modules',
265
+ affectedFiles: Array.from(dependents).filter(f => this.isProjectFile(f)),
266
+ metrics: {
267
+ dependentFileCount: dependents.size,
268
+ },
269
+ });
270
+ }
271
+ }
272
+
273
+ return patterns;
274
+ }
275
+
276
+ private countMethods(filePath: string): number {
277
+ try {
278
+ const content = readFileSync(filePath, 'utf-8');
279
+ const methodRegex = /(?:async\s+)?(?:function|public|private|protected|static)\s+\w+\s*\(/g;
280
+ const arrowMethodRegex = /(?:readonly\s+)?\w+\s*=\s*(?:async\s+)?\(/g;
281
+ const matches = content.match(methodRegex);
282
+ const arrowMatches = content.match(arrowMethodRegex);
283
+ return (matches ? matches.length : 0) + (arrowMatches ? arrowMatches.length : 0);
284
+ } catch {
285
+ return 0;
286
+ }
287
+ }
288
+
289
+ private countInternalExports(filePath: string): number {
290
+ try {
291
+ const content = readFileSync(filePath, 'utf-8');
292
+ const internalTypes = [
293
+ '_',
294
+ 'Internal',
295
+ 'Private',
296
+ 'Impl',
297
+ 'Detail',
298
+ ];
299
+ let count = 0;
300
+
301
+ for (const type of internalTypes) {
302
+ const regex = new RegExp(`export\\s+\\w*${type}\\w*`, 'g');
303
+ const matches = content.match(regex);
304
+ count += matches ? matches.length : 0;
305
+ }
306
+
307
+ return count;
308
+ } catch {
309
+ return 0;
310
+ }
311
+ }
312
+
313
+ private walkFileTree(
314
+ node: FileNode,
315
+ callback: (node: FileNode) => void
316
+ ): void {
317
+ callback(node);
318
+ if (node.children) {
319
+ for (const child of node.children) {
320
+ this.walkFileTree(child, callback);
321
+ }
322
+ }
323
+ }
324
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Interface for Abstract Syntax Tree-based code parsers.
3
+ * Replaces regex-based static analysis to improve deterministic import resolution
4
+ * without compiling the project.
5
+ */
6
+ export interface ASTParser {
7
+ /**
8
+ * Initializes the parser engine and loads required language parsers.
9
+ * Can throw an error if the native bindings fail.
10
+ */
11
+ initialize(): Promise<void>;
12
+
13
+ /**
14
+ * Parses the file content and extracts the imported/required module paths.
15
+ *
16
+ * @param content Raw file string content
17
+ * @param filePath Absolute path to the file
18
+ * @returns List of internal dependencies (import paths)
19
+ */
20
+ parseImports(content: string, filePath: string): string[];
21
+ }
@@ -0,0 +1,61 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export class PathResolver {
5
+ private projectRoot: string;
6
+ private tsconfigPaths: Record<string, string[]> = {};
7
+ private initialized = false;
8
+
9
+ constructor(projectRoot: string) {
10
+ this.projectRoot = projectRoot;
11
+ }
12
+
13
+ initialize(): void {
14
+ if (this.initialized) return;
15
+ try {
16
+ this.loadTsConfig();
17
+ // Em futuruos iteratives, carregar pyproject.toml e go.mod aliases
18
+ this.initialized = true;
19
+ } catch {
20
+ // Falhas na leitura de config não devem quebrar o parser
21
+ }
22
+ }
23
+
24
+ private loadTsConfig(): void {
25
+ const tsconfigPath = path.join(this.projectRoot, 'tsconfig.json');
26
+ if (fs.existsSync(tsconfigPath)) {
27
+ const content = fs.readFileSync(tsconfigPath, 'utf8');
28
+
29
+ // Limpeza brutal de comentários JS/TS do JSON
30
+ const cleanContent = content.replace(new RegExp('//.*$', 'gm'), '').replace(new RegExp('/\\\\*[\\\\s\\\\S]*?\\\\*/', 'g'), '');
31
+ const parsed = JSON.parse(cleanContent);
32
+
33
+ if (parsed?.compilerOptions?.paths) {
34
+ this.tsconfigPaths = parsed.compilerOptions.paths;
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Resolve `importPath` baseando-se no dicionário de Aliases.
41
+ * Ex: "@/" mapeia para "src/"
42
+ */
43
+ resolveAlias(importPath: string): string {
44
+ this.initialize();
45
+
46
+ for (const alias in this.tsconfigPaths) {
47
+ // Ex: alias = "@/components/*", targets = ["src/components/*"]
48
+ const cleanAlias = alias.replace('/*', '');
49
+
50
+ if (importPath.startsWith(cleanAlias)) {
51
+ const targets = this.tsconfigPaths[alias];
52
+ if (targets && targets.length > 0) {
53
+ const cleanTarget = targets[0].replace('/*', '');
54
+ return importPath.replace(cleanAlias, cleanTarget);
55
+ }
56
+ }
57
+ }
58
+
59
+ return importPath;
60
+ }
61
+ }