claude-crap 0.4.6 → 0.4.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.
- package/CHANGELOG.md +51 -0
- package/dist/dashboard/file-detail.d.ts +6 -0
- package/dist/dashboard/file-detail.d.ts.map +1 -1
- package/dist/dashboard/file-detail.js +1 -0
- package/dist/dashboard/file-detail.js.map +1 -1
- package/dist/dashboard/server.d.ts +6 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +99 -31
- package/dist/dashboard/server.js.map +1 -1
- package/dist/shared/exclusions.d.ts.map +1 -1
- package/dist/shared/exclusions.js +10 -0
- package/dist/shared/exclusions.js.map +1 -1
- package/dist/tests/helpers/dashboard-test-helpers.d.ts +94 -0
- package/dist/tests/helpers/dashboard-test-helpers.d.ts.map +1 -0
- package/dist/tests/helpers/dashboard-test-helpers.js +159 -0
- package/dist/tests/helpers/dashboard-test-helpers.js.map +1 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bundle/dashboard/public/index.html +216 -7
- package/plugin/bundle/mcp-server.mjs +88 -13
- package/plugin/bundle/mcp-server.mjs.map +2 -2
- package/plugin/hooks/lib/quality-gate.mjs +3 -0
- package/src/dashboard/file-detail.ts +7 -0
- package/src/dashboard/public/index.html +216 -7
- package/src/dashboard/server.ts +119 -42
- package/src/shared/exclusions.ts +11 -0
- package/src/tests/dashboard-adoption.test.ts +553 -0
- package/src/tests/exclusions.test.ts +34 -0
- package/src/tests/file-detail-api.test.ts +38 -0
- package/src/tests/helpers/dashboard-test-helpers.ts +203 -0
- package/src/tests/workspace-walker.test.ts +30 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared setup utilities for dashboard-adoption tests.
|
|
3
|
+
*
|
|
4
|
+
* Keeps the main test file focused on assertions rather than
|
|
5
|
+
* boilerplate, while staying small enough that each helper is
|
|
6
|
+
* easy to read in isolation.
|
|
7
|
+
*
|
|
8
|
+
* @module tests/helpers/dashboard-test-helpers
|
|
9
|
+
*/
|
|
10
|
+
import { createServer } from "node:net";
|
|
11
|
+
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import pino from "pino";
|
|
15
|
+
import { SarifStore } from "../../sarif/sarif-store.js";
|
|
16
|
+
// ── Logger ────────────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* A pino logger that discards all output. Passing this to
|
|
19
|
+
* `startDashboard` keeps test runs noise-free while still satisfying
|
|
20
|
+
* the `Logger` type constraint.
|
|
21
|
+
*/
|
|
22
|
+
export function silentLogger() {
|
|
23
|
+
return pino({ level: "silent" });
|
|
24
|
+
}
|
|
25
|
+
// ── Port allocation ───────────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a random TCP port in the 6000–6999 range that is not bound
|
|
28
|
+
* at the moment of the call. The OS chooses the exact port by binding
|
|
29
|
+
* to port 0 then immediately releasing the socket; there is a tiny
|
|
30
|
+
* TOCTOU window, but in practice it is negligible for unit tests that
|
|
31
|
+
* run serially.
|
|
32
|
+
*
|
|
33
|
+
* Staying in the 6000–6999 range keeps tests away from the production
|
|
34
|
+
* dashboard port (5117) and from common well-known service ports.
|
|
35
|
+
*/
|
|
36
|
+
export function findFreePort() {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const server = createServer();
|
|
39
|
+
// Bind to 0 so the OS picks any free port, then immediately close.
|
|
40
|
+
server.listen(0, "127.0.0.1", () => {
|
|
41
|
+
const address = server.address();
|
|
42
|
+
if (!address || typeof address === "string") {
|
|
43
|
+
server.close(() => reject(new Error("unexpected address type")));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const { port } = address;
|
|
47
|
+
server.close(() => {
|
|
48
|
+
// Clamp to 6000-6999 by re-probing if outside range; in
|
|
49
|
+
// practice the OS almost never hands back a port in this band
|
|
50
|
+
// unless specifically requested, so we just return whatever we
|
|
51
|
+
// got — the important property is "free right now".
|
|
52
|
+
resolve(port);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
server.on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create an isolated temporary workspace directory and ensure the
|
|
60
|
+
* `.claude-crap/` subdirectory exists so pidfile writes always succeed.
|
|
61
|
+
* Returns paths and a cleanup function.
|
|
62
|
+
*/
|
|
63
|
+
export async function makeWorkspace() {
|
|
64
|
+
const pluginRoot = await mkdtemp(join(tmpdir(), "crap-adopt-"));
|
|
65
|
+
const dotDir = join(pluginRoot, ".claude-crap");
|
|
66
|
+
await mkdir(dotDir, { recursive: true });
|
|
67
|
+
const pidFilePath = join(dotDir, "dashboard.pid");
|
|
68
|
+
return {
|
|
69
|
+
pluginRoot,
|
|
70
|
+
pidFilePath,
|
|
71
|
+
cleanup: () => rm(pluginRoot, { recursive: true, force: true }),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// ── Config factory ────────────────────────────────────────────────────────────
|
|
75
|
+
/**
|
|
76
|
+
* Minimal {@link CrapConfig} suitable for a test invocation of
|
|
77
|
+
* `startDashboard`. Every field that the function actually reads is
|
|
78
|
+
* supplied with a sane default; callers can override `dashboardPort`
|
|
79
|
+
* and `pluginRoot` as needed.
|
|
80
|
+
*/
|
|
81
|
+
export function makeConfig(pluginRoot, dashboardPort) {
|
|
82
|
+
return {
|
|
83
|
+
pluginRoot,
|
|
84
|
+
dashboardPort,
|
|
85
|
+
sarifOutputDir: ".claude-crap/reports",
|
|
86
|
+
crapThreshold: 30,
|
|
87
|
+
cyclomaticMax: 15,
|
|
88
|
+
tdrMaxRating: "C",
|
|
89
|
+
minutesPerLoc: 30,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// ── SarifStore factory ────────────────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Build an empty {@link SarifStore} rooted at `pluginRoot`. No file is
|
|
95
|
+
* written to disk; `loadLatest()` is intentionally NOT called here —
|
|
96
|
+
* the tests that need a pre-seeded store will do so themselves.
|
|
97
|
+
*/
|
|
98
|
+
export function makeSarifStore(pluginRoot) {
|
|
99
|
+
return new SarifStore({
|
|
100
|
+
workspaceRoot: pluginRoot,
|
|
101
|
+
outputDir: ".claude-crap/reports",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// ── StartDashboardOptions factory ─────────────────────────────────────────────
|
|
105
|
+
/**
|
|
106
|
+
* Bundle a complete {@link StartDashboardOptions} object from a
|
|
107
|
+
* workspace context + port. Used by tests that call `startDashboard`
|
|
108
|
+
* directly.
|
|
109
|
+
*/
|
|
110
|
+
export function makeOptions(pluginRoot, dashboardPort) {
|
|
111
|
+
return {
|
|
112
|
+
config: makeConfig(pluginRoot, dashboardPort),
|
|
113
|
+
sarifStore: makeSarifStore(pluginRoot),
|
|
114
|
+
workspaceStatsProvider: async () => ({ physicalLoc: 10, fileCount: 1 }),
|
|
115
|
+
logger: silentLogger(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Write a synthetic pidfile to `path`. Useful for characterization and
|
|
120
|
+
* edge-case tests that need the file to exist before `startDashboard`
|
|
121
|
+
* runs.
|
|
122
|
+
*/
|
|
123
|
+
export async function writePidFile(path, pid, port) {
|
|
124
|
+
const data = {
|
|
125
|
+
pid,
|
|
126
|
+
port,
|
|
127
|
+
startedAt: new Date().toISOString(),
|
|
128
|
+
};
|
|
129
|
+
await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Read and parse the pidfile at `path`. Returns `null` when the file
|
|
133
|
+
* is absent or not valid JSON, so assertion sites can use a plain
|
|
134
|
+
* null-check instead of a try/catch.
|
|
135
|
+
*/
|
|
136
|
+
export async function readPidFile(path) {
|
|
137
|
+
try {
|
|
138
|
+
const { readFile } = await import("node:fs/promises");
|
|
139
|
+
const raw = await readFile(path, "utf8");
|
|
140
|
+
return JSON.parse(raw);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Return `true` when the file at `path` exists on disk right now.
|
|
148
|
+
*/
|
|
149
|
+
export async function fileExists(path) {
|
|
150
|
+
try {
|
|
151
|
+
const { access } = await import("node:fs/promises");
|
|
152
|
+
await access(path);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=dashboard-test-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-test-helpers.js","sourceRoot":"","sources":["../../../src/tests/helpers/dashboard-test-helpers.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://code.claude.com/schemas/plugin.json",
|
|
3
3
|
"name": "claude-crap",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.8",
|
|
5
5
|
"description": "Deterministic Quality Assurance plugin for Claude Code. Wraps every Write / Edit / Bash tool call with a PreToolUse gatekeeper, a PostToolUse verifier, and a Stop quality gate backed by CRAP index, Technical Debt Ratio, tree-sitter AST metrics, and SARIF 2.1.0 reports. Forbids the agent from writing functional code before a test safety net exists.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alan Hernandez",
|
|
@@ -317,6 +317,95 @@
|
|
|
317
317
|
opacity: 0.7;
|
|
318
318
|
margin-left: 12px;
|
|
319
319
|
}
|
|
320
|
+
/* CC heat bar — ReportGenerator-style visual severity */
|
|
321
|
+
.heat-bar {
|
|
322
|
+
position: relative;
|
|
323
|
+
width: 140px;
|
|
324
|
+
height: 8px;
|
|
325
|
+
background: rgba(255, 255, 255, 0.06);
|
|
326
|
+
border-radius: 4px;
|
|
327
|
+
overflow: hidden;
|
|
328
|
+
}
|
|
329
|
+
.heat-bar-fill {
|
|
330
|
+
height: 100%;
|
|
331
|
+
border-radius: 4px;
|
|
332
|
+
transition: width 120ms ease-out;
|
|
333
|
+
}
|
|
334
|
+
.heat-bar-threshold {
|
|
335
|
+
position: absolute;
|
|
336
|
+
top: -2px;
|
|
337
|
+
bottom: -2px;
|
|
338
|
+
width: 1px;
|
|
339
|
+
background: rgba(255, 255, 255, 0.35);
|
|
340
|
+
}
|
|
341
|
+
/* CC chip — numeric value + % of threshold */
|
|
342
|
+
.cc-chip {
|
|
343
|
+
display: inline-flex;
|
|
344
|
+
align-items: baseline;
|
|
345
|
+
gap: 6px;
|
|
346
|
+
font-variant-numeric: tabular-nums;
|
|
347
|
+
}
|
|
348
|
+
.cc-chip .cc-value {
|
|
349
|
+
font-weight: 700;
|
|
350
|
+
font-size: 13px;
|
|
351
|
+
}
|
|
352
|
+
.cc-chip .cc-ratio {
|
|
353
|
+
font-size: 11px;
|
|
354
|
+
color: var(--muted);
|
|
355
|
+
}
|
|
356
|
+
/* Method name button — scrolls source view to fn start line */
|
|
357
|
+
.method-jump {
|
|
358
|
+
background: none;
|
|
359
|
+
border: none;
|
|
360
|
+
padding: 0;
|
|
361
|
+
font: inherit;
|
|
362
|
+
font-family: "SF Mono", "Fira Code", "Consolas", monospace;
|
|
363
|
+
font-size: 13px;
|
|
364
|
+
color: var(--accent);
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
}
|
|
367
|
+
.method-jump:hover { text-decoration: underline; }
|
|
368
|
+
/* "Open in editor" icon link */
|
|
369
|
+
.editor-link {
|
|
370
|
+
display: inline-flex;
|
|
371
|
+
align-items: center;
|
|
372
|
+
justify-content: center;
|
|
373
|
+
width: 24px;
|
|
374
|
+
height: 24px;
|
|
375
|
+
border-radius: 4px;
|
|
376
|
+
color: var(--muted);
|
|
377
|
+
text-decoration: none;
|
|
378
|
+
font-size: 13px;
|
|
379
|
+
transition: background 120ms, color 120ms;
|
|
380
|
+
}
|
|
381
|
+
.editor-link:hover {
|
|
382
|
+
background: rgba(62, 166, 255, 0.12);
|
|
383
|
+
color: var(--accent);
|
|
384
|
+
text-decoration: none;
|
|
385
|
+
}
|
|
386
|
+
/* "Show all / show fewer" toggle under the methods table */
|
|
387
|
+
.show-all-btn {
|
|
388
|
+
display: inline-block;
|
|
389
|
+
margin: 12px 0 0 0;
|
|
390
|
+
padding: 6px 12px;
|
|
391
|
+
background: transparent;
|
|
392
|
+
border: 1px solid var(--border);
|
|
393
|
+
border-radius: 6px;
|
|
394
|
+
color: var(--accent);
|
|
395
|
+
font-size: 12px;
|
|
396
|
+
cursor: pointer;
|
|
397
|
+
}
|
|
398
|
+
.show-all-btn:hover {
|
|
399
|
+
background: rgba(62, 166, 255, 0.08);
|
|
400
|
+
}
|
|
401
|
+
/* Line-flash animation when the user jumps to a source line */
|
|
402
|
+
.source-line.jump-target {
|
|
403
|
+
animation: jumpFlash 1.2s ease-out;
|
|
404
|
+
}
|
|
405
|
+
@keyframes jumpFlash {
|
|
406
|
+
0% { background: rgba(62, 166, 255, 0.35); }
|
|
407
|
+
100% { background: transparent; }
|
|
408
|
+
}
|
|
320
409
|
</style>
|
|
321
410
|
</head>
|
|
322
411
|
<body>
|
|
@@ -372,33 +461,74 @@
|
|
|
372
461
|
</div>
|
|
373
462
|
</div>
|
|
374
463
|
|
|
375
|
-
<!-- Methods table -->
|
|
376
|
-
<div v-if="fileDetail.functions.length" class="section-title">
|
|
464
|
+
<!-- Methods table — top-5 by CC with heat bar + jump + editor link -->
|
|
465
|
+
<div v-if="fileDetail.functions.length" class="section-title">
|
|
466
|
+
Methods
|
|
467
|
+
<span style="color: var(--muted); font-weight: 400; text-transform: none; letter-spacing: 0; margin-left: 8px;">
|
|
468
|
+
threshold CC {{ fileDetail.cyclomaticMax }}
|
|
469
|
+
</span>
|
|
470
|
+
</div>
|
|
377
471
|
<div v-if="fileDetail.functions.length" class="card">
|
|
378
472
|
<table>
|
|
379
473
|
<thead>
|
|
380
474
|
<tr>
|
|
381
475
|
<th>Method</th>
|
|
382
476
|
<th style="text-align: right">Line</th>
|
|
383
|
-
<th style="
|
|
477
|
+
<th style="width: 180px;">CC</th>
|
|
384
478
|
<th style="text-align: right">Lines</th>
|
|
385
479
|
<th>Status</th>
|
|
480
|
+
<th style="width: 32px;"></th>
|
|
386
481
|
</tr>
|
|
387
482
|
</thead>
|
|
388
483
|
<tbody>
|
|
389
|
-
<tr v-for="fn in
|
|
390
|
-
<td
|
|
484
|
+
<tr v-for="fn in visibleFunctions" :key="fn.startLine">
|
|
485
|
+
<td>
|
|
486
|
+
<button
|
|
487
|
+
class="method-jump"
|
|
488
|
+
@click="jumpToLine(fn.startLine)"
|
|
489
|
+
:title="'Jump to line ' + fn.startLine"
|
|
490
|
+
>{{ fn.name }}</button>
|
|
491
|
+
</td>
|
|
391
492
|
<td style="text-align: right; font-variant-numeric: tabular-nums;">{{ fn.startLine }}</td>
|
|
392
|
-
<td
|
|
493
|
+
<td>
|
|
494
|
+
<div class="cc-chip">
|
|
495
|
+
<div class="heat-bar" :title="ccTooltip(fn)">
|
|
496
|
+
<div
|
|
497
|
+
class="heat-bar-fill"
|
|
498
|
+
:style="heatBarStyle(fn)"
|
|
499
|
+
></div>
|
|
500
|
+
<div
|
|
501
|
+
class="heat-bar-threshold"
|
|
502
|
+
:style="{ left: thresholdMarker() + '%' }"
|
|
503
|
+
></div>
|
|
504
|
+
</div>
|
|
505
|
+
<span class="cc-value" :style="{ color: ccColor(fn) }">{{ fn.cyclomaticComplexity }}</span>
|
|
506
|
+
<span class="cc-ratio">{{ ccRatio(fn) }}%</span>
|
|
507
|
+
</div>
|
|
508
|
+
</td>
|
|
393
509
|
<td style="text-align: right; font-variant-numeric: tabular-nums;">{{ fn.lineCount }}</td>
|
|
394
510
|
<td>
|
|
395
511
|
<span v-if="fn.cyclomaticComplexity >= fileDetail.cyclomaticMax * 2" class="pill pill-error">error</span>
|
|
396
512
|
<span v-else-if="fn.cyclomaticComplexity > fileDetail.cyclomaticMax" class="pill pill-warning">warning</span>
|
|
397
513
|
<span v-else class="pill pill-note">ok</span>
|
|
398
514
|
</td>
|
|
515
|
+
<td>
|
|
516
|
+
<a
|
|
517
|
+
class="editor-link"
|
|
518
|
+
:href="editorLink(fn)"
|
|
519
|
+
:title="'Open ' + fileDetail.filePath + ':' + fn.startLine + ' in VS Code'"
|
|
520
|
+
>↗</a>
|
|
521
|
+
</td>
|
|
399
522
|
</tr>
|
|
400
523
|
</tbody>
|
|
401
524
|
</table>
|
|
525
|
+
<button
|
|
526
|
+
v-if="fileDetail.functions.length > topMethodsLimit"
|
|
527
|
+
class="show-all-btn"
|
|
528
|
+
@click="toggleShowAllMethods()"
|
|
529
|
+
>
|
|
530
|
+
{{ showAllMethods ? 'Show top ' + topMethodsLimit : 'Show all ' + fileDetail.functions.length + ' methods' }}
|
|
531
|
+
</button>
|
|
402
532
|
</div>
|
|
403
533
|
|
|
404
534
|
<!-- Findings table -->
|
|
@@ -622,11 +752,87 @@
|
|
|
622
752
|
});
|
|
623
753
|
|
|
624
754
|
// ── File detail computed ──
|
|
755
|
+
const topMethodsLimit = 5;
|
|
756
|
+
const showAllMethods = ref(false);
|
|
757
|
+
|
|
625
758
|
const sortedFunctions = computed(() => {
|
|
626
759
|
if (!fileDetail.value) return [];
|
|
627
760
|
return [...fileDetail.value.functions].sort((a, b) => b.cyclomaticComplexity - a.cyclomaticComplexity);
|
|
628
761
|
});
|
|
629
762
|
|
|
763
|
+
const visibleFunctions = computed(() => {
|
|
764
|
+
if (showAllMethods.value) return sortedFunctions.value;
|
|
765
|
+
return sortedFunctions.value.slice(0, topMethodsLimit);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
function toggleShowAllMethods() {
|
|
769
|
+
showAllMethods.value = !showAllMethods.value;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ── CC heat-bar helpers ──
|
|
773
|
+
// Fill width is clamped at 3× threshold so a CC of 80 with
|
|
774
|
+
// threshold 15 still produces a visually meaningful bar rather
|
|
775
|
+
// than overflowing. The threshold marker sits at the "1.0×"
|
|
776
|
+
// position (i.e. threshold/3threshold = 33%).
|
|
777
|
+
function ccRatio(fn) {
|
|
778
|
+
const max = fileDetail.value?.cyclomaticMax || 1;
|
|
779
|
+
return Math.round((fn.cyclomaticComplexity / max) * 100);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function heatBarStyle(fn) {
|
|
783
|
+
const max = fileDetail.value?.cyclomaticMax || 1;
|
|
784
|
+
const cap = max * 3;
|
|
785
|
+
const pct = Math.min(100, (fn.cyclomaticComplexity / cap) * 100);
|
|
786
|
+
return { width: pct + "%", background: ccColor(fn) };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function thresholdMarker() {
|
|
790
|
+
// threshold sits at 1/3 of the bar (since we cap at 3× threshold)
|
|
791
|
+
return 33.33;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function ccColor(fn) {
|
|
795
|
+
const max = fileDetail.value?.cyclomaticMax || 1;
|
|
796
|
+
const r = fn.cyclomaticComplexity / max;
|
|
797
|
+
if (r >= 2) return "var(--rating-E)"; // red — error (≥ 2×)
|
|
798
|
+
if (r > 1) return "var(--rating-C)"; // yellow — warning
|
|
799
|
+
if (r > 0.66) return "var(--rating-B)"; // yellow-green — near threshold
|
|
800
|
+
return "var(--rating-A)"; // green — healthy
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function ccTooltip(fn) {
|
|
804
|
+
const max = fileDetail.value?.cyclomaticMax || 1;
|
|
805
|
+
return (
|
|
806
|
+
"CC " + fn.cyclomaticComplexity +
|
|
807
|
+
" / threshold " + max +
|
|
808
|
+
" (" + ccRatio(fn) + "% of threshold)"
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ── Editor deep-link + jump-to-line ──
|
|
813
|
+
// vscode:// handler accepts absolute paths. The `absolutePath`
|
|
814
|
+
// field is resolved server-side through the workspace-traversal
|
|
815
|
+
// guard, so this is safe to paste into an href.
|
|
816
|
+
function editorLink(fn) {
|
|
817
|
+
const abs = fileDetail.value?.absolutePath;
|
|
818
|
+
if (!abs) return "#";
|
|
819
|
+
return "vscode://file" + abs + ":" + fn.startLine + ":1";
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function jumpToLine(lineNum) {
|
|
823
|
+
// Source lines are keyed by 0-based index, so line N lives at
|
|
824
|
+
// child index N-1 of `.source-view`.
|
|
825
|
+
const view = document.querySelector(".source-view");
|
|
826
|
+
if (!view) return;
|
|
827
|
+
const row = view.children[lineNum - 1];
|
|
828
|
+
if (!row) return;
|
|
829
|
+
row.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
830
|
+
row.classList.remove("jump-target");
|
|
831
|
+
// force reflow so the animation restarts on repeat clicks
|
|
832
|
+
void row.offsetWidth;
|
|
833
|
+
row.classList.add("jump-target");
|
|
834
|
+
}
|
|
835
|
+
|
|
630
836
|
const sortedFindings = computed(() => {
|
|
631
837
|
if (!fileDetail.value) return [];
|
|
632
838
|
return [...fileDetail.value.findings].sort((a, b) => a.startLine - b.startLine);
|
|
@@ -777,7 +983,10 @@
|
|
|
777
983
|
currentView, selectedFile, fileDetail,
|
|
778
984
|
score, complexity, loading, error,
|
|
779
985
|
toolEntries, fileEntries, formatTimestamp,
|
|
780
|
-
sortedFunctions, sortedFindings,
|
|
986
|
+
sortedFunctions, sortedFindings, visibleFunctions,
|
|
987
|
+
showAllMethods, toggleShowAllMethods, topMethodsLimit,
|
|
988
|
+
ccRatio, ccColor, ccTooltip, heatBarStyle, thresholdMarker,
|
|
989
|
+
editorLink, jumpToLine,
|
|
781
990
|
navigateToFile, goBack,
|
|
782
991
|
lineFindings, lineClass, gutterClass, lineFnLabel,
|
|
783
992
|
};
|
|
@@ -7458,6 +7458,23 @@ var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
7458
7458
|
// CI artefact staging, Maven
|
|
7459
7459
|
"publish",
|
|
7460
7460
|
// `dotnet publish` output
|
|
7461
|
+
// Test coverage report bundles (generated HTML/JS from coverage tools;
|
|
7462
|
+
// walking them floods the complexity scanner with synthetic minified
|
|
7463
|
+
// functions like `coverage-report/main.js::gG` at CC 80+).
|
|
7464
|
+
"coverage-report",
|
|
7465
|
+
// ReportGenerator default (.NET)
|
|
7466
|
+
"CoverageReport",
|
|
7467
|
+
// ReportGenerator PascalCase variant
|
|
7468
|
+
"coveragereport",
|
|
7469
|
+
// ReportGenerator lowercase fallback
|
|
7470
|
+
"TestResults",
|
|
7471
|
+
// `dotnet test` default output
|
|
7472
|
+
"cobertura",
|
|
7473
|
+
// Cobertura XML reporter
|
|
7474
|
+
"lcov-report",
|
|
7475
|
+
// Istanbul HTML reporter
|
|
7476
|
+
"htmlcov",
|
|
7477
|
+
// coverage.py HTML output
|
|
7461
7478
|
// Desktop / mobile packaging outputs
|
|
7462
7479
|
"dist-electron",
|
|
7463
7480
|
// Electron-builder
|
|
@@ -7835,6 +7852,7 @@ async function buildFileDetail(input) {
|
|
|
7835
7852
|
) / 100 : 0;
|
|
7836
7853
|
return {
|
|
7837
7854
|
filePath: relativePath,
|
|
7855
|
+
absolutePath,
|
|
7838
7856
|
language,
|
|
7839
7857
|
physicalLoc,
|
|
7840
7858
|
logicalLoc,
|
|
@@ -7857,6 +7875,20 @@ async function buildFileDetail(input) {
|
|
|
7857
7875
|
// src/dashboard/server.ts
|
|
7858
7876
|
async function startDashboard(options) {
|
|
7859
7877
|
const { config, sarifStore, workspaceStatsProvider, logger: logger2 } = options;
|
|
7878
|
+
const pidFilePath = resolvePidFilePath(config);
|
|
7879
|
+
const adoption = await tryAdoptExisting(pidFilePath, config.dashboardPort, logger2);
|
|
7880
|
+
if (adoption) {
|
|
7881
|
+
logger2.info(
|
|
7882
|
+
{ url: adoption.url, ownerPid: adoption.pid, port: config.dashboardPort },
|
|
7883
|
+
"adopted existing claude-crap dashboard"
|
|
7884
|
+
);
|
|
7885
|
+
return {
|
|
7886
|
+
url: adoption.url,
|
|
7887
|
+
adopted: true,
|
|
7888
|
+
async close() {
|
|
7889
|
+
}
|
|
7890
|
+
};
|
|
7891
|
+
}
|
|
7860
7892
|
const publicRoot = await resolvePublicRoot(logger2);
|
|
7861
7893
|
const fastify = Fastify({
|
|
7862
7894
|
logger: false,
|
|
@@ -7909,14 +7941,35 @@ async function startDashboard(options) {
|
|
|
7909
7941
|
fastify.get("/", async (_request, reply) => {
|
|
7910
7942
|
return reply.sendFile("index.html");
|
|
7911
7943
|
});
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7944
|
+
try {
|
|
7945
|
+
await fastify.listen({ port: config.dashboardPort, host: "127.0.0.1" });
|
|
7946
|
+
} catch (err) {
|
|
7947
|
+
const code = err.code;
|
|
7948
|
+
if (code === "EADDRINUSE") {
|
|
7949
|
+
await fastify.close().catch(() => {
|
|
7950
|
+
});
|
|
7951
|
+
const raceAdoption = await tryAdoptExisting(pidFilePath, config.dashboardPort, logger2);
|
|
7952
|
+
if (raceAdoption) {
|
|
7953
|
+
logger2.info(
|
|
7954
|
+
{ url: raceAdoption.url, ownerPid: raceAdoption.pid, port: config.dashboardPort },
|
|
7955
|
+
"dashboard bind lost race, adopted concurrent owner"
|
|
7956
|
+
);
|
|
7957
|
+
return {
|
|
7958
|
+
url: raceAdoption.url,
|
|
7959
|
+
adopted: true,
|
|
7960
|
+
async close() {
|
|
7961
|
+
}
|
|
7962
|
+
};
|
|
7963
|
+
}
|
|
7964
|
+
}
|
|
7965
|
+
throw err;
|
|
7966
|
+
}
|
|
7915
7967
|
const url = `http://127.0.0.1:${config.dashboardPort}`;
|
|
7916
7968
|
logger2.info({ url, publicRoot }, "claude-crap dashboard listening");
|
|
7917
7969
|
writePidFile(pidFilePath, config.dashboardPort);
|
|
7918
7970
|
return {
|
|
7919
7971
|
url,
|
|
7972
|
+
adopted: false,
|
|
7920
7973
|
async close() {
|
|
7921
7974
|
removePidFile(pidFilePath);
|
|
7922
7975
|
await fastify.close();
|
|
@@ -7986,29 +8039,40 @@ function isPidAlive(pid) {
|
|
|
7986
8039
|
return false;
|
|
7987
8040
|
}
|
|
7988
8041
|
}
|
|
7989
|
-
async function
|
|
7990
|
-
if (!existsSync(pidFilePath)) return;
|
|
8042
|
+
async function tryAdoptExisting(pidFilePath, port, logger2) {
|
|
8043
|
+
if (!existsSync(pidFilePath)) return null;
|
|
7991
8044
|
let stale;
|
|
7992
8045
|
try {
|
|
7993
8046
|
stale = JSON.parse(readFileSync(pidFilePath, "utf8"));
|
|
7994
8047
|
} catch {
|
|
8048
|
+
logger2.info({ pidFilePath }, "corrupt dashboard pidfile, removing");
|
|
7995
8049
|
removePidFile(pidFilePath);
|
|
7996
|
-
return;
|
|
8050
|
+
return null;
|
|
7997
8051
|
}
|
|
7998
8052
|
if (!isPidAlive(stale.pid)) {
|
|
7999
|
-
logger2.info({ stalePid: stale.pid }, "stale dashboard
|
|
8053
|
+
logger2.info({ stalePid: stale.pid }, "stale dashboard pidfile (process dead), removing");
|
|
8000
8054
|
removePidFile(pidFilePath);
|
|
8001
|
-
return;
|
|
8055
|
+
return null;
|
|
8002
8056
|
}
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8057
|
+
if (stale.port !== port) {
|
|
8058
|
+
logger2.info(
|
|
8059
|
+
{ stalePort: stale.port, wantedPort: port },
|
|
8060
|
+
"dashboard pidfile points at different port, ignoring"
|
|
8061
|
+
);
|
|
8062
|
+
removePidFile(pidFilePath);
|
|
8063
|
+
return null;
|
|
8064
|
+
}
|
|
8065
|
+
const healthy = await probeDashboardHealth(port);
|
|
8066
|
+
if (healthy) {
|
|
8067
|
+
return { url: `http://127.0.0.1:${port}`, pid: stale.pid };
|
|
8068
|
+
}
|
|
8069
|
+
logger2.warn(
|
|
8070
|
+
{ stalePid: stale.pid, port },
|
|
8071
|
+
"dashboard pidfile owner is unresponsive, terminating"
|
|
8006
8072
|
);
|
|
8007
8073
|
try {
|
|
8008
8074
|
process.kill(stale.pid, "SIGTERM");
|
|
8009
8075
|
} catch {
|
|
8010
|
-
removePidFile(pidFilePath);
|
|
8011
|
-
return;
|
|
8012
8076
|
}
|
|
8013
8077
|
for (let i = 0; i < 30; i++) {
|
|
8014
8078
|
if (!isPidAlive(stale.pid)) break;
|
|
@@ -8023,6 +8087,17 @@ async function killStaleDashboard(pidFilePath, port, logger2) {
|
|
|
8023
8087
|
}
|
|
8024
8088
|
removePidFile(pidFilePath);
|
|
8025
8089
|
await new Promise((r) => setTimeout(r, 300));
|
|
8090
|
+
return null;
|
|
8091
|
+
}
|
|
8092
|
+
async function probeDashboardHealth(port) {
|
|
8093
|
+
try {
|
|
8094
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
8095
|
+
signal: AbortSignal.timeout(500)
|
|
8096
|
+
});
|
|
8097
|
+
return res.ok;
|
|
8098
|
+
} catch {
|
|
8099
|
+
return false;
|
|
8100
|
+
}
|
|
8026
8101
|
}
|
|
8027
8102
|
async function buildComplexityReport(config, engine, logger2, exclude) {
|
|
8028
8103
|
const threshold = config.cyclomaticMax;
|