@girardelli/architect 1.2.0 → 1.3.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/README.md +3 -3
- package/dist/html-reporter.d.ts +18 -2
- package/dist/html-reporter.d.ts.map +1 -1
- package/dist/html-reporter.js +366 -32
- package/dist/html-reporter.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -7
- package/dist/index.js.map +1 -1
- package/dist/scorer.d.ts +12 -0
- package/dist/scorer.d.ts.map +1 -1
- package/dist/scorer.js +61 -17
- package/dist/scorer.js.map +1 -1
- package/package.json +1 -1
- package/src/html-reporter.ts +380 -33
- package/src/index.ts +69 -8
- package/src/scorer.ts +63 -17
package/src/scorer.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ArchitectureScore, DependencyEdge, AntiPattern, ScoreComponent } from './types.js';
|
|
2
|
+
import { basename } from 'path';
|
|
2
3
|
|
|
3
4
|
export class ArchitectureScorer {
|
|
4
5
|
private modularity: number = 0;
|
|
@@ -6,6 +7,15 @@ export class ArchitectureScorer {
|
|
|
6
7
|
private cohesion: number = 0;
|
|
7
8
|
private layering: number = 0;
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Barrel/index files that naturally have many connections and should be
|
|
12
|
+
* excluded from coupling max-edge penalty calculations.
|
|
13
|
+
*/
|
|
14
|
+
private static readonly BARREL_FILES = new Set([
|
|
15
|
+
'__init__.py', 'index.ts', 'index.js', 'index.tsx', 'index.jsx',
|
|
16
|
+
'mod.rs', '__init__.pyi',
|
|
17
|
+
]);
|
|
18
|
+
|
|
9
19
|
score(
|
|
10
20
|
edges: DependencyEdge[],
|
|
11
21
|
antiPatterns: AntiPattern[],
|
|
@@ -91,26 +101,42 @@ export class ArchitectureScorer {
|
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
private calculateCoupling(edges: DependencyEdge[], totalFiles: number): void {
|
|
94
|
-
if (totalFiles === 0) {
|
|
104
|
+
if (totalFiles === 0 || totalFiles === 1) {
|
|
95
105
|
this.coupling = 50;
|
|
96
106
|
return;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
|
|
109
|
+
// Exclude barrel/index files from max-edge calculation —
|
|
110
|
+
// they naturally have many connections by design.
|
|
111
|
+
const nonBarrelEdges = edges.filter((e) => {
|
|
112
|
+
const fromFile = basename(e.from);
|
|
113
|
+
const toFile = basename(e.to);
|
|
114
|
+
return !ArchitectureScorer.BARREL_FILES.has(fromFile) &&
|
|
115
|
+
!ArchitectureScorer.BARREL_FILES.has(toFile);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const nodeWithMaxEdges = this.findNodeWithMaxEdges(nonBarrelEdges);
|
|
100
119
|
const maxEdgeCount = nodeWithMaxEdges ? nodeWithMaxEdges.count : 0;
|
|
101
120
|
|
|
102
|
-
|
|
121
|
+
// Use non-barrel file count for ratio calculation
|
|
122
|
+
const effectiveFiles = Math.max(totalFiles - 1, 1);
|
|
123
|
+
const couplingRatio = maxEdgeCount / effectiveFiles;
|
|
103
124
|
|
|
104
|
-
|
|
125
|
+
// More granular thresholds
|
|
126
|
+
if (couplingRatio < 0.15) {
|
|
105
127
|
this.coupling = 95;
|
|
106
|
-
} else if (couplingRatio < 0.
|
|
128
|
+
} else if (couplingRatio < 0.25) {
|
|
107
129
|
this.coupling = 85;
|
|
108
|
-
} else if (couplingRatio < 0.
|
|
109
|
-
this.coupling =
|
|
110
|
-
} else if (couplingRatio < 0.
|
|
130
|
+
} else if (couplingRatio < 0.35) {
|
|
131
|
+
this.coupling = 75;
|
|
132
|
+
} else if (couplingRatio < 0.5) {
|
|
133
|
+
this.coupling = 65;
|
|
134
|
+
} else if (couplingRatio < 0.7) {
|
|
111
135
|
this.coupling = 50;
|
|
136
|
+
} else if (couplingRatio < 0.85) {
|
|
137
|
+
this.coupling = 35;
|
|
112
138
|
} else {
|
|
113
|
-
this.coupling =
|
|
139
|
+
this.coupling = 20;
|
|
114
140
|
}
|
|
115
141
|
}
|
|
116
142
|
|
|
@@ -149,23 +175,42 @@ export class ArchitectureScorer {
|
|
|
149
175
|
|
|
150
176
|
const cohesionRatio = internalEdges / edges.length;
|
|
151
177
|
|
|
152
|
-
|
|
178
|
+
// More granular thresholds
|
|
179
|
+
if (cohesionRatio > 0.8) {
|
|
153
180
|
this.cohesion = 95;
|
|
154
|
-
} else if (cohesionRatio > 0.
|
|
155
|
-
this.cohesion =
|
|
181
|
+
} else if (cohesionRatio > 0.6) {
|
|
182
|
+
this.cohesion = 85;
|
|
183
|
+
} else if (cohesionRatio > 0.45) {
|
|
184
|
+
this.cohesion = 75;
|
|
156
185
|
} else if (cohesionRatio > 0.3) {
|
|
157
186
|
this.cohesion = 65;
|
|
158
|
-
} else if (cohesionRatio > 0.
|
|
159
|
-
this.cohesion =
|
|
187
|
+
} else if (cohesionRatio > 0.15) {
|
|
188
|
+
this.cohesion = 50;
|
|
160
189
|
} else {
|
|
161
190
|
this.cohesion = 30;
|
|
162
191
|
}
|
|
163
192
|
}
|
|
164
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Determines if a dependency is "internal" (cohesive).
|
|
196
|
+
* Two files are considered cohesive if they share the same top-level
|
|
197
|
+
* package/directory (e.g., deepguard/cli.py → deepguard/analyzer.py).
|
|
198
|
+
* This is crucial for Python flat packages where all files live in
|
|
199
|
+
* one directory but ARE cohesive.
|
|
200
|
+
*/
|
|
165
201
|
private isInternalDependency(from: string, to: string): boolean {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
202
|
+
const fromParts = from.split('/');
|
|
203
|
+
const toParts = to.split('/');
|
|
204
|
+
|
|
205
|
+
// If both are in root (no directory), they're cohesive
|
|
206
|
+
if (fromParts.length <= 1 && toParts.length <= 1) return true;
|
|
207
|
+
|
|
208
|
+
// Compare top-level directory (package name)
|
|
209
|
+
// e.g., "deepguard/cli.py" and "deepguard/analyzer.py" → same package
|
|
210
|
+
const fromTopLevel = fromParts.length > 1 ? fromParts[0] : '';
|
|
211
|
+
const toTopLevel = toParts.length > 1 ? toParts[0] : '';
|
|
212
|
+
|
|
213
|
+
return fromTopLevel === toTopLevel;
|
|
169
214
|
}
|
|
170
215
|
|
|
171
216
|
private calculateLayering(antiPatterns: AntiPattern[]): void {
|
|
@@ -191,3 +236,4 @@ export class ArchitectureScorer {
|
|
|
191
236
|
}
|
|
192
237
|
}
|
|
193
238
|
}
|
|
239
|
+
|