@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 +425 -321
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
3015
|
-
if (NODE_BUILTINS.has(
|
|
3016
|
-
imported.add(
|
|
3017
|
-
if (!importLocations.has(
|
|
3018
|
-
importLocations.get(
|
|
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) => !
|
|
3012
|
+
(p) => !devToolPatterns.some((pat) => p.includes(pat))
|
|
3028
3013
|
);
|
|
3029
3014
|
if (realPhantom.length > 0) {
|
|
3030
|
-
|
|
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
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
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
|
|
3054
|
-
const missingSeverity = isMonorepo
|
|
3041
|
+
const missingConfidence = isMonorepo ? 0.4 : 0.75;
|
|
3042
|
+
const missingSeverity = isMonorepo ? "warning" : "error";
|
|
3055
3043
|
if (missing.length > 0) {
|
|
3056
|
-
|
|
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
|
|
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
|
|
4396
|
-
const
|
|
4397
|
-
|
|
4398
|
-
for (const
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
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
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4608
|
-
|
|
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
|
|
4753
|
-
|
|
4754
|
-
|
|
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
|
|
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
|
-
|
|
4776
|
-
|
|
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
|
-
|
|
4787
|
-
|
|
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
|
-
|
|
4794
|
-
|
|
4843
|
+
count++;
|
|
4844
|
+
locations.push({ file: file.relativePath, line: i + 1 });
|
|
4795
4845
|
}
|
|
4796
4846
|
}
|
|
4797
4847
|
}
|
|
4798
4848
|
}
|
|
4799
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
5267
|
-
const
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
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,
|
|
5282
|
-
totalFiles +=
|
|
5283
|
-
if (
|
|
5284
|
-
maxCount =
|
|
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
|
|
6063
|
+
if (allExports.length < 3) return [];
|
|
5867
6064
|
const usage = buildUsageGraph(ctx.files);
|
|
5868
|
-
const
|
|
5869
|
-
|
|
5870
|
-
|
|
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
|
|
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
|
-
|
|
8219
|
-
|
|
8306
|
+
return { bearerToken, apiUrl };
|
|
8307
|
+
}
|
|
8308
|
+
async function runAnalysisPipeline(rootDir, options, spinner) {
|
|
8220
8309
|
const isTerminal = options.format === "terminal" && !options.json;
|
|
8221
|
-
const
|
|
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
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
8402
|
-
file_count:
|
|
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",
|
|
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();
|