@vibedrift/cli 0.4.5 → 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 +405 -288
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -5069,27 +5126,35 @@ function buildProfile(file) {
|
|
|
5069
5126
|
if (dataAccess.length === 0 && errorHandling.length === 0) return null;
|
|
5070
5127
|
return { file: file.path, language: file.language, dataAccess, errorHandling, config, di };
|
|
5071
5128
|
}
|
|
5072
|
-
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) {
|
|
5073
5145
|
const counts = /* @__PURE__ */ new Map();
|
|
5074
5146
|
for (const p of profiles) {
|
|
5075
|
-
const
|
|
5076
|
-
for (const { pattern } of p.patterns) {
|
|
5077
|
-
fileCounts.set(pattern, (fileCounts.get(pattern) ?? 0) + 1);
|
|
5078
|
-
}
|
|
5079
|
-
let primaryPattern = null;
|
|
5080
|
-
let maxCount = 0;
|
|
5081
|
-
for (const [pat, count] of fileCounts) {
|
|
5082
|
-
if (count > maxCount) {
|
|
5083
|
-
maxCount = count;
|
|
5084
|
-
primaryPattern = pat;
|
|
5085
|
-
}
|
|
5086
|
-
}
|
|
5147
|
+
const primaryPattern = detectFilePattern(p.patterns);
|
|
5087
5148
|
if (!primaryPattern) continue;
|
|
5088
5149
|
if (!counts.has(primaryPattern)) counts.set(primaryPattern, { count: 0, files: [] });
|
|
5089
5150
|
const entry = counts.get(primaryPattern);
|
|
5090
5151
|
entry.count++;
|
|
5091
5152
|
entry.files.push(p.file);
|
|
5092
5153
|
}
|
|
5154
|
+
return counts;
|
|
5155
|
+
}
|
|
5156
|
+
function analyzePatternDistribution(profiles, patternNames) {
|
|
5157
|
+
const counts = buildPatternDistribution(profiles);
|
|
5093
5158
|
if (counts.size < 2) return null;
|
|
5094
5159
|
let dominant = null;
|
|
5095
5160
|
let dominantCount = 0;
|
|
@@ -5260,29 +5325,40 @@ function extractSymbols(file) {
|
|
|
5260
5325
|
}
|
|
5261
5326
|
return symbols;
|
|
5262
5327
|
}
|
|
5263
|
-
function
|
|
5264
|
-
const
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
fileNameConventions.get(conv).push(file.path);
|
|
5273
|
-
}
|
|
5274
|
-
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) {
|
|
5275
5337
|
let dominant = null;
|
|
5276
5338
|
let maxCount = 0;
|
|
5277
5339
|
let totalFiles = 0;
|
|
5278
|
-
for (const [conv,
|
|
5279
|
-
totalFiles +=
|
|
5280
|
-
if (
|
|
5281
|
-
maxCount =
|
|
5340
|
+
for (const [conv, files] of fileNameConventions) {
|
|
5341
|
+
totalFiles += files.length;
|
|
5342
|
+
if (files.length > maxCount) {
|
|
5343
|
+
maxCount = files.length;
|
|
5282
5344
|
dominant = conv;
|
|
5283
5345
|
}
|
|
5284
5346
|
}
|
|
5285
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;
|
|
5286
5362
|
const deviating = [];
|
|
5287
5363
|
for (const [conv, filePaths] of fileNameConventions) {
|
|
5288
5364
|
if (conv === dominant) continue;
|
|
@@ -5848,131 +5924,147 @@ function buildUsageGraph(files) {
|
|
|
5848
5924
|
}
|
|
5849
5925
|
return usage;
|
|
5850
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
|
+
}
|
|
5851
6052
|
var phantomScaffolding = {
|
|
5852
6053
|
id: "phantom-scaffolding",
|
|
5853
6054
|
name: "Phantom Scaffolding",
|
|
5854
6055
|
category: "phantom_scaffolding",
|
|
5855
6056
|
detect(ctx) {
|
|
5856
|
-
const findings = [];
|
|
5857
6057
|
const allExports = [];
|
|
5858
6058
|
const allRoutes = [];
|
|
5859
6059
|
for (const file of ctx.files) {
|
|
5860
6060
|
allExports.push(...extractExportedFunctions(file));
|
|
5861
6061
|
allRoutes.push(...extractRouteRegistrations(file));
|
|
5862
6062
|
}
|
|
5863
|
-
if (allExports.length < 3) return
|
|
6063
|
+
if (allExports.length < 3) return [];
|
|
5864
6064
|
const usage = buildUsageGraph(ctx.files);
|
|
5865
|
-
const
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
if (!handlerFiles.has(exp.file)) handlerFiles.set(exp.file, []);
|
|
5869
|
-
handlerFiles.get(exp.file).push(exp);
|
|
5870
|
-
}
|
|
5871
|
-
for (const [filePath, functions] of handlerFiles) {
|
|
5872
|
-
const crudFunctions = functions.filter((f) => f.crudType !== "other");
|
|
5873
|
-
if (crudFunctions.length < 2) continue;
|
|
5874
|
-
const usedFunctions = [];
|
|
5875
|
-
const unusedFunctions = [];
|
|
5876
|
-
for (const fn of crudFunctions) {
|
|
5877
|
-
const references = usage.get(fn.name);
|
|
5878
|
-
const usedExternally = references && [...references].some((f) => f !== filePath);
|
|
5879
|
-
const isRouted = allRoutes.some(
|
|
5880
|
-
(r) => r.handlerName.includes(fn.name) || r.handlerName.endsWith("." + fn.name)
|
|
5881
|
-
);
|
|
5882
|
-
if (usedExternally || isRouted) {
|
|
5883
|
-
usedFunctions.push(fn);
|
|
5884
|
-
} else {
|
|
5885
|
-
unusedFunctions.push(fn);
|
|
5886
|
-
}
|
|
5887
|
-
}
|
|
5888
|
-
if (unusedFunctions.length >= 2 && usedFunctions.length >= 1) {
|
|
5889
|
-
const usedTypes = usedFunctions.map((f) => f.crudType);
|
|
5890
|
-
const unusedTypes = unusedFunctions.map((f) => f.crudType);
|
|
5891
|
-
const isScaffoldingPattern = usedTypes.some((t) => t === "read" || t === "list") && unusedTypes.some((t) => t === "create" || t === "update" || t === "delete");
|
|
5892
|
-
findings.push({
|
|
5893
|
-
detector: "phantom_scaffolding",
|
|
5894
|
-
driftCategory: "phantom_scaffolding",
|
|
5895
|
-
severity: isScaffoldingPattern ? "warning" : "info",
|
|
5896
|
-
confidence: isScaffoldingPattern ? 0.8 : 0.6,
|
|
5897
|
-
finding: `CRUD scaffolding detected in ${filePath} \u2014 ${usedFunctions.length} functions used, ${unusedFunctions.length} appear unused`,
|
|
5898
|
-
dominantPattern: "used CRUD operations",
|
|
5899
|
-
dominantCount: usedFunctions.length,
|
|
5900
|
-
totalRelevantFiles: usedFunctions.length + unusedFunctions.length,
|
|
5901
|
-
consistencyScore: Math.round(usedFunctions.length / (usedFunctions.length + unusedFunctions.length) * 100),
|
|
5902
|
-
deviatingFiles: [{
|
|
5903
|
-
path: filePath,
|
|
5904
|
-
detectedPattern: `unused: ${unusedFunctions.map((f) => f.name).join(", ")}`,
|
|
5905
|
-
evidence: unusedFunctions.slice(0, 5).map((f) => ({
|
|
5906
|
-
line: f.line,
|
|
5907
|
-
code: `${f.name} (${f.crudType}) \u2014 defined but not routed or imported`
|
|
5908
|
-
}))
|
|
5909
|
-
}],
|
|
5910
|
-
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.`
|
|
5911
|
-
});
|
|
5912
|
-
}
|
|
5913
|
-
}
|
|
5914
|
-
const typeDefinitions = [];
|
|
5915
|
-
const goMethodReceivers = /* @__PURE__ */ new Set();
|
|
5916
|
-
for (const file of ctx.files) {
|
|
5917
|
-
if (file.language !== "go") continue;
|
|
5918
|
-
const receiverPattern = /func\s+\(\s*\w+\s+\*?(\w+)\s*\)/g;
|
|
5919
|
-
let m;
|
|
5920
|
-
while ((m = receiverPattern.exec(file.content)) !== null) {
|
|
5921
|
-
goMethodReceivers.add(m[1]);
|
|
5922
|
-
}
|
|
5923
|
-
}
|
|
5924
|
-
for (const file of ctx.files) {
|
|
5925
|
-
if (!file.language) continue;
|
|
5926
|
-
const lines = file.content.split("\n");
|
|
5927
|
-
for (let i = 0; i < lines.length; i++) {
|
|
5928
|
-
const line = lines[i];
|
|
5929
|
-
let typeName = null;
|
|
5930
|
-
if (file.language === "go") {
|
|
5931
|
-
const m = line.match(/^type\s+([A-Z]\w+)\s+struct\b/);
|
|
5932
|
-
if (m) typeName = m[1];
|
|
5933
|
-
} else if (file.language === "javascript" || file.language === "typescript") {
|
|
5934
|
-
const m = line.match(/export\s+(?:interface|type|class)\s+(\w+)/);
|
|
5935
|
-
if (m) typeName = m[1];
|
|
5936
|
-
}
|
|
5937
|
-
if (!typeName) continue;
|
|
5938
|
-
if (file.language === "go" && goMethodReceivers.has(typeName)) continue;
|
|
5939
|
-
if (/(?:Request|Response|Params|Config|Options|Input|Output|Payload|Body|DTO)$/i.test(typeName)) continue;
|
|
5940
|
-
typeDefinitions.push({ name: typeName, file: file.path, line: i + 1 });
|
|
5941
|
-
}
|
|
5942
|
-
}
|
|
5943
|
-
const unusedTypesByFile = /* @__PURE__ */ new Map();
|
|
5944
|
-
for (const td of typeDefinitions) {
|
|
5945
|
-
const refs = usage.get(td.name);
|
|
5946
|
-
const usedExternally = refs && [...refs].some((f) => f !== td.file);
|
|
5947
|
-
if (!usedExternally) {
|
|
5948
|
-
if (!unusedTypesByFile.has(td.file)) unusedTypesByFile.set(td.file, []);
|
|
5949
|
-
unusedTypesByFile.get(td.file).push(td);
|
|
5950
|
-
}
|
|
5951
|
-
}
|
|
5952
|
-
for (const [filePath, unusedTypes] of unusedTypesByFile) {
|
|
5953
|
-
if (unusedTypes.length < 3) continue;
|
|
5954
|
-
findings.push({
|
|
5955
|
-
detector: "phantom_scaffolding",
|
|
5956
|
-
subCategory: "unused_types",
|
|
5957
|
-
driftCategory: "phantom_scaffolding",
|
|
5958
|
-
severity: "info",
|
|
5959
|
-
confidence: 0.55,
|
|
5960
|
-
finding: `${unusedTypes.length} types/structs in ${filePath} appear unused outside their file`,
|
|
5961
|
-
dominantPattern: "used types",
|
|
5962
|
-
dominantCount: typeDefinitions.length - unusedTypes.length,
|
|
5963
|
-
totalRelevantFiles: typeDefinitions.length,
|
|
5964
|
-
consistencyScore: Math.round((typeDefinitions.length - unusedTypes.length) / typeDefinitions.length * 100),
|
|
5965
|
-
deviatingFiles: [{
|
|
5966
|
-
path: filePath,
|
|
5967
|
-
detectedPattern: `unused types: ${unusedTypes.map((t) => t.name).join(", ")}`,
|
|
5968
|
-
evidence: unusedTypes.slice(0, 5).map((t) => ({
|
|
5969
|
-
line: t.line,
|
|
5970
|
-
code: `type ${t.name} \u2014 defined but never imported`
|
|
5971
|
-
}))
|
|
5972
|
-
}],
|
|
5973
|
-
recommendation: `${unusedTypes.length} types in ${filePath.split("/").pop()} are never used outside their file. They may be AI-generated scaffolding that was never needed.`
|
|
5974
|
-
});
|
|
5975
|
-
}
|
|
6065
|
+
const findings = [];
|
|
6066
|
+
findings.push(...detectUnroutedHandlers(allExports, allRoutes, usage));
|
|
6067
|
+
findings.push(...detectUnusedTypeScaffolding(ctx.files, usage));
|
|
5976
6068
|
return findings;
|
|
5977
6069
|
}
|
|
5978
6070
|
};
|
|
@@ -8164,18 +8256,7 @@ async function createPortalSession(token, opts) {
|
|
|
8164
8256
|
}
|
|
8165
8257
|
|
|
8166
8258
|
// src/cli/commands/scan.ts
|
|
8167
|
-
async function
|
|
8168
|
-
const rootDir = resolve(targetPath);
|
|
8169
|
-
try {
|
|
8170
|
-
const info2 = await stat3(rootDir);
|
|
8171
|
-
if (!info2.isDirectory()) {
|
|
8172
|
-
console.error(`Error: ${rootDir} is not a directory`);
|
|
8173
|
-
process.exit(1);
|
|
8174
|
-
}
|
|
8175
|
-
} catch {
|
|
8176
|
-
console.error(`Error: ${rootDir} does not exist`);
|
|
8177
|
-
process.exit(1);
|
|
8178
|
-
}
|
|
8259
|
+
async function resolveAuthAndBanner(options) {
|
|
8179
8260
|
let bearerToken = null;
|
|
8180
8261
|
let apiUrl = options.apiUrl;
|
|
8181
8262
|
if (options.deep) {
|
|
@@ -8222,10 +8303,11 @@ async function runScan(targetPath, options) {
|
|
|
8222
8303
|
console.log("");
|
|
8223
8304
|
}
|
|
8224
8305
|
}
|
|
8225
|
-
|
|
8226
|
-
|
|
8306
|
+
return { bearerToken, apiUrl };
|
|
8307
|
+
}
|
|
8308
|
+
async function runAnalysisPipeline(rootDir, options, spinner) {
|
|
8227
8309
|
const isTerminal = options.format === "terminal" && !options.json;
|
|
8228
|
-
const
|
|
8310
|
+
const timings = {};
|
|
8229
8311
|
const t0 = Date.now();
|
|
8230
8312
|
const { ctx, warnings } = await buildAnalysisContext(rootDir);
|
|
8231
8313
|
const includes = options.include ?? [];
|
|
@@ -8288,30 +8370,33 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8288
8370
|
console.error(`[codedna] ${codeDnaResult.duplicateGroups.length} fingerprint duplicates, ${codeDnaResult.sequenceSimilarities.length} sequence matches, ${codeDnaResult.taintFlows.length} taint flows`);
|
|
8289
8371
|
}
|
|
8290
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 = {};
|
|
8291
8378
|
let mlMediumConfidence = [];
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
console.error(`[deep] ${mlResult.highConfidence.length} high-confidence findings shipped, ${mlResult.mediumConfidence.length} sent to LLM, ${mlResult.droppedCount} dropped`);
|
|
8308
|
-
}
|
|
8309
|
-
} catch (err) {
|
|
8310
|
-
console.error(chalk2.red(`[deep] AI analysis failed: ${err.message}`));
|
|
8311
|
-
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`);
|
|
8312
8394
|
}
|
|
8313
|
-
|
|
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."));
|
|
8314
8398
|
}
|
|
8399
|
+
timings.deep = Date.now() - t5;
|
|
8315
8400
|
const { deduplicateFindingsAcrossLayers: deduplicateFindingsAcrossLayers2 } = await Promise.resolve().then(() => (init_dedup(), dedup_exports));
|
|
8316
8401
|
const dedupedCount = allFindings.length;
|
|
8317
8402
|
const dedupedFindings = deduplicateFindingsAcrossLayers2(allFindings);
|
|
@@ -8320,6 +8405,12 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8320
8405
|
}
|
|
8321
8406
|
allFindings.length = 0;
|
|
8322
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;
|
|
8323
8414
|
const previousScores = await loadPreviousScores(rootDir);
|
|
8324
8415
|
if (spinner) spinner.text = "Computing scores...";
|
|
8325
8416
|
const { scores, compositeScore, maxCompositeScore, perFileScores } = computeScores(
|
|
@@ -8369,7 +8460,10 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8369
8460
|
if (options.verbose) console.error(`[summary] Failed: ${err.message}`);
|
|
8370
8461
|
}
|
|
8371
8462
|
}
|
|
8372
|
-
|
|
8463
|
+
return result;
|
|
8464
|
+
}
|
|
8465
|
+
async function logAndRender(result, options, bearerToken, apiUrl, rootDir, codeDnaResult) {
|
|
8466
|
+
const { findings: allFindings, compositeScore, maxCompositeScore, scanTimeMs } = result;
|
|
8373
8467
|
if (bearerToken) {
|
|
8374
8468
|
try {
|
|
8375
8469
|
const { logScan: logScan2 } = await Promise.resolve().then(() => (init_log_scan(), log_scan_exports));
|
|
@@ -8405,8 +8499,8 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8405
8499
|
payload: {
|
|
8406
8500
|
project_hash: projectIdentity.hash,
|
|
8407
8501
|
project_name: projectIdentity.name,
|
|
8408
|
-
language:
|
|
8409
|
-
file_count:
|
|
8502
|
+
language: result.context.dominantLanguage ?? "unknown",
|
|
8503
|
+
file_count: result.context.files.length,
|
|
8410
8504
|
function_count: codeDnaResult?.functions?.length ?? 0,
|
|
8411
8505
|
finding_count: allFindings.length,
|
|
8412
8506
|
score: compositeScore,
|
|
@@ -8479,6 +8573,29 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8479
8573
|
process.exit(1);
|
|
8480
8574
|
}
|
|
8481
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
|
+
}
|
|
8482
8599
|
|
|
8483
8600
|
// src/cli/commands/update.ts
|
|
8484
8601
|
init_esm_shims();
|