@wbern/obscene 1.5.0 → 2.0.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 (3) hide show
  1. package/README.md +18 -2
  2. package/dist/cli.js +65 -38
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -252,9 +252,25 @@ Docs: https://github.com/wbern/obscene#metrics
252
252
 
253
253
  Any language [scc supports](https://github.com/boyter/scc#features) — 200+ languages including C, C++, Go, Java, JavaScript, TypeScript, Python, Rust, Ruby, PHP, Swift, Kotlin, and many more. No configuration needed; scc auto-detects languages from file extensions.
254
254
 
255
- ## Default exclusions
255
+ ## Exclusions
256
256
 
257
- Test files, lock files, and package manifests are excluded automatically: `*.test.*`, `*.spec.*`, `__tests__/`, `__mocks__/`, `*.stories.*`, `*.d.ts`, `package.json`, `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, and similar patterns. scc also skips generated files by default (`--no-gen`).
257
+ All exclusions are opt-in. Run `obscene init` to generate a `.obsignore` file with recommended patterns for your project:
258
+
259
+ ```bash
260
+ obscene init
261
+ ```
262
+
263
+ This creates a `.obsignore` containing:
264
+ - **Universal exclusions** — test files (`*.test.*`, `*.spec.*`, `__tests__/`, etc.), lock files (`package-lock.json`, `pnpm-lock.yaml`, etc.), and package manifests (`package.json`)
265
+ - **Detected project patterns** — CI directories (`.github/`), config files (`*.config.*`), vendored code, etc., based on your project structure
266
+
267
+ If no `.obsignore` or `.obsceneignore` exists, obscene prints a hint to stderr:
268
+
269
+ ```
270
+ hint: no .obsignore found — run `obscene init` to generate one with recommended exclusions
271
+ ```
272
+
273
+ scc also skips generated files by default (`--no-gen`).
258
274
 
259
275
  ## Ignore files
260
276
 
package/dist/cli.js CHANGED
@@ -22,22 +22,32 @@ function readIgnoreFile() {
22
22
  }
23
23
  return [];
24
24
  }
25
- var DEFAULT_EXCLUDES = [
26
- /\.test\./,
27
- /\.spec\./,
28
- /\.integration\.test\./,
29
- /test-setup\./,
30
- /test-utils\./,
31
- /test-helpers\./,
32
- /__tests__\//,
33
- /__mocks__\//,
34
- /\.stories\./,
35
- /\.d\.ts$/,
36
- /(?:^|\/)package\.json$/,
37
- /(?:^|\/)package-lock\.json$/,
38
- /(?:^|\/)pnpm-lock\.yaml$/,
39
- /(?:^|\/)yarn\.lock$/,
40
- /(?:^|\/)bun\.lock$/
25
+ var UNIVERSAL_IGNORE_GROUPS = [
26
+ {
27
+ title: "Test files and test infrastructure",
28
+ patterns: [
29
+ { pattern: "*.test.*", comment: "Unit test files" },
30
+ { pattern: "*.spec.*", comment: "Spec test files" },
31
+ { pattern: "*.integration.test.*", comment: "Integration tests" },
32
+ { pattern: "test-setup.*", comment: "Test setup files" },
33
+ { pattern: "test-utils.*", comment: "Test utility files" },
34
+ { pattern: "test-helpers.*", comment: "Test helper files" },
35
+ { pattern: "__tests__/**", comment: "Test directories" },
36
+ { pattern: "__mocks__/**", comment: "Mock directories" },
37
+ { pattern: "*.stories.*", comment: "Storybook stories" },
38
+ { pattern: "*.d.ts", comment: "TypeScript declaration files" }
39
+ ]
40
+ },
41
+ {
42
+ title: "Lock files and package manifests",
43
+ patterns: [
44
+ { pattern: "package.json", comment: "npm package manifest" },
45
+ { pattern: "package-lock.json", comment: "npm lock file" },
46
+ { pattern: "pnpm-lock.yaml", comment: "pnpm lock file" },
47
+ { pattern: "yarn.lock", comment: "Yarn lock file" },
48
+ { pattern: "bun.lock", comment: "Bun lock file" }
49
+ ]
50
+ }
41
51
  ];
42
52
  var HOT_CUMULATIVE = 0.5;
43
53
  var WARM_CUMULATIVE = 0.8;
@@ -55,7 +65,7 @@ function normalizePath(p) {
55
65
  return forwardSlash.startsWith("./") ? forwardSlash.slice(2) : forwardSlash;
56
66
  }
57
67
  function runScc(excludes = []) {
58
- const patterns = [...DEFAULT_EXCLUDES, ...excludes.map(globToRegex)];
68
+ const patterns = excludes.map(globToRegex);
59
69
  let raw;
60
70
  try {
61
71
  raw = execSync("scc --by-file --format json --no-cocomo --no-gen", {
@@ -154,7 +164,7 @@ function getAuthors(months) {
154
164
  }
155
165
  var MAX_FILES_PER_COMMIT = 20;
156
166
  function getCoChanges(months, excludes = []) {
157
- const patterns = [...DEFAULT_EXCLUDES, ...excludes.map(globToRegex)];
167
+ const patterns = excludes.map(globToRegex);
158
168
  let raw;
159
169
  try {
160
170
  raw = execSync(
@@ -465,7 +475,7 @@ function detectIgnorePatterns() {
465
475
  }
466
476
  return patterns;
467
477
  }
468
- function formatIgnoreFile(patterns) {
478
+ function formatIgnoreFile(detectedPatterns, universalGroups = UNIVERSAL_IGNORE_GROUPS) {
469
479
  const lines = [
470
480
  "# Generated by obscene init",
471
481
  "# Edit this file to customize which files are excluded from analysis.",
@@ -473,16 +483,20 @@ function formatIgnoreFile(patterns) {
473
483
  "# See: https://github.com/wbern/obscene#ignore-files",
474
484
  ""
475
485
  ];
476
- if (patterns.length === 0) {
477
- lines.push("# No project-specific patterns detected.");
478
- lines.push("# Add glob patterns here, one per line.");
486
+ for (const group of universalGroups) {
487
+ lines.push(`# ${group.title}`);
488
+ for (const p of group.patterns) {
489
+ lines.push(p.pattern);
490
+ }
479
491
  lines.push("");
480
- } else {
481
- for (const p of patterns) {
492
+ }
493
+ if (detectedPatterns.length > 0) {
494
+ lines.push("# Project-specific patterns");
495
+ for (const p of detectedPatterns) {
482
496
  lines.push(`# ${p.comment}`);
483
497
  lines.push(p.pattern);
484
- lines.push("");
485
498
  }
499
+ lines.push("");
486
500
  }
487
501
  return lines.join("\n");
488
502
  }
@@ -857,7 +871,7 @@ function formatCompositeTable(output) {
857
871
 
858
872
  // src/cli.ts
859
873
  var program = new Command();
860
- program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.5.0");
874
+ program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("2.0.0");
861
875
  var REPORT_GUIDE = {
862
876
  complexity: "Cyclomatic complexity (branch/loop count). NOT a quality judgment \u2014 a 500-line parser will naturally score high. Compare density, not raw values.",
863
877
  complexityDensity: "Complexity per line of code. Normalizes for file size. >0.25 suggests dense logic worth reviewing; <0.10 is typical for straightforward code.",
@@ -923,7 +937,15 @@ program.command("init").description("generate a starter .obsignore based on proj
923
937
  function resolveExcludes(cliExcludes) {
924
938
  return [...readIgnoreFile(), ...cliExcludes ?? []];
925
939
  }
940
+ function warnIfNoIgnoreFile() {
941
+ if (!existsSync(".obsignore") && !existsSync(".obsceneignore")) {
942
+ process.stderr.write(
943
+ "hint: no .obsignore found \u2014 run `obscene init` to generate one with recommended exclusions\n"
944
+ );
945
+ }
946
+ }
926
947
  function runReport(opts) {
948
+ warnIfNoIgnoreFile();
927
949
  const top = parseInt(opts.top, 10);
928
950
  const allExcludes = resolveExcludes(opts.exclude);
929
951
  const files = runScc(allExcludes);
@@ -956,6 +978,7 @@ function runReport(opts) {
956
978
  }
957
979
  }
958
980
  function runHotspots(opts) {
981
+ warnIfNoIgnoreFile();
959
982
  const top = parseInt(opts.top, 10);
960
983
  const months = parseInt(opts.months, 10);
961
984
  const allExcludes = resolveExcludes(opts.exclude);
@@ -995,6 +1018,7 @@ ${formatCompositeTable(composite)}
995
1018
  }
996
1019
  }
997
1020
  function runCoupling(opts) {
1021
+ warnIfNoIgnoreFile();
998
1022
  const top = parseInt(opts.top, 10);
999
1023
  const months = parseInt(opts.months, 10);
1000
1024
  const minCochanges = parseInt(opts.minCochanges, 10);
@@ -1048,22 +1072,25 @@ function runInit() {
1048
1072
  ".obsceneignore already exists. Remove it first to regenerate."
1049
1073
  );
1050
1074
  }
1051
- const patterns = detectIgnorePatterns();
1052
- const content = formatIgnoreFile(patterns);
1075
+ const detected = detectIgnorePatterns();
1076
+ const content = formatIgnoreFile(detected);
1053
1077
  writeFileSync(".obsignore", content);
1054
- if (patterns.length === 0) {
1055
- process.stderr.write(
1056
- "Created .obsignore (no project-specific patterns detected)\n"
1057
- );
1058
- } else {
1059
- process.stderr.write(
1060
- `Created .obsignore with ${patterns.length} patterns:
1061
- `
1062
- );
1063
- for (const p of patterns) {
1078
+ const universalCount = UNIVERSAL_IGNORE_GROUPS.reduce(
1079
+ (sum, g) => sum + g.patterns.length,
1080
+ 0
1081
+ );
1082
+ process.stderr.write(
1083
+ `Created .obsignore with ${universalCount} universal exclusions`
1084
+ );
1085
+ if (detected.length > 0) {
1086
+ process.stderr.write(` + ${detected.length} detected patterns:
1087
+ `);
1088
+ for (const p of detected) {
1064
1089
  process.stderr.write(` ${p.pattern.padEnd(20)} ${p.comment}
1065
1090
  `);
1066
1091
  }
1092
+ } else {
1093
+ process.stderr.write(" (no project-specific patterns detected)\n");
1067
1094
  }
1068
1095
  }
1069
1096
  function exitWithError(err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/obscene",
3
- "version": "1.5.0",
3
+ "version": "2.0.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": {