@wbern/obscene 1.0.0 → 1.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/cli.js +107 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -344,12 +344,89 @@ function getNestingDepths(filePaths) {
|
|
|
344
344
|
}
|
|
345
345
|
return depths;
|
|
346
346
|
}
|
|
347
|
+
var RRF_K = 10;
|
|
348
|
+
function computeComposite(rankings, churn, top) {
|
|
349
|
+
const fileScores = /* @__PURE__ */ new Map();
|
|
350
|
+
for (const ranking of Object.values(rankings)) {
|
|
351
|
+
for (let i = 0; i < ranking.entries.length; i++) {
|
|
352
|
+
const file = ranking.entries[i].file;
|
|
353
|
+
const rrf = 1 / (RRF_K + i + 1);
|
|
354
|
+
const existing = fileScores.get(file);
|
|
355
|
+
if (existing) {
|
|
356
|
+
existing.score += rrf;
|
|
357
|
+
existing.dims += 1;
|
|
358
|
+
} else {
|
|
359
|
+
fileScores.set(file, { score: rrf, dims: 1 });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const entries = [];
|
|
364
|
+
for (const [file, data] of fileScores) {
|
|
365
|
+
entries.push({
|
|
366
|
+
file,
|
|
367
|
+
score: Math.round(data.score * 1e4) / 1e4,
|
|
368
|
+
percentOfTotal: 0,
|
|
369
|
+
tier: "stable",
|
|
370
|
+
churn: churn.get(file) ?? 0,
|
|
371
|
+
dimensionCount: data.dims
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
entries.sort((a, b) => b.score - a.score);
|
|
375
|
+
const totalScore = entries.reduce((sum, e) => sum + e.score, 0);
|
|
376
|
+
if (totalScore === 0) {
|
|
377
|
+
return {
|
|
378
|
+
label: "Combined",
|
|
379
|
+
scoreFormula: "reciprocal rank fusion across all dimensions",
|
|
380
|
+
totalScore: 0,
|
|
381
|
+
tierCounts: { danger: 0, watch: 0, stable: 0 },
|
|
382
|
+
totalEntries: 0,
|
|
383
|
+
showing: 0,
|
|
384
|
+
entries: []
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
assignTiers(entries, totalScore);
|
|
388
|
+
const limited = top > 0 ? entries.slice(0, top) : entries;
|
|
389
|
+
const tierCounts = { danger: 0, watch: 0, stable: 0 };
|
|
390
|
+
for (const e of entries) {
|
|
391
|
+
tierCounts[e.tier]++;
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
label: "Combined",
|
|
395
|
+
scoreFormula: "reciprocal rank fusion across all dimensions",
|
|
396
|
+
totalScore: Math.round(totalScore * 1e4) / 1e4,
|
|
397
|
+
tierCounts,
|
|
398
|
+
totalEntries: entries.length,
|
|
399
|
+
showing: limited.length,
|
|
400
|
+
entries: limited
|
|
401
|
+
};
|
|
402
|
+
}
|
|
347
403
|
|
|
348
404
|
// src/color.ts
|
|
349
405
|
import pc from "picocolors";
|
|
350
406
|
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
407
|
+
function isWide(cp) {
|
|
408
|
+
return (
|
|
409
|
+
// CJK Radicals through Katakana (U+2E80–U+30FF) + CJK Symbols (U+3000–U+303F)
|
|
410
|
+
cp >= 11904 && cp <= 12543 || // Enclosed CJK Letters + CJK Compatibility (U+3200–U+33FF)
|
|
411
|
+
cp >= 12800 && cp <= 13311 || // CJK Extension A (U+3400–U+4DBF) + CJK Unified Ideographs (U+4E00–U+9FFF)
|
|
412
|
+
cp >= 13312 && cp <= 40959 || // Hangul Syllables (U+AC00–U+D7AF)
|
|
413
|
+
cp >= 44032 && cp <= 55215 || // CJK Compatibility Ideographs (U+F900–U+FAFF)
|
|
414
|
+
cp >= 63744 && cp <= 64255 || // Fullwidth Forms (U+FF01–U+FF60, U+FFE0–U+FFE6)
|
|
415
|
+
cp >= 65281 && cp <= 65376 || cp >= 65504 && cp <= 65510 || // Emoji and symbol blocks in supplementary planes (U+1F300–U+1FAFF)
|
|
416
|
+
cp >= 127744 && cp <= 129791 || // CJK Extension B+ and supplementary ideographs (U+20000–U+2FA1F)
|
|
417
|
+
cp >= 131072 && cp <= 195103
|
|
418
|
+
);
|
|
419
|
+
}
|
|
351
420
|
function visualWidth(s) {
|
|
352
|
-
|
|
421
|
+
const stripped = s.replace(ANSI_RE, "");
|
|
422
|
+
let width = 0;
|
|
423
|
+
for (const ch of stripped) {
|
|
424
|
+
const cp = ch.codePointAt(0);
|
|
425
|
+
if (cp !== void 0) {
|
|
426
|
+
width += isWide(cp) ? 2 : 1;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return width;
|
|
353
430
|
}
|
|
354
431
|
function padRight(s, n) {
|
|
355
432
|
const w = visualWidth(s);
|
|
@@ -569,10 +646,29 @@ function formatCouplingTable(output) {
|
|
|
569
646
|
lines.push("Docs: https://github.com/wbern/obscene#metrics");
|
|
570
647
|
return lines.join("\n");
|
|
571
648
|
}
|
|
649
|
+
function formatCompositeTable(output) {
|
|
650
|
+
const lines = [];
|
|
651
|
+
lines.push(
|
|
652
|
+
`${output.label} \u2014 Total score: ${output.totalScore.toLocaleString()}`
|
|
653
|
+
);
|
|
654
|
+
lines.push(
|
|
655
|
+
...tierSummary(output.tierCounts, output.showing, output.totalEntries)
|
|
656
|
+
);
|
|
657
|
+
lines.push("");
|
|
658
|
+
lines.push(
|
|
659
|
+
padRight("File", 50) + padLeft("Score", 9) + padLeft("Churn", 7) + padLeft("Dims", 6) + padLeft("Tier", 12)
|
|
660
|
+
);
|
|
661
|
+
lines.push("\u2500".repeat(84));
|
|
662
|
+
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}/4`, 6) + padLeft(tierLabel(entry.tier), 12);
|
|
664
|
+
lines.push(colorRow(entry.tier, rawRow));
|
|
665
|
+
}
|
|
666
|
+
return lines.join("\n");
|
|
667
|
+
}
|
|
572
668
|
|
|
573
669
|
// src/cli.ts
|
|
574
670
|
var program = new Command();
|
|
575
|
-
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.
|
|
671
|
+
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.1.0");
|
|
576
672
|
var REPORT_GUIDE = {
|
|
577
673
|
complexity: "Cyclomatic complexity (branch/loop count). NOT a quality judgment \u2014 a 500-line parser will naturally score high. Compare density, not raw values.",
|
|
578
674
|
complexityDensity: "Complexity per line of code. Normalizes for file size. >0.25 suggests dense logic worth reviewing; <0.10 is typical for straightforward code.",
|
|
@@ -584,6 +680,7 @@ var HOTSPOTS_GUIDE = {
|
|
|
584
680
|
nesting: "maxNesting \xD7 churn. Deeply nested code that changes often is harder to reason about.",
|
|
585
681
|
defects: "defects \xD7 churn. Files with fix: commits that also churn heavily may contain latent bugs.",
|
|
586
682
|
authors: "authors \xD7 churn. Files touched by many authors and changing often may lack clear ownership.",
|
|
683
|
+
composite: "Combined ranking using Reciprocal Rank Fusion (RRF) across all dimensions. Files appearing near the top of multiple rankings score highest.",
|
|
587
684
|
tier: "Relative ranking within THIS codebase (top 50% = danger, next 30% = watch, bottom 20% = stable). NOT an absolute quality grade."
|
|
588
685
|
};
|
|
589
686
|
var COUPLING_GUIDE = {
|
|
@@ -674,15 +771,22 @@ function runHotspots(opts) {
|
|
|
674
771
|
authors,
|
|
675
772
|
top
|
|
676
773
|
);
|
|
774
|
+
const composite = computeComposite(rankings, churn, top);
|
|
677
775
|
const output = {
|
|
678
776
|
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
679
777
|
guide: HOTSPOTS_GUIDE,
|
|
680
778
|
churnWindow: `${months} months`,
|
|
681
|
-
rankings
|
|
779
|
+
rankings,
|
|
780
|
+
composite
|
|
682
781
|
};
|
|
683
782
|
if (opts.format === "table") {
|
|
684
783
|
process.stdout.write(`${formatHotspotsTable(output)}
|
|
685
784
|
`);
|
|
785
|
+
if (composite.entries.length > 0) {
|
|
786
|
+
process.stdout.write(`
|
|
787
|
+
${formatCompositeTable(composite)}
|
|
788
|
+
`);
|
|
789
|
+
}
|
|
686
790
|
} else {
|
|
687
791
|
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
688
792
|
`);
|