@wbern/obscene 1.0.1 → 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.
Files changed (2) hide show
  1. package/dist/cli.js +85 -2
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -344,6 +344,62 @@ 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";
@@ -590,10 +646,29 @@ function formatCouplingTable(output) {
590
646
  lines.push("Docs: https://github.com/wbern/obscene#metrics");
591
647
  return lines.join("\n");
592
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
+ }
593
668
 
594
669
  // src/cli.ts
595
670
  var program = new Command();
596
- program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.0.1");
671
+ program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.1.0");
597
672
  var REPORT_GUIDE = {
598
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.",
599
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.",
@@ -605,6 +680,7 @@ var HOTSPOTS_GUIDE = {
605
680
  nesting: "maxNesting \xD7 churn. Deeply nested code that changes often is harder to reason about.",
606
681
  defects: "defects \xD7 churn. Files with fix: commits that also churn heavily may contain latent bugs.",
607
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.",
608
684
  tier: "Relative ranking within THIS codebase (top 50% = danger, next 30% = watch, bottom 20% = stable). NOT an absolute quality grade."
609
685
  };
610
686
  var COUPLING_GUIDE = {
@@ -695,15 +771,22 @@ function runHotspots(opts) {
695
771
  authors,
696
772
  top
697
773
  );
774
+ const composite = computeComposite(rankings, churn, top);
698
775
  const output = {
699
776
  generated: (/* @__PURE__ */ new Date()).toISOString(),
700
777
  guide: HOTSPOTS_GUIDE,
701
778
  churnWindow: `${months} months`,
702
- rankings
779
+ rankings,
780
+ composite
703
781
  };
704
782
  if (opts.format === "table") {
705
783
  process.stdout.write(`${formatHotspotsTable(output)}
706
784
  `);
785
+ if (composite.entries.length > 0) {
786
+ process.stdout.write(`
787
+ ${formatCompositeTable(composite)}
788
+ `);
789
+ }
707
790
  } else {
708
791
  process.stdout.write(`${JSON.stringify(output, null, 2)}
709
792
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/obscene",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Identify hotspot files — complex code that changes frequently. Churn × complexity analysis for any git repo.",
5
5
  "type": "module",
6
6
  "bin": {