@safetnsr/vet 1.19.2 → 1.20.1
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/checks/architecture.js +139 -1
- package/dist/checks/deep.js +145 -7
- package/dist/checks/deps.js +15 -0
- package/dist/checks/semantic.js +1 -1
- package/package.json +1 -1
|
@@ -109,6 +109,116 @@ function findCycles(moduleGraph) {
|
|
|
109
109
|
return true;
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
|
+
function louvainCommunities(graph) {
|
|
113
|
+
const nodes = Array.from(graph.keys());
|
|
114
|
+
if (nodes.length < 2) {
|
|
115
|
+
return { modularity: 0, communities: new Map(), communityCount: 0 };
|
|
116
|
+
}
|
|
117
|
+
// Build undirected weighted adjacency (edge count = weight)
|
|
118
|
+
const adj = new Map();
|
|
119
|
+
for (const n of nodes) {
|
|
120
|
+
adj.set(n, new Map());
|
|
121
|
+
}
|
|
122
|
+
let totalEdges = 0;
|
|
123
|
+
for (const [from, deps] of graph) {
|
|
124
|
+
for (const to of deps) {
|
|
125
|
+
if (!adj.has(to)) {
|
|
126
|
+
adj.set(to, new Map());
|
|
127
|
+
}
|
|
128
|
+
// Undirected: add both directions
|
|
129
|
+
const w = (adj.get(from).get(to) || 0) + 1;
|
|
130
|
+
adj.get(from).set(to, w);
|
|
131
|
+
adj.get(to).set(from, w);
|
|
132
|
+
totalEdges++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (totalEdges === 0) {
|
|
136
|
+
return { modularity: 0, communities: new Map(), communityCount: 0 };
|
|
137
|
+
}
|
|
138
|
+
const m = totalEdges; // total edge weight
|
|
139
|
+
const m2 = 2 * m;
|
|
140
|
+
// Degree of each node (sum of edge weights)
|
|
141
|
+
const degree = new Map();
|
|
142
|
+
for (const [n, neighbors] of adj) {
|
|
143
|
+
let d = 0;
|
|
144
|
+
for (const w of neighbors.values())
|
|
145
|
+
d += w;
|
|
146
|
+
degree.set(n, d);
|
|
147
|
+
}
|
|
148
|
+
// Initialize: each node in its own community
|
|
149
|
+
const community = new Map();
|
|
150
|
+
nodes.forEach((n, i) => community.set(n, i));
|
|
151
|
+
// Community totals: sum of degrees in each community
|
|
152
|
+
const commDegree = new Map();
|
|
153
|
+
const commInternal = new Map(); // sum of internal edges × 2
|
|
154
|
+
for (const [n, c] of community) {
|
|
155
|
+
commDegree.set(c, degree.get(n) || 0);
|
|
156
|
+
commInternal.set(c, 0);
|
|
157
|
+
}
|
|
158
|
+
// Iterative optimization (1 pass — good enough for code graphs)
|
|
159
|
+
let moved = true;
|
|
160
|
+
let iterations = 0;
|
|
161
|
+
while (moved && iterations < 10) {
|
|
162
|
+
moved = false;
|
|
163
|
+
iterations++;
|
|
164
|
+
for (const node of nodes) {
|
|
165
|
+
const nodeDeg = degree.get(node) || 0;
|
|
166
|
+
const currentComm = community.get(node);
|
|
167
|
+
// Calculate edges to each neighboring community
|
|
168
|
+
const commEdges = new Map();
|
|
169
|
+
const neighbors = adj.get(node) || new Map();
|
|
170
|
+
for (const [neighbor, weight] of neighbors) {
|
|
171
|
+
const nc = community.get(neighbor);
|
|
172
|
+
if (nc !== undefined) {
|
|
173
|
+
commEdges.set(nc, (commEdges.get(nc) || 0) + weight);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Modularity gain for moving node to community c:
|
|
177
|
+
// ΔQ = [Σin + 2*ki,in] / 2m - [(Σtot + ki) / 2m]² - [Σin/2m - (Σtot/2m)² - (ki/2m)²]
|
|
178
|
+
let bestComm = currentComm;
|
|
179
|
+
let bestGain = 0;
|
|
180
|
+
// Remove node from current community
|
|
181
|
+
const edgesToCurrent = commEdges.get(currentComm) || 0;
|
|
182
|
+
for (const [targetComm, edgesToTarget] of commEdges) {
|
|
183
|
+
if (targetComm === currentComm)
|
|
184
|
+
continue;
|
|
185
|
+
const sigmaTot = commDegree.get(targetComm) || 0;
|
|
186
|
+
const sigmaIn = commInternal.get(targetComm) || 0;
|
|
187
|
+
// Simplified modularity gain
|
|
188
|
+
const gain = (edgesToTarget / m) - (sigmaTot * nodeDeg) / (m2 * m);
|
|
189
|
+
if (gain > bestGain) {
|
|
190
|
+
bestGain = gain;
|
|
191
|
+
bestComm = targetComm;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (bestComm !== currentComm) {
|
|
195
|
+
// Move node
|
|
196
|
+
commDegree.set(currentComm, (commDegree.get(currentComm) || 0) - nodeDeg);
|
|
197
|
+
commInternal.set(currentComm, (commInternal.get(currentComm) || 0) - 2 * edgesToCurrent);
|
|
198
|
+
commDegree.set(bestComm, (commDegree.get(bestComm) || 0) + nodeDeg);
|
|
199
|
+
const edgesToBest = commEdges.get(bestComm) || 0;
|
|
200
|
+
commInternal.set(bestComm, (commInternal.get(bestComm) || 0) + 2 * edgesToBest);
|
|
201
|
+
community.set(node, bestComm);
|
|
202
|
+
moved = true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Calculate final modularity Q
|
|
207
|
+
let Q = 0;
|
|
208
|
+
for (const [n1, neighbors] of adj) {
|
|
209
|
+
for (const [n2, w] of neighbors) {
|
|
210
|
+
if (community.get(n1) === community.get(n2)) {
|
|
211
|
+
const d1 = degree.get(n1) || 0;
|
|
212
|
+
const d2 = degree.get(n2) || 0;
|
|
213
|
+
Q += w - (d1 * d2) / m2;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
Q /= m2;
|
|
218
|
+
// Count unique communities
|
|
219
|
+
const uniqueComms = new Set(community.values());
|
|
220
|
+
return { modularity: Q, communities: community, communityCount: uniqueComms.size };
|
|
221
|
+
}
|
|
112
222
|
// ── Main check ───────────────────────────────────────────────────────────────
|
|
113
223
|
export function checkArchitecture(cwd) {
|
|
114
224
|
const allFiles = walkFiles(cwd);
|
|
@@ -258,6 +368,28 @@ export function checkArchitecture(cwd) {
|
|
|
258
368
|
fixHint: 'use barrel exports (index.ts) to define module boundaries',
|
|
259
369
|
});
|
|
260
370
|
}
|
|
371
|
+
// ── Louvain modularity ─────────────────────────────────────────────────
|
|
372
|
+
const louvain = louvainCommunities(moduleGraph);
|
|
373
|
+
if (moduleGraph.size >= 3) {
|
|
374
|
+
if (louvain.modularity < 0.3 && louvain.communityCount > 1) {
|
|
375
|
+
issues.push({
|
|
376
|
+
severity: 'warning',
|
|
377
|
+
message: `low modularity: Q=${louvain.modularity.toFixed(2)} (${louvain.communityCount} communities detected) — modules are too interconnected`,
|
|
378
|
+
file: '',
|
|
379
|
+
fixable: true,
|
|
380
|
+
fixHint: 'reduce cross-module dependencies, group related files into cohesive modules',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
else if (louvain.communityCount <= 1 && moduleGraph.size > 5) {
|
|
384
|
+
issues.push({
|
|
385
|
+
severity: 'info',
|
|
386
|
+
message: `monolithic structure: all ${moduleGraph.size} modules form a single community — no clear architectural boundaries`,
|
|
387
|
+
file: '',
|
|
388
|
+
fixable: true,
|
|
389
|
+
fixHint: 'introduce module boundaries with clear interfaces (barrel exports)',
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
261
393
|
// ── Scoring ───────────────────────────────────────────────────────────────
|
|
262
394
|
const moduleCount = moduleGraph.size;
|
|
263
395
|
const sizeScale = moduleCount <= 5 ? 1.0 : Math.max(0.3, 1.0 - Math.log10(moduleCount / 5) * 0.3);
|
|
@@ -266,11 +398,17 @@ export function checkArchitecture(cwd) {
|
|
|
266
398
|
score -= Math.min(20, godFiles.length * 10) * sizeScale;
|
|
267
399
|
score -= Math.min(15, Array.from(instability.values()).filter(i => i > 0.8).length * 5) * sizeScale;
|
|
268
400
|
score -= Math.min(10, Math.floor(boundaryViolations / 3) * 3) * sizeScale;
|
|
401
|
+
// Modularity penalty: low Q on non-trivial graphs
|
|
402
|
+
if (moduleGraph.size >= 5 && louvain.modularity < 0.3) {
|
|
403
|
+
score -= Math.min(15, Math.round((0.3 - louvain.modularity) * 50));
|
|
404
|
+
}
|
|
269
405
|
score = Math.max(25, Math.round(score));
|
|
270
406
|
// ── Summary ───────────────────────────────────────────────────────────────
|
|
271
407
|
const parts = [];
|
|
272
408
|
parts.push(`${moduleGraph.size} modules`);
|
|
273
|
-
parts.push(`${edges.length}
|
|
409
|
+
parts.push(`${edges.length} edges`);
|
|
410
|
+
if (louvain.communityCount > 0)
|
|
411
|
+
parts.push(`Q=${louvain.modularity.toFixed(2)} (${louvain.communityCount} communities)`);
|
|
274
412
|
if (cycles.length > 0)
|
|
275
413
|
parts.push(c.red + `${cycles.length} circular dep${cycles.length !== 1 ? 's' : ''}` + c.reset);
|
|
276
414
|
if (godFiles.length > 0)
|
package/dist/checks/deep.js
CHANGED
|
@@ -26,10 +26,16 @@ function analyzeCatch(node) {
|
|
|
26
26
|
const block = node.block;
|
|
27
27
|
const stmts = block.statements;
|
|
28
28
|
const line = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
29
|
+
const text = block.getText();
|
|
29
30
|
if (stmts.length === 0) {
|
|
31
|
+
// Check if there's a deliberate comment (/* skip */, /* ignore */, etc.)
|
|
32
|
+
const hasComment = /\/[/*]\s*(skip|ignore|noop|intentional|expected|ok|no-op)/i.test(text);
|
|
33
|
+
if (hasComment) {
|
|
34
|
+
// Deliberate empty catch — not a bug
|
|
35
|
+
return { line, isEmpty: false, isLazy: false, isRethrow: false };
|
|
36
|
+
}
|
|
30
37
|
return { line, isEmpty: true, isLazy: false, isRethrow: false };
|
|
31
38
|
}
|
|
32
|
-
const text = block.getText();
|
|
33
39
|
const isLazy = stmts.length === 1 && /console\.(log|error|warn)\s*\(/.test(text) && !text.includes('throw');
|
|
34
40
|
const isRethrow = text.includes('throw');
|
|
35
41
|
return { line, isEmpty: false, isLazy, isRethrow };
|
|
@@ -110,6 +116,98 @@ function analyzeFunction(node, file, src) {
|
|
|
110
116
|
cognitiveComplexity: cognitive,
|
|
111
117
|
};
|
|
112
118
|
}
|
|
119
|
+
// ── Halstead metrics + Maintainability Index ────────────────────────────────
|
|
120
|
+
const OPERATOR_KINDS = new Set([
|
|
121
|
+
ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken, ts.SyntaxKind.AsteriskToken,
|
|
122
|
+
ts.SyntaxKind.SlashToken, ts.SyntaxKind.PercentToken, ts.SyntaxKind.EqualsToken,
|
|
123
|
+
ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.MinusEqualsToken,
|
|
124
|
+
ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.SlashEqualsToken,
|
|
125
|
+
ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
126
|
+
ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
127
|
+
ts.SyntaxKind.LessThanToken, ts.SyntaxKind.GreaterThanToken,
|
|
128
|
+
ts.SyntaxKind.LessThanEqualsToken, ts.SyntaxKind.GreaterThanEqualsToken,
|
|
129
|
+
ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken,
|
|
130
|
+
ts.SyntaxKind.ExclamationToken, ts.SyntaxKind.QuestionQuestionToken,
|
|
131
|
+
ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken,
|
|
132
|
+
ts.SyntaxKind.AmpersandToken, ts.SyntaxKind.BarToken,
|
|
133
|
+
ts.SyntaxKind.CaretToken, ts.SyntaxKind.TildeToken,
|
|
134
|
+
ts.SyntaxKind.DotDotDotToken,
|
|
135
|
+
]);
|
|
136
|
+
const KEYWORD_OPERATOR_KINDS = new Set([
|
|
137
|
+
ts.SyntaxKind.IfStatement, ts.SyntaxKind.ElseKeyword,
|
|
138
|
+
ts.SyntaxKind.ForStatement, ts.SyntaxKind.ForInStatement, ts.SyntaxKind.ForOfStatement,
|
|
139
|
+
ts.SyntaxKind.WhileStatement, ts.SyntaxKind.DoStatement,
|
|
140
|
+
ts.SyntaxKind.SwitchStatement, ts.SyntaxKind.CaseClause,
|
|
141
|
+
ts.SyntaxKind.ReturnStatement, ts.SyntaxKind.ThrowStatement,
|
|
142
|
+
ts.SyntaxKind.TryStatement, ts.SyntaxKind.CatchClause,
|
|
143
|
+
ts.SyntaxKind.NewExpression, ts.SyntaxKind.DeleteExpression,
|
|
144
|
+
ts.SyntaxKind.TypeOfExpression, ts.SyntaxKind.VoidExpression,
|
|
145
|
+
ts.SyntaxKind.AwaitExpression, ts.SyntaxKind.YieldExpression,
|
|
146
|
+
]);
|
|
147
|
+
function computeHalstead(src) {
|
|
148
|
+
const operators = new Map();
|
|
149
|
+
const operands = new Map();
|
|
150
|
+
function countToken(map, key) {
|
|
151
|
+
map.set(key, (map.get(key) || 0) + 1);
|
|
152
|
+
}
|
|
153
|
+
function walk(node) {
|
|
154
|
+
// Operators: binary/unary/assignment operators + keyword operators
|
|
155
|
+
if (ts.isBinaryExpression(node)) {
|
|
156
|
+
countToken(operators, ts.SyntaxKind[node.operatorToken.kind]);
|
|
157
|
+
}
|
|
158
|
+
else if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) {
|
|
159
|
+
countToken(operators, ts.SyntaxKind[node.operator]);
|
|
160
|
+
}
|
|
161
|
+
else if (KEYWORD_OPERATOR_KINDS.has(node.kind)) {
|
|
162
|
+
countToken(operators, ts.SyntaxKind[node.kind]);
|
|
163
|
+
}
|
|
164
|
+
else if (ts.isCallExpression(node)) {
|
|
165
|
+
countToken(operators, 'Call');
|
|
166
|
+
}
|
|
167
|
+
else if (ts.isPropertyAccessExpression(node)) {
|
|
168
|
+
countToken(operators, 'PropertyAccess');
|
|
169
|
+
}
|
|
170
|
+
// Operands: identifiers, literals
|
|
171
|
+
if (ts.isIdentifier(node)) {
|
|
172
|
+
countToken(operands, node.text);
|
|
173
|
+
}
|
|
174
|
+
else if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
175
|
+
countToken(operands, `"${node.text.slice(0, 20)}"`);
|
|
176
|
+
}
|
|
177
|
+
else if (ts.isNumericLiteral(node)) {
|
|
178
|
+
countToken(operands, node.text);
|
|
179
|
+
}
|
|
180
|
+
ts.forEachChild(node, walk);
|
|
181
|
+
}
|
|
182
|
+
walk(src);
|
|
183
|
+
const n1 = operators.size;
|
|
184
|
+
const n2 = operands.size;
|
|
185
|
+
const N1 = Array.from(operators.values()).reduce((a, b) => a + b, 0);
|
|
186
|
+
const N2 = Array.from(operands.values()).reduce((a, b) => a + b, 0);
|
|
187
|
+
const n = n1 + n2;
|
|
188
|
+
const N = N1 + N2;
|
|
189
|
+
const volume = n > 0 ? N * Math.log2(n) : 0;
|
|
190
|
+
const difficulty = n2 > 0 ? (n1 / 2) * (N2 / n2) : 0;
|
|
191
|
+
const effort = difficulty * volume;
|
|
192
|
+
return {
|
|
193
|
+
operators: N1, operands: N2,
|
|
194
|
+
uniqueOperators: n1, uniqueOperands: n2,
|
|
195
|
+
vocabulary: n, length: N,
|
|
196
|
+
volume, difficulty, effort,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function computeMI(halsteadVolume, cyclomatic, sloc) {
|
|
200
|
+
// Standard Maintainability Index formula (SEI, 1992)
|
|
201
|
+
// MI = 171 - 5.2 × ln(V) - 0.23 × CC - 16.2 × ln(SLOC)
|
|
202
|
+
// Clamped to [0, 100]
|
|
203
|
+
if (halsteadVolume <= 0 || sloc <= 0)
|
|
204
|
+
return 100;
|
|
205
|
+
const mi = 171
|
|
206
|
+
- 5.2 * Math.log(halsteadVolume)
|
|
207
|
+
- 0.23 * cyclomatic
|
|
208
|
+
- 16.2 * Math.log(sloc);
|
|
209
|
+
return Math.max(0, Math.min(100, mi));
|
|
210
|
+
}
|
|
113
211
|
// ── Naming analysis (heuristic, no ML) ──────────────────────────────────────
|
|
114
212
|
function isDescriptiveName(name) {
|
|
115
213
|
if (name.startsWith('('))
|
|
@@ -130,6 +228,7 @@ export async function checkDeep(cwd) {
|
|
|
130
228
|
}
|
|
131
229
|
const issues = [];
|
|
132
230
|
const allMetrics = [];
|
|
231
|
+
const allFileMetrics = [];
|
|
133
232
|
const t0 = Date.now();
|
|
134
233
|
for (const file of sourceFiles) {
|
|
135
234
|
const content = readFile(join(cwd, file));
|
|
@@ -137,16 +236,25 @@ export async function checkDeep(cwd) {
|
|
|
137
236
|
continue;
|
|
138
237
|
try {
|
|
139
238
|
const src = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
|
|
239
|
+
// Per-function analysis
|
|
240
|
+
let fileCyclomatic = 1; // base complexity
|
|
140
241
|
function visit(node) {
|
|
141
242
|
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) ||
|
|
142
243
|
ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
|
|
143
244
|
const metrics = analyzeFunction(node, file, src);
|
|
144
|
-
if (metrics)
|
|
245
|
+
if (metrics) {
|
|
145
246
|
allMetrics.push(metrics);
|
|
247
|
+
fileCyclomatic += metrics.cyclomatic - 1; // add function's complexity
|
|
248
|
+
}
|
|
146
249
|
}
|
|
147
250
|
ts.forEachChild(node, visit);
|
|
148
251
|
}
|
|
149
252
|
visit(src);
|
|
253
|
+
// Per-file: Halstead + MI
|
|
254
|
+
const halstead = computeHalstead(src);
|
|
255
|
+
const sloc = content.split('\n').filter(l => l.trim() && !l.trim().startsWith('//')).length;
|
|
256
|
+
const mi = computeMI(halstead.volume, fileCyclomatic, sloc);
|
|
257
|
+
allFileMetrics.push({ file, sloc, cyclomatic: fileCyclomatic, halstead, maintainabilityIndex: mi });
|
|
150
258
|
}
|
|
151
259
|
catch {
|
|
152
260
|
// Skip files that can't be parsed
|
|
@@ -243,6 +351,27 @@ export async function checkDeep(cwd) {
|
|
|
243
351
|
fixHint: 'use descriptive verb+noun function names (e.g., calculateTotal, validateUser)',
|
|
244
352
|
});
|
|
245
353
|
}
|
|
354
|
+
// ── Maintainability Index ──────────────────────────────────────────────────
|
|
355
|
+
const lowMI = allFileMetrics.filter(f => f.maintainabilityIndex < 20).sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex);
|
|
356
|
+
const mediumMI = allFileMetrics.filter(f => f.maintainabilityIndex >= 20 && f.maintainabilityIndex < 40);
|
|
357
|
+
for (const fm of lowMI.slice(0, 3)) {
|
|
358
|
+
issues.push({
|
|
359
|
+
severity: 'warning',
|
|
360
|
+
message: `low maintainability: ${fm.file} has MI=${fm.maintainabilityIndex.toFixed(0)} (halstead volume ${fm.halstead.volume.toFixed(0)}, CC ${fm.cyclomatic}, ${fm.sloc} SLOC)`,
|
|
361
|
+
file: fm.file,
|
|
362
|
+
fixable: true,
|
|
363
|
+
fixHint: 'reduce complexity and file size — split into smaller modules',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (mediumMI.length > 5) {
|
|
367
|
+
issues.push({
|
|
368
|
+
severity: 'info',
|
|
369
|
+
message: `${mediumMI.length} files with moderate maintainability (MI 20-40)`,
|
|
370
|
+
file: mediumMI[0].file,
|
|
371
|
+
fixable: true,
|
|
372
|
+
fixHint: 'consider refactoring the most complex files',
|
|
373
|
+
});
|
|
374
|
+
}
|
|
246
375
|
// ── Scoring ───────────────────────────────────────────────────────────────
|
|
247
376
|
const total = allMetrics.length;
|
|
248
377
|
// Complexity score: % of functions below threshold
|
|
@@ -257,13 +386,22 @@ export async function checkDeep(cwd) {
|
|
|
257
386
|
// Naming score
|
|
258
387
|
const namingOk = allMetrics.filter(f => isDescriptiveName(f.name) === 'good').length;
|
|
259
388
|
const namingScore = Math.round((namingOk / total) * 100);
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
389
|
+
// Maintainability Index score: average MI across files, normalized to 0-100
|
|
390
|
+
const avgMI = allFileMetrics.length > 0
|
|
391
|
+
? allFileMetrics.reduce((sum, f) => sum + f.maintainabilityIndex, 0) / allFileMetrics.length
|
|
392
|
+
: 100;
|
|
393
|
+
const miScore = Math.round(avgMI);
|
|
394
|
+
const score = Math.max(25, Math.round(complexityScore * 0.25 +
|
|
395
|
+
nestingScore * 0.20 +
|
|
396
|
+
errorScore * 0.20 +
|
|
397
|
+
namingScore * 0.10 +
|
|
398
|
+
miScore * 0.25));
|
|
264
399
|
// ── Summary ───────────────────────────────────────────────────────────────
|
|
265
400
|
const parts = [];
|
|
266
|
-
parts.push(`${total} functions
|
|
401
|
+
parts.push(`${total} functions, ${allFileMetrics.length} files in ${elapsed}ms`);
|
|
402
|
+
parts.push(`avg MI=${avgMI.toFixed(0)}`);
|
|
403
|
+
if (lowMI.length > 0)
|
|
404
|
+
parts.push(c.red + `${lowMI.length} unmaintainable` + c.reset);
|
|
267
405
|
if (highComplexity.length > 0)
|
|
268
406
|
parts.push(c.yellow + `${highComplexity.length} complex` + c.reset);
|
|
269
407
|
if (deeplyNested.length > 0)
|
package/dist/checks/deps.js
CHANGED
|
@@ -311,6 +311,16 @@ const TOOLING_PACKAGES = new Set([
|
|
|
311
311
|
'del-cli', 'make-node',
|
|
312
312
|
// Type packages (consumed by TS compiler, not imported)
|
|
313
313
|
'@types/react', '@types/react-dom', '@types/jest', '@types/mocha',
|
|
314
|
+
// Test runners / e2e (used via CLI, not imported)
|
|
315
|
+
'playwright', '@playwright/test', 'cypress', 'puppeteer',
|
|
316
|
+
// Package quality tools (used via CLI)
|
|
317
|
+
'publint', 'arethetypeswrong', 'are-the-types-wrong', 'attw',
|
|
318
|
+
'pkg-pr-new', 'size-limit', '@size-limit/preset-small-lib',
|
|
319
|
+
// Monorepo/workspace tools
|
|
320
|
+
'update-ts-references', 'syncpack', 'manypkg',
|
|
321
|
+
// Prettier plugins (loaded via config, not imported)
|
|
322
|
+
'prettier-plugin-svelte', 'prettier-plugin-tailwindcss',
|
|
323
|
+
'prettier-plugin-organize-imports', 'prettier-plugin-packagejson',
|
|
314
324
|
]);
|
|
315
325
|
// ── Collect all deps declared in workspace sub-packages ──────────────────────
|
|
316
326
|
export function collectWorkspaceDeps(cwd) {
|
|
@@ -500,6 +510,11 @@ export async function checkDeps(cwd) {
|
|
|
500
510
|
// Skip known tooling packages that are devDependencies (used via CLI scripts, not imports)
|
|
501
511
|
if (TOOLING_PACKAGES.has(pkg) && devDepNames.has(pkg))
|
|
502
512
|
continue;
|
|
513
|
+
// Wildcard tooling patterns (eslint configs, prettier plugins, @types/*)
|
|
514
|
+
if (devDepNames.has(pkg) && (pkg.startsWith('eslint-config-') || pkg.startsWith('eslint-plugin-') ||
|
|
515
|
+
pkg.startsWith('prettier-plugin-') || pkg.startsWith('@types/') ||
|
|
516
|
+
pkg.startsWith('@typescript-eslint/') || pkg.startsWith('@eslint/')))
|
|
517
|
+
continue;
|
|
503
518
|
// Check if it's a CLI tool / plugin / type package (common false positives)
|
|
504
519
|
// Still flag it, but as info
|
|
505
520
|
issues.push({
|
package/dist/checks/semantic.js
CHANGED
|
@@ -130,7 +130,7 @@ export async function checkSemantic(cwd) {
|
|
|
130
130
|
patternEmbeddings.push({ pattern, embedding: new Float32Array(result.data) });
|
|
131
131
|
}
|
|
132
132
|
// Embed and compare each function
|
|
133
|
-
const THRESHOLD = 0.
|
|
133
|
+
const THRESHOLD = 0.45; // similarity threshold — code-to-code embeddings (0.40 gave false positives)
|
|
134
134
|
for (const func of funcsToAnalyze) {
|
|
135
135
|
const result = await extractor(func.body, { pooling: 'mean', normalize: true });
|
|
136
136
|
const funcEmb = new Float32Array(result.data);
|