claude-crap 0.3.6 → 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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-crap-plugin",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-crap-plugin",
9
- "version": "0.3.6",
9
+ "version": "0.3.7",
10
10
  "dependencies": {
11
11
  "@fastify/static": "^8.0.3",
12
12
  "@modelcontextprotocol/sdk": "^1.0.4",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crap-plugin",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for the claude-crap plugin bundle",
6
6
  "type": "module",
@@ -0,0 +1,197 @@
1
+ /**
2
+ * File detail builder for the dashboard.
3
+ *
4
+ * Given a workspace-relative file path, this module produces a rich
5
+ * detail payload combining source code, per-function AST metrics, and
6
+ * SARIF findings filtered to that file. The dashboard uses this to
7
+ * render a ReportGenerator-style annotated code view.
8
+ *
9
+ * The builder is extracted into its own module (rather than inlined in
10
+ * `server.ts`) so that:
11
+ * - The logic is unit-testable without booting the HTTP server.
12
+ * - The types are importable by both the Fastify route and tests.
13
+ *
14
+ * @module dashboard/file-detail
15
+ */
16
+
17
+ import { promises as fs } from "node:fs";
18
+ import { join } from "node:path";
19
+
20
+ import { resolveWithinWorkspace } from "../workspace-guard.js";
21
+ import { detectLanguageFromPath, type SupportedLanguage } from "../ast/language-config.js";
22
+ import type { TreeSitterEngine, FunctionMetrics } from "../ast/tree-sitter-engine.js";
23
+ import type { SarifStore, IngestedFinding } from "../sarif/sarif-store.js";
24
+
25
+ // ── Types ─────────────────────────────────────────────────────────
26
+
27
+ /** Per-function entry in the detail response. */
28
+ export interface FileDetailFunction {
29
+ readonly name: string;
30
+ readonly startLine: number;
31
+ readonly endLine: number;
32
+ readonly cyclomaticComplexity: number;
33
+ readonly lineCount: number;
34
+ }
35
+
36
+ /** Per-finding entry in the detail response. */
37
+ export interface FileDetailFinding {
38
+ readonly ruleId: string;
39
+ readonly level: string;
40
+ readonly message: string;
41
+ readonly sourceTool: string;
42
+ readonly startLine: number;
43
+ readonly startColumn: number;
44
+ readonly endLine: number;
45
+ readonly endColumn: number;
46
+ readonly effortMinutes: number;
47
+ }
48
+
49
+ /** Summary statistics for the file. */
50
+ export interface FileDetailSummary {
51
+ readonly totalFindings: number;
52
+ readonly errorCount: number;
53
+ readonly warningCount: number;
54
+ readonly noteCount: number;
55
+ readonly totalEffortMinutes: number;
56
+ readonly avgComplexity: number;
57
+ readonly maxComplexity: number;
58
+ }
59
+
60
+ /** Full response payload for the file detail endpoint. */
61
+ export interface FileDetailResponse {
62
+ readonly filePath: string;
63
+ readonly language: SupportedLanguage | null;
64
+ readonly physicalLoc: number;
65
+ readonly logicalLoc: number;
66
+ readonly cyclomaticMax: number;
67
+ readonly sourceLines: string[];
68
+ readonly functions: FileDetailFunction[];
69
+ readonly findings: FileDetailFinding[];
70
+ readonly summary: FileDetailSummary;
71
+ }
72
+
73
+ /** Input accepted by {@link buildFileDetail}. */
74
+ export interface BuildFileDetailInput {
75
+ readonly relativePath: string;
76
+ readonly workspaceRoot: string;
77
+ readonly astEngine?: TreeSitterEngine | undefined;
78
+ readonly sarifStore: SarifStore;
79
+ readonly cyclomaticMax: number;
80
+ }
81
+
82
+ // ── Builder ───────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Build the file detail payload. Pure function aside from the file
86
+ * read and the tree-sitter analysis (both deterministic for a given
87
+ * file).
88
+ *
89
+ * @throws When the file does not exist or the path escapes the workspace.
90
+ */
91
+ export async function buildFileDetail(
92
+ input: BuildFileDetailInput,
93
+ ): Promise<FileDetailResponse> {
94
+ const { relativePath, workspaceRoot, astEngine, sarifStore, cyclomaticMax } = input;
95
+
96
+ // 1. Guard against path traversal
97
+ const absolutePath = resolveWithinWorkspace(workspaceRoot, relativePath);
98
+
99
+ // 2. Read source
100
+ const source = await fs.readFile(absolutePath, "utf8");
101
+ const sourceLines = source.split(/\r?\n/);
102
+ // Remove trailing empty line from files ending with \n
103
+ if (sourceLines.length > 0 && sourceLines[sourceLines.length - 1] === "") {
104
+ sourceLines.pop();
105
+ }
106
+
107
+ const physicalLoc = sourceLines.length;
108
+ let logicalLoc = 0;
109
+ for (const line of sourceLines) {
110
+ if (line.trim().length > 0) logicalLoc += 1;
111
+ }
112
+
113
+ // 3. AST analysis (if language is supported)
114
+ const language = detectLanguageFromPath(relativePath);
115
+ let functions: FileDetailFunction[] = [];
116
+
117
+ if (language && astEngine) {
118
+ try {
119
+ const metrics = await astEngine.analyzeFile({
120
+ filePath: absolutePath,
121
+ language,
122
+ });
123
+ functions = metrics.functions.map((fn: FunctionMetrics) => ({
124
+ name: fn.name,
125
+ startLine: fn.startLine,
126
+ endLine: fn.endLine,
127
+ cyclomaticComplexity: fn.cyclomaticComplexity,
128
+ lineCount: fn.lineCount,
129
+ }));
130
+ } catch {
131
+ // Analysis failure is non-fatal — return empty functions
132
+ }
133
+ }
134
+
135
+ // 4. Filter SARIF findings for this file
136
+ const allFindings = sarifStore.list();
137
+ const fileFindings = allFindings.filter(
138
+ (f: IngestedFinding) => f.location.uri === relativePath,
139
+ );
140
+
141
+ const findings: FileDetailFinding[] = fileFindings.map((f: IngestedFinding) => ({
142
+ ruleId: f.ruleId,
143
+ level: f.level,
144
+ message: f.message,
145
+ sourceTool: f.sourceTool,
146
+ startLine: f.location.startLine,
147
+ startColumn: f.location.startColumn,
148
+ endLine: f.location.endLine ?? f.location.startLine,
149
+ endColumn: f.location.endColumn ?? 0,
150
+ effortMinutes:
151
+ typeof f.properties?.effortMinutes === "number"
152
+ ? f.properties.effortMinutes
153
+ : 0,
154
+ }));
155
+
156
+ // 5. Build summary
157
+ let errorCount = 0;
158
+ let warningCount = 0;
159
+ let noteCount = 0;
160
+ let totalEffortMinutes = 0;
161
+
162
+ for (const f of findings) {
163
+ if (f.level === "error") errorCount += 1;
164
+ else if (f.level === "warning") warningCount += 1;
165
+ else if (f.level === "note") noteCount += 1;
166
+ totalEffortMinutes += f.effortMinutes;
167
+ }
168
+
169
+ const complexities = functions.map((f) => f.cyclomaticComplexity);
170
+ const maxComplexity = complexities.length > 0 ? Math.max(...complexities) : 0;
171
+ const avgComplexity =
172
+ complexities.length > 0
173
+ ? Math.round(
174
+ (complexities.reduce((a, b) => a + b, 0) / complexities.length) * 100,
175
+ ) / 100
176
+ : 0;
177
+
178
+ return {
179
+ filePath: relativePath,
180
+ language,
181
+ physicalLoc,
182
+ logicalLoc,
183
+ cyclomaticMax,
184
+ sourceLines,
185
+ functions,
186
+ findings,
187
+ summary: {
188
+ totalFindings: findings.length,
189
+ errorCount,
190
+ warningCount,
191
+ noteCount,
192
+ totalEffortMinutes,
193
+ avgComplexity,
194
+ maxComplexity,
195
+ },
196
+ };
197
+ }