@vibedrift/cli 0.4.5 → 0.5.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 CHANGED
@@ -2985,83 +2985,95 @@ var dependenciesAnalyzer = {
2985
2985
  return findings;
2986
2986
  }
2987
2987
  };
2988
- function analyzeJsDeps(ctx) {
2989
- const findings = [];
2990
- const pkg = ctx.packageJson;
2991
- const declared = /* @__PURE__ */ new Set([
2992
- ...Object.keys(pkg.dependencies ?? {}),
2993
- ...Object.keys(pkg.devDependencies ?? {}),
2994
- ...Object.keys(pkg.peerDependencies ?? {}),
2995
- ...Object.keys(pkg.optionalDependencies ?? {})
2996
- ]);
2997
- const isMonorepo = [
2998
- ...Object.values(pkg.dependencies ?? {}),
2999
- ...Object.values(pkg.devDependencies ?? {})
3000
- ].some(
3001
- (v) => typeof v === "string" && (v.startsWith("workspace:") || v === "*")
3002
- );
3003
- const hasWorkspaces = !!pkg.workspaces;
2988
+ function collectImportedPackages(files) {
3004
2989
  const imported = /* @__PURE__ */ new Set();
3005
2990
  const importLocations = /* @__PURE__ */ new Map();
3006
- const jsFiles = ctx.files.filter(
3007
- (f) => (f.language === "javascript" || f.language === "typescript") && !FIXTURE_PATH_PATTERN.test(f.relativePath)
3008
- );
3009
- for (const file of jsFiles) {
2991
+ for (const file of files) {
3010
2992
  for (const pattern of JS_IMPORT_PATTERNS) {
3011
2993
  const regex = new RegExp(pattern.source, pattern.flags);
3012
2994
  let match;
3013
2995
  while ((match = regex.exec(file.content)) !== null) {
3014
- const pkg2 = extractJsPackageName(match[1]);
3015
- if (NODE_BUILTINS.has(pkg2) || pkg2.startsWith("node:") || pkg2.startsWith("@/") || pkg2.startsWith("~")) continue;
3016
- imported.add(pkg2);
3017
- if (!importLocations.has(pkg2)) importLocations.set(pkg2, []);
3018
- importLocations.get(pkg2).push({
2996
+ const pkg = extractJsPackageName(match[1]);
2997
+ if (NODE_BUILTINS.has(pkg) || pkg.startsWith("node:") || pkg.startsWith("@/") || pkg.startsWith("~")) continue;
2998
+ imported.add(pkg);
2999
+ if (!importLocations.has(pkg)) importLocations.set(pkg, []);
3000
+ importLocations.get(pkg).push({
3019
3001
  file: file.relativePath,
3020
3002
  line: file.content.slice(0, match.index).split("\n").length
3021
3003
  });
3022
3004
  }
3023
3005
  }
3024
3006
  }
3007
+ return { imported, importLocations };
3008
+ }
3009
+ function detectPhantomDeps(declared, imported, devToolPatterns) {
3025
3010
  const phantom = [...declared].filter((d) => !imported.has(d));
3026
3011
  const realPhantom = phantom.filter(
3027
- (p) => !DEV_TOOL_PATTERNS.some((pat) => p.includes(pat))
3012
+ (p) => !devToolPatterns.some((pat) => p.includes(pat))
3028
3013
  );
3029
3014
  if (realPhantom.length > 0) {
3030
- findings.push({
3015
+ return [{
3031
3016
  analyzerId: "dependencies",
3032
3017
  severity: realPhantom.length > 5 ? "error" : "warning",
3033
3018
  confidence: 0.75,
3034
3019
  message: `${realPhantom.length} phantom dependencies (declared but unused): ${realPhantom.slice(0, 5).join(", ")}${realPhantom.length > 5 ? "..." : ""}`,
3035
3020
  locations: realPhantom.map((p) => ({ file: "package.json" })),
3036
3021
  tags: ["deps", "phantom", "js"]
3037
- });
3038
- }
3039
- const ALIAS_PATTERNS = [
3040
- /^#/,
3041
- /^virtual:/,
3042
- /^vite\//,
3043
- /^next\//,
3044
- /^\$/,
3045
- /^server-only$/,
3046
- /^client-only$/
3047
- ];
3022
+ }];
3023
+ }
3024
+ return [];
3025
+ }
3026
+ var ALIAS_PATTERNS = [
3027
+ /^#/,
3028
+ /^virtual:/,
3029
+ /^vite\//,
3030
+ /^next\//,
3031
+ /^\$/,
3032
+ /^server-only$/,
3033
+ /^client-only$/
3034
+ ];
3035
+ function detectMissingDeps(declared, imported, isMonorepo, importLocations) {
3048
3036
  const missing = [...imported].filter((i) => {
3049
3037
  if (declared.has(i)) return false;
3050
3038
  if (ALIAS_PATTERNS.some((p) => p.test(i))) return false;
3051
3039
  return true;
3052
3040
  });
3053
- const missingConfidence = isMonorepo || hasWorkspaces ? 0.4 : 0.75;
3054
- const missingSeverity = isMonorepo || hasWorkspaces ? "warning" : "error";
3041
+ const missingConfidence = isMonorepo ? 0.4 : 0.75;
3042
+ const missingSeverity = isMonorepo ? "warning" : "error";
3055
3043
  if (missing.length > 0) {
3056
- findings.push({
3044
+ return [{
3057
3045
  analyzerId: "dependencies",
3058
3046
  severity: missingSeverity,
3059
3047
  confidence: missingConfidence,
3060
- message: `${missing.length} packages imported but not in package.json: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? "..." : ""}${isMonorepo || hasWorkspaces ? " (may be workspace packages)" : ""}`,
3048
+ message: `${missing.length} packages imported but not in package.json: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? "..." : ""}${isMonorepo ? " (may be workspace packages)" : ""}`,
3061
3049
  locations: missing.slice(0, 5).flatMap((p) => importLocations.get(p) ?? []),
3062
3050
  tags: ["deps", "missing", "js"]
3063
- });
3051
+ }];
3064
3052
  }
3053
+ return [];
3054
+ }
3055
+ function analyzeJsDeps(ctx) {
3056
+ const findings = [];
3057
+ const pkg = ctx.packageJson;
3058
+ const declared = /* @__PURE__ */ new Set([
3059
+ ...Object.keys(pkg.dependencies ?? {}),
3060
+ ...Object.keys(pkg.devDependencies ?? {}),
3061
+ ...Object.keys(pkg.peerDependencies ?? {}),
3062
+ ...Object.keys(pkg.optionalDependencies ?? {})
3063
+ ]);
3064
+ const isMonorepo = [
3065
+ ...Object.values(pkg.dependencies ?? {}),
3066
+ ...Object.values(pkg.devDependencies ?? {})
3067
+ ].some(
3068
+ (v) => typeof v === "string" && (v.startsWith("workspace:") || v === "*")
3069
+ );
3070
+ const hasWorkspaces = !!pkg.workspaces;
3071
+ const jsFiles = ctx.files.filter(
3072
+ (f) => (f.language === "javascript" || f.language === "typescript") && !FIXTURE_PATH_PATTERN.test(f.relativePath)
3073
+ );
3074
+ const { imported, importLocations } = collectImportedPackages(jsFiles);
3075
+ findings.push(...detectPhantomDeps(declared, imported, DEV_TOOL_PATTERNS));
3076
+ findings.push(...detectMissingDeps(declared, imported, isMonorepo || hasWorkspaces, importLocations));
3065
3077
  return findings;
3066
3078
  }
3067
3079
  function analyzeGoDeps(ctx) {
@@ -4392,43 +4404,24 @@ function detectLongFunctions(ctx) {
4392
4404
  }
4393
4405
  return findings;
4394
4406
  }
4395
- function detectLowDocumentation(ctx) {
4396
- const findings = [];
4397
- const underdocumented = [];
4398
- for (const file of ctx.files) {
4399
- if (file.lineCount < 100) continue;
4400
- const lines = file.content.split("\n");
4401
- let commentLines = 0;
4402
- for (const line of lines) {
4403
- const trimmed = line.trim();
4404
- if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("///") || trimmed.startsWith('"""')) {
4405
- commentLines++;
4406
- }
4407
- }
4408
- const ratio = commentLines / file.lineCount;
4409
- if (ratio < 0.05) {
4410
- underdocumented.push({
4411
- file: file.relativePath,
4412
- lines: file.lineCount,
4413
- commentRatio: Math.round(ratio * 100)
4414
- });
4407
+ function countFileCommentDensity(file) {
4408
+ const lines = file.content.split("\n");
4409
+ let commentLines = 0;
4410
+ for (const line of lines) {
4411
+ const trimmed = line.trim();
4412
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("///") || trimmed.startsWith('"""')) {
4413
+ commentLines++;
4415
4414
  }
4416
4415
  }
4417
- if (underdocumented.length > 2) {
4418
- findings.push({
4419
- analyzerId: "intent-clarity",
4420
- severity: underdocumented.length > 5 ? "warning" : "info",
4421
- confidence: 0.6,
4422
- message: `${underdocumented.length} files over 100 lines have <5% comment density \u2014 intent may be unclear to maintainers`,
4423
- locations: underdocumented.slice(0, 10).map((f) => ({
4424
- file: f.file,
4425
- snippet: `${f.lines} lines, ${f.commentRatio}% comments`
4426
- })),
4427
- tags: ["intent", "documentation"]
4428
- });
4429
- }
4416
+ return {
4417
+ lines: file.lineCount,
4418
+ commentLines,
4419
+ density: commentLines / file.lineCount
4420
+ };
4421
+ }
4422
+ function findUndocumentedExports(files) {
4430
4423
  const undocumentedExports = [];
4431
- for (const file of ctx.files) {
4424
+ for (const file of files) {
4432
4425
  if (!file.language) continue;
4433
4426
  const lines = file.content.split("\n");
4434
4427
  for (let i = 0; i < lines.length; i++) {
@@ -4452,6 +4445,36 @@ function detectLowDocumentation(ctx) {
4452
4445
  }
4453
4446
  }
4454
4447
  }
4448
+ return undocumentedExports;
4449
+ }
4450
+ function detectLowDocumentation(ctx) {
4451
+ const findings = [];
4452
+ const underdocumented = [];
4453
+ for (const file of ctx.files) {
4454
+ if (file.lineCount < 100) continue;
4455
+ const { density } = countFileCommentDensity(file);
4456
+ if (density < 0.05) {
4457
+ underdocumented.push({
4458
+ file: file.relativePath,
4459
+ lines: file.lineCount,
4460
+ commentRatio: Math.round(density * 100)
4461
+ });
4462
+ }
4463
+ }
4464
+ if (underdocumented.length > 2) {
4465
+ findings.push({
4466
+ analyzerId: "intent-clarity",
4467
+ severity: underdocumented.length > 5 ? "warning" : "info",
4468
+ confidence: 0.6,
4469
+ message: `${underdocumented.length} files over 100 lines have <5% comment density \u2014 intent may be unclear to maintainers`,
4470
+ locations: underdocumented.slice(0, 10).map((f) => ({
4471
+ file: f.file,
4472
+ snippet: `${f.lines} lines, ${f.commentRatio}% comments`
4473
+ })),
4474
+ tags: ["intent", "documentation"]
4475
+ });
4476
+ }
4477
+ const undocumentedExports = findUndocumentedExports(ctx.files);
4455
4478
  if (undocumentedExports.length > 10) {
4456
4479
  const ratio = ctx.files.length > 0 ? Math.round(undocumentedExports.length / ctx.files.length * 10) / 10 : 0;
4457
4480
  findings.push({
@@ -4538,8 +4561,7 @@ var deadCodeAnalyzer = {
4538
4561
  return findings;
4539
4562
  }
4540
4563
  };
4541
- function analyzeJsDeadExports(files) {
4542
- const findings = [];
4564
+ function buildExportMap(files) {
4543
4565
  const exports = [];
4544
4566
  for (const file of files) {
4545
4567
  if (isEntryPoint(file.relativePath)) continue;
@@ -4586,6 +4608,9 @@ function analyzeJsDeadExports(files) {
4586
4608
  }
4587
4609
  }
4588
4610
  }
4611
+ return exports;
4612
+ }
4613
+ function buildImportSet(files) {
4589
4614
  const importedNames = /* @__PURE__ */ new Set();
4590
4615
  for (const file of files) {
4591
4616
  for (const pattern of IMPORT_PATTERNS_JS) {
@@ -4604,10 +4629,19 @@ function analyzeJsDeadExports(files) {
4604
4629
  }
4605
4630
  }
4606
4631
  }
4607
- const allContent = files.map((f) => f.content).join("\n");
4608
- const deadExports = exports.filter(
4632
+ return importedNames;
4633
+ }
4634
+ function identifyDeadExports(exports, importedNames, allContent) {
4635
+ return exports.filter(
4609
4636
  (e) => !importedNames.has(e.name) && countOccurrences(allContent, e.name) <= 1
4610
4637
  );
4638
+ }
4639
+ function analyzeJsDeadExports(files) {
4640
+ const findings = [];
4641
+ const exports = buildExportMap(files);
4642
+ const importedNames = buildImportSet(files);
4643
+ const allContent = files.map((f) => f.content).join("\n");
4644
+ const deadExports = identifyDeadExports(exports, importedNames, allContent);
4611
4645
  if (deadExports.length > 3) {
4612
4646
  findings.push({
4613
4647
  analyzerId: "dead-code",
@@ -4749,19 +4783,13 @@ var languageSpecificAnalyzer = {
4749
4783
  return findings;
4750
4784
  }
4751
4785
  };
4752
- function analyzeGo(files) {
4753
- const findings = [];
4754
- let uncheckedErrors = 0;
4755
- const uncheckedLocations = [];
4756
- let nakedGoroutines = 0;
4757
- const goroutineLocations = [];
4758
- let unsafeMutex = 0;
4759
- const mutexLocations = [];
4786
+ function detectGoUncheckedErrors(files) {
4787
+ let count = 0;
4788
+ const locations = [];
4760
4789
  for (const file of files) {
4761
4790
  const lines = file.content.split("\n");
4762
4791
  for (let i = 0; i < lines.length; i++) {
4763
- const line = lines[i];
4764
- const trimmed = line.trim();
4792
+ const trimmed = lines[i].trim();
4765
4793
  if (/\berr\s*[:=]/.test(trimmed) && !trimmed.startsWith("//")) {
4766
4794
  let nextLine = "";
4767
4795
  for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
@@ -4772,57 +4800,86 @@ function analyzeGo(files) {
4772
4800
  }
4773
4801
  }
4774
4802
  if (nextLine && !nextLine.includes("err") && !nextLine.startsWith("return")) {
4775
- uncheckedErrors++;
4776
- uncheckedLocations.push({
4803
+ count++;
4804
+ locations.push({
4777
4805
  file: file.relativePath,
4778
4806
  line: i + 1,
4779
4807
  snippet: trimmed.slice(0, 80)
4780
4808
  });
4781
4809
  }
4782
4810
  }
4811
+ }
4812
+ }
4813
+ return { count, locations };
4814
+ }
4815
+ function detectGoNakedGoroutines(files) {
4816
+ let count = 0;
4817
+ const locations = [];
4818
+ for (const file of files) {
4819
+ const lines = file.content.split("\n");
4820
+ for (let i = 0; i < lines.length; i++) {
4821
+ const line = lines[i];
4783
4822
  if (/^\s*go\s+func\s*\(/.test(line) || /^\s*go\s+\w+\s*\(/.test(line)) {
4784
4823
  const nearby = lines.slice(Math.max(0, i - 2), i + 3).join(" ");
4785
4824
  if (!/\bctx\b/.test(nearby) && !/context\./.test(nearby)) {
4786
- nakedGoroutines++;
4787
- goroutineLocations.push({ file: file.relativePath, line: i + 1 });
4825
+ count++;
4826
+ locations.push({ file: file.relativePath, line: i + 1 });
4788
4827
  }
4789
4828
  }
4829
+ }
4830
+ }
4831
+ return { count, locations };
4832
+ }
4833
+ function detectGoUnsafeMutex(files) {
4834
+ let count = 0;
4835
+ const locations = [];
4836
+ for (const file of files) {
4837
+ const lines = file.content.split("\n");
4838
+ for (let i = 0; i < lines.length; i++) {
4839
+ const trimmed = lines[i].trim();
4790
4840
  if (/\.Lock\(\)/.test(trimmed)) {
4791
4841
  const nextLines = lines.slice(i + 1, i + 4).join(" ");
4792
4842
  if (!/defer\s+.*\.Unlock\(\)/.test(nextLines) && !/\.Unlock\(\)/.test(trimmed)) {
4793
- unsafeMutex++;
4794
- mutexLocations.push({ file: file.relativePath, line: i + 1 });
4843
+ count++;
4844
+ locations.push({ file: file.relativePath, line: i + 1 });
4795
4845
  }
4796
4846
  }
4797
4847
  }
4798
4848
  }
4799
- if (uncheckedErrors > 0) {
4849
+ return { count, locations };
4850
+ }
4851
+ function analyzeGo(files) {
4852
+ const findings = [];
4853
+ const uncheckedErrors = detectGoUncheckedErrors(files);
4854
+ const nakedGoroutines = detectGoNakedGoroutines(files);
4855
+ const unsafeMutex = detectGoUnsafeMutex(files);
4856
+ if (uncheckedErrors.count > 0) {
4800
4857
  findings.push({
4801
4858
  analyzerId: "language-specific",
4802
- severity: uncheckedErrors > 10 ? "error" : "warning",
4859
+ severity: uncheckedErrors.count > 10 ? "error" : "warning",
4803
4860
  confidence: 0.7,
4804
- message: `${uncheckedErrors} potentially unchecked errors in Go code`,
4805
- locations: uncheckedLocations.slice(0, 10),
4861
+ message: `${uncheckedErrors.count} potentially unchecked errors in Go code`,
4862
+ locations: uncheckedErrors.locations.slice(0, 10),
4806
4863
  tags: ["go", "error-handling", "unchecked-error"]
4807
4864
  });
4808
4865
  }
4809
- if (nakedGoroutines > 0) {
4866
+ if (nakedGoroutines.count > 0) {
4810
4867
  findings.push({
4811
4868
  analyzerId: "language-specific",
4812
4869
  severity: "warning",
4813
4870
  confidence: 0.6,
4814
- message: `${nakedGoroutines} goroutines launched without context \u2014 potential leak risk`,
4815
- locations: goroutineLocations.slice(0, 10),
4871
+ message: `${nakedGoroutines.count} goroutines launched without context \u2014 potential leak risk`,
4872
+ locations: nakedGoroutines.locations.slice(0, 10),
4816
4873
  tags: ["go", "goroutine", "leak"]
4817
4874
  });
4818
4875
  }
4819
- if (unsafeMutex > 0) {
4876
+ if (unsafeMutex.count > 0) {
4820
4877
  findings.push({
4821
4878
  analyzerId: "language-specific",
4822
4879
  severity: "warning",
4823
4880
  confidence: 0.75,
4824
- message: `${unsafeMutex} mutex locks without defer Unlock \u2014 risk of deadlock`,
4825
- locations: mutexLocations.slice(0, 10),
4881
+ message: `${unsafeMutex.count} mutex locks without defer Unlock \u2014 risk of deadlock`,
4882
+ locations: unsafeMutex.locations.slice(0, 10),
4826
4883
  tags: ["go", "mutex", "concurrency"]
4827
4884
  });
4828
4885
  }
@@ -5069,27 +5126,35 @@ function buildProfile(file) {
5069
5126
  if (dataAccess.length === 0 && errorHandling.length === 0) return null;
5070
5127
  return { file: file.path, language: file.language, dataAccess, errorHandling, config, di };
5071
5128
  }
5072
- function analyzePatternDistribution(profiles, patternNames) {
5129
+ function detectFilePattern(patterns) {
5130
+ const fileCounts = /* @__PURE__ */ new Map();
5131
+ for (const { pattern } of patterns) {
5132
+ fileCounts.set(pattern, (fileCounts.get(pattern) ?? 0) + 1);
5133
+ }
5134
+ let primaryPattern = null;
5135
+ let maxCount = 0;
5136
+ for (const [pat, count] of fileCounts) {
5137
+ if (count > maxCount) {
5138
+ maxCount = count;
5139
+ primaryPattern = pat;
5140
+ }
5141
+ }
5142
+ return primaryPattern;
5143
+ }
5144
+ function buildPatternDistribution(profiles) {
5073
5145
  const counts = /* @__PURE__ */ new Map();
5074
5146
  for (const p of profiles) {
5075
- const fileCounts = /* @__PURE__ */ new Map();
5076
- for (const { pattern } of p.patterns) {
5077
- fileCounts.set(pattern, (fileCounts.get(pattern) ?? 0) + 1);
5078
- }
5079
- let primaryPattern = null;
5080
- let maxCount = 0;
5081
- for (const [pat, count] of fileCounts) {
5082
- if (count > maxCount) {
5083
- maxCount = count;
5084
- primaryPattern = pat;
5085
- }
5086
- }
5147
+ const primaryPattern = detectFilePattern(p.patterns);
5087
5148
  if (!primaryPattern) continue;
5088
5149
  if (!counts.has(primaryPattern)) counts.set(primaryPattern, { count: 0, files: [] });
5089
5150
  const entry = counts.get(primaryPattern);
5090
5151
  entry.count++;
5091
5152
  entry.files.push(p.file);
5092
5153
  }
5154
+ return counts;
5155
+ }
5156
+ function analyzePatternDistribution(profiles, patternNames) {
5157
+ const counts = buildPatternDistribution(profiles);
5093
5158
  if (counts.size < 2) return null;
5094
5159
  let dominant = null;
5095
5160
  let dominantCount = 0;
@@ -5260,29 +5325,40 @@ function extractSymbols(file) {
5260
5325
  }
5261
5326
  return symbols;
5262
5327
  }
5263
- function analyzeFileNaming(files) {
5264
- const fileNameConventions = /* @__PURE__ */ new Map();
5265
- for (const file of files) {
5266
- const basename2 = file.path.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "";
5267
- if (basename2.length <= 1) continue;
5268
- if (/(?:test|spec|config|setup|__)/i.test(basename2)) continue;
5269
- const conv = classifyName(basename2);
5270
- if (!conv) continue;
5271
- if (!fileNameConventions.has(conv)) fileNameConventions.set(conv, []);
5272
- fileNameConventions.get(conv).push(file.path);
5273
- }
5274
- if (fileNameConventions.size < 2) return null;
5328
+ function classifyBaseName(filePath) {
5329
+ const basename2 = filePath.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "";
5330
+ if (basename2.length <= 1) return null;
5331
+ if (/(?:test|spec|config|setup|__)/i.test(basename2)) return null;
5332
+ const convention = classifyName(basename2);
5333
+ if (!convention) return null;
5334
+ return { basename: basename2, convention };
5335
+ }
5336
+ function findDominantConvention(fileNameConventions) {
5275
5337
  let dominant = null;
5276
5338
  let maxCount = 0;
5277
5339
  let totalFiles = 0;
5278
- for (const [conv, files2] of fileNameConventions) {
5279
- totalFiles += files2.length;
5280
- if (files2.length > maxCount) {
5281
- maxCount = files2.length;
5340
+ for (const [conv, files] of fileNameConventions) {
5341
+ totalFiles += files.length;
5342
+ if (files.length > maxCount) {
5343
+ maxCount = files.length;
5282
5344
  dominant = conv;
5283
5345
  }
5284
5346
  }
5285
5347
  if (!dominant || maxCount === totalFiles) return null;
5348
+ return { dominant, maxCount, totalFiles };
5349
+ }
5350
+ function analyzeFileNaming(files) {
5351
+ const fileNameConventions = /* @__PURE__ */ new Map();
5352
+ for (const file of files) {
5353
+ const classified = classifyBaseName(file.path);
5354
+ if (!classified) continue;
5355
+ if (!fileNameConventions.has(classified.convention)) fileNameConventions.set(classified.convention, []);
5356
+ fileNameConventions.get(classified.convention).push(file.path);
5357
+ }
5358
+ if (fileNameConventions.size < 2) return null;
5359
+ const result = findDominantConvention(fileNameConventions);
5360
+ if (!result) return null;
5361
+ const { dominant, maxCount, totalFiles } = result;
5286
5362
  const deviating = [];
5287
5363
  for (const [conv, filePaths] of fileNameConventions) {
5288
5364
  if (conv === dominant) continue;
@@ -5848,131 +5924,147 @@ function buildUsageGraph(files) {
5848
5924
  }
5849
5925
  return usage;
5850
5926
  }
5927
+ function isReferencedExternally(name, usage, currentFile, allRoutes) {
5928
+ const references = usage.get(name);
5929
+ const usedExternally = references && [...references].some((f) => f !== currentFile);
5930
+ const isRouted = allRoutes.some(
5931
+ (r) => r.handlerName.includes(name) || r.handlerName.endsWith("." + name)
5932
+ );
5933
+ return !!(usedExternally || isRouted);
5934
+ }
5935
+ function detectScaffoldingPattern(filePath, crudFunctions, usage, allRoutes) {
5936
+ const usedFunctions = [];
5937
+ const unusedFunctions = [];
5938
+ for (const fn of crudFunctions) {
5939
+ if (isReferencedExternally(fn.name, usage, filePath, allRoutes)) {
5940
+ usedFunctions.push(fn);
5941
+ } else {
5942
+ unusedFunctions.push(fn);
5943
+ }
5944
+ }
5945
+ if (unusedFunctions.length < 2 || usedFunctions.length < 1) return null;
5946
+ const usedTypes = usedFunctions.map((f) => f.crudType);
5947
+ const unusedTypes = unusedFunctions.map((f) => f.crudType);
5948
+ const isScaffoldingLike = usedTypes.some((t) => t === "read" || t === "list") && unusedTypes.some((t) => t === "create" || t === "update" || t === "delete");
5949
+ return {
5950
+ detector: "phantom_scaffolding",
5951
+ driftCategory: "phantom_scaffolding",
5952
+ severity: isScaffoldingLike ? "warning" : "info",
5953
+ confidence: isScaffoldingLike ? 0.8 : 0.6,
5954
+ finding: `CRUD scaffolding detected in ${filePath} \u2014 ${usedFunctions.length} functions used, ${unusedFunctions.length} appear unused`,
5955
+ dominantPattern: "used CRUD operations",
5956
+ dominantCount: usedFunctions.length,
5957
+ totalRelevantFiles: usedFunctions.length + unusedFunctions.length,
5958
+ consistencyScore: Math.round(usedFunctions.length / (usedFunctions.length + unusedFunctions.length) * 100),
5959
+ deviatingFiles: [{
5960
+ path: filePath,
5961
+ detectedPattern: `unused: ${unusedFunctions.map((f) => f.name).join(", ")}`,
5962
+ evidence: unusedFunctions.slice(0, 5).map((f) => ({
5963
+ line: f.line,
5964
+ code: `${f.name} (${f.crudType}) \u2014 defined but not routed or imported`
5965
+ }))
5966
+ }],
5967
+ recommendation: `${unusedFunctions.map((f) => f.name).join(", ")} are defined in ${filePath.split("/").pop()} but never registered in routes or called externally. This looks like AI-generated CRUD scaffolding \u2014 remove unused handlers or wire them to routes.`
5968
+ };
5969
+ }
5970
+ function detectUnroutedHandlers(allExports, allRoutes, usage) {
5971
+ const findings = [];
5972
+ const handlerFiles = /* @__PURE__ */ new Map();
5973
+ for (const exp of allExports) {
5974
+ if (exp.crudType === "other") continue;
5975
+ if (!handlerFiles.has(exp.file)) handlerFiles.set(exp.file, []);
5976
+ handlerFiles.get(exp.file).push(exp);
5977
+ }
5978
+ for (const [filePath, functions] of handlerFiles) {
5979
+ const crudFunctions = functions.filter((f) => f.crudType !== "other");
5980
+ if (crudFunctions.length < 2) continue;
5981
+ const finding = detectScaffoldingPattern(filePath, crudFunctions, usage, allRoutes);
5982
+ if (finding) findings.push(finding);
5983
+ }
5984
+ return findings;
5985
+ }
5986
+ function detectUnusedTypeScaffolding(files, usage) {
5987
+ const findings = [];
5988
+ const typeDefinitions = [];
5989
+ const goMethodReceivers = /* @__PURE__ */ new Set();
5990
+ for (const file of files) {
5991
+ if (file.language !== "go") continue;
5992
+ const receiverPattern = /func\s+\(\s*\w+\s+\*?(\w+)\s*\)/g;
5993
+ let m;
5994
+ while ((m = receiverPattern.exec(file.content)) !== null) {
5995
+ goMethodReceivers.add(m[1]);
5996
+ }
5997
+ }
5998
+ for (const file of files) {
5999
+ if (!file.language) continue;
6000
+ const lines = file.content.split("\n");
6001
+ for (let i = 0; i < lines.length; i++) {
6002
+ const line = lines[i];
6003
+ let typeName = null;
6004
+ if (file.language === "go") {
6005
+ const m = line.match(/^type\s+([A-Z]\w+)\s+struct\b/);
6006
+ if (m) typeName = m[1];
6007
+ } else if (file.language === "javascript" || file.language === "typescript") {
6008
+ const m = line.match(/export\s+(?:interface|type|class)\s+(\w+)/);
6009
+ if (m) typeName = m[1];
6010
+ }
6011
+ if (!typeName) continue;
6012
+ if (file.language === "go" && goMethodReceivers.has(typeName)) continue;
6013
+ if (/(?:Request|Response|Params|Config|Options|Input|Output|Payload|Body|DTO)$/i.test(typeName)) continue;
6014
+ typeDefinitions.push({ name: typeName, file: file.path, line: i + 1 });
6015
+ }
6016
+ }
6017
+ const unusedTypesByFile = /* @__PURE__ */ new Map();
6018
+ for (const td of typeDefinitions) {
6019
+ const refs = usage.get(td.name);
6020
+ const usedExternally = refs && [...refs].some((f) => f !== td.file);
6021
+ if (!usedExternally) {
6022
+ if (!unusedTypesByFile.has(td.file)) unusedTypesByFile.set(td.file, []);
6023
+ unusedTypesByFile.get(td.file).push(td);
6024
+ }
6025
+ }
6026
+ for (const [filePath, unusedTypes] of unusedTypesByFile) {
6027
+ if (unusedTypes.length < 3) continue;
6028
+ findings.push({
6029
+ detector: "phantom_scaffolding",
6030
+ subCategory: "unused_types",
6031
+ driftCategory: "phantom_scaffolding",
6032
+ severity: "info",
6033
+ confidence: 0.55,
6034
+ finding: `${unusedTypes.length} types/structs in ${filePath} appear unused outside their file`,
6035
+ dominantPattern: "used types",
6036
+ dominantCount: typeDefinitions.length - unusedTypes.length,
6037
+ totalRelevantFiles: typeDefinitions.length,
6038
+ consistencyScore: Math.round((typeDefinitions.length - unusedTypes.length) / typeDefinitions.length * 100),
6039
+ deviatingFiles: [{
6040
+ path: filePath,
6041
+ detectedPattern: `unused types: ${unusedTypes.map((t) => t.name).join(", ")}`,
6042
+ evidence: unusedTypes.slice(0, 5).map((t) => ({
6043
+ line: t.line,
6044
+ code: `type ${t.name} \u2014 defined but never imported`
6045
+ }))
6046
+ }],
6047
+ recommendation: `${unusedTypes.length} types in ${filePath.split("/").pop()} are never used outside their file. They may be AI-generated scaffolding that was never needed.`
6048
+ });
6049
+ }
6050
+ return findings;
6051
+ }
5851
6052
  var phantomScaffolding = {
5852
6053
  id: "phantom-scaffolding",
5853
6054
  name: "Phantom Scaffolding",
5854
6055
  category: "phantom_scaffolding",
5855
6056
  detect(ctx) {
5856
- const findings = [];
5857
6057
  const allExports = [];
5858
6058
  const allRoutes = [];
5859
6059
  for (const file of ctx.files) {
5860
6060
  allExports.push(...extractExportedFunctions(file));
5861
6061
  allRoutes.push(...extractRouteRegistrations(file));
5862
6062
  }
5863
- if (allExports.length < 3) return findings;
6063
+ if (allExports.length < 3) return [];
5864
6064
  const usage = buildUsageGraph(ctx.files);
5865
- const handlerFiles = /* @__PURE__ */ new Map();
5866
- for (const exp of allExports) {
5867
- if (exp.crudType === "other") continue;
5868
- if (!handlerFiles.has(exp.file)) handlerFiles.set(exp.file, []);
5869
- handlerFiles.get(exp.file).push(exp);
5870
- }
5871
- for (const [filePath, functions] of handlerFiles) {
5872
- const crudFunctions = functions.filter((f) => f.crudType !== "other");
5873
- if (crudFunctions.length < 2) continue;
5874
- const usedFunctions = [];
5875
- const unusedFunctions = [];
5876
- for (const fn of crudFunctions) {
5877
- const references = usage.get(fn.name);
5878
- const usedExternally = references && [...references].some((f) => f !== filePath);
5879
- const isRouted = allRoutes.some(
5880
- (r) => r.handlerName.includes(fn.name) || r.handlerName.endsWith("." + fn.name)
5881
- );
5882
- if (usedExternally || isRouted) {
5883
- usedFunctions.push(fn);
5884
- } else {
5885
- unusedFunctions.push(fn);
5886
- }
5887
- }
5888
- if (unusedFunctions.length >= 2 && usedFunctions.length >= 1) {
5889
- const usedTypes = usedFunctions.map((f) => f.crudType);
5890
- const unusedTypes = unusedFunctions.map((f) => f.crudType);
5891
- const isScaffoldingPattern = usedTypes.some((t) => t === "read" || t === "list") && unusedTypes.some((t) => t === "create" || t === "update" || t === "delete");
5892
- findings.push({
5893
- detector: "phantom_scaffolding",
5894
- driftCategory: "phantom_scaffolding",
5895
- severity: isScaffoldingPattern ? "warning" : "info",
5896
- confidence: isScaffoldingPattern ? 0.8 : 0.6,
5897
- finding: `CRUD scaffolding detected in ${filePath} \u2014 ${usedFunctions.length} functions used, ${unusedFunctions.length} appear unused`,
5898
- dominantPattern: "used CRUD operations",
5899
- dominantCount: usedFunctions.length,
5900
- totalRelevantFiles: usedFunctions.length + unusedFunctions.length,
5901
- consistencyScore: Math.round(usedFunctions.length / (usedFunctions.length + unusedFunctions.length) * 100),
5902
- deviatingFiles: [{
5903
- path: filePath,
5904
- detectedPattern: `unused: ${unusedFunctions.map((f) => f.name).join(", ")}`,
5905
- evidence: unusedFunctions.slice(0, 5).map((f) => ({
5906
- line: f.line,
5907
- code: `${f.name} (${f.crudType}) \u2014 defined but not routed or imported`
5908
- }))
5909
- }],
5910
- recommendation: `${unusedFunctions.map((f) => f.name).join(", ")} are defined in ${filePath.split("/").pop()} but never registered in routes or called externally. This looks like AI-generated CRUD scaffolding \u2014 remove unused handlers or wire them to routes.`
5911
- });
5912
- }
5913
- }
5914
- const typeDefinitions = [];
5915
- const goMethodReceivers = /* @__PURE__ */ new Set();
5916
- for (const file of ctx.files) {
5917
- if (file.language !== "go") continue;
5918
- const receiverPattern = /func\s+\(\s*\w+\s+\*?(\w+)\s*\)/g;
5919
- let m;
5920
- while ((m = receiverPattern.exec(file.content)) !== null) {
5921
- goMethodReceivers.add(m[1]);
5922
- }
5923
- }
5924
- for (const file of ctx.files) {
5925
- if (!file.language) continue;
5926
- const lines = file.content.split("\n");
5927
- for (let i = 0; i < lines.length; i++) {
5928
- const line = lines[i];
5929
- let typeName = null;
5930
- if (file.language === "go") {
5931
- const m = line.match(/^type\s+([A-Z]\w+)\s+struct\b/);
5932
- if (m) typeName = m[1];
5933
- } else if (file.language === "javascript" || file.language === "typescript") {
5934
- const m = line.match(/export\s+(?:interface|type|class)\s+(\w+)/);
5935
- if (m) typeName = m[1];
5936
- }
5937
- if (!typeName) continue;
5938
- if (file.language === "go" && goMethodReceivers.has(typeName)) continue;
5939
- if (/(?:Request|Response|Params|Config|Options|Input|Output|Payload|Body|DTO)$/i.test(typeName)) continue;
5940
- typeDefinitions.push({ name: typeName, file: file.path, line: i + 1 });
5941
- }
5942
- }
5943
- const unusedTypesByFile = /* @__PURE__ */ new Map();
5944
- for (const td of typeDefinitions) {
5945
- const refs = usage.get(td.name);
5946
- const usedExternally = refs && [...refs].some((f) => f !== td.file);
5947
- if (!usedExternally) {
5948
- if (!unusedTypesByFile.has(td.file)) unusedTypesByFile.set(td.file, []);
5949
- unusedTypesByFile.get(td.file).push(td);
5950
- }
5951
- }
5952
- for (const [filePath, unusedTypes] of unusedTypesByFile) {
5953
- if (unusedTypes.length < 3) continue;
5954
- findings.push({
5955
- detector: "phantom_scaffolding",
5956
- subCategory: "unused_types",
5957
- driftCategory: "phantom_scaffolding",
5958
- severity: "info",
5959
- confidence: 0.55,
5960
- finding: `${unusedTypes.length} types/structs in ${filePath} appear unused outside their file`,
5961
- dominantPattern: "used types",
5962
- dominantCount: typeDefinitions.length - unusedTypes.length,
5963
- totalRelevantFiles: typeDefinitions.length,
5964
- consistencyScore: Math.round((typeDefinitions.length - unusedTypes.length) / typeDefinitions.length * 100),
5965
- deviatingFiles: [{
5966
- path: filePath,
5967
- detectedPattern: `unused types: ${unusedTypes.map((t) => t.name).join(", ")}`,
5968
- evidence: unusedTypes.slice(0, 5).map((t) => ({
5969
- line: t.line,
5970
- code: `type ${t.name} \u2014 defined but never imported`
5971
- }))
5972
- }],
5973
- recommendation: `${unusedTypes.length} types in ${filePath.split("/").pop()} are never used outside their file. They may be AI-generated scaffolding that was never needed.`
5974
- });
5975
- }
6065
+ const findings = [];
6066
+ findings.push(...detectUnroutedHandlers(allExports, allRoutes, usage));
6067
+ findings.push(...detectUnusedTypeScaffolding(ctx.files, usage));
5976
6068
  return findings;
5977
6069
  }
5978
6070
  };
@@ -8164,18 +8256,7 @@ async function createPortalSession(token, opts) {
8164
8256
  }
8165
8257
 
8166
8258
  // src/cli/commands/scan.ts
8167
- async function runScan(targetPath, options) {
8168
- const rootDir = resolve(targetPath);
8169
- try {
8170
- const info2 = await stat3(rootDir);
8171
- if (!info2.isDirectory()) {
8172
- console.error(`Error: ${rootDir} is not a directory`);
8173
- process.exit(1);
8174
- }
8175
- } catch {
8176
- console.error(`Error: ${rootDir} does not exist`);
8177
- process.exit(1);
8178
- }
8259
+ async function resolveAuthAndBanner(options) {
8179
8260
  let bearerToken = null;
8180
8261
  let apiUrl = options.apiUrl;
8181
8262
  if (options.deep) {
@@ -8222,10 +8303,11 @@ async function runScan(targetPath, options) {
8222
8303
  console.log("");
8223
8304
  }
8224
8305
  }
8225
- const startTime = Date.now();
8226
- const timings = {};
8306
+ return { bearerToken, apiUrl };
8307
+ }
8308
+ async function runAnalysisPipeline(rootDir, options, spinner) {
8227
8309
  const isTerminal = options.format === "terminal" && !options.json;
8228
- const spinner = isTerminal ? ora("Discovering files...").start() : null;
8310
+ const timings = {};
8229
8311
  const t0 = Date.now();
8230
8312
  const { ctx, warnings } = await buildAnalysisContext(rootDir);
8231
8313
  const includes = options.include ?? [];
@@ -8288,30 +8370,33 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8288
8370
  console.error(`[codedna] ${codeDnaResult.duplicateGroups.length} fingerprint duplicates, ${codeDnaResult.sequenceSimilarities.length} sequence matches, ${codeDnaResult.taintFlows.length} taint flows`);
8289
8371
  }
8290
8372
  }
8373
+ return { ctx, allFindings, driftResult, codeDnaResult, timings };
8374
+ }
8375
+ async function runDeepAnalysis(pipeline, options, bearerToken, apiUrl, spinner) {
8376
+ const { allFindings, ctx, codeDnaResult, driftResult } = pipeline;
8377
+ const timings = {};
8291
8378
  let mlMediumConfidence = [];
8292
- if (options.deep && bearerToken) {
8293
- const t5 = Date.now();
8294
- if (spinner) spinner.text = "Running AI deep analysis (may take ~30s on cold start)...";
8295
- try {
8296
- const { runMlAnalysis: runMlAnalysis2 } = await Promise.resolve().then(() => (init_ml_client(), ml_client_exports));
8297
- const mlResult = await runMlAnalysis2(ctx, codeDnaResult, allFindings, {
8298
- token: bearerToken,
8299
- apiUrl,
8300
- verbose: options.verbose,
8301
- driftFindings: driftResult.driftFindings,
8302
- projectName: options.projectName
8303
- });
8304
- allFindings.push(...mlResult.highConfidence);
8305
- mlMediumConfidence = mlResult.mediumConfidence;
8306
- if (options.verbose) {
8307
- console.error(`[deep] ${mlResult.highConfidence.length} high-confidence findings shipped, ${mlResult.mediumConfidence.length} sent to LLM, ${mlResult.droppedCount} dropped`);
8308
- }
8309
- } catch (err) {
8310
- console.error(chalk2.red(`[deep] AI analysis failed: ${err.message}`));
8311
- console.error(chalk2.dim(" The local scan will continue. Run `vibedrift doctor` if this persists."));
8379
+ const t5 = Date.now();
8380
+ if (spinner) spinner.text = "Running AI deep analysis (may take ~30s on cold start)...";
8381
+ try {
8382
+ const { runMlAnalysis: runMlAnalysis2 } = await Promise.resolve().then(() => (init_ml_client(), ml_client_exports));
8383
+ const mlResult = await runMlAnalysis2(ctx, codeDnaResult, allFindings, {
8384
+ token: bearerToken,
8385
+ apiUrl,
8386
+ verbose: options.verbose,
8387
+ driftFindings: driftResult.driftFindings,
8388
+ projectName: options.projectName
8389
+ });
8390
+ allFindings.push(...mlResult.highConfidence);
8391
+ mlMediumConfidence = mlResult.mediumConfidence;
8392
+ if (options.verbose) {
8393
+ console.error(`[deep] ${mlResult.highConfidence.length} high-confidence findings shipped, ${mlResult.mediumConfidence.length} sent to LLM, ${mlResult.droppedCount} dropped`);
8312
8394
  }
8313
- timings.deep = Date.now() - t5;
8395
+ } catch (err) {
8396
+ console.error(chalk2.red(`[deep] AI analysis failed: ${err.message}`));
8397
+ console.error(chalk2.dim(" The local scan will continue. Run `vibedrift doctor` if this persists."));
8314
8398
  }
8399
+ timings.deep = Date.now() - t5;
8315
8400
  const { deduplicateFindingsAcrossLayers: deduplicateFindingsAcrossLayers2 } = await Promise.resolve().then(() => (init_dedup(), dedup_exports));
8316
8401
  const dedupedCount = allFindings.length;
8317
8402
  const dedupedFindings = deduplicateFindingsAcrossLayers2(allFindings);
@@ -8320,6 +8405,12 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8320
8405
  }
8321
8406
  allFindings.length = 0;
8322
8407
  allFindings.push(...dedupedFindings);
8408
+ void mlMediumConfidence;
8409
+ return { timings };
8410
+ }
8411
+ async function buildScanResult(pipeline, options, startTime, timings, bearerToken, apiUrl, spinner) {
8412
+ const { ctx, allFindings, driftResult, codeDnaResult } = pipeline;
8413
+ const rootDir = ctx.rootDir;
8323
8414
  const previousScores = await loadPreviousScores(rootDir);
8324
8415
  if (spinner) spinner.text = "Computing scores...";
8325
8416
  const { scores, compositeScore, maxCompositeScore, perFileScores } = computeScores(
@@ -8369,7 +8460,10 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8369
8460
  if (options.verbose) console.error(`[summary] Failed: ${err.message}`);
8370
8461
  }
8371
8462
  }
8372
- await saveScanResult(rootDir, scores, compositeScore);
8463
+ return result;
8464
+ }
8465
+ async function logAndRender(result, options, bearerToken, apiUrl, rootDir, codeDnaResult) {
8466
+ const { findings: allFindings, compositeScore, maxCompositeScore, scanTimeMs } = result;
8373
8467
  if (bearerToken) {
8374
8468
  try {
8375
8469
  const { logScan: logScan2 } = await Promise.resolve().then(() => (init_log_scan(), log_scan_exports));
@@ -8405,8 +8499,8 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8405
8499
  payload: {
8406
8500
  project_hash: projectIdentity.hash,
8407
8501
  project_name: projectIdentity.name,
8408
- language: ctx.dominantLanguage ?? "unknown",
8409
- file_count: ctx.files.length,
8502
+ language: result.context.dominantLanguage ?? "unknown",
8503
+ file_count: result.context.files.length,
8410
8504
  function_count: codeDnaResult?.functions?.length ?? 0,
8411
8505
  finding_count: allFindings.length,
8412
8506
  score: compositeScore,
@@ -8479,6 +8573,29 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8479
8573
  process.exit(1);
8480
8574
  }
8481
8575
  }
8576
+ async function runScan(targetPath, options) {
8577
+ const rootDir = resolve(targetPath);
8578
+ try {
8579
+ const info2 = await stat3(rootDir);
8580
+ if (!info2.isDirectory()) {
8581
+ console.error(`Error: ${rootDir} is not a directory`);
8582
+ process.exit(1);
8583
+ }
8584
+ } catch {
8585
+ console.error(`Error: ${rootDir} does not exist`);
8586
+ process.exit(1);
8587
+ }
8588
+ const { bearerToken, apiUrl } = await resolveAuthAndBanner(options);
8589
+ const startTime = Date.now();
8590
+ const isTerminal = options.format === "terminal" && !options.json;
8591
+ const spinner = isTerminal ? ora("Discovering files...").start() : null;
8592
+ const pipeline = await runAnalysisPipeline(rootDir, options, spinner);
8593
+ const deepTimings = options.deep && bearerToken ? await runDeepAnalysis(pipeline, options, bearerToken, apiUrl, spinner) : { timings: {} };
8594
+ const timings = { ...pipeline.timings, ...deepTimings.timings };
8595
+ const result = await buildScanResult(pipeline, options, startTime, timings, bearerToken, apiUrl, spinner);
8596
+ await saveScanResult(rootDir, result.scores, result.compositeScore);
8597
+ await logAndRender(result, options, bearerToken, apiUrl, rootDir, pipeline.codeDnaResult);
8598
+ }
8482
8599
 
8483
8600
  // src/cli/commands/update.ts
8484
8601
  init_esm_shims();