@vibedrift/cli 0.3.2 → 0.4.1

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
@@ -195,6 +195,7 @@ var init_version = __esm({
195
195
  });
196
196
 
197
197
  // src/codedna/semantic-fingerprint.ts
198
+ import { createHash as createHash2 } from "crypto";
198
199
  function normalizeBody(body, language) {
199
200
  let normalized = body;
200
201
  normalized = normalized.replace(/\/\/.*$/gm, "");
@@ -262,7 +263,11 @@ function computeSemanticFingerprints(functions) {
262
263
  normalizedHash: fnv1aHash(normalizeBody(fn.rawBody, fn.language))
263
264
  }));
264
265
  }
265
- function findDuplicateGroups(fingerprints) {
266
+ function findDuplicateGroups(fingerprints, functions) {
267
+ const fnLookup = /* @__PURE__ */ new Map();
268
+ for (const fn of functions) {
269
+ fnLookup.set(`${fn.name}:${fn.file}`, fn);
270
+ }
266
271
  const byHash = /* @__PURE__ */ new Map();
267
272
  for (const fp of fingerprints) {
268
273
  if (!byHash.has(fp.normalizedHash)) byHash.set(fp.normalizedHash, []);
@@ -272,13 +277,25 @@ function findDuplicateGroups(fingerprints) {
272
277
  let groupCounter = 0;
273
278
  for (const [hash, fps] of byHash) {
274
279
  if (fps.length < 2) continue;
275
- const uniqueFiles = new Set(fps.map((fp) => fp.functionRef.file));
276
- if (uniqueFiles.size < 2) continue;
277
- groups.push({
278
- groupId: `fingerprint-${groupCounter++}`,
279
- hash,
280
- functions: fps.map((fp) => fp.functionRef)
281
- });
280
+ const bySha = /* @__PURE__ */ new Map();
281
+ for (const fp of fps) {
282
+ const key = `${fp.functionRef.name}:${fp.functionRef.file}`;
283
+ const fn = fnLookup.get(key);
284
+ if (!fn) continue;
285
+ const sha = createHash2("sha256").update(normalizeBody(fn.rawBody, fn.language)).digest("hex");
286
+ if (!bySha.has(sha)) bySha.set(sha, []);
287
+ bySha.get(sha).push(fp);
288
+ }
289
+ for (const [, shaGroup] of bySha) {
290
+ if (shaGroup.length < 2) continue;
291
+ const uniqueFiles = new Set(shaGroup.map((fp) => fp.functionRef.file));
292
+ if (uniqueFiles.size < 2) continue;
293
+ groups.push({
294
+ groupId: `fingerprint-${groupCounter++}`,
295
+ hash,
296
+ functions: shaGroup.map((fp) => fp.functionRef)
297
+ });
298
+ }
282
299
  }
283
300
  return groups;
284
301
  }
@@ -923,7 +940,7 @@ function runCodeDnaAnalysis(ctx) {
923
940
  timings.extractionMs = Date.now() - t;
924
941
  t = Date.now();
925
942
  const fingerprints = computeSemanticFingerprints(functions);
926
- const duplicateGroups = findDuplicateGroups(fingerprints);
943
+ const duplicateGroups = findDuplicateGroups(fingerprints, functions);
927
944
  timings.fingerprintMs = Date.now() - t;
928
945
  t = Date.now();
929
946
  const sequences = extractOperationSequences(functions);
@@ -1206,9 +1223,9 @@ __export(project_name_exports, {
1206
1223
  });
1207
1224
  import { basename, join as join6 } from "path";
1208
1225
  import { readFile as readFile5 } from "fs/promises";
1209
- import { createHash as createHash2 } from "crypto";
1226
+ import { createHash as createHash3 } from "crypto";
1210
1227
  async function detectProjectIdentity(rootDir, override) {
1211
- const hash = createHash2("sha256").update(rootDir).digest("hex");
1228
+ const hash = createHash3("sha256").update(rootDir).digest("hex");
1212
1229
  if (override && override.trim()) {
1213
1230
  return { name: override.trim(), hash };
1214
1231
  }
@@ -1304,9 +1321,29 @@ function buildDeviationPayloads(codeDnaResult, driftFindings) {
1304
1321
  const hasComment = dj.signals?.some((s) => s.type === "explanatory_comment" && s.present) ?? false;
1305
1322
  const sqlComplexity = dj.signals?.find((s) => s.type === "complex_sql")?.present ? 3 : 0;
1306
1323
  const funcComplexity = dj.signals?.reduce((sum, s) => sum + (s.present ? Math.abs(s.weight) : 0), 0) ?? 0;
1324
+ const patternToType = {
1325
+ repository: "data_access",
1326
+ raw_sql: "data_access",
1327
+ orm: "data_access",
1328
+ direct_db: "data_access",
1329
+ http_client: "data_access",
1330
+ wrap_with_context: "error_handling",
1331
+ raw_propagation: "error_handling",
1332
+ swallow: "error_handling",
1333
+ http_error_response: "error_handling",
1334
+ exception_throw: "error_handling",
1335
+ result_type: "error_handling",
1336
+ constructor_injection: "di",
1337
+ global_import: "di",
1338
+ service_locator: "di",
1339
+ no_di: "di",
1340
+ env_direct: "config",
1341
+ config_struct_di: "config"
1342
+ };
1343
+ const inferredType = patternToType[dj.deviatingPattern] ?? patternToType[dj.dominantPattern] ?? "data_access";
1307
1344
  deviations.push({
1308
1345
  file: dj.relativePath,
1309
- deviation_type: "architectural",
1346
+ deviation_type: inferredType,
1310
1347
  dominant_pattern: dj.dominantPattern,
1311
1348
  actual_pattern: dj.deviatingPattern,
1312
1349
  dominant_count: 0,
@@ -1327,7 +1364,7 @@ function buildDeviationPayloads(codeDnaResult, driftFindings) {
1327
1364
  seen.add(key);
1328
1365
  deviations.push({
1329
1366
  file: df.path,
1330
- deviation_type: d.subCategory ?? "architectural",
1367
+ deviation_type: d.subCategory ?? "data_access",
1331
1368
  dominant_pattern: d.dominantPattern,
1332
1369
  actual_pattern: df.detectedPattern,
1333
1370
  dominant_count: d.dominantCount,
@@ -1456,9 +1493,11 @@ function deduplicateFindingsAcrossLayers(findings) {
1456
1493
  return [...nonDuplicate, ...dedupedDuplicates];
1457
1494
  }
1458
1495
  function makeFilePairKey(f) {
1459
- const files = f.locations.map((l) => l.file).filter(Boolean).sort();
1496
+ const files = [...new Set(
1497
+ f.locations.map((l) => l.file).filter(Boolean)
1498
+ )].sort();
1460
1499
  if (files.length >= 2) {
1461
- return `dup::${files[0]}::${files[1]}`;
1500
+ return `dup::${files.join("::")}`;
1462
1501
  }
1463
1502
  return `dup::${files[0] ?? "unknown"}::${f.analyzerId}`;
1464
1503
  }
@@ -2682,7 +2721,7 @@ var namingAnalyzer = {
2682
2721
  init_esm_shims();
2683
2722
  var ESM_PATTERN = /\b(?:import\s+|export\s+(?:default\s+)?(?:function|class|const|let|var|{))/;
2684
2723
  var CJS_PATTERN = /\b(?:require\s*\(|module\.exports|exports\.)/;
2685
- var CONFIG_FILE_PATTERN = /(?:\.config\.|\.setup\.|\.rc\.|jest\.|babel\.|webpack\.|rollup\.|vite\.)/;
2724
+ var CONFIG_FILE_PATTERN = /(?:\.config\.|\.setup\.|\.rc\.|jest\.|babel\.|webpack\.|rollup\.|vite\.|next\.config|tailwind\.config|postcss\.config|tsconfig|eslint\.config|prettier\.config|svelte\.config|nuxt\.config|astro\.config|vitest\.config|tsup\.config|esbuild\.config|turbo\.json|\.cjs$|\.mjs$)/;
2686
2725
  var importsAnalyzer = {
2687
2726
  id: "imports",
2688
2727
  name: "Import Patterns",
@@ -2743,8 +2782,6 @@ function densityPer1K(count, totalLines) {
2743
2782
 
2744
2783
  // src/analyzers/error-handling.ts
2745
2784
  var EMPTY_CATCH_PATTERN = /catch\s*\([^)]*\)\s*\{\s*\}/g;
2746
- var TRY_CATCH_PATTERN = /\btry\s*\{/g;
2747
- var ASYNC_PATTERN = /\basync\s+(?:function|\(|[a-zA-Z])/g;
2748
2785
  var errorHandlingAnalyzer = {
2749
2786
  id: "error-handling",
2750
2787
  name: "Error Handling",
@@ -2781,22 +2818,43 @@ var errorHandlingAnalyzer = {
2781
2818
  tags: ["error-handling", "empty-catch"]
2782
2819
  });
2783
2820
  }
2784
- const dirStats = /* @__PURE__ */ new Map();
2821
+ const ASYNC_FN_PATTERN = /async\s+(?:function\s+\w+|\(\w*\)|\w+)\s*\([^)]*\)\s*(?::\s*[^{]*)?\s*\{/g;
2822
+ const ERROR_HANDLING_PATTERNS = [
2823
+ /\btry\s*\{/,
2824
+ /\.catch\s*\(/,
2825
+ /\bcatch\s*\(/,
2826
+ /\b(?:Result|Either)\s*[<(]/
2827
+ ];
2828
+ const dirUnhandled = /* @__PURE__ */ new Map();
2785
2829
  for (const file of jsFiles) {
2786
2830
  const dir = file.relativePath.includes("/") ? file.relativePath.slice(0, file.relativePath.lastIndexOf("/")) : ".";
2787
- if (!dirStats.has(dir)) dirStats.set(dir, { tryCatch: 0, asyncFns: 0 });
2788
- const stats = dirStats.get(dir);
2789
- stats.tryCatch += (file.content.match(TRY_CATCH_PATTERN) ?? []).length;
2790
- stats.asyncFns += (file.content.match(ASYNC_PATTERN) ?? []).length;
2791
- }
2792
- for (const [dir, stats] of dirStats) {
2793
- const unhandled = stats.asyncFns - stats.tryCatch;
2794
- if (unhandled > 3) {
2831
+ const asyncRegex = new RegExp(ASYNC_FN_PATTERN.source, "g");
2832
+ let fnMatch;
2833
+ while ((fnMatch = asyncRegex.exec(file.content)) !== null) {
2834
+ const openBrace = file.content.indexOf("{", fnMatch.index + fnMatch[0].length - 1);
2835
+ if (openBrace === -1) continue;
2836
+ let depth = 1;
2837
+ let pos = openBrace + 1;
2838
+ while (pos < file.content.length && depth > 0) {
2839
+ if (file.content[pos] === "{") depth++;
2840
+ else if (file.content[pos] === "}") depth--;
2841
+ pos++;
2842
+ }
2843
+ const body = file.content.slice(openBrace + 1, pos - 1);
2844
+ if (!/\bawait\b/.test(body)) continue;
2845
+ const hasHandling = ERROR_HANDLING_PATTERNS.some((p) => p.test(body));
2846
+ if (!hasHandling) {
2847
+ dirUnhandled.set(dir, (dirUnhandled.get(dir) ?? 0) + 1);
2848
+ }
2849
+ }
2850
+ }
2851
+ for (const [dir, count] of dirUnhandled) {
2852
+ if (count > 3) {
2795
2853
  findings.push({
2796
2854
  analyzerId: "error-handling",
2797
2855
  severity: "info",
2798
2856
  confidence: 0.6,
2799
- message: `${unhandled} async functions without try/catch in ${dir}/`,
2857
+ message: `${count} async functions without error handling in ${dir}/`,
2800
2858
  locations: [{ file: dir }],
2801
2859
  tags: ["error-handling", "unhandled-async"]
2802
2860
  });
@@ -3418,6 +3476,16 @@ var duplicatesAnalyzer = {
3418
3476
  }
3419
3477
  }
3420
3478
  }
3479
+ if (allSequences.length > 200) {
3480
+ findings.push({
3481
+ analyzerId: "duplicates",
3482
+ severity: "info",
3483
+ confidence: 0.3,
3484
+ message: `Cross-function duplicate detection limited to hash-based matching (${allSequences.length} functions exceeds the 200-function threshold for full comparison). Use --include to narrow scope for deeper analysis.`,
3485
+ locations: [],
3486
+ tags: ["duplicates", "scalability"]
3487
+ });
3488
+ }
3421
3489
  if (allSequences.length <= 200) {
3422
3490
  for (let i = 0; i < allSequences.length; i++) {
3423
3491
  for (let j = i + 1; j < allSequences.length; j++) {
@@ -3768,7 +3836,9 @@ var SECURITY_PATTERNS = [
3768
3836
  message: "Math.random() is not cryptographically secure \u2014 use crypto.randomUUID()",
3769
3837
  languages: ["javascript", "typescript"],
3770
3838
  tags: ["security", "crypto"],
3771
- negativeFilter: /(?:test|mock|seed|shuffle|animation|color|position|offset|delay|jitter)/i
3839
+ negativeFilter: /(?:test|mock|seed|shuffle|animation|color|position|offset|delay|jitter)/i,
3840
+ // Only flag near security-relevant code — UI shuffles/animations are not a risk
3841
+ contextRequired: /(?:token|secret|password|key|nonce|salt|hash|crypto|auth|session|jwt|api.?key|credential)/i
3772
3842
  },
3773
3843
  // === Path Traversal ===
3774
3844
  {
@@ -3860,6 +3930,14 @@ var securityAnalyzer = {
3860
3930
  const line = file.content.slice(lineStart2, lineEnd2 === -1 ? void 0 : lineEnd2);
3861
3931
  if (pattern.negativeFilter.test(line)) continue;
3862
3932
  }
3933
+ if (pattern.contextRequired) {
3934
+ const lines = file.content.split("\n");
3935
+ const matchLine = file.content.slice(0, match.index).split("\n").length - 1;
3936
+ const start = Math.max(0, matchLine - 5);
3937
+ const end = Math.min(lines.length, matchLine + 6);
3938
+ const context = lines.slice(start, end).join("\n");
3939
+ if (!pattern.contextRequired.test(context)) continue;
3940
+ }
3863
3941
  const lineNum = getLineNumber(file.content, match.index);
3864
3942
  const lineStart = file.content.lastIndexOf("\n", match.index) + 1;
3865
3943
  const lineEnd = file.content.indexOf("\n", match.index);
@@ -3974,7 +4052,11 @@ function computeComplexityAST(node, language) {
3974
4052
  walk(node);
3975
4053
  return complexity;
3976
4054
  }
4055
+ function stripComments(code) {
4056
+ return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/#.*$/gm, "");
4057
+ }
3977
4058
  function computeComplexityRegex(content) {
4059
+ const stripped = stripComments(content);
3978
4060
  let complexity = 1;
3979
4061
  const patterns = [
3980
4062
  /\bif\s*\(/g,
@@ -3997,7 +4079,7 @@ function computeComplexityRegex(content) {
3997
4079
  ];
3998
4080
  for (const p of patterns) {
3999
4081
  const regex = new RegExp(p.source, p.flags);
4000
- const matches = content.match(regex);
4082
+ const matches = stripped.match(regex);
4001
4083
  if (matches) complexity += matches.length;
4002
4084
  }
4003
4085
  return complexity;
@@ -5397,7 +5479,7 @@ function analyzeSecurityProperty(routes, propertyName, getter, excludePaths) {
5397
5479
  const withProperty = applicableRoutes.filter(getter);
5398
5480
  const withoutProperty = applicableRoutes.filter((r) => !getter(r));
5399
5481
  const ratio = withProperty.length / applicableRoutes.length;
5400
- if (ratio <= 0.6 || withoutProperty.length === 0) return null;
5482
+ if (ratio <= 0.75 || withoutProperty.length === 0) return null;
5401
5483
  return {
5402
5484
  detector: "security_posture",
5403
5485
  subCategory: propertyName,
@@ -5957,9 +6039,9 @@ function computeDriftScores(findings) {
5957
6039
  ) / 10;
5958
6040
  let grade;
5959
6041
  if (composite >= 90) grade = "A";
5960
- else if (composite >= 80) grade = "B";
5961
- else if (composite >= 65) grade = "C";
5962
- else if (composite >= 50) grade = "D";
6042
+ else if (composite >= 75) grade = "B";
6043
+ else if (composite >= 50) grade = "C";
6044
+ else if (composite >= 25) grade = "D";
5963
6045
  else grade = "F";
5964
6046
  return {
5965
6047
  ...scores,
@@ -6118,10 +6200,10 @@ function computeCategoryScore(findings, maxScore, totalLines, applicable, correl
6118
6200
  }
6119
6201
  rawWeight += base * confidence * fileWeight * corrWeight;
6120
6202
  }
6121
- const sizeFactor = totalLines > 100 ? Math.sqrt(totalLines / 1e3) : 1;
6203
+ const sizeFactor = totalLines > 500 ? Math.sqrt(totalLines / 1e3) : 1;
6122
6204
  const clampedSizeFactor = Math.max(0.5, Math.min(3, sizeFactor));
6123
6205
  const adjustedWeight = rawWeight / clampedSizeFactor;
6124
- const k = Math.LN2 / 10;
6206
+ const k = Math.LN2 / 15;
6125
6207
  const factor = Math.exp(-k * adjustedWeight);
6126
6208
  const score = Math.round(maxScore * factor * 10) / 10;
6127
6209
  return {
@@ -6172,7 +6254,7 @@ function computeScores(findings, totalLines, ctx, previousScores) {
6172
6254
  }
6173
6255
  const DRAG_CATEGORIES = ["architecturalConsistency", "redundancy"];
6174
6256
  const DRAG_THRESHOLD = 0.5;
6175
- const DRAG_MAX_PENALTY = 0.15;
6257
+ const DRAG_MAX_PENALTY = 0.1;
6176
6258
  for (const cat of DRAG_CATEGORIES) {
6177
6259
  const s = scores[cat];
6178
6260
  if (!s.applicable || s.maxScore === 0) continue;