claude-crap 0.3.7 → 0.3.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.
Files changed (82) hide show
  1. package/README.md +25 -0
  2. package/dist/adapters/common.d.ts +1 -1
  3. package/dist/adapters/common.d.ts.map +1 -1
  4. package/dist/adapters/common.js +1 -1
  5. package/dist/adapters/common.js.map +1 -1
  6. package/dist/adapters/dart-analyzer.d.ts +41 -0
  7. package/dist/adapters/dart-analyzer.d.ts.map +1 -0
  8. package/dist/adapters/dart-analyzer.js +120 -0
  9. package/dist/adapters/dart-analyzer.js.map +1 -0
  10. package/dist/adapters/index.d.ts +1 -0
  11. package/dist/adapters/index.d.ts.map +1 -1
  12. package/dist/adapters/index.js +4 -0
  13. package/dist/adapters/index.js.map +1 -1
  14. package/dist/crap-config.d.ts +2 -0
  15. package/dist/crap-config.d.ts.map +1 -1
  16. package/dist/crap-config.js +36 -28
  17. package/dist/crap-config.js.map +1 -1
  18. package/dist/dashboard/file-detail.d.ts.map +1 -1
  19. package/dist/dashboard/file-detail.js.map +1 -1
  20. package/dist/dashboard/server.d.ts +2 -0
  21. package/dist/dashboard/server.d.ts.map +1 -1
  22. package/dist/dashboard/server.js +7 -12
  23. package/dist/dashboard/server.js.map +1 -1
  24. package/dist/index.js +17 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/metrics/workspace-walker.d.ts +4 -1
  27. package/dist/metrics/workspace-walker.d.ts.map +1 -1
  28. package/dist/metrics/workspace-walker.js +12 -28
  29. package/dist/metrics/workspace-walker.js.map +1 -1
  30. package/dist/scanner/auto-scan.d.ts +1 -0
  31. package/dist/scanner/auto-scan.d.ts.map +1 -1
  32. package/dist/scanner/auto-scan.js +14 -5
  33. package/dist/scanner/auto-scan.js.map +1 -1
  34. package/dist/scanner/bootstrap.d.ts +1 -1
  35. package/dist/scanner/bootstrap.d.ts.map +1 -1
  36. package/dist/scanner/bootstrap.js +9 -0
  37. package/dist/scanner/bootstrap.js.map +1 -1
  38. package/dist/scanner/complexity-scanner.d.ts +2 -0
  39. package/dist/scanner/complexity-scanner.d.ts.map +1 -1
  40. package/dist/scanner/complexity-scanner.js +11 -26
  41. package/dist/scanner/complexity-scanner.js.map +1 -1
  42. package/dist/scanner/detector.d.ts +24 -4
  43. package/dist/scanner/detector.d.ts.map +1 -1
  44. package/dist/scanner/detector.js +105 -10
  45. package/dist/scanner/detector.js.map +1 -1
  46. package/dist/scanner/runner.d.ts +4 -1
  47. package/dist/scanner/runner.d.ts.map +1 -1
  48. package/dist/scanner/runner.js +12 -3
  49. package/dist/scanner/runner.js.map +1 -1
  50. package/dist/schemas/tool-schemas.d.ts +1 -1
  51. package/dist/schemas/tool-schemas.js +1 -1
  52. package/dist/schemas/tool-schemas.js.map +1 -1
  53. package/dist/shared/exclusions.d.ts +53 -0
  54. package/dist/shared/exclusions.d.ts.map +1 -0
  55. package/dist/shared/exclusions.js +126 -0
  56. package/dist/shared/exclusions.js.map +1 -0
  57. package/package.json +3 -1
  58. package/plugin/.claude-plugin/plugin.json +1 -1
  59. package/plugin/bundle/mcp-server.mjs +393 -141
  60. package/plugin/bundle/mcp-server.mjs.map +4 -4
  61. package/plugin/package-lock.json +15 -2
  62. package/plugin/package.json +2 -1
  63. package/scripts/bundle-plugin.mjs +2 -1
  64. package/src/adapters/common.ts +1 -1
  65. package/src/adapters/dart-analyzer.ts +161 -0
  66. package/src/adapters/index.ts +4 -0
  67. package/src/crap-config.ts +55 -18
  68. package/src/dashboard/file-detail.ts +0 -2
  69. package/src/dashboard/server.ts +9 -10
  70. package/src/index.ts +17 -2
  71. package/src/metrics/workspace-walker.ts +15 -27
  72. package/src/scanner/auto-scan.ts +17 -6
  73. package/src/scanner/bootstrap.ts +11 -0
  74. package/src/scanner/complexity-scanner.ts +15 -26
  75. package/src/scanner/detector.ts +114 -10
  76. package/src/scanner/runner.ts +12 -2
  77. package/src/schemas/tool-schemas.ts +1 -1
  78. package/src/shared/exclusions.ts +156 -0
  79. package/src/tests/adapters/dispatch.test.ts +2 -2
  80. package/src/tests/auto-scan.test.ts +2 -2
  81. package/src/tests/exclusions.test.ts +117 -0
  82. package/src/tests/scanner-detector.test.ts +31 -11
@@ -2977,7 +2977,7 @@ var require_compile = __commonJS({
2977
2977
  const schOrFunc = root.refs[ref];
2978
2978
  if (schOrFunc)
2979
2979
  return schOrFunc;
2980
- let _sch = resolve6.call(this, root, ref);
2980
+ let _sch = resolve7.call(this, root, ref);
2981
2981
  if (_sch === void 0) {
2982
2982
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
2983
2983
  const { schemaId } = this.opts;
@@ -3004,7 +3004,7 @@ var require_compile = __commonJS({
3004
3004
  function sameSchemaEnv(s1, s2) {
3005
3005
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3006
3006
  }
3007
- function resolve6(root, ref) {
3007
+ function resolve7(root, ref) {
3008
3008
  let sch;
3009
3009
  while (typeof (sch = this.refs[ref]) == "string")
3010
3010
  ref = sch;
@@ -3579,55 +3579,55 @@ var require_fast_uri = __commonJS({
3579
3579
  }
3580
3580
  return uri;
3581
3581
  }
3582
- function resolve6(baseURI, relativeURI, options) {
3582
+ function resolve7(baseURI, relativeURI, options) {
3583
3583
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3584
3584
  const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
3585
3585
  schemelessOptions.skipEscape = true;
3586
3586
  return serialize(resolved, schemelessOptions);
3587
3587
  }
3588
- function resolveComponent(base, relative3, options, skipNormalization) {
3588
+ function resolveComponent(base, relative4, options, skipNormalization) {
3589
3589
  const target = {};
3590
3590
  if (!skipNormalization) {
3591
3591
  base = parse(serialize(base, options), options);
3592
- relative3 = parse(serialize(relative3, options), options);
3592
+ relative4 = parse(serialize(relative4, options), options);
3593
3593
  }
3594
3594
  options = options || {};
3595
- if (!options.tolerant && relative3.scheme) {
3596
- target.scheme = relative3.scheme;
3597
- target.userinfo = relative3.userinfo;
3598
- target.host = relative3.host;
3599
- target.port = relative3.port;
3600
- target.path = removeDotSegments(relative3.path || "");
3601
- target.query = relative3.query;
3595
+ if (!options.tolerant && relative4.scheme) {
3596
+ target.scheme = relative4.scheme;
3597
+ target.userinfo = relative4.userinfo;
3598
+ target.host = relative4.host;
3599
+ target.port = relative4.port;
3600
+ target.path = removeDotSegments(relative4.path || "");
3601
+ target.query = relative4.query;
3602
3602
  } else {
3603
- if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
3604
- target.userinfo = relative3.userinfo;
3605
- target.host = relative3.host;
3606
- target.port = relative3.port;
3607
- target.path = removeDotSegments(relative3.path || "");
3608
- target.query = relative3.query;
3603
+ if (relative4.userinfo !== void 0 || relative4.host !== void 0 || relative4.port !== void 0) {
3604
+ target.userinfo = relative4.userinfo;
3605
+ target.host = relative4.host;
3606
+ target.port = relative4.port;
3607
+ target.path = removeDotSegments(relative4.path || "");
3608
+ target.query = relative4.query;
3609
3609
  } else {
3610
- if (!relative3.path) {
3610
+ if (!relative4.path) {
3611
3611
  target.path = base.path;
3612
- if (relative3.query !== void 0) {
3613
- target.query = relative3.query;
3612
+ if (relative4.query !== void 0) {
3613
+ target.query = relative4.query;
3614
3614
  } else {
3615
3615
  target.query = base.query;
3616
3616
  }
3617
3617
  } else {
3618
- if (relative3.path[0] === "/") {
3619
- target.path = removeDotSegments(relative3.path);
3618
+ if (relative4.path[0] === "/") {
3619
+ target.path = removeDotSegments(relative4.path);
3620
3620
  } else {
3621
3621
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
3622
- target.path = "/" + relative3.path;
3622
+ target.path = "/" + relative4.path;
3623
3623
  } else if (!base.path) {
3624
- target.path = relative3.path;
3624
+ target.path = relative4.path;
3625
3625
  } else {
3626
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
3626
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
3627
3627
  }
3628
3628
  target.path = removeDotSegments(target.path);
3629
3629
  }
3630
- target.query = relative3.query;
3630
+ target.query = relative4.query;
3631
3631
  }
3632
3632
  target.userinfo = base.userinfo;
3633
3633
  target.host = base.host;
@@ -3635,7 +3635,7 @@ var require_fast_uri = __commonJS({
3635
3635
  }
3636
3636
  target.scheme = base.scheme;
3637
3637
  }
3638
- target.fragment = relative3.fragment;
3638
+ target.fragment = relative4.fragment;
3639
3639
  return target;
3640
3640
  }
3641
3641
  function equal(uriA, uriB, options) {
@@ -3806,7 +3806,7 @@ var require_fast_uri = __commonJS({
3806
3806
  var fastUri = {
3807
3807
  SCHEMES,
3808
3808
  normalize,
3809
- resolve: resolve6,
3809
+ resolve: resolve7,
3810
3810
  resolveComponent,
3811
3811
  equal,
3812
3812
  serialize,
@@ -6853,6 +6853,84 @@ function buildSarifResult3(opts) {
6853
6853
  };
6854
6854
  }
6855
6855
 
6856
+ // src/adapters/dart-analyzer.ts
6857
+ function mapSeverity3(dartSeverity) {
6858
+ switch (dartSeverity.toUpperCase()) {
6859
+ case "ERROR":
6860
+ return "error";
6861
+ case "WARNING":
6862
+ return "warning";
6863
+ case "INFO":
6864
+ return "note";
6865
+ default:
6866
+ return "warning";
6867
+ }
6868
+ }
6869
+ var EFFORT_BY_SEVERITY = {
6870
+ error: 30,
6871
+ warning: 15,
6872
+ note: 5,
6873
+ none: 0
6874
+ };
6875
+ function adaptDartAnalyzer(rawOutput) {
6876
+ let parsed;
6877
+ if (typeof rawOutput === "string") {
6878
+ try {
6879
+ parsed = JSON.parse(rawOutput);
6880
+ } catch {
6881
+ throw new Error("[dart-analyzer adapter] rawOutput is not valid JSON");
6882
+ }
6883
+ } else if (rawOutput && typeof rawOutput === "object" && "diagnostics" in rawOutput) {
6884
+ parsed = rawOutput;
6885
+ } else {
6886
+ throw new Error(
6887
+ "[dart-analyzer adapter] rawOutput must be a JSON string or an object with a 'diagnostics' array"
6888
+ );
6889
+ }
6890
+ if (!Array.isArray(parsed.diagnostics)) {
6891
+ throw new Error("[dart-analyzer adapter] 'diagnostics' must be an array");
6892
+ }
6893
+ const results = [];
6894
+ let totalEffortMinutes = 0;
6895
+ for (const diag of parsed.diagnostics) {
6896
+ const level = mapSeverity3(diag.severity);
6897
+ const effort = EFFORT_BY_SEVERITY[level] ?? estimateEffortMinutes(level);
6898
+ totalEffortMinutes += effort;
6899
+ results.push({
6900
+ ruleId: diag.code,
6901
+ level,
6902
+ message: {
6903
+ text: diag.problemMessage + (diag.correctionMessage ? ` ${diag.correctionMessage}` : "")
6904
+ },
6905
+ locations: [
6906
+ {
6907
+ physicalLocation: {
6908
+ artifactLocation: {
6909
+ uri: diag.location.file
6910
+ },
6911
+ region: {
6912
+ startLine: diag.location.range.start.line,
6913
+ startColumn: diag.location.range.start.column,
6914
+ endLine: diag.location.range.end.line,
6915
+ endColumn: diag.location.range.end.column
6916
+ }
6917
+ }
6918
+ }
6919
+ ],
6920
+ properties: {
6921
+ effortMinutes: effort,
6922
+ ...diag.documentation ? { helpUri: diag.documentation } : {}
6923
+ }
6924
+ });
6925
+ }
6926
+ return {
6927
+ document: wrapResultsInSarif("dart_analyze", "1.0.0", results),
6928
+ sourceTool: "dart_analyze",
6929
+ findingCount: parsed.diagnostics.length,
6930
+ totalEffortMinutes
6931
+ };
6932
+ }
6933
+
6856
6934
  // src/adapters/index.ts
6857
6935
  function adaptScannerOutput(scanner, rawOutput) {
6858
6936
  switch (scanner) {
@@ -6864,6 +6942,8 @@ function adaptScannerOutput(scanner, rawOutput) {
6864
6942
  return adaptBandit(rawOutput);
6865
6943
  case "stryker":
6866
6944
  return adaptStryker(rawOutput);
6945
+ case "dart_analyze":
6946
+ return adaptDartAnalyzer(rawOutput);
6867
6947
  default: {
6868
6948
  const exhaustive = scanner;
6869
6949
  throw new Error(`[adapters] Unknown scanner: ${String(exhaustive)}`);
@@ -7250,6 +7330,100 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
7250
7330
  import Fastify from "fastify";
7251
7331
  import fastifyStatic from "@fastify/static";
7252
7332
 
7333
+ // src/shared/exclusions.ts
7334
+ import picomatch from "picomatch";
7335
+ var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
7336
+ // Package managers / vendored deps
7337
+ "node_modules",
7338
+ "vendor",
7339
+ // Version control
7340
+ ".git",
7341
+ // Build outputs (general)
7342
+ "dist",
7343
+ "build",
7344
+ "bundle",
7345
+ "out",
7346
+ "target",
7347
+ "coverage",
7348
+ // Framework build outputs
7349
+ ".next",
7350
+ // Next.js
7351
+ ".nuxt",
7352
+ // Nuxt 2
7353
+ ".output",
7354
+ // Nuxt 3
7355
+ ".vercel",
7356
+ // Vercel
7357
+ ".svelte-kit",
7358
+ // SvelteKit
7359
+ ".astro",
7360
+ // Astro
7361
+ ".angular",
7362
+ // Angular
7363
+ ".turbo",
7364
+ // Turborepo
7365
+ ".parcel-cache",
7366
+ // Parcel
7367
+ ".expo",
7368
+ // Expo / React Native
7369
+ // Language-specific caches
7370
+ ".venv",
7371
+ "venv",
7372
+ "__pycache__",
7373
+ ".cache",
7374
+ ".dart_tool",
7375
+ // Dart / Flutter
7376
+ ".gradle",
7377
+ // Gradle
7378
+ // IDE state
7379
+ ".idea",
7380
+ // Plugin state
7381
+ ".claude-crap",
7382
+ ".codesight"
7383
+ ]);
7384
+ var DEFAULT_SKIP_PATTERNS = [
7385
+ "*.min.js",
7386
+ "*.min.css",
7387
+ "*.min.mjs",
7388
+ "*.min.cjs",
7389
+ "*.bundle.js",
7390
+ "*.chunk.js"
7391
+ ];
7392
+ function createExclusionFilter(userExclusions) {
7393
+ const extraDirs = /* @__PURE__ */ new Set();
7394
+ const fileGlobs = [];
7395
+ for (const pattern of userExclusions ?? []) {
7396
+ if (pattern.endsWith("/")) {
7397
+ extraDirs.add(pattern.slice(0, -1));
7398
+ } else {
7399
+ fileGlobs.push(pattern);
7400
+ }
7401
+ }
7402
+ const defaultFileMatchers = DEFAULT_SKIP_PATTERNS.map(
7403
+ (p) => picomatch(p, { dot: true })
7404
+ );
7405
+ const userFileMatchers = fileGlobs.map(
7406
+ (p) => picomatch(p, { dot: true })
7407
+ );
7408
+ return {
7409
+ shouldSkipDir(dirName) {
7410
+ if (dirName.startsWith(".") && dirName !== ".claude-plugin") {
7411
+ return DEFAULT_SKIP_DIRS.has(dirName) || true;
7412
+ }
7413
+ return DEFAULT_SKIP_DIRS.has(dirName) || extraDirs.has(dirName);
7414
+ },
7415
+ shouldSkipFile(relativePath, fileName) {
7416
+ for (const matcher of defaultFileMatchers) {
7417
+ if (matcher(fileName)) return true;
7418
+ }
7419
+ for (const matcher of userFileMatchers) {
7420
+ if (matcher(relativePath) || matcher(fileName)) return true;
7421
+ }
7422
+ return false;
7423
+ }
7424
+ };
7425
+ }
7426
+
7253
7427
  // src/metrics/tdr.ts
7254
7428
  var RATING_ORDER = ["A", "B", "C", "D", "E"];
7255
7429
  function ratingToRank(rating) {
@@ -7524,7 +7698,7 @@ async function startDashboard(options) {
7524
7698
  root: publicRoot,
7525
7699
  prefix: "/"
7526
7700
  });
7527
- fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.7" }));
7701
+ fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.8" }));
7528
7702
  fastify.get("/api/score", async () => {
7529
7703
  const stats = await workspaceStatsProvider();
7530
7704
  const score = await buildScore(config, sarifStore, stats, urlOf(fastify, config));
@@ -7535,7 +7709,7 @@ async function startDashboard(options) {
7535
7709
  if (!options.astEngine) {
7536
7710
  return { threshold: config.cyclomaticMax, totalFunctions: 0, violationCount: 0, topFunctions: [] };
7537
7711
  }
7538
- return buildComplexityReport(config, options.astEngine, logger2);
7712
+ return buildComplexityReport(config, options.astEngine, logger2, options.exclude);
7539
7713
  });
7540
7714
  fastify.get("/api/file-detail", async (request, reply) => {
7541
7715
  const { path: filePath } = request.query;
@@ -7681,24 +7855,9 @@ async function killStaleDashboard(pidFilePath, port, logger2) {
7681
7855
  removePidFile(pidFilePath);
7682
7856
  await new Promise((r) => setTimeout(r, 300));
7683
7857
  }
7684
- var SKIP_DIRS = /* @__PURE__ */ new Set([
7685
- "node_modules",
7686
- ".git",
7687
- "dist",
7688
- "build",
7689
- "out",
7690
- "target",
7691
- ".venv",
7692
- "venv",
7693
- "__pycache__",
7694
- ".cache",
7695
- ".next",
7696
- ".nuxt",
7697
- ".claude-crap",
7698
- ".codesight"
7699
- ]);
7700
- async function buildComplexityReport(config, engine, logger2) {
7858
+ async function buildComplexityReport(config, engine, logger2, exclude) {
7701
7859
  const threshold = config.cyclomaticMax;
7860
+ const filter = createExclusionFilter(exclude);
7702
7861
  const allFunctions = [];
7703
7862
  let totalFunctions = 0;
7704
7863
  async function walk2(dir) {
@@ -7709,10 +7868,9 @@ async function buildComplexityReport(config, engine, logger2) {
7709
7868
  return;
7710
7869
  }
7711
7870
  for (const entry of entries) {
7712
- if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
7713
7871
  const full = join2(dir, entry.name);
7714
7872
  if (entry.isDirectory()) {
7715
- if (SKIP_DIRS.has(entry.name)) continue;
7873
+ if (filter.shouldSkipDir(entry.name)) continue;
7716
7874
  await walk2(full);
7717
7875
  continue;
7718
7876
  }
@@ -7791,23 +7949,7 @@ function computeCrap(input, threshold) {
7791
7949
 
7792
7950
  // src/metrics/workspace-walker.ts
7793
7951
  import { promises as fs4 } from "node:fs";
7794
- import { join as join3 } from "node:path";
7795
- var SKIP_DIRS2 = /* @__PURE__ */ new Set([
7796
- "node_modules",
7797
- ".git",
7798
- "dist",
7799
- "build",
7800
- "out",
7801
- "target",
7802
- ".venv",
7803
- "venv",
7804
- "__pycache__",
7805
- ".cache",
7806
- ".next",
7807
- ".nuxt",
7808
- ".claude-crap",
7809
- ".codesight"
7810
- ]);
7952
+ import { join as join3, relative } from "node:path";
7811
7953
  var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
7812
7954
  ".ts",
7813
7955
  ".tsx",
@@ -7831,7 +7973,8 @@ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
7831
7973
  ".vue"
7832
7974
  ]);
7833
7975
  var MAX_FILES_WALKED = 2e4;
7834
- async function estimateWorkspaceLoc(workspaceRoot) {
7976
+ async function estimateWorkspaceLoc(workspaceRoot, options) {
7977
+ const filter = createExclusionFilter(options?.exclude);
7835
7978
  let physicalLoc = 0;
7836
7979
  let fileCount = 0;
7837
7980
  let truncated = false;
@@ -7845,10 +7988,9 @@ async function estimateWorkspaceLoc(workspaceRoot) {
7845
7988
  }
7846
7989
  for (const entry of entries) {
7847
7990
  if (truncated) return;
7848
- if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
7849
7991
  const full = join3(dir, entry.name);
7850
7992
  if (entry.isDirectory()) {
7851
- if (SKIP_DIRS2.has(entry.name)) continue;
7993
+ if (filter.shouldSkipDir(entry.name)) continue;
7852
7994
  await walk2(full);
7853
7995
  continue;
7854
7996
  }
@@ -7858,6 +8000,8 @@ async function estimateWorkspaceLoc(workspaceRoot) {
7858
8000
  if (dot < 0) continue;
7859
8001
  const ext = lower.substring(dot);
7860
8002
  if (!CODE_EXTENSIONS.has(ext)) continue;
8003
+ const relPath = relative(workspaceRoot, full);
8004
+ if (filter.shouldSkipFile(relPath, entry.name)) continue;
7861
8005
  fileCount += 1;
7862
8006
  if (fileCount > MAX_FILES_WALKED) {
7863
8007
  truncated = true;
@@ -8227,6 +8371,8 @@ var CrapConfigError = class extends Error {
8227
8371
  }
8228
8372
  };
8229
8373
  function loadCrapConfig(options) {
8374
+ const fileResult = readFromFile(options.workspaceRoot);
8375
+ const exclude = fileResult?.exclude ?? [];
8230
8376
  const envRaw = process.env["CLAUDE_CRAP_STRICTNESS"];
8231
8377
  if (typeof envRaw === "string" && envRaw.trim() !== "") {
8232
8378
  const normalized = envRaw.trim().toLowerCase();
@@ -8235,11 +8381,12 @@ function loadCrapConfig(options) {
8235
8381
  `[crap-config] CLAUDE_CRAP_STRICTNESS="${envRaw}" is not a valid strictness. Expected one of: ${STRICTNESS_VALUES.join(", ")}.`
8236
8382
  );
8237
8383
  }
8238
- return { strictness: normalized, strictnessSource: "env" };
8384
+ return { strictness: normalized, strictnessSource: "env", exclude };
8385
+ }
8386
+ if (fileResult?.strictness) {
8387
+ return { strictness: fileResult.strictness, strictnessSource: "file", exclude };
8239
8388
  }
8240
- const fromFile = readFromFile(options.workspaceRoot);
8241
- if (fromFile) return { strictness: fromFile, strictnessSource: "file" };
8242
- return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default" };
8389
+ return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default", exclude };
8243
8390
  }
8244
8391
  function readFromFile(workspaceRoot) {
8245
8392
  const filePath = join5(workspaceRoot, ".claude-crap.json");
@@ -8267,20 +8414,40 @@ function readFromFile(workspaceRoot) {
8267
8414
  );
8268
8415
  }
8269
8416
  const doc = parsed;
8270
- if (!("strictness" in doc)) return null;
8271
- const value = doc["strictness"];
8272
- if (typeof value !== "string") {
8273
- throw new CrapConfigError(
8274
- `[crap-config] ${filePath}: 'strictness' must be a string, got ${typeof value}`
8275
- );
8417
+ let strictness = null;
8418
+ if ("strictness" in doc) {
8419
+ const value = doc["strictness"];
8420
+ if (typeof value !== "string") {
8421
+ throw new CrapConfigError(
8422
+ `[crap-config] ${filePath}: 'strictness' must be a string, got ${typeof value}`
8423
+ );
8424
+ }
8425
+ const normalized = value.trim().toLowerCase();
8426
+ if (!isStrictness(normalized)) {
8427
+ throw new CrapConfigError(
8428
+ `[crap-config] ${filePath}: 'strictness' is "${value}"; expected one of ${STRICTNESS_VALUES.join(", ")}.`
8429
+ );
8430
+ }
8431
+ strictness = normalized;
8276
8432
  }
8277
- const normalized = value.trim().toLowerCase();
8278
- if (!isStrictness(normalized)) {
8279
- throw new CrapConfigError(
8280
- `[crap-config] ${filePath}: 'strictness' is "${value}"; expected one of ${STRICTNESS_VALUES.join(", ")}.`
8281
- );
8433
+ let exclude = [];
8434
+ if ("exclude" in doc) {
8435
+ const raw2 = doc["exclude"];
8436
+ if (!Array.isArray(raw2)) {
8437
+ throw new CrapConfigError(
8438
+ `[crap-config] ${filePath}: 'exclude' must be an array of strings`
8439
+ );
8440
+ }
8441
+ for (const item of raw2) {
8442
+ if (typeof item !== "string") {
8443
+ throw new CrapConfigError(
8444
+ `[crap-config] ${filePath}: every entry in 'exclude' must be a string, got ${typeof item}`
8445
+ );
8446
+ }
8447
+ }
8448
+ exclude = raw2;
8282
8449
  }
8283
- return normalized;
8450
+ return { strictness, exclude };
8284
8451
  }
8285
8452
  function isStrictness(value) {
8286
8453
  return STRICTNESS_VALUES.includes(value);
@@ -8288,7 +8455,7 @@ function isStrictness(value) {
8288
8455
 
8289
8456
  // src/tools/test-harness.ts
8290
8457
  import { promises as fs6 } from "node:fs";
8291
- import { basename, dirname as dirname4, extname, isAbsolute as isAbsolute3, join as join6, relative, resolve as resolve5, sep as sep2 } from "node:path";
8458
+ import { basename, dirname as dirname4, extname, isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve as resolve5, sep as sep2 } from "node:path";
8292
8459
  var TEST_SUFFIX_PATTERN = /\.(test|spec)\./;
8293
8460
  function isTestFile(filePath) {
8294
8461
  const base = basename(filePath);
@@ -8303,7 +8470,7 @@ function candidatePaths(workspaceRoot, filePath) {
8303
8470
  const base = basename(absSource, ext);
8304
8471
  const dir = dirname4(absSource);
8305
8472
  const absWorkspace = resolve5(workspaceRoot);
8306
- const relFromRoot = relative(absWorkspace, absSource);
8473
+ const relFromRoot = relative2(absWorkspace, absSource);
8307
8474
  const relDir = dirname4(relFromRoot);
8308
8475
  const candidates = /* @__PURE__ */ new Set();
8309
8476
  candidates.add(join6(dir, `${base}.test${ext}`));
@@ -8355,8 +8522,8 @@ import { existsSync as existsSync5 } from "node:fs";
8355
8522
  import { join as join11 } from "node:path";
8356
8523
 
8357
8524
  // src/scanner/detector.ts
8358
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
8359
- import { join as join7 } from "node:path";
8525
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "node:fs";
8526
+ import { join as join7, resolve as resolve6 } from "node:path";
8360
8527
  import { execFile } from "node:child_process";
8361
8528
  var SCANNER_SIGNALS = {
8362
8529
  eslint: {
@@ -8405,6 +8572,14 @@ var SCANNER_SIGNALS = {
8405
8572
  ],
8406
8573
  packageJsonKeys: ["@stryker-mutator/core"],
8407
8574
  binaryNames: ["stryker"]
8575
+ },
8576
+ dart_analyze: {
8577
+ configFiles: [
8578
+ "analysis_options.yaml",
8579
+ "pubspec.yaml"
8580
+ ],
8581
+ packageJsonKeys: [],
8582
+ binaryNames: ["dart"]
8408
8583
  }
8409
8584
  };
8410
8585
  function probeConfigFiles(workspaceRoot, scanner) {
@@ -8435,14 +8610,14 @@ function probePackageJson(workspaceRoot, scanner) {
8435
8610
  }
8436
8611
  }
8437
8612
  function probeBinary(binaryName) {
8438
- return new Promise((resolve6) => {
8613
+ return new Promise((resolve7) => {
8439
8614
  execFile("which", [binaryName], { timeout: 5e3 }, (err) => {
8440
- resolve6(err === null);
8615
+ resolve7(err === null);
8441
8616
  });
8442
8617
  });
8443
8618
  }
8444
8619
  async function detectScanners(workspaceRoot) {
8445
- const scanners = ["eslint", "semgrep", "bandit", "stryker"];
8620
+ const scanners = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze"];
8446
8621
  const results = await Promise.all(
8447
8622
  scanners.map(async (scanner) => {
8448
8623
  const configProbe = probeConfigFiles(workspaceRoot, scanner);
@@ -8455,10 +8630,13 @@ async function detectScanners(workspaceRoot) {
8455
8630
  };
8456
8631
  }
8457
8632
  if (probePackageJson(workspaceRoot, scanner)) {
8633
+ const binName = SCANNER_SIGNALS[scanner].binaryNames[0];
8634
+ const binPath = binName ? join7(workspaceRoot, "node_modules", ".bin", binName) : null;
8635
+ const installed = binPath !== null && existsSync2(binPath);
8458
8636
  return {
8459
8637
  scanner,
8460
- available: true,
8461
- reason: `found in package.json dependencies`
8638
+ available: installed,
8639
+ reason: installed ? "found in package.json and installed" : `found in package.json but not installed (run \`npm install\`)`
8462
8640
  };
8463
8641
  }
8464
8642
  const signals = SCANNER_SIGNALS[scanner];
@@ -8480,6 +8658,58 @@ async function detectScanners(workspaceRoot) {
8480
8658
  );
8481
8659
  return results;
8482
8660
  }
8661
+ var MONOREPO_DIRS = ["apps", "packages", "libs", "modules", "services"];
8662
+ async function detectMonorepoScanners(workspaceRoot) {
8663
+ const subdirs = /* @__PURE__ */ new Set();
8664
+ try {
8665
+ const pkgPath = join7(workspaceRoot, "package.json");
8666
+ const raw = readFileSync3(pkgPath, "utf-8");
8667
+ const pkg = JSON.parse(raw);
8668
+ if (Array.isArray(pkg.workspaces)) {
8669
+ for (const ws of pkg.workspaces) {
8670
+ if (typeof ws === "string" && !ws.includes("*")) {
8671
+ const full = resolve6(workspaceRoot, ws);
8672
+ if (existsSync2(full)) subdirs.add(full);
8673
+ }
8674
+ }
8675
+ }
8676
+ } catch {
8677
+ }
8678
+ for (const dir of MONOREPO_DIRS) {
8679
+ const full = join7(workspaceRoot, dir);
8680
+ try {
8681
+ const entries = readdirSync(full, { withFileTypes: true });
8682
+ for (const entry of entries) {
8683
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
8684
+ subdirs.add(join7(full, entry.name));
8685
+ }
8686
+ }
8687
+ } catch {
8688
+ }
8689
+ }
8690
+ if (subdirs.size === 0) return [];
8691
+ const detections = [];
8692
+ const scanners = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze"];
8693
+ for (const subdir of subdirs) {
8694
+ for (const scanner of scanners) {
8695
+ const configProbe = probeConfigFiles(subdir, scanner);
8696
+ if (!configProbe.found) continue;
8697
+ if (scanner === "dart_analyze") {
8698
+ const hasBinary = await probeBinary("dart");
8699
+ if (!hasBinary) continue;
8700
+ }
8701
+ const relDir = subdir.replace(workspaceRoot + "/", "");
8702
+ detections.push({
8703
+ scanner,
8704
+ available: true,
8705
+ reason: `config file found in ${relDir}/`,
8706
+ ...configProbe.path ? { configPath: configProbe.path } : {},
8707
+ workingDir: subdir
8708
+ });
8709
+ }
8710
+ }
8711
+ return detections;
8712
+ }
8483
8713
 
8484
8714
  // src/scanner/runner.ts
8485
8715
  import { execFile as execFile2 } from "node:child_process";
@@ -8516,17 +8746,26 @@ function getScannerCommand(scanner, workspaceRoot) {
8516
8746
  nonZeroIsNormal: false,
8517
8747
  outputFile: join8(workspaceRoot, "reports", "mutation", "mutation.json")
8518
8748
  };
8749
+ case "dart_analyze":
8750
+ return {
8751
+ command: "dart",
8752
+ args: ["analyze", "--format=json", "."],
8753
+ timeoutMs: 12e4,
8754
+ nonZeroIsNormal: true
8755
+ // exits 3 when findings exist
8756
+ };
8519
8757
  }
8520
8758
  }
8521
- function runScanner(scanner, workspaceRoot) {
8759
+ function runScanner(scanner, workspaceRoot, options) {
8522
8760
  const start = Date.now();
8523
- const cmd = getScannerCommand(scanner, workspaceRoot);
8524
- return new Promise((resolve6) => {
8761
+ const cwd = options?.workingDir ?? workspaceRoot;
8762
+ const cmd = getScannerCommand(scanner, cwd);
8763
+ return new Promise((resolve7) => {
8525
8764
  execFile2(
8526
8765
  cmd.command,
8527
8766
  cmd.args,
8528
8767
  {
8529
- cwd: workspaceRoot,
8768
+ cwd,
8530
8769
  timeout: cmd.timeoutMs,
8531
8770
  maxBuffer: 50 * 1024 * 1024,
8532
8771
  // 50 MB — large codebases produce verbose output
@@ -8540,7 +8779,7 @@ function runScanner(scanner, workspaceRoot) {
8540
8779
  if (cmd.outputFile && existsSync3(cmd.outputFile)) {
8541
8780
  try {
8542
8781
  const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
8543
- resolve6({
8782
+ resolve7({
8544
8783
  scanner,
8545
8784
  success: true,
8546
8785
  rawOutput: fileOutput,
@@ -8550,7 +8789,7 @@ function runScanner(scanner, workspaceRoot) {
8550
8789
  } catch {
8551
8790
  }
8552
8791
  }
8553
- resolve6({
8792
+ resolve7({
8554
8793
  scanner,
8555
8794
  success: false,
8556
8795
  rawOutput: "",
@@ -8563,7 +8802,7 @@ function runScanner(scanner, workspaceRoot) {
8563
8802
  if (existsSync3(cmd.outputFile)) {
8564
8803
  try {
8565
8804
  const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
8566
- resolve6({
8805
+ resolve7({
8567
8806
  scanner,
8568
8807
  success: true,
8569
8808
  rawOutput: fileOutput,
@@ -8571,7 +8810,7 @@ function runScanner(scanner, workspaceRoot) {
8571
8810
  });
8572
8811
  return;
8573
8812
  } catch (readErr) {
8574
- resolve6({
8813
+ resolve7({
8575
8814
  scanner,
8576
8815
  success: false,
8577
8816
  rawOutput: "",
@@ -8581,7 +8820,7 @@ function runScanner(scanner, workspaceRoot) {
8581
8820
  return;
8582
8821
  }
8583
8822
  }
8584
- resolve6({
8823
+ resolve7({
8585
8824
  scanner,
8586
8825
  success: false,
8587
8826
  rawOutput: "",
@@ -8592,7 +8831,7 @@ function runScanner(scanner, workspaceRoot) {
8592
8831
  }
8593
8832
  const output = stdout.trim();
8594
8833
  if (!output) {
8595
- resolve6({
8834
+ resolve7({
8596
8835
  scanner,
8597
8836
  success: true,
8598
8837
  rawOutput: "[]",
@@ -8601,7 +8840,7 @@ function runScanner(scanner, workspaceRoot) {
8601
8840
  });
8602
8841
  return;
8603
8842
  }
8604
- resolve6({
8843
+ resolve7({
8605
8844
  scanner,
8606
8845
  success: true,
8607
8846
  rawOutput: output,
@@ -8613,7 +8852,7 @@ function runScanner(scanner, workspaceRoot) {
8613
8852
  }
8614
8853
 
8615
8854
  // src/scanner/bootstrap.ts
8616
- import { existsSync as existsSync4, writeFileSync as writeFileSync2, readdirSync } from "node:fs";
8855
+ import { existsSync as existsSync4, writeFileSync as writeFileSync2, readdirSync as readdirSync2 } from "node:fs";
8617
8856
  import { join as join9 } from "node:path";
8618
8857
  import { execFile as execFile3 } from "node:child_process";
8619
8858
  function detectProjectType(workspaceRoot) {
@@ -8630,12 +8869,13 @@ function detectProjectType(workspaceRoot) {
8630
8869
  }
8631
8870
  if (has("Directory.Build.props")) return "csharp";
8632
8871
  try {
8633
- const entries = readdirSync(workspaceRoot);
8872
+ const entries = readdirSync2(workspaceRoot);
8634
8873
  if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
8635
8874
  return "csharp";
8636
8875
  }
8637
8876
  } catch {
8638
8877
  }
8878
+ if (has("pubspec.yaml")) return "dart";
8639
8879
  return "unknown";
8640
8880
  }
8641
8881
  function generateEslintConfig(isTypeScript) {
@@ -8677,7 +8917,7 @@ export default [
8677
8917
  `;
8678
8918
  }
8679
8919
  function npmInstall(workspaceRoot, packages) {
8680
- return new Promise((resolve6) => {
8920
+ return new Promise((resolve7) => {
8681
8921
  execFile3(
8682
8922
  "npm",
8683
8923
  ["install", "--save-dev", ...packages],
@@ -8688,14 +8928,14 @@ function npmInstall(workspaceRoot, packages) {
8688
8928
  },
8689
8929
  (err, stdout, stderr) => {
8690
8930
  if (err) {
8691
- resolve6({
8931
+ resolve7({
8692
8932
  action: `npm install --save-dev ${packages.join(" ")}`,
8693
8933
  success: false,
8694
8934
  detail: stderr || err.message
8695
8935
  });
8696
8936
  return;
8697
8937
  }
8698
- resolve6({
8938
+ resolve7({
8699
8939
  action: `npm install --save-dev ${packages.join(" ")}`,
8700
8940
  success: true,
8701
8941
  detail: `installed ${packages.join(", ")}`
@@ -8750,6 +8990,12 @@ function getRecommendation(projectType) {
8750
8990
  canAutoInstall: false,
8751
8991
  installInstructions: "brew install semgrep (or: pip install semgrep, pipx install semgrep)"
8752
8992
  };
8993
+ case "dart":
8994
+ return {
8995
+ scanner: "dart_analyze",
8996
+ canAutoInstall: false,
8997
+ installInstructions: "Install the Dart SDK: https://dart.dev/get-dart (or Flutter SDK which includes Dart)"
8998
+ };
8753
8999
  case "unknown":
8754
9000
  return {
8755
9001
  scanner: "semgrep",
@@ -8896,23 +9142,7 @@ function buildResult(projectType, steps, autoScanResult, recommendation) {
8896
9142
 
8897
9143
  // src/scanner/complexity-scanner.ts
8898
9144
  import { promises as fs7 } from "node:fs";
8899
- import { join as join10, relative as relative2 } from "node:path";
8900
- var SKIP_DIRS3 = /* @__PURE__ */ new Set([
8901
- "node_modules",
8902
- ".git",
8903
- "dist",
8904
- "build",
8905
- "out",
8906
- "target",
8907
- ".venv",
8908
- "venv",
8909
- "__pycache__",
8910
- ".cache",
8911
- ".next",
8912
- ".nuxt",
8913
- ".claude-crap",
8914
- ".codesight"
8915
- ]);
9145
+ import { join as join10, relative as relative3 } from "node:path";
8916
9146
  var MAX_FILES = 2e4;
8917
9147
  var RULE_ID = "complexity/cyclomatic-max";
8918
9148
  var SOURCE_TOOL = "complexity";
@@ -8920,7 +9150,8 @@ async function scanComplexity(workspaceRoot, engine, sarifStore, config, logger2
8920
9150
  const start = Date.now();
8921
9151
  const threshold = config.cyclomaticMax;
8922
9152
  const errorThreshold = threshold * 2;
8923
- const files = await collectSourceFiles(workspaceRoot);
9153
+ const filter = createExclusionFilter(config.exclude);
9154
+ const files = await collectSourceFiles(workspaceRoot, filter);
8924
9155
  logger2.info(
8925
9156
  { fileCount: files.length, threshold },
8926
9157
  "complexity-scanner: starting analysis"
@@ -8939,7 +9170,7 @@ async function scanComplexity(workspaceRoot, engine, sarifStore, config, logger2
8939
9170
  for (const fn of metrics.functions) {
8940
9171
  if (fn.cyclomaticComplexity <= threshold) continue;
8941
9172
  const level = fn.cyclomaticComplexity >= errorThreshold ? "error" : "warning";
8942
- const relPath = relative2(workspaceRoot, filePath);
9173
+ const relPath = relative3(workspaceRoot, filePath);
8943
9174
  sarifResults.push({
8944
9175
  ruleId: RULE_ID,
8945
9176
  level,
@@ -8990,7 +9221,7 @@ async function scanComplexity(workspaceRoot, engine, sarifStore, config, logger2
8990
9221
  );
8991
9222
  return { filesScanned, functionsAnalyzed, violations, durationMs };
8992
9223
  }
8993
- async function collectSourceFiles(workspaceRoot) {
9224
+ async function collectSourceFiles(workspaceRoot, filter) {
8994
9225
  const files = [];
8995
9226
  let truncated = false;
8996
9227
  async function walk2(dir) {
@@ -9003,15 +9234,16 @@ async function collectSourceFiles(workspaceRoot) {
9003
9234
  }
9004
9235
  for (const entry of entries) {
9005
9236
  if (truncated) return;
9006
- if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
9007
9237
  const full = join10(dir, entry.name);
9008
9238
  if (entry.isDirectory()) {
9009
- if (SKIP_DIRS3.has(entry.name)) continue;
9239
+ if (filter.shouldSkipDir(entry.name)) continue;
9010
9240
  await walk2(full);
9011
9241
  continue;
9012
9242
  }
9013
9243
  if (!entry.isFile()) continue;
9014
9244
  if (!detectLanguageFromPath(entry.name)) continue;
9245
+ const relPath = relative3(workspaceRoot, full);
9246
+ if (filter.shouldSkipFile(relPath, entry.name)) continue;
9015
9247
  files.push(full);
9016
9248
  if (files.length >= MAX_FILES) {
9017
9249
  truncated = true;
@@ -9038,10 +9270,18 @@ function ingestScannerRun(scanner, rawOutput, sarifStore) {
9038
9270
  async function autoScan(workspaceRoot, sarifStore, logger2, options) {
9039
9271
  const start = Date.now();
9040
9272
  const detected = await detectScanners(workspaceRoot);
9273
+ const monorepoDetected = await detectMonorepoScanners(workspaceRoot);
9274
+ const rootScannerSet = new Set(detected.filter((d) => d.available).map((d) => d.scanner));
9275
+ for (const md of monorepoDetected) {
9276
+ if (!rootScannerSet.has(md.scanner)) {
9277
+ detected.push(md);
9278
+ }
9279
+ }
9041
9280
  const available = detected.filter((d) => d.available);
9042
9281
  logger2.info(
9043
9282
  {
9044
9283
  detected: detected.map((d) => `${d.scanner}:${d.available}`),
9284
+ monorepo: monorepoDetected.length,
9045
9285
  available: available.length
9046
9286
  },
9047
9287
  "auto-scan: detection complete"
@@ -9096,7 +9336,7 @@ async function autoScan(workspaceRoot, sarifStore, logger2, options) {
9096
9336
  };
9097
9337
  }
9098
9338
  const runResults = await Promise.allSettled(
9099
- available.map((d) => runScanner(d.scanner, workspaceRoot))
9339
+ available.map((d) => runScanner(d.scanner, workspaceRoot, d.workingDir ? { workingDir: d.workingDir } : void 0))
9100
9340
  );
9101
9341
  const results = [];
9102
9342
  let totalFindings = 0;
@@ -9177,7 +9417,7 @@ async function autoScan(workspaceRoot, sarifStore, logger2, options) {
9177
9417
  workspaceRoot,
9178
9418
  options.engine,
9179
9419
  sarifStore,
9180
- { cyclomaticMax: options.cyclomaticMax ?? 15 },
9420
+ { cyclomaticMax: options.cyclomaticMax ?? 15, ...options.exclude ? { exclude: options.exclude } : {} },
9181
9421
  logger2
9182
9422
  );
9183
9423
  totalFindings += complexityScan.violations;
@@ -9313,7 +9553,7 @@ var ingestScannerOutputSchema = {
9313
9553
  properties: {
9314
9554
  scanner: {
9315
9555
  type: "string",
9316
- enum: ["semgrep", "eslint", "bandit", "stryker"],
9556
+ enum: ["semgrep", "eslint", "bandit", "stryker", "dart_analyze"],
9317
9557
  description: "Identifier of the producing scanner."
9318
9558
  },
9319
9559
  rawOutput: {
@@ -9373,6 +9613,15 @@ async function main() {
9373
9613
  { config: { ...config, pluginRoot: "<redacted>" } },
9374
9614
  "claude-crap MCP server starting"
9375
9615
  );
9616
+ let userExclusions = [];
9617
+ try {
9618
+ const crapConfig = loadCrapConfig({ workspaceRoot: config.pluginRoot });
9619
+ userExclusions = crapConfig.exclude;
9620
+ if (userExclusions.length > 0) {
9621
+ logger.info({ exclude: userExclusions }, "user exclusions loaded from .claude-crap.json");
9622
+ }
9623
+ } catch {
9624
+ }
9376
9625
  const astEngine = new TreeSitterEngine();
9377
9626
  const sarifStore = new SarifStore({
9378
9627
  workspaceRoot: config.pluginRoot,
@@ -9388,9 +9637,10 @@ async function main() {
9388
9637
  dashboard = await startDashboard({
9389
9638
  config,
9390
9639
  sarifStore,
9391
- workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot),
9640
+ workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot, { exclude: userExclusions }),
9392
9641
  logger,
9393
- astEngine
9642
+ astEngine,
9643
+ exclude: userExclusions
9394
9644
  });
9395
9645
  } catch (err) {
9396
9646
  logger.warn(
@@ -9560,7 +9810,7 @@ async function main() {
9560
9810
  const typed = args ?? {};
9561
9811
  const format = typed.format ?? "both";
9562
9812
  try {
9563
- const workspace = await estimateWorkspaceLoc(config.pluginRoot);
9813
+ const workspace = await estimateWorkspaceLoc(config.pluginRoot, { exclude: userExclusions });
9564
9814
  const score = computeProjectScore({
9565
9815
  workspaceRoot: config.pluginRoot,
9566
9816
  minutesPerLoc: config.minutesPerLoc,
@@ -9783,7 +10033,8 @@ async function main() {
9783
10033
  try {
9784
10034
  const result = await autoScan(config.pluginRoot, sarifStore, logger, {
9785
10035
  engine: astEngine,
9786
- cyclomaticMax: config.cyclomaticMax
10036
+ cyclomaticMax: config.cyclomaticMax,
10037
+ exclude: userExclusions
9787
10038
  });
9788
10039
  const markdown = renderAutoScanMarkdown(result);
9789
10040
  return {
@@ -9862,7 +10113,8 @@ async function main() {
9862
10113
  logger.info("claude-crap MCP server ready (stdio)");
9863
10114
  autoScan(config.pluginRoot, sarifStore, logger, {
9864
10115
  engine: astEngine,
9865
- cyclomaticMax: config.cyclomaticMax
10116
+ cyclomaticMax: config.cyclomaticMax,
10117
+ exclude: userExclusions
9866
10118
  }).then((result) => {
9867
10119
  const scanners = result.results.filter((r) => r.success).map((r) => r.scanner);
9868
10120
  logger.info(