claude-crap 0.3.5 → 0.3.7

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 (35) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/dashboard/file-detail.d.ts +77 -0
  3. package/dist/dashboard/file-detail.d.ts.map +1 -0
  4. package/dist/dashboard/file-detail.js +120 -0
  5. package/dist/dashboard/file-detail.js.map +1 -0
  6. package/dist/dashboard/server.d.ts +3 -0
  7. package/dist/dashboard/server.d.ts.map +1 -1
  8. package/dist/dashboard/server.js +108 -1
  9. package/dist/dashboard/server.js.map +1 -1
  10. package/dist/index.js +19 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/scanner/auto-scan.d.ts +8 -1
  13. package/dist/scanner/auto-scan.d.ts.map +1 -1
  14. package/dist/scanner/auto-scan.js +14 -1
  15. package/dist/scanner/auto-scan.js.map +1 -1
  16. package/dist/scanner/complexity-scanner.d.ts +54 -0
  17. package/dist/scanner/complexity-scanner.d.ts.map +1 -0
  18. package/dist/scanner/complexity-scanner.js +176 -0
  19. package/dist/scanner/complexity-scanner.js.map +1 -0
  20. package/package.json +1 -1
  21. package/plugin/.claude-plugin/plugin.json +1 -1
  22. package/plugin/bundle/dashboard/public/index.html +432 -12
  23. package/plugin/bundle/mcp-server.mjs +429 -71
  24. package/plugin/bundle/mcp-server.mjs.map +4 -4
  25. package/plugin/package-lock.json +2 -2
  26. package/plugin/package.json +1 -1
  27. package/scripts/bundle-plugin.mjs +53 -2
  28. package/src/dashboard/file-detail.ts +197 -0
  29. package/src/dashboard/public/index.html +432 -12
  30. package/src/dashboard/server.ts +141 -1
  31. package/src/index.ts +20 -2
  32. package/src/scanner/auto-scan.ts +26 -0
  33. package/src/scanner/complexity-scanner.ts +233 -0
  34. package/src/tests/complexity-scanner.test.ts +263 -0
  35. package/src/tests/file-detail-api.test.ts +258 -0
package/src/index.ts CHANGED
@@ -114,6 +114,7 @@ async function main(): Promise<void> {
114
114
  sarifStore,
115
115
  workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot),
116
116
  logger,
117
+ astEngine,
117
118
  });
118
119
  } catch (err) {
119
120
  logger.warn(
@@ -581,7 +582,10 @@ async function main(): Promise<void> {
581
582
  case "auto_scan": {
582
583
  logger.info({ tool: "auto_scan" }, "Tool call received");
583
584
  try {
584
- const result = await autoScan(config.pluginRoot, sarifStore, logger);
585
+ const result = await autoScan(config.pluginRoot, sarifStore, logger, {
586
+ engine: astEngine,
587
+ cyclomaticMax: config.cyclomaticMax,
588
+ });
585
589
  const markdown = renderAutoScanMarkdown(result);
586
590
  return {
587
591
  content: [
@@ -669,7 +673,10 @@ async function main(): Promise<void> {
669
673
  // If the agent calls score_project before scanning finishes, it gets
670
674
  // whatever is in the SARIF store so far. The next call after completion
671
675
  // reflects all findings.
672
- autoScan(config.pluginRoot, sarifStore, logger)
676
+ autoScan(config.pluginRoot, sarifStore, logger, {
677
+ engine: astEngine,
678
+ cyclomaticMax: config.cyclomaticMax,
679
+ })
673
680
  .then((result) => {
674
681
  const scanners = result.results
675
682
  .filter((r) => r.success)
@@ -759,6 +766,17 @@ function renderAutoScanMarkdown(result: import("./scanner/auto-scan.js").AutoSca
759
766
  lines.push("");
760
767
  }
761
768
 
769
+ // Complexity scan
770
+ if (result.complexityScan) {
771
+ const cs = result.complexityScan;
772
+ lines.push("### Cyclomatic complexity scan\n");
773
+ lines.push(`- Files scanned: **${cs.filesScanned}**`);
774
+ lines.push(`- Functions analyzed: **${cs.functionsAnalyzed}**`);
775
+ lines.push(`- Violations: **${cs.violations}**`);
776
+ lines.push(`- Duration: ${(cs.durationMs / 1000).toFixed(1)}s`);
777
+ lines.push("");
778
+ }
779
+
762
780
  // Summary
763
781
  lines.push(
764
782
  `**Total findings ingested:** ${result.totalFindings} in ${(result.totalDurationMs / 1000).toFixed(1)}s`,
@@ -25,7 +25,9 @@ import type { Logger } from "pino";
25
25
  import { detectScanners, type ScannerDetection } from "./detector.js";
26
26
  import { runScanner, type ScannerRunResult } from "./runner.js";
27
27
  import { bootstrapScanner } from "./bootstrap.js";
28
+ import { scanComplexity, type ComplexityScanResult } from "./complexity-scanner.js";
28
29
  import { adaptScannerOutput, type KnownScanner } from "../adapters/index.js";
30
+ import type { TreeSitterEngine } from "../ast/tree-sitter-engine.js";
29
31
  import type { SarifStore } from "../sarif/sarif-store.js";
30
32
 
31
33
  // ── Types ──────────────────────────────────────────────────────────
@@ -53,6 +55,8 @@ export interface AutoScanResult {
53
55
  totalFindings: number;
54
56
  /** Wall-clock time for the entire auto-scan. */
55
57
  totalDurationMs: number;
58
+ /** Result of the built-in cyclomatic complexity scan, when enabled. */
59
+ complexityScan?: ComplexityScanResult;
56
60
  }
57
61
 
58
62
  // ── Orchestrator ───────────────────────────────────────────────────
@@ -93,6 +97,7 @@ export async function autoScan(
93
97
  workspaceRoot: string,
94
98
  sarifStore: SarifStore,
95
99
  logger: Logger,
100
+ options?: { engine?: TreeSitterEngine; cyclomaticMax?: number },
96
101
  ): Promise<AutoScanResult> {
97
102
  const start = Date.now();
98
103
 
@@ -246,10 +251,31 @@ export async function autoScan(
246
251
  await sarifStore.persist();
247
252
  }
248
253
 
254
+ // 5. Run built-in cyclomatic complexity scanner
255
+ let complexityScan: ComplexityScanResult | undefined;
256
+ if (options?.engine) {
257
+ try {
258
+ complexityScan = await scanComplexity(
259
+ workspaceRoot,
260
+ options.engine,
261
+ sarifStore,
262
+ { cyclomaticMax: options.cyclomaticMax ?? 15 },
263
+ logger,
264
+ );
265
+ totalFindings += complexityScan.violations;
266
+ } catch (err) {
267
+ logger.warn(
268
+ { err: (err as Error).message },
269
+ "auto-scan: complexity scanner failed — continuing without it",
270
+ );
271
+ }
272
+ }
273
+
249
274
  return {
250
275
  detected,
251
276
  results,
252
277
  totalFindings,
253
278
  totalDurationMs: Date.now() - start,
279
+ ...(complexityScan ? { complexityScan } : {}),
254
280
  };
255
281
  }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Cyclomatic complexity scanner.
3
+ *
4
+ * Walks the workspace, analyzes each supported source file with
5
+ * tree-sitter, and emits SARIF findings for functions whose cyclomatic
6
+ * complexity exceeds the configured threshold (`cyclomaticMax`).
7
+ *
8
+ * This scanner is an internal analyzer — it is NOT a "known scanner"
9
+ * in the `KnownScanner` union (eslint/semgrep/bandit/stryker). It
10
+ * bypasses the adapter pipeline and writes SARIF directly via
11
+ * `wrapResultsInSarif()` from the common adapter helpers.
12
+ *
13
+ * Severity mapping:
14
+ * - `warning` — CC > threshold but < 2× threshold
15
+ * - `error` — CC >= 2× threshold (aligns with CRAP index hard block at CC >= 30)
16
+ *
17
+ * @module scanner/complexity-scanner
18
+ */
19
+
20
+ import { promises as fs } from "node:fs";
21
+ import { join, relative } from "node:path";
22
+ import type { Logger } from "pino";
23
+
24
+ import { TreeSitterEngine } from "../ast/tree-sitter-engine.js";
25
+ import { detectLanguageFromPath } from "../ast/language-config.js";
26
+ import { wrapResultsInSarif, estimateEffortMinutes } from "../adapters/common.js";
27
+ import type { SarifStore } from "../sarif/sarif-store.js";
28
+ import type { SarifLevel } from "../sarif/sarif-builder.js";
29
+
30
+ // ── Constants ─────────────────────────────────────────────────────
31
+
32
+ /** Directories that should never be scanned. Mirrors `workspace-walker.ts`. */
33
+ const SKIP_DIRS: ReadonlySet<string> = new Set([
34
+ "node_modules",
35
+ ".git",
36
+ "dist",
37
+ "build",
38
+ "out",
39
+ "target",
40
+ ".venv",
41
+ "venv",
42
+ "__pycache__",
43
+ ".cache",
44
+ ".next",
45
+ ".nuxt",
46
+ ".claude-crap",
47
+ ".codesight",
48
+ ]);
49
+
50
+ /** Hard cap on files to prevent unbounded analysis. */
51
+ const MAX_FILES = 20_000;
52
+
53
+ /** SARIF rule ID for cyclomatic complexity violations. */
54
+ const RULE_ID = "complexity/cyclomatic-max";
55
+
56
+ /** Source tool identifier used in SARIF properties. */
57
+ const SOURCE_TOOL = "complexity";
58
+
59
+ // ── Types ─────────────────────────────────────────────────────────
60
+
61
+ /** Result of a complexity scan run. */
62
+ export interface ComplexityScanResult {
63
+ /** Number of source files successfully analyzed. */
64
+ readonly filesScanned: number;
65
+ /** Total number of functions found across all files. */
66
+ readonly functionsAnalyzed: number;
67
+ /** Number of functions that exceeded the threshold. */
68
+ readonly violations: number;
69
+ /** Wall-clock time for the entire scan. */
70
+ readonly durationMs: number;
71
+ }
72
+
73
+ /** Configuration accepted by the scanner. */
74
+ export interface ComplexityScanConfig {
75
+ /** Maximum cyclomatic complexity allowed per function. */
76
+ readonly cyclomaticMax: number;
77
+ }
78
+
79
+ // ── Scanner ───────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Scan a workspace for cyclomatic complexity violations.
83
+ *
84
+ * Walks the directory tree, analyzes each source file with the
85
+ * tree-sitter engine, and emits SARIF findings for functions above
86
+ * the configured threshold. Findings are ingested into the provided
87
+ * `SarifStore` and persisted to disk.
88
+ *
89
+ * @param workspaceRoot Absolute path to the workspace root.
90
+ * @param engine Initialized tree-sitter engine instance.
91
+ * @param sarifStore Live SARIF store to ingest findings into.
92
+ * @param config Scanner configuration (threshold).
93
+ * @param logger Pino logger for progress and error reporting.
94
+ * @returns Summary of what was scanned and found.
95
+ */
96
+ export async function scanComplexity(
97
+ workspaceRoot: string,
98
+ engine: TreeSitterEngine,
99
+ sarifStore: SarifStore,
100
+ config: ComplexityScanConfig,
101
+ logger: Logger,
102
+ ): Promise<ComplexityScanResult> {
103
+ const start = Date.now();
104
+ const threshold = config.cyclomaticMax;
105
+ const errorThreshold = threshold * 2;
106
+
107
+ // 1. Collect supported source files
108
+ const files = await collectSourceFiles(workspaceRoot);
109
+ logger.info(
110
+ { fileCount: files.length, threshold },
111
+ "complexity-scanner: starting analysis",
112
+ );
113
+
114
+ // 2. Analyze each file and collect violations
115
+ const sarifResults: object[] = [];
116
+ let filesScanned = 0;
117
+ let functionsAnalyzed = 0;
118
+ let violations = 0;
119
+
120
+ for (const filePath of files) {
121
+ const language = detectLanguageFromPath(filePath);
122
+ if (!language) continue;
123
+
124
+ try {
125
+ const metrics = await engine.analyzeFile({ filePath, language });
126
+ filesScanned += 1;
127
+ functionsAnalyzed += metrics.functions.length;
128
+
129
+ for (const fn of metrics.functions) {
130
+ if (fn.cyclomaticComplexity <= threshold) continue;
131
+
132
+ const level: SarifLevel =
133
+ fn.cyclomaticComplexity >= errorThreshold ? "error" : "warning";
134
+
135
+ const relPath = relative(workspaceRoot, filePath);
136
+
137
+ sarifResults.push({
138
+ ruleId: RULE_ID,
139
+ level,
140
+ message: {
141
+ text: `Function '${fn.name}' has cyclomatic complexity ${fn.cyclomaticComplexity} (threshold: ${threshold})`,
142
+ },
143
+ locations: [
144
+ {
145
+ physicalLocation: {
146
+ artifactLocation: { uri: relPath },
147
+ region: {
148
+ startLine: fn.startLine,
149
+ startColumn: 1,
150
+ endLine: fn.endLine,
151
+ endColumn: 1,
152
+ },
153
+ },
154
+ },
155
+ ],
156
+ properties: {
157
+ sourceTool: SOURCE_TOOL,
158
+ effortMinutes: estimateEffortMinutes(level),
159
+ cyclomaticComplexity: fn.cyclomaticComplexity,
160
+ },
161
+ });
162
+ violations += 1;
163
+ }
164
+ } catch (err) {
165
+ logger.warn(
166
+ { filePath, err: (err as Error).message },
167
+ "complexity-scanner: failed to analyze file, skipping",
168
+ );
169
+ }
170
+ }
171
+
172
+ // 3. Ingest findings into the SARIF store
173
+ if (sarifResults.length > 0) {
174
+ const document = wrapResultsInSarif(
175
+ SOURCE_TOOL as never,
176
+ "0.1.0",
177
+ sarifResults,
178
+ );
179
+ sarifStore.ingestRun(document, SOURCE_TOOL);
180
+ await sarifStore.persist();
181
+ }
182
+
183
+ const durationMs = Date.now() - start;
184
+ logger.info(
185
+ { filesScanned, functionsAnalyzed, violations, durationMs },
186
+ "complexity-scanner: analysis complete",
187
+ );
188
+
189
+ return { filesScanned, functionsAnalyzed, violations, durationMs };
190
+ }
191
+
192
+ // ── File walker ───────────────────────────────────────────────────
193
+
194
+ /**
195
+ * Collect source files from the workspace that the tree-sitter engine
196
+ * can analyze. Skips directories in `SKIP_DIRS` and hidden directories.
197
+ * Only returns files whose extension maps to a supported language.
198
+ */
199
+ async function collectSourceFiles(workspaceRoot: string): Promise<string[]> {
200
+ const files: string[] = [];
201
+ let truncated = false;
202
+
203
+ async function walk(dir: string): Promise<void> {
204
+ if (truncated) return;
205
+ let entries;
206
+ try {
207
+ entries = await fs.readdir(dir, { withFileTypes: true });
208
+ } catch {
209
+ return;
210
+ }
211
+ for (const entry of entries) {
212
+ if (truncated) return;
213
+ if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
214
+ const full = join(dir, entry.name);
215
+ if (entry.isDirectory()) {
216
+ if (SKIP_DIRS.has(entry.name)) continue;
217
+ await walk(full);
218
+ continue;
219
+ }
220
+ if (!entry.isFile()) continue;
221
+ // Only include files the tree-sitter engine can parse
222
+ if (!detectLanguageFromPath(entry.name)) continue;
223
+ files.push(full);
224
+ if (files.length >= MAX_FILES) {
225
+ truncated = true;
226
+ return;
227
+ }
228
+ }
229
+ }
230
+
231
+ await walk(workspaceRoot);
232
+ return files;
233
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Unit tests for the cyclomatic complexity scanner.
3
+ *
4
+ * These tests verify that the scanner walks a workspace, analyzes source
5
+ * files with tree-sitter, and emits SARIF findings for functions whose
6
+ * cyclomatic complexity exceeds the configured threshold.
7
+ *
8
+ * @module tests/complexity-scanner.test
9
+ */
10
+
11
+ import { describe, it } from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+ import pino from "pino";
17
+
18
+ import { scanComplexity } from "../scanner/complexity-scanner.js";
19
+ import { TreeSitterEngine } from "../ast/tree-sitter-engine.js";
20
+ import { SarifStore } from "../sarif/sarif-store.js";
21
+
22
+ const logger = pino({ level: "silent" });
23
+
24
+ function makeTmpDir(): string {
25
+ return mkdtempSync(join(tmpdir(), "crap-complexity-"));
26
+ }
27
+
28
+ /** Simple function: CC = 1 (straight-line). */
29
+ const SIMPLE_TS = `
30
+ export function greet(name: string): string {
31
+ return "hello " + name;
32
+ }
33
+ `;
34
+
35
+ /** Function with CC = 5: 4 if-branches + 1 baseline. */
36
+ const COMPLEX_TS = `
37
+ export function classify(x: number): string {
38
+ if (x < 0) return "negative";
39
+ if (x === 0) return "zero";
40
+ if (x < 10) return "small";
41
+ if (x < 100) return "medium";
42
+ return "large";
43
+ }
44
+ `;
45
+
46
+ /**
47
+ * Extremely complex function: many branches to push CC well above 30.
48
+ * Each if/else-if adds +1, plus boolean operators.
49
+ */
50
+ const EXTREME_TS = `
51
+ export function extremelyComplex(a: number, b: number, c: number): string {
52
+ if (a > 0) { return "a1"; }
53
+ if (a > 1) { return "a2"; }
54
+ if (a > 2) { return "a3"; }
55
+ if (a > 3) { return "a4"; }
56
+ if (a > 4) { return "a5"; }
57
+ if (a > 5) { return "a6"; }
58
+ if (a > 6) { return "a7"; }
59
+ if (a > 7) { return "a8"; }
60
+ if (a > 8) { return "a9"; }
61
+ if (a > 9) { return "a10"; }
62
+ if (b > 0) { return "b1"; }
63
+ if (b > 1) { return "b2"; }
64
+ if (b > 2) { return "b3"; }
65
+ if (b > 3) { return "b4"; }
66
+ if (b > 4) { return "b5"; }
67
+ if (b > 5) { return "b6"; }
68
+ if (b > 6) { return "b7"; }
69
+ if (b > 7) { return "b8"; }
70
+ if (b > 8) { return "b9"; }
71
+ if (b > 9) { return "b10"; }
72
+ if (c > 0) { return "c1"; }
73
+ if (c > 1) { return "c2"; }
74
+ if (c > 2) { return "c3"; }
75
+ if (c > 3) { return "c4"; }
76
+ if (c > 4) { return "c5"; }
77
+ if (c > 5) { return "c6"; }
78
+ if (c > 6) { return "c7"; }
79
+ if (c > 7) { return "c8"; }
80
+ if (c > 8) { return "c9"; }
81
+ if (c > 9) { return "c10"; }
82
+ return "default";
83
+ }
84
+ `;
85
+
86
+ describe("scanComplexity", () => {
87
+ const engine = new TreeSitterEngine();
88
+
89
+ it("returns zero violations for an empty workspace", async () => {
90
+ const dir = makeTmpDir();
91
+ try {
92
+ const store = new SarifStore({
93
+ workspaceRoot: dir,
94
+ outputDir: join(dir, ".claude-crap/reports"),
95
+ });
96
+ const result = await scanComplexity(
97
+ dir, engine, store, { cyclomaticMax: 15 }, logger,
98
+ );
99
+ assert.equal(result.violations, 0);
100
+ assert.equal(result.filesScanned, 0);
101
+ assert.equal(result.functionsAnalyzed, 0);
102
+ assert.ok(result.durationMs >= 0);
103
+ } finally {
104
+ rmSync(dir, { recursive: true, force: true });
105
+ }
106
+ });
107
+
108
+ it("finds no violations when all functions are below threshold", async () => {
109
+ const dir = makeTmpDir();
110
+ try {
111
+ writeFileSync(join(dir, "simple.ts"), SIMPLE_TS);
112
+ const store = new SarifStore({
113
+ workspaceRoot: dir,
114
+ outputDir: join(dir, ".claude-crap/reports"),
115
+ });
116
+ const result = await scanComplexity(
117
+ dir, engine, store, { cyclomaticMax: 15 }, logger,
118
+ );
119
+ assert.equal(result.violations, 0);
120
+ assert.equal(result.filesScanned, 1);
121
+ assert.ok(result.functionsAnalyzed >= 1);
122
+ } finally {
123
+ rmSync(dir, { recursive: true, force: true });
124
+ }
125
+ });
126
+
127
+ it("flags functions above cyclomaticMax as warnings", async () => {
128
+ const dir = makeTmpDir();
129
+ try {
130
+ writeFileSync(join(dir, "complex.ts"), COMPLEX_TS);
131
+ const store = new SarifStore({
132
+ workspaceRoot: dir,
133
+ outputDir: join(dir, ".claude-crap/reports"),
134
+ });
135
+ // Set threshold to 3 so the CC=5 function triggers
136
+ const result = await scanComplexity(
137
+ dir, engine, store, { cyclomaticMax: 3 }, logger,
138
+ );
139
+ assert.equal(result.violations, 1);
140
+
141
+ // Verify the finding is in the SARIF store
142
+ const findings = store.list();
143
+ const complexityFindings = findings.filter(
144
+ (f) => f.ruleId === "complexity/cyclomatic-max",
145
+ );
146
+ assert.equal(complexityFindings.length, 1);
147
+ assert.equal(complexityFindings[0]!.level, "warning");
148
+ assert.equal(complexityFindings[0]!.sourceTool, "complexity");
149
+ assert.ok(complexityFindings[0]!.message.includes("classify"));
150
+ } finally {
151
+ rmSync(dir, { recursive: true, force: true });
152
+ }
153
+ });
154
+
155
+ it("emits error level for functions at >= 2x threshold", async () => {
156
+ const dir = makeTmpDir();
157
+ try {
158
+ writeFileSync(join(dir, "extreme.ts"), EXTREME_TS);
159
+ const store = new SarifStore({
160
+ workspaceRoot: dir,
161
+ outputDir: join(dir, ".claude-crap/reports"),
162
+ });
163
+ // threshold=15, CC=31 → 31 >= 30 (2x15) → error
164
+ const result = await scanComplexity(
165
+ dir, engine, store, { cyclomaticMax: 15 }, logger,
166
+ );
167
+ assert.equal(result.violations, 1);
168
+
169
+ const findings = store.list();
170
+ const complexityFindings = findings.filter(
171
+ (f) => f.ruleId === "complexity/cyclomatic-max",
172
+ );
173
+ assert.equal(complexityFindings.length, 1);
174
+ assert.equal(complexityFindings[0]!.level, "error");
175
+ } finally {
176
+ rmSync(dir, { recursive: true, force: true });
177
+ }
178
+ });
179
+
180
+ it("skips directories in SKIP_DIRS", async () => {
181
+ const dir = makeTmpDir();
182
+ try {
183
+ // Put a complex file inside node_modules — should be skipped
184
+ const nmDir = join(dir, "node_modules", "pkg");
185
+ mkdirSync(nmDir, { recursive: true });
186
+ writeFileSync(join(nmDir, "index.ts"), COMPLEX_TS);
187
+
188
+ const store = new SarifStore({
189
+ workspaceRoot: dir,
190
+ outputDir: join(dir, ".claude-crap/reports"),
191
+ });
192
+ const result = await scanComplexity(
193
+ dir, engine, store, { cyclomaticMax: 3 }, logger,
194
+ );
195
+ assert.equal(result.filesScanned, 0);
196
+ assert.equal(result.violations, 0);
197
+ } finally {
198
+ rmSync(dir, { recursive: true, force: true });
199
+ }
200
+ });
201
+
202
+ it("skips unsupported file extensions", async () => {
203
+ const dir = makeTmpDir();
204
+ try {
205
+ writeFileSync(join(dir, "data.json"), '{"key": "value"}');
206
+ writeFileSync(join(dir, "readme.md"), "# Hello");
207
+ writeFileSync(join(dir, "styles.css"), "body {}");
208
+
209
+ const store = new SarifStore({
210
+ workspaceRoot: dir,
211
+ outputDir: join(dir, ".claude-crap/reports"),
212
+ });
213
+ const result = await scanComplexity(
214
+ dir, engine, store, { cyclomaticMax: 15 }, logger,
215
+ );
216
+ assert.equal(result.filesScanned, 0);
217
+ } finally {
218
+ rmSync(dir, { recursive: true, force: true });
219
+ }
220
+ });
221
+
222
+ it("respects custom cyclomaticMax threshold", async () => {
223
+ const dir = makeTmpDir();
224
+ try {
225
+ writeFileSync(join(dir, "complex.ts"), COMPLEX_TS);
226
+ const store = new SarifStore({
227
+ workspaceRoot: dir,
228
+ outputDir: join(dir, ".claude-crap/reports"),
229
+ });
230
+ // CC=5, threshold=10 → no violation
231
+ const result = await scanComplexity(
232
+ dir, engine, store, { cyclomaticMax: 10 }, logger,
233
+ );
234
+ assert.equal(result.violations, 0);
235
+ } finally {
236
+ rmSync(dir, { recursive: true, force: true });
237
+ }
238
+ });
239
+
240
+ it("sets effortMinutes and cyclomaticComplexity in finding properties", async () => {
241
+ const dir = makeTmpDir();
242
+ try {
243
+ writeFileSync(join(dir, "complex.ts"), COMPLEX_TS);
244
+ const store = new SarifStore({
245
+ workspaceRoot: dir,
246
+ outputDir: join(dir, ".claude-crap/reports"),
247
+ });
248
+ const result = await scanComplexity(
249
+ dir, engine, store, { cyclomaticMax: 3 }, logger,
250
+ );
251
+ assert.equal(result.violations, 1);
252
+
253
+ const findings = store.list();
254
+ const finding = findings.find((f) => f.ruleId === "complexity/cyclomatic-max");
255
+ assert.ok(finding);
256
+ assert.equal(typeof finding.properties?.effortMinutes, "number");
257
+ assert.ok((finding.properties?.effortMinutes as number) > 0);
258
+ assert.equal(typeof finding.properties?.cyclomaticComplexity, "number");
259
+ } finally {
260
+ rmSync(dir, { recursive: true, force: true });
261
+ }
262
+ });
263
+ });