@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.
- package/dist/src/core/analyzer.d.ts +42 -0
- package/dist/src/core/analyzer.js +431 -0
- package/dist/src/core/analyzer.js.map +1 -0
- package/dist/src/core/analyzers/forecast.d.ts +84 -0
- package/dist/src/core/analyzers/forecast.js +338 -0
- package/dist/src/core/analyzers/forecast.js.map +1 -0
- package/dist/src/core/analyzers/index.d.ts +9 -0
- package/dist/src/core/analyzers/index.js +7 -0
- package/dist/src/core/analyzers/index.js.map +1 -0
- package/dist/src/core/analyzers/temporal-scorer.d.ts +71 -0
- package/dist/src/core/analyzers/temporal-scorer.js +141 -0
- package/dist/src/core/analyzers/temporal-scorer.js.map +1 -0
- package/dist/src/core/anti-patterns.d.ts +28 -0
- package/dist/src/core/anti-patterns.js +264 -0
- package/dist/src/core/anti-patterns.js.map +1 -0
- package/dist/src/core/ast/ast-parser.interface.d.ts +20 -0
- package/dist/src/core/ast/ast-parser.interface.js +2 -0
- package/dist/src/core/ast/ast-parser.interface.js.map +1 -0
- package/dist/src/core/ast/path-resolver.d.ts +13 -0
- package/dist/src/core/ast/path-resolver.js +54 -0
- package/dist/src/core/ast/path-resolver.js.map +1 -0
- package/dist/src/core/ast/tree-sitter-parser.d.ts +10 -0
- package/dist/src/core/ast/tree-sitter-parser.js +142 -0
- package/dist/src/core/ast/tree-sitter-parser.js.map +1 -0
- package/dist/src/core/config.d.ts +11 -0
- package/dist/src/core/config.js +112 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/diagram.d.ts +9 -0
- package/dist/src/core/diagram.js +101 -0
- package/dist/src/core/diagram.js.map +1 -0
- package/dist/src/core/i18n.d.ts +14 -0
- package/dist/src/core/i18n.js +54 -0
- package/dist/src/core/i18n.js.map +1 -0
- package/dist/src/core/locales/en.d.ts +2 -0
- package/dist/src/core/locales/en.js +337 -0
- package/dist/src/core/locales/en.js.map +1 -0
- package/dist/src/core/locales/pt-BR.d.ts +172 -0
- package/dist/src/core/locales/pt-BR.js +337 -0
- package/dist/src/core/locales/pt-BR.js.map +1 -0
- package/dist/src/core/locales/types.d.ts +86 -0
- package/dist/src/core/locales/types.js +2 -0
- package/dist/src/core/locales/types.js.map +1 -0
- package/dist/src/core/plugin-loader.d.ts +11 -0
- package/dist/src/core/plugin-loader.js +67 -0
- package/dist/src/core/plugin-loader.js.map +1 -0
- package/dist/src/core/project-summarizer.d.ts +16 -0
- package/dist/src/core/project-summarizer.js +37 -0
- package/dist/src/core/project-summarizer.js.map +1 -0
- package/dist/src/core/refactor-engine.d.ts +18 -0
- package/dist/src/core/refactor-engine.js +87 -0
- package/dist/src/core/refactor-engine.js.map +1 -0
- package/dist/src/core/rules/barrel-optimizer.d.ts +13 -0
- package/dist/src/core/rules/barrel-optimizer.js +76 -0
- package/dist/src/core/rules/barrel-optimizer.js.map +1 -0
- package/dist/src/core/rules/dead-code-detector.d.ts +21 -0
- package/dist/src/core/rules/dead-code-detector.js +116 -0
- package/dist/src/core/rules/dead-code-detector.js.map +1 -0
- package/dist/src/core/rules/hub-splitter.d.ts +13 -0
- package/dist/src/core/rules/hub-splitter.js +117 -0
- package/dist/src/core/rules/hub-splitter.js.map +1 -0
- package/dist/src/core/rules/import-organizer.d.ts +13 -0
- package/dist/src/core/rules/import-organizer.js +84 -0
- package/dist/src/core/rules/import-organizer.js.map +1 -0
- package/dist/src/core/rules/module-grouper.d.ts +13 -0
- package/dist/src/core/rules/module-grouper.js +116 -0
- package/dist/src/core/rules/module-grouper.js.map +1 -0
- package/dist/src/core/rules-engine.d.ts +7 -0
- package/dist/src/core/rules-engine.js +89 -0
- package/dist/src/core/rules-engine.js.map +1 -0
- package/dist/src/core/scorer.d.ts +15 -0
- package/dist/src/core/scorer.js +165 -0
- package/dist/src/core/scorer.js.map +1 -0
- package/dist/src/core/summarizer/keyword-extractor.d.ts +6 -0
- package/dist/src/core/summarizer/keyword-extractor.js +38 -0
- package/dist/src/core/summarizer/keyword-extractor.js.map +1 -0
- package/dist/src/core/summarizer/module-inferrer.d.ts +11 -0
- package/dist/src/core/summarizer/module-inferrer.js +171 -0
- package/dist/src/core/summarizer/module-inferrer.js.map +1 -0
- package/dist/src/core/summarizer/package-reader.d.ts +3 -0
- package/dist/src/core/summarizer/package-reader.js +33 -0
- package/dist/src/core/summarizer/package-reader.js.map +1 -0
- package/dist/src/core/summarizer/purpose-inferrer.d.ts +8 -0
- package/dist/src/core/summarizer/purpose-inferrer.js +179 -0
- package/dist/src/core/summarizer/purpose-inferrer.js.map +1 -0
- package/dist/src/core/summarizer/readme-reader.d.ts +3 -0
- package/dist/src/core/summarizer/readme-reader.js +24 -0
- package/dist/src/core/summarizer/readme-reader.js.map +1 -0
- package/dist/src/core/types/architect-rules.d.ts +27 -0
- package/dist/src/core/types/architect-rules.js +2 -0
- package/dist/src/core/types/architect-rules.js.map +1 -0
- package/dist/src/core/types/core.d.ts +87 -0
- package/dist/src/core/types/core.js +2 -0
- package/dist/src/core/types/core.js.map +1 -0
- package/dist/src/core/types/infrastructure.d.ts +38 -0
- package/dist/src/core/types/infrastructure.js +2 -0
- package/dist/src/core/types/infrastructure.js.map +1 -0
- package/dist/src/core/types/plugin.d.ts +12 -0
- package/dist/src/core/types/plugin.js +2 -0
- package/dist/src/core/types/plugin.js.map +1 -0
- package/dist/src/core/types/rules.d.ts +53 -0
- package/dist/src/core/types/rules.js +2 -0
- package/dist/src/core/types/rules.js.map +1 -0
- package/dist/src/core/types/summarizer.d.ts +12 -0
- package/dist/src/core/types/summarizer.js +2 -0
- package/dist/src/core/types/summarizer.js.map +1 -0
- package/dist/src/infrastructure/git-cache.d.ts +6 -0
- package/dist/src/infrastructure/git-cache.js +41 -0
- package/dist/src/infrastructure/git-cache.js.map +1 -0
- package/dist/src/infrastructure/git-history.d.ts +112 -0
- package/dist/src/infrastructure/git-history.js +340 -0
- package/dist/src/infrastructure/git-history.js.map +1 -0
- package/dist/src/infrastructure/logger.d.ts +20 -0
- package/dist/src/infrastructure/logger.js +57 -0
- package/dist/src/infrastructure/logger.js.map +1 -0
- package/dist/src/infrastructure/scanner.d.ts +31 -0
- package/dist/src/infrastructure/scanner.js +334 -0
- package/dist/src/infrastructure/scanner.js.map +1 -0
- package/dist/tests/analyzers-integration.test.d.ts +7 -0
- package/dist/tests/analyzers-integration.test.js +140 -0
- package/dist/tests/analyzers-integration.test.js.map +1 -0
- package/dist/tests/anti-patterns.test.d.ts +1 -0
- package/dist/tests/anti-patterns.test.js +81 -0
- package/dist/tests/anti-patterns.test.js.map +1 -0
- package/dist/tests/ast-parser.test.d.ts +1 -0
- package/dist/tests/ast-parser.test.js +94 -0
- package/dist/tests/ast-parser.test.js.map +1 -0
- package/dist/tests/fixtures/monorepo/packages/app/src/index.d.ts +1 -0
- package/dist/tests/fixtures/monorepo/packages/app/src/index.js +9 -0
- package/dist/tests/fixtures/monorepo/packages/app/src/index.js.map +1 -0
- package/dist/tests/fixtures/monorepo/packages/core/src/index.d.ts +2 -0
- package/dist/tests/fixtures/monorepo/packages/core/src/index.js +11 -0
- package/dist/tests/fixtures/monorepo/packages/core/src/index.js.map +1 -0
- package/dist/tests/forecast.test.d.ts +7 -0
- package/dist/tests/forecast.test.js +380 -0
- package/dist/tests/forecast.test.js.map +1 -0
- package/dist/tests/git-history.test.d.ts +7 -0
- package/dist/tests/git-history.test.js +193 -0
- package/dist/tests/git-history.test.js.map +1 -0
- package/dist/tests/i18n.test.d.ts +1 -0
- package/dist/tests/i18n.test.js +39 -0
- package/dist/tests/i18n.test.js.map +1 -0
- package/dist/tests/monorepo-scan.test.d.ts +11 -0
- package/dist/tests/monorepo-scan.test.js +143 -0
- package/dist/tests/monorepo-scan.test.js.map +1 -0
- package/dist/tests/plugin-loader.test.d.ts +1 -0
- package/dist/tests/plugin-loader.test.js +31 -0
- package/dist/tests/plugin-loader.test.js.map +1 -0
- package/dist/tests/rules-engine.test.d.ts +1 -0
- package/dist/tests/rules-engine.test.js +112 -0
- package/dist/tests/rules-engine.test.js.map +1 -0
- package/dist/tests/scanner.test.d.ts +1 -0
- package/dist/tests/scanner.test.js +44 -0
- package/dist/tests/scanner.test.js.map +1 -0
- package/dist/tests/scorer.test.d.ts +1 -0
- package/dist/tests/scorer.test.js +610 -0
- package/dist/tests/scorer.test.js.map +1 -0
- package/dist/tests/temporal-scorer.test.d.ts +7 -0
- package/dist/tests/temporal-scorer.test.js +239 -0
- package/dist/tests/temporal-scorer.test.js.map +1 -0
- package/package.json +29 -0
- package/src/core/analyzer.ts +499 -0
- package/src/core/analyzers/forecast.ts +497 -0
- package/src/core/analyzers/index.ts +33 -0
- package/src/core/analyzers/temporal-scorer.ts +227 -0
- package/src/core/anti-patterns.ts +324 -0
- package/src/core/ast/ast-parser.interface.ts +21 -0
- package/src/core/ast/path-resolver.ts +61 -0
- package/src/core/ast/tree-sitter-parser.ts +158 -0
- package/src/core/config.ts +125 -0
- package/src/core/diagram.ts +129 -0
- package/src/core/i18n.ts +64 -0
- package/src/core/locales/en.ts +340 -0
- package/src/core/locales/pt-BR.ts +341 -0
- package/src/core/locales/types.ts +95 -0
- package/src/core/plugin-loader.ts +80 -0
- package/src/core/project-summarizer.ts +42 -0
- package/src/core/refactor-engine.ts +112 -0
- package/src/core/rules/barrel-optimizer.ts +99 -0
- package/src/core/rules/dead-code-detector.ts +134 -0
- package/src/core/rules/hub-splitter.ts +135 -0
- package/src/core/rules/import-organizer.ts +100 -0
- package/src/core/rules/module-grouper.ts +133 -0
- package/src/core/rules-engine.ts +100 -0
- package/src/core/scorer.ts +181 -0
- package/src/core/summarizer/keyword-extractor.ts +53 -0
- package/src/core/summarizer/module-inferrer.ts +194 -0
- package/src/core/summarizer/package-reader.ts +34 -0
- package/src/core/summarizer/purpose-inferrer.ts +197 -0
- package/src/core/summarizer/readme-reader.ts +24 -0
- package/src/core/types/architect-rules.ts +29 -0
- package/src/core/types/core.ts +94 -0
- package/src/core/types/infrastructure.ts +41 -0
- package/src/core/types/plugin.ts +19 -0
- package/src/core/types/rules.ts +51 -0
- package/src/core/types/summarizer.ts +8 -0
- package/src/infrastructure/git-cache.ts +52 -0
- package/src/infrastructure/git-history.ts +496 -0
- package/src/infrastructure/logger.ts +68 -0
- package/src/infrastructure/scanner.ts +349 -0
- package/tests/analyzers-integration.test.ts +174 -0
- package/tests/anti-patterns.test.ts +95 -0
- package/tests/ast-parser.test.ts +102 -0
- package/tests/fixtures/monorepo/package.json +6 -0
- package/tests/fixtures/monorepo/packages/app/package.json +12 -0
- package/tests/fixtures/monorepo/packages/app/src/index.ts +6 -0
- package/tests/fixtures/monorepo/packages/core/package.json +7 -0
- package/tests/fixtures/monorepo/packages/core/src/index.ts +7 -0
- package/tests/forecast.test.ts +504 -0
- package/tests/git-history.test.ts +254 -0
- package/tests/i18n.test.ts +47 -0
- package/tests/monorepo-scan.test.ts +170 -0
- package/tests/plugin-loader.test.ts +40 -0
- package/tests/rules-engine.test.ts +131 -0
- package/tests/scanner.test.ts +54 -0
- package/tests/scorer.test.ts +675 -0
- package/tests/temporal-scorer.test.ts +306 -0
- 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
|
+
}
|