claude-crap 0.3.7 → 0.4.0

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 (100) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +74 -7
  3. package/dist/adapters/common.d.ts +1 -1
  4. package/dist/adapters/common.d.ts.map +1 -1
  5. package/dist/adapters/common.js +1 -1
  6. package/dist/adapters/common.js.map +1 -1
  7. package/dist/adapters/dart-analyzer.d.ts +41 -0
  8. package/dist/adapters/dart-analyzer.d.ts.map +1 -0
  9. package/dist/adapters/dart-analyzer.js +120 -0
  10. package/dist/adapters/dart-analyzer.js.map +1 -0
  11. package/dist/adapters/dotnet-format.d.ts +35 -0
  12. package/dist/adapters/dotnet-format.d.ts.map +1 -0
  13. package/dist/adapters/dotnet-format.js +96 -0
  14. package/dist/adapters/dotnet-format.js.map +1 -0
  15. package/dist/adapters/index.d.ts +2 -0
  16. package/dist/adapters/index.d.ts.map +1 -1
  17. package/dist/adapters/index.js +8 -0
  18. package/dist/adapters/index.js.map +1 -1
  19. package/dist/crap-config.d.ts +4 -0
  20. package/dist/crap-config.d.ts.map +1 -1
  21. package/dist/crap-config.js +51 -28
  22. package/dist/crap-config.js.map +1 -1
  23. package/dist/dashboard/file-detail.d.ts.map +1 -1
  24. package/dist/dashboard/file-detail.js.map +1 -1
  25. package/dist/dashboard/server.d.ts +2 -0
  26. package/dist/dashboard/server.d.ts.map +1 -1
  27. package/dist/dashboard/server.js +7 -12
  28. package/dist/dashboard/server.js.map +1 -1
  29. package/dist/index.js +89 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/metrics/workspace-walker.d.ts +4 -1
  32. package/dist/metrics/workspace-walker.d.ts.map +1 -1
  33. package/dist/metrics/workspace-walker.js +12 -28
  34. package/dist/metrics/workspace-walker.js.map +1 -1
  35. package/dist/monorepo/project-map.d.ts +112 -0
  36. package/dist/monorepo/project-map.d.ts.map +1 -0
  37. package/dist/monorepo/project-map.js +384 -0
  38. package/dist/monorepo/project-map.js.map +1 -0
  39. package/dist/scanner/auto-scan.d.ts +1 -0
  40. package/dist/scanner/auto-scan.d.ts.map +1 -1
  41. package/dist/scanner/auto-scan.js +14 -5
  42. package/dist/scanner/auto-scan.js.map +1 -1
  43. package/dist/scanner/bootstrap.d.ts +1 -1
  44. package/dist/scanner/bootstrap.d.ts.map +1 -1
  45. package/dist/scanner/bootstrap.js +15 -1
  46. package/dist/scanner/bootstrap.js.map +1 -1
  47. package/dist/scanner/complexity-scanner.d.ts +2 -0
  48. package/dist/scanner/complexity-scanner.d.ts.map +1 -1
  49. package/dist/scanner/complexity-scanner.js +11 -26
  50. package/dist/scanner/complexity-scanner.js.map +1 -1
  51. package/dist/scanner/detector.d.ts +24 -4
  52. package/dist/scanner/detector.d.ts.map +1 -1
  53. package/dist/scanner/detector.js +110 -10
  54. package/dist/scanner/detector.js.map +1 -1
  55. package/dist/scanner/runner.d.ts +4 -1
  56. package/dist/scanner/runner.d.ts.map +1 -1
  57. package/dist/scanner/runner.js +25 -3
  58. package/dist/scanner/runner.js.map +1 -1
  59. package/dist/schemas/tool-schemas.d.ts +16 -1
  60. package/dist/schemas/tool-schemas.d.ts.map +1 -1
  61. package/dist/schemas/tool-schemas.js +16 -1
  62. package/dist/schemas/tool-schemas.js.map +1 -1
  63. package/dist/shared/exclusions.d.ts +53 -0
  64. package/dist/shared/exclusions.d.ts.map +1 -0
  65. package/dist/shared/exclusions.js +126 -0
  66. package/dist/shared/exclusions.js.map +1 -0
  67. package/package.json +3 -1
  68. package/plugin/.claude-plugin/plugin.json +1 -1
  69. package/plugin/CLAUDE.md +37 -0
  70. package/plugin/bundle/mcp-server.mjs +762 -144
  71. package/plugin/bundle/mcp-server.mjs.map +4 -4
  72. package/plugin/package-lock.json +15 -2
  73. package/plugin/package.json +2 -1
  74. package/scripts/bundle-plugin.mjs +2 -1
  75. package/src/adapters/common.ts +1 -1
  76. package/src/adapters/dart-analyzer.ts +161 -0
  77. package/src/adapters/dotnet-format.ts +125 -0
  78. package/src/adapters/index.ts +8 -0
  79. package/src/crap-config.ts +78 -18
  80. package/src/dashboard/file-detail.ts +0 -2
  81. package/src/dashboard/server.ts +9 -10
  82. package/src/index.ts +103 -5
  83. package/src/metrics/workspace-walker.ts +15 -27
  84. package/src/monorepo/project-map.ts +476 -0
  85. package/src/scanner/auto-scan.ts +17 -6
  86. package/src/scanner/bootstrap.ts +18 -1
  87. package/src/scanner/complexity-scanner.ts +15 -26
  88. package/src/scanner/detector.ts +119 -10
  89. package/src/scanner/runner.ts +25 -2
  90. package/src/schemas/tool-schemas.ts +17 -1
  91. package/src/shared/exclusions.ts +156 -0
  92. package/src/tests/adapters/dispatch.test.ts +2 -2
  93. package/src/tests/auto-scan.test.ts +2 -2
  94. package/src/tests/boot-monorepo.test.ts +804 -0
  95. package/src/tests/boot-scanner-detection.test.ts +692 -0
  96. package/src/tests/boot-single-project.test.ts +780 -0
  97. package/src/tests/exclusions.test.ts +117 -0
  98. package/src/tests/integration/mcp-server.integration.test.ts +2 -1
  99. package/src/tests/project-map.test.ts +302 -0
  100. package/src/tests/scanner-detector.test.ts +31 -11
@@ -17,8 +17,8 @@
17
17
  * @module scanner/detector
18
18
  */
19
19
 
20
- import { existsSync, readFileSync } from "node:fs";
21
- import { join } from "node:path";
20
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
21
+ import { join, resolve } from "node:path";
22
22
  import { execFile } from "node:child_process";
23
23
  import type { KnownScanner } from "../adapters/common.js";
24
24
 
@@ -36,6 +36,8 @@ export interface ScannerDetection {
36
36
  reason: string;
37
37
  /** Path to the config file that triggered detection, if any. */
38
38
  configPath?: string;
39
+ /** Working directory to run the scanner from (defaults to workspace root). */
40
+ workingDir?: string;
39
41
  }
40
42
 
41
43
  // ── Detection signals ──────────────────────────────────────────────
@@ -98,6 +100,19 @@ const SCANNER_SIGNALS: Record<KnownScanner, ScannerSignals> = {
98
100
  packageJsonKeys: ["@stryker-mutator/core"],
99
101
  binaryNames: ["stryker"],
100
102
  },
103
+ dart_analyze: {
104
+ configFiles: [
105
+ "analysis_options.yaml",
106
+ "pubspec.yaml",
107
+ ],
108
+ packageJsonKeys: [],
109
+ binaryNames: ["dart"],
110
+ },
111
+ dotnet_format: {
112
+ configFiles: [],
113
+ packageJsonKeys: [],
114
+ binaryNames: ["dotnet"],
115
+ },
101
116
  };
102
117
 
103
118
  // ── Probes ──────────────────────────────────────────────────────────
@@ -163,9 +178,9 @@ function probeBinary(binaryName: string): Promise<boolean> {
163
178
  // ── Public API ──────────────────────────────────────────────────────
164
179
 
165
180
  /**
166
- * Detect which of the four supported scanners are available in the
167
- * given workspace. Probes config files, package.json, and binary
168
- * availability in order, short-circuiting on first match.
181
+ * Detect which supported scanners are available in the given workspace.
182
+ * Probes config files, package.json, and binary availability in order,
183
+ * short-circuiting on first match.
169
184
  *
170
185
  * @param workspaceRoot Absolute path to the project root.
171
186
  * @returns One {@link ScannerDetection} per known scanner.
@@ -173,7 +188,7 @@ function probeBinary(binaryName: string): Promise<boolean> {
173
188
  export async function detectScanners(
174
189
  workspaceRoot: string,
175
190
  ): Promise<ScannerDetection[]> {
176
- const scanners: KnownScanner[] = ["eslint", "semgrep", "bandit", "stryker"];
191
+ const scanners: KnownScanner[] = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze", "dotnet_format"];
177
192
 
178
193
  const results = await Promise.all(
179
194
  scanners.map(async (scanner): Promise<ScannerDetection> => {
@@ -188,12 +203,18 @@ export async function detectScanners(
188
203
  };
189
204
  }
190
205
 
191
- // 2. Package.json probe
206
+ // 2. Package.json probe — declared in deps/devDeps, but is it
207
+ // actually installed? Check node_modules/.bin/ for the binary.
192
208
  if (probePackageJson(workspaceRoot, scanner)) {
209
+ const binName = SCANNER_SIGNALS[scanner].binaryNames[0];
210
+ const binPath = binName ? join(workspaceRoot, "node_modules", ".bin", binName) : null;
211
+ const installed = binPath !== null && existsSync(binPath);
193
212
  return {
194
213
  scanner,
195
- available: true,
196
- reason: `found in package.json dependencies`,
214
+ available: installed,
215
+ reason: installed
216
+ ? "found in package.json and installed"
217
+ : `found in package.json but not installed (run \`npm install\`)`,
197
218
  };
198
219
  }
199
220
 
@@ -220,5 +241,93 @@ export async function detectScanners(
220
241
  return results;
221
242
  }
222
243
 
244
+ // ── Monorepo subdirectory probing ────────────────────────────────
245
+
246
+ /**
247
+ * Common monorepo directory names that may contain workspace
248
+ * subdirectories. Checked one level deep only.
249
+ */
250
+ const MONOREPO_DIRS = ["apps", "packages", "libs", "modules", "services"];
251
+
252
+ /**
253
+ * Detect scanners in monorepo subdirectories. Probes first-level
254
+ * children of common monorepo directories (apps/, packages/, etc.)
255
+ * and npm workspaces for scanner config files. Returns detections
256
+ * with a `workingDir` pointing to the subdirectory.
257
+ *
258
+ * This catches e.g. `apps/mobile/pubspec.yaml` in a polyglot monorepo
259
+ * where the root-level detector only finds ESLint.
260
+ *
261
+ * @param workspaceRoot Absolute path to the project root.
262
+ * @returns Additional detections from subdirectories (may be empty).
263
+ */
264
+ export async function detectMonorepoScanners(
265
+ workspaceRoot: string,
266
+ ): Promise<ScannerDetection[]> {
267
+ const subdirs = new Set<string>();
268
+
269
+ // 1. Read npm workspaces from package.json
270
+ try {
271
+ const pkgPath = join(workspaceRoot, "package.json");
272
+ const raw = readFileSync(pkgPath, "utf-8");
273
+ const pkg = JSON.parse(raw) as Record<string, unknown>;
274
+ if (Array.isArray(pkg.workspaces)) {
275
+ for (const ws of pkg.workspaces) {
276
+ if (typeof ws === "string" && !ws.includes("*")) {
277
+ const full = resolve(workspaceRoot, ws);
278
+ if (existsSync(full)) subdirs.add(full);
279
+ }
280
+ }
281
+ }
282
+ } catch {
283
+ // No package.json or not parseable — continue
284
+ }
285
+
286
+ // 2. Scan common monorepo directories one level deep
287
+ for (const dir of MONOREPO_DIRS) {
288
+ const full = join(workspaceRoot, dir);
289
+ try {
290
+ const entries = readdirSync(full, { withFileTypes: true });
291
+ for (const entry of entries) {
292
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
293
+ subdirs.add(join(full, entry.name));
294
+ }
295
+ }
296
+ } catch {
297
+ // Directory doesn't exist — skip
298
+ }
299
+ }
300
+
301
+ if (subdirs.size === 0) return [];
302
+
303
+ // 3. Probe each subdirectory for scanner config files
304
+ const detections: ScannerDetection[] = [];
305
+ const scanners: KnownScanner[] = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze", "dotnet_format"];
306
+
307
+ for (const subdir of subdirs) {
308
+ for (const scanner of scanners) {
309
+ const configProbe = probeConfigFiles(subdir, scanner);
310
+ if (!configProbe.found) continue;
311
+
312
+ // For dart_analyze, also verify the binary is on PATH
313
+ if (scanner === "dart_analyze") {
314
+ const hasBinary = await probeBinary("dart");
315
+ if (!hasBinary) continue;
316
+ }
317
+
318
+ const relDir = subdir.replace(workspaceRoot + "/", "");
319
+ detections.push({
320
+ scanner,
321
+ available: true,
322
+ reason: `config file found in ${relDir}/`,
323
+ ...(configProbe.path ? { configPath: configProbe.path } : {}),
324
+ workingDir: subdir,
325
+ });
326
+ }
327
+ }
328
+
329
+ return detections;
330
+ }
331
+
223
332
  // Exported for testing
224
- export { SCANNER_SIGNALS };
333
+ export { SCANNER_SIGNALS, MONOREPO_DIRS };
@@ -91,6 +91,26 @@ function getScannerCommand(
91
91
  nonZeroIsNormal: false,
92
92
  outputFile: join(workspaceRoot, "reports", "mutation", "mutation.json"),
93
93
  };
94
+ case "dart_analyze":
95
+ return {
96
+ command: "dart",
97
+ args: ["analyze", "--format=json", "."],
98
+ timeoutMs: 120_000,
99
+ nonZeroIsNormal: true, // exits 3 when findings exist
100
+ };
101
+ case "dotnet_format":
102
+ return {
103
+ command: "dotnet",
104
+ args: [
105
+ "format",
106
+ "--verify-no-changes",
107
+ "--report",
108
+ join(workspaceRoot, ".claude-crap", "dotnet-report.json"),
109
+ ],
110
+ timeoutMs: 120_000,
111
+ nonZeroIsNormal: true,
112
+ outputFile: join(workspaceRoot, ".claude-crap", "dotnet-report.json"),
113
+ };
94
114
  }
95
115
  }
96
116
 
@@ -101,21 +121,24 @@ function getScannerCommand(
101
121
  *
102
122
  * @param scanner Which scanner to run.
103
123
  * @param workspaceRoot Absolute path to the project root (used as cwd).
124
+ * @param options Optional overrides.
104
125
  * @returns A {@link ScannerRunResult} with stdout or file output.
105
126
  */
106
127
  export function runScanner(
107
128
  scanner: KnownScanner,
108
129
  workspaceRoot: string,
130
+ options?: { workingDir?: string },
109
131
  ): Promise<ScannerRunResult> {
110
132
  const start = Date.now();
111
- const cmd = getScannerCommand(scanner, workspaceRoot);
133
+ const cwd = options?.workingDir ?? workspaceRoot;
134
+ const cmd = getScannerCommand(scanner, cwd);
112
135
 
113
136
  return new Promise((resolve) => {
114
137
  execFile(
115
138
  cmd.command,
116
139
  cmd.args,
117
140
  {
118
- cwd: workspaceRoot,
141
+ cwd,
119
142
  timeout: cmd.timeoutMs,
120
143
  maxBuffer: 50 * 1024 * 1024, // 50 MB — large codebases produce verbose output
121
144
  env: { ...process.env, FORCE_COLOR: "0" }, // suppress ANSI in output
@@ -134,11 +134,27 @@ export const scoreProjectSchema = {
134
134
  description:
135
135
  "Output format. `markdown` returns only the chat summary, `json` returns only the structured snapshot, `both` (default) returns both as separate content blocks.",
136
136
  },
137
+ scope: {
138
+ type: "string",
139
+ description: "Optional project name from the project map. When provided, the score is computed only for files within that project's subtree. Omit to score the entire workspace.",
140
+ },
137
141
  },
138
142
  required: [],
139
143
  additionalProperties: false,
140
144
  } as const;
141
145
 
146
+ /**
147
+ * Schema for the `list_projects` tool. Returns the discovered sub-projects
148
+ * in the workspace, or an empty list for single-project workspaces.
149
+ */
150
+ export const listProjectsSchema = {
151
+ type: "object",
152
+ description: "List all discovered sub-projects in the workspace. In a monorepo, returns each sub-project with its type, path, and recommended scanner. In a single-project workspace, returns an empty list.",
153
+ properties: {},
154
+ required: [],
155
+ additionalProperties: false,
156
+ } as const;
157
+
142
158
  /**
143
159
  * Schema for the `require_test_harness` tool. Checks whether a production
144
160
  * source file has an accompanying test file in any of the conventional
@@ -181,7 +197,7 @@ export const ingestScannerOutputSchema = {
181
197
  properties: {
182
198
  scanner: {
183
199
  type: "string",
184
- enum: ["semgrep", "eslint", "bandit", "stryker"],
200
+ enum: ["semgrep", "eslint", "bandit", "stryker", "dart_analyze", "dotnet_format"],
185
201
  description: "Identifier of the producing scanner.",
186
202
  },
187
203
  rawOutput: {
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Centralized file and directory exclusion system.
3
+ *
4
+ * Every filesystem walker in the codebase (workspace-walker,
5
+ * complexity-scanner, dashboard file-detail) imports from this module
6
+ * instead of maintaining its own `SKIP_DIRS` constant. This
7
+ * guarantees all subsystems agree on what to exclude.
8
+ *
9
+ * User-configurable exclusions from `.claude-crap.json` are layered
10
+ * on top of the defaults via {@link createExclusionFilter}.
11
+ *
12
+ * @module shared/exclusions
13
+ */
14
+
15
+ import picomatch from "picomatch";
16
+
17
+ // ── Default exclusions ──────────────────────────────────────────
18
+
19
+ /**
20
+ * Directories excluded by name at any depth. A walker that encounters
21
+ * a directory entry whose name is in this set should skip the entire
22
+ * subtree. The set covers package managers, VCS, build outputs for
23
+ * all major frameworks, language-specific caches, and plugin state.
24
+ */
25
+ export const DEFAULT_SKIP_DIRS: ReadonlySet<string> = new Set([
26
+ // Package managers / vendored deps
27
+ "node_modules",
28
+ "vendor",
29
+
30
+ // Version control
31
+ ".git",
32
+
33
+ // Build outputs (general)
34
+ "dist",
35
+ "build",
36
+ "bundle",
37
+ "out",
38
+ "target",
39
+ "coverage",
40
+
41
+ // Framework build outputs
42
+ ".next", // Next.js
43
+ ".nuxt", // Nuxt 2
44
+ ".output", // Nuxt 3
45
+ ".vercel", // Vercel
46
+ ".svelte-kit", // SvelteKit
47
+ ".astro", // Astro
48
+ ".angular", // Angular
49
+ ".turbo", // Turborepo
50
+ ".parcel-cache",// Parcel
51
+ ".expo", // Expo / React Native
52
+
53
+ // Language-specific caches
54
+ ".venv",
55
+ "venv",
56
+ "__pycache__",
57
+ ".cache",
58
+ ".dart_tool", // Dart / Flutter
59
+ ".gradle", // Gradle
60
+
61
+ // IDE state
62
+ ".idea",
63
+
64
+ // Plugin state
65
+ ".claude-crap",
66
+ ".codesight",
67
+ ]);
68
+
69
+ /**
70
+ * Filename-level glob patterns that match generated or minified files
71
+ * regardless of which directory they live in. Matched against the
72
+ * bare filename (not the full path).
73
+ */
74
+ export const DEFAULT_SKIP_PATTERNS: ReadonlyArray<string> = [
75
+ "*.min.js",
76
+ "*.min.css",
77
+ "*.min.mjs",
78
+ "*.min.cjs",
79
+ "*.bundle.js",
80
+ "*.chunk.js",
81
+ ];
82
+
83
+ // ── Exclusion filter ────────────────────────────────────────────
84
+
85
+ /**
86
+ * Stateless, pre-compiled filter that every filesystem walker uses
87
+ * to decide whether to skip a directory or file.
88
+ */
89
+ export interface ExclusionFilter {
90
+ /** Returns `true` when the directory should be skipped entirely. */
91
+ shouldSkipDir(dirName: string): boolean;
92
+ /** Returns `true` when the file should be excluded from analysis. */
93
+ shouldSkipFile(relativePath: string, fileName: string): boolean;
94
+ }
95
+
96
+ /**
97
+ * Create an {@link ExclusionFilter} that combines the built-in
98
+ * defaults with optional user-defined patterns from `.claude-crap.json`.
99
+ *
100
+ * User patterns follow `.gitignore`-style conventions:
101
+ * - `apps/legacy/` → trailing `/` means directory exclusion
102
+ * - `*.proto.ts` → glob matched against workspace-relative path
103
+ * - `src/generated/**` → path-prefix glob
104
+ *
105
+ * Picomatch matchers are compiled once at construction, so per-file
106
+ * checks are O(1) set lookups plus O(n) matcher calls where n is
107
+ * the small number of user patterns (typically < 20).
108
+ *
109
+ * @param userExclusions Optional patterns from `.claude-crap.json`.
110
+ */
111
+ export function createExclusionFilter(
112
+ userExclusions?: ReadonlyArray<string>,
113
+ ): ExclusionFilter {
114
+ // Split user patterns into directory exclusions and file globs
115
+ const extraDirs = new Set<string>();
116
+ const fileGlobs: string[] = [];
117
+
118
+ for (const pattern of userExclusions ?? []) {
119
+ if (pattern.endsWith("/")) {
120
+ // Directory exclusion — strip trailing slash
121
+ extraDirs.add(pattern.slice(0, -1));
122
+ } else {
123
+ fileGlobs.push(pattern);
124
+ }
125
+ }
126
+
127
+ // Compile filename-level matchers once
128
+ const defaultFileMatchers = DEFAULT_SKIP_PATTERNS.map((p) =>
129
+ picomatch(p, { dot: true }),
130
+ );
131
+ const userFileMatchers = fileGlobs.map((p) =>
132
+ picomatch(p, { dot: true }),
133
+ );
134
+
135
+ return {
136
+ shouldSkipDir(dirName: string): boolean {
137
+ // Hidden directories are always skipped except .claude-plugin
138
+ if (dirName.startsWith(".") && dirName !== ".claude-plugin") {
139
+ return DEFAULT_SKIP_DIRS.has(dirName) || true;
140
+ }
141
+ return DEFAULT_SKIP_DIRS.has(dirName) || extraDirs.has(dirName);
142
+ },
143
+
144
+ shouldSkipFile(relativePath: string, fileName: string): boolean {
145
+ // Check filename against default minified/bundled patterns
146
+ for (const matcher of defaultFileMatchers) {
147
+ if (matcher(fileName)) return true;
148
+ }
149
+ // Check against user-defined globs (matched on relative path)
150
+ for (const matcher of userFileMatchers) {
151
+ if (matcher(relativePath) || matcher(fileName)) return true;
152
+ }
153
+ return false;
154
+ },
155
+ };
156
+ }
@@ -94,7 +94,7 @@ describe("adaptScannerOutput", () => {
94
94
  assert.throws(() => adaptScannerOutput(scanner, {}));
95
95
  });
96
96
 
97
- it("KNOWN_SCANNERS is frozen and contains exactly the four supported names", () => {
98
- assert.deepEqual([...KNOWN_SCANNERS].sort(), ["bandit", "eslint", "semgrep", "stryker"]);
97
+ it("KNOWN_SCANNERS is frozen and contains all supported names", () => {
98
+ assert.deepEqual([...KNOWN_SCANNERS].sort(), ["bandit", "dart_analyze", "dotnet_format", "eslint", "semgrep", "stryker"]);
99
99
  });
100
100
  });
@@ -34,7 +34,7 @@ describe("autoScan", () => {
34
34
  outputDir: join(dir, ".claude-crap/reports"),
35
35
  });
36
36
  const result = await autoScan(dir, store, logger);
37
- assert.equal(result.detected.length, 4);
37
+ assert.equal(result.detected.length, 6);
38
38
  assert.ok(result.totalDurationMs >= 0);
39
39
  // No scanners available means no results
40
40
  // (unless the host has scanner binaries installed)
@@ -129,7 +129,7 @@ describe("autoScan", () => {
129
129
  const result = await autoScan(dir, store, logger);
130
130
 
131
131
  const scannerNames = result.detected.map((d) => d.scanner).sort();
132
- assert.deepEqual(scannerNames, ["bandit", "eslint", "semgrep", "stryker"]);
132
+ assert.deepEqual(scannerNames, ["bandit", "dart_analyze", "dotnet_format", "eslint", "semgrep", "stryker"]);
133
133
  } finally {
134
134
  rmSync(dir, { recursive: true, force: true });
135
135
  }