claude-crap 0.4.0 → 0.4.2
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/README.md +18 -15
- package/dist/crap-config.d.ts +3 -1
- package/dist/crap-config.d.ts.map +1 -1
- package/dist/crap-config.js +4 -2
- package/dist/crap-config.js.map +1 -1
- package/dist/index.js +164 -334
- package/dist/index.js.map +1 -1
- package/dist/shared/exclusions.d.ts.map +1 -1
- package/dist/shared/exclusions.js +8 -0
- package/dist/shared/exclusions.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bundle/launcher.mjs +26 -1
- package/plugin/bundle/mcp-server.mjs +194 -362
- package/plugin/bundle/mcp-server.mjs.map +2 -2
- package/plugin/hooks/lib/crap-config.mjs +1 -1
- package/plugin/hooks/lib/quality-gate.mjs +16 -13
- package/plugin/launcher.mjs +26 -1
- package/plugin/package-lock.json +2 -2
- package/plugin/package.json +1 -1
- package/scripts/bundle-plugin.mjs +12 -0
- package/src/crap-config.ts +4 -2
- package/src/index.ts +184 -400
- package/src/shared/exclusions.ts +9 -0
- package/src/tests/crap-config.test.ts +4 -4
- package/src/tests/exclusions.test.ts +1 -1
- package/src/tests/stop-quality-gate-strictness.test.ts +3 -4
package/README.md
CHANGED
|
@@ -62,15 +62,16 @@ automatically — no further setup required.
|
|
|
62
62
|
|
|
63
63
|
## Configuration
|
|
64
64
|
|
|
65
|
-
> **Default: `
|
|
66
|
-
>
|
|
65
|
+
> **Default: `warn`.** No config file needed. The Stop gate
|
|
66
|
+
> shows all violations but lets tasks close. Teams that want
|
|
67
|
+
> hard enforcement can set `"strict"` in `.claude-crap.json`.
|
|
67
68
|
|
|
68
69
|
The `strictness` value controls how the Stop gate reacts to failures:
|
|
69
70
|
|
|
70
71
|
| Mode | Stop exit | Effect |
|
|
71
72
|
| :--------- | :-------: | :------------------------------------------------------------- |
|
|
72
|
-
| `strict` | `2` | Task cannot close until rules pass.
|
|
73
|
-
| `warn` | `0` | Full verdict visible to agent, but task closes.
|
|
73
|
+
| `strict` | `2` | Task cannot close until rules pass. |
|
|
74
|
+
| `warn` | `0` | Full verdict visible to agent, but task closes. **Default.** |
|
|
74
75
|
| `advisory` | `0` | Single-line nudge only. |
|
|
75
76
|
|
|
76
77
|
Override per workspace:
|
|
@@ -78,13 +79,13 @@ Override per workspace:
|
|
|
78
79
|
```jsonc
|
|
79
80
|
// .claude-crap.json — commit to git for team-wide policy
|
|
80
81
|
{
|
|
81
|
-
"strictness": "
|
|
82
|
+
"strictness": "strict"
|
|
82
83
|
}
|
|
83
84
|
```
|
|
84
85
|
|
|
85
|
-
Or per session: `CLAUDE_CRAP_STRICTNESS=
|
|
86
|
+
Or per session: `CLAUDE_CRAP_STRICTNESS=strict claude`
|
|
86
87
|
|
|
87
|
-
**Precedence:** env var > `.claude-crap.json` > hardcoded `
|
|
88
|
+
**Precedence:** env var > `.claude-crap.json` > hardcoded `warn`.
|
|
88
89
|
|
|
89
90
|
See [docs/quality-gate.md](./docs/quality-gate.md) for the full
|
|
90
91
|
CRAP formula, TDR formula, letter ratings, and adoption strategy.
|
|
@@ -183,11 +184,9 @@ probes npm workspaces and common directories (`apps/`, `packages/`,
|
|
|
183
184
|
```
|
|
184
185
|
Session start
|
|
185
186
|
→ discover project map
|
|
186
|
-
→ detect
|
|
187
|
-
→
|
|
188
|
-
→ run
|
|
189
|
-
→ run dart analyze from apps/mobile/
|
|
190
|
-
→ run dotnet format from apps/api/
|
|
187
|
+
→ detect sub-projects by type (TypeScript, Dart, C#, Python, etc.)
|
|
188
|
+
→ install missing scanners (ESLint auto-installed via npm)
|
|
189
|
+
→ run each scanner from its project directory
|
|
191
190
|
→ aggregate all findings into one SARIF store
|
|
192
191
|
→ score_project ready with real data
|
|
193
192
|
```
|
|
@@ -197,10 +196,13 @@ exposed via the `list_projects` MCP tool. Use `score_project` with
|
|
|
197
196
|
the optional `scope` parameter to score a single sub-project:
|
|
198
197
|
|
|
199
198
|
```ts
|
|
200
|
-
// Score only
|
|
201
|
-
score_project({ format: "both", scope: "
|
|
199
|
+
// Score only one sub-project
|
|
200
|
+
score_project({ format: "both", scope: "frontend" })
|
|
202
201
|
```
|
|
203
202
|
|
|
203
|
+
See [docs/supported-languages.md](./docs/supported-languages.md) for
|
|
204
|
+
detailed per-language setup and behavior.
|
|
205
|
+
|
|
204
206
|
**File exclusions** are centralized and cover all major frameworks
|
|
205
207
|
out of the box: `dist/`, `build/`, `bundle/`, `vendor/`,
|
|
206
208
|
`.next`, `.nuxt`, `.astro`, `.svelte-kit`, `.dart_tool`,
|
|
@@ -220,6 +222,7 @@ added via `.claude-crap.json`:
|
|
|
220
222
|
|
|
221
223
|
| Section | Link |
|
|
222
224
|
| :------ | :--- |
|
|
225
|
+
| Supported languages & scanners | [docs/supported-languages.md](./docs/supported-languages.md) |
|
|
223
226
|
| Architecture & boot sequence | [docs/architecture-overview.md](./docs/architecture-overview.md) |
|
|
224
227
|
| Quality gate math (CRAP, TDR, ratings) | [docs/quality-gate.md](./docs/quality-gate.md) |
|
|
225
228
|
| Project score aggregation | [docs/scoring.md](./docs/scoring.md) |
|
|
@@ -237,7 +240,7 @@ added via `.claude-crap.json`:
|
|
|
237
240
|
|
|
238
241
|
```bash
|
|
239
242
|
npm install # postinstall builds dist/ automatically
|
|
240
|
-
npm test #
|
|
243
|
+
npm test # 339 tests across 89 suites
|
|
241
244
|
npm run build:fast # esbuild dev build (10-20x faster than tsc)
|
|
242
245
|
npm run doctor # full diagnostic
|
|
243
246
|
```
|
package/dist/crap-config.d.ts
CHANGED
|
@@ -47,7 +47,9 @@ export type Strictness = (typeof STRICTNESS_VALUES)[number];
|
|
|
47
47
|
/**
|
|
48
48
|
* Hardcoded default used when neither the environment variable nor
|
|
49
49
|
* `.claude-crap.json` provides a value. Chosen as `"strict"` so the
|
|
50
|
-
* plugin
|
|
50
|
+
* plugin adopts gradually on existing codebases without blocking.
|
|
51
|
+
* Teams that want hard enforcement can set `"strict"` in
|
|
52
|
+
* `.claude-crap.json` or via `CLAUDE_CRAP_STRICTNESS=strict`.
|
|
51
53
|
*/
|
|
52
54
|
export declare const DEFAULT_STRICTNESS: Strictness;
|
|
53
55
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crap-config.d.ts","sourceRoot":"","sources":["../src/crap-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,yCAA0C,CAAC;AAEzE;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D
|
|
1
|
+
{"version":3,"file":"crap-config.d.ts","sourceRoot":"","sources":["../src/crap-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,yCAA0C,CAAC;AAEzE;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,UAAmB,CAAC;AAErD;;;;;GAKG;AACH,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;IACtD,sFAAsF;IACtF,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC,2FAA2F;IAC3F,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC7C;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,UAAU,CAwBzE"}
|
package/dist/crap-config.js
CHANGED
|
@@ -43,9 +43,11 @@ export const STRICTNESS_VALUES = ["strict", "warn", "advisory"];
|
|
|
43
43
|
/**
|
|
44
44
|
* Hardcoded default used when neither the environment variable nor
|
|
45
45
|
* `.claude-crap.json` provides a value. Chosen as `"strict"` so the
|
|
46
|
-
* plugin
|
|
46
|
+
* plugin adopts gradually on existing codebases without blocking.
|
|
47
|
+
* Teams that want hard enforcement can set `"strict"` in
|
|
48
|
+
* `.claude-crap.json` or via `CLAUDE_CRAP_STRICTNESS=strict`.
|
|
47
49
|
*/
|
|
48
|
-
export const DEFAULT_STRICTNESS = "
|
|
50
|
+
export const DEFAULT_STRICTNESS = "warn";
|
|
49
51
|
/**
|
|
50
52
|
* Thrown by {@link loadCrapConfig} when the configuration is
|
|
51
53
|
* rejected. Callers in the hook layer fall back to the default on a
|
package/dist/crap-config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crap-config.js","sourceRoot":"","sources":["../src/crap-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAU,CAAC;AASzE
|
|
1
|
+
{"version":3,"file":"crap-config.js","sourceRoot":"","sources":["../src/crap-config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -240,347 +240,177 @@ async function main() {
|
|
|
240
240
|
logger.info({ tool: name }, "Tool call received");
|
|
241
241
|
return handleToolCall(name, args);
|
|
242
242
|
});
|
|
243
|
+
function handleComputeCrap(args) {
|
|
244
|
+
const typed = args;
|
|
245
|
+
const result = computeCrap({ cyclomaticComplexity: typed.cyclomaticComplexity, coveragePercent: typed.coveragePercent }, config.crapThreshold);
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text", text: JSON.stringify({ tool: "compute_crap", function: typed.functionName, file: typed.filePath, ...result }, null, 2) }],
|
|
248
|
+
isError: result.exceedsThreshold,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function handleComputeTdr(args) {
|
|
252
|
+
const typed = args;
|
|
253
|
+
const result = computeTdr({
|
|
254
|
+
remediationMinutes: typed.remediationMinutes,
|
|
255
|
+
totalLinesOfCode: typed.totalLinesOfCode,
|
|
256
|
+
minutesPerLoc: config.minutesPerLoc,
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: "text", text: JSON.stringify({ tool: "compute_tdr", scope: typed.scope, ...result }, null, 2) }],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async function handleAnalyzeFileAst(args) {
|
|
263
|
+
const typed = args;
|
|
264
|
+
const absolutePath = resolveWithinWorkspace(config.pluginRoot, typed.filePath);
|
|
265
|
+
try {
|
|
266
|
+
const metrics = await astEngine.analyzeFile({ filePath: absolutePath, language: typed.language });
|
|
267
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "analyze_file_ast", ...metrics }, null, 2) }] };
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
logger.error({ err, filePath: absolutePath, language: typed.language }, "analyze_file_ast failed");
|
|
271
|
+
return {
|
|
272
|
+
content: [{ type: "text", text: JSON.stringify({ tool: "analyze_file_ast", status: "error", message: err.message, filePath: typed.filePath, language: typed.language }, null, 2) }],
|
|
273
|
+
isError: true,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
243
277
|
/** Dispatch a tool call to the correct handler. */
|
|
244
278
|
async function handleToolCall(name, args) {
|
|
245
279
|
switch (name) {
|
|
246
|
-
case "compute_crap":
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
case "
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
totalLinesOfCode: typed.totalLinesOfCode,
|
|
267
|
-
minutesPerLoc: config.minutesPerLoc,
|
|
268
|
-
});
|
|
269
|
-
return {
|
|
270
|
-
content: [
|
|
271
|
-
{
|
|
272
|
-
type: "text",
|
|
273
|
-
text: JSON.stringify({ tool: "compute_tdr", scope: typed.scope, ...result }, null, 2),
|
|
274
|
-
},
|
|
275
|
-
],
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
case "analyze_file_ast": {
|
|
279
|
-
const typed = args;
|
|
280
|
-
const absolutePath = resolveWithinWorkspace(config.pluginRoot, typed.filePath);
|
|
281
|
-
try {
|
|
282
|
-
const metrics = await astEngine.analyzeFile({
|
|
283
|
-
filePath: absolutePath,
|
|
284
|
-
language: typed.language,
|
|
285
|
-
});
|
|
286
|
-
return {
|
|
287
|
-
content: [
|
|
288
|
-
{
|
|
289
|
-
type: "text",
|
|
290
|
-
text: JSON.stringify({ tool: "analyze_file_ast", ...metrics }, null, 2),
|
|
291
|
-
},
|
|
292
|
-
],
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
catch (err) {
|
|
296
|
-
logger.error({ err, filePath: absolutePath, language: typed.language }, "analyze_file_ast failed");
|
|
297
|
-
return {
|
|
298
|
-
content: [
|
|
299
|
-
{
|
|
300
|
-
type: "text",
|
|
301
|
-
text: JSON.stringify({
|
|
302
|
-
tool: "analyze_file_ast",
|
|
303
|
-
status: "error",
|
|
304
|
-
message: err.message,
|
|
305
|
-
filePath: typed.filePath,
|
|
306
|
-
language: typed.language,
|
|
307
|
-
}, null, 2),
|
|
308
|
-
},
|
|
309
|
-
],
|
|
310
|
-
isError: true,
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
case "score_project": {
|
|
315
|
-
const typed = (args ?? {});
|
|
316
|
-
const format = typed.format ?? "both";
|
|
317
|
-
// Resolve scope to a workspace subdirectory
|
|
318
|
-
let scoreRoot = config.pluginRoot;
|
|
319
|
-
if (typed.scope && projectMap) {
|
|
320
|
-
const project = projectMap.projects.find((p) => p.name === typed.scope);
|
|
321
|
-
if (project) {
|
|
322
|
-
const { join } = await import("node:path");
|
|
323
|
-
scoreRoot = join(config.pluginRoot, project.path);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
try {
|
|
327
|
-
const workspace = await estimateWorkspaceLoc(scoreRoot, { exclude: userExclusions });
|
|
328
|
-
const score = computeProjectScore({
|
|
329
|
-
workspaceRoot: scoreRoot,
|
|
330
|
-
minutesPerLoc: config.minutesPerLoc,
|
|
331
|
-
tdrMaxRating: config.tdrMaxRating,
|
|
332
|
-
workspace: { physicalLoc: workspace.physicalLoc, fileCount: workspace.fileCount },
|
|
333
|
-
sarifStore,
|
|
334
|
-
dashboardUrl: dashboard?.url ?? null,
|
|
335
|
-
sarifReportPath: sarifStore.consolidatedReportPath,
|
|
336
|
-
});
|
|
337
|
-
const blocks = [];
|
|
338
|
-
if (format === "markdown" || format === "both") {
|
|
339
|
-
blocks.push({ type: "text", text: renderProjectScoreMarkdown(score) });
|
|
340
|
-
}
|
|
341
|
-
if (format === "json" || format === "both") {
|
|
342
|
-
blocks.push({ type: "text", text: JSON.stringify(score, null, 2) });
|
|
343
|
-
}
|
|
344
|
-
// Respect the workspace strictness setting: only `strict`
|
|
345
|
-
// mode should flag a failing project as an MCP tool error
|
|
346
|
-
// and push the agent toward remediation. In `warn` and
|
|
347
|
-
// `advisory` modes the Stop hook lets the task close, so
|
|
348
|
-
// `score_project` must stay consistent and return the
|
|
349
|
-
// score as plain content.
|
|
350
|
-
const strictness = safeLoadStrictness(config.pluginRoot, logger);
|
|
351
|
-
const shouldFlagError = strictness === "strict" && !score.overall.passes;
|
|
352
|
-
return {
|
|
353
|
-
content: blocks,
|
|
354
|
-
isError: shouldFlagError,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
catch (err) {
|
|
358
|
-
logger.error({ err }, "score_project failed");
|
|
359
|
-
return {
|
|
360
|
-
content: [
|
|
361
|
-
{
|
|
362
|
-
type: "text",
|
|
363
|
-
text: JSON.stringify({ tool: "score_project", status: "error", message: err.message }, null, 2),
|
|
364
|
-
},
|
|
365
|
-
],
|
|
366
|
-
isError: true,
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
case "require_test_harness": {
|
|
371
|
-
const typed = args;
|
|
372
|
-
const absolutePath = resolveWithinWorkspace(config.pluginRoot, typed.filePath);
|
|
373
|
-
try {
|
|
374
|
-
const resolution = await findTestFile(config.pluginRoot, absolutePath);
|
|
375
|
-
const hasTest = resolution.testFile !== null;
|
|
376
|
-
return {
|
|
377
|
-
content: [
|
|
378
|
-
{
|
|
379
|
-
type: "text",
|
|
380
|
-
text: JSON.stringify({
|
|
381
|
-
tool: "require_test_harness",
|
|
382
|
-
filePath: typed.filePath,
|
|
383
|
-
hasTest,
|
|
384
|
-
isTestFile: resolution.isTestFile,
|
|
385
|
-
testFile: resolution.testFile,
|
|
386
|
-
candidates: resolution.candidates,
|
|
387
|
-
...(hasTest
|
|
388
|
-
? {}
|
|
389
|
-
: {
|
|
390
|
-
corrective: "No test file found. Per the CLAUDE.md Golden Rule, create a characterization " +
|
|
391
|
-
"test at one of the candidate paths before writing any functional code for this file.",
|
|
392
|
-
}),
|
|
393
|
-
}, null, 2),
|
|
394
|
-
},
|
|
395
|
-
],
|
|
396
|
-
// The Golden Rule says "no code without a test", so the absence
|
|
397
|
-
// of a test is a blocking condition. Surface it as an error.
|
|
398
|
-
isError: !hasTest,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
catch (err) {
|
|
402
|
-
logger.error({ err, filePath: absolutePath }, "require_test_harness failed");
|
|
403
|
-
return {
|
|
404
|
-
content: [
|
|
405
|
-
{
|
|
406
|
-
type: "text",
|
|
407
|
-
text: JSON.stringify({
|
|
408
|
-
tool: "require_test_harness",
|
|
409
|
-
status: "error",
|
|
410
|
-
message: err.message,
|
|
411
|
-
filePath: typed.filePath,
|
|
412
|
-
}, null, 2),
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
isError: true,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
case "ingest_scanner_output": {
|
|
420
|
-
const typed = args;
|
|
421
|
-
try {
|
|
422
|
-
const adapted = adaptScannerOutput(typed.scanner, typed.rawOutput);
|
|
423
|
-
// F-A05-01: validate the adapter's output against the same
|
|
424
|
-
// schema used by `ingest_sarif`. Adapters are internal and
|
|
425
|
-
// should already emit conformant documents, but this catches
|
|
426
|
-
// regressions before they reach the store or the dashboard.
|
|
427
|
-
validateSarifDocument(adapted.document);
|
|
428
|
-
const stats = sarifStore.ingestRun(adapted.document, adapted.sourceTool);
|
|
429
|
-
await sarifStore.persist();
|
|
430
|
-
return {
|
|
431
|
-
content: [
|
|
432
|
-
{
|
|
433
|
-
type: "text",
|
|
434
|
-
text: JSON.stringify({
|
|
435
|
-
tool: "ingest_scanner_output",
|
|
436
|
-
status: "accepted",
|
|
437
|
-
scanner: typed.scanner,
|
|
438
|
-
findingsParsed: adapted.findingCount,
|
|
439
|
-
totalEffortMinutes: adapted.totalEffortMinutes,
|
|
440
|
-
accepted: stats.accepted,
|
|
441
|
-
duplicates: stats.duplicates,
|
|
442
|
-
total: stats.total,
|
|
443
|
-
storeSize: sarifStore.size(),
|
|
444
|
-
reportPath: sarifStore.consolidatedReportPath,
|
|
445
|
-
}, null, 2),
|
|
446
|
-
},
|
|
447
|
-
],
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
catch (err) {
|
|
451
|
-
logger.error({ err, scanner: typed.scanner }, "ingest_scanner_output failed");
|
|
452
|
-
return {
|
|
453
|
-
content: [
|
|
454
|
-
{
|
|
455
|
-
type: "text",
|
|
456
|
-
text: JSON.stringify({
|
|
457
|
-
tool: "ingest_scanner_output",
|
|
458
|
-
status: "error",
|
|
459
|
-
scanner: typed.scanner,
|
|
460
|
-
message: err.message,
|
|
461
|
-
}, null, 2),
|
|
462
|
-
},
|
|
463
|
-
],
|
|
464
|
-
isError: true,
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
case "ingest_sarif": {
|
|
469
|
-
const typed = args;
|
|
470
|
-
try {
|
|
471
|
-
// F-A05-01: validate the caller-supplied document against a
|
|
472
|
-
// minimal SARIF 2.1.0 schema BEFORE touching the store. The
|
|
473
|
-
// MCP SDK already validated the outer tool-call shape, but
|
|
474
|
-
// the inner `sarifDocument` is declared as `type: "object"`
|
|
475
|
-
// in tool-schemas.ts and would otherwise flow through
|
|
476
|
-
// un-checked.
|
|
477
|
-
validateSarifDocument(typed.sarifDocument);
|
|
478
|
-
const stats = sarifStore.ingestRun(typed.sarifDocument, typed.sourceTool);
|
|
479
|
-
await sarifStore.persist();
|
|
480
|
-
return {
|
|
481
|
-
content: [
|
|
482
|
-
{
|
|
483
|
-
type: "text",
|
|
484
|
-
text: JSON.stringify({
|
|
485
|
-
tool: "ingest_sarif",
|
|
486
|
-
status: "accepted",
|
|
487
|
-
sourceTool: typed.sourceTool,
|
|
488
|
-
accepted: stats.accepted,
|
|
489
|
-
duplicates: stats.duplicates,
|
|
490
|
-
total: stats.total,
|
|
491
|
-
storeSize: sarifStore.size(),
|
|
492
|
-
reportPath: sarifStore.consolidatedReportPath,
|
|
493
|
-
}, null, 2),
|
|
494
|
-
},
|
|
495
|
-
],
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
catch (err) {
|
|
499
|
-
logger.error({ err, sourceTool: typed.sourceTool }, "ingest_sarif failed");
|
|
500
|
-
return {
|
|
501
|
-
content: [
|
|
502
|
-
{
|
|
503
|
-
type: "text",
|
|
504
|
-
text: JSON.stringify({ tool: "ingest_sarif", status: "error", message: err.message }, null, 2),
|
|
505
|
-
},
|
|
506
|
-
],
|
|
507
|
-
isError: true,
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
case "bootstrap_scanner": {
|
|
512
|
-
logger.info({ tool: "bootstrap_scanner" }, "Tool call received");
|
|
513
|
-
try {
|
|
514
|
-
const result = await bootstrapScanner(config.pluginRoot, sarifStore, logger);
|
|
515
|
-
const markdown = renderBootstrapMarkdown(result);
|
|
516
|
-
return {
|
|
517
|
-
content: [
|
|
518
|
-
{ type: "text", text: markdown },
|
|
519
|
-
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
520
|
-
],
|
|
521
|
-
isError: !result.success,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
catch (err) {
|
|
525
|
-
logger.error({ err }, "bootstrap_scanner failed");
|
|
526
|
-
return {
|
|
527
|
-
content: [
|
|
528
|
-
{
|
|
529
|
-
type: "text",
|
|
530
|
-
text: JSON.stringify({ tool: "bootstrap_scanner", status: "error", message: err.message }, null, 2),
|
|
531
|
-
},
|
|
532
|
-
],
|
|
533
|
-
isError: true,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
case "auto_scan": {
|
|
538
|
-
logger.info({ tool: "auto_scan" }, "Tool call received");
|
|
539
|
-
try {
|
|
540
|
-
const result = await autoScan(config.pluginRoot, sarifStore, logger, {
|
|
541
|
-
engine: astEngine,
|
|
542
|
-
cyclomaticMax: config.cyclomaticMax,
|
|
543
|
-
exclude: userExclusions,
|
|
544
|
-
});
|
|
545
|
-
const markdown = renderAutoScanMarkdown(result);
|
|
546
|
-
return {
|
|
547
|
-
content: [
|
|
548
|
-
{ type: "text", text: markdown },
|
|
549
|
-
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
550
|
-
],
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
catch (err) {
|
|
554
|
-
logger.error({ err }, "auto_scan failed");
|
|
555
|
-
return {
|
|
556
|
-
content: [
|
|
557
|
-
{
|
|
558
|
-
type: "text",
|
|
559
|
-
text: JSON.stringify({ tool: "auto_scan", status: "error", message: err.message }, null, 2),
|
|
560
|
-
},
|
|
561
|
-
],
|
|
562
|
-
isError: true,
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
case "list_projects": {
|
|
567
|
-
return {
|
|
568
|
-
content: [
|
|
569
|
-
{
|
|
570
|
-
type: "text",
|
|
571
|
-
text: JSON.stringify({
|
|
572
|
-
tool: "list_projects",
|
|
573
|
-
isMonorepo: projectMap?.isMonorepo ?? false,
|
|
574
|
-
projects: projectMap?.projects ?? [],
|
|
575
|
-
}, null, 2),
|
|
576
|
-
},
|
|
577
|
-
],
|
|
578
|
-
};
|
|
579
|
-
}
|
|
280
|
+
case "compute_crap":
|
|
281
|
+
return handleComputeCrap(args ?? {});
|
|
282
|
+
case "compute_tdr":
|
|
283
|
+
return handleComputeTdr(args ?? {});
|
|
284
|
+
case "analyze_file_ast":
|
|
285
|
+
return handleAnalyzeFileAst(args ?? {});
|
|
286
|
+
case "score_project":
|
|
287
|
+
return handleScoreProject(args ?? {});
|
|
288
|
+
case "require_test_harness":
|
|
289
|
+
return handleRequireTestHarness(args ?? {});
|
|
290
|
+
case "ingest_scanner_output":
|
|
291
|
+
return handleIngestScannerOutput(args ?? {});
|
|
292
|
+
case "ingest_sarif":
|
|
293
|
+
return handleIngestSarif(args ?? {});
|
|
294
|
+
case "bootstrap_scanner":
|
|
295
|
+
return handleBootstrapScanner();
|
|
296
|
+
case "auto_scan":
|
|
297
|
+
return handleAutoScan();
|
|
298
|
+
case "list_projects":
|
|
299
|
+
return handleListProjects();
|
|
580
300
|
default:
|
|
581
301
|
throw new Error(`[claude-crap] Unknown tool: ${name}`);
|
|
582
302
|
}
|
|
583
303
|
}
|
|
304
|
+
async function handleScoreProject(args) {
|
|
305
|
+
const typed = args;
|
|
306
|
+
const format = typed.format ?? "both";
|
|
307
|
+
let scoreRoot = config.pluginRoot;
|
|
308
|
+
if (typed.scope && projectMap) {
|
|
309
|
+
const project = projectMap.projects.find((p) => p.name === typed.scope);
|
|
310
|
+
if (project) {
|
|
311
|
+
const { join } = await import("node:path");
|
|
312
|
+
scoreRoot = join(config.pluginRoot, project.path);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const workspace = await estimateWorkspaceLoc(scoreRoot, { exclude: userExclusions });
|
|
317
|
+
const score = computeProjectScore({
|
|
318
|
+
workspaceRoot: scoreRoot, minutesPerLoc: config.minutesPerLoc, tdrMaxRating: config.tdrMaxRating,
|
|
319
|
+
workspace: { physicalLoc: workspace.physicalLoc, fileCount: workspace.fileCount },
|
|
320
|
+
sarifStore, dashboardUrl: dashboard?.url ?? null, sarifReportPath: sarifStore.consolidatedReportPath,
|
|
321
|
+
});
|
|
322
|
+
const blocks = [];
|
|
323
|
+
if (format === "markdown" || format === "both")
|
|
324
|
+
blocks.push({ type: "text", text: renderProjectScoreMarkdown(score) });
|
|
325
|
+
if (format === "json" || format === "both")
|
|
326
|
+
blocks.push({ type: "text", text: JSON.stringify(score, null, 2) });
|
|
327
|
+
const strictness = safeLoadStrictness(config.pluginRoot, logger);
|
|
328
|
+
return { content: blocks, isError: strictness === "strict" && !score.overall.passes };
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
logger.error({ err }, "score_project failed");
|
|
332
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "score_project", status: "error", message: err.message }, null, 2) }], isError: true };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function handleRequireTestHarness(args) {
|
|
336
|
+
const typed = args;
|
|
337
|
+
const absolutePath = resolveWithinWorkspace(config.pluginRoot, typed.filePath);
|
|
338
|
+
try {
|
|
339
|
+
const resolution = await findTestFile(config.pluginRoot, absolutePath);
|
|
340
|
+
const hasTest = resolution.testFile !== null;
|
|
341
|
+
return {
|
|
342
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
343
|
+
tool: "require_test_harness", filePath: typed.filePath, hasTest, isTestFile: resolution.isTestFile,
|
|
344
|
+
testFile: resolution.testFile, candidates: resolution.candidates,
|
|
345
|
+
...(hasTest ? {} : { corrective: "No test file found. Per the CLAUDE.md Golden Rule, create a characterization test at one of the candidate paths before writing any functional code for this file." }),
|
|
346
|
+
}, null, 2) }],
|
|
347
|
+
isError: !hasTest,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
logger.error({ err, filePath: absolutePath }, "require_test_harness failed");
|
|
352
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "require_test_harness", status: "error", message: err.message, filePath: typed.filePath }, null, 2) }], isError: true };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function handleIngestScannerOutput(args) {
|
|
356
|
+
const typed = args;
|
|
357
|
+
try {
|
|
358
|
+
const adapted = adaptScannerOutput(typed.scanner, typed.rawOutput);
|
|
359
|
+
validateSarifDocument(adapted.document);
|
|
360
|
+
const stats = sarifStore.ingestRun(adapted.document, adapted.sourceTool);
|
|
361
|
+
await sarifStore.persist();
|
|
362
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
363
|
+
tool: "ingest_scanner_output", status: "accepted", scanner: typed.scanner,
|
|
364
|
+
findingsParsed: adapted.findingCount, totalEffortMinutes: adapted.totalEffortMinutes,
|
|
365
|
+
accepted: stats.accepted, duplicates: stats.duplicates, total: stats.total,
|
|
366
|
+
storeSize: sarifStore.size(), reportPath: sarifStore.consolidatedReportPath,
|
|
367
|
+
}, null, 2) }] };
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
logger.error({ err, scanner: typed.scanner }, "ingest_scanner_output failed");
|
|
371
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "ingest_scanner_output", status: "error", scanner: typed.scanner, message: err.message }, null, 2) }], isError: true };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async function handleIngestSarif(args) {
|
|
375
|
+
const typed = args;
|
|
376
|
+
try {
|
|
377
|
+
validateSarifDocument(typed.sarifDocument);
|
|
378
|
+
const stats = sarifStore.ingestRun(typed.sarifDocument, typed.sourceTool);
|
|
379
|
+
await sarifStore.persist();
|
|
380
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
381
|
+
tool: "ingest_sarif", status: "accepted", sourceTool: typed.sourceTool,
|
|
382
|
+
accepted: stats.accepted, duplicates: stats.duplicates, total: stats.total,
|
|
383
|
+
storeSize: sarifStore.size(), reportPath: sarifStore.consolidatedReportPath,
|
|
384
|
+
}, null, 2) }] };
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
logger.error({ err, sourceTool: typed.sourceTool }, "ingest_sarif failed");
|
|
388
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "ingest_sarif", status: "error", message: err.message }, null, 2) }], isError: true };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function handleBootstrapScanner() {
|
|
392
|
+
try {
|
|
393
|
+
const result = await bootstrapScanner(config.pluginRoot, sarifStore, logger);
|
|
394
|
+
return { content: [{ type: "text", text: renderBootstrapMarkdown(result) }, { type: "text", text: JSON.stringify(result, null, 2) }], isError: !result.success };
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
logger.error({ err }, "bootstrap_scanner failed");
|
|
398
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "bootstrap_scanner", status: "error", message: err.message }, null, 2) }], isError: true };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function handleAutoScan() {
|
|
402
|
+
try {
|
|
403
|
+
const result = await autoScan(config.pluginRoot, sarifStore, logger, { engine: astEngine, cyclomaticMax: config.cyclomaticMax, exclude: userExclusions });
|
|
404
|
+
return { content: [{ type: "text", text: renderAutoScanMarkdown(result) }, { type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
logger.error({ err }, "auto_scan failed");
|
|
408
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "auto_scan", status: "error", message: err.message }, null, 2) }], isError: true };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function handleListProjects() {
|
|
412
|
+
return { content: [{ type: "text", text: JSON.stringify({ tool: "list_projects", isMonorepo: projectMap?.isMonorepo ?? false, projects: projectMap?.projects ?? [] }, null, 2) }] };
|
|
413
|
+
}
|
|
584
414
|
// ------------------------------------------------------------------
|
|
585
415
|
// Resources — topology and reports
|
|
586
416
|
// ------------------------------------------------------------------
|