@wbern/obscene 1.1.0 → 1.2.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/cli.js +63 -28
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -18,8 +18,10 @@ var DEFAULT_EXCLUDES = [
|
|
|
18
18
|
/\.stories\./,
|
|
19
19
|
/\.d\.ts$/
|
|
20
20
|
];
|
|
21
|
-
var
|
|
22
|
-
var
|
|
21
|
+
var HOT_CUMULATIVE = 0.5;
|
|
22
|
+
var WARM_CUMULATIVE = 0.8;
|
|
23
|
+
var MIN_FIX_COMMITS = 5;
|
|
24
|
+
var MIN_FILES_WITH_FIXES = 3;
|
|
23
25
|
function isExcluded(location, patterns) {
|
|
24
26
|
return patterns.some((p) => p.test(location));
|
|
25
27
|
}
|
|
@@ -174,12 +176,12 @@ function assignTiers(items, totalScore) {
|
|
|
174
176
|
item.percentOfTotal = Math.round(item.score / totalScore * 1e3) / 10;
|
|
175
177
|
cumulative += item.score;
|
|
176
178
|
const cumulativeShare = cumulative / totalScore;
|
|
177
|
-
if (cumulativeShare <=
|
|
178
|
-
item.tier = "
|
|
179
|
-
} else if (cumulativeShare <=
|
|
180
|
-
item.tier = "
|
|
179
|
+
if (cumulativeShare <= HOT_CUMULATIVE) {
|
|
180
|
+
item.tier = "hot";
|
|
181
|
+
} else if (cumulativeShare <= WARM_CUMULATIVE) {
|
|
182
|
+
item.tier = "warm";
|
|
181
183
|
} else {
|
|
182
|
-
item.tier = "
|
|
184
|
+
item.tier = "cool";
|
|
183
185
|
}
|
|
184
186
|
}
|
|
185
187
|
}
|
|
@@ -213,7 +215,7 @@ function computeRanking(files, churn, metricExtractor, densityExtractor) {
|
|
|
213
215
|
file: f.file,
|
|
214
216
|
score: metricValue * fileChurn,
|
|
215
217
|
percentOfTotal: 0,
|
|
216
|
-
tier: "
|
|
218
|
+
tier: "cool",
|
|
217
219
|
churn: fileChurn,
|
|
218
220
|
metricValue,
|
|
219
221
|
metricDensity: densityExtractor ? densityExtractor(f) : void 0
|
|
@@ -244,16 +246,35 @@ function computeAllRankings(files, churn, defects, nestingDepths, authors, top)
|
|
|
244
246
|
extract: (f) => authors.get(f.file) ?? 0
|
|
245
247
|
}
|
|
246
248
|
};
|
|
249
|
+
const skipped = {};
|
|
250
|
+
const totalFixCommits = [...defects.values()].reduce((s, v) => s + v, 0);
|
|
251
|
+
const filesWithFixes = defects.size;
|
|
252
|
+
if (totalFixCommits < MIN_FIX_COMMITS || filesWithFixes < MIN_FILES_WITH_FIXES) {
|
|
253
|
+
skipped.defects = {
|
|
254
|
+
reason: `insufficient data (${totalFixCommits} fix: commits across ${filesWithFixes} files, need ${MIN_FIX_COMMITS}+ commits across ${MIN_FILES_WITH_FIXES}+ files)`,
|
|
255
|
+
suggestion: "Adopt conventional commits with fix: prefix. See conventionalcommits.org"
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
let maxAuthors = 0;
|
|
259
|
+
for (const count of authors.values()) {
|
|
260
|
+
if (count > maxAuthors) maxAuthors = count;
|
|
261
|
+
}
|
|
262
|
+
if (maxAuthors <= 1) {
|
|
263
|
+
skipped.authors = {
|
|
264
|
+
reason: "all files have the same author count \u2014 no variance to rank"
|
|
265
|
+
};
|
|
266
|
+
}
|
|
247
267
|
const rankings = {};
|
|
248
268
|
for (const def of RANKING_DEFS) {
|
|
269
|
+
if (skipped[def.key]) continue;
|
|
249
270
|
const ext = extractors[def.key];
|
|
250
271
|
const allEntries = computeRanking(files, churn, ext.extract, ext.density);
|
|
251
272
|
if (allEntries.length === 0) continue;
|
|
252
273
|
const limited = top > 0 ? allEntries.slice(0, top) : allEntries;
|
|
253
274
|
const tierCounts = {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
275
|
+
hot: 0,
|
|
276
|
+
warm: 0,
|
|
277
|
+
cool: 0
|
|
257
278
|
};
|
|
258
279
|
for (const e of allEntries) {
|
|
259
280
|
tierCounts[e.tier]++;
|
|
@@ -268,7 +289,7 @@ function computeAllRankings(files, churn, defects, nestingDepths, authors, top)
|
|
|
268
289
|
entries: limited
|
|
269
290
|
};
|
|
270
291
|
}
|
|
271
|
-
return rankings;
|
|
292
|
+
return { rankings, skipped };
|
|
272
293
|
}
|
|
273
294
|
function computeCoupling(cochanges, churn, complexityMap, minCochanges) {
|
|
274
295
|
const entries = [];
|
|
@@ -286,7 +307,7 @@ function computeCoupling(cochanges, churn, complexityMap, minCochanges) {
|
|
|
286
307
|
totalComplexity,
|
|
287
308
|
couplingScore: count,
|
|
288
309
|
percentOfTotal: 0,
|
|
289
|
-
tier: "
|
|
310
|
+
tier: "cool"
|
|
290
311
|
});
|
|
291
312
|
}
|
|
292
313
|
entries.sort((a, b) => b.couplingScore - a.couplingScore);
|
|
@@ -346,6 +367,7 @@ function getNestingDepths(filePaths) {
|
|
|
346
367
|
}
|
|
347
368
|
var RRF_K = 10;
|
|
348
369
|
function computeComposite(rankings, churn, top) {
|
|
370
|
+
const totalDimensions = Object.keys(rankings).length;
|
|
349
371
|
const fileScores = /* @__PURE__ */ new Map();
|
|
350
372
|
for (const ranking of Object.values(rankings)) {
|
|
351
373
|
for (let i = 0; i < ranking.entries.length; i++) {
|
|
@@ -366,7 +388,7 @@ function computeComposite(rankings, churn, top) {
|
|
|
366
388
|
file,
|
|
367
389
|
score: Math.round(data.score * 1e4) / 1e4,
|
|
368
390
|
percentOfTotal: 0,
|
|
369
|
-
tier: "
|
|
391
|
+
tier: "cool",
|
|
370
392
|
churn: churn.get(file) ?? 0,
|
|
371
393
|
dimensionCount: data.dims
|
|
372
394
|
});
|
|
@@ -378,7 +400,8 @@ function computeComposite(rankings, churn, top) {
|
|
|
378
400
|
label: "Combined",
|
|
379
401
|
scoreFormula: "reciprocal rank fusion across all dimensions",
|
|
380
402
|
totalScore: 0,
|
|
381
|
-
tierCounts: {
|
|
403
|
+
tierCounts: { hot: 0, warm: 0, cool: 0 },
|
|
404
|
+
totalDimensions,
|
|
382
405
|
totalEntries: 0,
|
|
383
406
|
showing: 0,
|
|
384
407
|
entries: []
|
|
@@ -386,7 +409,7 @@ function computeComposite(rankings, churn, top) {
|
|
|
386
409
|
}
|
|
387
410
|
assignTiers(entries, totalScore);
|
|
388
411
|
const limited = top > 0 ? entries.slice(0, top) : entries;
|
|
389
|
-
const tierCounts = {
|
|
412
|
+
const tierCounts = { hot: 0, warm: 0, cool: 0 };
|
|
390
413
|
for (const e of entries) {
|
|
391
414
|
tierCounts[e.tier]++;
|
|
392
415
|
}
|
|
@@ -395,6 +418,7 @@ function computeComposite(rankings, churn, top) {
|
|
|
395
418
|
scoreFormula: "reciprocal rank fusion across all dimensions",
|
|
396
419
|
totalScore: Math.round(totalScore * 1e4) / 1e4,
|
|
397
420
|
tierCounts,
|
|
421
|
+
totalDimensions,
|
|
398
422
|
totalEntries: entries.length,
|
|
399
423
|
showing: limited.length,
|
|
400
424
|
entries: limited
|
|
@@ -440,19 +464,19 @@ function truncate(s, max) {
|
|
|
440
464
|
return s.length <= max ? s : `\u2026${s.slice(s.length - max + 1)}`;
|
|
441
465
|
}
|
|
442
466
|
function tierLabel(tier) {
|
|
443
|
-
if (tier === "
|
|
444
|
-
if (tier === "
|
|
445
|
-
return pc.green("\u{
|
|
467
|
+
if (tier === "hot") return pc.red("\u{1F525} HOT");
|
|
468
|
+
if (tier === "warm") return pc.yellow("\u2600\uFE0F WARM");
|
|
469
|
+
return pc.green("\u{1F9CA} cool");
|
|
446
470
|
}
|
|
447
471
|
function colorRow(tier, text) {
|
|
448
|
-
if (tier === "
|
|
449
|
-
if (tier === "
|
|
472
|
+
if (tier === "hot") return pc.red(text);
|
|
473
|
+
if (tier === "warm") return pc.yellow(text);
|
|
450
474
|
return pc.green(text);
|
|
451
475
|
}
|
|
452
476
|
function tierSummary(tierCounts, showing, total) {
|
|
453
477
|
const lines = [];
|
|
454
478
|
lines.push(
|
|
455
|
-
`Tiers: ${pc.red(`${tierCounts.
|
|
479
|
+
`Tiers: ${pc.red(`${tierCounts.hot} hot`)}, ${pc.yellow(`${tierCounts.warm} warm`)}, ${pc.green(`${tierCounts.cool} cool`)}`
|
|
456
480
|
);
|
|
457
481
|
lines.push(`Showing: ${showing} of ${total}`);
|
|
458
482
|
return lines;
|
|
@@ -608,6 +632,16 @@ function formatHotspotsTable(output) {
|
|
|
608
632
|
lines.push("");
|
|
609
633
|
}
|
|
610
634
|
}
|
|
635
|
+
if (output.skipped) {
|
|
636
|
+
for (const [key, info] of Object.entries(output.skipped)) {
|
|
637
|
+
lines.push("");
|
|
638
|
+
const label = key.charAt(0).toUpperCase() + key.slice(1);
|
|
639
|
+
lines.push(`${label} \xD7 Churn \u2014 skipped (${info.reason})`);
|
|
640
|
+
if (info.suggestion) {
|
|
641
|
+
lines.push(` ${info.suggestion}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
611
645
|
lines.push("");
|
|
612
646
|
lines.push(
|
|
613
647
|
"Score=metric\xD7churn | Tiers are relative to THIS codebase, not absolute quality grades."
|
|
@@ -660,7 +694,7 @@ function formatCompositeTable(output) {
|
|
|
660
694
|
);
|
|
661
695
|
lines.push("\u2500".repeat(84));
|
|
662
696
|
for (const entry of output.entries) {
|
|
663
|
-
const rawRow = padRight(truncate(entry.file, 48), 50) + padLeft(entry.score.toFixed(4), 9) + padLeft(String(entry.churn), 7) + padLeft(`${entry.dimensionCount}
|
|
697
|
+
const rawRow = padRight(truncate(entry.file, 48), 50) + padLeft(entry.score.toFixed(4), 9) + padLeft(String(entry.churn), 7) + padLeft(`${entry.dimensionCount}/${output.totalDimensions}`, 6) + padLeft(tierLabel(entry.tier), 12);
|
|
664
698
|
lines.push(colorRow(entry.tier, rawRow));
|
|
665
699
|
}
|
|
666
700
|
return lines.join("\n");
|
|
@@ -668,7 +702,7 @@ function formatCompositeTable(output) {
|
|
|
668
702
|
|
|
669
703
|
// src/cli.ts
|
|
670
704
|
var program = new Command();
|
|
671
|
-
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.
|
|
705
|
+
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.2.0");
|
|
672
706
|
var REPORT_GUIDE = {
|
|
673
707
|
complexity: "Cyclomatic complexity (branch/loop count). NOT a quality judgment \u2014 a 500-line parser will naturally score high. Compare density, not raw values.",
|
|
674
708
|
complexityDensity: "Complexity per line of code. Normalizes for file size. >0.25 suggests dense logic worth reviewing; <0.10 is typical for straightforward code.",
|
|
@@ -681,13 +715,13 @@ var HOTSPOTS_GUIDE = {
|
|
|
681
715
|
defects: "defects \xD7 churn. Files with fix: commits that also churn heavily may contain latent bugs.",
|
|
682
716
|
authors: "authors \xD7 churn. Files touched by many authors and changing often may lack clear ownership.",
|
|
683
717
|
composite: "Combined ranking using Reciprocal Rank Fusion (RRF) across all dimensions. Files appearing near the top of multiple rankings score highest.",
|
|
684
|
-
tier: "Relative ranking within THIS codebase (top 50% =
|
|
718
|
+
tier: "Relative ranking within THIS codebase (top 50% = hot, next 30% = warm, bottom 20% = cool). NOT an absolute quality grade \u2014 a hot file is under heavy load, not necessarily broken."
|
|
685
719
|
};
|
|
686
720
|
var COUPLING_GUIDE = {
|
|
687
721
|
cochanges: "Times both files appeared in the same commit. Higher values suggest a dependency between the files. Same-directory pairs are excluded \u2014 only cross-directory pairs are shown.",
|
|
688
722
|
degree: "Percentage: shared commits / min(churn of file1, file2) \xD7 100. Shows how tightly coupled the pair is relative to their individual change rates. 100% means every change to the less-active file also touched the other.",
|
|
689
723
|
totalComplexity: "Sum of both files' cyclomatic complexity. Highlights coupled pairs where the involved code is also complex \u2014 hidden dependency + high complexity compounds maintenance risk.",
|
|
690
|
-
tier: "Relative ranking within THIS codebase's coupling pairs (top 50% =
|
|
724
|
+
tier: "Relative ranking within THIS codebase's coupling pairs (top 50% = hot, next 30% = warm, bottom 20% = cool). NOT an absolute quality grade. 'hot' means this pair co-changes more than most \u2014 it may be intentional and fine."
|
|
691
725
|
};
|
|
692
726
|
function addSharedOptions(cmd) {
|
|
693
727
|
return cmd.option("--top <n>", "limit to top N entries (0 = all)", "20").option("--format <type>", "output format: json | table", "json").option(
|
|
@@ -763,7 +797,7 @@ function runHotspots(opts) {
|
|
|
763
797
|
const defects = getDefects(months);
|
|
764
798
|
const authors = getAuthors(months);
|
|
765
799
|
const nestingDepths = getNestingDepths(files.map((f) => f.file));
|
|
766
|
-
const rankings = computeAllRankings(
|
|
800
|
+
const { rankings, skipped } = computeAllRankings(
|
|
767
801
|
files,
|
|
768
802
|
churn,
|
|
769
803
|
defects,
|
|
@@ -777,6 +811,7 @@ function runHotspots(opts) {
|
|
|
777
811
|
guide: HOTSPOTS_GUIDE,
|
|
778
812
|
churnWindow: `${months} months`,
|
|
779
813
|
rankings,
|
|
814
|
+
skipped: Object.keys(skipped).length > 0 ? skipped : void 0,
|
|
780
815
|
composite
|
|
781
816
|
};
|
|
782
817
|
if (opts.format === "table") {
|
|
@@ -810,7 +845,7 @@ function runCoupling(opts) {
|
|
|
810
845
|
minCochanges
|
|
811
846
|
);
|
|
812
847
|
const limited = top > 0 ? couplings.slice(0, top) : couplings;
|
|
813
|
-
const tierCounts = {
|
|
848
|
+
const tierCounts = { hot: 0, warm: 0, cool: 0 };
|
|
814
849
|
for (const c of couplings) {
|
|
815
850
|
tierCounts[c.tier]++;
|
|
816
851
|
}
|