@vibedrift/cli 0.4.4 → 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
@@ -24,6 +24,7 @@ __export(function_extractor_exports, {
24
24
  extractAllFunctions: () => extractAllFunctions,
25
25
  extractFunctionsFromFile: () => extractFunctionsFromFile,
26
26
  simpleHash: () => simpleHash,
27
+ toFunctionRef: () => toFunctionRef,
27
28
  tokenizeBody: () => tokenizeBody
28
29
  });
29
30
  function detectDomainCategory(name, body) {
@@ -149,6 +150,9 @@ function extractAllFunctions(files) {
149
150
  }
150
151
  return allFunctions;
151
152
  }
153
+ function toFunctionRef(fn) {
154
+ return { file: fn.file, relativePath: fn.relativePath, name: fn.name, line: fn.line };
155
+ }
152
156
  var init_function_extractor = __esm({
153
157
  "src/codedna/function-extractor.ts"() {
154
158
  "use strict";
@@ -254,12 +258,9 @@ function fnv1aHash(str) {
254
258
  const h2 = (hash2 >>> 0).toString(16).padStart(8, "0");
255
259
  return h1 + h2;
256
260
  }
257
- function toRef(fn) {
258
- return { file: fn.file, relativePath: fn.relativePath, name: fn.name, line: fn.line };
259
- }
260
261
  function computeSemanticFingerprints(functions) {
261
262
  return functions.map((fn) => ({
262
- functionRef: toRef(fn),
263
+ functionRef: toFunctionRef(fn),
263
264
  normalizedHash: fnv1aHash(normalizeBody(fn.rawBody, fn.language))
264
265
  }));
265
266
  }
@@ -321,6 +322,7 @@ var init_semantic_fingerprint = __esm({
321
322
  "src/codedna/semantic-fingerprint.ts"() {
322
323
  "use strict";
323
324
  init_esm_shims();
325
+ init_function_extractor();
324
326
  }
325
327
  });
326
328
 
@@ -366,9 +368,6 @@ function classifyLine(line, language) {
366
368
  }
367
369
  return null;
368
370
  }
369
- function toRef2(fn) {
370
- return { file: fn.file, relativePath: fn.relativePath, name: fn.name, line: fn.line };
371
- }
372
371
  function extractOperationSequences(functions) {
373
372
  return functions.map((fn) => {
374
373
  const lines = fn.rawBody.split("\n");
@@ -381,7 +380,7 @@ function extractOperationSequences(functions) {
381
380
  }
382
381
  }
383
382
  }
384
- return { functionRef: toRef2(fn), sequence };
383
+ return { functionRef: toFunctionRef(fn), sequence };
385
384
  });
386
385
  }
387
386
  function lcsLength(a, b) {
@@ -456,6 +455,7 @@ var init_operation_sequence = __esm({
456
455
  "src/codedna/operation-sequence.ts"() {
457
456
  "use strict";
458
457
  init_esm_shims();
458
+ init_function_extractor();
459
459
  }
460
460
  });
461
461
 
@@ -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
  }
@@ -4967,9 +5024,6 @@ var DRIFT_WEIGHTS = {
4967
5024
 
4968
5025
  // src/drift/architectural-contradiction.ts
4969
5026
  init_esm_shims();
4970
- function getLine(content, index) {
4971
- return content.slice(0, index).split("\n").length;
4972
- }
4973
5027
  function getLineContent(content, lineNum) {
4974
5028
  return (content.split("\n")[lineNum - 1] ?? "").trim();
4975
5029
  }
@@ -4978,7 +5032,7 @@ function extractEvidence(content, pattern, maxResults = 3) {
4978
5032
  const regex = new RegExp(pattern.source, pattern.flags);
4979
5033
  let match;
4980
5034
  while ((match = regex.exec(content)) !== null && evidence.length < maxResults) {
4981
- const line = getLine(content, match.index);
5035
+ const line = getLineNumber(content, match.index);
4982
5036
  evidence.push({ line, code: getLineContent(content, line) });
4983
5037
  }
4984
5038
  return evidence;
@@ -5072,27 +5126,35 @@ function buildProfile(file) {
5072
5126
  if (dataAccess.length === 0 && errorHandling.length === 0) return null;
5073
5127
  return { file: file.path, language: file.language, dataAccess, errorHandling, config, di };
5074
5128
  }
5075
- 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) {
5076
5145
  const counts = /* @__PURE__ */ new Map();
5077
5146
  for (const p of profiles) {
5078
- const fileCounts = /* @__PURE__ */ new Map();
5079
- for (const { pattern } of p.patterns) {
5080
- fileCounts.set(pattern, (fileCounts.get(pattern) ?? 0) + 1);
5081
- }
5082
- let primaryPattern = null;
5083
- let maxCount = 0;
5084
- for (const [pat, count] of fileCounts) {
5085
- if (count > maxCount) {
5086
- maxCount = count;
5087
- primaryPattern = pat;
5088
- }
5089
- }
5147
+ const primaryPattern = detectFilePattern(p.patterns);
5090
5148
  if (!primaryPattern) continue;
5091
5149
  if (!counts.has(primaryPattern)) counts.set(primaryPattern, { count: 0, files: [] });
5092
5150
  const entry = counts.get(primaryPattern);
5093
5151
  entry.count++;
5094
5152
  entry.files.push(p.file);
5095
5153
  }
5154
+ return counts;
5155
+ }
5156
+ function analyzePatternDistribution(profiles, patternNames) {
5157
+ const counts = buildPatternDistribution(profiles);
5096
5158
  if (counts.size < 2) return null;
5097
5159
  let dominant = null;
5098
5160
  let dominantCount = 0;
@@ -5263,29 +5325,40 @@ function extractSymbols(file) {
5263
5325
  }
5264
5326
  return symbols;
5265
5327
  }
5266
- function analyzeFileNaming(files) {
5267
- const fileNameConventions = /* @__PURE__ */ new Map();
5268
- for (const file of files) {
5269
- const basename2 = file.path.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "";
5270
- if (basename2.length <= 1) continue;
5271
- if (/(?:test|spec|config|setup|__)/i.test(basename2)) continue;
5272
- const conv = classifyName(basename2);
5273
- if (!conv) continue;
5274
- if (!fileNameConventions.has(conv)) fileNameConventions.set(conv, []);
5275
- fileNameConventions.get(conv).push(file.path);
5276
- }
5277
- 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) {
5278
5337
  let dominant = null;
5279
5338
  let maxCount = 0;
5280
5339
  let totalFiles = 0;
5281
- for (const [conv, files2] of fileNameConventions) {
5282
- totalFiles += files2.length;
5283
- if (files2.length > maxCount) {
5284
- maxCount = files2.length;
5340
+ for (const [conv, files] of fileNameConventions) {
5341
+ totalFiles += files.length;
5342
+ if (files.length > maxCount) {
5343
+ maxCount = files.length;
5285
5344
  dominant = conv;
5286
5345
  }
5287
5346
  }
5288
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;
5289
5362
  const deviating = [];
5290
5363
  for (const [conv, filePaths] of fileNameConventions) {
5291
5364
  if (conv === dominant) continue;
@@ -5851,131 +5924,147 @@ function buildUsageGraph(files) {
5851
5924
  }
5852
5925
  return usage;
5853
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
+ }
5854
6052
  var phantomScaffolding = {
5855
6053
  id: "phantom-scaffolding",
5856
6054
  name: "Phantom Scaffolding",
5857
6055
  category: "phantom_scaffolding",
5858
6056
  detect(ctx) {
5859
- const findings = [];
5860
6057
  const allExports = [];
5861
6058
  const allRoutes = [];
5862
6059
  for (const file of ctx.files) {
5863
6060
  allExports.push(...extractExportedFunctions(file));
5864
6061
  allRoutes.push(...extractRouteRegistrations(file));
5865
6062
  }
5866
- if (allExports.length < 3) return findings;
6063
+ if (allExports.length < 3) return [];
5867
6064
  const usage = buildUsageGraph(ctx.files);
5868
- const handlerFiles = /* @__PURE__ */ new Map();
5869
- for (const exp of allExports) {
5870
- if (exp.crudType === "other") continue;
5871
- if (!handlerFiles.has(exp.file)) handlerFiles.set(exp.file, []);
5872
- handlerFiles.get(exp.file).push(exp);
5873
- }
5874
- for (const [filePath, functions] of handlerFiles) {
5875
- const crudFunctions = functions.filter((f) => f.crudType !== "other");
5876
- if (crudFunctions.length < 2) continue;
5877
- const usedFunctions = [];
5878
- const unusedFunctions = [];
5879
- for (const fn of crudFunctions) {
5880
- const references = usage.get(fn.name);
5881
- const usedExternally = references && [...references].some((f) => f !== filePath);
5882
- const isRouted = allRoutes.some(
5883
- (r) => r.handlerName.includes(fn.name) || r.handlerName.endsWith("." + fn.name)
5884
- );
5885
- if (usedExternally || isRouted) {
5886
- usedFunctions.push(fn);
5887
- } else {
5888
- unusedFunctions.push(fn);
5889
- }
5890
- }
5891
- if (unusedFunctions.length >= 2 && usedFunctions.length >= 1) {
5892
- const usedTypes = usedFunctions.map((f) => f.crudType);
5893
- const unusedTypes = unusedFunctions.map((f) => f.crudType);
5894
- const isScaffoldingPattern = usedTypes.some((t) => t === "read" || t === "list") && unusedTypes.some((t) => t === "create" || t === "update" || t === "delete");
5895
- findings.push({
5896
- detector: "phantom_scaffolding",
5897
- driftCategory: "phantom_scaffolding",
5898
- severity: isScaffoldingPattern ? "warning" : "info",
5899
- confidence: isScaffoldingPattern ? 0.8 : 0.6,
5900
- finding: `CRUD scaffolding detected in ${filePath} \u2014 ${usedFunctions.length} functions used, ${unusedFunctions.length} appear unused`,
5901
- dominantPattern: "used CRUD operations",
5902
- dominantCount: usedFunctions.length,
5903
- totalRelevantFiles: usedFunctions.length + unusedFunctions.length,
5904
- consistencyScore: Math.round(usedFunctions.length / (usedFunctions.length + unusedFunctions.length) * 100),
5905
- deviatingFiles: [{
5906
- path: filePath,
5907
- detectedPattern: `unused: ${unusedFunctions.map((f) => f.name).join(", ")}`,
5908
- evidence: unusedFunctions.slice(0, 5).map((f) => ({
5909
- line: f.line,
5910
- code: `${f.name} (${f.crudType}) \u2014 defined but not routed or imported`
5911
- }))
5912
- }],
5913
- 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.`
5914
- });
5915
- }
5916
- }
5917
- const typeDefinitions = [];
5918
- const goMethodReceivers = /* @__PURE__ */ new Set();
5919
- for (const file of ctx.files) {
5920
- if (file.language !== "go") continue;
5921
- const receiverPattern = /func\s+\(\s*\w+\s+\*?(\w+)\s*\)/g;
5922
- let m;
5923
- while ((m = receiverPattern.exec(file.content)) !== null) {
5924
- goMethodReceivers.add(m[1]);
5925
- }
5926
- }
5927
- for (const file of ctx.files) {
5928
- if (!file.language) continue;
5929
- const lines = file.content.split("\n");
5930
- for (let i = 0; i < lines.length; i++) {
5931
- const line = lines[i];
5932
- let typeName = null;
5933
- if (file.language === "go") {
5934
- const m = line.match(/^type\s+([A-Z]\w+)\s+struct\b/);
5935
- if (m) typeName = m[1];
5936
- } else if (file.language === "javascript" || file.language === "typescript") {
5937
- const m = line.match(/export\s+(?:interface|type|class)\s+(\w+)/);
5938
- if (m) typeName = m[1];
5939
- }
5940
- if (!typeName) continue;
5941
- if (file.language === "go" && goMethodReceivers.has(typeName)) continue;
5942
- if (/(?:Request|Response|Params|Config|Options|Input|Output|Payload|Body|DTO)$/i.test(typeName)) continue;
5943
- typeDefinitions.push({ name: typeName, file: file.path, line: i + 1 });
5944
- }
5945
- }
5946
- const unusedTypesByFile = /* @__PURE__ */ new Map();
5947
- for (const td of typeDefinitions) {
5948
- const refs = usage.get(td.name);
5949
- const usedExternally = refs && [...refs].some((f) => f !== td.file);
5950
- if (!usedExternally) {
5951
- if (!unusedTypesByFile.has(td.file)) unusedTypesByFile.set(td.file, []);
5952
- unusedTypesByFile.get(td.file).push(td);
5953
- }
5954
- }
5955
- for (const [filePath, unusedTypes] of unusedTypesByFile) {
5956
- if (unusedTypes.length < 3) continue;
5957
- findings.push({
5958
- detector: "phantom_scaffolding",
5959
- subCategory: "unused_types",
5960
- driftCategory: "phantom_scaffolding",
5961
- severity: "info",
5962
- confidence: 0.55,
5963
- finding: `${unusedTypes.length} types/structs in ${filePath} appear unused outside their file`,
5964
- dominantPattern: "used types",
5965
- dominantCount: typeDefinitions.length - unusedTypes.length,
5966
- totalRelevantFiles: typeDefinitions.length,
5967
- consistencyScore: Math.round((typeDefinitions.length - unusedTypes.length) / typeDefinitions.length * 100),
5968
- deviatingFiles: [{
5969
- path: filePath,
5970
- detectedPattern: `unused types: ${unusedTypes.map((t) => t.name).join(", ")}`,
5971
- evidence: unusedTypes.slice(0, 5).map((t) => ({
5972
- line: t.line,
5973
- code: `type ${t.name} \u2014 defined but never imported`
5974
- }))
5975
- }],
5976
- recommendation: `${unusedTypes.length} types in ${filePath.split("/").pop()} are never used outside their file. They may be AI-generated scaffolding that was never needed.`
5977
- });
5978
- }
6065
+ const findings = [];
6066
+ findings.push(...detectUnroutedHandlers(allExports, allRoutes, usage));
6067
+ findings.push(...detectUnusedTypeScaffolding(ctx.files, usage));
5979
6068
  return findings;
5980
6069
  }
5981
6070
  };
@@ -8042,6 +8131,16 @@ function previewToken(token) {
8042
8131
  if (token.length <= 12) return token.slice(0, 4) + "\u2026";
8043
8132
  return token.slice(0, 12) + "\u2026";
8044
8133
  }
8134
+ function describeSource(source) {
8135
+ switch (source) {
8136
+ case "flag":
8137
+ return "command-line flag";
8138
+ case "env":
8139
+ return "VIBEDRIFT_TOKEN environment variable";
8140
+ case "config":
8141
+ return "~/.vibedrift/config.json";
8142
+ }
8143
+ }
8045
8144
 
8046
8145
  // src/auth/api.ts
8047
8146
  init_esm_shims();
@@ -8157,18 +8256,7 @@ async function createPortalSession(token, opts) {
8157
8256
  }
8158
8257
 
8159
8258
  // src/cli/commands/scan.ts
8160
- async function runScan(targetPath, options) {
8161
- const rootDir = resolve(targetPath);
8162
- try {
8163
- const info2 = await stat3(rootDir);
8164
- if (!info2.isDirectory()) {
8165
- console.error(`Error: ${rootDir} is not a directory`);
8166
- process.exit(1);
8167
- }
8168
- } catch {
8169
- console.error(`Error: ${rootDir} does not exist`);
8170
- process.exit(1);
8171
- }
8259
+ async function resolveAuthAndBanner(options) {
8172
8260
  let bearerToken = null;
8173
8261
  let apiUrl = options.apiUrl;
8174
8262
  if (options.deep) {
@@ -8215,10 +8303,11 @@ async function runScan(targetPath, options) {
8215
8303
  console.log("");
8216
8304
  }
8217
8305
  }
8218
- const startTime = Date.now();
8219
- const timings = {};
8306
+ return { bearerToken, apiUrl };
8307
+ }
8308
+ async function runAnalysisPipeline(rootDir, options, spinner) {
8220
8309
  const isTerminal = options.format === "terminal" && !options.json;
8221
- const spinner = isTerminal ? ora("Discovering files...").start() : null;
8310
+ const timings = {};
8222
8311
  const t0 = Date.now();
8223
8312
  const { ctx, warnings } = await buildAnalysisContext(rootDir);
8224
8313
  const includes = options.include ?? [];
@@ -8281,30 +8370,33 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8281
8370
  console.error(`[codedna] ${codeDnaResult.duplicateGroups.length} fingerprint duplicates, ${codeDnaResult.sequenceSimilarities.length} sequence matches, ${codeDnaResult.taintFlows.length} taint flows`);
8282
8371
  }
8283
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 = {};
8284
8378
  let mlMediumConfidence = [];
8285
- if (options.deep && bearerToken) {
8286
- const t5 = Date.now();
8287
- if (spinner) spinner.text = "Running AI deep analysis (may take ~30s on cold start)...";
8288
- try {
8289
- const { runMlAnalysis: runMlAnalysis2 } = await Promise.resolve().then(() => (init_ml_client(), ml_client_exports));
8290
- const mlResult = await runMlAnalysis2(ctx, codeDnaResult, allFindings, {
8291
- token: bearerToken,
8292
- apiUrl,
8293
- verbose: options.verbose,
8294
- driftFindings: driftResult.driftFindings,
8295
- projectName: options.projectName
8296
- });
8297
- allFindings.push(...mlResult.highConfidence);
8298
- mlMediumConfidence = mlResult.mediumConfidence;
8299
- if (options.verbose) {
8300
- console.error(`[deep] ${mlResult.highConfidence.length} high-confidence findings shipped, ${mlResult.mediumConfidence.length} sent to LLM, ${mlResult.droppedCount} dropped`);
8301
- }
8302
- } catch (err) {
8303
- console.error(chalk2.red(`[deep] AI analysis failed: ${err.message}`));
8304
- 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`);
8305
8394
  }
8306
- 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."));
8307
8398
  }
8399
+ timings.deep = Date.now() - t5;
8308
8400
  const { deduplicateFindingsAcrossLayers: deduplicateFindingsAcrossLayers2 } = await Promise.resolve().then(() => (init_dedup(), dedup_exports));
8309
8401
  const dedupedCount = allFindings.length;
8310
8402
  const dedupedFindings = deduplicateFindingsAcrossLayers2(allFindings);
@@ -8313,6 +8405,12 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8313
8405
  }
8314
8406
  allFindings.length = 0;
8315
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;
8316
8414
  const previousScores = await loadPreviousScores(rootDir);
8317
8415
  if (spinner) spinner.text = "Computing scores...";
8318
8416
  const { scores, compositeScore, maxCompositeScore, perFileScores } = computeScores(
@@ -8362,7 +8460,10 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8362
8460
  if (options.verbose) console.error(`[summary] Failed: ${err.message}`);
8363
8461
  }
8364
8462
  }
8365
- 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;
8366
8467
  if (bearerToken) {
8367
8468
  try {
8368
8469
  const { logScan: logScan2 } = await Promise.resolve().then(() => (init_log_scan(), log_scan_exports));
@@ -8398,8 +8499,8 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8398
8499
  payload: {
8399
8500
  project_hash: projectIdentity.hash,
8400
8501
  project_name: projectIdentity.name,
8401
- language: ctx.dominantLanguage ?? "unknown",
8402
- file_count: ctx.files.length,
8502
+ language: result.context.dominantLanguage ?? "unknown",
8503
+ file_count: result.context.files.length,
8403
8504
  function_count: codeDnaResult?.functions?.length ?? 0,
8404
8505
  finding_count: allFindings.length,
8405
8506
  score: compositeScore,
@@ -8472,6 +8573,29 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8472
8573
  process.exit(1);
8473
8574
  }
8474
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
+ }
8475
8599
 
8476
8600
  // src/cli/commands/update.ts
8477
8601
  init_esm_shims();
@@ -8840,16 +8964,6 @@ async function runStatus() {
8840
8964
  }
8841
8965
  console.log("");
8842
8966
  }
8843
- function describeSource(source) {
8844
- switch (source) {
8845
- case "flag":
8846
- return "command-line flag";
8847
- case "env":
8848
- return "VIBEDRIFT_TOKEN environment variable";
8849
- case "config":
8850
- return "~/.vibedrift/config.json";
8851
- }
8852
- }
8853
8967
 
8854
8968
  // src/cli/commands/usage.ts
8855
8969
  init_esm_shims();
@@ -9046,7 +9160,7 @@ async function runDoctor() {
9046
9160
  if (!resolved) {
9047
9161
  info("Login state", "not logged in");
9048
9162
  } else {
9049
- ok("Token source", describeSource2(resolved.source));
9163
+ ok("Token source", describeSource(resolved.source));
9050
9164
  ok("Token preview", previewToken(resolved.token));
9051
9165
  if (resolved.source === "config") {
9052
9166
  if (config.email) ok("Email", config.email);
@@ -9118,16 +9232,6 @@ function bad(value) {
9118
9232
  function info(label, value) {
9119
9233
  console.log(` ${chalk10.dim("\xB7")} ${label.padEnd(14)} ${chalk10.dim(value)}`);
9120
9234
  }
9121
- function describeSource2(source) {
9122
- switch (source) {
9123
- case "flag":
9124
- return "command-line flag";
9125
- case "env":
9126
- return "VIBEDRIFT_TOKEN environment variable";
9127
- case "config":
9128
- return "~/.vibedrift/config.json";
9129
- }
9130
- }
9131
9235
 
9132
9236
  // src/cli/commands/feedback.ts
9133
9237
  init_esm_shims();