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 CHANGED
@@ -62,15 +62,16 @@ automatically — no further setup required.
62
62
 
63
63
  ## Configuration
64
64
 
65
- > **Default: `strict`.** No config file needed. The Stop gate
66
- > hard-fails on any policy violation out of the box.
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. **Default.** |
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": "warn"
82
+ "strictness": "strict"
82
83
  }
83
84
  ```
84
85
 
85
- Or per session: `CLAUDE_CRAP_STRICTNESS=advisory claude`
86
+ Or per session: `CLAUDE_CRAP_STRICTNESS=strict claude`
86
87
 
87
- **Precedence:** env var > `.claude-crap.json` > hardcoded `strict`.
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: www (TypeScript), app (TypeScript), mobile (Dart), api (C#)
187
- ESLint not installed? auto-install at monorepo root
188
- → run ESLint from root (covers all JS/TS)
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 the mobile app
201
- score_project({ format: "both", scope: "mobile" })
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 # 265 tests across 46 suites
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
  ```
@@ -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's hard-failing Stop gate stays the default experience.
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;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,UAAqB,CAAC;AAEvD;;;;;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"}
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"}
@@ -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's hard-failing Stop gate stays the default experience.
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 = "strict";
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
@@ -1 +1 @@
1
- {"version":3,"file":"crap-config.js","sourceRoot":"","sources":["../src/crap-config.ts"],"names":[],"mappings":"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"}
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
- const typed = args;
248
- const result = computeCrap({ cyclomaticComplexity: typed.cyclomaticComplexity, coveragePercent: typed.coveragePercent }, config.crapThreshold);
249
- return {
250
- content: [
251
- {
252
- type: "text",
253
- text: JSON.stringify({ tool: "compute_crap", function: typed.functionName, file: typed.filePath, ...result }, null, 2),
254
- },
255
- ],
256
- // Setting isError=true tells the LLM this call should be treated
257
- // as a failure, which pushes it toward corrective action rather
258
- // than assuming the score was acceptable.
259
- isError: result.exceedsThreshold,
260
- };
261
- }
262
- case "compute_tdr": {
263
- const typed = args;
264
- const result = computeTdr({
265
- remediationMinutes: typed.remediationMinutes,
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
  // ------------------------------------------------------------------