@shakecodeslikecray/whiterose 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,2794 @@
1
+ import { z } from 'zod';
2
+ import { existsSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
3
+ import { join, dirname, resolve, extname, relative, basename, isAbsolute } from 'path';
4
+ import YAML from 'yaml';
5
+ import fg3 from 'fast-glob';
6
+ import { createHash } from 'crypto';
7
+ import { execa } from 'execa';
8
+ import { homedir, tmpdir } from 'os';
9
+
10
+ // src/types.ts
11
+ var BugSeverity = z.enum(["critical", "high", "medium", "low"]);
12
+ var BugCategory = z.enum([
13
+ "logic-error",
14
+ "security",
15
+ "async-race-condition",
16
+ "edge-case",
17
+ "null-reference",
18
+ "type-coercion",
19
+ "resource-leak",
20
+ "intent-violation"
21
+ ]);
22
+ var ConfidenceLevel = z.enum(["high", "medium", "low"]);
23
+ var ConfidenceScore = z.object({
24
+ overall: ConfidenceLevel,
25
+ codePathValidity: z.number().min(0).max(1),
26
+ reachability: z.number().min(0).max(1),
27
+ intentViolation: z.boolean(),
28
+ staticToolSignal: z.boolean(),
29
+ adversarialSurvived: z.boolean()
30
+ });
31
+ var CodePathStep = z.object({
32
+ step: z.number(),
33
+ file: z.string(),
34
+ line: z.number(),
35
+ code: z.string(),
36
+ explanation: z.string()
37
+ });
38
+ var Bug = z.object({
39
+ id: z.string(),
40
+ title: z.string(),
41
+ description: z.string(),
42
+ file: z.string(),
43
+ line: z.number(),
44
+ endLine: z.number().optional(),
45
+ severity: BugSeverity,
46
+ category: BugCategory,
47
+ confidence: ConfidenceScore,
48
+ codePath: z.array(CodePathStep),
49
+ evidence: z.array(z.string()),
50
+ suggestedFix: z.string().optional(),
51
+ relatedContract: z.string().optional(),
52
+ staticAnalysisSignals: z.array(z.string()).optional(),
53
+ createdAt: z.string().datetime()
54
+ });
55
+ var ProviderType = z.enum([
56
+ "claude-code",
57
+ "aider",
58
+ "codex",
59
+ "opencode",
60
+ "ollama",
61
+ "gemini"
62
+ ]);
63
+ var PriorityLevel = z.enum(["critical", "high", "medium", "low", "ignore"]);
64
+ var PackageConfig = z.object({
65
+ path: z.string(),
66
+ priority: PriorityLevel,
67
+ include: z.array(z.string()).optional(),
68
+ exclude: z.array(z.string()).optional()
69
+ });
70
+ var MonorepoConfig = z.object({
71
+ detection: z.enum(["auto", "explicit"]),
72
+ packages: z.array(PackageConfig).optional(),
73
+ crossPackageAnalysis: z.boolean().default(true)
74
+ });
75
+ var WhiteroseConfig = z.object({
76
+ version: z.string().default("1"),
77
+ provider: ProviderType.default("claude-code"),
78
+ providerFallback: z.array(ProviderType).optional(),
79
+ // Scan settings
80
+ include: z.array(z.string()).default(["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]),
81
+ exclude: z.array(z.string()).default(["node_modules", "dist", "build", ".next", "coverage"]),
82
+ // Priority areas
83
+ priorities: z.record(z.string(), PriorityLevel).default({}),
84
+ // Bug categories to scan for
85
+ categories: z.array(BugCategory).default([
86
+ "logic-error",
87
+ "security",
88
+ "async-race-condition",
89
+ "edge-case",
90
+ "null-reference"
91
+ ]),
92
+ // Confidence threshold for reporting
93
+ minConfidence: ConfidenceLevel.default("low"),
94
+ // Monorepo settings
95
+ monorepo: MonorepoConfig.optional(),
96
+ // Static analysis integration
97
+ staticAnalysis: z.object({
98
+ typescript: z.boolean().default(true),
99
+ eslint: z.boolean().default(true)
100
+ }).default({}),
101
+ // Output settings
102
+ output: z.object({
103
+ sarif: z.boolean().default(true),
104
+ markdown: z.boolean().default(true),
105
+ sarifPath: z.string().default(".whiterose/reports"),
106
+ markdownPath: z.string().default("BUGS.md")
107
+ }).default({})
108
+ });
109
+ var BehavioralContract = z.object({
110
+ function: z.string(),
111
+ file: z.string(),
112
+ inputs: z.array(
113
+ z.object({
114
+ name: z.string(),
115
+ type: z.string(),
116
+ constraints: z.string().optional()
117
+ })
118
+ ),
119
+ outputs: z.object({
120
+ type: z.string(),
121
+ constraints: z.string().optional()
122
+ }),
123
+ invariants: z.array(z.string()),
124
+ sideEffects: z.array(z.string()),
125
+ throws: z.array(z.string()).optional()
126
+ });
127
+ var FeatureIntent = z.object({
128
+ name: z.string(),
129
+ description: z.string(),
130
+ priority: PriorityLevel,
131
+ constraints: z.array(z.string()),
132
+ relatedFiles: z.array(z.string())
133
+ });
134
+ var CodebaseUnderstanding = z.object({
135
+ version: z.string(),
136
+ generatedAt: z.string().datetime(),
137
+ summary: z.object({
138
+ framework: z.string().optional(),
139
+ language: z.string(),
140
+ type: z.string(),
141
+ // e-commerce, saas, api, etc.
142
+ description: z.string()
143
+ }),
144
+ features: z.array(FeatureIntent),
145
+ contracts: z.array(BehavioralContract),
146
+ dependencies: z.record(z.string(), z.string()),
147
+ structure: z.object({
148
+ totalFiles: z.number(),
149
+ totalLines: z.number(),
150
+ packages: z.array(z.string()).optional()
151
+ })
152
+ });
153
+ var FileHash = z.object({
154
+ path: z.string(),
155
+ hash: z.string(),
156
+ lastModified: z.string().datetime()
157
+ });
158
+ var CacheState = z.object({
159
+ version: z.string(),
160
+ lastFullScan: z.string().datetime().optional(),
161
+ lastIncrementalScan: z.string().datetime().optional(),
162
+ fileHashes: z.array(FileHash)
163
+ });
164
+ var ScanResult = z.object({
165
+ id: z.string(),
166
+ timestamp: z.string().datetime(),
167
+ scanType: z.enum(["full", "incremental"]),
168
+ filesScanned: z.number(),
169
+ filesChanged: z.number().optional(),
170
+ duration: z.number(),
171
+ // ms
172
+ bugs: z.array(Bug),
173
+ summary: z.object({
174
+ critical: z.number(),
175
+ high: z.number(),
176
+ medium: z.number(),
177
+ low: z.number(),
178
+ total: z.number()
179
+ })
180
+ });
181
+ async function loadConfig(cwd) {
182
+ const configPath = join(cwd, ".whiterose", "config.yml");
183
+ if (!existsSync(configPath)) {
184
+ throw new Error('Config file not found. Run "whiterose init" first.');
185
+ }
186
+ const content = readFileSync(configPath, "utf-8");
187
+ const parsed = YAML.parse(content);
188
+ return WhiteroseConfig.parse(parsed);
189
+ }
190
+ async function loadUnderstanding(cwd) {
191
+ const understandingPath = join(cwd, ".whiterose", "cache", "understanding.json");
192
+ if (!existsSync(understandingPath)) {
193
+ return null;
194
+ }
195
+ const content = readFileSync(understandingPath, "utf-8");
196
+ return JSON.parse(content);
197
+ }
198
+ async function saveConfig(cwd, config) {
199
+ const { writeFileSync: writeFileSync4 } = await import('fs');
200
+ const configPath = join(cwd, ".whiterose", "config.yml");
201
+ writeFileSync4(configPath, YAML.stringify(config), "utf-8");
202
+ }
203
+ async function buildDependencyGraph(cwd, files) {
204
+ const graph = {
205
+ files: /* @__PURE__ */ new Map(),
206
+ dependents: /* @__PURE__ */ new Map()
207
+ };
208
+ for (const file of files) {
209
+ const imports = await getFileImports(file);
210
+ const resolvedImports = /* @__PURE__ */ new Set();
211
+ for (const imp of imports) {
212
+ const resolved = resolveImport(file, imp.source, cwd);
213
+ if (resolved && files.includes(resolved)) {
214
+ resolvedImports.add(resolved);
215
+ }
216
+ }
217
+ graph.files.set(file, resolvedImports);
218
+ for (const imported of resolvedImports) {
219
+ if (!graph.dependents.has(imported)) {
220
+ graph.dependents.set(imported, /* @__PURE__ */ new Set());
221
+ }
222
+ graph.dependents.get(imported).add(file);
223
+ }
224
+ }
225
+ return graph;
226
+ }
227
+ async function getDependentFiles(changedFiles, cwd, allFiles) {
228
+ if (!allFiles) {
229
+ allFiles = await fg3(["**/*.{ts,tsx,js,jsx}"], {
230
+ cwd,
231
+ ignore: ["node_modules/**", "dist/**", "build/**", ".next/**"],
232
+ absolute: true
233
+ });
234
+ }
235
+ const graph = await buildDependencyGraph(cwd, allFiles);
236
+ const dependents = new Set(changedFiles);
237
+ const queue = [...changedFiles];
238
+ while (queue.length > 0) {
239
+ const file = queue.shift();
240
+ const fileDependents = graph.dependents.get(file);
241
+ if (fileDependents) {
242
+ for (const dep of fileDependents) {
243
+ if (!dependents.has(dep)) {
244
+ dependents.add(dep);
245
+ queue.push(dep);
246
+ }
247
+ }
248
+ }
249
+ }
250
+ return Array.from(dependents);
251
+ }
252
+ async function getFileImports(filePath) {
253
+ if (!existsSync(filePath)) {
254
+ return [];
255
+ }
256
+ const content = readFileSync(filePath, "utf-8");
257
+ const imports = [];
258
+ const importRegex = /import\s+(?:(type)\s+)?(?:(\*\s+as\s+\w+)|(\{[^}]+\})|(\w+)(?:\s*,\s*(\{[^}]+\}))?|)\s*(?:from\s+)?['"]([^'"]+)['"]/g;
259
+ let match;
260
+ while ((match = importRegex.exec(content)) !== null) {
261
+ const isTypeOnly = !!match[1];
262
+ const isNamespace = !!match[2];
263
+ const namedImports = match[3] || match[5] || "";
264
+ const defaultImport = match[4] || "";
265
+ const source = match[6];
266
+ const specifiers = [];
267
+ if (defaultImport) {
268
+ specifiers.push(defaultImport);
269
+ }
270
+ if (namedImports) {
271
+ const named = namedImports.replace(/[{}]/g, "").split(",").map((s) => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
272
+ specifiers.push(...named);
273
+ }
274
+ imports.push({
275
+ source,
276
+ specifiers,
277
+ isDefault: !!defaultImport,
278
+ isNamespace,
279
+ isTypeOnly
280
+ });
281
+ }
282
+ const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
283
+ while ((match = requireRegex.exec(content)) !== null) {
284
+ imports.push({
285
+ source: match[1],
286
+ specifiers: [],
287
+ isDefault: false,
288
+ isNamespace: false,
289
+ isTypeOnly: false
290
+ });
291
+ }
292
+ const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
293
+ while ((match = dynamicImportRegex.exec(content)) !== null) {
294
+ imports.push({
295
+ source: match[1],
296
+ specifiers: [],
297
+ isDefault: false,
298
+ isNamespace: false,
299
+ isTypeOnly: false
300
+ });
301
+ }
302
+ return imports;
303
+ }
304
+ function resolveImport(fromFile, importPath, cwd) {
305
+ if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
306
+ return null;
307
+ }
308
+ const fromDir = dirname(fromFile);
309
+ let resolved;
310
+ if (importPath.startsWith("/")) {
311
+ resolved = join(cwd, importPath);
312
+ } else {
313
+ resolved = resolve(fromDir, importPath);
314
+ }
315
+ const extensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
316
+ if (existsSync(resolved)) {
317
+ const ext = extname(resolved);
318
+ if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") {
319
+ return resolved;
320
+ }
321
+ for (const ext2 of ["/index.ts", "/index.tsx", "/index.js", "/index.jsx"]) {
322
+ if (existsSync(resolved + ext2)) {
323
+ return resolved + ext2;
324
+ }
325
+ }
326
+ }
327
+ for (const ext of extensions) {
328
+ const withExt = resolved + ext;
329
+ if (existsSync(withExt)) {
330
+ return withExt;
331
+ }
332
+ }
333
+ if (importPath.endsWith(".js")) {
334
+ const tsPath = resolved.replace(/\.js$/, ".ts");
335
+ if (existsSync(tsPath)) {
336
+ return tsPath;
337
+ }
338
+ const tsxPath = resolved.replace(/\.js$/, ".tsx");
339
+ if (existsSync(tsxPath)) {
340
+ return tsxPath;
341
+ }
342
+ }
343
+ return null;
344
+ }
345
+ async function getImportsOf(filePath) {
346
+ const imports = await getFileImports(filePath);
347
+ return imports.map((i) => i.source);
348
+ }
349
+ async function dependsOn(fileA, fileB, cwd, allFiles) {
350
+ const graph = await buildDependencyGraph(cwd, allFiles);
351
+ const visited = /* @__PURE__ */ new Set();
352
+ const queue = [fileA];
353
+ while (queue.length > 0) {
354
+ const current = queue.shift();
355
+ if (visited.has(current)) continue;
356
+ visited.add(current);
357
+ const deps = graph.files.get(current);
358
+ if (!deps) continue;
359
+ if (deps.has(fileB)) {
360
+ return true;
361
+ }
362
+ for (const dep of deps) {
363
+ if (!visited.has(dep)) {
364
+ queue.push(dep);
365
+ }
366
+ }
367
+ }
368
+ return false;
369
+ }
370
+ async function findCircularDependencies(cwd, allFiles) {
371
+ const graph = await buildDependencyGraph(cwd, allFiles);
372
+ const circles = [];
373
+ for (const file of allFiles) {
374
+ const path = findCycle(file, graph, []);
375
+ if (path && !circles.some((c) => arraysEqual(c, path))) {
376
+ circles.push(path);
377
+ }
378
+ }
379
+ return circles;
380
+ }
381
+ function findCycle(file, graph, path) {
382
+ const index = path.indexOf(file);
383
+ if (index !== -1) {
384
+ return path.slice(index);
385
+ }
386
+ const deps = graph.files.get(file);
387
+ if (!deps) return null;
388
+ for (const dep of deps) {
389
+ const cycle = findCycle(dep, graph, [...path, file]);
390
+ if (cycle) return cycle;
391
+ }
392
+ return null;
393
+ }
394
+ function arraysEqual(a, b) {
395
+ if (a.length !== b.length) return false;
396
+ const sortedA = [...a].sort();
397
+ const sortedB = [...b].sort();
398
+ return sortedA.every((v, i) => v === sortedB[i]);
399
+ }
400
+
401
+ // src/core/scanner/index.ts
402
+ var DEFAULT_INCLUDE = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"];
403
+ var DEFAULT_EXCLUDE = [
404
+ "node_modules/**",
405
+ "dist/**",
406
+ "build/**",
407
+ ".next/**",
408
+ "coverage/**",
409
+ "**/*.test.*",
410
+ "**/*.spec.*",
411
+ "**/*.d.ts",
412
+ ".whiterose/**"
413
+ ];
414
+ async function scanCodebase(cwd, config) {
415
+ const include = config?.include || DEFAULT_INCLUDE;
416
+ const exclude = config?.exclude || DEFAULT_EXCLUDE;
417
+ const files = await fg3(include, {
418
+ cwd,
419
+ ignore: exclude,
420
+ absolute: true,
421
+ onlyFiles: true
422
+ });
423
+ return files.sort();
424
+ }
425
+ function hashFile(filePath) {
426
+ const content = readFileSync(filePath, "utf-8");
427
+ return createHash("md5").update(content).digest("hex");
428
+ }
429
+ async function getChangedFiles(cwd, config) {
430
+ const cachePath = join(cwd, ".whiterose", "cache", "file-hashes.json");
431
+ const currentFiles = await scanCodebase(cwd, config);
432
+ let cachedState = {
433
+ fileHashes: []
434
+ };
435
+ if (existsSync(cachePath)) {
436
+ cachedState = JSON.parse(readFileSync(cachePath, "utf-8"));
437
+ }
438
+ const cachedHashes = new Map(cachedState.fileHashes.map((h) => [h.path, h.hash]));
439
+ const changedFiles = [];
440
+ const newHashes = [];
441
+ for (const file of currentFiles) {
442
+ const relativePath = relative(cwd, file);
443
+ const currentHash = hashFile(file);
444
+ newHashes.push({
445
+ path: relativePath,
446
+ hash: currentHash,
447
+ lastModified: (/* @__PURE__ */ new Date()).toISOString()
448
+ });
449
+ const cachedHash = cachedHashes.get(relativePath);
450
+ if (!cachedHash || cachedHash !== currentHash) {
451
+ changedFiles.push(file);
452
+ }
453
+ }
454
+ const newState = {
455
+ version: "1",
456
+ lastIncrementalScan: (/* @__PURE__ */ new Date()).toISOString(),
457
+ lastFullScan: cachedState.lastFullScan,
458
+ fileHashes: newHashes
459
+ };
460
+ writeFileSync(cachePath, JSON.stringify(newState, null, 2), "utf-8");
461
+ return { files: changedFiles, hashes: newHashes };
462
+ }
463
+ async function getDependentFiles2(changedFiles, cwd, allFiles) {
464
+ return getDependentFiles(changedFiles, cwd, allFiles);
465
+ }
466
+ var providerChecks = [
467
+ {
468
+ name: "claude-code",
469
+ command: "claude",
470
+ args: ["--version"],
471
+ paths: [
472
+ join(homedir(), ".local", "bin", "claude"),
473
+ "/usr/local/bin/claude",
474
+ "/opt/homebrew/bin/claude"
475
+ ]
476
+ },
477
+ {
478
+ name: "aider",
479
+ command: "aider",
480
+ args: ["--version"],
481
+ paths: [
482
+ join(homedir(), ".local", "bin", "aider"),
483
+ "/usr/local/bin/aider",
484
+ "/opt/homebrew/bin/aider"
485
+ ]
486
+ },
487
+ {
488
+ name: "codex",
489
+ command: "codex",
490
+ args: ["--version"],
491
+ paths: [
492
+ join(homedir(), ".local", "bin", "codex"),
493
+ "/usr/local/bin/codex",
494
+ "/opt/homebrew/bin/codex"
495
+ ]
496
+ },
497
+ {
498
+ name: "opencode",
499
+ command: "opencode",
500
+ args: ["--version"],
501
+ paths: [
502
+ join(homedir(), ".local", "bin", "opencode"),
503
+ "/usr/local/bin/opencode",
504
+ "/opt/homebrew/bin/opencode"
505
+ ]
506
+ },
507
+ {
508
+ name: "gemini",
509
+ command: "gemini",
510
+ args: ["--version"],
511
+ paths: [
512
+ join(homedir(), ".local", "bin", "gemini"),
513
+ "/usr/local/bin/gemini",
514
+ "/opt/homebrew/bin/gemini"
515
+ ]
516
+ },
517
+ {
518
+ name: "ollama",
519
+ command: "ollama",
520
+ args: ["--version"],
521
+ paths: [
522
+ join(homedir(), ".local", "bin", "ollama"),
523
+ "/usr/local/bin/ollama",
524
+ "/opt/homebrew/bin/ollama"
525
+ ]
526
+ }
527
+ ];
528
+ var resolvedPaths = /* @__PURE__ */ new Map();
529
+ async function findCommand(check) {
530
+ try {
531
+ await execa(check.command, check.args, { timeout: 5e3 });
532
+ return check.command;
533
+ } catch {
534
+ }
535
+ if (check.paths) {
536
+ for (const path of check.paths) {
537
+ if (existsSync(path)) {
538
+ try {
539
+ await execa(path, check.args, { timeout: 5e3 });
540
+ return path;
541
+ } catch {
542
+ }
543
+ }
544
+ }
545
+ }
546
+ return null;
547
+ }
548
+ async function detectProvider() {
549
+ const available = [];
550
+ for (const check of providerChecks) {
551
+ const commandPath = await findCommand(check);
552
+ if (commandPath) {
553
+ resolvedPaths.set(check.name, commandPath);
554
+ available.push(check.name);
555
+ }
556
+ }
557
+ return available;
558
+ }
559
+ async function isProviderAvailable(name) {
560
+ const check = providerChecks.find((c) => c.name === name);
561
+ if (!check) return false;
562
+ const commandPath = await findCommand(check);
563
+ if (commandPath) {
564
+ resolvedPaths.set(name, commandPath);
565
+ return true;
566
+ }
567
+ return false;
568
+ }
569
+ function getProviderCommand(name) {
570
+ return resolvedPaths.get(name) || name.replace("-code", "");
571
+ }
572
+
573
+ // src/core/utils.ts
574
+ function generateBugId(index) {
575
+ return `WR-${String(index + 1).padStart(3, "0")}`;
576
+ }
577
+ function safeParseJson(json, schema) {
578
+ try {
579
+ const parsed = JSON.parse(json);
580
+ const result = schema.safeParse(parsed);
581
+ if (result.success) {
582
+ return { success: true, data: result.data };
583
+ }
584
+ return { success: false, error: formatZodError(result.error) };
585
+ } catch (error) {
586
+ return { success: false, error: error.message || "Invalid JSON" };
587
+ }
588
+ }
589
+ function formatZodError(error) {
590
+ return error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
591
+ }
592
+ var PartialBugFromLLM = z.object({
593
+ file: z.string(),
594
+ line: z.number(),
595
+ endLine: z.number().optional(),
596
+ title: z.string(),
597
+ description: z.string().optional().default(""),
598
+ severity: BugSeverity.optional().default("medium"),
599
+ category: BugCategory.optional().default("logic-error"),
600
+ codePath: z.array(z.object({
601
+ step: z.number().optional(),
602
+ file: z.string().optional(),
603
+ line: z.number().optional(),
604
+ code: z.string().optional().default(""),
605
+ explanation: z.string().optional().default("")
606
+ })).optional().default([]),
607
+ evidence: z.array(z.string()).optional().default([]),
608
+ suggestedFix: z.string().optional(),
609
+ confidence: z.object({
610
+ overall: ConfidenceLevel.optional().default("medium"),
611
+ codePathValidity: z.number().min(0).max(1).optional().default(0.5),
612
+ reachability: z.number().min(0).max(1).optional().default(0.5),
613
+ intentViolation: z.boolean().optional().default(false),
614
+ staticToolSignal: z.boolean().optional().default(false),
615
+ adversarialSurvived: z.boolean().optional().default(false)
616
+ }).optional().default({})
617
+ });
618
+ var PartialUnderstandingFromLLM = z.object({
619
+ summary: z.object({
620
+ type: z.string().optional().default("unknown"),
621
+ framework: z.string().optional(),
622
+ language: z.string().optional().default("unknown"),
623
+ description: z.string().optional().default("")
624
+ }).optional().default({}),
625
+ features: z.array(z.object({
626
+ name: z.string(),
627
+ description: z.string(),
628
+ priority: z.enum(["critical", "high", "medium", "low", "ignore"]).optional().default("medium"),
629
+ constraints: z.array(z.string()).optional().default([]),
630
+ relatedFiles: z.array(z.string()).optional().default([])
631
+ })).optional().default([]),
632
+ contracts: z.array(z.object({
633
+ function: z.string(),
634
+ file: z.string(),
635
+ inputs: z.array(z.object({
636
+ name: z.string(),
637
+ type: z.string(),
638
+ constraints: z.string().optional()
639
+ })).optional().default([]),
640
+ outputs: z.object({
641
+ type: z.string(),
642
+ constraints: z.string().optional()
643
+ }).optional().default({ type: "unknown" }),
644
+ invariants: z.array(z.string()).optional().default([]),
645
+ sideEffects: z.array(z.string()).optional().default([]),
646
+ throws: z.array(z.string()).optional()
647
+ })).optional().default([]),
648
+ dependencies: z.record(z.string(), z.string()).optional().default({})
649
+ });
650
+ var AdversarialResultSchema = z.object({
651
+ survived: z.boolean(),
652
+ counterArguments: z.array(z.string()).optional().default([]),
653
+ confidence: ConfidenceLevel.optional()
654
+ });
655
+ z.object({
656
+ $schema: z.string().optional(),
657
+ version: z.string(),
658
+ runs: z.array(z.object({
659
+ tool: z.object({
660
+ driver: z.object({
661
+ name: z.string(),
662
+ version: z.string().optional(),
663
+ informationUri: z.string().optional(),
664
+ rules: z.array(z.any()).optional()
665
+ })
666
+ }),
667
+ results: z.array(z.object({
668
+ ruleId: z.string().optional(),
669
+ level: z.enum(["none", "note", "warning", "error"]).optional(),
670
+ message: z.object({
671
+ text: z.string()
672
+ }),
673
+ locations: z.array(z.object({
674
+ physicalLocation: z.object({
675
+ artifactLocation: z.object({
676
+ uri: z.string()
677
+ }),
678
+ region: z.object({
679
+ startLine: z.number(),
680
+ endLine: z.number().optional()
681
+ }).optional()
682
+ })
683
+ })).optional()
684
+ })).optional().default([])
685
+ }))
686
+ });
687
+ z.object({
688
+ number: z.number(),
689
+ title: z.string(),
690
+ body: z.string().nullable(),
691
+ state: z.string(),
692
+ labels: z.array(z.object({
693
+ name: z.string()
694
+ })).optional().default([]),
695
+ url: z.string().optional()
696
+ });
697
+ z.array(z.object({
698
+ filePath: z.string(),
699
+ messages: z.array(z.object({
700
+ ruleId: z.string().nullable(),
701
+ severity: z.number(),
702
+ message: z.string(),
703
+ line: z.number(),
704
+ column: z.number()
705
+ })),
706
+ errorCount: z.number(),
707
+ warningCount: z.number()
708
+ }));
709
+ z.object({
710
+ name: z.string().optional(),
711
+ version: z.string().optional(),
712
+ description: z.string().optional(),
713
+ scripts: z.record(z.string(), z.string()).optional(),
714
+ dependencies: z.record(z.string(), z.string()).optional(),
715
+ devDependencies: z.record(z.string(), z.string()).optional(),
716
+ workspaces: z.union([
717
+ z.array(z.string()),
718
+ z.object({ packages: z.array(z.string()) })
719
+ ]).optional()
720
+ });
721
+
722
+ // src/providers/adapters/claude-code.ts
723
+ var MARKERS = {
724
+ SCANNING: "###SCANNING:",
725
+ BUG: "###BUG:",
726
+ UNDERSTANDING: "###UNDERSTANDING:",
727
+ COMPLETE: "###COMPLETE",
728
+ ERROR: "###ERROR:"
729
+ };
730
+ var ClaudeCodeProvider = class {
731
+ name = "claude-code";
732
+ progressCallback;
733
+ bugFoundCallback;
734
+ currentProcess;
735
+ unsafeMode = false;
736
+ async detect() {
737
+ return isProviderAvailable("claude-code");
738
+ }
739
+ async isAvailable() {
740
+ return isProviderAvailable("claude-code");
741
+ }
742
+ /**
743
+ * Enable unsafe mode (--dangerously-skip-permissions).
744
+ * WARNING: This bypasses Claude's permission prompts and should only be used
745
+ * when you trust the codebase being analyzed.
746
+ */
747
+ setUnsafeMode(enabled) {
748
+ this.unsafeMode = enabled;
749
+ }
750
+ isUnsafeMode() {
751
+ return this.unsafeMode;
752
+ }
753
+ setProgressCallback(callback) {
754
+ this.progressCallback = callback;
755
+ }
756
+ setBugFoundCallback(callback) {
757
+ this.bugFoundCallback = callback;
758
+ }
759
+ reportProgress(message) {
760
+ if (this.progressCallback) {
761
+ this.progressCallback(message);
762
+ }
763
+ }
764
+ reportBug(bug) {
765
+ if (this.bugFoundCallback) {
766
+ this.bugFoundCallback(bug);
767
+ }
768
+ }
769
+ // Cancel any running analysis
770
+ cancel() {
771
+ if (this.currentProcess) {
772
+ this.currentProcess.kill();
773
+ this.currentProcess = void 0;
774
+ }
775
+ }
776
+ async analyze(context) {
777
+ const { files, understanding } = context;
778
+ if (files.length === 0) {
779
+ return [];
780
+ }
781
+ const cwd = process.cwd();
782
+ const bugs = [];
783
+ let bugIndex = 0;
784
+ const prompt = this.buildAgenticAnalysisPrompt(understanding);
785
+ this.reportProgress("Starting agentic analysis...");
786
+ try {
787
+ await this.runAgenticClaude(prompt, cwd, {
788
+ onScanning: (file) => {
789
+ this.reportProgress(`Scanning: ${file}`);
790
+ },
791
+ onBugFound: (bugData) => {
792
+ const bug = this.parseBugData(bugData, bugIndex++, files);
793
+ if (bug) {
794
+ bugs.push(bug);
795
+ this.reportBug(bug);
796
+ this.reportProgress(`Found: ${bug.title} (${bug.severity})`);
797
+ }
798
+ },
799
+ onComplete: () => {
800
+ this.reportProgress(`Analysis complete. Found ${bugs.length} bugs.`);
801
+ },
802
+ onError: (error) => {
803
+ this.reportProgress(`Error: ${error}`);
804
+ }
805
+ });
806
+ } catch (error) {
807
+ if (error.message?.includes("ENOENT")) {
808
+ throw new Error("Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code");
809
+ }
810
+ throw error;
811
+ }
812
+ return bugs;
813
+ }
814
+ async adversarialValidate(bug, _context) {
815
+ let fileContent = "";
816
+ try {
817
+ if (existsSync(bug.file)) {
818
+ fileContent = readFileSync(bug.file, "utf-8");
819
+ const lines = fileContent.split("\n");
820
+ const start = Math.max(0, bug.line - 20);
821
+ const end = Math.min(lines.length, (bug.endLine || bug.line) + 20);
822
+ fileContent = lines.slice(start, end).join("\n");
823
+ }
824
+ } catch {
825
+ }
826
+ const prompt = this.buildAdversarialPrompt(bug, fileContent);
827
+ const result = await this.runSimpleClaude(prompt, process.cwd());
828
+ return this.parseAdversarialResponse(result, bug);
829
+ }
830
+ async generateUnderstanding(files, existingDocsSummary) {
831
+ const cwd = process.cwd();
832
+ this.reportProgress(`Starting codebase analysis (${files.length} files)...`);
833
+ const prompt = this.buildAgenticUnderstandingPrompt(existingDocsSummary);
834
+ let understandingJson = "";
835
+ try {
836
+ await this.runAgenticClaude(prompt, cwd, {
837
+ onScanning: (file) => {
838
+ this.reportProgress(`Examining: ${file}`);
839
+ },
840
+ onUnderstanding: (json) => {
841
+ understandingJson = json;
842
+ },
843
+ onComplete: () => {
844
+ this.reportProgress("Understanding complete.");
845
+ },
846
+ onError: (error) => {
847
+ this.reportProgress(`Error: ${error}`);
848
+ }
849
+ });
850
+ return this.parseUnderstandingResponse(understandingJson, files);
851
+ } catch (error) {
852
+ if (error.message?.includes("ENOENT")) {
853
+ throw new Error("Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code");
854
+ }
855
+ throw error;
856
+ }
857
+ }
858
+ // ─────────────────────────────────────────────────────────────
859
+ // Agentic Prompts
860
+ // ─────────────────────────────────────────────────────────────
861
+ buildAgenticAnalysisPrompt(understanding) {
862
+ return `You are whiterose, an expert bug hunter. Your task is to explore this codebase and find real bugs.
863
+
864
+ CODEBASE CONTEXT:
865
+ - Type: ${understanding.summary.type}
866
+ - Framework: ${understanding.summary.framework || "Unknown"}
867
+ - Description: ${understanding.summary.description}
868
+
869
+ YOUR TASK:
870
+ 1. Explore the codebase by reading files
871
+ 2. Look for bugs in these categories:
872
+ - Logic errors (off-by-one, wrong operators, incorrect conditions)
873
+ - Null/undefined dereference
874
+ - Security vulnerabilities (injection, auth bypass, XSS)
875
+ - Async/race conditions (missing await, unhandled promises)
876
+ - Edge cases (empty arrays, zero values, boundaries)
877
+ - Resource leaks (unclosed connections)
878
+
879
+ PROTOCOL - You MUST output these markers:
880
+ - Before reading each file, output: ${MARKERS.SCANNING}<filepath>
881
+ - When you find a bug, output: ${MARKERS.BUG}<json>
882
+ - When completely done, output: ${MARKERS.COMPLETE}
883
+ - If you encounter an error, output: ${MARKERS.ERROR}<message>
884
+
885
+ BUG JSON FORMAT:
886
+ ${MARKERS.BUG}{"file":"src/api/users.ts","line":42,"title":"Null dereference in getUserById","description":"...","severity":"high","category":"null-reference","evidence":["..."],"suggestedFix":"..."}
887
+
888
+ IMPORTANT:
889
+ - Only report bugs you have HIGH confidence in
890
+ - Include exact line numbers
891
+ - Focus on real bugs, not style issues
892
+ - Explore systematically - check API routes, data handling, auth flows
893
+
894
+ Now explore this codebase and find bugs. Start by reading the main entry points.`;
895
+ }
896
+ buildAgenticUnderstandingPrompt(existingDocsSummary) {
897
+ const docsSection = existingDocsSummary ? `
898
+
899
+ EXISTING DOCUMENTATION (merge this with your exploration):
900
+ ${existingDocsSummary}
901
+ ` : "";
902
+ return `You are whiterose. Your task is to understand this codebase.
903
+ ${docsSection}
904
+ YOUR TASK:
905
+ 1. Review the existing documentation above (if any)
906
+ 2. Explore the codebase structure to fill in gaps
907
+ 3. Read key files (main entry points, config files, core modules)
908
+ 4. Build a comprehensive understanding merging docs + code exploration
909
+ 5. Identify main features, business rules, and behavioral contracts
910
+
911
+ PROTOCOL - You MUST output these markers:
912
+ - Before reading each file, output: ${MARKERS.SCANNING}<filepath>
913
+ - When you have full understanding, output: ${MARKERS.UNDERSTANDING}<json>
914
+ - When completely done, output: ${MARKERS.COMPLETE}
915
+
916
+ UNDERSTANDING JSON FORMAT:
917
+ ${MARKERS.UNDERSTANDING}{
918
+ "summary": {
919
+ "type": "api|web-app|cli|library|etc",
920
+ "framework": "next.js|express|react|etc",
921
+ "language": "typescript|javascript",
922
+ "description": "2-3 sentence description"
923
+ },
924
+ "features": [
925
+ {"name": "Feature", "description": "What it does", "priority": "critical|high|medium|low", "constraints": ["business rule 1", "invariant 2"], "relatedFiles": ["path/to/file.ts"]}
926
+ ],
927
+ "contracts": [
928
+ {"function": "functionName", "file": "path/to/file.ts", "inputs": [], "outputs": {}, "invariants": ["must do X before Y"], "sideEffects": [], "throws": []}
929
+ ]
930
+ }
931
+
932
+ IMPORTANT:
933
+ - Merge existing documentation with what you discover in the code
934
+ - Focus on business rules and invariants (what MUST be true)
935
+ - Identify critical paths (checkout, auth, payments, etc.)
936
+ - Document behavioral contracts for important functions
937
+
938
+ Now explore this codebase and build understanding.`;
939
+ }
940
+ buildAdversarialPrompt(bug, fileContent) {
941
+ return `You are a skeptical code reviewer. Try to DISPROVE this bug report.
942
+
943
+ REPORTED BUG:
944
+ - File: ${bug.file}:${bug.line}
945
+ - Title: ${bug.title}
946
+ - Description: ${bug.description}
947
+ - Severity: ${bug.severity}
948
+
949
+ CODE CONTEXT:
950
+ ${fileContent}
951
+
952
+ Try to prove this is NOT a bug by finding:
953
+ 1. Guards or validation that prevents this
954
+ 2. Type system guarantees
955
+ 3. Framework behavior that handles this
956
+ 4. Unreachable code paths
957
+
958
+ OUTPUT AS JSON:
959
+ {"survived": true/false, "counterArguments": ["reason 1"], "confidence": "high/medium/low", "explanation": "..."}
960
+
961
+ Set "survived": true if you CANNOT disprove it (it's a real bug).`;
962
+ }
963
+ // ─────────────────────────────────────────────────────────────
964
+ // Claude CLI Execution (Agentic Mode)
965
+ // ─────────────────────────────────────────────────────────────
966
+ async runAgenticClaude(prompt, cwd, callbacks) {
967
+ const claudeCommand = getProviderCommand("claude-code");
968
+ const args = ["--verbose", "-p", prompt];
969
+ if (this.unsafeMode) {
970
+ args.unshift("--dangerously-skip-permissions");
971
+ }
972
+ this.currentProcess = execa(
973
+ claudeCommand,
974
+ args,
975
+ {
976
+ cwd,
977
+ env: {
978
+ ...process.env,
979
+ NO_COLOR: "1"
980
+ },
981
+ reject: false
982
+ }
983
+ );
984
+ let buffer = "";
985
+ this.currentProcess.stdout?.on("data", (chunk) => {
986
+ buffer += chunk.toString();
987
+ const lines = buffer.split("\n");
988
+ buffer = lines.pop() || "";
989
+ for (const line of lines) {
990
+ this.processAgentOutput(line, callbacks);
991
+ }
992
+ });
993
+ this.currentProcess.stderr?.on("data", (chunk) => {
994
+ const text = chunk.toString().trim();
995
+ if (text && !text.includes("Loading")) ;
996
+ });
997
+ await this.currentProcess;
998
+ if (buffer.trim()) {
999
+ this.processAgentOutput(buffer, callbacks);
1000
+ }
1001
+ this.currentProcess = void 0;
1002
+ }
1003
+ processAgentOutput(line, callbacks) {
1004
+ const trimmed = line.trim();
1005
+ if (trimmed.startsWith(MARKERS.SCANNING)) {
1006
+ const file = trimmed.slice(MARKERS.SCANNING.length).trim();
1007
+ callbacks.onScanning?.(file);
1008
+ } else if (trimmed.startsWith(MARKERS.BUG)) {
1009
+ const json = trimmed.slice(MARKERS.BUG.length).trim();
1010
+ callbacks.onBugFound?.(json);
1011
+ } else if (trimmed.startsWith(MARKERS.UNDERSTANDING)) {
1012
+ const json = trimmed.slice(MARKERS.UNDERSTANDING.length).trim();
1013
+ callbacks.onUnderstanding?.(json);
1014
+ } else if (trimmed.startsWith(MARKERS.COMPLETE)) {
1015
+ callbacks.onComplete?.();
1016
+ } else if (trimmed.startsWith(MARKERS.ERROR)) {
1017
+ const error = trimmed.slice(MARKERS.ERROR.length).trim();
1018
+ callbacks.onError?.(error);
1019
+ }
1020
+ }
1021
+ // Simple non-agentic mode for short prompts (adversarial validation)
1022
+ async runSimpleClaude(prompt, cwd) {
1023
+ const claudeCommand = getProviderCommand("claude-code");
1024
+ try {
1025
+ const { stdout } = await execa(
1026
+ claudeCommand,
1027
+ ["-p", prompt, "--output-format", "text"],
1028
+ {
1029
+ cwd,
1030
+ timeout: 12e4,
1031
+ // 2 min for simple prompts
1032
+ env: { ...process.env, NO_COLOR: "1" }
1033
+ }
1034
+ );
1035
+ return stdout;
1036
+ } catch (error) {
1037
+ if (error.stdout) return error.stdout;
1038
+ throw error;
1039
+ }
1040
+ }
1041
+ // ─────────────────────────────────────────────────────────────
1042
+ // Response Parsers
1043
+ // ─────────────────────────────────────────────────────────────
1044
+ parseBugData(json, index, files) {
1045
+ const result = safeParseJson(json, PartialBugFromLLM);
1046
+ if (!result.success) {
1047
+ return null;
1048
+ }
1049
+ const data = result.data;
1050
+ let filePath = data.file;
1051
+ if (!filePath.startsWith("/")) {
1052
+ const match = files.find((f) => f.endsWith(filePath) || f.includes(filePath));
1053
+ if (match) filePath = match;
1054
+ }
1055
+ return {
1056
+ id: generateBugId(index),
1057
+ title: String(data.title).slice(0, 100),
1058
+ description: String(data.description || ""),
1059
+ file: filePath,
1060
+ line: data.line,
1061
+ endLine: data.endLine,
1062
+ severity: data.severity ?? "medium",
1063
+ category: data.category ?? "logic-error",
1064
+ confidence: {
1065
+ overall: data.confidence?.overall || "medium",
1066
+ codePathValidity: data.confidence?.codePathValidity ?? 0.8,
1067
+ reachability: data.confidence?.reachability ?? 0.8,
1068
+ intentViolation: data.confidence?.intentViolation ?? false,
1069
+ staticToolSignal: data.confidence?.staticToolSignal ?? false,
1070
+ adversarialSurvived: data.confidence?.adversarialSurvived ?? false
1071
+ },
1072
+ codePath: (data.codePath || []).map((step, idx) => ({
1073
+ step: idx + 1,
1074
+ file: step.file || filePath,
1075
+ line: step.line || data.line,
1076
+ code: step.code || "",
1077
+ explanation: step.explanation || ""
1078
+ })),
1079
+ evidence: data.evidence || [],
1080
+ suggestedFix: data.suggestedFix,
1081
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1082
+ };
1083
+ }
1084
+ parseAdversarialResponse(response, bug) {
1085
+ const json = this.extractJson(response);
1086
+ if (!json) {
1087
+ return { survived: true, counterArguments: [] };
1088
+ }
1089
+ const result = safeParseJson(json, AdversarialResultSchema);
1090
+ if (!result.success) {
1091
+ return { survived: true, counterArguments: [] };
1092
+ }
1093
+ const parsed = result.data;
1094
+ const survived = parsed.survived !== false;
1095
+ return {
1096
+ survived,
1097
+ counterArguments: parsed.counterArguments || [],
1098
+ adjustedConfidence: survived ? {
1099
+ ...bug.confidence,
1100
+ overall: parsed.confidence || bug.confidence.overall,
1101
+ adversarialSurvived: true
1102
+ } : void 0
1103
+ };
1104
+ }
1105
+ parseUnderstandingResponse(response, files) {
1106
+ let totalLines = 0;
1107
+ for (const file of files.slice(0, 50)) {
1108
+ try {
1109
+ const content = readFileSync(file, "utf-8");
1110
+ totalLines += content.split("\n").length;
1111
+ } catch {
1112
+ }
1113
+ }
1114
+ const json = this.extractJson(response);
1115
+ if (!json) {
1116
+ return {
1117
+ version: "1",
1118
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1119
+ summary: {
1120
+ type: "unknown",
1121
+ language: "unknown",
1122
+ description: "Failed to analyze codebase: No JSON found in response"
1123
+ },
1124
+ features: [],
1125
+ contracts: [],
1126
+ dependencies: {},
1127
+ structure: { totalFiles: files.length, totalLines }
1128
+ };
1129
+ }
1130
+ const result = safeParseJson(json, PartialUnderstandingFromLLM);
1131
+ if (!result.success) {
1132
+ return {
1133
+ version: "1",
1134
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1135
+ summary: {
1136
+ type: "unknown",
1137
+ language: "unknown",
1138
+ description: `Failed to analyze codebase: ${result.error}`
1139
+ },
1140
+ features: [],
1141
+ contracts: [],
1142
+ dependencies: {},
1143
+ structure: { totalFiles: files.length, totalLines }
1144
+ };
1145
+ }
1146
+ const parsed = result.data;
1147
+ return {
1148
+ version: "1",
1149
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1150
+ summary: {
1151
+ type: parsed.summary?.type || "unknown",
1152
+ framework: parsed.summary?.framework || void 0,
1153
+ language: parsed.summary?.language || "typescript",
1154
+ description: parsed.summary?.description || "No description available"
1155
+ },
1156
+ features: (parsed.features || []).map((f) => ({
1157
+ name: f.name || "Unknown",
1158
+ description: f.description || "",
1159
+ priority: f.priority || "medium",
1160
+ constraints: f.constraints || [],
1161
+ relatedFiles: f.relatedFiles || []
1162
+ })),
1163
+ contracts: (parsed.contracts || []).map((c) => ({
1164
+ function: c.function || "unknown",
1165
+ file: c.file || "unknown",
1166
+ inputs: c.inputs || [],
1167
+ outputs: c.outputs || { type: "unknown" },
1168
+ invariants: c.invariants || [],
1169
+ sideEffects: c.sideEffects || [],
1170
+ throws: c.throws
1171
+ })),
1172
+ dependencies: {},
1173
+ structure: {
1174
+ totalFiles: files.length,
1175
+ totalLines
1176
+ }
1177
+ };
1178
+ }
1179
+ // ─────────────────────────────────────────────────────────────
1180
+ // Utilities
1181
+ // ─────────────────────────────────────────────────────────────
1182
+ extractJson(text) {
1183
+ const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
1184
+ if (codeBlockMatch) {
1185
+ return codeBlockMatch[1].trim();
1186
+ }
1187
+ const arrayMatch = text.match(/\[[\s\S]*\]/);
1188
+ if (arrayMatch) {
1189
+ return arrayMatch[0];
1190
+ }
1191
+ const objectMatch = text.match(/\{[\s\S]*\}/);
1192
+ if (objectMatch) {
1193
+ return objectMatch[0];
1194
+ }
1195
+ return null;
1196
+ }
1197
+ };
1198
+ var MAX_FILE_SIZE = 5e4;
1199
+ var MAX_TOTAL_CONTEXT = 2e5;
1200
+ var AIDER_TIMEOUT = 3e5;
1201
+ var AiderProvider = class {
1202
+ name = "aider";
1203
+ async detect() {
1204
+ return isProviderAvailable("aider");
1205
+ }
1206
+ async isAvailable() {
1207
+ return isProviderAvailable("aider");
1208
+ }
1209
+ async analyze(context) {
1210
+ const { files, understanding, staticAnalysisResults } = context;
1211
+ if (files.length === 0) {
1212
+ return [];
1213
+ }
1214
+ const fileContents = this.readFilesWithLimit(files, MAX_TOTAL_CONTEXT);
1215
+ const prompt = this.buildAnalysisPrompt(fileContents, understanding, staticAnalysisResults);
1216
+ const result = await this.runAider(prompt, files, dirname(files[0]));
1217
+ return this.parseAnalysisResponse(result, files);
1218
+ }
1219
+ async adversarialValidate(bug, _context) {
1220
+ let fileContent = "";
1221
+ try {
1222
+ if (existsSync(bug.file)) {
1223
+ fileContent = readFileSync(bug.file, "utf-8");
1224
+ const lines = fileContent.split("\n");
1225
+ const start = Math.max(0, bug.line - 20);
1226
+ const end = Math.min(lines.length, (bug.endLine || bug.line) + 20);
1227
+ fileContent = lines.slice(start, end).join("\n");
1228
+ }
1229
+ } catch {
1230
+ }
1231
+ const prompt = this.buildAdversarialPrompt(bug, fileContent);
1232
+ const result = await this.runAider(prompt, [bug.file], dirname(bug.file));
1233
+ return this.parseAdversarialResponse(result, bug);
1234
+ }
1235
+ async generateUnderstanding(files, _existingDocsSummary) {
1236
+ const sampledFiles = this.prioritizeFiles(files, 40);
1237
+ const fileContents = this.readFilesWithLimit(sampledFiles, MAX_TOTAL_CONTEXT);
1238
+ let packageJson = null;
1239
+ const packageJsonPath = files.find((f) => f.endsWith("package.json"));
1240
+ if (packageJsonPath) {
1241
+ try {
1242
+ packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1243
+ } catch {
1244
+ }
1245
+ }
1246
+ const prompt = this.buildUnderstandingPrompt(files.length, fileContents, packageJson);
1247
+ const result = await this.runAider(prompt, sampledFiles.slice(0, 5), process.cwd());
1248
+ return this.parseUnderstandingResponse(result, files);
1249
+ }
1250
+ // ─────────────────────────────────────────────────────────────
1251
+ // File Reading Helpers
1252
+ // ─────────────────────────────────────────────────────────────
1253
+ readFilesWithLimit(files, maxTotal) {
1254
+ const result = [];
1255
+ let totalSize = 0;
1256
+ for (const file of files) {
1257
+ if (totalSize >= maxTotal) break;
1258
+ try {
1259
+ if (!existsSync(file)) continue;
1260
+ let content = readFileSync(file, "utf-8");
1261
+ if (content.length > MAX_FILE_SIZE) {
1262
+ content = content.slice(0, MAX_FILE_SIZE) + "\n// ... truncated ...";
1263
+ }
1264
+ if (totalSize + content.length > maxTotal) {
1265
+ const remaining = maxTotal - totalSize;
1266
+ content = content.slice(0, remaining) + "\n// ... truncated ...";
1267
+ }
1268
+ result.push({ path: file, content });
1269
+ totalSize += content.length;
1270
+ } catch {
1271
+ }
1272
+ }
1273
+ return result;
1274
+ }
1275
+ prioritizeFiles(files, count) {
1276
+ if (files.length <= count) return files;
1277
+ const priorityPatterns = [
1278
+ { pattern: /package\.json$/, priority: 100 },
1279
+ { pattern: /tsconfig\.json$/, priority: 90 },
1280
+ { pattern: /README\.md$/i, priority: 80 },
1281
+ { pattern: /\/index\.(ts|js|tsx|jsx)$/, priority: 70 },
1282
+ { pattern: /\/app\.(ts|js|tsx|jsx)$/, priority: 70 },
1283
+ { pattern: /\/main\.(ts|js|tsx|jsx)$/, priority: 70 },
1284
+ { pattern: /\/api\//, priority: 60 },
1285
+ { pattern: /\/routes?\//, priority: 55 },
1286
+ { pattern: /\/pages\//, priority: 55 },
1287
+ { pattern: /\/services?\//, priority: 50 },
1288
+ { pattern: /\.(ts|tsx)$/, priority: 30 },
1289
+ { pattern: /\.(js|jsx)$/, priority: 20 }
1290
+ ];
1291
+ const scored = files.map((file) => {
1292
+ let score = 0;
1293
+ for (const { pattern, priority } of priorityPatterns) {
1294
+ if (pattern.test(file)) {
1295
+ score = Math.max(score, priority);
1296
+ }
1297
+ }
1298
+ return { file, score };
1299
+ });
1300
+ scored.sort((a, b) => b.score - a.score);
1301
+ return scored.slice(0, count).map((s) => s.file);
1302
+ }
1303
+ // ─────────────────────────────────────────────────────────────
1304
+ // Prompt Builders (similar to Claude Code but adapted for Aider)
1305
+ // ─────────────────────────────────────────────────────────────
1306
+ buildAnalysisPrompt(fileContents, understanding, staticResults) {
1307
+ const filesSection = fileContents.map((f) => `=== ${f.path} ===
1308
+ ${f.content}`).join("\n\n");
1309
+ const staticSignals = staticResults.length > 0 ? `
1310
+ Static analysis signals:
1311
+ ${staticResults.slice(0, 50).map((r) => `- ${r.file}:${r.line}: ${r.message}`).join("\n")}` : "";
1312
+ return `Analyze the following code for bugs. This is a ${understanding.summary.type} application using ${understanding.summary.framework || "no specific framework"}.
1313
+
1314
+ ${filesSection}
1315
+ ${staticSignals}
1316
+
1317
+ Find bugs in these categories:
1318
+ 1. Logic errors (off-by-one, wrong operators)
1319
+ 2. Null/undefined dereference
1320
+ 3. Security vulnerabilities
1321
+ 4. Async/race conditions
1322
+ 5. Edge cases not handled
1323
+
1324
+ Output as JSON array ONLY:
1325
+ [{"file": "path", "line": 42, "title": "Bug title", "description": "Description", "severity": "high", "category": "null-reference", "codePath": [{"step": 1, "file": "path", "line": 40, "code": "code", "explanation": "explanation"}], "evidence": ["evidence1"], "suggestedFix": "fix code"}]
1326
+
1327
+ If no bugs found, output: []`;
1328
+ }
1329
+ buildAdversarialPrompt(bug, fileContent) {
1330
+ return `Try to DISPROVE this bug report:
1331
+
1332
+ Bug: ${bug.title}
1333
+ File: ${bug.file}:${bug.line}
1334
+ Description: ${bug.description}
1335
+
1336
+ Code context:
1337
+ ${fileContent}
1338
+
1339
+ Find reasons this is NOT a bug (guards, type checks, etc).
1340
+
1341
+ Output JSON ONLY:
1342
+ {"survived": true/false, "counterArguments": ["reason1", "reason2"], "confidence": "high/medium/low"}`;
1343
+ }
1344
+ buildUnderstandingPrompt(totalFiles, fileContents, packageJson) {
1345
+ const filesSection = fileContents.map((f) => `=== ${f.path} ===
1346
+ ${f.content}`).join("\n\n");
1347
+ const depsSection = packageJson ? `
1348
+ Dependencies: ${JSON.stringify(packageJson.dependencies || {})}` : "";
1349
+ return `Analyze this codebase (${totalFiles} total files).
1350
+ ${depsSection}
1351
+
1352
+ ${filesSection}
1353
+
1354
+ Output JSON ONLY describing:
1355
+ {
1356
+ "summary": {"type": "app-type", "framework": "framework", "language": "typescript", "description": "description"},
1357
+ "features": [{"name": "Feature", "description": "desc", "priority": "critical", "constraints": ["constraint"], "relatedFiles": ["file"]}],
1358
+ "contracts": [{"function": "funcName", "file": "file.ts", "inputs": [{"name": "param", "type": "string"}], "outputs": {"type": "Result"}, "invariants": ["rule"], "sideEffects": ["effect"]}]
1359
+ }`;
1360
+ }
1361
+ // ─────────────────────────────────────────────────────────────
1362
+ // Aider CLI Execution
1363
+ // ─────────────────────────────────────────────────────────────
1364
+ async runAider(prompt, files, cwd) {
1365
+ const tempDir = mkdtempSync(join(tmpdir(), "whiterose-"));
1366
+ const promptFile = join(tempDir, "prompt.txt");
1367
+ try {
1368
+ writeFileSync(promptFile, prompt, "utf-8");
1369
+ const args = [
1370
+ "--no-auto-commits",
1371
+ "--no-git",
1372
+ "--yes",
1373
+ "--message-file",
1374
+ promptFile
1375
+ ];
1376
+ for (const file of files.slice(0, 10)) {
1377
+ if (existsSync(file)) {
1378
+ args.push(file);
1379
+ }
1380
+ }
1381
+ const aiderCommand = getProviderCommand("aider");
1382
+ const { stdout, stderr } = await execa(aiderCommand, args, {
1383
+ cwd,
1384
+ timeout: AIDER_TIMEOUT,
1385
+ env: {
1386
+ ...process.env,
1387
+ NO_COLOR: "1"
1388
+ },
1389
+ reject: false
1390
+ });
1391
+ return stdout || stderr || "";
1392
+ } catch (error) {
1393
+ if (error.stdout) {
1394
+ return error.stdout;
1395
+ }
1396
+ if (error.message?.includes("ENOENT")) {
1397
+ throw new Error("Aider not found. Install it with: pip install aider-chat");
1398
+ }
1399
+ throw new Error(`Aider failed: ${error.message}`);
1400
+ } finally {
1401
+ try {
1402
+ rmSync(tempDir, { recursive: true, force: true });
1403
+ } catch {
1404
+ }
1405
+ }
1406
+ }
1407
+ // ─────────────────────────────────────────────────────────────
1408
+ // Response Parsers
1409
+ // ─────────────────────────────────────────────────────────────
1410
+ parseAnalysisResponse(response, files) {
1411
+ try {
1412
+ const json = this.extractJson(response);
1413
+ if (!json) return [];
1414
+ const parsed = JSON.parse(json);
1415
+ if (!Array.isArray(parsed)) return [];
1416
+ const bugs = [];
1417
+ for (let i = 0; i < parsed.length; i++) {
1418
+ const item = parsed[i];
1419
+ if (!item.file || !item.line || !item.title) continue;
1420
+ let filePath = item.file;
1421
+ if (!filePath.startsWith("/")) {
1422
+ const match = files.find((f) => f.endsWith(filePath) || f.includes(filePath));
1423
+ if (match) filePath = match;
1424
+ }
1425
+ const codePath = (item.codePath || []).map(
1426
+ (step, idx) => ({
1427
+ step: step.step || idx + 1,
1428
+ file: step.file || filePath,
1429
+ line: step.line || item.line,
1430
+ code: step.code || "",
1431
+ explanation: step.explanation || ""
1432
+ })
1433
+ );
1434
+ bugs.push({
1435
+ id: generateBugId(i),
1436
+ title: String(item.title).slice(0, 100),
1437
+ description: String(item.description || ""),
1438
+ file: filePath,
1439
+ line: Number(item.line) || 0,
1440
+ endLine: item.endLine ? Number(item.endLine) : void 0,
1441
+ severity: this.parseSeverity(item.severity),
1442
+ category: this.parseCategory(item.category),
1443
+ confidence: {
1444
+ overall: "medium",
1445
+ codePathValidity: 0.75,
1446
+ reachability: 0.75,
1447
+ intentViolation: false,
1448
+ staticToolSignal: false,
1449
+ adversarialSurvived: false
1450
+ },
1451
+ codePath,
1452
+ evidence: Array.isArray(item.evidence) ? item.evidence.map(String) : [],
1453
+ suggestedFix: item.suggestedFix ? String(item.suggestedFix) : void 0,
1454
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1455
+ });
1456
+ }
1457
+ return bugs;
1458
+ } catch {
1459
+ return [];
1460
+ }
1461
+ }
1462
+ parseAdversarialResponse(response, bug) {
1463
+ try {
1464
+ const json = this.extractJson(response);
1465
+ if (!json) return { survived: true, counterArguments: [] };
1466
+ const parsed = JSON.parse(json);
1467
+ const survived = parsed.survived !== false;
1468
+ return {
1469
+ survived,
1470
+ counterArguments: Array.isArray(parsed.counterArguments) ? parsed.counterArguments.map(String) : [],
1471
+ adjustedConfidence: survived ? {
1472
+ ...bug.confidence,
1473
+ overall: this.parseConfidence(parsed.confidence),
1474
+ adversarialSurvived: true
1475
+ } : void 0
1476
+ };
1477
+ } catch {
1478
+ return { survived: true, counterArguments: [] };
1479
+ }
1480
+ }
1481
+ parseUnderstandingResponse(response, files) {
1482
+ try {
1483
+ const json = this.extractJson(response);
1484
+ if (!json) throw new Error("No JSON found");
1485
+ const parsed = JSON.parse(json);
1486
+ let totalLines = 0;
1487
+ for (const file of files.slice(0, 50)) {
1488
+ try {
1489
+ totalLines += readFileSync(file, "utf-8").split("\n").length;
1490
+ } catch {
1491
+ }
1492
+ }
1493
+ return {
1494
+ version: "1",
1495
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1496
+ summary: {
1497
+ type: parsed.summary?.type || "unknown",
1498
+ framework: parsed.summary?.framework,
1499
+ language: parsed.summary?.language || "typescript",
1500
+ description: parsed.summary?.description || "No description available"
1501
+ },
1502
+ features: (parsed.features || []).map((f) => ({
1503
+ name: f.name || "Unknown",
1504
+ description: f.description || "",
1505
+ priority: f.priority || "medium",
1506
+ constraints: Array.isArray(f.constraints) ? f.constraints : [],
1507
+ relatedFiles: Array.isArray(f.relatedFiles) ? f.relatedFiles : []
1508
+ })),
1509
+ contracts: (parsed.contracts || []).map((c) => ({
1510
+ function: c.function || "unknown",
1511
+ file: c.file || "unknown",
1512
+ inputs: Array.isArray(c.inputs) ? c.inputs : [],
1513
+ outputs: c.outputs || { type: "unknown" },
1514
+ invariants: Array.isArray(c.invariants) ? c.invariants : [],
1515
+ sideEffects: Array.isArray(c.sideEffects) ? c.sideEffects : []
1516
+ })),
1517
+ dependencies: {},
1518
+ structure: {
1519
+ totalFiles: files.length,
1520
+ totalLines
1521
+ }
1522
+ };
1523
+ } catch {
1524
+ return {
1525
+ version: "1",
1526
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1527
+ summary: {
1528
+ type: "unknown",
1529
+ language: "typescript",
1530
+ description: "Failed to analyze codebase"
1531
+ },
1532
+ features: [],
1533
+ contracts: [],
1534
+ dependencies: {},
1535
+ structure: {
1536
+ totalFiles: files.length,
1537
+ totalLines: 0
1538
+ }
1539
+ };
1540
+ }
1541
+ }
1542
+ // ─────────────────────────────────────────────────────────────
1543
+ // Utilities
1544
+ // ─────────────────────────────────────────────────────────────
1545
+ extractJson(text) {
1546
+ const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
1547
+ if (codeBlockMatch) return codeBlockMatch[1].trim();
1548
+ const arrayMatch = text.match(/\[[\s\S]*\]/);
1549
+ if (arrayMatch) return arrayMatch[0];
1550
+ const objectMatch = text.match(/\{[\s\S]*\}/);
1551
+ if (objectMatch) return objectMatch[0];
1552
+ return null;
1553
+ }
1554
+ parseSeverity(value) {
1555
+ const str = String(value).toLowerCase();
1556
+ if (["critical", "high", "medium", "low"].includes(str)) {
1557
+ return str;
1558
+ }
1559
+ return "medium";
1560
+ }
1561
+ parseCategory(value) {
1562
+ const str = String(value).toLowerCase().replace(/_/g, "-");
1563
+ const validCategories = [
1564
+ "logic-error",
1565
+ "security",
1566
+ "async-race-condition",
1567
+ "edge-case",
1568
+ "null-reference",
1569
+ "type-coercion",
1570
+ "resource-leak",
1571
+ "intent-violation"
1572
+ ];
1573
+ if (validCategories.includes(str)) {
1574
+ return str;
1575
+ }
1576
+ if (str.includes("null") || str.includes("undefined")) return "null-reference";
1577
+ if (str.includes("security")) return "security";
1578
+ if (str.includes("async") || str.includes("race")) return "async-race-condition";
1579
+ return "logic-error";
1580
+ }
1581
+ parseConfidence(value) {
1582
+ const str = String(value).toLowerCase();
1583
+ if (["high", "medium", "low"].includes(str)) {
1584
+ return str;
1585
+ }
1586
+ return "medium";
1587
+ }
1588
+ };
1589
+
1590
+ // src/providers/index.ts
1591
+ var providers = {
1592
+ "claude-code": () => new ClaudeCodeProvider(),
1593
+ aider: () => new AiderProvider(),
1594
+ codex: () => {
1595
+ throw new Error("Codex provider not yet implemented");
1596
+ },
1597
+ opencode: () => {
1598
+ throw new Error("OpenCode provider not yet implemented");
1599
+ },
1600
+ ollama: () => {
1601
+ throw new Error("Ollama provider not yet implemented");
1602
+ },
1603
+ gemini: () => {
1604
+ throw new Error("Gemini provider not yet implemented");
1605
+ }
1606
+ };
1607
+ async function getProvider(name) {
1608
+ const factory = providers[name];
1609
+ if (!factory) {
1610
+ throw new Error(`Unknown provider: ${name}`);
1611
+ }
1612
+ const provider = factory();
1613
+ const available = await provider.isAvailable();
1614
+ if (!available) {
1615
+ throw new Error(`Provider ${name} is not available. Make sure it's installed and configured.`);
1616
+ }
1617
+ return provider;
1618
+ }
1619
+ async function runStaticAnalysis(cwd, files, config) {
1620
+ const results = [];
1621
+ if (config.staticAnalysis.typescript) {
1622
+ const tscResults = await runTypeScript(cwd);
1623
+ results.push(...tscResults);
1624
+ }
1625
+ if (config.staticAnalysis.eslint) {
1626
+ const eslintResults = await runEslint(cwd, files);
1627
+ results.push(...eslintResults);
1628
+ }
1629
+ return results;
1630
+ }
1631
+ async function runTypeScript(cwd) {
1632
+ const results = [];
1633
+ const tsconfigPath = join(cwd, "tsconfig.json");
1634
+ if (!existsSync(tsconfigPath)) {
1635
+ return results;
1636
+ }
1637
+ try {
1638
+ await execa("npx", ["tsc", "--noEmit", "--pretty", "false"], {
1639
+ cwd,
1640
+ timeout: 6e4
1641
+ });
1642
+ } catch (error) {
1643
+ const output = error.stdout || "";
1644
+ const lines = output.split("\n");
1645
+ for (const line of lines) {
1646
+ const match = line.match(/^(.+)\((\d+),(\d+)\):\s+(error|warning)\s+TS\d+:\s+(.+)$/);
1647
+ if (match) {
1648
+ results.push({
1649
+ tool: "typescript",
1650
+ file: match[1],
1651
+ line: parseInt(match[2], 10),
1652
+ message: match[5],
1653
+ severity: match[4] === "error" ? "error" : "warning",
1654
+ code: `TS${match[4]}`
1655
+ });
1656
+ }
1657
+ }
1658
+ }
1659
+ return results;
1660
+ }
1661
+ async function runEslint(cwd, files) {
1662
+ const results = [];
1663
+ const hasEslint = existsSync(join(cwd, ".eslintrc")) || existsSync(join(cwd, ".eslintrc.js")) || existsSync(join(cwd, ".eslintrc.json")) || existsSync(join(cwd, ".eslintrc.yml")) || existsSync(join(cwd, "eslint.config.js")) || existsSync(join(cwd, "eslint.config.mjs"));
1664
+ if (!hasEslint) {
1665
+ return results;
1666
+ }
1667
+ try {
1668
+ const { stdout } = await execa(
1669
+ "npx",
1670
+ ["eslint", "--format", "json", "--no-error-on-unmatched-pattern", ...files.slice(0, 50)],
1671
+ {
1672
+ cwd,
1673
+ timeout: 6e4,
1674
+ reject: false
1675
+ }
1676
+ );
1677
+ const eslintResults = JSON.parse(stdout || "[]");
1678
+ for (const fileResult of eslintResults) {
1679
+ for (const message of fileResult.messages || []) {
1680
+ results.push({
1681
+ tool: "eslint",
1682
+ file: fileResult.filePath,
1683
+ line: message.line || 0,
1684
+ message: message.message,
1685
+ severity: message.severity === 2 ? "error" : "warning",
1686
+ code: message.ruleId
1687
+ });
1688
+ }
1689
+ }
1690
+ } catch {
1691
+ }
1692
+ return results;
1693
+ }
1694
+
1695
+ // src/output/sarif.ts
1696
+ function outputSarif(result) {
1697
+ const rules = [];
1698
+ const results = [];
1699
+ const seenRules = /* @__PURE__ */ new Set();
1700
+ for (const bug of result.bugs) {
1701
+ if (!seenRules.has(bug.category)) {
1702
+ seenRules.add(bug.category);
1703
+ rules.push({
1704
+ id: bug.category,
1705
+ name: formatRuleName(bug.category),
1706
+ shortDescription: { text: getCategoryDescription(bug.category) },
1707
+ fullDescription: { text: getCategoryDescription(bug.category) },
1708
+ defaultConfiguration: { level: "warning" },
1709
+ properties: { category: bug.category }
1710
+ });
1711
+ }
1712
+ results.push(bugToSarifResult(bug));
1713
+ }
1714
+ return {
1715
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
1716
+ version: "2.1.0",
1717
+ runs: [
1718
+ {
1719
+ tool: {
1720
+ driver: {
1721
+ name: "whiterose",
1722
+ version: "0.1.0",
1723
+ informationUri: "https://github.com/shakecodeslikecray/whiterose",
1724
+ rules
1725
+ }
1726
+ },
1727
+ results
1728
+ }
1729
+ ]
1730
+ };
1731
+ }
1732
+ function bugToSarifResult(bug) {
1733
+ const result = {
1734
+ ruleId: bug.id,
1735
+ level: severityToLevel(bug.severity),
1736
+ message: {
1737
+ text: bug.title,
1738
+ markdown: `**${bug.title}**
1739
+
1740
+ ${bug.description}
1741
+
1742
+ **Evidence:**
1743
+ ${bug.evidence.map((e) => `- ${e}`).join("\n")}`
1744
+ },
1745
+ locations: [
1746
+ {
1747
+ physicalLocation: {
1748
+ artifactLocation: { uri: bug.file },
1749
+ region: { startLine: bug.line, endLine: bug.endLine }
1750
+ }
1751
+ }
1752
+ ]
1753
+ };
1754
+ if (bug.codePath.length > 0) {
1755
+ result.codeFlows = [
1756
+ {
1757
+ threadFlows: [
1758
+ {
1759
+ locations: bug.codePath.map((step) => ({
1760
+ location: {
1761
+ physicalLocation: {
1762
+ artifactLocation: { uri: step.file },
1763
+ region: { startLine: step.line }
1764
+ }
1765
+ },
1766
+ message: { text: step.explanation }
1767
+ }))
1768
+ }
1769
+ ]
1770
+ }
1771
+ ];
1772
+ }
1773
+ return result;
1774
+ }
1775
+ function severityToLevel(severity) {
1776
+ switch (severity) {
1777
+ case "critical":
1778
+ return "error";
1779
+ case "high":
1780
+ return "error";
1781
+ case "medium":
1782
+ return "warning";
1783
+ case "low":
1784
+ return "note";
1785
+ default:
1786
+ return "warning";
1787
+ }
1788
+ }
1789
+ function formatRuleName(category) {
1790
+ return category.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1791
+ }
1792
+ function getCategoryDescription(category) {
1793
+ const descriptions = {
1794
+ "logic-error": "Logic errors such as off-by-one, wrong operators, or incorrect conditions",
1795
+ security: "Security vulnerabilities including injection, auth bypass, and data exposure",
1796
+ "async-race-condition": "Async/concurrency issues like race conditions and missing awaits",
1797
+ "edge-case": "Edge cases that are not properly handled",
1798
+ "null-reference": "Potential null or undefined reference issues",
1799
+ "type-coercion": "Type coercion bugs that may cause unexpected behavior",
1800
+ "resource-leak": "Resource leaks such as unclosed handles or connections",
1801
+ "intent-violation": "Violations of documented behavioral contracts or business rules"
1802
+ };
1803
+ return descriptions[category] || "Unknown bug category";
1804
+ }
1805
+
1806
+ // src/output/markdown.ts
1807
+ function outputMarkdown(result) {
1808
+ const lines = [];
1809
+ lines.push("# Bug Report");
1810
+ lines.push("");
1811
+ lines.push(`> Generated by [whiterose](https://github.com/shakecodeslikecray/whiterose) on ${(/* @__PURE__ */ new Date()).toISOString()}`);
1812
+ lines.push("");
1813
+ lines.push("## Summary");
1814
+ lines.push("");
1815
+ lines.push(`| Severity | Count |`);
1816
+ lines.push(`|----------|-------|`);
1817
+ lines.push(`| Critical | ${result.summary.critical} |`);
1818
+ lines.push(`| High | ${result.summary.high} |`);
1819
+ lines.push(`| Medium | ${result.summary.medium} |`);
1820
+ lines.push(`| Low | ${result.summary.low} |`);
1821
+ lines.push(`| **Total** | **${result.summary.total}** |`);
1822
+ lines.push("");
1823
+ lines.push(`- **Scan Type:** ${result.scanType}`);
1824
+ lines.push(`- **Files Scanned:** ${result.filesScanned}`);
1825
+ if (result.filesChanged !== void 0) {
1826
+ lines.push(`- **Files Changed:** ${result.filesChanged}`);
1827
+ }
1828
+ lines.push("");
1829
+ if (result.bugs.length === 0) {
1830
+ lines.push("No bugs found.");
1831
+ return lines.join("\n");
1832
+ }
1833
+ const bySeverity = {
1834
+ critical: [],
1835
+ high: [],
1836
+ medium: [],
1837
+ low: []
1838
+ };
1839
+ for (const bug of result.bugs) {
1840
+ bySeverity[bug.severity].push(bug);
1841
+ }
1842
+ if (bySeverity.critical.length > 0) {
1843
+ lines.push("## Critical");
1844
+ lines.push("");
1845
+ for (const bug of bySeverity.critical) {
1846
+ lines.push(formatBug(bug));
1847
+ }
1848
+ }
1849
+ if (bySeverity.high.length > 0) {
1850
+ lines.push("## High");
1851
+ lines.push("");
1852
+ for (const bug of bySeverity.high) {
1853
+ lines.push(formatBug(bug));
1854
+ }
1855
+ }
1856
+ if (bySeverity.medium.length > 0) {
1857
+ lines.push("## Medium");
1858
+ lines.push("");
1859
+ for (const bug of bySeverity.medium) {
1860
+ lines.push(formatBug(bug));
1861
+ }
1862
+ }
1863
+ if (bySeverity.low.length > 0) {
1864
+ lines.push("## Low");
1865
+ lines.push("");
1866
+ for (const bug of bySeverity.low) {
1867
+ lines.push(formatBug(bug));
1868
+ }
1869
+ }
1870
+ return lines.join("\n");
1871
+ }
1872
+ function formatBug(bug) {
1873
+ const lines = [];
1874
+ const confidenceBadge = getConfidenceBadge(bug.confidence.overall);
1875
+ lines.push(`### ${bug.id}: ${bug.title} ${confidenceBadge}`);
1876
+ lines.push("");
1877
+ lines.push(`**Location:** \`${bug.file}:${bug.line}\``);
1878
+ lines.push(`**Category:** ${formatCategory(bug.category)}`);
1879
+ lines.push("");
1880
+ lines.push(bug.description);
1881
+ lines.push("");
1882
+ if (bug.codePath.length > 0) {
1883
+ lines.push("<details>");
1884
+ lines.push("<summary>Code Path</summary>");
1885
+ lines.push("");
1886
+ for (const step of bug.codePath) {
1887
+ lines.push(`${step.step}. \`${step.file}:${step.line}\``);
1888
+ lines.push(` \`\`\`${getLanguage(step.file)}`);
1889
+ lines.push(` ${step.code}`);
1890
+ lines.push(" ```");
1891
+ lines.push(` ${step.explanation}`);
1892
+ lines.push("");
1893
+ }
1894
+ lines.push("</details>");
1895
+ lines.push("");
1896
+ }
1897
+ if (bug.evidence.length > 0) {
1898
+ lines.push("**Evidence:**");
1899
+ for (const e of bug.evidence) {
1900
+ lines.push(`- ${e}`);
1901
+ }
1902
+ lines.push("");
1903
+ }
1904
+ if (bug.suggestedFix) {
1905
+ lines.push("**Suggested Fix:**");
1906
+ lines.push("```");
1907
+ lines.push(bug.suggestedFix);
1908
+ lines.push("```");
1909
+ lines.push("");
1910
+ }
1911
+ lines.push("---");
1912
+ lines.push("");
1913
+ return lines.join("\n");
1914
+ }
1915
+ function getConfidenceBadge(confidence) {
1916
+ switch (confidence) {
1917
+ case "high":
1918
+ return "`[HIGH CONFIDENCE]`";
1919
+ case "medium":
1920
+ return "`[MEDIUM CONFIDENCE]`";
1921
+ case "low":
1922
+ return "`[LOW CONFIDENCE]`";
1923
+ default:
1924
+ return "";
1925
+ }
1926
+ }
1927
+ function formatCategory(category) {
1928
+ return category.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1929
+ }
1930
+ function getLanguage(file) {
1931
+ if (file.endsWith(".ts") || file.endsWith(".tsx")) return "typescript";
1932
+ if (file.endsWith(".js") || file.endsWith(".jsx")) return "javascript";
1933
+ if (file.endsWith(".py")) return "python";
1934
+ if (file.endsWith(".go")) return "go";
1935
+ return "";
1936
+ }
1937
+
1938
+ // src/core/contracts/intent.ts
1939
+ function generateIntentDocument(understanding) {
1940
+ const lines = [];
1941
+ lines.push(`# App Intent: ${understanding.summary.type}`);
1942
+ lines.push("");
1943
+ lines.push(`> Generated by whiterose on ${(/* @__PURE__ */ new Date()).toISOString()}`);
1944
+ lines.push("> Edit the sections above the line. Contracts below are auto-generated.");
1945
+ lines.push("");
1946
+ lines.push("## Overview");
1947
+ lines.push("");
1948
+ lines.push(understanding.summary.description);
1949
+ lines.push("");
1950
+ lines.push(`- **Framework:** ${understanding.summary.framework || "None detected"}`);
1951
+ lines.push(`- **Language:** ${understanding.summary.language}`);
1952
+ lines.push(`- **Files:** ${understanding.structure.totalFiles}`);
1953
+ if (understanding.structure.packages?.length) {
1954
+ lines.push(`- **Packages:** ${understanding.structure.packages.join(", ")}`);
1955
+ }
1956
+ lines.push("");
1957
+ if (understanding.features.length > 0) {
1958
+ lines.push("## Critical Features");
1959
+ lines.push("");
1960
+ const criticalFeatures = understanding.features.filter((f) => f.priority === "critical");
1961
+ const highFeatures = understanding.features.filter((f) => f.priority === "high");
1962
+ const otherFeatures = understanding.features.filter(
1963
+ (f) => f.priority !== "critical" && f.priority !== "high"
1964
+ );
1965
+ for (const feature of criticalFeatures) {
1966
+ lines.push(formatFeature(feature, "CRITICAL"));
1967
+ }
1968
+ if (highFeatures.length > 0) {
1969
+ lines.push("## High Priority Features");
1970
+ lines.push("");
1971
+ for (const feature of highFeatures) {
1972
+ lines.push(formatFeature(feature, "HIGH"));
1973
+ }
1974
+ }
1975
+ if (otherFeatures.length > 0) {
1976
+ lines.push("## Other Features");
1977
+ lines.push("");
1978
+ for (const feature of otherFeatures) {
1979
+ lines.push(formatFeature(feature));
1980
+ }
1981
+ }
1982
+ }
1983
+ lines.push("## Known Constraints");
1984
+ lines.push("");
1985
+ lines.push("<!-- Add any known constraints or business rules here -->");
1986
+ lines.push("");
1987
+ lines.push("- (Add your constraints here)");
1988
+ lines.push("");
1989
+ lines.push("## Areas of Concern");
1990
+ lines.push("");
1991
+ lines.push("<!-- Add files or areas that need extra scrutiny -->");
1992
+ lines.push("");
1993
+ lines.push("- (Add files that have had bugs before)");
1994
+ lines.push("");
1995
+ lines.push("---");
1996
+ lines.push("");
1997
+ lines.push("<!-- \u26A0\uFE0F DO NOT EDIT BELOW THIS LINE - Auto-generated contracts -->");
1998
+ lines.push("");
1999
+ if (understanding.contracts.length > 0) {
2000
+ lines.push("## Behavioral Contracts");
2001
+ lines.push("");
2002
+ for (const contract of understanding.contracts) {
2003
+ lines.push(formatContract(contract));
2004
+ }
2005
+ }
2006
+ return lines.join("\n");
2007
+ }
2008
+ function formatFeature(feature, badge) {
2009
+ const lines = [];
2010
+ const badgeStr = badge ? ` [${badge}]` : "";
2011
+ lines.push(`### ${feature.name}${badgeStr}`);
2012
+ lines.push("");
2013
+ lines.push(feature.description);
2014
+ lines.push("");
2015
+ if (feature.constraints.length > 0) {
2016
+ lines.push("**Constraints:**");
2017
+ for (const constraint of feature.constraints) {
2018
+ lines.push(`- ${constraint}`);
2019
+ }
2020
+ lines.push("");
2021
+ }
2022
+ if (feature.relatedFiles.length > 0) {
2023
+ lines.push(`**Files:** \`${feature.relatedFiles.join("`, `")}\``);
2024
+ lines.push("");
2025
+ }
2026
+ return lines.join("\n");
2027
+ }
2028
+ function formatContract(contract) {
2029
+ const lines = [];
2030
+ lines.push(`### \`${contract.file}:${contract.function}()\``);
2031
+ lines.push("");
2032
+ if (contract.inputs.length > 0) {
2033
+ lines.push("**Inputs:**");
2034
+ for (const input of contract.inputs) {
2035
+ const constraints = input.constraints ? ` (${input.constraints})` : "";
2036
+ lines.push(`- \`${input.name}\`: ${input.type}${constraints}`);
2037
+ }
2038
+ lines.push("");
2039
+ }
2040
+ lines.push("**Returns:** `" + contract.outputs.type + "`");
2041
+ if (contract.outputs.constraints) {
2042
+ lines.push(` - ${contract.outputs.constraints}`);
2043
+ }
2044
+ lines.push("");
2045
+ if (contract.invariants.length > 0) {
2046
+ lines.push("**Invariants:**");
2047
+ for (const invariant of contract.invariants) {
2048
+ lines.push(`- ${invariant}`);
2049
+ }
2050
+ lines.push("");
2051
+ }
2052
+ if (contract.sideEffects.length > 0) {
2053
+ lines.push("**Side Effects:**");
2054
+ for (const effect of contract.sideEffects) {
2055
+ lines.push(`- ${effect}`);
2056
+ }
2057
+ lines.push("");
2058
+ }
2059
+ if (contract.throws && contract.throws.length > 0) {
2060
+ lines.push("**Throws:**");
2061
+ for (const t of contract.throws) {
2062
+ lines.push(`- ${t}`);
2063
+ }
2064
+ lines.push("");
2065
+ }
2066
+ return lines.join("\n");
2067
+ }
2068
+ function parseIntentDocument(content) {
2069
+ const result = {
2070
+ knownConstraints: [],
2071
+ areasOfConcern: [],
2072
+ customFeatures: [],
2073
+ overrides: {}
2074
+ };
2075
+ const sections = splitIntoSections(content);
2076
+ const constraintsSection = sections.find((s) => s.title.includes("Known Constraints"));
2077
+ if (constraintsSection) {
2078
+ result.knownConstraints = parseListItems(constraintsSection.content);
2079
+ }
2080
+ const concernsSection = sections.find((s) => s.title.includes("Areas of Concern"));
2081
+ if (concernsSection) {
2082
+ result.areasOfConcern = parseListItems(concernsSection.content);
2083
+ }
2084
+ const overviewSection = sections.find((s) => s.title.includes("Overview"));
2085
+ if (overviewSection) {
2086
+ const frameworkMatch = overviewSection.content.match(/\*\*Framework:\*\*\s*(.+)/);
2087
+ if (frameworkMatch && !frameworkMatch[1].includes("None detected")) {
2088
+ result.overrides.framework = frameworkMatch[1].trim();
2089
+ }
2090
+ const paragraphs = overviewSection.content.split(/\n\n+/);
2091
+ const firstParagraph = paragraphs[0]?.trim();
2092
+ if (firstParagraph && !firstParagraph.startsWith("-") && !firstParagraph.startsWith("*")) {
2093
+ result.overrides.description = firstParagraph;
2094
+ }
2095
+ }
2096
+ const editableSections = content.split(/---+/)[0] || "";
2097
+ const featureSections = editableSections.match(/###\s+([^\n]+)\n([\s\S]*?)(?=###|##|$)/g);
2098
+ if (featureSections) {
2099
+ for (const section of featureSections) {
2100
+ const feature = parseFeatureSection(section);
2101
+ if (feature && isUserAddedFeature(feature)) {
2102
+ result.customFeatures.push(feature);
2103
+ }
2104
+ }
2105
+ }
2106
+ return result;
2107
+ }
2108
+ function splitIntoSections(content) {
2109
+ const sections = [];
2110
+ const sectionRegex = /^##\s+([^\n]+)\n([\s\S]*?)(?=^##\s|$)/gm;
2111
+ let match;
2112
+ while ((match = sectionRegex.exec(content)) !== null) {
2113
+ sections.push({
2114
+ title: match[1].trim(),
2115
+ content: match[2].trim()
2116
+ });
2117
+ }
2118
+ return sections;
2119
+ }
2120
+ function parseListItems(content) {
2121
+ const items = [];
2122
+ const lines = content.split("\n");
2123
+ for (const line of lines) {
2124
+ const match = line.match(/^[-*]\s+(.+)/);
2125
+ if (match) {
2126
+ const item = match[1].trim();
2127
+ if (!item.includes("Add your") && !item.includes("Add files") && item !== "") {
2128
+ items.push(item);
2129
+ }
2130
+ }
2131
+ }
2132
+ return items;
2133
+ }
2134
+ function parseFeatureSection(section) {
2135
+ const titleMatch = section.match(/###\s+([^\[\n]+)(?:\s*\[([^\]]+)\])?/);
2136
+ if (!titleMatch) return null;
2137
+ const name = titleMatch[1].trim();
2138
+ const badge = titleMatch[2]?.trim().toLowerCase();
2139
+ let priority = "medium";
2140
+ if (badge === "critical") priority = "critical";
2141
+ else if (badge === "high") priority = "high";
2142
+ else if (badge === "low") priority = "low";
2143
+ const contentAfterTitle = section.replace(/###\s+[^\n]+\n/, "").trim();
2144
+ const paragraphs = contentAfterTitle.split(/\n\n+/);
2145
+ const description = paragraphs[0]?.trim() || "";
2146
+ const constraints = [];
2147
+ const constraintsMatch = section.match(/\*\*Constraints:\*\*\n((?:[-*]\s+[^\n]+\n?)+)/);
2148
+ if (constraintsMatch) {
2149
+ const constraintLines = constraintsMatch[1].split("\n");
2150
+ for (const line of constraintLines) {
2151
+ const match = line.match(/^[-*]\s+(.+)/);
2152
+ if (match) {
2153
+ constraints.push(match[1].trim());
2154
+ }
2155
+ }
2156
+ }
2157
+ const relatedFiles = [];
2158
+ const filesMatch = section.match(/\*\*Files:\*\*\s*`([^`]+)`/);
2159
+ if (filesMatch) {
2160
+ const files = filesMatch[1].split(/`,\s*`/);
2161
+ relatedFiles.push(...files.map((f) => f.replace(/`/g, "").trim()));
2162
+ }
2163
+ return {
2164
+ name,
2165
+ description,
2166
+ priority,
2167
+ constraints,
2168
+ relatedFiles
2169
+ };
2170
+ }
2171
+ function isUserAddedFeature(feature) {
2172
+ return feature.constraints.length > 0 || feature.description.length > 50 || feature.relatedFiles.length > 0;
2173
+ }
2174
+ function mergeIntentWithUnderstanding(understanding, parsedIntent) {
2175
+ const merged = { ...understanding };
2176
+ if (parsedIntent.overrides.description) {
2177
+ merged.summary = {
2178
+ ...merged.summary,
2179
+ description: parsedIntent.overrides.description
2180
+ };
2181
+ }
2182
+ if (parsedIntent.overrides.framework) {
2183
+ merged.summary = {
2184
+ ...merged.summary,
2185
+ framework: parsedIntent.overrides.framework
2186
+ };
2187
+ }
2188
+ if (parsedIntent.customFeatures.length > 0) {
2189
+ merged.features = [
2190
+ ...parsedIntent.customFeatures,
2191
+ ...merged.features.filter(
2192
+ (f) => !parsedIntent.customFeatures.some((cf) => cf.name === f.name)
2193
+ )
2194
+ ];
2195
+ }
2196
+ if (parsedIntent.knownConstraints.length > 0) {
2197
+ merged.globalConstraints = parsedIntent.knownConstraints;
2198
+ }
2199
+ if (parsedIntent.areasOfConcern.length > 0) {
2200
+ merged.areasOfConcern = parsedIntent.areasOfConcern;
2201
+ }
2202
+ return merged;
2203
+ }
2204
+ var GIT_TIMEOUT = 3e4;
2205
+ async function isGitRepo(cwd = process.cwd()) {
2206
+ try {
2207
+ await execa("git", ["rev-parse", "--git-dir"], { cwd, timeout: GIT_TIMEOUT });
2208
+ return true;
2209
+ } catch {
2210
+ return false;
2211
+ }
2212
+ }
2213
+ async function getCurrentBranch(cwd = process.cwd()) {
2214
+ try {
2215
+ const { stdout } = await execa("git", ["branch", "--show-current"], {
2216
+ cwd,
2217
+ timeout: GIT_TIMEOUT
2218
+ });
2219
+ return stdout.trim();
2220
+ } catch {
2221
+ return "main";
2222
+ }
2223
+ }
2224
+ async function hasUncommittedChanges(cwd = process.cwd()) {
2225
+ try {
2226
+ const { stdout } = await execa("git", ["status", "--porcelain"], {
2227
+ cwd,
2228
+ timeout: GIT_TIMEOUT
2229
+ });
2230
+ return stdout.trim().length > 0;
2231
+ } catch {
2232
+ return false;
2233
+ }
2234
+ }
2235
+ async function createFixBranch(branchName, bug, cwd = process.cwd()) {
2236
+ const safeBugId = bug.id.toLowerCase().replace(/[^a-z0-9]/g, "-");
2237
+ const safeTitle = bug.title.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, "-").slice(0, 30);
2238
+ const fullBranchName = branchName || `whiterose/fix-${safeBugId}-${safeTitle}`;
2239
+ try {
2240
+ try {
2241
+ await execa("git", ["rev-parse", "--verify", fullBranchName], {
2242
+ cwd,
2243
+ timeout: GIT_TIMEOUT
2244
+ });
2245
+ await execa("git", ["checkout", fullBranchName], { cwd, timeout: GIT_TIMEOUT });
2246
+ } catch {
2247
+ await execa("git", ["checkout", "-b", fullBranchName], { cwd, timeout: GIT_TIMEOUT });
2248
+ }
2249
+ return fullBranchName;
2250
+ } catch (error) {
2251
+ throw new Error(`Failed to create branch: ${error.message}`);
2252
+ }
2253
+ }
2254
+ async function commitFix(bug, cwd = process.cwd()) {
2255
+ try {
2256
+ await execa("git", ["add", bug.file], { cwd, timeout: GIT_TIMEOUT });
2257
+ const { stdout: diff } = await execa("git", ["diff", "--cached", "--name-only"], {
2258
+ cwd,
2259
+ timeout: GIT_TIMEOUT
2260
+ });
2261
+ if (!diff.trim()) {
2262
+ return "";
2263
+ }
2264
+ const commitMessage = `fix(${bug.category}): ${bug.title}
2265
+
2266
+ Bug ID: ${bug.id}
2267
+ File: ${bug.file}:${bug.line}
2268
+ Severity: ${bug.severity}
2269
+
2270
+ ${bug.description}
2271
+
2272
+ Fixed by whiterose`;
2273
+ await execa("git", ["commit", "-m", commitMessage], {
2274
+ cwd,
2275
+ timeout: GIT_TIMEOUT
2276
+ });
2277
+ const { stdout: hash } = await execa("git", ["rev-parse", "HEAD"], {
2278
+ cwd,
2279
+ timeout: GIT_TIMEOUT
2280
+ });
2281
+ return hash.trim();
2282
+ } catch (error) {
2283
+ throw new Error(`Failed to commit fix: ${error.message}`);
2284
+ }
2285
+ }
2286
+ async function stashChanges(cwd = process.cwd()) {
2287
+ try {
2288
+ const hasChanges = await hasUncommittedChanges(cwd);
2289
+ if (!hasChanges) {
2290
+ return false;
2291
+ }
2292
+ await execa("git", ["stash", "push", "-m", "whiterose: stash before fix"], {
2293
+ cwd,
2294
+ timeout: GIT_TIMEOUT
2295
+ });
2296
+ return true;
2297
+ } catch (error) {
2298
+ throw new Error(`Failed to stash changes: ${error.message}`);
2299
+ }
2300
+ }
2301
+ async function popStash(cwd = process.cwd()) {
2302
+ try {
2303
+ await execa("git", ["stash", "pop"], { cwd, timeout: GIT_TIMEOUT });
2304
+ } catch (error) {
2305
+ throw new Error(`Failed to pop stash: ${error.message}`);
2306
+ }
2307
+ }
2308
+ async function getDiff(file, cwd = process.cwd()) {
2309
+ try {
2310
+ const { stdout } = await execa("git", ["diff", file], { cwd, timeout: GIT_TIMEOUT });
2311
+ return stdout;
2312
+ } catch {
2313
+ return "";
2314
+ }
2315
+ }
2316
+ async function getStagedDiff(cwd = process.cwd()) {
2317
+ try {
2318
+ const { stdout } = await execa("git", ["diff", "--cached"], { cwd, timeout: GIT_TIMEOUT });
2319
+ return stdout;
2320
+ } catch {
2321
+ return "";
2322
+ }
2323
+ }
2324
+ async function resetFile(file, cwd = process.cwd()) {
2325
+ try {
2326
+ await execa("git", ["checkout", "--", file], { cwd, timeout: GIT_TIMEOUT });
2327
+ } catch (error) {
2328
+ throw new Error(`Failed to reset file: ${error.message}`);
2329
+ }
2330
+ }
2331
+ async function getFileAtHead(file, cwd = process.cwd()) {
2332
+ try {
2333
+ const { stdout } = await execa("git", ["show", `HEAD:${file}`], {
2334
+ cwd,
2335
+ timeout: GIT_TIMEOUT
2336
+ });
2337
+ return stdout;
2338
+ } catch {
2339
+ return "";
2340
+ }
2341
+ }
2342
+ async function getGitStatus(cwd = process.cwd()) {
2343
+ const isRepo = await isGitRepo(cwd);
2344
+ if (!isRepo) {
2345
+ return {
2346
+ isRepo: false,
2347
+ branch: "",
2348
+ hasChanges: false,
2349
+ staged: [],
2350
+ modified: [],
2351
+ untracked: []
2352
+ };
2353
+ }
2354
+ const branch = await getCurrentBranch(cwd);
2355
+ try {
2356
+ const { stdout } = await execa("git", ["status", "--porcelain"], {
2357
+ cwd,
2358
+ timeout: GIT_TIMEOUT
2359
+ });
2360
+ const staged = [];
2361
+ const modified = [];
2362
+ const untracked = [];
2363
+ for (const line of stdout.split("\n")) {
2364
+ if (!line.trim()) continue;
2365
+ const status = line.slice(0, 2);
2366
+ const file = line.slice(3);
2367
+ if (status[0] !== " " && status[0] !== "?") {
2368
+ staged.push(file);
2369
+ }
2370
+ if (status[1] === "M") {
2371
+ modified.push(file);
2372
+ }
2373
+ if (status === "??") {
2374
+ untracked.push(file);
2375
+ }
2376
+ }
2377
+ return {
2378
+ isRepo: true,
2379
+ branch,
2380
+ hasChanges: staged.length > 0 || modified.length > 0,
2381
+ staged,
2382
+ modified,
2383
+ untracked
2384
+ };
2385
+ } catch {
2386
+ return {
2387
+ isRepo: true,
2388
+ branch,
2389
+ hasChanges: false,
2390
+ staged: [],
2391
+ modified: [],
2392
+ untracked: []
2393
+ };
2394
+ }
2395
+ }
2396
+
2397
+ // src/core/fixer.ts
2398
+ function isPathWithinProject(filePath, projectDir) {
2399
+ const resolvedPath = resolve(projectDir, filePath);
2400
+ const relativePath = relative(projectDir, resolvedPath);
2401
+ return !relativePath.startsWith("..") && !isAbsolute(relativePath);
2402
+ }
2403
+ function validateFilePath(filePath, projectDir) {
2404
+ const resolvedPath = isAbsolute(filePath) ? filePath : resolve(projectDir, filePath);
2405
+ if (!isPathWithinProject(resolvedPath, projectDir)) {
2406
+ throw new Error(`Security: Refusing to access file outside project directory: ${filePath}`);
2407
+ }
2408
+ if (filePath.includes("\0") || filePath.includes("..")) {
2409
+ throw new Error(`Security: Invalid file path contains suspicious characters: ${filePath}`);
2410
+ }
2411
+ return resolvedPath;
2412
+ }
2413
+ async function applyFix(bug, config, options) {
2414
+ const { dryRun, branch } = options;
2415
+ const projectDir = process.cwd();
2416
+ let safePath;
2417
+ try {
2418
+ safePath = validateFilePath(bug.file, projectDir);
2419
+ } catch (error) {
2420
+ return {
2421
+ success: false,
2422
+ error: error.message
2423
+ };
2424
+ }
2425
+ if (!existsSync(safePath)) {
2426
+ return {
2427
+ success: false,
2428
+ error: `File not found: ${bug.file}`
2429
+ };
2430
+ }
2431
+ const originalContent = readFileSync(safePath, "utf-8");
2432
+ const lines = originalContent.split("\n");
2433
+ let fixedContent;
2434
+ let diff;
2435
+ if (bug.suggestedFix) {
2436
+ const result = applySimpleFix(lines, bug.line, bug.endLine || bug.line, bug.suggestedFix);
2437
+ if (result.success) {
2438
+ fixedContent = result.content;
2439
+ diff = result.diff;
2440
+ } else {
2441
+ const llmResult = await generateAndApplyFix(bug, config, originalContent, safePath);
2442
+ if (!llmResult.success) {
2443
+ return llmResult;
2444
+ }
2445
+ fixedContent = llmResult.content;
2446
+ diff = llmResult.diff;
2447
+ }
2448
+ } else {
2449
+ const llmResult = await generateAndApplyFix(bug, config, originalContent, safePath);
2450
+ if (!llmResult.success) {
2451
+ return llmResult;
2452
+ }
2453
+ fixedContent = llmResult.content;
2454
+ diff = llmResult.diff;
2455
+ }
2456
+ if (dryRun) {
2457
+ console.log("\n--- Dry Run: Proposed changes ---");
2458
+ console.log(diff);
2459
+ console.log("--- End of proposed changes ---\n");
2460
+ return {
2461
+ success: true,
2462
+ diff
2463
+ };
2464
+ }
2465
+ let branchName;
2466
+ if (branch) {
2467
+ branchName = await createFixBranch(branch, bug);
2468
+ }
2469
+ writeFileSync(safePath, fixedContent, "utf-8");
2470
+ if (branchName || !branch) {
2471
+ await commitFix(bug);
2472
+ }
2473
+ return {
2474
+ success: true,
2475
+ diff,
2476
+ branchName
2477
+ };
2478
+ }
2479
+ function applySimpleFix(lines, startLine, endLine, fix) {
2480
+ const lineIndex = startLine - 1;
2481
+ const endIndex = endLine - 1;
2482
+ if (lineIndex < 0 || lineIndex >= lines.length) {
2483
+ return { success: false, content: "", diff: "" };
2484
+ }
2485
+ const originalLine = lines[lineIndex];
2486
+ const indentMatch = originalLine.match(/^(\s*)/);
2487
+ const indent = indentMatch ? indentMatch[1] : "";
2488
+ const fixLines = fix.split("\n").map((line, i) => {
2489
+ if (i === 0 || line.trim() === "") return line;
2490
+ return indent + line.trimStart();
2491
+ });
2492
+ const removedLines = lines.slice(lineIndex, endIndex + 1);
2493
+ const diff = [
2494
+ `--- ${lines[lineIndex]}`,
2495
+ ...removedLines.map((l) => `- ${l}`),
2496
+ ...fixLines.map((l) => `+ ${l}`)
2497
+ ].join("\n");
2498
+ const newLines = [...lines.slice(0, lineIndex), ...fixLines, ...lines.slice(endIndex + 1)];
2499
+ return {
2500
+ success: true,
2501
+ content: newLines.join("\n"),
2502
+ diff
2503
+ };
2504
+ }
2505
+ async function generateAndApplyFix(bug, _config, originalContent, safePath) {
2506
+ try {
2507
+ const prompt = buildFixPrompt(bug, originalContent);
2508
+ const workingDir = safePath ? dirname(safePath) : process.cwd();
2509
+ const { stdout } = await execa(
2510
+ "claude",
2511
+ ["-p", prompt, "--output-format", "text"],
2512
+ {
2513
+ cwd: workingDir,
2514
+ timeout: 12e4,
2515
+ env: { ...process.env, NO_COLOR: "1" }
2516
+ }
2517
+ );
2518
+ const fixedContent = parseFixResponse(stdout, originalContent);
2519
+ if (!fixedContent) {
2520
+ return {
2521
+ success: false,
2522
+ error: "Failed to parse fix from LLM response"
2523
+ };
2524
+ }
2525
+ const diff = generateDiff(originalContent, fixedContent, bug.file);
2526
+ return {
2527
+ success: true,
2528
+ content: fixedContent,
2529
+ diff
2530
+ };
2531
+ } catch (error) {
2532
+ return {
2533
+ success: false,
2534
+ error: error.message || "Unknown error generating fix"
2535
+ };
2536
+ }
2537
+ }
2538
+ function buildFixPrompt(bug, originalContent) {
2539
+ return `Fix the following bug in the code.
2540
+
2541
+ BUG DETAILS:
2542
+ - Title: ${bug.title}
2543
+ - Description: ${bug.description}
2544
+ - File: ${bug.file}
2545
+ - Line: ${bug.line}${bug.endLine ? `-${bug.endLine}` : ""}
2546
+ - Category: ${bug.category}
2547
+ - Severity: ${bug.severity}
2548
+
2549
+ EVIDENCE:
2550
+ ${bug.evidence.map((e) => `- ${e}`).join("\n")}
2551
+
2552
+ CODE PATH:
2553
+ ${bug.codePath.map((s) => `${s.step}. ${s.file}:${s.line} - ${s.explanation}`).join("\n")}
2554
+
2555
+ ORIGINAL FILE CONTENT:
2556
+ \`\`\`
2557
+ ${originalContent}
2558
+ \`\`\`
2559
+
2560
+ ${bug.suggestedFix ? `SUGGESTED FIX APPROACH:
2561
+ ${bug.suggestedFix}
2562
+
2563
+ ` : ""}
2564
+
2565
+ Please provide the COMPLETE fixed file content. Output ONLY the fixed code, no explanations.
2566
+ Wrap the code in \`\`\` code blocks.
2567
+
2568
+ IMPORTANT:
2569
+ - Fix ONLY the identified bug
2570
+ - Do not refactor or change anything else
2571
+ - Preserve all formatting and style
2572
+ - Ensure the fix actually addresses the bug`;
2573
+ }
2574
+ function parseFixResponse(response, originalContent) {
2575
+ const codeBlockMatch = response.match(/```(?:\w+)?\s*([\s\S]*?)```/);
2576
+ if (codeBlockMatch) {
2577
+ const extracted = codeBlockMatch[1].trim();
2578
+ const originalLines = originalContent.split("\n").slice(0, 5);
2579
+ const extractedLines = extracted.split("\n").slice(0, 5);
2580
+ let matchCount = 0;
2581
+ for (const origLine of originalLines) {
2582
+ if (extractedLines.some((l) => l.trim() === origLine.trim())) {
2583
+ matchCount++;
2584
+ }
2585
+ }
2586
+ if (matchCount >= 2 || extracted.length > originalContent.length * 0.5) {
2587
+ return extracted;
2588
+ }
2589
+ }
2590
+ if (response.includes("function") || response.includes("const ") || response.includes("import ")) {
2591
+ return response.trim();
2592
+ }
2593
+ return null;
2594
+ }
2595
+ function generateDiff(original, fixed, filename) {
2596
+ const origLines = original.split("\n");
2597
+ const fixedLines = fixed.split("\n");
2598
+ const diff = [`--- a/${basename(filename)}`, `+++ b/${basename(filename)}`];
2599
+ const maxLen = Math.max(origLines.length, fixedLines.length);
2600
+ for (let i = 0; i < maxLen; i++) {
2601
+ const origLine = origLines[i];
2602
+ const fixedLine = fixedLines[i];
2603
+ if (origLine === void 0) {
2604
+ diff.push(`+ ${fixedLine}`);
2605
+ } else if (fixedLine === void 0) {
2606
+ diff.push(`- ${origLine}`);
2607
+ } else if (origLine !== fixedLine) {
2608
+ diff.push(`- ${origLine}`);
2609
+ diff.push(`+ ${fixedLine}`);
2610
+ }
2611
+ }
2612
+ return diff.join("\n");
2613
+ }
2614
+ async function batchFix(bugs, config, options) {
2615
+ const results = /* @__PURE__ */ new Map();
2616
+ for (const bug of bugs) {
2617
+ const result = await applyFix(bug, config, options);
2618
+ results.set(bug.id, result);
2619
+ if (!result.success && !options.dryRun) {
2620
+ break;
2621
+ }
2622
+ }
2623
+ return results;
2624
+ }
2625
+ async function detectMonorepo(cwd) {
2626
+ const packageJsonPath = join(cwd, "package.json");
2627
+ if (!existsSync(packageJsonPath)) {
2628
+ return {
2629
+ isMonorepo: false,
2630
+ type: "none",
2631
+ rootPath: cwd,
2632
+ packages: []
2633
+ };
2634
+ }
2635
+ let packageJson;
2636
+ try {
2637
+ packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2638
+ } catch {
2639
+ return {
2640
+ isMonorepo: false,
2641
+ type: "none",
2642
+ rootPath: cwd,
2643
+ packages: []
2644
+ };
2645
+ }
2646
+ if (packageJson.workspaces) {
2647
+ const workspacePatterns = Array.isArray(packageJson.workspaces) ? packageJson.workspaces : packageJson.workspaces.packages || [];
2648
+ const packages = await findPackages(cwd, workspacePatterns);
2649
+ const type = existsSync(join(cwd, "yarn.lock")) ? "yarn-workspaces" : "npm-workspaces";
2650
+ return {
2651
+ isMonorepo: packages.length > 0,
2652
+ type,
2653
+ rootPath: cwd,
2654
+ packages
2655
+ };
2656
+ }
2657
+ const pnpmWorkspacePath = join(cwd, "pnpm-workspace.yaml");
2658
+ if (existsSync(pnpmWorkspacePath)) {
2659
+ const pnpmWorkspace = readFileSync(pnpmWorkspacePath, "utf-8");
2660
+ const packagesMatch = pnpmWorkspace.match(/packages:\s*([\s\S]*?)(?=\n\w|$)/);
2661
+ if (packagesMatch) {
2662
+ const patterns = packagesMatch[1].split("\n").map((line) => line.replace(/^\s*-\s*['"]?/, "").replace(/['"]?\s*$/, "")).filter((line) => line && !line.startsWith("#"));
2663
+ const packages = await findPackages(cwd, patterns);
2664
+ return {
2665
+ isMonorepo: packages.length > 0,
2666
+ type: "pnpm-workspaces",
2667
+ rootPath: cwd,
2668
+ packages
2669
+ };
2670
+ }
2671
+ }
2672
+ const lernaJsonPath = join(cwd, "lerna.json");
2673
+ if (existsSync(lernaJsonPath)) {
2674
+ try {
2675
+ const lernaConfig = JSON.parse(readFileSync(lernaJsonPath, "utf-8"));
2676
+ const patterns = lernaConfig.packages || ["packages/*"];
2677
+ const packages = await findPackages(cwd, patterns);
2678
+ return {
2679
+ isMonorepo: packages.length > 0,
2680
+ type: "lerna",
2681
+ rootPath: cwd,
2682
+ packages
2683
+ };
2684
+ } catch {
2685
+ }
2686
+ }
2687
+ const nxJsonPath = join(cwd, "nx.json");
2688
+ if (existsSync(nxJsonPath)) {
2689
+ const nxPatterns = ["apps/*", "libs/*", "packages/*"];
2690
+ const packages = await findPackages(cwd, nxPatterns);
2691
+ return {
2692
+ isMonorepo: packages.length > 0,
2693
+ type: "nx",
2694
+ rootPath: cwd,
2695
+ packages
2696
+ };
2697
+ }
2698
+ const turboJsonPath = join(cwd, "turbo.json");
2699
+ if (existsSync(turboJsonPath)) {
2700
+ const workspacePatterns = packageJson.workspaces ? Array.isArray(packageJson.workspaces) ? packageJson.workspaces : packageJson.workspaces.packages || [] : ["packages/*", "apps/*"];
2701
+ const packages = await findPackages(cwd, workspacePatterns);
2702
+ return {
2703
+ isMonorepo: packages.length > 0,
2704
+ type: "turborepo",
2705
+ rootPath: cwd,
2706
+ packages
2707
+ };
2708
+ }
2709
+ return {
2710
+ isMonorepo: false,
2711
+ type: "none",
2712
+ rootPath: cwd,
2713
+ packages: []
2714
+ };
2715
+ }
2716
+ async function findPackages(cwd, patterns) {
2717
+ const packages = [];
2718
+ const globPatterns = patterns.map((pattern) => {
2719
+ pattern = pattern.replace(/\/$/, "");
2720
+ if (pattern.endsWith("*")) {
2721
+ return `${pattern}/package.json`;
2722
+ }
2723
+ return `${pattern}/package.json`;
2724
+ });
2725
+ const packageJsonPaths = await fg3(globPatterns, {
2726
+ cwd,
2727
+ absolute: true,
2728
+ ignore: ["**/node_modules/**"]
2729
+ });
2730
+ for (const pkgJsonPath of packageJsonPaths) {
2731
+ try {
2732
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
2733
+ const pkgDir = dirname(pkgJsonPath);
2734
+ packages.push({
2735
+ name: pkgJson.name || relative(cwd, pkgDir),
2736
+ path: pkgDir,
2737
+ relativePath: relative(cwd, pkgDir),
2738
+ packageJson: pkgJson
2739
+ });
2740
+ } catch {
2741
+ }
2742
+ }
2743
+ return packages.sort((a, b) => a.name.localeCompare(b.name));
2744
+ }
2745
+ async function getPackageFiles(pkg, patterns = ["**/*.{ts,tsx,js,jsx}"], ignore = ["node_modules/**", "dist/**", "build/**"]) {
2746
+ return fg3(patterns, {
2747
+ cwd: pkg.path,
2748
+ absolute: true,
2749
+ ignore,
2750
+ onlyFiles: true
2751
+ });
2752
+ }
2753
+ function getPackageForFile(file, packages) {
2754
+ for (const pkg of packages) {
2755
+ if (file.startsWith(pkg.path)) {
2756
+ return pkg;
2757
+ }
2758
+ }
2759
+ return null;
2760
+ }
2761
+ function groupFilesByPackage(files, packages) {
2762
+ const grouped = /* @__PURE__ */ new Map();
2763
+ for (const file of files) {
2764
+ const pkg = getPackageForFile(file, packages);
2765
+ const existing = grouped.get(pkg) || [];
2766
+ existing.push(file);
2767
+ grouped.set(pkg, existing);
2768
+ }
2769
+ return grouped;
2770
+ }
2771
+ async function getCrossPackageDependencies(packages) {
2772
+ const deps = /* @__PURE__ */ new Map();
2773
+ for (const pkg of packages) {
2774
+ const pkgDeps = [];
2775
+ const allDeps = {
2776
+ ...pkg.packageJson.dependencies,
2777
+ ...pkg.packageJson.devDependencies,
2778
+ ...pkg.packageJson.peerDependencies
2779
+ };
2780
+ for (const depName of Object.keys(allDeps)) {
2781
+ if (packages.some((p) => p.name === depName)) {
2782
+ pkgDeps.push(depName);
2783
+ }
2784
+ }
2785
+ if (pkgDeps.length > 0) {
2786
+ deps.set(pkg.name, pkgDeps);
2787
+ }
2788
+ }
2789
+ return deps;
2790
+ }
2791
+
2792
+ export { AiderProvider, BehavioralContract, Bug, BugCategory, BugSeverity, CacheState, ClaudeCodeProvider, CodePathStep, CodebaseUnderstanding, ConfidenceLevel, ConfidenceScore, FeatureIntent, FileHash, MonorepoConfig, PackageConfig, PriorityLevel, ProviderType, ScanResult, WhiteroseConfig, applyFix, batchFix, buildDependencyGraph, commitFix, createFixBranch, dependsOn, detectMonorepo, detectProvider, findCircularDependencies, generateIntentDocument, getChangedFiles, getCrossPackageDependencies, getCurrentBranch, getDependentFiles2 as getDependentFiles, getDiff, getFileAtHead, getGitStatus, getImportsOf, getPackageFiles, getPackageForFile, getProvider, getStagedDiff, groupFilesByPackage, hasUncommittedChanges, isGitRepo, isProviderAvailable, loadConfig, loadUnderstanding, mergeIntentWithUnderstanding, outputMarkdown, outputSarif, parseIntentDocument, popStash, resetFile, runStaticAnalysis, saveConfig, scanCodebase, stashChanges };
2793
+ //# sourceMappingURL=index.js.map
2794
+ //# sourceMappingURL=index.js.map