claude-crap 0.3.6 → 0.3.8

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 (88) hide show
  1. package/README.md +25 -0
  2. package/dist/adapters/common.d.ts +1 -1
  3. package/dist/adapters/common.d.ts.map +1 -1
  4. package/dist/adapters/common.js +1 -1
  5. package/dist/adapters/common.js.map +1 -1
  6. package/dist/adapters/dart-analyzer.d.ts +41 -0
  7. package/dist/adapters/dart-analyzer.d.ts.map +1 -0
  8. package/dist/adapters/dart-analyzer.js +120 -0
  9. package/dist/adapters/dart-analyzer.js.map +1 -0
  10. package/dist/adapters/index.d.ts +1 -0
  11. package/dist/adapters/index.d.ts.map +1 -1
  12. package/dist/adapters/index.js +4 -0
  13. package/dist/adapters/index.js.map +1 -1
  14. package/dist/crap-config.d.ts +2 -0
  15. package/dist/crap-config.d.ts.map +1 -1
  16. package/dist/crap-config.js +36 -28
  17. package/dist/crap-config.js.map +1 -1
  18. package/dist/dashboard/file-detail.d.ts +77 -0
  19. package/dist/dashboard/file-detail.d.ts.map +1 -0
  20. package/dist/dashboard/file-detail.js +120 -0
  21. package/dist/dashboard/file-detail.js.map +1 -0
  22. package/dist/dashboard/server.d.ts +5 -0
  23. package/dist/dashboard/server.d.ts.map +1 -1
  24. package/dist/dashboard/server.js +103 -1
  25. package/dist/dashboard/server.js.map +1 -1
  26. package/dist/index.js +36 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/metrics/workspace-walker.d.ts +4 -1
  29. package/dist/metrics/workspace-walker.d.ts.map +1 -1
  30. package/dist/metrics/workspace-walker.js +12 -28
  31. package/dist/metrics/workspace-walker.js.map +1 -1
  32. package/dist/scanner/auto-scan.d.ts +9 -1
  33. package/dist/scanner/auto-scan.d.ts.map +1 -1
  34. package/dist/scanner/auto-scan.js +27 -5
  35. package/dist/scanner/auto-scan.js.map +1 -1
  36. package/dist/scanner/bootstrap.d.ts +1 -1
  37. package/dist/scanner/bootstrap.d.ts.map +1 -1
  38. package/dist/scanner/bootstrap.js +9 -0
  39. package/dist/scanner/bootstrap.js.map +1 -1
  40. package/dist/scanner/complexity-scanner.d.ts +56 -0
  41. package/dist/scanner/complexity-scanner.d.ts.map +1 -0
  42. package/dist/scanner/complexity-scanner.js +161 -0
  43. package/dist/scanner/complexity-scanner.js.map +1 -0
  44. package/dist/scanner/detector.d.ts +24 -4
  45. package/dist/scanner/detector.d.ts.map +1 -1
  46. package/dist/scanner/detector.js +105 -10
  47. package/dist/scanner/detector.js.map +1 -1
  48. package/dist/scanner/runner.d.ts +4 -1
  49. package/dist/scanner/runner.d.ts.map +1 -1
  50. package/dist/scanner/runner.js +12 -3
  51. package/dist/scanner/runner.js.map +1 -1
  52. package/dist/schemas/tool-schemas.d.ts +1 -1
  53. package/dist/schemas/tool-schemas.js +1 -1
  54. package/dist/schemas/tool-schemas.js.map +1 -1
  55. package/dist/shared/exclusions.d.ts +53 -0
  56. package/dist/shared/exclusions.d.ts.map +1 -0
  57. package/dist/shared/exclusions.js +126 -0
  58. package/dist/shared/exclusions.js.map +1 -0
  59. package/package.json +3 -1
  60. package/plugin/.claude-plugin/plugin.json +1 -1
  61. package/plugin/bundle/dashboard/public/index.html +432 -12
  62. package/plugin/bundle/mcp-server.mjs +747 -137
  63. package/plugin/bundle/mcp-server.mjs.map +4 -4
  64. package/plugin/package-lock.json +15 -2
  65. package/plugin/package.json +2 -1
  66. package/scripts/bundle-plugin.mjs +2 -1
  67. package/src/adapters/common.ts +1 -1
  68. package/src/adapters/dart-analyzer.ts +161 -0
  69. package/src/adapters/index.ts +4 -0
  70. package/src/crap-config.ts +55 -18
  71. package/src/dashboard/file-detail.ts +195 -0
  72. package/src/dashboard/public/index.html +432 -12
  73. package/src/dashboard/server.ts +140 -1
  74. package/src/index.ts +37 -4
  75. package/src/metrics/workspace-walker.ts +15 -27
  76. package/src/scanner/auto-scan.ts +41 -4
  77. package/src/scanner/bootstrap.ts +11 -0
  78. package/src/scanner/complexity-scanner.ts +222 -0
  79. package/src/scanner/detector.ts +114 -10
  80. package/src/scanner/runner.ts +12 -2
  81. package/src/schemas/tool-schemas.ts +1 -1
  82. package/src/shared/exclusions.ts +156 -0
  83. package/src/tests/adapters/dispatch.test.ts +2 -2
  84. package/src/tests/auto-scan.test.ts +2 -2
  85. package/src/tests/complexity-scanner.test.ts +263 -0
  86. package/src/tests/exclusions.test.ts +117 -0
  87. package/src/tests/file-detail-api.test.ts +258 -0
  88. package/src/tests/scanner-detector.test.ts +31 -11
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Unit tests for the /api/file-detail endpoint logic.
3
+ *
4
+ * These tests exercise the file detail builder function directly,
5
+ * without spawning the full MCP server. They verify that source lines,
6
+ * function metrics, and filtered findings are returned correctly.
7
+ *
8
+ * @module tests/file-detail-api.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
+
17
+ import { buildFileDetail } from "../dashboard/file-detail.js";
18
+ import { TreeSitterEngine } from "../ast/tree-sitter-engine.js";
19
+ import { SarifStore } from "../sarif/sarif-store.js";
20
+ import { wrapResultsInSarif } from "../adapters/common.js";
21
+
22
+ function makeTmpDir(): string {
23
+ return mkdtempSync(join(tmpdir(), "crap-file-detail-"));
24
+ }
25
+
26
+ const SAMPLE_TS = `export function greet(name: string): string {
27
+ return "hello " + name;
28
+ }
29
+
30
+ export function classify(x: number): string {
31
+ if (x < 0) return "negative";
32
+ if (x === 0) return "zero";
33
+ if (x < 10) return "small";
34
+ if (x < 100) return "medium";
35
+ return "large";
36
+ }
37
+ `;
38
+
39
+ describe("buildFileDetail", () => {
40
+ const engine = new TreeSitterEngine();
41
+
42
+ it("returns source lines for a valid file", async () => {
43
+ const dir = makeTmpDir();
44
+ try {
45
+ writeFileSync(join(dir, "hello.ts"), SAMPLE_TS);
46
+ const store = new SarifStore({
47
+ workspaceRoot: dir,
48
+ outputDir: join(dir, ".claude-crap/reports"),
49
+ });
50
+ const result = await buildFileDetail({
51
+ relativePath: "hello.ts",
52
+ workspaceRoot: dir,
53
+ astEngine: engine,
54
+ sarifStore: store,
55
+ cyclomaticMax: 15,
56
+ });
57
+ assert.equal(result.filePath, "hello.ts");
58
+ assert.ok(result.sourceLines.length > 0);
59
+ assert.ok(result.sourceLines[0]!.includes("export function greet"));
60
+ } finally {
61
+ rmSync(dir, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ it("returns function metrics for supported languages", async () => {
66
+ const dir = makeTmpDir();
67
+ try {
68
+ writeFileSync(join(dir, "hello.ts"), SAMPLE_TS);
69
+ const store = new SarifStore({
70
+ workspaceRoot: dir,
71
+ outputDir: join(dir, ".claude-crap/reports"),
72
+ });
73
+ const result = await buildFileDetail({
74
+ relativePath: "hello.ts",
75
+ workspaceRoot: dir,
76
+ astEngine: engine,
77
+ sarifStore: store,
78
+ cyclomaticMax: 15,
79
+ });
80
+ assert.equal(result.language, "typescript");
81
+ assert.ok(result.functions.length >= 2);
82
+ const greet = result.functions.find((f) => f.name === "greet");
83
+ assert.ok(greet);
84
+ assert.equal(greet.cyclomaticComplexity, 1);
85
+ const classify = result.functions.find((f) => f.name === "classify");
86
+ assert.ok(classify);
87
+ assert.ok(classify.cyclomaticComplexity >= 4);
88
+ } finally {
89
+ rmSync(dir, { recursive: true, force: true });
90
+ }
91
+ });
92
+
93
+ it("filters findings to the requested file only", async () => {
94
+ const dir = makeTmpDir();
95
+ try {
96
+ writeFileSync(join(dir, "a.ts"), 'export const a = 1;\n');
97
+ writeFileSync(join(dir, "b.ts"), 'export const b = 2;\n');
98
+
99
+ const store = new SarifStore({
100
+ workspaceRoot: dir,
101
+ outputDir: join(dir, ".claude-crap/reports"),
102
+ });
103
+
104
+ // Ingest findings for both files
105
+ const doc = wrapResultsInSarif("eslint" as never, "0.1.0", [
106
+ {
107
+ ruleId: "no-unused-vars",
108
+ level: "warning",
109
+ message: { text: "unused in a" },
110
+ locations: [{
111
+ physicalLocation: {
112
+ artifactLocation: { uri: "a.ts" },
113
+ region: { startLine: 1, startColumn: 1 },
114
+ },
115
+ }],
116
+ properties: { sourceTool: "eslint", effortMinutes: 30 },
117
+ },
118
+ {
119
+ ruleId: "no-unused-vars",
120
+ level: "warning",
121
+ message: { text: "unused in b" },
122
+ locations: [{
123
+ physicalLocation: {
124
+ artifactLocation: { uri: "b.ts" },
125
+ region: { startLine: 1, startColumn: 1 },
126
+ },
127
+ }],
128
+ properties: { sourceTool: "eslint", effortMinutes: 30 },
129
+ },
130
+ ]);
131
+ store.ingestRun(doc, "eslint");
132
+
133
+ const result = await buildFileDetail({
134
+ relativePath: "a.ts",
135
+ workspaceRoot: dir,
136
+ astEngine: engine,
137
+ sarifStore: store,
138
+ cyclomaticMax: 15,
139
+ });
140
+
141
+ assert.equal(result.findings.length, 1);
142
+ assert.ok(result.findings[0]!.message.includes("unused in a"));
143
+ assert.equal(result.summary.totalFindings, 1);
144
+ } finally {
145
+ rmSync(dir, { recursive: true, force: true });
146
+ }
147
+ });
148
+
149
+ it("throws for non-existent file", async () => {
150
+ const dir = makeTmpDir();
151
+ try {
152
+ const store = new SarifStore({
153
+ workspaceRoot: dir,
154
+ outputDir: join(dir, ".claude-crap/reports"),
155
+ });
156
+ await assert.rejects(
157
+ buildFileDetail({
158
+ relativePath: "nonexistent.ts",
159
+ workspaceRoot: dir,
160
+ astEngine: engine,
161
+ sarifStore: store,
162
+ cyclomaticMax: 15,
163
+ }),
164
+ /not found|ENOENT/i,
165
+ );
166
+ } finally {
167
+ rmSync(dir, { recursive: true, force: true });
168
+ }
169
+ });
170
+
171
+ it("rejects path traversal attempts", async () => {
172
+ const dir = makeTmpDir();
173
+ try {
174
+ const store = new SarifStore({
175
+ workspaceRoot: dir,
176
+ outputDir: join(dir, ".claude-crap/reports"),
177
+ });
178
+ await assert.rejects(
179
+ buildFileDetail({
180
+ relativePath: "../../etc/passwd",
181
+ workspaceRoot: dir,
182
+ astEngine: engine,
183
+ sarifStore: store,
184
+ cyclomaticMax: 15,
185
+ }),
186
+ /escapes the workspace/i,
187
+ );
188
+ } finally {
189
+ rmSync(dir, { recursive: true, force: true });
190
+ }
191
+ });
192
+
193
+ it("returns empty functions for unsupported languages", async () => {
194
+ const dir = makeTmpDir();
195
+ try {
196
+ writeFileSync(join(dir, "data.json"), '{"key": "value"}\n');
197
+ const store = new SarifStore({
198
+ workspaceRoot: dir,
199
+ outputDir: join(dir, ".claude-crap/reports"),
200
+ });
201
+ const result = await buildFileDetail({
202
+ relativePath: "data.json",
203
+ workspaceRoot: dir,
204
+ astEngine: engine,
205
+ sarifStore: store,
206
+ cyclomaticMax: 15,
207
+ });
208
+ assert.equal(result.language, null);
209
+ assert.equal(result.functions.length, 0);
210
+ } finally {
211
+ rmSync(dir, { recursive: true, force: true });
212
+ }
213
+ });
214
+
215
+ it("computes summary with correct effort and complexity stats", async () => {
216
+ const dir = makeTmpDir();
217
+ try {
218
+ writeFileSync(join(dir, "hello.ts"), SAMPLE_TS);
219
+ const store = new SarifStore({
220
+ workspaceRoot: dir,
221
+ outputDir: join(dir, ".claude-crap/reports"),
222
+ });
223
+
224
+ const doc = wrapResultsInSarif("eslint" as never, "0.1.0", [
225
+ {
226
+ ruleId: "no-magic-numbers",
227
+ level: "warning",
228
+ message: { text: "magic number" },
229
+ locations: [{
230
+ physicalLocation: {
231
+ artifactLocation: { uri: "hello.ts" },
232
+ region: { startLine: 6, startColumn: 7 },
233
+ },
234
+ }],
235
+ properties: { sourceTool: "eslint", effortMinutes: 30 },
236
+ },
237
+ ]);
238
+ store.ingestRun(doc, "eslint");
239
+
240
+ const result = await buildFileDetail({
241
+ relativePath: "hello.ts",
242
+ workspaceRoot: dir,
243
+ astEngine: engine,
244
+ sarifStore: store,
245
+ cyclomaticMax: 15,
246
+ });
247
+
248
+ assert.equal(result.summary.totalFindings, 1);
249
+ assert.equal(result.summary.warningCount, 1);
250
+ assert.equal(result.summary.errorCount, 0);
251
+ assert.ok(result.summary.totalEffortMinutes >= 30);
252
+ assert.ok(result.summary.maxComplexity >= 4); // classify has CC>=4
253
+ assert.ok(result.summary.avgComplexity > 0);
254
+ } finally {
255
+ rmSync(dir, { recursive: true, force: true });
256
+ }
257
+ });
258
+ });
@@ -90,24 +90,34 @@ describe("detectScanners", () => {
90
90
  }
91
91
  });
92
92
 
93
- it("detects eslint from package.json devDependencies", async () => {
93
+ it("detects eslint from package.json — not installed vs installed", async () => {
94
94
  const dir = makeTmpDir();
95
95
  try {
96
96
  writeFileSync(
97
97
  join(dir, "package.json"),
98
98
  JSON.stringify({ devDependencies: { eslint: "^9.0.0" } }),
99
99
  );
100
+ // Without node_modules/.bin/eslint — declared but not installed
100
101
  const results = await detectScanners(dir);
101
102
  const eslint = results.find((r) => r.scanner === "eslint");
102
103
  assert.ok(eslint);
103
- assert.equal(eslint.available, true);
104
- assert.ok(eslint.reason.includes("package.json"));
104
+ assert.equal(eslint.available, false);
105
+ assert.ok(eslint.reason.includes("not installed"));
106
+
107
+ // With binary present — installed
108
+ mkdirSync(join(dir, "node_modules", ".bin"), { recursive: true });
109
+ writeFileSync(join(dir, "node_modules", ".bin", "eslint"), "");
110
+ const results2 = await detectScanners(dir);
111
+ const eslint2 = results2.find((r) => r.scanner === "eslint");
112
+ assert.ok(eslint2);
113
+ assert.equal(eslint2.available, true);
114
+ assert.ok(eslint2.reason.includes("installed"));
105
115
  } finally {
106
116
  rmSync(dir, { recursive: true, force: true });
107
117
  }
108
118
  });
109
119
 
110
- it("detects stryker from package.json @stryker-mutator/core", async () => {
120
+ it("detects stryker from package.json — not installed vs installed", async () => {
111
121
  const dir = makeTmpDir();
112
122
  try {
113
123
  writeFileSync(
@@ -116,11 +126,21 @@ describe("detectScanners", () => {
116
126
  devDependencies: { "@stryker-mutator/core": "^7.0.0" },
117
127
  }),
118
128
  );
129
+ // Without binary — declared but not installed
119
130
  const results = await detectScanners(dir);
120
131
  const stryker = results.find((r) => r.scanner === "stryker");
121
132
  assert.ok(stryker);
122
- assert.equal(stryker.available, true);
123
- assert.ok(stryker.reason.includes("package.json"));
133
+ assert.equal(stryker.available, false);
134
+ assert.ok(stryker.reason.includes("not installed"));
135
+
136
+ // With binary present — installed
137
+ mkdirSync(join(dir, "node_modules", ".bin"), { recursive: true });
138
+ writeFileSync(join(dir, "node_modules", ".bin", "stryker"), "");
139
+ const results2 = await detectScanners(dir);
140
+ const stryker2 = results2.find((r) => r.scanner === "stryker");
141
+ assert.ok(stryker2);
142
+ assert.equal(stryker2.available, true);
143
+ assert.ok(stryker2.reason.includes("installed"));
124
144
  } finally {
125
145
  rmSync(dir, { recursive: true, force: true });
126
146
  }
@@ -133,9 +153,9 @@ describe("detectScanners", () => {
133
153
  // Config and package.json probes will all fail.
134
154
  // Binary probe results depend on the host — don't assert on those,
135
155
  // but do assert the structure is correct.
136
- assert.equal(results.length, 4);
156
+ assert.equal(results.length, 5);
137
157
  for (const r of results) {
138
- assert.ok(["eslint", "semgrep", "bandit", "stryker"].includes(r.scanner));
158
+ assert.ok(["eslint", "semgrep", "bandit", "stryker", "dart_analyze"].includes(r.scanner));
139
159
  assert.equal(typeof r.available, "boolean");
140
160
  assert.equal(typeof r.reason, "string");
141
161
  }
@@ -150,7 +170,7 @@ describe("detectScanners", () => {
150
170
  writeFileSync(join(dir, "package.json"), "not json at all");
151
171
  // Should not throw — just skip the package.json probe
152
172
  const results = await detectScanners(dir);
153
- assert.equal(results.length, 4);
173
+ assert.equal(results.length, 5);
154
174
  } finally {
155
175
  rmSync(dir, { recursive: true, force: true });
156
176
  }
@@ -172,10 +192,10 @@ describe("detectScanners", () => {
172
192
  }
173
193
  });
174
194
 
175
- it("SCANNER_SIGNALS covers all four scanners", () => {
195
+ it("SCANNER_SIGNALS covers all supported scanners", () => {
176
196
  assert.deepEqual(
177
197
  Object.keys(SCANNER_SIGNALS).sort(),
178
- ["bandit", "eslint", "semgrep", "stryker"],
198
+ ["bandit", "dart_analyze", "eslint", "semgrep", "stryker"],
179
199
  );
180
200
  });
181
201
  });