claude-crap 0.3.7 → 0.4.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/CHANGELOG.md +33 -0
- package/README.md +74 -7
- package/dist/adapters/common.d.ts +1 -1
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +1 -1
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/dart-analyzer.d.ts +41 -0
- package/dist/adapters/dart-analyzer.d.ts.map +1 -0
- package/dist/adapters/dart-analyzer.js +120 -0
- package/dist/adapters/dart-analyzer.js.map +1 -0
- package/dist/adapters/dotnet-format.d.ts +35 -0
- package/dist/adapters/dotnet-format.d.ts.map +1 -0
- package/dist/adapters/dotnet-format.js +96 -0
- package/dist/adapters/dotnet-format.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/crap-config.d.ts +4 -0
- package/dist/crap-config.d.ts.map +1 -1
- package/dist/crap-config.js +51 -28
- package/dist/crap-config.js.map +1 -1
- package/dist/dashboard/file-detail.d.ts.map +1 -1
- package/dist/dashboard/file-detail.js.map +1 -1
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +7 -12
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +89 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics/workspace-walker.d.ts +4 -1
- package/dist/metrics/workspace-walker.d.ts.map +1 -1
- package/dist/metrics/workspace-walker.js +12 -28
- package/dist/metrics/workspace-walker.js.map +1 -1
- package/dist/monorepo/project-map.d.ts +112 -0
- package/dist/monorepo/project-map.d.ts.map +1 -0
- package/dist/monorepo/project-map.js +384 -0
- package/dist/monorepo/project-map.js.map +1 -0
- package/dist/scanner/auto-scan.d.ts +1 -0
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +14 -5
- package/dist/scanner/auto-scan.js.map +1 -1
- package/dist/scanner/bootstrap.d.ts +1 -1
- package/dist/scanner/bootstrap.d.ts.map +1 -1
- package/dist/scanner/bootstrap.js +15 -1
- package/dist/scanner/bootstrap.js.map +1 -1
- package/dist/scanner/complexity-scanner.d.ts +2 -0
- package/dist/scanner/complexity-scanner.d.ts.map +1 -1
- package/dist/scanner/complexity-scanner.js +11 -26
- package/dist/scanner/complexity-scanner.js.map +1 -1
- package/dist/scanner/detector.d.ts +24 -4
- package/dist/scanner/detector.d.ts.map +1 -1
- package/dist/scanner/detector.js +110 -10
- package/dist/scanner/detector.js.map +1 -1
- package/dist/scanner/runner.d.ts +4 -1
- package/dist/scanner/runner.d.ts.map +1 -1
- package/dist/scanner/runner.js +25 -3
- package/dist/scanner/runner.js.map +1 -1
- package/dist/schemas/tool-schemas.d.ts +16 -1
- package/dist/schemas/tool-schemas.d.ts.map +1 -1
- package/dist/schemas/tool-schemas.js +16 -1
- package/dist/schemas/tool-schemas.js.map +1 -1
- package/dist/shared/exclusions.d.ts +53 -0
- package/dist/shared/exclusions.d.ts.map +1 -0
- package/dist/shared/exclusions.js +126 -0
- package/dist/shared/exclusions.js.map +1 -0
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +37 -0
- package/plugin/bundle/mcp-server.mjs +762 -144
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package-lock.json +15 -2
- package/plugin/package.json +2 -1
- package/scripts/bundle-plugin.mjs +2 -1
- package/src/adapters/common.ts +1 -1
- package/src/adapters/dart-analyzer.ts +161 -0
- package/src/adapters/dotnet-format.ts +125 -0
- package/src/adapters/index.ts +8 -0
- package/src/crap-config.ts +78 -18
- package/src/dashboard/file-detail.ts +0 -2
- package/src/dashboard/server.ts +9 -10
- package/src/index.ts +103 -5
- package/src/metrics/workspace-walker.ts +15 -27
- package/src/monorepo/project-map.ts +476 -0
- package/src/scanner/auto-scan.ts +17 -6
- package/src/scanner/bootstrap.ts +18 -1
- package/src/scanner/complexity-scanner.ts +15 -26
- package/src/scanner/detector.ts +119 -10
- package/src/scanner/runner.ts +25 -2
- package/src/schemas/tool-schemas.ts +17 -1
- package/src/shared/exclusions.ts +156 -0
- package/src/tests/adapters/dispatch.test.ts +2 -2
- package/src/tests/auto-scan.test.ts +2 -2
- package/src/tests/boot-monorepo.test.ts +804 -0
- package/src/tests/boot-scanner-detection.test.ts +692 -0
- package/src/tests/boot-single-project.test.ts +780 -0
- package/src/tests/exclusions.test.ts +117 -0
- package/src/tests/integration/mcp-server.integration.test.ts +2 -1
- package/src/tests/project-map.test.ts +302 -0
- package/src/tests/scanner-detector.test.ts +31 -11
package/plugin/package-lock.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-crap-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-crap-plugin",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.4.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@fastify/static": "^8.0.3",
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
13
13
|
"fastify": "^5.2.0",
|
|
14
|
+
"picomatch": "^2.3.0",
|
|
14
15
|
"pino": "^9.5.0",
|
|
15
16
|
"tree-sitter-wasms": "^0.1.12",
|
|
16
17
|
"web-tree-sitter": "^0.24.4"
|
|
@@ -2069,6 +2070,18 @@
|
|
|
2069
2070
|
"url": "https://opencollective.com/express"
|
|
2070
2071
|
}
|
|
2071
2072
|
},
|
|
2073
|
+
"node_modules/picomatch": {
|
|
2074
|
+
"version": "2.3.2",
|
|
2075
|
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
|
2076
|
+
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
|
2077
|
+
"license": "MIT",
|
|
2078
|
+
"engines": {
|
|
2079
|
+
"node": ">=8.6"
|
|
2080
|
+
},
|
|
2081
|
+
"funding": {
|
|
2082
|
+
"url": "https://github.com/sponsors/jonschlinkert"
|
|
2083
|
+
}
|
|
2084
|
+
},
|
|
2072
2085
|
"node_modules/pino": {
|
|
2073
2086
|
"version": "9.14.0",
|
|
2074
2087
|
"resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
|
package/plugin/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-crap-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Runtime dependencies for the claude-crap plugin bundle",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"fastify": "^5.2.0",
|
|
11
11
|
"pino": "^9.5.0",
|
|
12
12
|
"tree-sitter-wasms": "^0.1.12",
|
|
13
|
+
"picomatch": "^2.3.0",
|
|
13
14
|
"web-tree-sitter": "^0.24.4"
|
|
14
15
|
},
|
|
15
16
|
"engines": {
|
package/src/adapters/common.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type { SarifLevel } from "../sarif/sarif-builder.js";
|
|
|
23
23
|
* `ingest_scanner_output` MCP tool uses this as its `enum` constraint,
|
|
24
24
|
* so keeping it narrow prevents drift.
|
|
25
25
|
*/
|
|
26
|
-
export const KNOWN_SCANNERS = ["semgrep", "eslint", "bandit", "stryker"] as const;
|
|
26
|
+
export const KNOWN_SCANNERS = ["semgrep", "eslint", "bandit", "stryker", "dart_analyze", "dotnet_format"] as const;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Union of supported scanner identifiers.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: `dart analyze --format=json` → SARIF 2.1.0.
|
|
3
|
+
*
|
|
4
|
+
* The Dart analyzer emits JSON with this shape:
|
|
5
|
+
*
|
|
6
|
+
* {
|
|
7
|
+
* "version": 1,
|
|
8
|
+
* "diagnostics": [
|
|
9
|
+
* {
|
|
10
|
+
* "code": "unused_import",
|
|
11
|
+
* "severity": "WARNING",
|
|
12
|
+
* "type": "STATIC_WARNING",
|
|
13
|
+
* "location": {
|
|
14
|
+
* "file": "/absolute/path/to/file.dart",
|
|
15
|
+
* "range": {
|
|
16
|
+
* "start": { "offset": 7, "line": 1, "column": 8 },
|
|
17
|
+
* "end": { "offset": 16, "line": 1, "column": 17 }
|
|
18
|
+
* }
|
|
19
|
+
* },
|
|
20
|
+
* "problemMessage": "Unused import: 'dart:io'.",
|
|
21
|
+
* "correctionMessage": "Try removing the import directive.",
|
|
22
|
+
* "documentation": "https://dart.dev/diagnostics/unused_import"
|
|
23
|
+
* }
|
|
24
|
+
* ]
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* Severity mapping:
|
|
28
|
+
* - "ERROR" → SARIF "error" (30 min effort)
|
|
29
|
+
* - "WARNING" → SARIF "warning" (15 min effort)
|
|
30
|
+
* - "INFO" → SARIF "note" (5 min effort)
|
|
31
|
+
*
|
|
32
|
+
* @module adapters/dart-analyzer
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
type AdapterResult,
|
|
37
|
+
wrapResultsInSarif,
|
|
38
|
+
estimateEffortMinutes,
|
|
39
|
+
} from "./common.js";
|
|
40
|
+
import type { SarifLevel } from "../sarif/sarif-builder.js";
|
|
41
|
+
|
|
42
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
interface DartDiagnosticLocation {
|
|
45
|
+
file: string;
|
|
46
|
+
range: {
|
|
47
|
+
start: { offset: number; line: number; column: number };
|
|
48
|
+
end: { offset: number; line: number; column: number };
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface DartDiagnostic {
|
|
53
|
+
code: string;
|
|
54
|
+
severity: string;
|
|
55
|
+
type: string;
|
|
56
|
+
location: DartDiagnosticLocation;
|
|
57
|
+
problemMessage: string;
|
|
58
|
+
correctionMessage?: string;
|
|
59
|
+
documentation?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface DartAnalyzeOutput {
|
|
63
|
+
version: number;
|
|
64
|
+
diagnostics: DartDiagnostic[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Severity mapping ───────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function mapSeverity(dartSeverity: string): SarifLevel {
|
|
70
|
+
switch (dartSeverity.toUpperCase()) {
|
|
71
|
+
case "ERROR":
|
|
72
|
+
return "error";
|
|
73
|
+
case "WARNING":
|
|
74
|
+
return "warning";
|
|
75
|
+
case "INFO":
|
|
76
|
+
return "note";
|
|
77
|
+
default:
|
|
78
|
+
return "warning";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Effort estimates per severity ──────────────────────────────────
|
|
83
|
+
|
|
84
|
+
const EFFORT_BY_SEVERITY: Record<SarifLevel, number> = {
|
|
85
|
+
error: 30,
|
|
86
|
+
warning: 15,
|
|
87
|
+
note: 5,
|
|
88
|
+
none: 0,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Convert `dart analyze --format=json` output to SARIF 2.1.0.
|
|
95
|
+
*
|
|
96
|
+
* @param rawOutput The JSON string or pre-parsed object from `dart analyze`.
|
|
97
|
+
*/
|
|
98
|
+
export function adaptDartAnalyzer(rawOutput: unknown): AdapterResult {
|
|
99
|
+
let parsed: DartAnalyzeOutput;
|
|
100
|
+
|
|
101
|
+
if (typeof rawOutput === "string") {
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(rawOutput) as DartAnalyzeOutput;
|
|
104
|
+
} catch {
|
|
105
|
+
throw new Error("[dart-analyzer adapter] rawOutput is not valid JSON");
|
|
106
|
+
}
|
|
107
|
+
} else if (rawOutput && typeof rawOutput === "object" && "diagnostics" in rawOutput) {
|
|
108
|
+
parsed = rawOutput as DartAnalyzeOutput;
|
|
109
|
+
} else {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"[dart-analyzer adapter] rawOutput must be a JSON string or an object with a 'diagnostics' array",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!Array.isArray(parsed.diagnostics)) {
|
|
116
|
+
throw new Error("[dart-analyzer adapter] 'diagnostics' must be an array");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const results: object[] = [];
|
|
120
|
+
let totalEffortMinutes = 0;
|
|
121
|
+
|
|
122
|
+
for (const diag of parsed.diagnostics) {
|
|
123
|
+
const level = mapSeverity(diag.severity);
|
|
124
|
+
const effort = EFFORT_BY_SEVERITY[level] ?? estimateEffortMinutes(level);
|
|
125
|
+
totalEffortMinutes += effort;
|
|
126
|
+
|
|
127
|
+
results.push({
|
|
128
|
+
ruleId: diag.code,
|
|
129
|
+
level,
|
|
130
|
+
message: {
|
|
131
|
+
text: diag.problemMessage + (diag.correctionMessage ? ` ${diag.correctionMessage}` : ""),
|
|
132
|
+
},
|
|
133
|
+
locations: [
|
|
134
|
+
{
|
|
135
|
+
physicalLocation: {
|
|
136
|
+
artifactLocation: {
|
|
137
|
+
uri: diag.location.file,
|
|
138
|
+
},
|
|
139
|
+
region: {
|
|
140
|
+
startLine: diag.location.range.start.line,
|
|
141
|
+
startColumn: diag.location.range.start.column,
|
|
142
|
+
endLine: diag.location.range.end.line,
|
|
143
|
+
endColumn: diag.location.range.end.column,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
properties: {
|
|
149
|
+
effortMinutes: effort,
|
|
150
|
+
...(diag.documentation ? { helpUri: diag.documentation } : {}),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
document: wrapResultsInSarif("dart_analyze", "1.0.0", results),
|
|
157
|
+
sourceTool: "dart_analyze",
|
|
158
|
+
findingCount: parsed.diagnostics.length,
|
|
159
|
+
totalEffortMinutes,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: `dotnet format --report <path>` JSON output → SARIF 2.1.0.
|
|
3
|
+
*
|
|
4
|
+
* The dotnet format tool emits a JSON array with this shape:
|
|
5
|
+
*
|
|
6
|
+
* [
|
|
7
|
+
* {
|
|
8
|
+
* "DocumentId": { "ProjectId": { "Id": "..." }, "Id": "..." },
|
|
9
|
+
* "FileName": "AuthController.cs",
|
|
10
|
+
* "FilePath": "/absolute/path/to/AuthController.cs",
|
|
11
|
+
* "FileChanges": [
|
|
12
|
+
* {
|
|
13
|
+
* "LineNumber": 84,
|
|
14
|
+
* "CharNumber": 16,
|
|
15
|
+
* "DiagnosticId": "WHITESPACE",
|
|
16
|
+
* "FormatDescription": "Fix whitespace formatting. Delete 5 characters."
|
|
17
|
+
* }
|
|
18
|
+
* ]
|
|
19
|
+
* }
|
|
20
|
+
* ]
|
|
21
|
+
*
|
|
22
|
+
* All dotnet format findings are style/formatting issues, so they
|
|
23
|
+
* map uniformly to SARIF "warning" level with a 5-minute effort
|
|
24
|
+
* estimate (formatting fixes are quick, mechanical changes).
|
|
25
|
+
*
|
|
26
|
+
* @module adapters/dotnet-format
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
type AdapterResult,
|
|
31
|
+
wrapResultsInSarif,
|
|
32
|
+
} from "./common.js";
|
|
33
|
+
|
|
34
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
interface DotnetFileChange {
|
|
37
|
+
LineNumber: number;
|
|
38
|
+
CharNumber: number;
|
|
39
|
+
DiagnosticId: string;
|
|
40
|
+
FormatDescription: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface DotnetFormatDocument {
|
|
44
|
+
DocumentId: {
|
|
45
|
+
ProjectId: { Id: string };
|
|
46
|
+
Id: string;
|
|
47
|
+
};
|
|
48
|
+
FileName: string;
|
|
49
|
+
FilePath: string;
|
|
50
|
+
FileChanges: DotnetFileChange[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Convert `dotnet format --report <path>` JSON output to SARIF 2.1.0.
|
|
57
|
+
*
|
|
58
|
+
* @param rawOutput The JSON string or pre-parsed array from `dotnet format`.
|
|
59
|
+
*/
|
|
60
|
+
export function adaptDotnetFormat(rawOutput: unknown): AdapterResult {
|
|
61
|
+
let parsed: DotnetFormatDocument[];
|
|
62
|
+
|
|
63
|
+
if (typeof rawOutput === "string") {
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(rawOutput) as DotnetFormatDocument[];
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error("[dotnet-format adapter] rawOutput is not valid JSON");
|
|
68
|
+
}
|
|
69
|
+
} else if (Array.isArray(rawOutput)) {
|
|
70
|
+
parsed = rawOutput as DotnetFormatDocument[];
|
|
71
|
+
} else {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"[dotnet-format adapter] rawOutput must be a JSON string or an array of document entries",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!Array.isArray(parsed)) {
|
|
78
|
+
throw new Error("[dotnet-format adapter] parsed output must be an array");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const EFFORT_MINUTES = 5;
|
|
82
|
+
const results: object[] = [];
|
|
83
|
+
let findingCount = 0;
|
|
84
|
+
let totalEffortMinutes = 0;
|
|
85
|
+
|
|
86
|
+
for (const doc of parsed) {
|
|
87
|
+
if (!Array.isArray(doc.FileChanges)) continue;
|
|
88
|
+
|
|
89
|
+
for (const change of doc.FileChanges) {
|
|
90
|
+
findingCount++;
|
|
91
|
+
totalEffortMinutes += EFFORT_MINUTES;
|
|
92
|
+
|
|
93
|
+
results.push({
|
|
94
|
+
ruleId: change.DiagnosticId,
|
|
95
|
+
level: "warning",
|
|
96
|
+
message: {
|
|
97
|
+
text: change.FormatDescription,
|
|
98
|
+
},
|
|
99
|
+
locations: [
|
|
100
|
+
{
|
|
101
|
+
physicalLocation: {
|
|
102
|
+
artifactLocation: {
|
|
103
|
+
uri: doc.FilePath,
|
|
104
|
+
},
|
|
105
|
+
region: {
|
|
106
|
+
startLine: change.LineNumber,
|
|
107
|
+
startColumn: change.CharNumber,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
properties: {
|
|
113
|
+
effortMinutes: EFFORT_MINUTES,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
document: wrapResultsInSarif("dotnet_format", "1.0.0", results),
|
|
121
|
+
sourceTool: "dotnet_format",
|
|
122
|
+
findingCount,
|
|
123
|
+
totalEffortMinutes,
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/adapters/index.ts
CHANGED
|
@@ -30,6 +30,8 @@ export { adaptSemgrep } from "./semgrep.js";
|
|
|
30
30
|
export { adaptEslint } from "./eslint.js";
|
|
31
31
|
export { adaptBandit } from "./bandit.js";
|
|
32
32
|
export { adaptStryker } from "./stryker.js";
|
|
33
|
+
export { adaptDartAnalyzer } from "./dart-analyzer.js";
|
|
34
|
+
export { adaptDotnetFormat } from "./dotnet-format.js";
|
|
33
35
|
|
|
34
36
|
export {
|
|
35
37
|
DEFAULT_EFFORT_BY_SEVERITY,
|
|
@@ -44,6 +46,8 @@ import { adaptSemgrep } from "./semgrep.js";
|
|
|
44
46
|
import { adaptEslint } from "./eslint.js";
|
|
45
47
|
import { adaptBandit } from "./bandit.js";
|
|
46
48
|
import { adaptStryker } from "./stryker.js";
|
|
49
|
+
import { adaptDartAnalyzer } from "./dart-analyzer.js";
|
|
50
|
+
import { adaptDotnetFormat } from "./dotnet-format.js";
|
|
47
51
|
import type { AdapterResult, KnownScanner } from "./common.js";
|
|
48
52
|
|
|
49
53
|
/**
|
|
@@ -70,6 +74,10 @@ export function adaptScannerOutput(
|
|
|
70
74
|
return adaptBandit(rawOutput);
|
|
71
75
|
case "stryker":
|
|
72
76
|
return adaptStryker(rawOutput);
|
|
77
|
+
case "dart_analyze":
|
|
78
|
+
return adaptDartAnalyzer(rawOutput);
|
|
79
|
+
case "dotnet_format":
|
|
80
|
+
return adaptDotnetFormat(rawOutput);
|
|
73
81
|
default: {
|
|
74
82
|
const exhaustive: never = scanner;
|
|
75
83
|
throw new Error(`[adapters] Unknown scanner: ${String(exhaustive)}`);
|
package/src/crap-config.ts
CHANGED
|
@@ -81,6 +81,10 @@ export interface CrapConfig {
|
|
|
81
81
|
readonly strictness: Strictness;
|
|
82
82
|
/** Where the strictness value actually came from. Useful for diagnostics. */
|
|
83
83
|
readonly strictnessSource: "env" | "file" | "default";
|
|
84
|
+
/** User-defined exclusion patterns (directories with trailing `/`, or file globs). */
|
|
85
|
+
readonly exclude: ReadonlyArray<string>;
|
|
86
|
+
/** Relative paths to directories containing sub-projects (e.g. `["apps", "packages"]`). */
|
|
87
|
+
readonly projectDirs: ReadonlyArray<string>;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
/**
|
|
@@ -107,6 +111,12 @@ export interface LoadCrapConfigOptions {
|
|
|
107
111
|
* @throws {@link CrapConfigError} on any invalid input.
|
|
108
112
|
*/
|
|
109
113
|
export function loadCrapConfig(options: LoadCrapConfigOptions): CrapConfig {
|
|
114
|
+
// Always read the file to extract `exclude`, even when strictness
|
|
115
|
+
// comes from the environment variable.
|
|
116
|
+
const fileResult = readFromFile(options.workspaceRoot);
|
|
117
|
+
const exclude = fileResult?.exclude ?? [];
|
|
118
|
+
const projectDirs = fileResult?.projectDirs ?? [];
|
|
119
|
+
|
|
110
120
|
const envRaw = process.env["CLAUDE_CRAP_STRICTNESS"];
|
|
111
121
|
if (typeof envRaw === "string" && envRaw.trim() !== "") {
|
|
112
122
|
const normalized = envRaw.trim().toLowerCase();
|
|
@@ -116,13 +126,14 @@ export function loadCrapConfig(options: LoadCrapConfigOptions): CrapConfig {
|
|
|
116
126
|
`Expected one of: ${STRICTNESS_VALUES.join(", ")}.`,
|
|
117
127
|
);
|
|
118
128
|
}
|
|
119
|
-
return { strictness: normalized, strictnessSource: "env" };
|
|
129
|
+
return { strictness: normalized, strictnessSource: "env", exclude, projectDirs };
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
if (fileResult?.strictness) {
|
|
133
|
+
return { strictness: fileResult.strictness, strictnessSource: "file", exclude, projectDirs };
|
|
134
|
+
}
|
|
124
135
|
|
|
125
|
-
return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default" };
|
|
136
|
+
return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default", exclude, projectDirs };
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
/**
|
|
@@ -138,7 +149,13 @@ export function loadCrapConfig(options: LoadCrapConfigOptions): CrapConfig {
|
|
|
138
149
|
* @returns The validated strictness, or `null` when no
|
|
139
150
|
* file is present.
|
|
140
151
|
*/
|
|
141
|
-
|
|
152
|
+
interface FileResult {
|
|
153
|
+
strictness: Strictness | null;
|
|
154
|
+
exclude: string[];
|
|
155
|
+
projectDirs: string[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readFromFile(workspaceRoot: string): FileResult | null {
|
|
142
159
|
const filePath = join(workspaceRoot, ".claude-crap.json");
|
|
143
160
|
let raw: string;
|
|
144
161
|
try {
|
|
@@ -166,22 +183,65 @@ function readFromFile(workspaceRoot: string): Strictness | null {
|
|
|
166
183
|
);
|
|
167
184
|
}
|
|
168
185
|
const doc = parsed as Record<string, unknown>;
|
|
169
|
-
if (!("strictness" in doc)) return null;
|
|
170
186
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
187
|
+
// Parse strictness
|
|
188
|
+
let strictness: Strictness | null = null;
|
|
189
|
+
if ("strictness" in doc) {
|
|
190
|
+
const value = doc["strictness"];
|
|
191
|
+
if (typeof value !== "string") {
|
|
192
|
+
throw new CrapConfigError(
|
|
193
|
+
`[crap-config] ${filePath}: 'strictness' must be a string, got ${typeof value}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const normalized = value.trim().toLowerCase();
|
|
197
|
+
if (!isStrictness(normalized)) {
|
|
198
|
+
throw new CrapConfigError(
|
|
199
|
+
`[crap-config] ${filePath}: 'strictness' is "${value}"; ` +
|
|
200
|
+
`expected one of ${STRICTNESS_VALUES.join(", ")}.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
strictness = normalized;
|
|
176
204
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
)
|
|
205
|
+
|
|
206
|
+
// Parse exclude
|
|
207
|
+
let exclude: string[] = [];
|
|
208
|
+
if ("exclude" in doc) {
|
|
209
|
+
const raw = doc["exclude"];
|
|
210
|
+
if (!Array.isArray(raw)) {
|
|
211
|
+
throw new CrapConfigError(
|
|
212
|
+
`[crap-config] ${filePath}: 'exclude' must be an array of strings`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
for (const item of raw) {
|
|
216
|
+
if (typeof item !== "string") {
|
|
217
|
+
throw new CrapConfigError(
|
|
218
|
+
`[crap-config] ${filePath}: every entry in 'exclude' must be a string, got ${typeof item}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
exclude = raw as string[];
|
|
183
223
|
}
|
|
184
|
-
|
|
224
|
+
|
|
225
|
+
// Parse projectDirs
|
|
226
|
+
let projectDirs: string[] = [];
|
|
227
|
+
if ("projectDirs" in doc) {
|
|
228
|
+
const raw = doc["projectDirs"];
|
|
229
|
+
if (!Array.isArray(raw)) {
|
|
230
|
+
throw new CrapConfigError(
|
|
231
|
+
`[crap-config] ${filePath}: 'projectDirs' must be an array of strings`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
for (const item of raw) {
|
|
235
|
+
if (typeof item !== "string") {
|
|
236
|
+
throw new CrapConfigError(
|
|
237
|
+
`[crap-config] ${filePath}: every entry in 'projectDirs' must be a string, got ${typeof item}`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
projectDirs = raw as string[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { strictness, exclude, projectDirs };
|
|
185
245
|
}
|
|
186
246
|
|
|
187
247
|
/**
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { promises as fs } from "node:fs";
|
|
18
|
-
import { join } from "node:path";
|
|
19
|
-
|
|
20
18
|
import { resolveWithinWorkspace } from "../workspace-guard.js";
|
|
21
19
|
import { detectLanguageFromPath, type SupportedLanguage } from "../ast/language-config.js";
|
|
22
20
|
import type { TreeSitterEngine, FunctionMetrics } from "../ast/tree-sitter-engine.js";
|
package/src/dashboard/server.ts
CHANGED
|
@@ -33,6 +33,7 @@ import fastifyStatic from "@fastify/static";
|
|
|
33
33
|
import type { Logger } from "pino";
|
|
34
34
|
|
|
35
35
|
import type { CrapConfig } from "../config.js";
|
|
36
|
+
import { createExclusionFilter } from "../shared/exclusions.js";
|
|
36
37
|
import {
|
|
37
38
|
computeProjectScore,
|
|
38
39
|
type ProjectScore,
|
|
@@ -64,6 +65,8 @@ export interface StartDashboardOptions {
|
|
|
64
65
|
readonly logger: Logger;
|
|
65
66
|
/** Tree-sitter engine for the /api/complexity endpoint. */
|
|
66
67
|
readonly astEngine?: TreeSitterEngine;
|
|
68
|
+
/** User-defined exclusion patterns from .claude-crap.json. */
|
|
69
|
+
readonly exclude?: ReadonlyArray<string>;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
/**
|
|
@@ -104,7 +107,7 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
104
107
|
// ------------------------------------------------------------------
|
|
105
108
|
// /api/health — liveness probe
|
|
106
109
|
// ------------------------------------------------------------------
|
|
107
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.
|
|
110
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.4.0" }));
|
|
108
111
|
|
|
109
112
|
// ------------------------------------------------------------------
|
|
110
113
|
// /api/score — live project score
|
|
@@ -127,7 +130,7 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
127
130
|
if (!options.astEngine) {
|
|
128
131
|
return { threshold: config.cyclomaticMax, totalFunctions: 0, violationCount: 0, topFunctions: [] };
|
|
129
132
|
}
|
|
130
|
-
return buildComplexityReport(config, options.astEngine, logger);
|
|
133
|
+
return buildComplexityReport(config, options.astEngine, logger, options.exclude);
|
|
131
134
|
});
|
|
132
135
|
|
|
133
136
|
// ------------------------------------------------------------------
|
|
@@ -394,12 +397,7 @@ interface ComplexityReport {
|
|
|
394
397
|
topFunctions: ComplexityEntry[];
|
|
395
398
|
}
|
|
396
399
|
|
|
397
|
-
|
|
398
|
-
const SKIP_DIRS: ReadonlySet<string> = new Set([
|
|
399
|
-
"node_modules", ".git", "dist", "build", "out", "target",
|
|
400
|
-
".venv", "venv", "__pycache__", ".cache", ".next", ".nuxt",
|
|
401
|
-
".claude-crap", ".codesight",
|
|
402
|
-
]);
|
|
400
|
+
// Directory exclusions are now centralized in src/shared/exclusions.ts.
|
|
403
401
|
|
|
404
402
|
/**
|
|
405
403
|
* Walk the workspace and collect per-function complexity metrics,
|
|
@@ -410,8 +408,10 @@ async function buildComplexityReport(
|
|
|
410
408
|
config: CrapConfig,
|
|
411
409
|
engine: TreeSitterEngine,
|
|
412
410
|
logger: Logger,
|
|
411
|
+
exclude?: ReadonlyArray<string>,
|
|
413
412
|
): Promise<ComplexityReport> {
|
|
414
413
|
const threshold = config.cyclomaticMax;
|
|
414
|
+
const filter = createExclusionFilter(exclude);
|
|
415
415
|
const allFunctions: ComplexityEntry[] = [];
|
|
416
416
|
let totalFunctions = 0;
|
|
417
417
|
|
|
@@ -423,10 +423,9 @@ async function buildComplexityReport(
|
|
|
423
423
|
return;
|
|
424
424
|
}
|
|
425
425
|
for (const entry of entries) {
|
|
426
|
-
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
|
|
427
426
|
const full = join(dir, entry.name);
|
|
428
427
|
if (entry.isDirectory()) {
|
|
429
|
-
if (
|
|
428
|
+
if (filter.shouldSkipDir(entry.name)) continue;
|
|
430
429
|
await walk(full);
|
|
431
430
|
continue;
|
|
432
431
|
}
|