@vibedrift/cli 0.5.3 → 0.5.5

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/README.md CHANGED
@@ -21,13 +21,13 @@ vibedrift ./my-project
21
21
 
22
22
  That's it. No signup, no API key, no config file. The default run produces an interactive HTML report and serves it on a local port.
23
23
 
24
- ### Free deep scan
24
+ ### Free deep scans
25
25
 
26
- Every account gets **1 free deep scan** on signup — no card required. Deep scans add Claude-powered AI analysis on top of the local scan (semantic duplicates, intent mismatches, anomaly detection):
26
+ Every account gets **3 free deep scans per month** — no card required. Deep scans add Claude-powered AI analysis on top of the local scan (semantic duplicates, intent mismatches, anomaly detection):
27
27
 
28
28
  ```bash
29
29
  vibedrift login # one-time browser sign-in
30
- vibedrift . --deep # uses your free credit (or Pro subscription)
30
+ vibedrift . --deep # uses one of your 3 monthly deep scans
31
31
  ```
32
32
 
33
33
  ---
@@ -41,12 +41,15 @@ vibedrift . --deep # uses your free credit (or Pro subscription)
41
41
  - **Dependency health** — phantom deps, missing deps, undocumented env vars
42
42
  - **Code quality** — cyclomatic complexity, dead code, TODO density, long functions, unclear naming
43
43
 
44
- ### Layer 1.5 — Cross-File Drift Detection (5 detectors)
45
- - **Architectural consistency** — data access patterns, error handling, DI, config approaches
44
+ ### Layer 1.5 — Cross-File Drift Detection (8 detectors)
45
+ - **Architectural consistency** — error handling, data access, DI, and config patterns across all source files
46
46
  - **Convention oscillation** — naming conventions, file naming across the whole project
47
47
  - **Security consistency** — auth middleware, input validation, rate limiting across routes
48
48
  - **Semantic duplication** — functions that do the same thing under different names in different files
49
- - **Phantom scaffolding** — CRUD endpoints the AI generated but never wired up
49
+ - **Phantom scaffolding** — exported types and CRUD endpoints that are never used
50
+ - **Import style** — relative paths vs path aliases, default vs named imports
51
+ - **Export style** — default exports vs named-only exports per file
52
+ - **Async patterns** — async/await vs .then() chains consistency
50
53
 
51
54
  ### Layer 1.7 — Code DNA Engine (5 modules, runs locally)
52
55
  - **Semantic fingerprinting** — normalizes function bodies and hashes them. Identical hashes = exact semantic duplicates at confidence 1.0
@@ -195,10 +198,10 @@ All scans (free and deep) are logged to your dashboard at [vibedrift.ai/dashboar
195
198
 
196
199
  | Tier | Price | What you get |
197
200
  |------|-------|-------------|
198
- | **Free** | $0 | Unlimited local scans + 1 free deep scan on signup |
199
- | **Per scan** | $5/scan | One-time deep scan credit (Claude AI + ML embeddings) |
200
- | **Pro** | $29/month | Unlimited deep scans + GitHub Action + dashboard |
201
- | **Team** | $99/month | Everything in Pro + team features + API access |
201
+ | **Free** | $0 | Unlimited local scans + 3 deep scans per month + dashboard |
202
+ | **Pro** | $15/month | Unlimited deep scans + private repo CI/CD + API access |
203
+ | **Team** | $30/seat/month | GitHub App (auto PR) + team dashboard + quality gates + SSO |
204
+ | **Enterprise** | Custom | Custom rules + compliance + self-hosted + SLA |
202
205
 
203
206
  See [vibedrift.ai/#pricing](https://vibedrift.ai/#pricing) for details.
204
207
 
package/dist/index.js CHANGED
@@ -5178,11 +5178,14 @@ init_esm_shims();
5178
5178
  // src/drift/types.ts
5179
5179
  init_esm_shims();
5180
5180
  var DRIFT_WEIGHTS = {
5181
- architectural_consistency: 25,
5182
- security_posture: 25,
5183
- semantic_duplication: 20,
5184
- naming_conventions: 15,
5185
- phantom_scaffolding: 15
5181
+ architectural_consistency: 16,
5182
+ security_posture: 14,
5183
+ semantic_duplication: 14,
5184
+ naming_conventions: 12,
5185
+ phantom_scaffolding: 12,
5186
+ import_style: 12,
5187
+ export_style: 10,
5188
+ async_patterns: 10
5186
5189
  };
5187
5190
 
5188
5191
  // src/drift/architectural-contradiction.ts
@@ -5200,9 +5203,6 @@ function extractEvidence(content, pattern, maxResults = 3) {
5200
5203
  }
5201
5204
  return evidence;
5202
5205
  }
5203
- function isHandlerOrController(path2) {
5204
- return /(?:handler|controller|route|endpoint|view|api|resource|page|action)/i.test(path2) && !/(?:repository|repo|store|dal|model|query|migration|test|spec|mock)/i.test(path2);
5205
- }
5206
5206
  function detectDataAccess(file) {
5207
5207
  const results = [];
5208
5208
  const c = file.content;
@@ -5220,7 +5220,7 @@ function detectDataAccess(file) {
5220
5220
  results.push({ pattern: "direct_db", evidence: dbEvidence });
5221
5221
  }
5222
5222
  const httpEvidence = extractEvidence(c, /\b(?:fetch|axios|http\.(?:Get|Post|Put)|requests\.(?:get|post))\s*\(/g);
5223
- if (httpEvidence.length > 0 && isHandlerOrController(file.path)) {
5223
+ if (httpEvidence.length > 0) {
5224
5224
  results.push({ pattern: "http_client", evidence: httpEvidence });
5225
5225
  }
5226
5226
  return results;
@@ -5280,13 +5280,19 @@ function detectDIPattern(file) {
5280
5280
  }
5281
5281
  return results;
5282
5282
  }
5283
+ function isAnalyzableSource(path2) {
5284
+ if (/(?:test|spec|mock|fixture|__test__|__mocks__|\.test\.|\.spec\.)/i.test(path2)) return false;
5285
+ if (/(?:\.config\.|\.d\.ts$|node_modules|dist\/|build\/)/i.test(path2)) return false;
5286
+ return true;
5287
+ }
5283
5288
  function buildProfile(file) {
5284
- if (!file.language || !isHandlerOrController(file.path)) return null;
5289
+ if (!file.language) return null;
5290
+ if (!isAnalyzableSource(file.path)) return null;
5285
5291
  const dataAccess = detectDataAccess(file);
5286
5292
  const errorHandling = detectErrorHandling(file);
5287
5293
  const config = detectConfigPattern(file);
5288
5294
  const di = detectDIPattern(file);
5289
- if (dataAccess.length === 0 && errorHandling.length === 0) return null;
5295
+ if (dataAccess.length === 0 && errorHandling.length === 0 && config.length === 0) return null;
5290
5296
  return { file: file.path, language: file.language, dataAccess, errorHandling, config, di };
5291
5297
  }
5292
5298
  function detectFilePattern(patterns) {
@@ -6285,6 +6291,341 @@ var phantomScaffolding = {
6285
6291
  }
6286
6292
  };
6287
6293
 
6294
+ // src/drift/import-consistency.ts
6295
+ init_esm_shims();
6296
+ function isSourceFile(path2) {
6297
+ if (/(?:test|spec|mock|fixture|__test__|__mocks__|\.test\.|\.spec\.)/i.test(path2)) return false;
6298
+ if (/(?:\.config\.|\.d\.ts$|node_modules|dist\/|build\/)/i.test(path2)) return false;
6299
+ return true;
6300
+ }
6301
+ function analyzeImports(file) {
6302
+ if (!file.language || !["javascript", "typescript"].includes(file.language)) return null;
6303
+ if (!isSourceFile(file.path)) return null;
6304
+ const lines = file.content.split("\n");
6305
+ let relativeCount = 0;
6306
+ let aliasCount = 0;
6307
+ let defaultCount = 0;
6308
+ let namedCount = 0;
6309
+ const evidence = [];
6310
+ for (let i = 0; i < lines.length; i++) {
6311
+ const line = lines[i].trim();
6312
+ const importMatch = line.match(/^import\s+(?!type\s)/);
6313
+ if (!importMatch) continue;
6314
+ const fromMatch = line.match(/from\s+["']([^"']+)["']/);
6315
+ if (!fromMatch) continue;
6316
+ const importPath = fromMatch[1];
6317
+ if (!importPath.startsWith(".") && !importPath.startsWith("@/") && !importPath.startsWith("~/")) continue;
6318
+ if (importPath.startsWith("./") || importPath.startsWith("../")) {
6319
+ relativeCount++;
6320
+ } else if (importPath.startsWith("@/") || importPath.startsWith("~/")) {
6321
+ aliasCount++;
6322
+ }
6323
+ if (/^import\s+\{/.test(line)) {
6324
+ namedCount++;
6325
+ } else if (/^import\s+\w+\s+from/.test(line) || /^import\s+\w+,\s*\{/.test(line)) {
6326
+ defaultCount++;
6327
+ }
6328
+ if (evidence.length < 3) {
6329
+ evidence.push({ line: i + 1, code: line });
6330
+ }
6331
+ }
6332
+ const totalLocalImports = relativeCount + aliasCount;
6333
+ if (totalLocalImports < 3) return null;
6334
+ const pathStyle = aliasCount === 0 ? "relative" : relativeCount === 0 ? "alias" : relativeCount >= aliasCount ? "relative" : "alias";
6335
+ const totalBindings = defaultCount + namedCount;
6336
+ let bindingStyle = null;
6337
+ if (totalBindings >= 3) {
6338
+ const defaultRatio = defaultCount / totalBindings;
6339
+ bindingStyle = defaultRatio > 0.6 ? "default_heavy" : defaultRatio < 0.4 ? "named_heavy" : "mixed";
6340
+ }
6341
+ return {
6342
+ file: file.path,
6343
+ pathStyle,
6344
+ bindingStyle,
6345
+ relativeCount,
6346
+ aliasCount,
6347
+ defaultCount,
6348
+ namedCount,
6349
+ evidence
6350
+ };
6351
+ }
6352
+ var PATH_STYLE_NAMES = {
6353
+ relative: "relative paths (./)",
6354
+ alias: "path aliases (@/)"
6355
+ };
6356
+ function buildFinding(profiles, getStyle, styleNames, subCategory, label) {
6357
+ const withStyle = profiles.filter((p) => getStyle(p) !== null);
6358
+ if (withStyle.length < 5) return null;
6359
+ const counts = /* @__PURE__ */ new Map();
6360
+ for (const p of withStyle) {
6361
+ const style = getStyle(p);
6362
+ if (!counts.has(style)) counts.set(style, { count: 0, files: [] });
6363
+ const entry = counts.get(style);
6364
+ entry.count++;
6365
+ entry.files.push(p.file);
6366
+ }
6367
+ if (counts.size < 2) return null;
6368
+ let dominant = null;
6369
+ let dominantCount = 0;
6370
+ for (const [style, data] of counts) {
6371
+ if (data.count > dominantCount) {
6372
+ dominantCount = data.count;
6373
+ dominant = style;
6374
+ }
6375
+ }
6376
+ if (!dominant) return null;
6377
+ const totalFiles = withStyle.length;
6378
+ const consistencyScore = Math.round(dominantCount / totalFiles * 100);
6379
+ if (consistencyScore === 100) return null;
6380
+ const deviating = [];
6381
+ for (const [style, data] of counts) {
6382
+ if (style === dominant) continue;
6383
+ for (const filePath of data.files) {
6384
+ const profile = withStyle.find((p) => p.file === filePath);
6385
+ deviating.push({
6386
+ path: filePath,
6387
+ detectedPattern: styleNames[style] ?? style,
6388
+ evidence: profile?.evidence.slice(0, 2) ?? []
6389
+ });
6390
+ }
6391
+ }
6392
+ return {
6393
+ detector: "import_style",
6394
+ subCategory,
6395
+ driftCategory: "import_style",
6396
+ severity: deviating.length >= 5 ? "warning" : "info",
6397
+ confidence: 0.8,
6398
+ finding: `${label}: ${dominantCount} files use ${styleNames[dominant] ?? dominant}, ${deviating.length} deviate`,
6399
+ dominantPattern: styleNames[dominant] ?? dominant,
6400
+ dominantCount,
6401
+ totalRelevantFiles: totalFiles,
6402
+ consistencyScore,
6403
+ deviatingFiles: deviating,
6404
+ recommendation: `Standardize on ${styleNames[dominant] ?? dominant} across all files for consistency.`
6405
+ };
6406
+ }
6407
+ var importConsistency = {
6408
+ id: "import-consistency",
6409
+ name: "Import Style Consistency",
6410
+ category: "import_style",
6411
+ detect(ctx) {
6412
+ const findings = [];
6413
+ const profiles = [];
6414
+ for (const file of ctx.files) {
6415
+ const p = analyzeImports(file);
6416
+ if (p) profiles.push(p);
6417
+ }
6418
+ if (profiles.length < 5) return findings;
6419
+ const pathFinding = buildFinding(
6420
+ profiles,
6421
+ (p) => p.pathStyle,
6422
+ PATH_STYLE_NAMES,
6423
+ "path_style",
6424
+ "Import path style"
6425
+ );
6426
+ if (pathFinding) findings.push(pathFinding);
6427
+ return findings;
6428
+ }
6429
+ };
6430
+
6431
+ // src/drift/export-consistency.ts
6432
+ init_esm_shims();
6433
+ function isSourceFile2(path2) {
6434
+ if (/(?:test|spec|mock|fixture|__test__|__mocks__|\.test\.|\.spec\.)/i.test(path2)) return false;
6435
+ if (/(?:\.config\.|\.d\.ts$|node_modules|dist\/|build\/|index\.)/i.test(path2)) return false;
6436
+ return true;
6437
+ }
6438
+ function analyzeExports(file) {
6439
+ if (!file.language || !["javascript", "typescript"].includes(file.language)) return null;
6440
+ if (!isSourceFile2(file.path)) return null;
6441
+ const lines = file.content.split("\n");
6442
+ let hasDefaultExport = false;
6443
+ let hasNamedExport = false;
6444
+ const evidence = [];
6445
+ for (let i = 0; i < lines.length; i++) {
6446
+ const line = lines[i].trim();
6447
+ if (/^export\s+default\b/.test(line) || /\bmodule\.exports\s*=/.test(line)) {
6448
+ hasDefaultExport = true;
6449
+ if (evidence.length < 2) evidence.push({ line: i + 1, code: line.slice(0, 100) });
6450
+ } else if (/^export\s+(?:function|class|const|let|var|interface|type|enum|abstract)\b/.test(line)) {
6451
+ hasNamedExport = true;
6452
+ if (evidence.length < 2) evidence.push({ line: i + 1, code: line.slice(0, 100) });
6453
+ } else if (/^export\s*\{/.test(line)) {
6454
+ hasNamedExport = true;
6455
+ if (evidence.length < 2) evidence.push({ line: i + 1, code: line.slice(0, 100) });
6456
+ }
6457
+ }
6458
+ if (!hasDefaultExport && !hasNamedExport) return null;
6459
+ const style = hasDefaultExport ? "default_export" : "named_only";
6460
+ return { file: file.path, style, evidence };
6461
+ }
6462
+ var STYLE_NAMES = {
6463
+ default_export: "default exports",
6464
+ named_only: "named exports only"
6465
+ };
6466
+ var exportConsistency = {
6467
+ id: "export-consistency",
6468
+ name: "Export Style Consistency",
6469
+ category: "export_style",
6470
+ detect(ctx) {
6471
+ const profiles = [];
6472
+ for (const file of ctx.files) {
6473
+ const p = analyzeExports(file);
6474
+ if (p) profiles.push(p);
6475
+ }
6476
+ if (profiles.length < 5) return [];
6477
+ const counts = /* @__PURE__ */ new Map();
6478
+ for (const p of profiles) {
6479
+ if (!counts.has(p.style)) counts.set(p.style, { count: 0, files: [] });
6480
+ const entry = counts.get(p.style);
6481
+ entry.count++;
6482
+ entry.files.push(p.file);
6483
+ }
6484
+ if (counts.size < 2) return [];
6485
+ let dominant = null;
6486
+ let dominantCount = 0;
6487
+ for (const [style, data] of counts) {
6488
+ if (data.count > dominantCount) {
6489
+ dominantCount = data.count;
6490
+ dominant = style;
6491
+ }
6492
+ }
6493
+ if (!dominant) return [];
6494
+ const totalFiles = profiles.length;
6495
+ const consistencyScore = Math.round(dominantCount / totalFiles * 100);
6496
+ if (consistencyScore === 100) return [];
6497
+ const deviating = [];
6498
+ for (const [style, data] of counts) {
6499
+ if (style === dominant) continue;
6500
+ for (const filePath of data.files) {
6501
+ const profile = profiles.find((p) => p.file === filePath);
6502
+ deviating.push({
6503
+ path: filePath,
6504
+ detectedPattern: STYLE_NAMES[style],
6505
+ evidence: profile?.evidence.slice(0, 2) ?? []
6506
+ });
6507
+ }
6508
+ }
6509
+ return [{
6510
+ detector: "export_style",
6511
+ subCategory: "export_style",
6512
+ driftCategory: "export_style",
6513
+ severity: deviating.length >= 5 ? "warning" : "info",
6514
+ confidence: 0.75,
6515
+ finding: `Export style: ${dominantCount} files use ${STYLE_NAMES[dominant]}, ${deviating.length} use ${STYLE_NAMES[dominant === "default_export" ? "named_only" : "default_export"]}`,
6516
+ dominantPattern: STYLE_NAMES[dominant],
6517
+ dominantCount,
6518
+ totalRelevantFiles: totalFiles,
6519
+ consistencyScore,
6520
+ deviatingFiles: deviating,
6521
+ recommendation: `Standardize on ${STYLE_NAMES[dominant]}. ${dominant === "named_only" ? "Named exports enable tree-shaking and explicit imports." : "Default exports simplify imports but disable tree-shaking."}`
6522
+ }];
6523
+ }
6524
+ };
6525
+
6526
+ // src/drift/async-consistency.ts
6527
+ init_esm_shims();
6528
+ function isSourceFile3(path2) {
6529
+ if (/(?:test|spec|mock|fixture|__test__|__mocks__|\.test\.|\.spec\.)/i.test(path2)) return false;
6530
+ if (/(?:\.config\.|\.d\.ts$|node_modules|dist\/|build\/)/i.test(path2)) return false;
6531
+ return true;
6532
+ }
6533
+ function analyzeAsync(file) {
6534
+ if (!file.language || !["javascript", "typescript"].includes(file.language)) return null;
6535
+ if (!isSourceFile3(file.path)) return null;
6536
+ const lines = file.content.split("\n");
6537
+ let awaitCount = 0;
6538
+ let thenCount = 0;
6539
+ const evidence = [];
6540
+ for (let i = 0; i < lines.length; i++) {
6541
+ const line = lines[i];
6542
+ if (/\bawait\s+/.test(line) && !line.trim().startsWith("//") && !line.trim().startsWith("*")) {
6543
+ awaitCount++;
6544
+ if (evidence.length < 3) {
6545
+ evidence.push({ line: i + 1, code: line.trim().slice(0, 100) });
6546
+ }
6547
+ }
6548
+ if (/\.\s*then\s*\(/.test(line) && !line.trim().startsWith("//") && !line.trim().startsWith("*") && !/type\s|interface\s/.test(line)) {
6549
+ thenCount++;
6550
+ if (evidence.length < 3) {
6551
+ evidence.push({ line: i + 1, code: line.trim().slice(0, 100) });
6552
+ }
6553
+ }
6554
+ }
6555
+ const total = awaitCount + thenCount;
6556
+ if (total < 2) return null;
6557
+ let style;
6558
+ const awaitRatio = awaitCount / total;
6559
+ if (awaitRatio > 0.7) style = "async_await";
6560
+ else if (awaitRatio < 0.3) style = "then_chains";
6561
+ else style = "mixed";
6562
+ return { file: file.path, style, awaitCount, thenCount, evidence };
6563
+ }
6564
+ var STYLE_NAMES2 = {
6565
+ async_await: "async/await",
6566
+ then_chains: ".then() chains",
6567
+ mixed: "mixed async patterns"
6568
+ };
6569
+ var asyncConsistency = {
6570
+ id: "async-consistency",
6571
+ name: "Async Pattern Consistency",
6572
+ category: "async_patterns",
6573
+ detect(ctx) {
6574
+ const profiles = [];
6575
+ for (const file of ctx.files) {
6576
+ const p = analyzeAsync(file);
6577
+ if (p) profiles.push(p);
6578
+ }
6579
+ if (profiles.length < 5) return [];
6580
+ const counts = /* @__PURE__ */ new Map();
6581
+ for (const p of profiles) {
6582
+ if (!counts.has(p.style)) counts.set(p.style, { count: 0, files: [] });
6583
+ const entry = counts.get(p.style);
6584
+ entry.count++;
6585
+ entry.files.push(p.file);
6586
+ }
6587
+ if (counts.size < 2) return [];
6588
+ let dominant = null;
6589
+ let dominantCount = 0;
6590
+ for (const [style, data] of counts) {
6591
+ if (data.count > dominantCount) {
6592
+ dominantCount = data.count;
6593
+ dominant = style;
6594
+ }
6595
+ }
6596
+ if (!dominant) return [];
6597
+ const totalFiles = profiles.length;
6598
+ const consistencyScore = Math.round(dominantCount / totalFiles * 100);
6599
+ if (consistencyScore === 100) return [];
6600
+ const deviating = [];
6601
+ for (const [style, data] of counts) {
6602
+ if (style === dominant) continue;
6603
+ for (const filePath of data.files) {
6604
+ const profile = profiles.find((p) => p.file === filePath);
6605
+ deviating.push({
6606
+ path: filePath,
6607
+ detectedPattern: STYLE_NAMES2[style],
6608
+ evidence: profile?.evidence.slice(0, 2) ?? []
6609
+ });
6610
+ }
6611
+ }
6612
+ return [{
6613
+ detector: "async_patterns",
6614
+ subCategory: "async_style",
6615
+ driftCategory: "async_patterns",
6616
+ severity: deviating.length >= 5 ? "warning" : "info",
6617
+ confidence: 0.8,
6618
+ finding: `Async style: ${dominantCount} files use ${STYLE_NAMES2[dominant]}, ${deviating.length} use different patterns`,
6619
+ dominantPattern: STYLE_NAMES2[dominant],
6620
+ dominantCount,
6621
+ totalRelevantFiles: totalFiles,
6622
+ consistencyScore,
6623
+ deviatingFiles: deviating,
6624
+ recommendation: `Standardize on ${STYLE_NAMES2[dominant]}. ${dominant === "async_await" ? "async/await is more readable and has clearer error handling with try/catch." : "Consider migrating to async/await for consistency and readability."}`
6625
+ }];
6626
+ }
6627
+ };
6628
+
6288
6629
  // src/drift/index.ts
6289
6630
  function createDriftDetectors() {
6290
6631
  return [
@@ -6292,7 +6633,10 @@ function createDriftDetectors() {
6292
6633
  conventionOscillation,
6293
6634
  securityConsistency,
6294
6635
  semanticDuplication,
6295
- phantomScaffolding
6636
+ phantomScaffolding,
6637
+ importConsistency,
6638
+ exportConsistency,
6639
+ asyncConsistency
6296
6640
  ];
6297
6641
  }
6298
6642
  function buildDriftContext(ctx) {
@@ -6325,7 +6669,10 @@ function computeDriftScores(findings) {
6325
6669
  "security_posture",
6326
6670
  "semantic_duplication",
6327
6671
  "naming_conventions",
6328
- "phantom_scaffolding"
6672
+ "phantom_scaffolding",
6673
+ "import_style",
6674
+ "export_style",
6675
+ "async_patterns"
6329
6676
  ];
6330
6677
  const scores = {};
6331
6678
  for (const cat of categories) {
@@ -7246,7 +7593,7 @@ function buildRadarSection(result) {
7246
7593
  return `<section class="section">
7247
7594
  <div class="label">DRIFT FINGERPRINT</div>
7248
7595
  <p style="font-size:13px;color:var(--text-secondary);margin-bottom:20px;line-height:1.6">
7249
- This radar shows how well your codebase maintains consistency across 5 drift categories.
7596
+ This radar shows how well your codebase maintains consistency across 8 drift categories.
7250
7597
  The outer edge represents a perfect score in each category. Points closer to the center indicate more drift.
7251
7598
  A balanced shape means consistent quality; an uneven shape reveals where drift concentrates.
7252
7599
  </p>