@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 +119 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
1226
|
+
import { createHash as createHash3 } from "crypto";
|
|
1210
1227
|
async function detectProjectIdentity(rootDir, override) {
|
|
1211
|
-
const hash =
|
|
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:
|
|
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 ?? "
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
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: `${
|
|
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 =
|
|
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.
|
|
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 >=
|
|
5961
|
-
else if (composite >=
|
|
5962
|
-
else if (composite >=
|
|
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 >
|
|
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 /
|
|
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.
|
|
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;
|